Merge remote-tracking branch 'aosp/upstream-master' into merge-upstream
Conflicts:
android/jps-plugin/src/org/jetbrains/jps/android/AndroidDexBuilder.java
android/src/org/jetbrains/android/logcat/AndroidToolWindowFactory.java
This includes the following CLs:
e0a5c04: uppercase prefix should middle-match lowercase string
6588401: support for resource directories: enable java-specific actions only for directories under source roots
44dc6a5: IDEA-100044 Parceable.CREATOR shouldn't be marker as unused
f255477: IDEA-89390 proguard vm options settings
9763c39: IDEA-113138 do not suggest class-based test run configuration in method context
a4baa06: do not open logcat automatically if it is test configuration
f5e0724: fix typo
02615b7: fix regression of IDEA-80976: restore access to logcat view
b602f9e: IDEA-85457 activate "DDMS" tool window automatically after application is successfully deployed and launched if we're not in debug mo
d52b94e: IDEA-112293 clear resolved apklibs info when importing is finished
6815307: first letter case sensitivity should match pattern start with name start even in middle matches
6234bda: spellchecker: suggest to renaming of value resource, ID, app package to update all usages
4439bc0: IDEA-112979 spellchecker inspection should be suppressed for symbols user cannot edit
2ed6ec6: IDEA-102180 Android XML: markup nested to string resources should not be reformatted
bd6ee47: IDEA-112932 place "width" and then "height" attributes right after "layout_*" attrs
c107496: do not add "generated_by_ide" mark 2 times
b0e3c52: IDEA-100046 BuildConfig.DEBUG shouldn't be marked as always true/false
Change-Id: I890945feb64f12547d3301807deb95120150283a
diff --git a/adt-branding/src/idea/AndroidStudioApplicationInfo.xml b/adt-branding/src/idea/AndroidStudioApplicationInfo.xml
index 47f9e4a..85c4e4a 100755
--- a/adt-branding/src/idea/AndroidStudioApplicationInfo.xml
+++ b/adt-branding/src/idea/AndroidStudioApplicationInfo.xml
@@ -14,7 +14,7 @@
~ limitations under the License.
-->
<component>
- <version codename="I/O Preview" major="0" minor="1.6" eap="true"/>
+ <version codename="I/O Preview" major="0" minor="2.7" eap="true"/>
<company name="Google Inc." url="http://developer.android.com"/>
<build number="__BUILD_NUMBER__" date="__BUILD_DATE__"/>
<install-over minbuild="0.1" maxbuild="999.999999" version="0"/>
diff --git a/android-designer/src/com/intellij/android/designer/designSurface/AndroidDesignerActionPanel.java b/android-designer/src/com/intellij/android/designer/designSurface/AndroidDesignerActionPanel.java
index 7f6aff8..db9dc18 100644
--- a/android-designer/src/com/intellij/android/designer/designSurface/AndroidDesignerActionPanel.java
+++ b/android-designer/src/com/intellij/android/designer/designSurface/AndroidDesignerActionPanel.java
@@ -15,6 +15,7 @@
*/
package com.intellij.android.designer.designSurface;
+import com.android.tools.idea.rendering.SaveScreenshotAction;
import com.intellij.android.designer.model.RadViewComponent;
import com.intellij.android.designer.model.RadViewLayout;
import com.intellij.designer.actions.DesignerActionPanel;
@@ -42,6 +43,10 @@
public class AndroidDesignerActionPanel extends DesignerActionPanel {
public AndroidDesignerActionPanel(DesignerEditorPanel designer, JComponent shortcuts) {
super(designer, shortcuts);
+
+ DefaultActionGroup popupGroup = getPopupGroup();
+ popupGroup.addSeparator();
+ popupGroup.add(new SaveScreenshotAction((AndroidDesignerEditorPanel)myDesigner));
}
@Override
diff --git a/android-designer/src/com/intellij/android/designer/designSurface/AndroidDesignerEditorPanel.java b/android-designer/src/com/intellij/android/designer/designSurface/AndroidDesignerEditorPanel.java
index c0947d1..d4959ce 100644
--- a/android-designer/src/com/intellij/android/designer/designSurface/AndroidDesignerEditorPanel.java
+++ b/android-designer/src/com/intellij/android/designer/designSurface/AndroidDesignerEditorPanel.java
@@ -24,8 +24,6 @@
import com.android.tools.idea.configurations.ConfigurationListener;
import com.android.tools.idea.configurations.ConfigurationToolBar;
import com.android.tools.idea.configurations.RenderContext;
-import com.android.tools.idea.gradle.IdeaAndroidProject;
-import com.android.tools.idea.gradle.variant.view.BuildVariantView;
import com.android.tools.idea.rendering.*;
import com.android.tools.idea.rendering.multi.RenderPreviewManager;
import com.android.tools.idea.rendering.multi.RenderPreviewMode;
@@ -33,7 +31,9 @@
import com.intellij.android.designer.componentTree.AndroidTreeDecorator;
import com.intellij.android.designer.inspection.ErrorAnalyzer;
import com.intellij.android.designer.model.*;
+import com.intellij.android.designer.model.layout.actions.ToggleRenderModeAction;
import com.intellij.designer.DesignerEditor;
+import com.intellij.designer.DesignerToolWindow;
import com.intellij.designer.DesignerToolWindowManager;
import com.intellij.designer.actions.DesignerActionPanel;
import com.intellij.designer.componentTree.TreeComponentDecorator;
@@ -88,6 +88,7 @@
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Lock;
@@ -95,14 +96,14 @@
import static com.android.tools.idea.configurations.ConfigurationListener.MASK_ALL;
import static com.android.tools.idea.configurations.ConfigurationListener.MASK_RENDERING;
-import static com.android.tools.idea.gradle.variant.view.BuildVariantView.BuildVariantSelectionChangeListener;
import static com.android.tools.idea.rendering.RenderErrorPanel.SIZE_ERROR_PANEL_DYNAMICALLY;
import static com.intellij.designer.designSurface.ZoomType.FIT_INTO;
+import static org.jetbrains.android.facet.ResourceFolderManager.ResourceFolderListener;
/**
* @author Alexander Lobas
*/
-public final class AndroidDesignerEditorPanel extends DesignerEditorPanel implements RenderContext, BuildVariantSelectionChangeListener {
+public final class AndroidDesignerEditorPanel extends DesignerEditorPanel implements RenderContext, ResourceFolderListener {
private static final int DEFAULT_HORIZONTAL_MARGIN = 30;
private static final int DEFAULT_VERTICAL_MARGIN = 20;
private static final Integer LAYER_ERRORS = LAYER_INPLACE_EDITING + 150; // Must be an Integer, not an int; see JLayeredPane.addImpl
@@ -144,39 +145,16 @@
AndroidFacet facet = AndroidFacet.getInstance(getModule());
assert facet != null;
myFacet = facet;
- // The configuration depends on project state, which may not yet be available: defer
- boolean initializeConfiguration = true;
if (facet.isGradleProject()) {
- IdeaAndroidProject gradleProject = facet.getIdeaAndroidProject();
- if (gradleProject == null) {
- initializeConfiguration = false;
- // Still syncing model; typically on IDE restart when the editor is reopened but
- // project model has not yet been fully initialized
- facet.addListener(new AndroidFacet.GradleProjectAvailableListener() {
- @Override
- public void gradleProjectAvailable(@NotNull IdeaAndroidProject project) {
- myFacet.removeListener(this);
- initializeConfiguration();
- requestRender();
- }
- });
- }
- BuildVariantView variantView = BuildVariantView.getInstance(facet.getModule().getProject());
- if (variantView != null) {
- // Ensure that the project resources have been initialized first, since
- // we want it to add its own variant listeners before ours (such that
- // when the variant changes, the project resources get notified and updated
- // before our own update listener attempts a re-render)
- facet.getProjectResources(false /*libraries*/, true /*createIfNecessary*/);
-
- variantView.removeListener(this);
- variantView.addListener(this);
- }
+ // Ensure that the project resources have been initialized first, since
+ // we want it to add its own variant listeners before ours (such that
+ // when the variant changes, the project resources get notified and updated
+ // before our own update listener attempts a re-render)
+ facet.getProjectResources(false /*libraries*/, true /*createIfNecessary*/);
+ myFacet.getResourceFolderManager().addListener(this);
}
myConfigListener = new LayoutConfigurationListener();
- if (initializeConfiguration) {
- initializeConfiguration();
- }
+ initializeConfiguration();
mySessionQueue = ViewsMetaManager.getInstance(project).getSessionQueue();
myXmlFile = (XmlFile)ApplicationManager.getApplication().runReadAction(new Computable<PsiFile>() {
@@ -453,9 +431,6 @@
return;
}
- // TODO: Get rid of this when the project resources are read directly from (possibly unsaved) PSI elements
- ApplicationManager.getApplication().saveAll();
-
mySessionAlarm.addRequest(new Runnable() {
@Override
public void run() {
@@ -483,31 +458,33 @@
@Override
public void run() {
try {
- final Module module = getModule();
- AndroidFacet facet = AndroidFacet.getInstance(module);
- if (facet == null) {
- throw new RenderingException("No facet available");
- }
-
- if (myConfiguration.getTarget() == null) {
- throw new RenderingException("No target selected");
- }
-
- if (myConfiguration.getTheme() == null) {
- throw new RenderingException("No theme");
- }
-
if (sessionId != mySessionId) {
cancel();
return;
}
+ final Module module = getModule();
final RenderLogger logger = new RenderLogger(myFile.getName(), module);
+
+ if (myConfiguration.getTarget() == null) {
+ logger.error(null, "No render target selected", null);
+ } else if (myConfiguration.getTheme() == null) {
+ logger.error(null, "No theme selected", null);
+ }
+
+ if (logger.hasProblems()) {
+ cancel();
+ RenderResult renderResult = new RenderResult(null, null, myXmlFile, logger);
+ runnable.consume(renderResult);
+ updateErrors(renderResult);
+ return;
+ }
+
final RenderResult renderResult;
RenderContext renderContext = AndroidDesignerEditorPanel.this;
if (myRendererLock.tryLock()) {
try {
- final RenderService service = RenderService.create(facet, module, myXmlFile, myConfiguration, logger, renderContext);
+ final RenderService service = RenderService.create(myFacet, module, myXmlFile, myConfiguration, logger, renderContext);
if (service != null) {
// Prefetch outside of read lock
service.getResourceResolver();
@@ -515,6 +492,9 @@
@Nullable
@Override
public RenderResult compute() {
+ if (!ToggleRenderModeAction.isRenderViewPort()) {
+ service.useDesignMode(myXmlFile.getRootTag());
+ }
return service.render();
}
});
@@ -636,7 +616,10 @@
myHorizontalCaption.update();
myVerticalCaption.update();
- DesignerToolWindowManager.getInstance(AndroidDesignerEditorPanel.this).refresh(updateProperties);
+ DesignerToolWindow toolWindow = getToolWindow();
+ if (toolWindow != null) {
+ toolWindow.refresh(updateProperties);
+ }
if (RenderPreviewMode.getCurrent() != RenderPreviewMode.NONE) {
RenderPreviewManager previewManager = getPreviewManager(true);
@@ -648,6 +631,17 @@
});
}
+ @Nullable
+ private DesignerToolWindow getToolWindow() {
+ try {
+ // This method sometimes returns null. We don't want to bother the user with that; the worst that
+ // can happen is that the property view is not updated.
+ return DesignerToolWindowManager.getInstance(this);
+ } catch (Exception ex) {
+ return null;
+ }
+ }
+
/**
* Auto fits the scene, if requested. This will be the case the first time
* the layout is opened, and after orientation or device changes.
@@ -786,6 +780,8 @@
myPreviewManager.dispose();
myPreviewManager = null;
}
+
+ myFacet.getResourceFolderManager().removeListener(this);
}
@Override
@@ -942,14 +938,26 @@
}
@Override
- protected boolean execute(ThrowableRunnable<Exception> operation, boolean updateProperties) {
+ protected boolean execute(ThrowableRunnable<Exception> operation, final boolean updateProperties) {
if (!ReadonlyStatusHandler.ensureFilesWritable(getProject(), myFile)) {
return false;
}
try {
myPsiChangeListener.stop();
operation.run();
- updateRenderer(updateProperties);
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ boolean active = myPsiChangeListener.isActive();
+ if (active) {
+ myPsiChangeListener.stop();
+ }
+ updateRenderer(updateProperties);
+ if (active) {
+ myPsiChangeListener.start();
+ }
+ }
+ });
return true;
}
catch (Throwable e) {
@@ -1113,6 +1121,9 @@
break;
case FIT_INTO:
case FIT: {
+ if (myRootComponent == null) {
+ return;
+ }
Dimension sceneSize = myRootComponent.getBounds().getSize();
Dimension screenSize = getDesignerViewSize();
if (screenSize.width > 0 && screenSize.height > 0) {
@@ -1385,6 +1396,11 @@
// TODO
}
+ @Nullable
+ @Override
+ public BufferedImage getRenderedImage() {
+ return myRootView != null ? myRootView.getImage() : null;
+ }
@Override
@NotNull
@@ -1449,11 +1465,13 @@
return myPreviewManager;
}
-
- // ---- Implements BuildVariantSelectionChangeListener ----
+ // ---- Implements ResourceFolderManager.ResourceFolderListener ----
@Override
- public void buildVariantSelected(@NotNull AndroidFacet facet) {
+ public void resourceFoldersChanged(@NotNull AndroidFacet facet,
+ @NotNull List<VirtualFile> folders,
+ @NotNull Collection<VirtualFile> added,
+ @NotNull Collection<VirtualFile> removed) {
if (facet == myFacet) {
if (myActive) {
// The project resources should already have been refreshed by their own variant listener
diff --git a/android-designer/src/com/intellij/android/designer/model/RadScrollViewLayout.java b/android-designer/src/com/intellij/android/designer/model/RadScrollViewLayout.java
new file mode 100644
index 0000000..a41b8c5
--- /dev/null
+++ b/android-designer/src/com/intellij/android/designer/model/RadScrollViewLayout.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.android.designer.model;
+
+import com.intellij.android.designer.model.layout.actions.ToggleRenderModeAction;
+import com.intellij.designer.designSurface.DesignerEditorPanel;
+import com.intellij.openapi.actionSystem.DefaultActionGroup;
+import com.intellij.openapi.actionSystem.Separator;
+
+import java.util.List;
+
+public class RadScrollViewLayout extends RadSingleChildrenViewLayout {
+ @Override
+ public void addContainerSelectionActions(DesignerEditorPanel designer,
+ DefaultActionGroup actionGroup,
+ List<? extends RadViewComponent> selection) {
+
+ // Add render mode action
+ if (myContainer != null && (myContainer.isBackground() || myContainer.getParent() != null && myContainer.getParent().isBackground())) {
+ actionGroup.add(new ToggleRenderModeAction(designer));
+ actionGroup.add(new Separator());
+ }
+
+ super.addContainerSelectionActions(designer, actionGroup, selection);
+ }
+}
\ No newline at end of file
diff --git a/android-designer/src/com/intellij/android/designer/model/RadViewComponent.java b/android-designer/src/com/intellij/android/designer/model/RadViewComponent.java
index 7f97082..7027110 100644
--- a/android-designer/src/com/intellij/android/designer/model/RadViewComponent.java
+++ b/android-designer/src/com/intellij/android/designer/model/RadViewComponent.java
@@ -18,7 +18,6 @@
import com.android.SdkConstants;
import com.android.ide.common.rendering.api.ViewInfo;
import com.intellij.android.designer.designSurface.AndroidDesignerEditorPanel;
-import com.intellij.designer.designSurface.DesignerEditorPanel;
import com.intellij.designer.designSurface.ScalableComponent;
import com.intellij.designer.model.*;
import com.intellij.designer.palette.PaletteItem;
@@ -211,9 +210,14 @@
@Override
public void delete() throws Exception {
- IdManager.get(this).removeComponent(this, true);
+ IdManager idManager = IdManager.get(this);
+ if (idManager != null) {
+ idManager.removeComponent(this, true);
+ }
- removeFromParent();
+ if (getParent() != null) {
+ removeFromParent();
+ }
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
diff --git a/android-designer/src/com/intellij/android/designer/model/layout/actions/AllGravityAction.java b/android-designer/src/com/intellij/android/designer/model/layout/actions/AllGravityAction.java
index f184cd8..3540711 100644
--- a/android-designer/src/com/intellij/android/designer/model/layout/actions/AllGravityAction.java
+++ b/android-designer/src/com/intellij/android/designer/model/layout/actions/AllGravityAction.java
@@ -47,12 +47,15 @@
@NotNull
@Override
protected DefaultActionGroup createPopupActionGroup(JComponent button) {
- Iterator<? extends RadViewComponent> I = myComponents.iterator();
- int flags = Gravity.getFlags(I.next());
- while (I.hasNext()) {
- if (flags != Gravity.getFlags(I.next())) {
- flags = 0;
- break;
+ Iterator<? extends RadViewComponent> iterator = myComponents.iterator();
+ int flags = 0;
+ if (iterator.hasNext()) {
+ flags = Gravity.getFlags(iterator.next());
+ while (iterator.hasNext()) {
+ if (flags != Gravity.getFlags(iterator.next())) {
+ flags = 0;
+ break;
+ }
}
}
mySelection = Gravity.flagToValues(flags);
diff --git a/android-designer/src/com/intellij/android/designer/model/layout/actions/ToggleRenderModeAction.java b/android-designer/src/com/intellij/android/designer/model/layout/actions/ToggleRenderModeAction.java
new file mode 100644
index 0000000..cddcee7
--- /dev/null
+++ b/android-designer/src/com/intellij/android/designer/model/layout/actions/ToggleRenderModeAction.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.android.designer.model.layout.actions;
+
+import com.intellij.android.designer.designSurface.AndroidDesignerEditorPanel;
+import com.intellij.android.designer.model.RadViewComponent;
+import com.intellij.designer.designSurface.DesignerEditorPanel;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.actionSystem.ToggleAction;
+import icons.AndroidDesignerIcons;
+import org.jetbrains.annotations.NotNull;
+
+public class ToggleRenderModeAction extends ToggleAction {
+ private final DesignerEditorPanel myDesigner;
+ /** Whether we should render just the viewport */
+ private static boolean ourRenderViewPort;
+
+ public ToggleRenderModeAction(@NotNull DesignerEditorPanel designer) {
+ myDesigner = designer;
+
+ Presentation presentation = getTemplatePresentation();
+ String label = "Toggle Viewport Render Mode";
+ presentation.setDescription(label);
+ presentation.setText(label);
+ updateIcon(presentation);
+ }
+
+ @Override
+ public void update(AnActionEvent e) {
+ super.update(e);
+ updateIcon(e.getPresentation());
+ }
+
+ private static void updateIcon(Presentation presentation) {
+ presentation.setIcon(ourRenderViewPort ? AndroidDesignerIcons.NormalRender : AndroidDesignerIcons.ViewportRender);
+ }
+
+ @Override
+ public boolean isSelected(AnActionEvent e) {
+ return ourRenderViewPort;
+ }
+
+ @Override
+ public void setSelected(AnActionEvent e, boolean state) {
+ ourRenderViewPort = state;
+ updateIcon(e.getPresentation());
+ ((AndroidDesignerEditorPanel)myDesigner).requestRender();
+ }
+
+ public static boolean isRenderViewPort() {
+ return ourRenderViewPort;
+ }
+}
diff --git a/android-designer/src/com/intellij/android/designer/model/views-meta-model.xml b/android-designer/src/com/intellij/android/designer/model/views-meta-model.xml
index 2015520..24cdba0 100644
--- a/android-designer/src/com/intellij/android/designer/model/views-meta-model.xml
+++ b/android-designer/src/com/intellij/android/designer/model/views-meta-model.xml
@@ -9,21 +9,21 @@
tag="<root>"
delete="false">
- <palette title="Device Screen" icon="AndroidDesignerIcons.DeviceScreen"/>
+ <palette title="Device Screen" icon="AndroidIcons.Views.DeviceScreen"/>
</meta>
<meta model="com.intellij.android.designer.model.RadViewComponent"
tag="merge"
delete="false">
- <palette title="Device Screen (#merge)" icon="AndroidDesignerIcons.DeviceScreen"/>
+ <palette title="Device Screen (#merge)" icon="AndroidIcons.Views.DeviceScreen"/>
</meta>
<meta model="com.intellij.android.designer.model.RadRequestFocus"
class="java.lang.Object"
tag="requestFocus">
- <palette title="requestFocus" icon="AndroidDesignerIcons.RequestFocus"
+ <palette title="requestFocus" icon="AndroidIcons.Views.RequestFocus"
tooltip="Requests focus for the parent element or one of its descendants."/>
<creation>
@@ -40,7 +40,7 @@
<presentation title=" - %layout:xml%"/>
- <palette title="<include>" icon="AndroidDesignerIcons.Include"
+ <palette title="<include>" icon="AndroidIcons.Views.Include"
tooltip="Lets you statically include XML layout inside other XML layouts."/>
<properties inplace="layout:xml"/>
@@ -52,7 +52,7 @@
<presentation title=" - %name%"/>
- <palette title="<fragment>" icon="AndroidDesignerIcons.Fragment"
+ <palette title="<fragment>" icon="AndroidIcons.Views.Fragment"
tooltip="A Fragment is a piece of an application's user interface or behavior that can be placed in an Activity."/>
<properties inplace="name"/>
@@ -61,7 +61,7 @@
<meta model="com.intellij.android.designer.model.RadCustomViewComponent"
tag="view">
- <palette title="CustomView" icon="AndroidDesignerIcons.Unknown"/>
+ <palette title="CustomView" icon="AndroidIcons.Views.Unknown"/>
</meta>
<meta class="android.view.View">
@@ -85,7 +85,7 @@
<presentation title=" - "%text%""/>
- <palette title="TextView" icon="AndroidDesignerIcons.TextView"
+ <palette title="TextView" icon="AndroidIcons.Views.TextView"
tooltip="Displays text to the user and optionally allows them to edit it."/>
<morphing to="EditText ImageView CheckedTextView"/>
@@ -113,7 +113,7 @@
<presentation title=" - "%text%""/>
- <palette title="Button" icon="AndroidDesignerIcons.Button"
+ <palette title="Button" icon="AndroidIcons.Views.Button"
tooltip="Represents a push-button widget."/>
<morphing to="RadioButton CheckBox ToggleButton ZoomButton ImageButton TextView EditText"/>
@@ -146,7 +146,7 @@
<presentation title=" - "%text%""/>
- <palette title="Switch" icon="AndroidDesignerIcons.Switch"
+ <palette title="Switch" icon="AndroidIcons.Views.Switch"
version="14"
tooltip="A Switch is a two-state toggle switch widget that can select between two options."/>
@@ -167,7 +167,7 @@
<presentation title=" - "%text%""/>
- <palette title="RadioButton" icon="AndroidDesignerIcons.RadioButton"
+ <palette title="RadioButton" icon="AndroidIcons.Views.RadioButton"
tooltip="A radio button is a two-states button that can be either checked or unchecked."/>
<morphing to="Button CheckBox ToggleButton ZoomButton ImageButton TextView EditText"/>
@@ -191,7 +191,7 @@
<morphing to="Button RadioButton ToggleButton ZoomButton ImageButton TextView EditText"/>
- <palette title="CheckBox" icon="AndroidDesignerIcons.CheckBox"
+ <palette title="CheckBox" icon="AndroidIcons.Views.CheckBox"
tooltip="A checkbox is a specific type of two-states button that can be either checked or unchecked."/>
<creation>
@@ -211,7 +211,7 @@
<presentation title=" - "%text%""/>
- <palette title="ToggleButton" icon="AndroidDesignerIcons.ToggleButton"
+ <palette title="ToggleButton" icon="AndroidIcons.Views.ToggleButton"
tooltip="Displays checked/unchecked states as a button with a 'light' indicator and by default accompanied with the text 'ON' or 'OFF'. "/>
<morphing to="Button RadioButton CheckBox ZoomButton ImageButton TextView EditText"/>
@@ -235,7 +235,7 @@
<presentation title=" - "%text%""/>
- <palette title="CheckedTextView" icon="AndroidDesignerIcons.CheckedTextView"
+ <palette title="CheckedTextView" icon="AndroidIcons.Views.CheckedTextView"
tooltip="An extension to TextView that supports the Checkable interface."/>
<morphing to="TextView EditText ImageView"/>
@@ -259,7 +259,7 @@
<presentation title=" - "%text%""/>
- <palette title="EditText" icon="AndroidDesignerIcons.EditText"
+ <palette title="EditText" icon="AndroidIcons.Views.EditText"
tooltip="EditText is a thin veneer over TextView that configures itself to be editable."/>
<morphing to="TextView CheckedTextView ImageView SearchView"/>
@@ -282,7 +282,7 @@
<presentation title=" - "%text%""/>
- <palette title="AutoCompleteTextView" icon="AndroidDesignerIcons.AutoCompleteTextView"
+ <palette title="AutoCompleteTextView" icon="AndroidIcons.Views.AutoCompleteTextView"
tooltip="An editable text view that shows completion suggestions automatically while the user is typing."/>
<morphing to="TextView CheckedTextView EditText MultiAutoCompleteTextView ImageView SearchView"/>
@@ -306,7 +306,7 @@
<presentation title=" - "%text%""/>
- <palette title="MultiAutoCompleteTextView" icon="AndroidDesignerIcons.MultiAutoCompleteTextView"
+ <palette title="MultiAutoCompleteTextView" icon="AndroidIcons.Views.MultiAutoCompleteTextView"
tooltip="An editable text view, extending AutoCompleteTextView, that can show completion suggestions for the substring of the text where the user is typing instead of necessarily for the entire thing."/>
<morphing to="TextView CheckedTextView EditText AutoCompleteTextView ImageView SearchView"/>
@@ -328,7 +328,7 @@
<presentation title=" - "%text%""/>
- <palette title="ExtractEditText" icon="AndroidDesignerIcons.TextView"
+ <palette title="ExtractEditText" icon="AndroidIcons.Views.TextView"
tooltip="Specialization of EditText for showing and interacting with the extracted text in a full-screen input method."/>
<creation>
@@ -346,7 +346,7 @@
class="android.widget.AnalogClock"
tag="AnalogClock">
- <palette title="AnalogClock" icon="AndroidDesignerIcons.AnalogClock"
+ <palette title="AnalogClock" icon="AndroidIcons.Views.AnalogClock"
tooltip="This widget displays an analog clock with two hands for hours and minutes."/>
<morphing to="DigitalClock TextClock"/>
@@ -367,7 +367,7 @@
class="android.widget.TextClock"
tag="TextClock">
- <palette title="TextClock" icon="AndroidDesignerIcons.TextClock"
+ <palette title="TextClock" icon="AndroidIcons.Views.TextClock"
version="17"
tooltip="This widget displays the current date and/or time as a formatted string"/>
@@ -389,7 +389,7 @@
class="android.widget.DigitalClock"
tag="DigitalClock">
- <palette title="DigitalClock" icon="AndroidDesignerIcons.DigitalClock"
+ <palette title="DigitalClock" icon="AndroidIcons.Views.DigitalClock"
tooltip="Like AnalogClock, but digital. Shows seconds."
deprecated="17"
deprecatedHint="Use a TextClock instead"/>
@@ -412,7 +412,7 @@
<presentation title=" - "%format%""/>
- <palette title="Chronometer" icon="AndroidDesignerIcons.Chronometer"
+ <palette title="Chronometer" icon="AndroidIcons.Views.Chronometer"
tooltip="Class that implements a simple timer."/>
<morphing to="TextClock"/>
@@ -434,7 +434,7 @@
class="android.view.SurfaceView"
tag="SurfaceView">
- <palette title="SurfaceView" icon="AndroidDesignerIcons.SurfaceView"
+ <palette title="SurfaceView" icon="AndroidIcons.Views.SurfaceView"
tooltip="Provides a dedicated drawing surface embedded inside of a view hierarchy. You can control the format of this surface and, if you like, its size; the SurfaceView takes care of placing the surface at the correct location on the screen."/>
<creation>
@@ -451,7 +451,7 @@
class="android.view.TextureView"
tag="TextureView">
- <palette title="TextureView" icon="AndroidDesignerIcons.TextureView"
+ <palette title="TextureView" icon="AndroidIcons.Views.TextureView"
tooltip="A TextureView can be used to display a content stream. Such a content stream can for instance be a video or an OpenGL scene."/>
<creation>
@@ -468,7 +468,7 @@
class="android.widget.VideoView"
tag="VideoView">
- <palette title="VideoView" icon="AndroidDesignerIcons.VideoView"
+ <palette title="VideoView" icon="AndroidIcons.Views.VideoView"
tooltip="Displays a video file. The VideoView class can load images from various sources (such as resources or content providers), takes care of computing its measurement from the video so that it can be used in any layout manager, and provides various display options such as scaling and tinting."/>
<creation>
@@ -485,7 +485,7 @@
class="android.widget.ProgressBar"
tag="ProgressBar">
- <palette title="ProgressBar" icon="AndroidDesignerIcons.ProgressBar"
+ <palette title="ProgressBar" icon="AndroidIcons.Views.ProgressBar"
tooltip="Visual indicator of progress in some operation."/>
<morphing to="SeekBar RatingBar"/>
@@ -507,7 +507,7 @@
class="android.widget.SeekBar"
tag="SeekBar">
- <palette title="SeekBar" icon="AndroidDesignerIcons.SeekBar"
+ <palette title="SeekBar" icon="AndroidIcons.Views.SeekBar"
tooltip="A SeekBar is an extension of ProgressBar that adds a draggable thumb."/>
<morphing to="ProgressBar RatingBar"/>
@@ -528,7 +528,7 @@
class="android.widget.RatingBar"
tag="RatingBar">
- <palette title="RatingBar" icon="AndroidDesignerIcons.RatingBar"
+ <palette title="RatingBar" icon="AndroidIcons.Views.RatingBar"
tooltip="A RatingBar is an extension of SeekBar and ProgressBar that shows a rating in stars."/>
<morphing to="ProgressBar SeekBar"/>
@@ -553,7 +553,7 @@
<presentation title=" - "%src%""/>
- <palette title="ImageView" icon="AndroidDesignerIcons.ImageView"
+ <palette title="ImageView" icon="AndroidIcons.Views.ImageView"
tooltip="Displays an arbitrary image, such as an icon."/>
<morphing to="TextView EditText CheckedTextView"/>
@@ -577,7 +577,7 @@
<presentation title=" - "%src%""/>
- <palette title="ImageButton" icon="AndroidDesignerIcons.ImageButton"
+ <palette title="ImageButton" icon="AndroidIcons.Views.ImageButton"
tooltip="Displays a button with an image (instead of text) that can be pressed or clicked by the user."/>
<morphing to="Button RadioButton CheckBox ToggleButton ZoomButton TextView EditText"/>
@@ -596,7 +596,7 @@
class="android.widget.ZoomButton"
tag="ZoomButton">
- <palette title="ZoomButton" icon="AndroidDesignerIcons.ZoomButton"/>
+ <palette title="ZoomButton" icon="AndroidIcons.Views.ZoomButton"/>
<morphing to="Button RadioButton CheckBox ToggleButton ImageButton TextView EditText"/>
@@ -614,7 +614,7 @@
class="android.widget.QuickContactBadge"
tag="QuickContactBadge">
- <palette title="QuickContactBadge" icon="AndroidDesignerIcons.QuickContactBadge"
+ <palette title="QuickContactBadge" icon="AndroidIcons.Views.QuickContactBadge"
version="5"
tooltip="Widget used to show an image with the standard QuickContact badge and on-click behavior."/>
@@ -634,7 +634,7 @@
class="android.widget.Space"
tag="Space">
- <palette title="Space" icon="AndroidDesignerIcons.Space"
+ <palette title="Space" icon="AndroidIcons.Views.Space"
version="14"
tooltip="Space is a lightweight View subclass that may be used to create gaps between components in general purpose layouts."/>
@@ -652,7 +652,7 @@
class="android.inputmethodservice.KeyboardView"
tag="KeyboardView">
- <palette title="KeyboardView" icon="AndroidDesignerIcons.TextView"
+ <palette title="KeyboardView" icon="AndroidIcons.Views.TextView"
tooltip="A view that renders a virtual Keyboard. It handles rendering of keys and detecting key presses and touch movements."/>
<creation>
@@ -669,7 +669,7 @@
class="android.gesture.GestureOverlayView"
tag="GestureOverlayView">
- <palette title="GestureOverlayView" icon="AndroidDesignerIcons.GestureOverlayView"
+ <palette title="GestureOverlayView" icon="AndroidIcons.Views.GestureOverlayView"
tooltip="A transparent overlay for gesture input that can be placed on top of other widgets or contain other widgets."/>
<properties inplace="gestureColor orientation"
@@ -690,7 +690,7 @@
class="android.view.ViewStub"
tag="ViewStub">
- <palette title="ViewStub" icon="AndroidDesignerIcons.ViewStub"
+ <palette title="ViewStub" icon="AndroidIcons.Views.ViewStub"
tooltip="A ViewStub is an invisible, zero-sized View that can be used to lazily inflate layout resources at runtime."/>
<properties inplace="layout inflatedId"
@@ -712,7 +712,7 @@
class="android.widget.DatePicker"
tag="DatePicker">
- <palette title="DatePicker" icon="AndroidDesignerIcons.DatePicker"
+ <palette title="DatePicker" icon="AndroidIcons.Views.DatePicker"
tooltip="This class is a widget for selecting a date."/>
<morphing to="TimePicker CalendarView"/>
@@ -733,7 +733,7 @@
class="android.widget.CalendarView"
tag="CalendarView">
- <palette title="CalendarView" icon="AndroidDesignerIcons.CalendarView"
+ <palette title="CalendarView" icon="AndroidIcons.Views.CalendarView"
version="11"
tooltip="This class is a calendar widget for displaying and selecting dates."/>
@@ -756,7 +756,7 @@
class="android.widget.TimePicker"
tag="TimePicker">
- <palette title="TimePicker" icon="AndroidDesignerIcons.TimePicker"
+ <palette title="TimePicker" icon="AndroidIcons.Views.TimePicker"
tooltip="A view for selecting the time of day, in either 24 hour or AM/PM mode."/>
<morphing to="DatePicker CalendarView"/>
@@ -778,7 +778,7 @@
class="android.widget.NumberPicker"
tag="NumberPicker">
- <palette title="NumberPicker" icon="AndroidDesignerIcons.NumberPicker"
+ <palette title="NumberPicker" icon="AndroidIcons.Views.NumberPicker"
version="11"
tooltip="A widget that enables the user to select a number form a predefined range. The widget presents an input field and up and down buttons for selecting the current value. Pressing/long-pressing the up and down buttons increments and decrements the current value respectively."/>
@@ -798,7 +798,7 @@
class="android.widget.ZoomControls"
tag="ZoomControls">
- <palette title="ZoomControls" icon="AndroidDesignerIcons.ZoomControls"
+ <palette title="ZoomControls" icon="AndroidIcons.Views.ZoomControls"
tooltip="The ZoomControls class displays a simple set of controls used for zooming and provides callbacks to register for events."/>
<creation>
@@ -815,7 +815,7 @@
class="android.widget.SearchView"
tag="SearchView">
- <palette title="SearchView" icon="AndroidDesignerIcons.SearchView"
+ <palette title="SearchView" icon="AndroidIcons.Views.SearchView"
version="11"
tooltip="A widget that provides a user interface for the user to enter a search query and submit a request to a search provider. Shows a list of query suggestions or results, if available, and allows the user to pick a suggestion or result to launch into."/>
@@ -841,7 +841,7 @@
class="android.widget.ListView"
tag="ListView">
- <palette title="ListView" icon="AndroidDesignerIcons.ListView"
+ <palette title="ListView" icon="AndroidIcons.Views.ListView"
tooltip="A view that shows items in a vertically scrolling list. The items come from the ListAdapter associated with this view."/>
<morphing to="ExpandableListView GridView"/>
@@ -862,7 +862,7 @@
class="android.widget.ExpandableListView"
tag="ExpandableListView">
- <palette title="ExpandableListView" icon="AndroidDesignerIcons.ExpandableListView"
+ <palette title="ExpandableListView" icon="AndroidIcons.Views.ExpandableListView"
tooltip="A view that shows items in a vertically scrolling two-level list. This differs from the ListView by allowing two levels: groups which can individually be expanded to show its children."/>
<morphing to="ListView GridView"/>
@@ -883,7 +883,7 @@
class="android.widget.GridView"
tag="GridView">
- <palette title="GridView" icon="AndroidDesignerIcons.GridView"
+ <palette title="GridView" icon="AndroidIcons.Views.GridView"
tooltip="A view that shows items in two-dimensional scrolling grid. The items in the grid come from the ListAdapter associated with this view."/>
<morphing to="ListView ExpandableListView"/>
@@ -905,7 +905,7 @@
class="android.webkit.WebView"
tag="WebView">
- <palette title="WebView" icon="AndroidDesignerIcons.WebView"
+ <palette title="WebView" icon="AndroidIcons.Views.WebView"
tooltip="A View that displays web pages. This class is the basis upon which you can roll your own web browser or simply display some online content within your Activity."/>
<creation>
@@ -919,11 +919,11 @@
</meta>
<meta model="com.intellij.android.designer.model.RadViewComponent"
- layout="com.intellij.android.designer.model.RadSingleChildrenViewLayout"
+ layout="com.intellij.android.designer.model.RadScrollViewLayout"
class="android.widget.ScrollView"
tag="ScrollView">
- <palette title="ScrollView" icon="AndroidDesignerIcons.ScrollView"
+ <palette title="ScrollView" icon="AndroidIcons.Views.ScrollView"
tooltip="Layout container for a view hierarchy that can be scrolled by the user, allowing it to be larger than the physical display. ScrollView only supports vertical scrolling."/>
<morphing to="HorizontalScrollView"/>
@@ -942,11 +942,11 @@
</meta>
<meta model="com.intellij.android.designer.model.RadViewComponent"
- layout="com.intellij.android.designer.model.RadSingleChildrenViewLayout"
+ layout="com.intellij.android.designer.model.RadScrollViewLayout"
class="android.widget.HorizontalScrollView"
tag="HorizontalScrollView">
- <palette title="HorizontalScrollView" icon="AndroidDesignerIcons.HorizontalScrollView"
+ <palette title="HorizontalScrollView" icon="AndroidIcons.Views.HorizontalScrollView"
version="3"
tooltip="Layout container for a view hierarchy that can be scrolled by the user, allowing it to be larger than the physical display."/>
@@ -969,7 +969,7 @@
class="android.widget.Gallery"
tag="Gallery">
- <palette title="Gallery" icon="AndroidDesignerIcons.Gallery"
+ <palette title="Gallery" icon="AndroidIcons.Views.Gallery"
tooltip="A view that shows items in a center-locked, horizontally scrolling list."
deprecated="16"
deprecatedHint="This widget is no longer supported. Other horizontally scrolling widgets include HorizontalScrollView and ViewPager from the support library."/>
@@ -993,7 +993,7 @@
class="android.widget.Spinner"
tag="Spinner">
- <palette title="Spinner" icon="AndroidDesignerIcons.Spinner"
+ <palette title="Spinner" icon="AndroidIcons.Views.Spinner"
tooltip="A view that displays one child at a time and lets the user pick among them."/>
<properties inplace="spinnerMode"
@@ -1013,7 +1013,7 @@
class="android.widget.MediaController"
tag="MediaController">
- <palette title="MediaController" icon="AndroidDesignerIcons.MediaController"
+ <palette title="MediaController" icon="AndroidIcons.Views.MediaController"
tooltip="A view containing controls for a MediaPlayer. Typically contains the buttons like 'Play/Pause', 'Rewind', 'Fast Forward' and a progress slider. It takes care of synchronizing the controls with the state of the MediaPlayer."/>
<creation>
@@ -1030,7 +1030,7 @@
class="android.widget.StackView"
tag="StackView">
- <palette title="StackView" icon="AndroidDesignerIcons.StackView"
+ <palette title="StackView" icon="AndroidIcons.Views.StackView"
version="11"
tooltip="A view that displays its children in a stack and allows users to discretely swipe through the children."/>
@@ -1050,7 +1050,7 @@
class="android.widget.AdapterViewFlipper"
tag="AdapterViewFlipper">
- <palette title="AdapterViewFlipper" icon="AndroidDesignerIcons.AdapterViewFlipper"
+ <palette title="AdapterViewFlipper" icon="AndroidIcons.Views.AdapterViewFlipper"
version="11"
tooltip="Simple ViewAnimator that will animate between two or more views that have been added to it. Only one child is shown at a time. If requested, can automatically flip between each child at a regular interval."/>
@@ -1071,7 +1071,7 @@
class="android.widget.ViewAnimator"
tag="ViewAnimator">
- <palette title="ViewAnimator" icon="AndroidDesignerIcons.ViewAnimator"
+ <palette title="ViewAnimator" icon="AndroidIcons.Views.ViewAnimator"
tooltip="Container that will perform animations when switching between its views."/>
<properties important="inAnimation outAnimation animateFirstView"/>
@@ -1091,7 +1091,7 @@
class="android.widget.ViewFlipper"
tag="ViewFlipper">
- <palette title="ViewFlipper" icon="AndroidDesignerIcons.ViewFlipper"
+ <palette title="ViewFlipper" icon="AndroidIcons.Views.ViewFlipper"
tooltip="Simple ViewAnimator that will animate between two or more views that have been added to it. Only one child is shown at a time. If requested, can automatically flip between each child at a regular interval."/>
<properties important="flipInterval autoStart"/>
@@ -1111,7 +1111,7 @@
class="android.widget.ViewSwitcher"
tag="ViewSwitcher">
- <palette title="ViewSwitcher" icon="AndroidDesignerIcons.ViewSwitcher"
+ <palette title="ViewSwitcher" icon="AndroidIcons.Views.ViewSwitcher"
tooltip="ViewAnimator that switches between two views, and has a factory from which these views are created. You can either use the factory to create the views, or add them yourself. A ViewSwitcher can only have two child views, of which only one is shown at a time."/>
<creation>
@@ -1129,7 +1129,7 @@
class="android.widget.ImageSwitcher"
tag="ImageSwitcher">
- <palette title="ImageSwitcher" icon="AndroidDesignerIcons.ImageSwitcher"/>
+ <palette title="ImageSwitcher" icon="AndroidIcons.Views.ImageSwitcher"/>
<creation>
<![CDATA[
@@ -1146,7 +1146,7 @@
class="android.widget.TextSwitcher"
tag="TextSwitcher">
- <palette title="TextSwitcher" icon="AndroidDesignerIcons.TextSwitcher"
+ <palette title="TextSwitcher" icon="AndroidIcons.Views.TextSwitcher"
tooltip="Specialized ViewSwitcher that contains only children of type TextView. A TextSwitcher is useful to animate a label on screen."/>
<creation>
@@ -1166,7 +1166,7 @@
<presentation title=" (%orientation%)"/>
- <palette title="RadioGroup" icon="AndroidDesignerIcons.RadioGroup"
+ <palette title="RadioGroup" icon="AndroidIcons.Views.RadioGroup"
tooltip="This class is used to create a multiple-exclusion scope for a set of radio buttons. Checking one radio button that belongs to a radio group unchecks any previously checked radio button within the same group."/>
<morphing to="LinearLayout TableLayout GridLayout RelativeLayout"/>
@@ -1193,7 +1193,7 @@
<properties inplace="mode"
important="mode"/>
- <palette title="TwoLineListItem" icon="AndroidDesignerIcons.TwoLineListItem"
+ <palette title="TwoLineListItem" icon="AndroidIcons.Views.TwoLineListItem"
tooltip="A view group with two children, intended for use in ListViews."
deprecated="17"
deprecatedHint="This class can be implemented easily by apps using a RelativeLayout or a LinearLayout."/>
@@ -1214,7 +1214,7 @@
class="android.widget.DialerFilter"
tag="DialerFilter">
- <palette title="DialerFilter" icon="AndroidDesignerIcons.DialerFilter"/>
+ <palette title="DialerFilter" icon="AndroidIcons.Views.DialerFilter"/>
<creation>
<![CDATA[
@@ -1244,7 +1244,7 @@
class="android.widget.TabHost"
tag="TabHost">
- <palette title="TabHost" icon="AndroidDesignerIcons.TabHost"
+ <palette title="TabHost" icon="AndroidIcons.Views.TabHost"
tooltip="Container for a tabbed window view. This object holds two children: a set of tab labels that the user clicks to select a specific tab, and a FrameLayout object that displays the contents of that page. The individual elements are typically controlled using this container object, rather than setting values on the child elements themselves."/>
<creation>
@@ -1291,7 +1291,7 @@
class="android.widget.TabWidget"
tag="TabWidget">
- <palette title="TabWidget" icon="AndroidDesignerIcons.TabWidget"/>
+ <palette title="TabWidget" icon="AndroidIcons.Views.TabWidget"/>
<properties important="divider tabStripEnabled tabStripLeft tabStripRight tabLayout"/>
</meta>
@@ -1304,7 +1304,7 @@
important="orientation"
expert="handle content"/>
- <palette title="SlidingDrawer" icon="AndroidDesignerIcons.SlidingDrawer"
+ <palette title="SlidingDrawer" icon="AndroidIcons.Views.SlidingDrawer"
version="3"
tooltip="SlidingDrawer hides content out of the screen and allows the user to drag a handle to bring the content on screen. SlidingDrawer can be used vertically or horizontally. A special widget composed of two children views: the handle, that the users drags, and the content, attached to the handle and dragged with it."
deprecated="17"
@@ -1342,7 +1342,7 @@
<presentation title=" (%orientation%)"/>
- <palette title="LinearLayout" icon="AndroidDesignerIcons.LinearLayout"
+ <palette title="LinearLayout" icon="AndroidIcons.Views.LinearLayout"
tooltip="A Layout that arranges its children in a single row or column."/>
<morphing to="RadioGroup TableLayout GridLayout RelativeLayout"/>
@@ -1368,7 +1368,7 @@
class="android.widget.FrameLayout"
tag="FrameLayout">
- <palette title="FrameLayout" icon="AndroidDesignerIcons.FrameLayout"
+ <palette title="FrameLayout" icon="AndroidIcons.Views.FrameLayout"
tooltip="FrameLayout is designed to block out an area on the screen to display a single item."/>
<morphing to="TableLayout GridLayout RelativeLayout"/>
@@ -1392,7 +1392,7 @@
class="android.widget.AbsoluteLayout"
tag="AbsoluteLayout">
- <palette title="AbsoluteLayout" icon="AndroidDesignerIcons.AbsoluteLayout"
+ <palette title="AbsoluteLayout" icon="AndroidIcons.Views.AbsoluteLayout"
tooltip="A layout that lets you specify exact locations (x/y coordinates) of its children. Absolute layouts are less flexible and harder to maintain than other types of layouts without absolute positioning."
deprecated="3"
deprecatedHint=""
@@ -1417,7 +1417,7 @@
<properties important="stretchColumns shrinkColumns collapseColumns"/>
- <palette title="TableLayout" icon="AndroidDesignerIcons.TableLayout"
+ <palette title="TableLayout" icon="AndroidIcons.Views.TableLayout"
tooltip="A layout that arranges its children into rows and columns. A TableLayout consists of a number of TableRow objects, each defining a row (actually, you can have other children, which will be explained below). TableLayout containers do not display border lines for their rows, columns, or cells. Each row has zero or more cells; each cell can hold one View object. The table has as many columns as the row with the most cells. A table can leave cells empty. Cells can span columns, as they can in HTML."/>
<morphing to="GridLayout RelativeLayout"/>
@@ -1437,7 +1437,7 @@
class="android.widget.TableRow"
tag="TableRow">
- <palette title="TableRow" icon="AndroidDesignerIcons.TableRow"
+ <palette title="TableRow" icon="AndroidIcons.Views.TableRow"
tooltip="A layout that arranges its children horizontally. A TableRow should always be used as a child of a TableLayout. If a TableRow's parent is not a TableLayout, the TableRow will behave as an horizontal LinearLayout."/>
<creation>
@@ -1463,7 +1463,7 @@
<morphing to="TableLayout RelativeLayout"/>
- <palette title="GridLayout" icon="AndroidDesignerIcons.GridLayout"
+ <palette title="GridLayout" icon="AndroidIcons.Views.GridLayout"
version="14"
tooltip="A layout that places its children in a rectangular grid."/>
@@ -1490,7 +1490,7 @@
<morphing to="TableLayout RelativeLayout"/>
- <palette title="v7.GridLayout" icon="AndroidDesignerIcons.GridLayout"
+ <palette title="v7.GridLayout" icon="AndroidIcons.Views.GridLayout"
version="7"
tooltip="A layout that places its children in a rectangular grid."/>
@@ -1512,7 +1512,7 @@
<properties important="gravity ignoreGravity"/>
- <palette title="RelativeLayout" icon="AndroidDesignerIcons.RelativeLayout"
+ <palette title="RelativeLayout" icon="AndroidIcons.Views.RelativeLayout"
tooltip="A Layout where the positions of the children can be described in relation to each other or to the parent."/>
<creation>
@@ -1541,10 +1541,10 @@
<item tag="FrameLayout"/>
<item tag="LinearLayout"
title="LinearLayout (Horizontal)"
- icon="AndroidDesignerIcons.LinearLayout"
+ icon="AndroidIcons.Views.LinearLayout"
tooltip="A Layout that arranges its children in a single row.">
<item title="LinearLayout (Vertical)"
- icon="AndroidDesignerIcons.VerticalLinearLayout"
+ icon="AndroidIcons.Views.VerticalLinearLayout"
tooltip="A Layout that arranges its children in a single column.">
<creation>
<![CDATA[
diff --git a/android-designer/src/icons/AndroidDesignerIcons.java b/android-designer/src/icons/AndroidDesignerIcons.java
index 441a9ea..a864f25 100644
--- a/android-designer/src/icons/AndroidDesignerIcons.java
+++ b/android-designer/src/icons/AndroidDesignerIcons.java
@@ -13,79 +13,8 @@
return IconLoader.getIcon(path, AndroidDesignerIcons.class);
}
- public static final Icon AbsoluteLayout = load("/icons/AbsoluteLayout.png"); // 16x16
- public static final Icon AdapterViewFlipper = load("/icons/AdapterViewFlipper.png"); // 16x16
- public static final Icon AnalogClock = load("/icons/AnalogClock.png"); // 16x16
- public static final Icon AutoCompleteTextView = load("/icons/AutoCompleteTextView.png"); // 16x16
- public static final Icon Button = load("/icons/Button.png"); // 16x16
- public static final Icon CalendarView = load("/icons/CalendarView.png"); // 16x16
- public static final Icon CheckBox = load("/icons/CheckBox.png"); // 16x16
- public static final Icon CheckedTextView = load("/icons/CheckedTextView.png"); // 16x16
- public static final Icon Chronometer = load("/icons/Chronometer.png"); // 16x16
- public static final Icon DatePicker = load("/icons/DatePicker.png"); // 16x16
- public static final Icon DeviceScreen = load("/icons/DeviceScreen.png"); // 16x16
- public static final Icon DialerFilter = load("/icons/DialerFilter.png"); // 16x16
- public static final Icon DigitalClock = load("/icons/DigitalClock.png"); // 16x16
- public static final Icon EditText = load("/icons/EditText.png"); // 16x16
- public static final Icon ExpandableListView = load("/icons/ExpandableListView.png"); // 16x16
- public static final Icon Fragment = load("/icons/fragment.png"); // 16x16
- public static final Icon FrameLayout = load("/icons/FrameLayout.png"); // 16x16
- public static final Icon Gallery = load("/icons/Gallery.png"); // 16x16
- public static final Icon GestureOverlayView = load("/icons/GestureOverlayView.png"); // 16x16
- public static final Icon Gravity = load("/icons/gravity.png"); // 16x16
- public static final Icon GridLayout = load("/icons/GridLayout.png"); // 16x16
- public static final Icon GridView = load("/icons/GridView.png"); // 16x16
- public static final Icon HorizontalScrollView = load("/icons/HorizontalScrollView.png"); // 16x16
- public static final Icon ImageButton = load("/icons/ImageButton.png"); // 16x16
- public static final Icon ImageSwitcher = load("/icons/ImageSwitcher.png"); // 16x16
- public static final Icon ImageView = load("/icons/ImageView.png"); // 16x16
- public static final Icon Include = load("/icons/include.png"); // 16x16
- public static final Icon LinearLayout = load("/icons/LinearLayout.png"); // 16x16
- public static final Icon VerticalLinearLayout = load("/icons/VerticalLinearLayout.png"); // 16x16
- public static final Icon LinearLayout3 = load("/icons/LinearLayout3.png"); // 16x16
- public static final Icon ListView = load("/icons/ListView.png"); // 16x16
- public static final Icon MediaController = load("/icons/MediaController.png"); // 16x16
- public static final Icon MultiAutoCompleteTextView = load("/icons/MultiAutoCompleteTextView.png"); // 16x16
- public static final Icon NumberPicker = load("/icons/NumberPicker.png"); // 16x16
- public static final Icon ProgressBar = load("/icons/ProgressBar.png"); // 16x16
- public static final Icon QuickContactBadge = load("/icons/QuickContactBadge.png"); // 16x16
- public static final Icon RadioButton = load("/icons/RadioButton.png"); // 16x16
- public static final Icon RadioGroup = load("/icons/RadioGroup.png"); // 16x16
- public static final Icon RatingBar = load("/icons/RatingBar.png"); // 16x16
- public static final Icon RelativeLayout = load("/icons/RelativeLayout.png"); // 16x16
- public static final Icon RequestFocus = load("/icons/requestFocus.png"); // 16x16
- public static final Icon ScrollView = load("/icons/ScrollView.png"); // 16x16
- public static final Icon SearchView = load("/icons/SearchView.png"); // 16x16
- public static final Icon SeekBar = load("/icons/SeekBar.png"); // 16x16
- public static final Icon SlidingDrawer = load("/icons/SlidingDrawer.png"); // 16x16
- public static final Icon Space = load("/icons/Space.png"); // 16x16
- public static final Icon Spinner = load("/icons/Spinner.png"); // 16x16
- public static final Icon StackView = load("/icons/StackView.png"); // 16x16
- public static final Icon SurfaceView = load("/icons/SurfaceView.png"); // 16x16
- public static final Icon Switch = load("/icons/Switch.png"); // 16x16
- public static final Icon TabHost = load("/icons/TabHost.png"); // 16x16
- public static final Icon TableLayout = load("/icons/TableLayout.png"); // 16x16
- public static final Icon TableRow = load("/icons/TableRow.png"); // 16x16
- public static final Icon TabWidget = load("/icons/TabWidget.png"); // 16x16
- public static final Icon TextClock = load("/icons/TextClock.png"); // 16x16
- public static final Icon TextSwitcher = load("/icons/TextSwitcher.png"); // 16x16
- public static final Icon TextureView = load("/icons/TextureView.png"); // 16x16
- public static final Icon TextView = load("/icons/TextView.png"); // 16x16
- public static final Icon TimePicker = load("/icons/TimePicker.png"); // 16x16
- public static final Icon ToggleButton = load("/icons/ToggleButton.png"); // 16x16
- public static final Icon TwoLineListItem = load("/icons/TwoLineListItem.png"); // 16x16
- public static final Icon Unknown = load("/icons/customView.png"); // 16x16
- public static final Icon VideoView = load("/icons/VideoView.png"); // 16x13
- public static final Icon View = load("/icons/View.png"); // 16x16
- public static final Icon ViewAnimator = load("/icons/ViewAnimator.png"); // 16x16
- public static final Icon ViewFlipper = load("/icons/ViewFlipper.png"); // 16x16
- public static final Icon ViewStub = load("/icons/ViewStub.png"); // 16x16
- public static final Icon ViewSwitcher = load("/icons/ViewSwitcher.png"); // 16x16
- public static final Icon WebView = load("/icons/WebView.png"); // 16x16
- public static final Icon ZoomButton = load("/icons/ZoomButton.png"); // 16x16
- public static final Icon ZoomControls = load("/icons/ZoomControls.png"); // 16x16
-
// Layout actions
+ public static final Icon Gravity = load("/icons/gravity.png"); // 16x16
public static final Icon Margins = load("/icons/margins.png"); // 16x16
public static final Icon FillWidth = load("/icons/fillwidth.png"); // 16x16
public static final Icon WrapWidth = load("/icons/wrapwidth.png"); // 16x16
@@ -113,6 +42,12 @@
public static final Icon CenterVertically = load("/icons/centerVertically.png"); // 16x16
public static final Icon CenterHorizontally = load("/icons/centerHorizontally.png"); // 16x16
+ // LinearLayout layout actions
public static final Icon SwitchHorizontalLinear = load("/icons/hlinear.png"); // 16x16
public static final Icon SwitchVerticalLinear = load("/icons/vlinear.png"); // 16x16
+
+ // ScrollView/HorizontalScrollView layout actions
+ public static final Icon NormalRender = load("/icons/normalrender.png"); // 16x16
+ public static final Icon ViewportRender = load("/icons/viewportrender.png"); // 16x16
+
}
diff --git a/android-designer/src/icons/normalrender.png b/android-designer/src/icons/normalrender.png
new file mode 100644
index 0000000..a321f8b
--- /dev/null
+++ b/android-designer/src/icons/normalrender.png
Binary files differ
diff --git a/android-designer/src/icons/viewportrender.png b/android-designer/src/icons/viewportrender.png
new file mode 100644
index 0000000..0f8dd54
--- /dev/null
+++ b/android-designer/src/icons/viewportrender.png
Binary files differ
diff --git a/android-gradle-jps/src/com/android/tools/idea/jps/builder/AndroidGradleBuildTarget.java b/android-gradle-jps/src/com/android/tools/idea/jps/builder/AndroidGradleBuildTarget.java
new file mode 100644
index 0000000..d203bed
--- /dev/null
+++ b/android-gradle-jps/src/com/android/tools/idea/jps/builder/AndroidGradleBuildTarget.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.jps.builder;
+
+import com.android.tools.idea.gradle.compiler.AndroidGradleBuildTargetScopeProvider;
+import com.android.tools.idea.jps.AndroidGradleJps;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.jps.builders.*;
+import org.jetbrains.jps.builders.impl.BuildRootDescriptorImpl;
+import org.jetbrains.jps.builders.storage.BuildDataPaths;
+import org.jetbrains.jps.incremental.CompileContext;
+import org.jetbrains.jps.indices.IgnoredFileIndex;
+import org.jetbrains.jps.indices.ModuleExcludeIndex;
+import org.jetbrains.jps.model.JpsModel;
+import org.jetbrains.jps.model.JpsProject;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+public class AndroidGradleBuildTarget extends BuildTarget<AndroidGradleBuildTarget.RootDescriptor> {
+ @NonNls private static final String BUILD_TARGET_NAME = "Android Gradle Build Target";
+
+ @NotNull private final JpsProject myProject;
+
+ protected AndroidGradleBuildTarget(@NotNull JpsProject project) {
+ super(TargetType.INSTANCE);
+ myProject = project;
+ }
+
+ @NotNull
+ public JpsProject getProject() {
+ return myProject;
+ }
+
+ @Override
+ public String getId() {
+ return AndroidGradleBuildTargetScopeProvider.TARGET_ID;
+ }
+
+ @Override
+ public Collection<BuildTarget<?>> computeDependencies(BuildTargetRegistry targetRegistry, TargetOutputIndex outputIndex) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ @NotNull
+ public List<RootDescriptor> computeRootDescriptors(JpsModel model,
+ ModuleExcludeIndex index,
+ IgnoredFileIndex ignoredFileIndex,
+ BuildDataPaths dataPaths) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ @Nullable
+ public RootDescriptor findRootDescriptor(String rootId, BuildRootIndex rootIndex) {
+ for (RootDescriptor descriptor : rootIndex.getTargetRoots(this, null)) {
+ if (descriptor.getRootId().equals(rootId)) {
+ return descriptor;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ @NotNull
+ public String getPresentableName() {
+ return BUILD_TARGET_NAME;
+ }
+
+ @NotNull
+ @Override
+ public Collection<File> getOutputRoots(CompileContext context) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AndroidGradleBuildTarget that = (AndroidGradleBuildTarget)o;
+ return myProject.equals(that.myProject);
+ }
+
+ @Override
+ public int hashCode() {
+ return myProject.hashCode();
+ }
+
+ public static class TargetType extends BuildTargetType<AndroidGradleBuildTarget> {
+ public static final TargetType INSTANCE = new TargetType();
+
+ private TargetType() {
+ super(AndroidGradleBuildTargetScopeProvider.TARGET_TYPE_ID);
+ }
+
+ @Override
+ @NotNull
+ public List<AndroidGradleBuildTarget> computeAllTargets(@NotNull JpsModel model) {
+ JpsProject project = model.getProject();
+ if (!AndroidGradleJps.hasAndroidGradleFacet(project)) {
+ return Collections.emptyList();
+ }
+
+ return Collections.singletonList(new AndroidGradleBuildTarget(project));
+ }
+
+ @Override
+ @NotNull
+ public BuildTargetLoader<AndroidGradleBuildTarget> createLoader(@NotNull JpsModel model) {
+ final JpsProject project = model.getProject();
+ return new BuildTargetLoader<AndroidGradleBuildTarget>() {
+ @Nullable
+ @Override
+ public AndroidGradleBuildTarget createTarget(@NotNull String targetId) {
+ return AndroidGradleBuildTargetScopeProvider.TARGET_ID.equals(targetId) && AndroidGradleJps.hasAndroidGradleFacet(project)
+ ? new AndroidGradleBuildTarget(project)
+ : null;
+ }
+ };
+ }
+ }
+
+ public static class RootDescriptor extends BuildRootDescriptorImpl {
+ RootDescriptor(BuildTarget target, File root) {
+ super(target, root);
+ }
+ }
+}
diff --git a/android-gradle-jps/src/com/android/tools/idea/jps/builder/AndroidGradleBuilder.java b/android-gradle-jps/src/com/android/tools/idea/jps/builder/AndroidGradleBuilder.java
index 91383cd..eb8418f 100644
--- a/android-gradle-jps/src/com/android/tools/idea/jps/builder/AndroidGradleBuilder.java
+++ b/android-gradle-jps/src/com/android/tools/idea/jps/builder/AndroidGradleBuilder.java
@@ -16,68 +16,32 @@
package com.android.tools.idea.jps.builder;
import com.android.SdkConstants;
-import com.android.tools.idea.gradle.facet.AndroidGradleFacet;
-import com.android.tools.idea.gradle.util.LocalProperties;
import com.android.tools.idea.jps.AndroidGradleJps;
import com.android.tools.idea.jps.model.JpsAndroidGradleModuleExtension;
-import com.android.tools.idea.jps.output.parser.GradleErrorOutputParser;
-import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.common.io.Closeables;
-import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.util.io.FileUtil;
-import com.intellij.util.SystemProperties;
-import org.gradle.tooling.BuildException;
-import org.gradle.tooling.BuildLauncher;
-import org.gradle.tooling.GradleConnector;
-import org.gradle.tooling.ProjectConnection;
-import org.gradle.tooling.internal.consumer.DefaultGradleConnector;
-import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.ModuleChunk;
-import org.jetbrains.jps.android.AndroidJpsUtil;
import org.jetbrains.jps.android.AndroidSourceGeneratingBuilder;
-import org.jetbrains.jps.android.model.JpsAndroidSdkProperties;
-import org.jetbrains.jps.android.model.impl.JpsAndroidModuleExtensionImpl;
-import org.jetbrains.jps.android.model.impl.JpsAndroidModuleProperties;
import org.jetbrains.jps.builders.DirtyFilesHolder;
-import org.jetbrains.jps.builders.java.JavaBuilderUtil;
import org.jetbrains.jps.builders.java.JavaSourceRootDescriptor;
-import org.jetbrains.jps.incremental.*;
+import org.jetbrains.jps.incremental.BuilderCategory;
+import org.jetbrains.jps.incremental.CompileContext;
+import org.jetbrains.jps.incremental.ModuleBuildTarget;
+import org.jetbrains.jps.incremental.ModuleLevelBuilder;
import org.jetbrains.jps.incremental.java.JavaBuilder;
-import org.jetbrains.jps.incremental.messages.BuildMessage;
-import org.jetbrains.jps.incremental.messages.CompilerMessage;
-import org.jetbrains.jps.incremental.messages.ProgressMessage;
import org.jetbrains.jps.incremental.resources.ResourcesBuilder;
import org.jetbrains.jps.incremental.resources.StandardResourceBuilderEnabler;
import org.jetbrains.jps.model.JpsProject;
-import org.jetbrains.jps.model.JpsSimpleElement;
-import org.jetbrains.jps.model.library.sdk.JpsSdk;
import org.jetbrains.jps.model.module.JpsModule;
-import java.io.*;
-import java.util.Arrays;
-import java.util.Collection;
import java.util.List;
-import java.util.Properties;
-import java.util.concurrent.TimeUnit;
/**
- * Builds an IDEA project using Gradle.
+ * The only purpose of this builder is to disable all instances of {@link ModuleLevelBuilder} that are not related to Gradle.
*/
public class AndroidGradleBuilder extends ModuleLevelBuilder {
- private static final Logger LOG = Logger.getInstance(AndroidGradleBuilder.class);
- private static final GradleErrorOutputParser ERROR_OUTPUT_PARSER = new GradleErrorOutputParser();
-
- @NonNls private static final String JVM_ARG_FORMAT = "-D%1$s=%2$s";
- @NonNls private static final String JVM_ARG_WITH_QUOTED_VALUE_FORMAT = "-D%1$s=\"%2$s\"";
-
@NonNls private static final String BUILDER_NAME = "Android Gradle Builder";
- @NonNls private static final String DEFAULT_ASSEMBLE_TASK_NAME = "assemble";
- @NonNls private static final String GRADLE_SEPARATOR = ":";
protected AndroidGradleBuilder() {
super(BuilderCategory.TRANSLATOR);
@@ -103,326 +67,24 @@
}
/**
- * Builds a project using Gradle.
+ * It does nothing.
*
- * @return {@link ExitCode#OK} if compilation with Gradle succeeds without errors.
- * @throws ProjectBuildException if something goes wrong while invoking Gradle or if there are compilation errors. Compilation errors are
- * displayed in IDEA's "Problems" view.
+ * @return {@link ExitCode#OK} if the modules to build are Gradle ones, otherwise it returns {@link ExitCode#NOTHING_DONE}.
*/
@NotNull
@Override
public ExitCode build(CompileContext context,
ModuleChunk chunk,
DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
- OutputConsumer outputConsumer) throws ProjectBuildException {
+ OutputConsumer outputConsumer) {
JpsAndroidGradleModuleExtension extension = AndroidGradleJps.getFirstExtension(chunk);
if (extension == null) {
- if (LOG.isDebugEnabled()) {
- String format = "Project '%1$s' does not have the '%2$s' facet. Nothing done.";
- LOG.info(String.format(format, getProjectName(context), AndroidGradleFacet.NAME));
- }
return ExitCode.NOTHING_DONE;
}
-
- String[] buildTasks = getBuildTasks(context, chunk);
- if (buildTasks.length == 0) {
- String format = "No build tasks found for project '%1$s'. Nothing done.";
- LOG.info(String.format(format, getProjectName(context)));
- return ExitCode.NOTHING_DONE;
- }
-
- String msg = "Gradle build using tasks: " + Arrays.toString(buildTasks);
- context.processMessage(new ProgressMessage(msg));
- LOG.info(msg);
-
- ensureTempDirExists();
-
- BuilderExecutionSettings executionSettings;
- try {
- executionSettings = new BuilderExecutionSettings();
- } catch (RuntimeException e) {
- throw new ProjectBuildException(e);
- }
-
- LOG.info("Using execution settings: " + executionSettings);
-
- String androidHome = null;
- if (!isAndroidHomeKnown(executionSettings)) {
- androidHome = getAndroidHomeFromModuleSdk(context, chunk);
- }
-
- String format = "About to build project '%1$s' located at %2$s";
- LOG.info(String.format(format, getProjectName(context), executionSettings.getProjectDir().getAbsolutePath()));
-
- return doBuild(context, buildTasks, executionSettings, androidHome);
- }
-
- @NotNull
- private static String[] getBuildTasks(@NotNull CompileContext context, @NotNull ModuleChunk chunk) {
- List<String> tasks = Lists.newArrayList();
- boolean isRebuild = JavaBuilderUtil.isForcedRecompilationAllJavaModules(context);
- for (JpsModule module : chunk.getModules()) {
- populateBuildTasks(module, tasks, isRebuild);
- }
- return tasks.toArray(new String[tasks.size()]);
- }
-
- private static void populateBuildTasks(@NotNull JpsModule module, @NotNull List<String> tasks, boolean isRebuild) {
- JpsAndroidGradleModuleExtension androidGradleFacet = AndroidGradleJps.getExtension(module);
- if (androidGradleFacet == null) {
- return;
- }
- String gradleProjectPath = androidGradleFacet.getProperties().GRADLE_PROJECT_PATH;
- if (gradleProjectPath == null) {
- // Gradle project path is never, ever null. If the path is empty, it shows as ":". We had reports of this happening. It is likely that
- // users manually added the Android-Gradle facet to a project. After all it is likely not to be a Gradle module. Better quit and not
- // build the module.
- return;
- }
- String assembleTaskName = null;
- JpsAndroidModuleExtensionImpl androidFacet = (JpsAndroidModuleExtensionImpl)AndroidJpsUtil.getExtension(module);
- if (androidFacet != null) {
- JpsAndroidModuleProperties properties = androidFacet.getProperties();
- assembleTaskName = properties.ASSEMBLE_TASK_NAME;
- }
- if (Strings.isNullOrEmpty(assembleTaskName)) {
- if (GRADLE_SEPARATOR.equals(gradleProjectPath)) {
- // This module is in reality the root project directory. If there is no task, don't assume there is an "assemble" one.
- return;
- }
- assembleTaskName = DEFAULT_ASSEMBLE_TASK_NAME;
- }
- if (isRebuild) {
- tasks.add(createBuildTask(gradleProjectPath, "clean"));
- }
- assert assembleTaskName != null;
- tasks.add(createBuildTask(gradleProjectPath, assembleTaskName));
- }
-
- @NotNull
- private static String createBuildTask(@NotNull String gradleProjectPath, @NotNull String taskName) {
- return gradleProjectPath + GRADLE_SEPARATOR + taskName;
- }
-
- private static void ensureTempDirExists() {
- // Gradle checks that the dir at "java.io.tmpdir" exists, and if it doesn't it fails (on Windows.)
- String tmpDirProperty = System.getProperty("java.io.tmpdir");
- if (!Strings.isNullOrEmpty(tmpDirProperty)) {
- File tmpDir = new File(tmpDirProperty);
- try {
- FileUtil.ensureExists(tmpDir);
- }
- catch (IOException e) {
- LOG.warn("Unable to create temp directory", e);
- }
- }
- }
-
- @NotNull
- private static CompilerMessage createCompilerErrorMessage(@NotNull String msg) {
- return AndroidGradleJps.createCompilerMessage(BuildMessage.Kind.ERROR, msg);
- }
-
- /**
- * Indicates whether the path of the Android SDK home directory is specified in a local.properties file or in the ANDROID_HOME environment
- * variable.
- *
- * @param settings build execution settings.
- * @return {@code true} if the Android SDK home directory is specified in the project's local.properties file or in the ANDROID_HOME
- * environment variable; {@code false} otherwise.
- */
- private static boolean isAndroidHomeKnown(@NotNull BuilderExecutionSettings settings) {
- String androidHome = getAndroidHomeFromLocalPropertiesFile(settings.getProjectDir());
- if (!Strings.isNullOrEmpty(androidHome)) {
- String msg = String.format("Found Android SDK home at '%1$s' (from local.properties file)", androidHome);
- LOG.info(msg);
- return true;
- }
- androidHome = System.getenv(AndroidSdkUtils.ANDROID_HOME_ENV);
- if (!Strings.isNullOrEmpty(androidHome)) {
- String msg = String.format("Found Android SDK home at '%1$s' (from ANDROID_HOME environment variable)", androidHome);
- LOG.info(msg);
- return true;
- }
- return false;
- }
-
- @Nullable
- private static String getAndroidHomeFromLocalPropertiesFile(@NotNull File projectDir) {
- File filePath = new File(projectDir, SdkConstants.FN_LOCAL_PROPERTIES);
- if (!filePath.isFile()) {
- return null;
- }
- Properties properties = new Properties();
- FileInputStream fileInputStream = null;
- try {
- //noinspection IOResourceOpenedButNotSafelyClosed
- fileInputStream = new FileInputStream(filePath);
- properties.load(fileInputStream);
- } catch (FileNotFoundException e) {
- return null;
- } catch (IOException e) {
- String msg = String.format("Failed to read file '%1$s'", filePath.getPath());
- LOG.error(msg, e);
- return null;
- } finally {
- Closeables.closeQuietly(fileInputStream);
- }
- return properties.getProperty(LocalProperties.SDK_DIR_PROPERTY);
- }
-
-
- @Nullable
- private static String getAndroidHomeFromModuleSdk(@NotNull CompileContext context, @NotNull ModuleChunk chunk) {
- JpsSdk<JpsSimpleElement<JpsAndroidSdkProperties>> androidSdk = AndroidGradleJps.getFirstAndroidSdk(chunk);
- if (androidSdk == null) {
- // TODO: Figure out what changes in IDEA made androidSdk null. It used to work.
- String msg = String.format("There is no Android SDK specified for project '%1$s'", getProjectName(context));
- LOG.error(msg);
- return null;
- }
- String androidHome = androidSdk.getHomePath();
- if (Strings.isNullOrEmpty(androidHome)) {
- String msg = "Selected Android SDK does not have a home directory path";
- LOG.error(msg);
- return null;
- }
- return androidHome;
- }
-
- @NotNull
- private static String getProjectName(@NotNull CompileContext context) {
- return context.getProjectDescriptor().getProject().getName();
- }
-
- @NotNull
- private static ExitCode doBuild(@NotNull CompileContext context,
- @NotNull String[] buildTasks,
- @NotNull BuilderExecutionSettings executionSettings,
- @Nullable String androidHome) throws ProjectBuildException {
- GradleConnector connector = getGradleConnector(executionSettings);
-
- ProjectConnection connection = connector.connect();
- ByteArrayOutputStream stdout = new ByteArrayOutputStream();
- ByteArrayOutputStream stderr = new ByteArrayOutputStream();
-
- try {
- BuildLauncher launcher = connection.newBuild();
- launcher.forTasks(buildTasks);
-
- List<String> jvmArgs = Lists.newArrayList();
-
- if (androidHome != null && !androidHome.isEmpty()) {
- String androidSdkArg = getAndroidHomeJvmArg(androidHome);
- jvmArgs.add(androidSdkArg);
- }
-
- int xmx = executionSettings.getGradleDaemonMaxMemoryInMb();
- if (xmx > 0) {
- jvmArgs.add(String.format("-Xmx%dm", xmx));
- }
-
- if (!jvmArgs.isEmpty()) {
- LOG.info("Passing JVM args to Gradle Tooling API: " + jvmArgs);
- launcher.setJvmArguments(jvmArgs.toArray(new String[jvmArgs.size()]));
- }
-
- launcher.setStandardOutput(stdout);
- launcher.setStandardError(stderr);
- launcher.run();
- }
- catch (BuildException e) {
- handleBuildException(e, context, stderr.toString());
- }
- finally {
- String outText = stdout.toString();
- context.processMessage(new ProgressMessage(outText, 1.0f));
- Closeables.closeQuietly(stdout);
- Closeables.closeQuietly(stderr);
- connection.close();
- }
-
return ExitCode.OK;
}
@NotNull
- private static GradleConnector getGradleConnector(@NotNull BuilderExecutionSettings executionSettings) {
- GradleConnector connector = GradleConnector.newConnector();
- if (connector instanceof DefaultGradleConnector) {
- DefaultGradleConnector defaultConnector = (DefaultGradleConnector)connector;
-
- if (executionSettings.isEmbeddedGradleDaemonEnabled()) {
- LOG.info("Using Gradle embedded mode.");
- defaultConnector.embedded(true);
- }
-
- defaultConnector.setVerboseLogging(executionSettings.isVerboseLoggingEnabled());
-
- int daemonMaxIdleTimeInMs = executionSettings.getGradleDaemonMaxIdleTimeInMs();
- if (daemonMaxIdleTimeInMs > 0) {
- defaultConnector.daemonMaxIdleTime(daemonMaxIdleTimeInMs, TimeUnit.MILLISECONDS);
- }
- }
-
- connector.forProjectDirectory(executionSettings.getProjectDir());
-
- File gradleHomeDir = executionSettings.getGradleHomeDir();
- if (gradleHomeDir != null) {
- connector.useInstallation(gradleHomeDir);
- }
-
- File gradleServiceDir = executionSettings.getGradleServiceDir();
- if (gradleServiceDir != null) {
- connector.useGradleUserHomeDir(gradleServiceDir);
- }
-
- return connector;
- }
-
- @NotNull
- private static String getAndroidHomeJvmArg(@NotNull String androidHome) {
- String format = JVM_ARG_FORMAT;
- if (androidHome.contains(" ")) {
- format = JVM_ARG_WITH_QUOTED_VALUE_FORMAT;
- }
- return String.format(format, "android.home", androidHome);
- }
-
- /**
- * Something went wrong while invoking Gradle. Since we cannot distinguish an execution error from compilation errors easily, we first try
- * to show, in the "Problems" view, compilation errors by parsing the error output. If no errors are found, we show the stack trace in the
- * "Problems" view. The idea is that we need to somehow inform the user that something went wrong.
- */
- private static void handleBuildException(BuildException e, CompileContext context, String stdErr) throws ProjectBuildException {
- Collection<CompilerMessage> compilerMessages = ERROR_OUTPUT_PARSER.parseErrorOutput(stdErr);
- if (!compilerMessages.isEmpty()) {
- for (CompilerMessage message : compilerMessages) {
- context.processMessage(message);
- }
- return;
- }
- // There are no error messages to present. Show some feedback indicating that something went wrong.
- if (!stdErr.isEmpty()) {
- // Show the contents of stderr as a compiler error.
- context.processMessage(createCompilerErrorMessage(stdErr));
- }
- else {
- // Since we have nothing else to show, just print the stack trace of the caught exception.
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- try {
- //noinspection IOResourceOpenedButNotSafelyClosed
- e.printStackTrace(new PrintStream(out));
- String message = "Internal error:" + SystemProperties.getLineSeparator() + out.toString();
- context.processMessage(createCompilerErrorMessage(message));
- }
- finally {
- Closeables.closeQuietly(out);
- }
- }
- throw new ProjectBuildException(e.getMessage());
- }
-
- @NotNull
@Override
public String getPresentableName() {
return BUILDER_NAME;
diff --git a/android-gradle-jps/src/com/android/tools/idea/jps/builder/AndroidGradleBuilderService.java b/android-gradle-jps/src/com/android/tools/idea/jps/builder/AndroidGradleBuilderService.java
index 5d9731d..1170fbf 100644
--- a/android-gradle-jps/src/com/android/tools/idea/jps/builder/AndroidGradleBuilderService.java
+++ b/android-gradle-jps/src/com/android/tools/idea/jps/builder/AndroidGradleBuilderService.java
@@ -15,20 +15,33 @@
*/
package com.android.tools.idea.jps.builder;
-import com.google.common.collect.ImmutableList;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.jps.builders.BuildTargetType;
import org.jetbrains.jps.incremental.BuilderService;
import org.jetbrains.jps.incremental.ModuleLevelBuilder;
+import org.jetbrains.jps.incremental.TargetBuilder;
+import java.util.Collections;
import java.util.List;
/**
* Factory of {@link AndroidGradleBuilder} instances.
*/
public class AndroidGradleBuilderService extends BuilderService {
- @NotNull
@Override
+ @NotNull
public List<? extends ModuleLevelBuilder> createModuleLevelBuilders() {
- return ImmutableList.of(new AndroidGradleBuilder());
+ return Collections.singletonList(new AndroidGradleBuilder());
+ }
+
+ @Override
+ public List<? extends BuildTargetType<?>> getTargetTypes() {
+ return Collections.singletonList(AndroidGradleBuildTarget.TargetType.INSTANCE);
+ }
+
+ @Override
+ @NotNull
+ public List<? extends TargetBuilder<?, ?>> createBuilders() {
+ return Collections.singletonList(new AndroidGradleTargetBuilder());
}
}
diff --git a/android-gradle-jps/src/com/android/tools/idea/jps/builder/AndroidGradleTargetBuilder.java b/android-gradle-jps/src/com/android/tools/idea/jps/builder/AndroidGradleTargetBuilder.java
new file mode 100644
index 0000000..fcb76e0
--- /dev/null
+++ b/android-gradle-jps/src/com/android/tools/idea/jps/builder/AndroidGradleTargetBuilder.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.jps.builder;
+
+import com.android.SdkConstants;
+import com.android.tools.idea.gradle.util.AndroidGradleSettings;
+import com.android.tools.idea.jps.AndroidGradleJps;
+import com.android.tools.idea.jps.model.JpsAndroidGradleModuleExtension;
+import com.android.tools.idea.jps.output.parser.GradleErrorOutputParser;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.io.Closeables;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.util.SystemProperties;
+import org.gradle.tooling.BuildException;
+import org.gradle.tooling.BuildLauncher;
+import org.gradle.tooling.GradleConnector;
+import org.gradle.tooling.ProjectConnection;
+import org.gradle.tooling.internal.consumer.DefaultGradleConnector;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.jps.android.AndroidJpsUtil;
+import org.jetbrains.jps.android.model.JpsAndroidSdkProperties;
+import org.jetbrains.jps.android.model.JpsAndroidSdkType;
+import org.jetbrains.jps.android.model.impl.JpsAndroidModuleExtensionImpl;
+import org.jetbrains.jps.android.model.impl.JpsAndroidModuleProperties;
+import org.jetbrains.jps.builders.BuildOutputConsumer;
+import org.jetbrains.jps.builders.DirtyFilesHolder;
+import org.jetbrains.jps.builders.java.JavaBuilderUtil;
+import org.jetbrains.jps.incremental.CompileContext;
+import org.jetbrains.jps.incremental.ProjectBuildException;
+import org.jetbrains.jps.incremental.TargetBuilder;
+import org.jetbrains.jps.incremental.messages.BuildMessage;
+import org.jetbrains.jps.incremental.messages.CompilerMessage;
+import org.jetbrains.jps.incremental.messages.ProgressMessage;
+import org.jetbrains.jps.model.JpsProject;
+import org.jetbrains.jps.model.JpsSimpleElement;
+import org.jetbrains.jps.model.library.sdk.JpsSdk;
+import org.jetbrains.jps.model.module.JpsModule;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Builds Gradle-based Android project using Gradle.
+ */
+public class AndroidGradleTargetBuilder extends TargetBuilder<AndroidGradleBuildTarget.RootDescriptor, AndroidGradleBuildTarget> {
+ private static final Logger LOG = Logger.getInstance(AndroidGradleTargetBuilder.class);
+ private static final GradleErrorOutputParser ERROR_OUTPUT_PARSER = new GradleErrorOutputParser();
+
+ @NonNls private static final String CLEAN_TASK_NAME = "clean";
+ @NonNls private static final String DEFAULT_ASSEMBLE_TASK_NAME = "assemble";
+
+ @NonNls private static final String BUILDER_NAME = "Android Gradle Target Builder";
+
+ public AndroidGradleTargetBuilder() {
+ super(Collections.singletonList(AndroidGradleBuildTarget.TargetType.INSTANCE));
+ }
+
+ /**
+ * Builds a Gradle-based Android project using Gradle.
+ */
+ @Override
+ public void build(@NotNull AndroidGradleBuildTarget target,
+ @NotNull DirtyFilesHolder<AndroidGradleBuildTarget.RootDescriptor, AndroidGradleBuildTarget> holder,
+ @NotNull BuildOutputConsumer outputConsumer,
+ @NotNull CompileContext context) throws ProjectBuildException, IOException {
+ JpsProject project = target.getProject();
+
+ BuilderExecutionSettings executionSettings;
+ try {
+ executionSettings = new BuilderExecutionSettings();
+ } catch (RuntimeException e) {
+ throw new ProjectBuildException(e);
+ }
+
+ LOG.info("Using execution settings: " + executionSettings);
+
+
+ String[] buildTasks = getBuildTasks(project, context, executionSettings);
+ if (buildTasks.length == 0) {
+ String format = "No build tasks found for project '%1$s'. Nothing done.";
+ LOG.info(String.format(format, project.getName()));
+ return;
+ }
+
+ String msg = "Gradle build using tasks: " + Arrays.toString(buildTasks);
+ context.processMessage(new ProgressMessage(msg));
+ LOG.info(msg);
+
+ ensureTempDirExists();
+
+ String androidHome = null;
+ if (!AndroidGradleSettings.isAndroidSdkDirInLocalPropertiesFile(executionSettings.getProjectDir())) {
+ androidHome = getAndroidHomeFromModuleSdk(project);
+ }
+
+ String format = "About to build project '%1$s' located at %2$s";
+ LOG.info(String.format(format, project.getName(), executionSettings.getProjectDir().getAbsolutePath()));
+
+ doBuild(context, buildTasks, executionSettings, androidHome);
+ }
+
+ @NotNull
+ private static String[] getBuildTasks(@NotNull JpsProject project,
+ @NotNull CompileContext context,
+ @NotNull BuilderExecutionSettings executionSettings) {
+ boolean buildTests = AndroidJpsUtil.isInstrumentationTestContext(context);
+ List<String> tasks = Lists.newArrayList();
+ for (JpsModule module : project.getModules()) {
+ populateBuildTasks(module, executionSettings, tasks, buildTests);
+ }
+ if (!tasks.isEmpty()) {
+ boolean rebuild = JavaBuilderUtil.isForcedRecompilationAllJavaModules(context);
+ if (rebuild) {
+ tasks.add(0, CLEAN_TASK_NAME);
+ }
+ }
+ return tasks.toArray(new String[tasks.size()]);
+ }
+
+ private static void populateBuildTasks(@NotNull JpsModule module,
+ @NotNull BuilderExecutionSettings executionSettings,
+ @NotNull List<String> tasks,
+ boolean buildTests) {
+ JpsAndroidGradleModuleExtension androidGradleFacet = AndroidGradleJps.getExtension(module);
+ if (androidGradleFacet == null) {
+ return;
+ }
+ String gradleProjectPath = androidGradleFacet.getProperties().GRADLE_PROJECT_PATH;
+ if (gradleProjectPath == null) {
+ // Gradle project path is never, ever null. If the path is empty, it shows as ":". We had reports of this happening. It is likely that
+ // users manually added the Android-Gradle facet to a project. After all it is likely not to be a Gradle module. Better quit and not
+ // build the module.
+ String format = "Module '%1$s' does not have a Gradle path. It is likely that this module was manually added by the user.";
+ String msg = String.format(format, module.getName());
+ LOG.warn(msg);
+ return;
+ }
+ String assembleTaskName = null;
+ JpsAndroidModuleExtensionImpl androidFacet = (JpsAndroidModuleExtensionImpl)AndroidJpsUtil.getExtension(module);
+ if (androidFacet != null) {
+ JpsAndroidModuleProperties properties = androidFacet.getProperties();
+ assembleTaskName = executionSettings.isGenerateSourceOnly() ? properties.SOURCE_GEN_TASK_NAME : properties.ASSEMBLE_TASK_NAME;
+ }
+ if (Strings.isNullOrEmpty(assembleTaskName)) {
+ assembleTaskName = DEFAULT_ASSEMBLE_TASK_NAME;
+ }
+ assert assembleTaskName != null;
+ tasks.add(createBuildTask(gradleProjectPath, assembleTaskName));
+
+ if (buildTests && androidFacet != null) {
+ JpsAndroidModuleProperties properties = androidFacet.getProperties();
+ String assembleTestTaskName = properties.ASSEMBLE_TEST_TASK_NAME;
+ if (!Strings.isNullOrEmpty(assembleTestTaskName)) {
+ tasks.add(createBuildTask(gradleProjectPath, assembleTestTaskName));
+ }
+ }
+ }
+
+ @NotNull
+ private static String createBuildTask(@NotNull String gradleProjectPath, @NotNull String taskName) {
+ return gradleProjectPath + SdkConstants.GRADLE_PATH_SEPARATOR + taskName;
+ }
+
+ private static void ensureTempDirExists() {
+ // Gradle checks that the dir at "java.io.tmpdir" exists, and if it doesn't it fails (on Windows.)
+ String tmpDirProperty = System.getProperty("java.io.tmpdir");
+ if (!Strings.isNullOrEmpty(tmpDirProperty)) {
+ File tmpDir = new File(tmpDirProperty);
+ try {
+ FileUtil.ensureExists(tmpDir);
+ }
+ catch (IOException e) {
+ LOG.warn("Unable to create temp directory", e);
+ }
+ }
+ }
+
+ @Nullable
+ private static String getAndroidHomeFromModuleSdk(@NotNull JpsProject project) {
+ JpsSdk<JpsSimpleElement<JpsAndroidSdkProperties>> androidSdk = getFirstAndroidSdk(project);
+ if (androidSdk == null) {
+ // TODO: Figure out what changes in IDEA made androidSdk null. It used to work.
+ String msg = String.format("There is no Android SDK specified for project '%1$s'", project.getName());
+ LOG.error(msg);
+ return null;
+ }
+ String androidHome = androidSdk.getHomePath();
+ if (Strings.isNullOrEmpty(androidHome)) {
+ String msg = "Selected Android SDK does not have a home directory path";
+ LOG.error(msg);
+ return null;
+ }
+ return androidHome;
+ }
+
+ @Nullable
+ private static JpsSdk<JpsSimpleElement<JpsAndroidSdkProperties>> getFirstAndroidSdk(@NotNull JpsProject project) {
+ for (JpsModule module : project.getModules()) {
+ JpsSdk<JpsSimpleElement<JpsAndroidSdkProperties>> sdk = module.getSdk(JpsAndroidSdkType.INSTANCE);
+ if (sdk != null) {
+ return sdk;
+ }
+ }
+ return null;
+ }
+
+ private static void doBuild(@NotNull CompileContext context,
+ @NotNull String[] buildTasks,
+ @NotNull BuilderExecutionSettings executionSettings,
+ @Nullable String androidHome) throws ProjectBuildException {
+ GradleConnector connector = getGradleConnector(executionSettings);
+
+ ProjectConnection connection = connector.connect();
+ ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+ ByteArrayOutputStream stderr = new ByteArrayOutputStream();
+
+ try {
+ BuildLauncher launcher = connection.newBuild();
+ launcher.forTasks(buildTasks);
+
+ List<String> jvmArgs = Lists.newArrayList();
+
+ if (androidHome != null && !androidHome.isEmpty()) {
+ String androidSdkArg = AndroidGradleSettings.createAndroidHomeJvmArg(androidHome);
+ jvmArgs.add(androidSdkArg);
+ }
+
+ jvmArgs.addAll(executionSettings.getGradleDaemonVmOptions());
+
+ if (!jvmArgs.isEmpty()) {
+ LOG.info("Passing JVM args to Gradle Tooling API: " + jvmArgs);
+ launcher.setJvmArguments(jvmArgs.toArray(new String[jvmArgs.size()]));
+ }
+
+ if (executionSettings.isParallelBuild()) {
+ LOG.info("Using 'parallel' build option");
+ launcher.withArguments("--parallel");
+ }
+
+ File javaHomeDir = executionSettings.getJavaHomeDir();
+ if (javaHomeDir != null) {
+ launcher.setJavaHome(javaHomeDir);
+ }
+
+ launcher.setStandardOutput(stdout);
+ launcher.setStandardError(stderr);
+ launcher.run();
+ }
+ catch (BuildException e) {
+ handleBuildException(e, context, stderr.toString());
+ }
+ finally {
+ String outText = stdout.toString();
+ context.processMessage(new ProgressMessage(outText, 1.0f));
+ Closeables.closeQuietly(stdout);
+ Closeables.closeQuietly(stderr);
+ connection.close();
+ }
+ }
+
+ @NotNull
+ private static GradleConnector getGradleConnector(@NotNull BuilderExecutionSettings executionSettings) {
+ GradleConnector connector = GradleConnector.newConnector();
+ if (connector instanceof DefaultGradleConnector) {
+ DefaultGradleConnector defaultConnector = (DefaultGradleConnector)connector;
+
+ if (executionSettings.isEmbeddedGradleDaemonEnabled()) {
+ LOG.info("Using Gradle embedded mode.");
+ defaultConnector.embedded(true);
+ }
+
+ defaultConnector.setVerboseLogging(executionSettings.isVerboseLoggingEnabled());
+
+ int daemonMaxIdleTimeInMs = executionSettings.getGradleDaemonMaxIdleTimeInMs();
+ if (daemonMaxIdleTimeInMs > 0) {
+ defaultConnector.daemonMaxIdleTime(daemonMaxIdleTimeInMs, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ connector.forProjectDirectory(executionSettings.getProjectDir());
+
+ File gradleHomeDir = executionSettings.getGradleHomeDir();
+ if (gradleHomeDir != null) {
+ connector.useInstallation(gradleHomeDir);
+ }
+
+ File gradleServiceDir = executionSettings.getGradleServiceDir();
+ if (gradleServiceDir != null) {
+ connector.useGradleUserHomeDir(gradleServiceDir);
+ }
+
+ return connector;
+ }
+
+ /**
+ * Something went wrong while invoking Gradle. Since we cannot distinguish an execution error from compilation errors easily, we first try
+ * to show, in the "Problems" view, compilation errors by parsing the error output. If no errors are found, we show the stack trace in the
+ * "Problems" view. The idea is that we need to somehow inform the user that something went wrong.
+ */
+ private static void handleBuildException(BuildException e, CompileContext context, String stdErr) throws ProjectBuildException {
+ Collection<CompilerMessage> compilerMessages = ERROR_OUTPUT_PARSER.parseErrorOutput(stdErr);
+ if (!compilerMessages.isEmpty()) {
+ for (CompilerMessage message : compilerMessages) {
+ context.processMessage(message);
+ }
+ return;
+ }
+ // There are no error messages to present. Show some feedback indicating that something went wrong.
+ if (!stdErr.isEmpty()) {
+ // Show the contents of stderr as a compiler error.
+ context.processMessage(createCompilerErrorMessage(stdErr));
+ }
+ else {
+ // Since we have nothing else to show, just print the stack trace of the caught exception.
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try {
+ //noinspection IOResourceOpenedButNotSafelyClosed
+ e.printStackTrace(new PrintStream(out));
+ String message = "Internal error:" + SystemProperties.getLineSeparator() + out.toString();
+ context.processMessage(createCompilerErrorMessage(message));
+ }
+ finally {
+ Closeables.closeQuietly(out);
+ }
+ }
+ throw new ProjectBuildException(e.getMessage());
+ }
+
+ @NotNull
+ private static CompilerMessage createCompilerErrorMessage(@NotNull String msg) {
+ return AndroidGradleJps.createCompilerMessage(BuildMessage.Kind.ERROR, msg);
+ }
+
+ @Override
+ @NotNull
+ public String getPresentableName() {
+ return BUILDER_NAME;
+ }
+}
diff --git a/android-gradle-jps/src/com/android/tools/idea/jps/builder/BuilderExecutionSettings.java b/android-gradle-jps/src/com/android/tools/idea/jps/builder/BuilderExecutionSettings.java
index 41f39ce..b23d2af 100644
--- a/android-gradle-jps/src/com/android/tools/idea/jps/builder/BuilderExecutionSettings.java
+++ b/android-gradle-jps/src/com/android/tools/idea/jps/builder/BuilderExecutionSettings.java
@@ -16,11 +16,16 @@
package com.android.tools.idea.jps.builder;
import com.android.tools.idea.gradle.compiler.BuildProcessJvmArgs;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
import com.intellij.util.SystemProperties;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import org.jetbrains.jps.api.GlobalOptions;
import java.io.File;
+import java.util.Collections;
+import java.util.List;
/**
* Settings used to build a Gradle project.
@@ -28,47 +33,51 @@
class BuilderExecutionSettings {
private final boolean myEmbeddedGradleDaemonEnabled;
private final int myGradleDaemonMaxIdleTimeInMs;
- private final int myGradleDaemonMaxMemoryInMb;
+ @NotNull private final List<String> myGradleDaemonVmOptions;
@Nullable private final File myGradleHomeDir;
@Nullable private final File myGradleServiceDir;
+ @Nullable private final File myJavaHomeDir;
@NotNull private final File myProjectDir;
private final boolean myVerboseLoggingEnabled;
+ private final boolean myParallelBuild;
+ private final boolean myGenerateSourceOnly;
BuilderExecutionSettings() {
myEmbeddedGradleDaemonEnabled = SystemProperties.getBooleanProperty(BuildProcessJvmArgs.USE_EMBEDDED_GRADLE_DAEMON, false);
myGradleDaemonMaxIdleTimeInMs = SystemProperties.getIntProperty(BuildProcessJvmArgs.GRADLE_DAEMON_MAX_IDLE_TIME_IN_MS, -1);
- myGradleDaemonMaxMemoryInMb = SystemProperties.getIntProperty(BuildProcessJvmArgs.GRADLE_DAEMON_MAX_MEMORY_IN_MB, 512);
- myGradleHomeDir = findGradleHomeDir();
- myGradleServiceDir = findGradleServiceDir();
+ myGradleDaemonVmOptions = getJvmOptions();
+ myGradleHomeDir = findDir(BuildProcessJvmArgs.GRADLE_HOME_DIR_PATH, "Gradle home");
+ myGradleServiceDir = findDir(BuildProcessJvmArgs.GRADLE_SERVICE_DIR_PATH, "Gradle service");
+ myJavaHomeDir = findDir(BuildProcessJvmArgs.GRADLE_JAVA_HOME_DIR_PATH, "Java home");
myProjectDir = findProjectRootDir();
myVerboseLoggingEnabled = SystemProperties.getBooleanProperty(BuildProcessJvmArgs.USE_GRADLE_VERBOSE_LOGGING, false);
+ myParallelBuild = SystemProperties.getBooleanProperty(GlobalOptions.COMPILE_PARALLEL_OPTION, false);
+ myGenerateSourceOnly = SystemProperties.getBooleanProperty(BuildProcessJvmArgs.GENERATE_SOURCE_ONLY_ON_COMPILE, false);
+ }
+
+ @NotNull
+ private static List<String> getJvmOptions() {
+ int vmOptionCount = SystemProperties.getIntProperty(BuildProcessJvmArgs.GRADLE_DAEMON_VM_OPTION_COUNT, 0);
+ if (vmOptionCount <= 0) {
+ return Collections.emptyList();
+ }
+ List<String> vmOptions = Lists.newArrayList();
+ for (int i = 0; i < vmOptionCount; i++) {
+ String vmOption = System.getProperty(BuildProcessJvmArgs.GRADLE_DAEMON_VM_OPTION_DOT + i);
+ if (!Strings.isNullOrEmpty(vmOption)) {
+ vmOptions.add(vmOption);
+ }
+ }
+ return vmOptions;
}
@Nullable
- private static File findGradleHomeDir() {
- File gradleHomeDir = createFile(BuildProcessJvmArgs.GRADLE_HOME_DIR_PATH);
- if (gradleHomeDir == null) {
- return null;
- }
- if (!gradleHomeDir.isDirectory()) {
- String path = gradleHomeDir.getAbsolutePath();
- String msg = String.format("Unable to obtain Gradle home directory: the path '%1$s' is not a directory", path);
- throw new IllegalArgumentException(msg);
- }
- return gradleHomeDir;
- }
-
- @Nullable
- private static File findGradleServiceDir() {
- File gradleServiceDir = createFile(BuildProcessJvmArgs.GRADLE_SERVICE_DIR_PATH);
+ private static File findDir(@NotNull String jvmArgName, @NotNull String dirType) {
+ File gradleServiceDir = createFile(jvmArgName);
if (gradleServiceDir == null) {
return null;
}
- if (!gradleServiceDir.isDirectory()) {
- String path = gradleServiceDir.getAbsolutePath();
- String msg = String.format("Unable to obtain Gradle service directory: the path '%1$s' is not a directory", path);
- throw new IllegalArgumentException(msg);
- }
+ ensureDirectoryExists(gradleServiceDir, dirType);
return gradleServiceDir;
}
@@ -78,24 +87,22 @@
if (projectRootDir == null) {
throw new NullPointerException("Project directory not specified");
}
- if (!projectRootDir.isDirectory()) {
- String path = projectRootDir.getAbsolutePath();
- String msg = String.format("Unable to obtain the project directory: the path '%1$s' is not a directory", path);
+ ensureDirectoryExists(projectRootDir, "project");
+ return projectRootDir;
+ }
+
+ private static void ensureDirectoryExists(@NotNull File dir, @NotNull String type) {
+ if (!dir.isDirectory()) {
+ String path = dir.getPath();
+ String msg = String.format("Unable to obtain %1$s directory: the file '%2$s' is not a directory", type, path);
throw new IllegalArgumentException(msg);
}
- return projectRootDir;
}
@Nullable
private static File createFile(@NotNull String jvmArgName) {
String path = System.getProperty(jvmArgName);
- if (path == null || path.isEmpty()) {
- return null;
- }
- if (path.startsWith("\"") && path.endsWith("\"")) {
- path = path.substring(1, path.length() - 1);
- }
- return new File(path);
+ return path != null && !path.isEmpty() ? new File(path) : null;
}
boolean isEmbeddedGradleDaemonEnabled() {
@@ -110,8 +117,9 @@
return myGradleDaemonMaxIdleTimeInMs;
}
- int getGradleDaemonMaxMemoryInMb() {
- return myGradleDaemonMaxMemoryInMb;
+ @NotNull
+ List<String> getGradleDaemonVmOptions() {
+ return myGradleDaemonVmOptions;
}
@Nullable
@@ -124,19 +132,35 @@
return myGradleServiceDir;
}
+ @Nullable
+ File getJavaHomeDir() {
+ return myJavaHomeDir;
+ }
+
@NotNull
File getProjectDir() {
return myProjectDir;
}
+ boolean isGenerateSourceOnly() {
+ return myGenerateSourceOnly;
+ }
+
+ boolean isParallelBuild() {
+ return myParallelBuild;
+ }
+
@Override
public String toString() {
return "BuilderExecutionSettings[" +
"embeddedGradleDaemonEnabled=" + myEmbeddedGradleDaemonEnabled +
+ ", generateSourceOnly=" + myGenerateSourceOnly +
", gradleDaemonMaxIdleTimeInMs=" + myGradleDaemonMaxIdleTimeInMs +
- ", gradleDaemonMaxMemoryInMb=" + myGradleDaemonMaxMemoryInMb +
+ ", gradleDaemonVmOptions=" + myGradleDaemonVmOptions +
", gradleHomeDir=" + myGradleHomeDir +
", gradleServiceDir=" + myGradleServiceDir +
+ ", javaHomeDir=" + myJavaHomeDir +
+ ", parallelBuild=" + myParallelBuild +
", projectDir=" + myProjectDir +
", verboseLoggingEnabled=" + myVerboseLoggingEnabled +
']';
diff --git a/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/GradleErrorOutputParser.java b/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/GradleErrorOutputParser.java
index 2cf429f..8eeb505 100644
--- a/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/GradleErrorOutputParser.java
+++ b/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/GradleErrorOutputParser.java
@@ -21,9 +21,10 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.jps.incremental.messages.BuildMessage;
import org.jetbrains.jps.incremental.messages.CompilerMessage;
-import java.util.Collection;
+import java.util.List;
/**
* Parses Gradle's error output and creates error/warning messages when appropriate.
@@ -39,19 +40,24 @@
* output or if an error occurred while parsing the given output.
*/
@NotNull
- public Collection<CompilerMessage> parseErrorOutput(@NotNull String output) {
+ public List<CompilerMessage> parseErrorOutput(@NotNull String output) {
OutputLineReader outputReader = new OutputLineReader(output);
if (outputReader.getLineCount() == 0) {
return ImmutableList.of();
}
- Collection<CompilerMessage> messages = Lists.newArrayList();
+ List<CompilerMessage> messages = Lists.newArrayList();
String line;
while ((line = outputReader.readLine()) != null) {
+ if (line.isEmpty()) {
+ continue;
+ }
+ boolean handled = false;
for (CompilerOutputParser parser : PARSERS) {
try {
if (parser.parse(line, outputReader, messages)) {
+ handled = true;
break;
}
}
@@ -59,7 +65,15 @@
return ImmutableList.of();
}
}
+ if (!handled) {
+ // If none of the standard parsers recognize the input, include it as info such
+ // that users don't miss potentially vital output such as gradle plugin exceptions.
+ // If there is predictable useless input we don't want to appear here, add a custom
+ // parser to digest it.
+ messages.add(new CompilerMessage("", BuildMessage.Kind.INFO, line));
+ }
}
+
return messages;
}
}
diff --git a/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/aapt/AbstractAaptOutputParser.java b/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/aapt/AbstractAaptOutputParser.java
index b05c775..21b4612 100644
--- a/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/aapt/AbstractAaptOutputParser.java
+++ b/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/aapt/AbstractAaptOutputParser.java
@@ -15,12 +15,14 @@
*/
package com.android.tools.idea.jps.output.parser.aapt;
+import com.android.annotations.VisibleForTesting;
import com.android.tools.idea.jps.AndroidGradleJps;
import com.android.tools.idea.jps.output.parser.CompilerOutputParser;
import com.android.tools.idea.jps.output.parser.OutputLineReader;
import com.android.tools.idea.jps.output.parser.ParsingFailedException;
-import com.google.common.collect.Maps;
import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.util.Pair;
+import com.intellij.util.containers.SoftValueHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.incremental.messages.BuildMessage;
@@ -28,13 +30,16 @@
import java.io.File;
import java.io.IOException;
-import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-abstract class AbstractAaptOutputParser implements CompilerOutputParser {
+@VisibleForTesting
+public abstract class AbstractAaptOutputParser implements CompilerOutputParser {
private static final Logger LOG = Logger.getInstance(AbstractAaptOutputParser.class);
+ @VisibleForTesting
+ public static File ourRootDir;
+
/**
* Portion of the error message which states the context in which the error occurred,
* such as which property was being processed and what the string value was that
@@ -73,7 +78,11 @@
*/
private static final Pattern REQUIRED_ATTRIBUTE = Pattern.compile("A '(.+)' attribute is required for <(.+)>");
- @NotNull private final Map<String, ReadOnlyDocument> myDocumentsByPathCache = Maps.newHashMap();
+ private static final String START_MARKER = "<!-- From: "; // Keep in sync with MergedResourceWriter#FILENAME_PREFIX
+ private static final String END_MARKER = " -->";
+
+ @NotNull private static final SoftValueHashMap<String, ReadOnlyDocument> ourDocumentsByPathCache =
+ new SoftValueHashMap<String, ReadOnlyDocument>();
@Nullable
final Matcher getNextLineMatcher(@NotNull OutputLineReader reader, @NotNull Pattern pattern) {
@@ -111,7 +120,7 @@
throw new ParsingFailedException();
}
}
- long lineNumber = -1L;
+ int lineNumber = -1;
if (lineNumberAsText != null) {
try {
lineNumber = Integer.parseInt(lineNumberAsText);
@@ -120,7 +129,19 @@
throw new ParsingFailedException();
}
}
- long column = -1L;
+ int column = -1;
+
+ if (sourcePath != null) {
+ Pair<File,Integer> source = findSourcePosition(file, lineNumber, text);
+ if (source != null) {
+ file = source.getFirst();
+ sourcePath = file.getPath();
+ if (source.getSecond() != null) {
+ lineNumber = source.getSecond();
+ }
+ }
+ }
+
// Attempt to determine the exact range of characters affected by this error.
// This will look up the actual text of the file, go to the particular error line and findText for the specific string mentioned in the
// error.
@@ -135,7 +156,7 @@
}
@Nullable
- private Position findMessagePositionInFile(@NotNull File file, @NotNull String msgText, long locationLine) {
+ private static Position findMessagePositionInFile(@NotNull File file, @NotNull String msgText, int locationLine) {
Matcher matcher = PROPERTY_NAME_AND_VALUE.matcher(msgText);
if (matcher.find()) {
String name = matcher.group(1);
@@ -188,16 +209,16 @@
}
@Nullable
- private Position findText(@NotNull File file, @NotNull String first, @Nullable String second, long locationLine) {
+ private static Position findText(@NotNull File file, @NotNull String first, @Nullable String second, int locationLine) {
ReadOnlyDocument document = getDocument(file);
if (document == null) {
return null;
}
- long offset = document.lineOffset(locationLine);
+ int offset = document.lineOffset(locationLine);
if (offset == -1L) {
return null;
}
- long resultOffset = document.findText(first, offset);
+ int resultOffset = document.findText(first, offset);
if (resultOffset == -1L) {
return null;
}
@@ -207,27 +228,27 @@
return null;
}
}
- long lineNumber = document.lineNumber(resultOffset);
- long lineOffset = document.lineOffset(lineNumber);
+ int lineNumber = document.lineNumber(resultOffset);
+ int lineOffset = document.lineOffset(lineNumber);
return new Position(lineNumber, resultOffset - lineOffset + 1, resultOffset);
}
@Nullable
- private Position findLineStart(@NotNull File file, long locationLine) {
+ private static Position findLineStart(@NotNull File file, int locationLine) {
ReadOnlyDocument document = getDocument(file);
if (document == null) {
return null;
}
- long lineOffset = document.lineOffset(locationLine);
+ int lineOffset = document.lineOffset(locationLine);
if (lineOffset == -1L) {
return null;
}
- long nextLineOffset = document.lineOffset(locationLine + 1);
+ int nextLineOffset = document.lineOffset(locationLine + 1);
if (nextLineOffset == -1) {
nextLineOffset = document.length();
}
- long resultOffset = -1;
- for (long i = lineOffset; i < nextLineOffset; i++) {
+ int resultOffset = -1;
+ for (int i = lineOffset; i < nextLineOffset; i++) {
char c = document.charAt(i);
if (!Character.isWhitespace(c)) {
resultOffset = i;
@@ -241,13 +262,20 @@
}
@Nullable
- private ReadOnlyDocument getDocument(@NotNull File file) {
+ private static ReadOnlyDocument getDocument(@NotNull File file) {
String filePath = file.getAbsolutePath();
- ReadOnlyDocument document = myDocumentsByPathCache.get(filePath);
+ ReadOnlyDocument document = ourDocumentsByPathCache.get(filePath);
if (document == null) {
try {
+ if (!file.exists()) {
+ if (ourRootDir != null && ourRootDir.isAbsolute() && !file.isAbsolute()) {
+ file = new File(ourRootDir, file.getPath());
+ return getDocument(file);
+ }
+ return null;
+ }
document = new ReadOnlyDocument(file);
- myDocumentsByPathCache.put(filePath, document);
+ ourDocumentsByPathCache.put(filePath, document);
}
catch (IOException e) {
String format = "Unexpected error occurred while reading file '%s'";
@@ -258,12 +286,62 @@
return document;
}
- private static class Position {
- final long myLineNumber;
- final long myColumn;
- final long myOffset;
+ @Nullable
+ protected Pair<File,Integer> findSourcePosition(@NotNull File file, int locationLine, String message) {
+ if (!file.getPath().endsWith(".xml")) {
+ return null;
+ }
- Position(long lineNumber, long column, long offset) {
+ ReadOnlyDocument document = getDocument(file);
+ if (document == null) {
+ return null;
+ }
+ // All value files get merged together into a single values file; in that case, we need to
+ // search for comment markers backwards which indicates the source file for the current file
+
+ int searchStart;
+ String fileName = file.getName();
+ boolean isValueFile = fileName.equals("values.xml"); // Keep in sync with MergedResourceWriter.FN_VALUES_XML
+ if (isValueFile) {
+ searchStart = document.lineOffset(locationLine);
+ } else {
+ searchStart = document.length();
+ }
+ if (searchStart == -1L) {
+ return null;
+ }
+
+ int start = document.findTextBackwards(START_MARKER, searchStart);
+ if (start == -1) {
+ return null;
+ }
+ start += START_MARKER.length();
+ int end = document.findText(END_MARKER, start);
+ if (end == -1) {
+ return null;
+ }
+ String sourcePath = document.subsequence(start, end).toString();
+ File sourceFile = new File(sourcePath);
+
+ if (isValueFile) {
+ // Look up the line number
+ locationLine = -1;
+
+ Position position = findMessagePositionInFile(sourceFile, message, 1); // Search from the beginning
+ if (position != null) {
+ locationLine = position.myLineNumber;
+ }
+ }
+
+ return Pair.create(sourceFile, locationLine);
+ }
+
+ private static class Position {
+ final int myLineNumber;
+ final int myColumn;
+ final int myOffset;
+
+ Position(int lineNumber, int column, int offset) {
myLineNumber = lineNumber;
myColumn = column;
myOffset = offset;
diff --git a/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/aapt/ReadOnlyDocument.java b/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/aapt/ReadOnlyDocument.java
index 4354241..d2330cf 100644
--- a/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/aapt/ReadOnlyDocument.java
+++ b/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/aapt/ReadOnlyDocument.java
@@ -17,15 +17,12 @@
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
-import com.google.common.io.Closeables;
+import com.google.common.io.Files;
import com.intellij.util.text.StringSearcher;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
-import java.io.RandomAccessFile;
-import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
import java.util.List;
/**
@@ -33,7 +30,7 @@
*/
class ReadOnlyDocument {
@NotNull private final CharSequence myContents;
- @NotNull private final List<Long> myOffsets;
+ @NotNull private final List<Integer> myOffsets;
/**
* Creates a new {@link ReadOnlyDocument} for the given file.
@@ -43,21 +40,14 @@
*/
@SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
ReadOnlyDocument(@NotNull File file) throws IOException {
- myOffsets = Lists.newArrayList();
- RandomAccessFile raf = null;
- try {
- raf = new RandomAccessFile(file, "r");
- myOffsets.add(raf.getFilePointer());
- while (raf.readLine() != null) {
- myOffsets.add(raf.getFilePointer());
+ myContents = Files.toString(file, Charsets.UTF_8);
+ myOffsets = Lists.newArrayListWithExpectedSize(myContents.length() / 30);
+ myOffsets.add(0);
+ for (int i = 0; i < myContents.length(); i++) {
+ char c = myContents.charAt(i);
+ if (c == '\n') {
+ myOffsets.add(i + 1);
}
- FileChannel channel = raf.getChannel();
- long channelSize = channel.size();
- ByteBuffer byteBuffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channelSize);
- myContents = Charsets.UTF_8.newDecoder().decode(byteBuffer);
- }
- finally {
- Closeables.closeQuietly(raf);
}
}
@@ -68,10 +58,10 @@
* @return the offset of the given line. -1 is returned if the document is empty, or if the given line number is negative or greater than
* the number of lines in the document.
*/
- long lineOffset(long lineNumber) {
- int index = (int)lineNumber - 1;
- if (index <= 0 || index >= myOffsets.size()) {
- return -1L;
+ int lineOffset(int lineNumber) {
+ int index = lineNumber - 1;
+ if (index < 0 || index >= myOffsets.size()) {
+ return -1;
}
return myOffsets.get(index);
}
@@ -83,14 +73,14 @@
* @return the line number of the given offset. -1 is returned if the document is empty or if the offset is greater than the position of
* the last character in the document.
*/
- long lineNumber(long offset) {
+ int lineNumber(int offset) {
for (int i = 0; i < myOffsets.size(); i++) {
- long savedOffset = myOffsets.get(i);
+ int savedOffset = myOffsets.get(i);
if (offset <= savedOffset) {
return i;
}
}
- return -1L;
+ return -1;
}
/**
@@ -100,9 +90,14 @@
* @param offset the starting point of the search.
* @return the offset of the found result, or -1 if no match was found.
*/
- long findText(String text, long offset) {
+ int findText(String text, int offset) {
StringSearcher searcher = new StringSearcher(text, true, true);
- return searcher.scan(myContents, (int)offset, myContents.length());
+ return searcher.scan(myContents, offset, myContents.length());
+ }
+
+ int findTextBackwards(String text, int offset) {
+ StringSearcher searcher = new StringSearcher(text, true, false);
+ return searcher.scan(myContents, offset, myContents.length());
}
/**
@@ -112,14 +107,24 @@
* @return the character at the given offset.
* @throws IndexOutOfBoundsException if the {@code offset} argument is negative or not less than the document's size.
*/
- char charAt(long offset) {
- return myContents.charAt((int)offset);
+ char charAt(int offset) {
+ return myContents.charAt(offset);
+ }
+
+ /**
+ * Returns the sub sequence for the given range.
+ * @param start the starting offset.
+ * @param end the ending offset, or -1 for the end of the file.
+ * @return the sub sequence.
+ */
+ CharSequence subsequence(int start, int end) {
+ return myContents.subSequence(start, end == -1 ? myContents.length() : end);
}
/**
* @return the size (or length) of the document.
*/
- long length() {
+ int length() {
return myContents.length();
}
}
diff --git a/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/androidPlugin/GradleBuildFailureParser.java b/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/androidPlugin/GradleBuildFailureParser.java
index 0ae07df..68bf320 100644
--- a/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/androidPlugin/GradleBuildFailureParser.java
+++ b/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/androidPlugin/GradleBuildFailureParser.java
@@ -19,7 +19,9 @@
import com.android.tools.idea.jps.output.parser.CompilerOutputParser;
import com.android.tools.idea.jps.output.parser.OutputLineReader;
import com.android.tools.idea.jps.output.parser.ParsingFailedException;
+import com.android.tools.idea.jps.output.parser.aapt.AaptOutputParser;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.incremental.messages.BuildMessage;
import org.jetbrains.jps.incremental.messages.CompilerMessage;
@@ -30,21 +32,22 @@
/**
* A parser for Gradle's final error message on failed builds. It is of the form:
*
+ * <pre>
+ * FAILURE: Build failed with an exception.
*
- FAILURE: Build failed with an exception.
-
- * What went wrong:
- Execution failed for task 'TASK_PATH'.
-
- * Where:
- Build file 'PATHNAME' line: LINE_NUM
- > ERROR_MESSAGE
-
- * Try:
- Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
-
- BUILD FAILED
-
+ * * What went wrong:
+ * Execution failed for task 'TASK_PATH'.
+ *
+ * * Where:
+ * Build file 'PATHNAME' line: LINE_NUM
+ * > ERROR_MESSAGE
+ *
+ * * Try:
+ * Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
+ *
+ * BUILD FAILED
+ * </pre>
+ *
* The Where section may not appear (it usually only shows up if there's a problem in the build.gradle file itself). We parse this
* out to get the failure message and module, and the where output if it appears.
*/
@@ -62,13 +65,24 @@
Pattern.compile("^Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.")
};
+ // If there's a failure executing a command-line tool, Gradle will output the complete command line of the tool and will embed the
+ // output from that tool. We catch the command line, pull out the tool being invoked, and then take the output and run it through a
+ // subparser to generate parsed error messages.
+ private static final Pattern COMMAND_FAILURE_MESSAGE = Pattern.compile("^> Failed to run command:");
+ private static final Pattern COMMAND_LINE_PARSER = Pattern.compile("^\\s+/([^/ ]+/)+([^/ ]+) (.*)");
+ private static final Pattern COMMAND_LINE_ERROR_OUTPUT = Pattern.compile("^ Output:$");
+
private enum State {
BEGINNING,
WHERE,
MESSAGE,
+ COMMAND_FAILURE_COMMAND_LINE,
+ COMMAND_FAILURE_OUTPUT,
ENDING
}
+ private AaptOutputParser myAaptParser = new AaptOutputParser();
+
State myState;
@Override
@@ -78,8 +92,13 @@
int pos = 0;
String currentLine = line;
String file = null;
- int lineNum = 0;
+ long lineNum = -1;
+ long column = -1;
+ String lastQuotedLine = null;
StringBuilder errorMessage = new StringBuilder();
+ Matcher matcher;
+ // TODO: If the output isn't quite matching this format (for example, the "Try" statement is missing) this will eat
+ // some of the output. We should fall back to emitting all the output in that case.
while (true) {
switch(myState) {
case BEGINNING:
@@ -92,33 +111,85 @@
}
break;
case WHERE:
- Matcher matcher = WHERE_LINE_2.matcher(currentLine);
+ matcher = WHERE_LINE_2.matcher(currentLine);
if (!matcher.matches()) {
return false;
}
file = matcher.group(1);
lineNum = Integer.parseInt(matcher.group(2));
+ column = 0;
myState = State.BEGINNING;
break;
case MESSAGE:
if (ENDING_PATTERNS[0].matcher(currentLine).matches()) {
myState = State.ENDING;
pos = 1;
+ } else if (COMMAND_FAILURE_MESSAGE.matcher(currentLine).matches()) {
+ myState = State.COMMAND_FAILURE_COMMAND_LINE;
} else {
if (errorMessage.length() > 0) {
errorMessage.append("\n");
}
+ if (isGradleQuotedLine(currentLine)) {
+ lastQuotedLine = currentLine;
+ }
errorMessage.append(currentLine);
}
break;
+ case COMMAND_FAILURE_COMMAND_LINE:
+ // Gradle can put an unescaped "Android Studio" in its command-line output. (It doesn't care because this doesn't have to be
+ // a perfectly valid command line; it's just an error message). To keep it from messing up our parsing, let's convert those
+ // to "Android_Studio". If there are other spaces in the command-line path, though, it will mess up our parsing. Oh, well.
+ currentLine = currentLine.replaceAll("Android Studio", "Android_Studio");
+ matcher = COMMAND_LINE_PARSER.matcher(currentLine);
+ if (matcher.matches()) {
+ String message = String.format("Error while executing %s command", matcher.group(2));
+ messages.add(AndroidGradleJps.createCompilerMessage(BuildMessage.Kind.ERROR, message));
+ } else if (COMMAND_LINE_ERROR_OUTPUT.matcher(currentLine).matches()) {
+ myState = State.COMMAND_FAILURE_OUTPUT;
+ } else if (ENDING_PATTERNS[0].matcher(currentLine).matches()) {
+ myState = State.ENDING;
+ pos = 1;
+ }
+ break;
+ case COMMAND_FAILURE_OUTPUT:
+ if (ENDING_PATTERNS[0].matcher(currentLine).matches()) {
+ myState = State.ENDING;
+ pos = 1;
+ } else {
+ currentLine = currentLine.trim();
+ if (!myAaptParser.parse(currentLine, reader, messages)) {
+ // The AAPT parser punted on it. Just create a message with the unparsed error.
+ messages.add(AndroidGradleJps.createCompilerMessage(BuildMessage.Kind.ERROR, currentLine));
+ }
+ }
+ break;
case ENDING:
if (!ENDING_PATTERNS[pos].matcher(currentLine).matches()) {
return false;
} else if (++pos >= ENDING_PATTERNS.length) {
- if (file != null) {
- messages.add(AndroidGradleJps.createCompilerMessage(BuildMessage.Kind.ERROR, errorMessage.toString(), file, lineNum, 0));
- } else {
- messages.add(AndroidGradleJps.createCompilerMessage(BuildMessage.Kind.ERROR, errorMessage.toString()));
+ if (errorMessage.length() > 0) {
+ // Sometimes Gradle exits with an error message that doesn't have an associated
+ // file. This will show up first in the output, for errors without file associations.
+ // However, in some cases we can guess what the error is by looking at the other error
+ // messages, for example from the XML Validation parser, where the same error message is
+ // provided along with an error message. See for example the parser unit test for
+ // duplicate resources.
+ if (file == null && lastQuotedLine != null) {
+ String msg = unquoteGradleLine(lastQuotedLine);
+ CompilerMessage rootCause = findRootCause(msg, messages);
+ if (rootCause != null) {
+ file = rootCause.getSourcePath();
+ lineNum = rootCause.getLine();
+ column = rootCause.getColumn();
+ }
+ }
+ if (file != null) {
+ messages.add(AndroidGradleJps.createCompilerMessage(BuildMessage.Kind.ERROR, errorMessage.toString(), file, lineNum,
+ column));
+ } else {
+ messages.add(AndroidGradleJps.createCompilerMessage(BuildMessage.Kind.ERROR, errorMessage.toString()));
+ }
}
return true;
}
@@ -135,4 +206,46 @@
}
}
}
+
+ /** Looks through the existing errors and attempts to find one that has the same root cause */
+ @Nullable
+ private static CompilerMessage findRootCause(String text, Collection<CompilerMessage> messages) {
+ for (CompilerMessage message : messages) {
+ if (message.getKind() != BuildMessage.Kind.INFO && message.getMessageText().contains(text)) {
+ String sourcePath = message.getSourcePath();
+ if (sourcePath != null) {
+ return message;
+ }
+ }
+ }
+
+ // We sometimes strip out the exception name prefix in the error messages;
+ // e.g. the gradle output may be "> java.io.IOException: My error message" whereas
+ // the XML validation error message was "My error message", so look for these
+ // scenarios too
+ int index = text.indexOf(':');
+ if (index != -1 && index < text.length() - 1) {
+ return findRootCause(text.substring(index + 1).trim(), messages);
+ }
+
+ return null;
+ }
+
+ private static boolean isGradleQuotedLine(String line) {
+ for (int i = 0, n = line.length() - 1; i < n; i++) {
+ char c = line.charAt(i);
+ if (c == '>') {
+ return line.charAt(i + 1) == ' ';
+ } else if (c != ' ') {
+ break;
+ }
+ }
+
+ return false;
+ }
+
+ private static String unquoteGradleLine(String line) {
+ assert isGradleQuotedLine(line);
+ return line.substring(line.indexOf('>') + 2);
+ }
}
diff --git a/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/androidPlugin/XmlValidationErrorParser.java b/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/androidPlugin/XmlValidationErrorParser.java
index d2d0a63..cbed25f 100644
--- a/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/androidPlugin/XmlValidationErrorParser.java
+++ b/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/androidPlugin/XmlValidationErrorParser.java
@@ -32,8 +32,10 @@
/**
* A parser for errors that occur during XML validation of various resource source files. They are of the form:
*
- [Fatal Error] :LINE:COL: MESSAGE
- Failed to parse PATHNAME
+ * <pre>
+ * [Fatal Error] :LINE:COL: MESSAGE
+ * Failed to parse PATHNAME
+ * </pre>
*
* The second line with the pathname may not appear (which means we can't tell the user what file the error occurred in. Bummer.)
*/
@@ -46,18 +48,35 @@
throws ParsingFailedException {
Matcher m1 = FATAL_ERROR.matcher(line);
if (!m1.matches()) {
+ // Sometimes the parse failure message appears by itself (for example with duplicate resources);
+ // in this case also recognize the line by itself even though it's separated from the next message
+ Matcher m2 = FILE_REFERENCE.matcher(line);
+ if (m2.matches()) {
+ String sourcePath = m2.group(1);
+ if (new File(sourcePath).exists()) {
+ String message = line;
+ // Eat the entire stacktrace
+ String exceptionMessage = digestStackTrace(reader);
+ if (exceptionMessage != null) {
+ message = exceptionMessage + ": " + message;
+ }
+ messages.add(AndroidGradleJps.createCompilerMessage(BuildMessage.Kind.ERROR, message, sourcePath, -1, -1));
+ return true;
+ }
+ }
return false;
}
String message = m1.group(3);
int lineNumber = Integer.parseInt(m1.group(1));
int column = Integer.parseInt(m1.group(2));
String sourcePath = null;
- String nextLine = reader.readLine();
+ String nextLine = reader.peek(0);
if (nextLine == null) {
return false;
}
Matcher m2 = FILE_REFERENCE.matcher(nextLine);
if (m2.matches()) {
+ reader.readLine(); // digest peeked line
sourcePath = m2.group(1);
if (!new File(sourcePath).exists()) {
sourcePath = null;
@@ -66,4 +85,35 @@
messages.add(AndroidGradleJps.createCompilerMessage(BuildMessage.Kind.ERROR, message, sourcePath, lineNumber, column));
return true;
}
+
+ @Nullable
+ private static String digestStackTrace(OutputLineReader reader) {
+ String message = null;
+ String next = reader.peek(0);
+ if (next == null) {
+ return null;
+ }
+ int index = next.indexOf(':');
+ if (index == -1) {
+ return null;
+ }
+
+ String exceptionName = next.substring(0, index);
+ if (exceptionName.endsWith("Exception") || exceptionName.endsWith("Error")) {
+ message = next.substring(index + 1).trim();
+ reader.readLine();
+
+ // Digest stack frames below it
+ while (true) {
+ String peek = reader.peek(0);
+ if (peek != null && peek.startsWith("\t")) {
+ reader.readLine();
+ } else {
+ break;
+ }
+ }
+ }
+
+ return message;
+ }
}
diff --git a/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/javac/JavacOutputParser.java b/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/javac/JavacOutputParser.java
index 952e0e5..e3ffa37 100644
--- a/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/javac/JavacOutputParser.java
+++ b/android-gradle-jps/src/com/android/tools/idea/jps/output/parser/javac/JavacOutputParser.java
@@ -60,6 +60,7 @@
}
if (part1.equalsIgnoreCase("javac")) {
messages.add(createErrorMessage(line));
+ return true;
}
int colonIndex2 = line.indexOf(COLON, colonIndex1 + 1);
@@ -128,10 +129,13 @@
}
}
}
- if(line.endsWith("java.lang.OutOfMemoryError")) {
+
+ if (line.endsWith("java.lang.OutOfMemoryError")) {
messages.add(createErrorMessage("Out of memory."));
+ return true;
}
- return true;
+
+ return false;
}
@NotNull
diff --git a/android-gradle-jps/testSrc/com/android/tools/idea/jps/builder/BuilderExecutionSettingsTest.java b/android-gradle-jps/testSrc/com/android/tools/idea/jps/builder/BuilderExecutionSettingsTest.java
index ab795f3..53db34d 100644
--- a/android-gradle-jps/testSrc/com/android/tools/idea/jps/builder/BuilderExecutionSettingsTest.java
+++ b/android-gradle-jps/testSrc/com/android/tools/idea/jps/builder/BuilderExecutionSettingsTest.java
@@ -23,6 +23,7 @@
import org.jetbrains.annotations.NotNull;
import java.io.File;
+import java.util.List;
/**
* Tests for {@link BuilderExecutionSettings}.
@@ -30,6 +31,7 @@
public class BuilderExecutionSettingsTest extends TestCase {
private File myGradleHomeDir;
private File myGradleServiceDir;
+ private File myJavaHomeDir;
private File myProjectDir;
@Override
@@ -38,6 +40,7 @@
File tempDir = Files.createTempDir();
myGradleHomeDir = createDirectory(tempDir, "gradle-1.6");
myGradleServiceDir = createDirectory(tempDir, "gradle");
+ myJavaHomeDir = createDirectory(tempDir, "java");
myProjectDir = createDirectory(tempDir, "project1");
}
@@ -52,11 +55,12 @@
protected void tearDown() throws Exception {
delete(myGradleHomeDir);
delete(myGradleServiceDir);
+ delete(myJavaHomeDir);
delete(myProjectDir);
super.tearDown();
}
- private void delete(@Nullable File dir) {
+ private static void delete(@Nullable File dir) {
if (dir != null) {
dir.delete();
}
@@ -64,32 +68,48 @@
public void testConstructorWithValidVmArgs() {
System.setProperty(BuildProcessJvmArgs.GRADLE_DAEMON_MAX_IDLE_TIME_IN_MS, "55");
- System.setProperty(BuildProcessJvmArgs.GRADLE_DAEMON_MAX_MEMORY_IN_MB, "1024");
- String gradleHomeDirPath = myGradleHomeDir.getAbsolutePath();
+ String gradleHomeDirPath = myGradleHomeDir.getPath();
System.setProperty(BuildProcessJvmArgs.GRADLE_HOME_DIR_PATH, gradleHomeDirPath);
- String gradleHomeServicePath = myGradleServiceDir.getAbsolutePath();
+ String gradleHomeServicePath = myGradleServiceDir.getPath();
System.setProperty(BuildProcessJvmArgs.GRADLE_SERVICE_DIR_PATH, gradleHomeServicePath);
- String projectDirPath = myProjectDir.getAbsolutePath();
+ String javaHomePath = myJavaHomeDir.getPath();
+ System.setProperty(BuildProcessJvmArgs.GRADLE_JAVA_HOME_DIR_PATH, javaHomePath);
+
+ String projectDirPath = myProjectDir.getPath();
System.setProperty(BuildProcessJvmArgs.PROJECT_DIR_PATH, projectDirPath);
System.setProperty(BuildProcessJvmArgs.USE_EMBEDDED_GRADLE_DAEMON, "true");
System.setProperty(BuildProcessJvmArgs.USE_GRADLE_VERBOSE_LOGGING, "true");
+ System.setProperty(BuildProcessJvmArgs.GRADLE_DAEMON_VM_OPTION_COUNT, "2");
+
+ String xmx = "-Xmx2048m";
+ System.setProperty(BuildProcessJvmArgs.GRADLE_DAEMON_VM_OPTION_DOT + 0, xmx);
+
+ String maxPermSize = "-XX:MaxPermSize=512m";
+ System.setProperty(BuildProcessJvmArgs.GRADLE_DAEMON_VM_OPTION_DOT + 1, maxPermSize);
+
+
BuilderExecutionSettings settings = new BuilderExecutionSettings();
assertEquals(55, settings.getGradleDaemonMaxIdleTimeInMs());
- assertEquals(1024, settings.getGradleDaemonMaxMemoryInMb());
assertEquals(gradleHomeDirPath, pathOf(settings.getGradleHomeDir()));
assertEquals(gradleHomeServicePath, pathOf(settings.getGradleServiceDir()));
- assertEquals(projectDirPath, settings.getProjectDir().getAbsolutePath());
+ assertEquals(javaHomePath, pathOf(settings.getJavaHomeDir()));
+ assertEquals(projectDirPath, settings.getProjectDir().getPath());
assertTrue(settings.isEmbeddedGradleDaemonEnabled());
assertTrue(settings.isVerboseLoggingEnabled());
+
+ List<String> vmOptions = settings.getGradleDaemonVmOptions();
+ assertEquals(2, vmOptions.size());
+ assertEquals(xmx, vmOptions.get(0));
+ assertEquals(maxPermSize, vmOptions.get(1));
}
- private static String pathOf(@NotNull File dir) {
+ private static String pathOf(@Nullable File dir) {
assertNotNull(dir);
- return dir.getAbsolutePath();
+ return dir.getPath();
}
}
diff --git a/android-gradle-jps/testSrc/com/android/tools/idea/jps/output/parser/GradleErrorOutputParserTest.java b/android-gradle-jps/testSrc/com/android/tools/idea/jps/output/parser/GradleErrorOutputParserTest.java
index 6cbd8d4..a46dab6 100644
--- a/android-gradle-jps/testSrc/com/android/tools/idea/jps/output/parser/GradleErrorOutputParserTest.java
+++ b/android-gradle-jps/testSrc/com/android/tools/idea/jps/output/parser/GradleErrorOutputParserTest.java
@@ -15,8 +15,12 @@
*/
package com.android.tools.idea.jps.output.parser;
+import com.android.tools.idea.jps.output.parser.aapt.AbstractAaptOutputParser;
import com.google.common.io.Closeables;
+import com.google.common.io.Files;
+import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.SystemProperties;
import com.intellij.util.containers.ContainerUtil;
import junit.framework.TestCase;
@@ -28,10 +32,13 @@
import java.io.FileWriter;
import java.io.IOException;
import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
/**
* Tests for {@link GradleErrorOutputParser}.
*/
+@SuppressWarnings({"ResultOfMethodCallIgnored", "StringBufferReplaceableByString"})
public class GradleErrorOutputParserTest extends TestCase {
private static final String NEWLINE = SystemProperties.getLineSeparator();
@@ -269,17 +276,25 @@
NEWLINE).append(NEWLINE);
err.append("BUILD FAILED").append(NEWLINE).append(NEWLINE);
err.append("Total time: 18.303 secs\n");
- Collection<CompilerMessage> messages = parser.parseErrorOutput(err.toString());
- assertHasCorrectErrorMessage(messages, "A problem occurred evaluating project ':project'." + NEWLINE +
- "> Could not find method ERROR() for arguments [{plugin=android}] on project ':project'.", 9, 0);
+ List<CompilerMessage> messages = parser.parseErrorOutput(err.toString());
+
+ assertEquals("0: Gradle:Error:A problem occurred evaluating project ':project'.\n" +
+ "> Could not find method ERROR() for arguments [{plugin=android}] on project ':project'.\n" +
+ "\t" + sourceFilePath + ":9:0\n" +
+ "1: Info:BUILD FAILED\n" +
+ "2: Info:Total time: 18.303 secs\n",
+ toString(messages));
}
public void testParseXmlValidationErrorOutput() {
StringBuilder err = new StringBuilder();
err.append("[Fatal Error] :5:7: The element type \"error\" must be terminated by the matching end-tag \"</error>\".").append(NEWLINE);
err.append("FAILURE: Build failed with an exception.").append(NEWLINE);
- Collection<CompilerMessage> messages = parser.parseErrorOutput(err.toString());
- assertHasCorrectErrorMessage(messages, "The element type \"error\" must be terminated by the matching end-tag \"</error>\".", 5, 7);
+ List<CompilerMessage> messages = parser.parseErrorOutput(err.toString());
+
+ assertEquals("0: Gradle:Error:The element type \"error\" must be terminated by the matching end-tag \"</error>\".\n" +
+ "1: Info:FAILURE: Build failed with an exception.\n",
+ toString(messages));
}
private void createTempFile(String fileExtension) throws IOException {
@@ -308,10 +323,453 @@
long expectedColumn) {
assertEquals("[message count]", 1, messages.size());
CompilerMessage message = ContainerUtil.getFirstItem(messages);
+ assertNotNull(message);
assertEquals("[file path]", sourceFilePath, message.getSourcePath());
assertEquals("[message severity]", BuildMessage.Kind.ERROR, message.getKind());
assertEquals("[message text]", expectedText, message.getMessageText());
assertEquals("[position line]", expectedLine, message.getLine());
assertEquals("[position column]", expectedColumn, message.getColumn());
}
+
+ private static String toString(List<CompilerMessage> messages) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0, n = messages.size(); i < n; i++) {
+ CompilerMessage message = messages.get(i);
+ sb.append(Integer.toString(i)).append(':').append(' ');
+ if (!message.getCompilerName().isEmpty()) {
+ sb.append(message.getCompilerName()).append(':');
+ }
+ sb.append(StringUtil.capitalize(message.getKind().toString().toLowerCase(Locale.US))).append(':'); // INFO => Info
+ sb.append(message.getMessageText());
+ if (message.getSourcePath() != null) {
+ sb.append('\n');
+ sb.append('\t');
+ sb.append(message.getSourcePath());
+ sb.append(':').append(Long.toString(message.getLine()));
+ sb.append(':').append(Long.toString(message.getColumn()));
+ }
+ sb.append('\n');
+ }
+
+ return sb.toString();
+ }
+
+ public void testRedirectValueLinksOutput() throws Exception {
+ String homePath = PathManager.getHomePath();
+ assertNotNull(homePath);
+ // The relative paths in the output file below is relative to the sdk-common directory in tools/base
+ // (it's from one of the unit tests there)
+ AbstractAaptOutputParser.ourRootDir = new File(homePath, ".." + File.separator + "base" + File.separator + "sdk-common");
+
+ // Need file to be named (exactly) values.xml
+ File tempDir = Files.createTempDir();
+ File valueDir = new File(tempDir, "values-en");
+ valueDir.mkdirs();
+ sourceFile = new File(valueDir, "values.xml"); // Keep in sync with MergedResourceWriter.FN_VALUES_XML
+ sourceFilePath = sourceFile.getAbsolutePath();
+
+ writeToFile(
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
+ "<resources xmlns:ns1=\"urn:oasis:names:tc:xliff:document:1.2\">\n" +
+ "\n" +
+ " <!-- From: src/test/resources/testData/resources/baseSet/values/values.xml -->\n" +
+ " <string-array name=\"string_array\" translatable=\"false\">\n" +
+ " <item/> <!-- 0 -->\n" +
+ " <item/> <!-- 1 -->\n" +
+ " <item>ABC</item> <!-- 2 -->\n" +
+ " <item>DEF</item> <!-- 3 -->\n" +
+ " <item>GHI</item> <!-- 4 -->\n" +
+ " <item>JKL</item> <!-- 5 -->\n" +
+ " <item>MNO</item> <!-- 6 -->\n" +
+ " <item>PQRS</item> <!-- 7 -->\n" +
+ " <item>TUV</item> <!-- 8 -->\n" +
+ " <item>WXYZ</item> <!-- 9 -->\n" +
+ " </string-array>\n" +
+ "\n" +
+ " <attr name=\"dimen_attr\" format=\"dimension\" />\n" +
+ " <attr name=\"enum_attr\">\n" +
+ " <enum name=\"normal\" value=\"0\" />\n" +
+ " <enum name=\"sans\" value=\"1\" />\n" +
+ " <enum name=\"serif\" value=\"2\" />\n" +
+ " <enum name=\"monospace\" value=\"3\" />\n" +
+ " </attr>\n" +
+ " <attr name=\"flag_attr\">\n" +
+ " <flag name=\"normal\" value=\"0\" />\n" +
+ " <flag name=\"bold\" value=\"1\" />\n" +
+ " <flag name=\"italic\" value=\"2\" />\n" +
+ " </attr>\n" +
+ " <attr name=\"string_attr\" format=\"string\" />\n" +
+ " <!-- From: src/test/resources/testData/resources/baseMerge/overlay/values/values.xml -->\n" +
+ " <color name=\"color\">#FFFFFFFF</color>\n" +
+ " <!-- From: src/test/resources/testData/resources/baseSet/values/values.xml -->\n" +
+ " <declare-styleable name=\"declare_styleable\">\n" +
+ "\n" +
+ " <!-- ============== -->\n" +
+ " <!-- Generic styles -->\n" +
+ " <!-- ============== -->\n" +
+ " <eat-comment />\n" +
+ "\n" +
+ " <!-- Default color of foreground imagery. -->\n" +
+ " <attr name=\"blah\" format=\"color\" />\n" +
+ " <!-- Default color of foreground imagery on an inverted background. -->\n" +
+ " <attr name=\"android:colorForegroundInverse\" />\n" +
+ " </declare-styleable>\n" +
+ "\n" +
+ " <dimen name=\"dimen\">164dp</dimen>\n" +
+ "\n" +
+ " <drawable name=\"color_drawable\">#ffffffff</drawable>\n" +
+ " <drawable name=\"drawable_ref\">@drawable/stat_notify_sync_anim0</drawable>\n" +
+ "\n" +
+ " <item name=\"item_id\" type=\"id\"/>\n" +
+ "\n" +
+ " <integer name=\"integer\">75</integer>\n" +
+ " <!-- From: src/test/resources/testData/resources/baseMerge/overlay/values/values.xml -->\n" +
+ " <item name=\"file_replaced_by_alias\" type=\"layout\">@layout/ref</item>\n" +
+ " <!-- From: src/test/resources/testData/resources/baseSet/values/values.xml -->\n" +
+ " <item name=\"layout_ref\" type=\"layout\">@layout/ref</item>\n" +
+ " <!-- From: src/test/resources/testData/resources/baseMerge/overlay/values/values.xml -->\n" +
+ " <string name=\"basic_string\">overlay_string</string>\n" +
+ " <!-- From: src/test/resources/testData/resources/baseSet/values/values.xml -->\n" +
+ " <string name=\"styled_string\">Forgot your username or password\\?\\nVisit <b>google.com/accounts/recovery</b>.</string>\n" +
+ " <string name=\"xliff_string\"><ns1:g id=\"number\" example=\"123\">%1$s</ns1:g><ns1:g id=\"unit\" example=\"KB\">%2$s</ns1:g></string>\n" +
+ "\n" +
+ " <style name=\"style\" parent=\"@android:style/Holo.Light\">\n" +
+ " <item name=\"android:singleLine\">true</item>\n" +
+ " <item name=\"android:textAppearance\">@style/TextAppearance.WindowTitle</item>\n" +
+ " <item name=\"android:shadowColor\">#BB000000</item>\n" +
+ " <item name=\"android:shadowRadius\">2.75</item>\n" +
+ " <item name=\"foo\">foo</item>\n" +
+ " </style>\n" +
+ "\n" +
+ "</resources>\n");
+
+ String messageText = "String types not allowed (at 'drawable_ref' with value '@drawable/stat_notify_sync_anim0').";
+ String err = sourceFilePath + ":46: error: Error: " + messageText;
+ Collection<CompilerMessage> messages = parser.parseErrorOutput(err);
+ assertEquals(1, messages.size());
+
+ assertEquals("[message count]", 1, messages.size());
+ CompilerMessage message = ContainerUtil.getFirstItem(messages);
+ assertNotNull(message);
+
+ // NOT sourceFilePath; should be translated back from source comment
+ assertEquals("[file path]", "src/test/resources/testData/resources/baseSet/values/values.xml", message.getSourcePath());
+
+ assertEquals("[message severity]", BuildMessage.Kind.ERROR, message.getKind());
+ assertEquals("[message text]", messageText, message.getMessageText());
+ assertEquals("[position line]", 9, message.getLine());
+ assertEquals("[position column]", 35, message.getColumn());
+ }
+
+ public void testRedirectFileLinksOutput() throws Exception {
+ String homePath = PathManager.getHomePath();
+ assertNotNull(homePath);
+ // The relative paths in the output file below is relative to the sdk-common directory in tools/base
+ // (it's from one of the unit tests there)
+ AbstractAaptOutputParser.ourRootDir = new File(homePath, ".." + File.separator + "base" + File.separator + "sdk-common");
+
+ // Need file to be named (exactly) values.xml
+ File tempDir = Files.createTempDir();
+ File layoutDir = new File(tempDir, "layout-land");
+ layoutDir.mkdirs();
+ sourceFile = new File(layoutDir, "main.xml");
+ sourceFilePath = sourceFile.getAbsolutePath();
+
+ writeToFile(
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
+ "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
+ " android:orientation=\"vertical\"\n" +
+ " android:layout_width=\"fill_parent\"\n" +
+ " android:layout_height=\"fill_parent\"\n" +
+ " >\n" +
+ "<TextView\n" +
+ " android:layout_width=\"fill_parent\"\n" +
+ " android:layout_height=\"wrap_content\"\n" +
+ " android:text=\"Test App - Basic\"\n" +
+ " android:id=\"@+id/text\"\n" +
+ " />\n" +
+ "</LinearLayout>\n" +
+ "\n" +
+ "<!-- From: src/test/resources/testData/resources/incMergeData/filesVsValues/main/layout/main.xml -->");
+
+ String messageText = "Random error message here";
+ String err = sourceFilePath + ":4: error: Error: " + messageText;
+ Collection<CompilerMessage> messages = parser.parseErrorOutput(err);
+ assertEquals(1, messages.size());
+
+ assertEquals("[message count]", 1, messages.size());
+ CompilerMessage message = ContainerUtil.getFirstItem(messages);
+ assertNotNull(message);
+
+ // NOT sourceFilePath; should be translated back from source comment
+ assertEquals("[file path]", "src/test/resources/testData/resources/incMergeData/filesVsValues/main/layout/main.xml", message.getSourcePath());
+
+ assertEquals("[message severity]", BuildMessage.Kind.ERROR, message.getKind());
+ assertEquals("[message text]", messageText, message.getMessageText());
+ assertEquals("[position line]", 4, message.getLine());
+ //assertEquals("[position column]", 35, message.getColumn());
+
+ // TODO: Test encoding issues (e.g. & in path where the XML source comment would be & instead)
+ }
+
+ public void testGradleAaptErrorParser() throws IOException {
+ createTempXmlFile();
+ writeToFile("<resources xmlns:android='http://schemas.android.com/apk/res/android'>",
+ " <TextView\n" +
+ " android:layout_width=\"fill_parent\"\n" +
+ " android:layout_height=\"wrap_content\"\n" +
+ " android:text=\"@string/does_not_exist\"\n" +
+ " android:id=\"@+id/text\"\n" +
+ " />\n");
+ final String messageText = "No resource found that matches the given name (at 'text' with value '@string/does_not_exist').";
+ String err = "FAILURE: Build failed with an exception.\n" +
+ "\n" +
+ "* What went wrong:\n" +
+ "Execution failed for task ':five:processDebugResources'.\n" +
+ "> Failed to run command:\n" +
+ " \t/Applications/Android Studio.app/sdk/build-tools/android-4.2.2/aapt package -f --no-crunch -I /Applications/Android Studio.app/sdk/platforms/android-17/android.jar -M /Users/sbarta/AndroidStudioProjects/fiveProject/five/build/manifests/debug/AndroidManifest.xml -S /Users/sbarta/AndroidStudioProjects/fiveProject/five/build/res/all/debug -A /Users/sbarta/AndroidStudioProjects/fiveProject/five/build/assets/debug -m -J /Users/sbarta/AndroidStudioProjects/fiveProject/five/build/source/r/debug -F /Users/sbarta/AndroidStudioProjects/fiveProject/five/build/libs/five-debug.ap_ --debug-mode --custom-package com.example.five\n" +
+ " Error Code:\n" +
+ " \t1\n" +
+ " Output:\n" +
+ " \t" + sourceFilePath + ":5: error: Error: " + messageText + "\n" +
+ "\n" +
+ "\n" +
+ "* Try:\n" +
+ "Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.\n" +
+ "\n" +
+ "BUILD FAILED\n";
+
+ assertEquals("0: Gradle:Error:Error while executing aapt command\n" +
+ "1: Gradle:Error:No resource found that matches the given name (at 'text' with value '@string/does_not_exist').\n" +
+ "\t" + sourceFilePath + ":5:27\n" +
+ "2: Gradle:Error:Execution failed for task ':five:processDebugResources'.\n" +
+ "3: Info:BUILD FAILED\n",
+ toString(parser.parseErrorOutput(err)));
+ }
+
+ public void testDuplicateResources() throws Exception {
+ // To reproduce, create a source file with two duplicate string item definitions
+ createTempXmlFile();
+ String output =
+ "Failed to parse " + sourceFilePath + "\n" +
+ "java.io.IOException: Found item String/drawer_open more than one time\n" +
+ "\tat com.android.ide.common.res2.ValueResourceParser2.checkDuplicate(ValueResourceParser2.java:249)\n" +
+ "\tat com.android.ide.common.res2.ValueResourceParser2.parseFile(ValueResourceParser2.java:103)\n" +
+ "\tat com.android.ide.common.res2.ResourceSet.createResourceFile(ResourceSet.java:273)\n" +
+ "\tat com.android.ide.common.res2.ResourceSet.parseFolder(ResourceSet.java:248)\n" +
+ // ...
+ "\tat org.gradle.launcher.daemon.server.DefaultIncomingConnectionHandler$ConnectionWorker.run(DefaultIncomingConnectionHandler.java:116)\n" +
+ "\tat org.gradle.internal.concurrent.DefaultExecutorFactory$StoppableExecutorImpl$1.run(DefaultExecutorFactory.java:66)\n" +
+ "\tat java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895)\n" +
+ "\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)\n" +
+ "\tat java.lang.Thread.run(Thread.java:680)\n" +
+ "\n" +
+ "FAILURE: Build failed with an exception.\n" +
+ "\n" +
+ "* What went wrong:\n" +
+ "Execution failed for task ':MyApp:mergeDebugResources'.\n" +
+ "> Found item String/drawer_open more than one time\n" +
+ "\n" +
+ "* Try:\n" +
+ "Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.\n";
+
+ assertEquals("0: Gradle:Error:Found item String/drawer_open more than one time: Failed to parse " + sourceFilePath + "\n" +
+ "\t" + sourceFilePath + ":-1:-1\n" +
+ "1: Gradle:Error:Execution failed for task ':MyApp:mergeDebugResources'.\n" +
+ "> Found item String/drawer_open more than one time\n" +
+ "\t" + sourceFilePath + ":-1:-1\n",
+ toString(parser.parseErrorOutput(output)));
+
+ // Also test CRLF handling:
+ output = output.replace("\n", "\r\n");
+ assertEquals("0: Gradle:Error:Found item String/drawer_open more than one time: Failed to parse " + sourceFilePath + "\n" +
+ "\t" + sourceFilePath + ":-1:-1\n" +
+ "1: Gradle:Error:Execution failed for task ':MyApp:mergeDebugResources'.\n" +
+ "> Found item String/drawer_open more than one time\n" +
+ "\t" + sourceFilePath + ":-1:-1\n",
+ toString(parser.parseErrorOutput(output)));
+ }
+
+ public void testUnexpectedOutput() throws Exception {
+ // To reproduce, create a source file with two duplicate string item definitions
+ createTempXmlFile();
+ String output =
+ "This output is not expected.\n" +
+ "Nor is this.\n" +
+ "java.io.SurpriseSurpriseError: Bet you didn't expect to see this\n" +
+ "\tat com.android.ide.common.res2.ValueResourceParser2.checkSurpriseSurprise(ValueResourceParser2.java:249)\n" +
+ "\tat com.android.ide.common.res2.ValueResourceParser2.parseFile(ValueResourceParser2.java:103)\n" +
+ "\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)\n" +
+ "\tat java.lang.Thread.run(Thread.java:680)\n" +
+ "More unexpected output.\n" +
+ "\n" +
+ "FAILURE: Build failed with an exception.\n" +
+ "\n" +
+ "* What went wrong:\n" +
+ "Execution failed for task ':MyApp:mergeDebugResources'.\n" +
+ "> I was surprised\n" +
+ "\n" +
+ "* Try:\n" +
+ "Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.\n";
+
+ assertEquals("0: Info:This output is not expected.\n" +
+ "1: Info:Nor is this.\n" +
+ "2: Info:java.io.SurpriseSurpriseError: Bet you didn't expect to see this\n" +
+ "3: Info:\tat com.android.ide.common.res2.ValueResourceParser2.checkSurpriseSurprise(ValueResourceParser2.java:249)\n" +
+ "4: Info:\tat com.android.ide.common.res2.ValueResourceParser2.parseFile(ValueResourceParser2.java:103)\n" +
+ "5: Info:\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)\n" +
+ "6: Info:\tat java.lang.Thread.run(Thread.java:680)\n" +
+ "7: Info:More unexpected output.\n" +
+ "8: Gradle:Error:Execution failed for task ':MyApp:mergeDebugResources'.\n" +
+ "> I was surprised\n",
+ toString(parser.parseErrorOutput(output)));
+ }
+
+ public void testXmlError() throws Exception {
+ createTempXmlFile();
+ String output =
+ "[Fatal Error] :7:18: Open quote is expected for attribute \"{1}\" associated with an element type \"name\".\n" +
+ "Failed to parse " + sourceFilePath + "\n" +
+ "java.io.IOException: org.xml.sax.SAXParseException: Open quote is expected for attribute \"{1}\" associated with an element type \"name\".\n" +
+ "\tat com.android.ide.common.res2.ValueResourceParser2.parseDocument(ValueResourceParser2.java:193)\n" +
+ "\tat com.android.ide.common.res2.ValueResourceParser2.parseFile(ValueResourceParser2.java:78)\n" +
+ "\tat com.android.ide.common.res2.ResourceSet.createResourceFile(ResourceSet.java:273)\n" +
+ "\tat com.android.ide.common.res2.ResourceSet.parseFolder(ResourceSet.java:248)\n" +
+ // ...
+ "\tat org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:130)\n" +
+ "\tat org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)\n" +
+ "Caused by: org.xml.sax.SAXParseException: Open quote is expected for attribute \"{1}\" associated with an element type \"name\".\n" +
+ "\tat com.sun.org.apache.xerces.internal.parsers.DOMParser.parse(DOMParser.java:246)\n" +
+ "\tat com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:284)\n" +
+ "\tat com.android.ide.common.res2.ValueResourceParser2.parseDocument(ValueResourceParser2.java:189)\n" +
+ "\t... 109 more\n" +
+ ":MyApp:mergeDebugResources FAILED\n" +
+ "\n" +
+ "FAILURE: Build failed with an exception.\n" +
+ "\n" +
+ "* What went wrong:\n" +
+ "Execution failed for task ':MyApp:mergeDebugResources'.\n" +
+ "> org.xml.sax.SAXParseException: Open quote is expected for attribute \"{1}\" associated with an element type \"name\".\n" +
+ "\n" +
+ "* Try:\n" +
+ "Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.\n" +
+ "\n" +
+ "BUILD FAILED\n" +
+ "\n" +
+ "Total time: 7.245 secs\n";
+
+ assertEquals("0: Gradle:Error:Open quote is expected for attribute \"{1}\" associated with an element type \"name\".\n" +
+ "\t" + sourceFilePath + ":7:18\n" +
+ "1: Info:java.io.IOException: org.xml.sax.SAXParseException: Open quote is expected for attribute \"{1}\" associated with an element type \"name\".\n" +
+ "2: Info:\tat com.android.ide.common.res2.ValueResourceParser2.parseDocument(ValueResourceParser2.java:193)\n" +
+ "3: Info:\tat com.android.ide.common.res2.ValueResourceParser2.parseFile(ValueResourceParser2.java:78)\n" +
+ "4: Info:\tat com.android.ide.common.res2.ResourceSet.createResourceFile(ResourceSet.java:273)\n" +
+ "5: Info:\tat com.android.ide.common.res2.ResourceSet.parseFolder(ResourceSet.java:248)\n" +
+ "6: Info:\tat org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:130)\n" +
+ "7: Info:\tat org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)\n" +
+ "8: Gradle:Error:: org.xml.sax.SAXParseException: Open quote is expected for attribute \"{1}\" associated with an element type \"name\".\n" +
+ "9: Info:\tat com.sun.org.apache.xerces.internal.parsers.DOMParser.parse(DOMParser.java:246)\n" +
+ "10: Info:\tat com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:284)\n" +
+ "11: Info:\tat com.android.ide.common.res2.ValueResourceParser2.parseDocument(ValueResourceParser2.java:189)\n" +
+ "12: Info:\t... 109 more\n" +
+ "13: Info::MyApp:mergeDebugResources FAILED\n" +
+ "14: Gradle:Error:Execution failed for task ':MyApp:mergeDebugResources'.\n" +
+ "> org.xml.sax.SAXParseException: Open quote is expected for attribute \"{1}\" associated with an element type \"name\".\n" +
+ "\t" + sourceFilePath + ":7:18\n" +
+ "15: Info:BUILD FAILED\n" +
+ "16: Info:Total time: 7.245 secs\n",
+ toString(parser.parseErrorOutput(output)));
+ }
+
+ public void testJavac() throws Exception {
+ createTempFile(".java");
+ String output =
+ sourceFilePath + ":70: <identifier> expected\n" +
+ "x\n" +
+ " ^\n" +
+ sourceFilePath + ":71: <identifier> expected\n" +
+ " @Override\n" +
+ " ^\n" +
+ "2 errors\n" +
+ ":MyApp:compileDebug FAILED\n" +
+ "\n" +
+ "FAILURE: Build failed with an exception.\n" +
+ "\n" +
+ "* What went wrong:\n" +
+ "Execution failed for task ':MyApp:compileDebug'.\n" +
+ "> Compilation failed; see the compiler error output for details.\n" +
+ "\n" +
+ "* Try:\n" +
+ "Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.\n" +
+ "\n" +
+ "BUILD FAILED\n" +
+ "\n" +
+ "Total time: 12.42 secs";
+
+ assertEquals("0: Gradle:Error:<identifier> expected\n" +
+ "\t" + sourceFilePath + ":70:2\n" +
+ "1: Gradle:Error:<identifier> expected\n" +
+ "\t" + sourceFilePath + ":71:14\n" +
+ "2: Info:2 errors\n" +
+ "3: Info::MyApp:compileDebug FAILED\n" +
+ "4: Gradle:Error:Execution failed for task ':MyApp:compileDebug'.\n" +
+ "> Compilation failed; see the compiler error output for details.\n" +
+ "5: Info:BUILD FAILED\n" +
+ "6: Info:Total time: 12.42 secs\n",
+ toString(parser.parseErrorOutput(output)));
+ }
+
+ public void testOom() throws Exception {
+ createTempFile(".java");
+ String output =
+ // Came across this output on StackOverflow:
+ "FAILURE: Build failed with an exception.\n" +
+ "* What went wrong:\n" +
+ "A problem occurred configuring project ':EpicMix'.\n" +
+ "> Failed to notify project evaluation listener.\n" +
+ " > A problem occurred configuring project ':facebook'.\n" +
+ " > Failed to notify project evaluation listener.\n" +
+ " > java.lang.OutOfMemoryError: PermGen space" +
+ "\n" +
+ "* Try:\n" +
+ "Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.\n" +
+ "\n" +
+ "BUILD FAILED\n" +
+ "\n" +
+ "Total time: 24.154 secs";
+
+ assertEquals("0: Gradle:Error:A problem occurred configuring project ':EpicMix'.\n" +
+ "> Failed to notify project evaluation listener.\n" +
+ " > A problem occurred configuring project ':facebook'.\n" +
+ " > Failed to notify project evaluation listener.\n" +
+ " > java.lang.OutOfMemoryError: PermGen space\n" +
+ "1: Info:BUILD FAILED\n" +
+ "2: Info:Total time: 24.154 secs\n",
+ toString(parser.parseErrorOutput(output)));
+
+ String output2 =
+ "To honour the JVM settings for this build a new JVM will be forked. Please consider using the daemon: http://gradle.org/docs/1.7/userguide/gradle_daemon.html.\n" +
+ "\n" +
+ "FAILURE: Build failed with an exception.\n" +
+ "\n" +
+ "* What went wrong:\n" +
+ "A problem occurred configuring project ':MyNewApp'.\n" +
+ "> Failed to notify project evaluation listener.\n" +
+ " > java.lang.OutOfMemoryError: Java heap space\n" +
+ "\n" +
+ "* Try:\n" +
+ "Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.\n" +
+ "\n" +
+ "BUILD FAILED\n" +
+ "\n" +
+ "Total time: 24.154 secs";
+
+ assertEquals("0: Info:To honour the JVM settings for this build a new JVM will be forked. Please consider using the daemon: http://gradle.org/docs/1.7/userguide/gradle_daemon.html.\n" +
+ "1: Gradle:Error:A problem occurred configuring project ':MyNewApp'.\n" +
+ "> Failed to notify project evaluation listener.\n" +
+ " > java.lang.OutOfMemoryError: Java heap space\n" +
+ "2: Info:BUILD FAILED\n" +
+ "3: Info:Total time: 24.154 secs\n",
+ toString(parser.parseErrorOutput(output2)));
+ }
}
diff --git a/android/android.iml b/android/android.iml
index a551444..d4d9206 100755
--- a/android/android.iml
+++ b/android/android.iml
@@ -55,22 +55,20 @@
<orderEntry type="module-library">
<library name="android-gradle-model">
<CLASSES>
- <root url="jar://$MODULE_DIR$/lib/builder-0.4-SNAPSHOT.jar!/" />
- <root url="jar://$MODULE_DIR$/lib/builder-model-0.4-SNAPSHOT.jar!/" />
- <root url="jar://$MODULE_DIR$/lib/gradle-0.4-SNAPSHOT.jar!/" />
- <root url="jar://$MODULE_DIR$/lib/gradle-model-0.4-SNAPSHOT.jar!/" />
+ <root url="jar://$MODULE_DIR$/lib/builder-0.5.0-SNAPSHOT.jar!/" />
+ <root url="jar://$MODULE_DIR$/lib/builder-model-0.5.0-SNAPSHOT.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
- <root url="jar://$MODULE_DIR$/lib/src/builder-0.4-SNAPSHOT-sources.jar!/" />
- <root url="jar://$MODULE_DIR$/lib/src/builder-model-0.4-SNAPSHOT-sources.jar!/" />
- <root url="jar://$MODULE_DIR$/lib/src/gradle-0.4-SNAPSHOT-sources.jar!/" />
- <root url="jar://$MODULE_DIR$/lib/src/gradle-model-0.4-SNAPSHOT-sources.jar!/" />
+ <root url="jar://$MODULE_DIR$/lib/src/builder-0.5.0-SNAPSHOT-sources.jar!/" />
+ <root url="jar://$MODULE_DIR$/lib/src/builder-model-0.5.0-SNAPSHOT-sources.jar!/" />
</SOURCES>
</library>
</orderEntry>
<orderEntry type="module" module-name="external-system-impl" />
<orderEntry type="library" name="gson" level="project" />
+ <orderEntry type="module" module-name="jetgroovy" />
+ <orderEntry type="module" module-name="perflib" />
</component>
</module>
diff --git a/android/common/src/com/android/tools/idea/gradle/util/AndroidGradleSettings.java b/android/common/src/com/android/tools/idea/gradle/util/AndroidGradleSettings.java
new file mode 100644
index 0000000..45715f5
--- /dev/null
+++ b/android/common/src/com/android/tools/idea/gradle/util/AndroidGradleSettings.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.util;
+
+import com.android.SdkConstants;
+import com.google.common.base.Strings;
+import com.google.common.io.Closeables;
+import com.intellij.openapi.diagnostic.Logger;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Properties;
+
+/**
+ * Utility methods related to Gradle-specific Android settings.
+ */
+public final class AndroidGradleSettings {
+ private static final Logger LOG = Logger.getInstance(AndroidGradleSettings.class);
+
+ @NonNls private static final String JVM_ARG_FORMAT = "-D%1$s=%2$s";
+ @NonNls private static final String ANDROID_HOME_JVM_ARG = "android.home";
+
+ private AndroidGradleSettings() {
+ }
+
+ /**
+ * Indicates whether the path of the Android SDK home directory is specified in a local.properties file.
+ *
+ * @param projectDir the project directory.
+ * @return {@code true} if the Android SDK home directory is specified in the project's local.properties.
+ */
+ public static boolean isAndroidSdkDirInLocalPropertiesFile(@NotNull File projectDir) {
+ String androidHome = getAndroidHomeFromLocalPropertiesFile(projectDir);
+ if (!Strings.isNullOrEmpty(androidHome)) {
+ String msg = String.format("Found Android SDK home at '%1$s' (from local.properties file)", androidHome);
+ LOG.info(msg);
+ return true;
+ }
+ return false;
+ }
+
+ @Nullable
+ private static String getAndroidHomeFromLocalPropertiesFile(@NotNull File projectDir) {
+ File filePath = new File(projectDir, SdkConstants.FN_LOCAL_PROPERTIES);
+ if (!filePath.isFile()) {
+ return null;
+ }
+ Properties properties = new Properties();
+ FileInputStream fileInputStream = null;
+ try {
+ //noinspection IOResourceOpenedButNotSafelyClosed
+ fileInputStream = new FileInputStream(filePath);
+ properties.load(fileInputStream);
+ } catch (FileNotFoundException e) {
+ return null;
+ } catch (IOException e) {
+ String msg = String.format("Failed to read file '%1$s'", filePath.getPath());
+ LOG.error(msg, e);
+ return null;
+ } finally {
+ Closeables.closeQuietly(fileInputStream);
+ }
+ return properties.getProperty(SdkConstants.SDK_DIR_PROPERTY);
+ }
+
+ @NotNull
+ public static String createAndroidHomeJvmArg(@NotNull String androidHome) {
+ return createJvmArg(ANDROID_HOME_JVM_ARG, androidHome);
+ }
+
+ @NotNull
+ public static String createJvmArg(@NotNull String name, @NotNull String value) {
+ return String.format(JVM_ARG_FORMAT, name, value);
+ }
+}
diff --git a/android/common/src/org/jetbrains/android/sdk/MessageBuildingSdkLog.java b/android/common/src/org/jetbrains/android/sdk/MessageBuildingSdkLog.java
index 003f8b9..65bf9b4 100644
--- a/android/common/src/org/jetbrains/android/sdk/MessageBuildingSdkLog.java
+++ b/android/common/src/org/jetbrains/android/sdk/MessageBuildingSdkLog.java
@@ -42,8 +42,13 @@
public void verbose(@NonNull String msgFormat, Object... args) {
}
+ @Override
public void error(Throwable t, String errorFormat, Object... args) {
if (t != null) {
+ String message = t.getMessage();
+ if (message != null) {
+ builder.append(message).append('\n');
+ }
LOG.info(t);
}
if (errorFormat != null) {
diff --git a/android/common/src/org/jetbrains/android/util/AndroidCommonUtils.java b/android/common/src/org/jetbrains/android/util/AndroidCommonUtils.java
index 4e96156..858c86e 100644
--- a/android/common/src/org/jetbrains/android/util/AndroidCommonUtils.java
+++ b/android/common/src/org/jetbrains/android/util/AndroidCommonUtils.java
@@ -115,6 +115,9 @@
@NonNls public static final String LIBRARY_PACKAGING_BUILD_TARGET_ID = "android-library-packaging";
@NonNls public static final String AUTOGENERATED_JAVA_FILE_HEADER = "/*___Generated_by_IDEA___*/";
+ /** Android Test Run Configuration Type Id, defined here so as to be accessible to both JPS and Android plugin. */
+ @NonNls public static final String ANDROID_TEST_RUN_CONFIGURATION_TYPE = "AndroidTestRunConfigurationType";
+
private AndroidCommonUtils() {
}
@@ -122,6 +125,10 @@
return ArrayUtil.find(TEST_CONFIGURATION_TYPE_IDS, typeId) >= 0;
}
+ public static boolean isInstrumentationTestConfiguration(@NotNull String typeId) {
+ return ANDROID_TEST_RUN_CONFIGURATION_TYPE.equals(typeId);
+ }
+
public static String command2string(@NotNull Collection<String> command) {
final StringBuilder builder = new StringBuilder();
for (Iterator<String> it = command.iterator(); it.hasNext(); ) {
@@ -150,14 +157,7 @@
return null;
}
- // safety from errors inside sdklib
- try {
- return SdkManager.createManager(path + File.separatorChar, log);
- }
- catch (Exception e) {
- LOG.error(e);
- return null;
- }
+ return SdkManager.createManager(path + File.separatorChar, log);
}
public static void moveAllFiles(@NotNull File from, @NotNull File to, @NotNull Collection<File> newFiles) throws IOException {
diff --git a/android/common/src/org/jetbrains/jps/android/model/impl/JpsAndroidModuleProperties.java b/android/common/src/org/jetbrains/jps/android/model/impl/JpsAndroidModuleProperties.java
index af6a02a..67b61cb 100644
--- a/android/common/src/org/jetbrains/jps/android/model/impl/JpsAndroidModuleProperties.java
+++ b/android/common/src/org/jetbrains/jps/android/model/impl/JpsAndroidModuleProperties.java
@@ -29,6 +29,10 @@
public class JpsAndroidModuleProperties {
public String SELECTED_BUILD_VARIANT = "";
public String ASSEMBLE_TASK_NAME = "";
+ public String ASSEMBLE_TEST_TASK_NAME = "";
+ public String SOURCE_GEN_TASK_NAME = "";
+
+ // This value is false when the Android project is Gradle-based.
public boolean ALLOW_USER_CONFIGURATION = true;
public String GEN_FOLDER_RELATIVE_PATH_APT = "/" + SdkConstants.FD_GEN_SOURCES;
@@ -37,6 +41,7 @@
public String MANIFEST_FILE_RELATIVE_PATH = "/" + SdkConstants.FN_ANDROID_MANIFEST_XML;
public String RES_FOLDER_RELATIVE_PATH = "/" + SdkConstants.FD_RES;
+ public String RES_FOLDERS_RELATIVE_PATH;
public String ASSETS_FOLDER_RELATIVE_PATH = "/" + SdkConstants.FD_ASSETS;
public String LIBS_FOLDER_RELATIVE_PATH = "/" + SdkConstants.FD_NATIVE_LIBS;
diff --git a/android/jps-plugin/src/com/android/tools/idea/jps/AndroidTargetBuilder.java b/android/jps-plugin/src/com/android/tools/idea/jps/AndroidTargetBuilder.java
new file mode 100644
index 0000000..d23d6ec
--- /dev/null
+++ b/android/jps-plugin/src/com/android/tools/idea/jps/AndroidTargetBuilder.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.jps;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.jps.android.AndroidSourceGeneratingBuilder;
+import org.jetbrains.jps.builders.*;
+import org.jetbrains.jps.incremental.CompileContext;
+import org.jetbrains.jps.incremental.ProjectBuildException;
+import org.jetbrains.jps.incremental.TargetBuilder;
+
+import java.io.IOException;
+import java.util.Collection;
+
+public abstract class AndroidTargetBuilder<R extends BuildRootDescriptor, T extends BuildTarget<R>> extends TargetBuilder<R, T> {
+ protected AndroidTargetBuilder(Collection<? extends BuildTargetType<? extends T>> buildTargetTypes) {
+ super(buildTargetTypes);
+ }
+
+ @Override
+ public final void build(@NotNull T target,
+ @NotNull DirtyFilesHolder<R, T> holder,
+ @NotNull BuildOutputConsumer outputConsumer,
+ @NotNull CompileContext context) throws ProjectBuildException, IOException {
+ if (AndroidSourceGeneratingBuilder.IS_ENABLED.get(context, true)) {
+ // Only build targets for non-Gradle Android project.
+ buildTarget(target, holder, outputConsumer, context);
+ }
+ }
+
+ protected abstract void buildTarget(@NotNull T target,
+ @NotNull DirtyFilesHolder<R, T> holder,
+ @NotNull BuildOutputConsumer outputConsumer,
+ @NotNull CompileContext context) throws ProjectBuildException, IOException;
+}
diff --git a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidDexBuilder.java b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidDexBuilder.java
index c2bd104..943f4be 100644
--- a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidDexBuilder.java
+++ b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidDexBuilder.java
@@ -16,6 +16,7 @@
package org.jetbrains.jps.android;
import com.android.sdklib.BuildToolInfo;
+import com.android.tools.idea.jps.AndroidTargetBuilder;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
@@ -60,7 +61,7 @@
/**
* @author Eugene.Kudelevsky
*/
-public class AndroidDexBuilder extends TargetBuilder<BuildRootDescriptor, AndroidDexBuildTarget> {
+public class AndroidDexBuilder extends AndroidTargetBuilder<BuildRootDescriptor, AndroidDexBuildTarget> {
private static final Logger LOG = Logger.getInstance("#org.jetbrains.jps.android.AndroidDexBuilder");
@NonNls private static final String DEX_BUILDER_NAME = "Android Dex";
@NonNls private static final String PRO_GUARD_BUILDER_NAME = "ProGuard";
@@ -70,13 +71,10 @@
}
@Override
- public void build(@NotNull final AndroidDexBuildTarget buildTarget,
- @NotNull DirtyFilesHolder<BuildRootDescriptor, AndroidDexBuildTarget> holder,
- @NotNull BuildOutputConsumer outputConsumer,
- @NotNull CompileContext context) throws ProjectBuildException, IOException {
- if (!AndroidSourceGeneratingBuilder.IS_ENABLED.get(context, true)) {
- return;
- }
+ protected void buildTarget(@NotNull final AndroidDexBuildTarget buildTarget,
+ @NotNull DirtyFilesHolder<BuildRootDescriptor, AndroidDexBuildTarget> holder,
+ @NotNull BuildOutputConsumer outputConsumer,
+ @NotNull CompileContext context) throws ProjectBuildException, IOException {
assert !AndroidJpsUtil.isLightBuild(context);
try {
diff --git a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidJpsUtil.java b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidJpsUtil.java
index 3c6b402..304bab0 100644
--- a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidJpsUtil.java
+++ b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidJpsUtil.java
@@ -84,8 +84,7 @@
*/
public static boolean shouldProcessDependenciesRecursively(JpsModule module) {
return JpsJavaDependenciesEnumerationHandler.shouldProcessDependenciesRecursively(
- JpsJavaDependenciesEnumerationHandler.createHandlers(
- Collections.singletonList(module)));
+ JpsJavaDependenciesEnumerationHandler.createHandlers(Collections.singletonList(module)));
}
@Nullable
@@ -413,29 +412,10 @@
}
}
- public static boolean containsAndroidFacet(@NotNull ModuleChunk chunk) {
- for (JpsModule module : chunk.getModules()) {
- if (getExtension(module) != null) {
- return true;
- }
- }
- return false;
- }
-
- public static boolean containsAndroidFacet(@NotNull JpsProject project) {
- for (JpsModule module : project.getModules()) {
- if (getExtension(module) != null) {
- return true;
- }
- }
- return false;
- }
-
public static ModuleLevelBuilder.ExitCode handleException(@NotNull CompileContext context,
@NotNull Exception e,
@NotNull String builderName,
@Nullable Logger logger) throws ProjectBuildException {
-
if (logger != null) {
logger.info(e);
}
@@ -537,6 +517,11 @@
return typeId != null && AndroidCommonUtils.isTestConfiguration(typeId);
}
+ public static boolean isInstrumentationTestContext(@NotNull CompileContext context) {
+ final String typeId = getRunConfigurationTypeId(context);
+ return typeId != null && AndroidCommonUtils.isInstrumentationTestConfiguration(typeId);
+ }
+
@Nullable
public static String getRunConfigurationTypeId(@NotNull CompileContext context) {
return context.getBuilderParameter("RUN_CONFIGURATION_TYPE_ID");
@@ -825,4 +810,38 @@
}
return null;
}
+
+ /**
+ * Indicates whether the given project is a non-Gradle Android project.
+ *
+ * @param project the given project.
+ * @return {@code true} if the the given project is a non-Gradle Android project, {@code false} otherwise.
+ */
+ public static boolean isAndroidProjectWithoutGradleFacet(@NotNull JpsProject project) {
+ return isAndroidProjectWithoutGradleFacet(project.getModules());
+ }
+
+ /**
+ * Indicates whether the given modules belong to a non-Gradle Android project.
+ *
+ * @param chunk the given modules.
+ * @return {@code true} if the the given modules belong to a non-Gradle Android project, {@code false} otherwise.
+ */
+ public static boolean isAndroidProjectWithoutGradleFacet(@NotNull ModuleChunk chunk) {
+ return isAndroidProjectWithoutGradleFacet(chunk.getModules());
+ }
+
+ private static boolean isAndroidProjectWithoutGradleFacet(@NotNull Collection<JpsModule> modules) {
+ boolean hasAndroidFacet = false;
+ for (JpsModule module : modules) {
+ JpsAndroidModuleExtension androidFacet = getExtension(module);
+ if (androidFacet != null) {
+ hasAndroidFacet = true;
+ if (androidFacet.isGradleProject()) {
+ return false;
+ }
+ }
+ }
+ return hasAndroidFacet;
+ }
}
diff --git a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidLibraryPackagingBuilder.java b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidLibraryPackagingBuilder.java
index 629239e..3c5c180 100644
--- a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidLibraryPackagingBuilder.java
+++ b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidLibraryPackagingBuilder.java
@@ -1,5 +1,6 @@
package org.jetbrains.jps.android;
+import com.android.tools.idea.jps.AndroidTargetBuilder;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.HashSet;
import org.jetbrains.android.util.AndroidBuildTestingManager;
@@ -28,7 +29,7 @@
/**
* @author Eugene.Kudelevsky
*/
-public class AndroidLibraryPackagingBuilder extends TargetBuilder<BuildRootDescriptor, AndroidLibraryPackagingTarget> {
+public class AndroidLibraryPackagingBuilder extends AndroidTargetBuilder<BuildRootDescriptor, AndroidLibraryPackagingTarget> {
@NonNls private static final String BUILDER_NAME = "Android Library Packaging";
protected AndroidLibraryPackagingBuilder() {
@@ -36,13 +37,10 @@
}
@Override
- public void build(@NotNull AndroidLibraryPackagingTarget target,
- @NotNull DirtyFilesHolder<BuildRootDescriptor, AndroidLibraryPackagingTarget> holder,
- @NotNull BuildOutputConsumer outputConsumer,
- @NotNull CompileContext context) throws ProjectBuildException, IOException {
- if (!AndroidSourceGeneratingBuilder.IS_ENABLED.get(context, true)) {
- return;
- }
+ protected void buildTarget(@NotNull AndroidLibraryPackagingTarget target,
+ @NotNull DirtyFilesHolder<BuildRootDescriptor, AndroidLibraryPackagingTarget> holder,
+ @NotNull BuildOutputConsumer outputConsumer,
+ @NotNull CompileContext context) throws ProjectBuildException, IOException {
if (!holder.hasDirtyFiles() && !holder.hasRemovedFiles()) {
return;
}
diff --git a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidManifestMergingBuilder.java b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidManifestMergingBuilder.java
index 9ac0a41..72ddc05 100644
--- a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidManifestMergingBuilder.java
+++ b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidManifestMergingBuilder.java
@@ -9,6 +9,7 @@
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkManager;
+import com.android.tools.idea.jps.AndroidTargetBuilder;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.io.FileUtil;
import org.jetbrains.android.util.AndroidBuildTestingManager;
@@ -34,7 +35,7 @@
* @author Eugene.Kudelevsky
*/
public class AndroidManifestMergingBuilder
- extends TargetBuilder<AndroidManifestMergingTarget.MyRootDescriptor, AndroidManifestMergingTarget> {
+ extends AndroidTargetBuilder<AndroidManifestMergingTarget.MyRootDescriptor, AndroidManifestMergingTarget> {
private static final Logger LOG = Logger.getInstance("#org.jetbrains.jps.android.AndroidManifestMergingBuilder");
private static final String BUILDER_NAME = "Android Manifest Merger";
@@ -44,12 +45,11 @@
}
@Override
- public void build(@NotNull AndroidManifestMergingTarget target,
- @NotNull DirtyFilesHolder<AndroidManifestMergingTarget.MyRootDescriptor, AndroidManifestMergingTarget> holder,
- @NotNull BuildOutputConsumer outputConsumer,
- @NotNull CompileContext context) throws ProjectBuildException, IOException {
- if (!AndroidSourceGeneratingBuilder.IS_ENABLED.get(context, true) ||
- !holder.hasDirtyFiles() && !holder.hasRemovedFiles()) {
+ protected void buildTarget(@NotNull AndroidManifestMergingTarget target,
+ @NotNull DirtyFilesHolder<AndroidManifestMergingTarget.MyRootDescriptor, AndroidManifestMergingTarget> holder,
+ @NotNull BuildOutputConsumer outputConsumer,
+ @NotNull CompileContext context) throws ProjectBuildException, IOException {
+ if (!holder.hasDirtyFiles() && !holder.hasRemovedFiles()) {
return;
}
diff --git a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidManifestMergingTarget.java b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidManifestMergingTarget.java
index d53f547..abde90c 100644
--- a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidManifestMergingTarget.java
+++ b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidManifestMergingTarget.java
@@ -122,7 +122,7 @@
@NotNull
@Override
public List<AndroidManifestMergingTarget> computeAllTargets(@NotNull JpsModel model) {
- if (!AndroidJpsUtil.containsAndroidFacet(model.getProject())) {
+ if (!AndroidJpsUtil.isAndroidProjectWithoutGradleFacet(model.getProject())) {
return Collections.emptyList();
}
final List<AndroidManifestMergingTarget> targets = new ArrayList<AndroidManifestMergingTarget>();
diff --git a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidPackagingBuilder.java b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidPackagingBuilder.java
index c1ef6db..f4f871b 100644
--- a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidPackagingBuilder.java
+++ b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidPackagingBuilder.java
@@ -1,5 +1,6 @@
package org.jetbrains.jps.android;
+import com.android.tools.idea.jps.AndroidTargetBuilder;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.io.FileUtil;
@@ -40,7 +41,7 @@
/**
* @author Eugene.Kudelevsky
*/
-public class AndroidPackagingBuilder extends TargetBuilder<BuildRootDescriptor, AndroidPackagingBuildTarget> {
+public class AndroidPackagingBuilder extends AndroidTargetBuilder<BuildRootDescriptor, AndroidPackagingBuildTarget> {
private static final Logger LOG = Logger.getInstance("#org.jetbrains.jps.android.AndroidPackagingBuilder");
private static final String BUILDER_NAME = "Android Packager";
@@ -56,11 +57,11 @@
}
@Override
- public void build(@NotNull AndroidPackagingBuildTarget target,
- @NotNull DirtyFilesHolder<BuildRootDescriptor, AndroidPackagingBuildTarget> holder,
- @NotNull BuildOutputConsumer outputConsumer,
- @NotNull CompileContext context) throws ProjectBuildException, IOException {
- if (!AndroidSourceGeneratingBuilder.IS_ENABLED.get(context, true) || AndroidJpsUtil.isLightBuild(context)) {
+ protected void buildTarget(@NotNull AndroidPackagingBuildTarget target,
+ @NotNull DirtyFilesHolder<BuildRootDescriptor, AndroidPackagingBuildTarget> holder,
+ @NotNull BuildOutputConsumer outputConsumer,
+ @NotNull CompileContext context) throws ProjectBuildException, IOException {
+ if (AndroidJpsUtil.isLightBuild(context)) {
return;
}
final boolean hasDirtyFiles = holder.hasDirtyFiles() || holder.hasRemovedFiles();
diff --git a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidPreDexBuilder.java b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidPreDexBuilder.java
index 19a4624..753b7e9 100644
--- a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidPreDexBuilder.java
+++ b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidPreDexBuilder.java
@@ -1,5 +1,6 @@
package org.jetbrains.jps.android;
+import com.android.tools.idea.jps.AndroidTargetBuilder;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
@@ -14,7 +15,6 @@
import org.jetbrains.jps.incremental.CompileContext;
import org.jetbrains.jps.incremental.ProjectBuildException;
import org.jetbrains.jps.incremental.StopBuildException;
-import org.jetbrains.jps.incremental.TargetBuilder;
import org.jetbrains.jps.incremental.messages.BuildMessage;
import org.jetbrains.jps.incremental.messages.CompilerMessage;
import org.jetbrains.jps.incremental.messages.ProgressMessage;
@@ -30,7 +30,7 @@
/**
* @author Eugene.Kudelevsky
*/
-public class AndroidPreDexBuilder extends TargetBuilder<AndroidPreDexBuildTarget.MyRootDescriptor, AndroidPreDexBuildTarget> {
+public class AndroidPreDexBuilder extends AndroidTargetBuilder<AndroidPreDexBuildTarget.MyRootDescriptor, AndroidPreDexBuildTarget> {
@NonNls private static final String BUILDER_NAME = "Android Pre Dex";
@@ -54,10 +54,10 @@
}
@Override
- public void build(@NotNull AndroidPreDexBuildTarget target,
- @NotNull DirtyFilesHolder<AndroidPreDexBuildTarget.MyRootDescriptor, AndroidPreDexBuildTarget> holder,
- @NotNull BuildOutputConsumer outputConsumer,
- @NotNull final CompileContext context) throws ProjectBuildException, IOException {
+ protected void buildTarget(@NotNull AndroidPreDexBuildTarget target,
+ @NotNull DirtyFilesHolder<AndroidPreDexBuildTarget.MyRootDescriptor, AndroidPreDexBuildTarget> holder,
+ @NotNull BuildOutputConsumer outputConsumer,
+ @NotNull final CompileContext context) throws ProjectBuildException, IOException {
if (!doBuild(target, holder, outputConsumer, context)) {
throw new StopBuildException();
}
diff --git a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidResourceCachingBuilder.java b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidResourceCachingBuilder.java
index 680c563..7857cb6 100644
--- a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidResourceCachingBuilder.java
+++ b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidResourceCachingBuilder.java
@@ -1,6 +1,7 @@
package org.jetbrains.jps.android;
import com.android.sdklib.IAndroidTarget;
+import com.android.tools.idea.jps.AndroidTargetBuilder;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.util.Processor;
import com.intellij.util.containers.HashMap;
@@ -16,7 +17,6 @@
import org.jetbrains.jps.incremental.CompileContext;
import org.jetbrains.jps.incremental.ProjectBuildException;
import org.jetbrains.jps.incremental.StopBuildException;
-import org.jetbrains.jps.incremental.TargetBuilder;
import org.jetbrains.jps.incremental.messages.BuildMessage;
import org.jetbrains.jps.incremental.messages.CompilerMessage;
import org.jetbrains.jps.model.module.JpsModule;
@@ -30,7 +30,7 @@
/**
* @author Eugene.Kudelevsky
*/
-public class AndroidResourceCachingBuilder extends TargetBuilder<BuildRootDescriptor, AndroidResourceCachingBuildTarget> {
+public class AndroidResourceCachingBuilder extends AndroidTargetBuilder<BuildRootDescriptor, AndroidResourceCachingBuildTarget> {
@NonNls private static final String BUILDER_NAME = "Android Resource Caching";
protected AndroidResourceCachingBuilder() {
@@ -38,13 +38,11 @@
}
@Override
- public void build(@NotNull AndroidResourceCachingBuildTarget target,
- @NotNull DirtyFilesHolder<BuildRootDescriptor, AndroidResourceCachingBuildTarget> holder,
- @NotNull BuildOutputConsumer outputConsumer,
- @NotNull CompileContext context) throws ProjectBuildException, IOException {
- if (!AndroidSourceGeneratingBuilder.IS_ENABLED.get(context, true) ||
- AndroidJpsUtil.isLightBuild(context) ||
- (!holder.hasDirtyFiles() && !holder.hasRemovedFiles())) {
+ protected void buildTarget(@NotNull AndroidResourceCachingBuildTarget target,
+ @NotNull DirtyFilesHolder<BuildRootDescriptor, AndroidResourceCachingBuildTarget> holder,
+ @NotNull BuildOutputConsumer outputConsumer,
+ @NotNull CompileContext context) throws ProjectBuildException, IOException {
+ if (AndroidJpsUtil.isLightBuild(context) || (!holder.hasDirtyFiles() && !holder.hasRemovedFiles())) {
return;
}
diff --git a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidResourcePackagingBuilder.java b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidResourcePackagingBuilder.java
index a6f8007..20f08f1 100644
--- a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidResourcePackagingBuilder.java
+++ b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidResourcePackagingBuilder.java
@@ -1,6 +1,7 @@
package org.jetbrains.jps.android;
import com.android.sdklib.IAndroidTarget;
+import com.android.tools.idea.jps.AndroidTargetBuilder;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.PathUtilRt;
@@ -18,7 +19,6 @@
import org.jetbrains.jps.incremental.CompileContext;
import org.jetbrains.jps.incremental.ProjectBuildException;
import org.jetbrains.jps.incremental.StopBuildException;
-import org.jetbrains.jps.incremental.TargetBuilder;
import org.jetbrains.jps.incremental.messages.BuildMessage;
import org.jetbrains.jps.incremental.messages.CompilerMessage;
import org.jetbrains.jps.incremental.messages.ProgressMessage;
@@ -36,7 +36,7 @@
/**
* @author Eugene.Kudelevsky
*/
-public class AndroidResourcePackagingBuilder extends TargetBuilder<BuildRootDescriptor, AndroidResourcePackagingBuildTarget> {
+public class AndroidResourcePackagingBuilder extends AndroidTargetBuilder<BuildRootDescriptor, AndroidResourcePackagingBuildTarget> {
@NonNls private static final String BUILDER_NAME = "Android Resource Packaging";
protected AndroidResourcePackagingBuilder() {
@@ -44,13 +44,10 @@
}
@Override
- public void build(@NotNull AndroidResourcePackagingBuildTarget target,
- @NotNull DirtyFilesHolder<BuildRootDescriptor, AndroidResourcePackagingBuildTarget> holder,
- @NotNull BuildOutputConsumer outputConsumer,
- @NotNull CompileContext context) throws ProjectBuildException, IOException {
- if (!AndroidSourceGeneratingBuilder.IS_ENABLED.get(context, true)) {
- return;
- }
+ protected void buildTarget(@NotNull AndroidResourcePackagingBuildTarget target,
+ @NotNull DirtyFilesHolder<BuildRootDescriptor, AndroidResourcePackagingBuildTarget> holder,
+ @NotNull BuildOutputConsumer outputConsumer,
+ @NotNull CompileContext context) throws ProjectBuildException, IOException {
final boolean releaseBuild = AndroidJpsUtil.isReleaseBuild(context);
final AndroidPackagingStateStorage packagingStateStorage =
context.getProjectDescriptor().dataManager.getStorage(target, AndroidPackagingStateStorage.Provider.INSTANCE);
diff --git a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidSourceGeneratingBuilder.java b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidSourceGeneratingBuilder.java
index 39b8453..828f8da 100644
--- a/android/jps-plugin/src/org/jetbrains/jps/android/AndroidSourceGeneratingBuilder.java
+++ b/android/jps-plugin/src/org/jetbrains/jps/android/AndroidSourceGeneratingBuilder.java
@@ -81,6 +81,7 @@
private static final int MIN_SDK_TOOLS_REVISION = 19;
public static final Key<Boolean> IS_ENABLED = Key.create("_android_source_generator_enabled_");
+
@NonNls private static final String R_TXT_OUTPUT_DIR_NAME = "r_txt";
public AndroidSourceGeneratingBuilder() {
@@ -102,7 +103,7 @@
ModuleChunk chunk,
DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
OutputConsumer outputConsumer) throws ProjectBuildException {
- if (!IS_ENABLED.get(context, Boolean.TRUE) || chunk.containsTests() || !AndroidJpsUtil.containsAndroidFacet(chunk)) {
+ if (!IS_ENABLED.get(context, Boolean.TRUE) || chunk.containsTests() || !AndroidJpsUtil.isAndroidProjectWithoutGradleFacet(chunk)) {
return ExitCode.NOTHING_DONE;
}
diff --git a/android/jps-plugin/src/org/jetbrains/jps/android/builder/AndroidBuildTarget.java b/android/jps-plugin/src/org/jetbrains/jps/android/builder/AndroidBuildTarget.java
index caaa64f..3c7674d 100644
--- a/android/jps-plugin/src/org/jetbrains/jps/android/builder/AndroidBuildTarget.java
+++ b/android/jps-plugin/src/org/jetbrains/jps/android/builder/AndroidBuildTarget.java
@@ -65,8 +65,7 @@
JpsAndroidModuleExtension extension = AndroidJpsUtil.getExtension(myModule);
assert extension != null;
if (extension.isGradleProject()) {
- List<BuildRootDescriptor> noDescriptors = Collections.emptyList();
- return noDescriptors;
+ return Collections.emptyList();
}
return doComputeRootDescriptors(model, index, ignoredFileIndex, dataPaths);
}
diff --git a/android/jps-plugin/src/org/jetbrains/jps/android/builder/AndroidBuildTargetType.java b/android/jps-plugin/src/org/jetbrains/jps/android/builder/AndroidBuildTargetType.java
index dc0bc79..fb3df58 100644
--- a/android/jps-plugin/src/org/jetbrains/jps/android/builder/AndroidBuildTargetType.java
+++ b/android/jps-plugin/src/org/jetbrains/jps/android/builder/AndroidBuildTargetType.java
@@ -34,7 +34,7 @@
@NotNull
@Override
public List<T> computeAllTargets(@NotNull JpsModel model) {
- if (!AndroidJpsUtil.containsAndroidFacet(model.getProject())) {
+ if (!AndroidJpsUtil.isAndroidProjectWithoutGradleFacet(model.getProject())) {
return Collections.emptyList();
}
final List<T> targets = new ArrayList<T>();
diff --git a/android/jps-plugin/src/org/jetbrains/jps/android/builder/AndroidPreDexBuildTarget.java b/android/jps-plugin/src/org/jetbrains/jps/android/builder/AndroidPreDexBuildTarget.java
index 1b460e4..964ec55 100644
--- a/android/jps-plugin/src/org/jetbrains/jps/android/builder/AndroidPreDexBuildTarget.java
+++ b/android/jps-plugin/src/org/jetbrains/jps/android/builder/AndroidPreDexBuildTarget.java
@@ -187,7 +187,7 @@
@NotNull
@Override
public List<AndroidPreDexBuildTarget> computeAllTargets(@NotNull JpsModel model) {
- if (!AndroidJpsUtil.containsAndroidFacet(model.getProject())) {
+ if (!AndroidJpsUtil.isAndroidProjectWithoutGradleFacet(model.getProject())) {
return Collections.emptyList();
}
return Collections.singletonList(new AndroidPreDexBuildTarget(model.getProject()));
@@ -202,9 +202,7 @@
@Nullable
@Override
public AndroidPreDexBuildTarget createTarget(@NotNull String targetId) {
- return ID.equals(targetId) && AndroidJpsUtil.containsAndroidFacet(model.getProject())
- ? new AndroidPreDexBuildTarget(project)
- : null;
+ return ID.equals(targetId) && AndroidJpsUtil.isAndroidProjectWithoutGradleFacet(project) ? new AndroidPreDexBuildTarget(project) : null;
}
};
}
diff --git a/android/lib/GoogleFeedback.jar b/android/lib/GoogleFeedback.jar
index e4ca8ff..f40c373 100644
--- a/android/lib/GoogleFeedback.jar
+++ b/android/lib/GoogleFeedback.jar
Binary files differ
diff --git a/android/lib/builder-0.4-SNAPSHOT.jar b/android/lib/builder-0.4-SNAPSHOT.jar
deleted file mode 100644
index c4759e6..0000000
--- a/android/lib/builder-0.4-SNAPSHOT.jar
+++ /dev/null
Binary files differ
diff --git a/android/lib/builder-0.5.0-SNAPSHOT.jar b/android/lib/builder-0.5.0-SNAPSHOT.jar
new file mode 100644
index 0000000..9dd7e56
--- /dev/null
+++ b/android/lib/builder-0.5.0-SNAPSHOT.jar
Binary files differ
diff --git a/android/lib/builder-model-0.4-SNAPSHOT.jar b/android/lib/builder-model-0.4-SNAPSHOT.jar
deleted file mode 100644
index 55e3102..0000000
--- a/android/lib/builder-model-0.4-SNAPSHOT.jar
+++ /dev/null
Binary files differ
diff --git a/android/lib/builder-model-0.5.0-SNAPSHOT.jar b/android/lib/builder-model-0.5.0-SNAPSHOT.jar
new file mode 100644
index 0000000..6b11cfc
--- /dev/null
+++ b/android/lib/builder-model-0.5.0-SNAPSHOT.jar
Binary files differ
diff --git a/android/lib/gradle-0.4-SNAPSHOT.jar b/android/lib/gradle-0.4-SNAPSHOT.jar
deleted file mode 100644
index 8e60d37..0000000
--- a/android/lib/gradle-0.4-SNAPSHOT.jar
+++ /dev/null
Binary files differ
diff --git a/android/lib/gradle-model-0.4-SNAPSHOT.jar b/android/lib/gradle-model-0.4-SNAPSHOT.jar
deleted file mode 100644
index 9f4a8e2..0000000
--- a/android/lib/gradle-model-0.4-SNAPSHOT.jar
+++ /dev/null
Binary files differ
diff --git a/android/lib/src/builder-0.4-SNAPSHOT-sources.jar b/android/lib/src/builder-0.4-SNAPSHOT-sources.jar
deleted file mode 100644
index 9211e60..0000000
--- a/android/lib/src/builder-0.4-SNAPSHOT-sources.jar
+++ /dev/null
Binary files differ
diff --git a/android/lib/src/builder-0.5.0-SNAPSHOT-sources.jar b/android/lib/src/builder-0.5.0-SNAPSHOT-sources.jar
new file mode 100644
index 0000000..6206a29
--- /dev/null
+++ b/android/lib/src/builder-0.5.0-SNAPSHOT-sources.jar
Binary files differ
diff --git a/android/lib/src/builder-model-0.4-SNAPSHOT-sources.jar b/android/lib/src/builder-model-0.4-SNAPSHOT-sources.jar
deleted file mode 100644
index cd1f1dd..0000000
--- a/android/lib/src/builder-model-0.4-SNAPSHOT-sources.jar
+++ /dev/null
Binary files differ
diff --git a/android/lib/src/builder-model-0.5.0-SNAPSHOT-sources.jar b/android/lib/src/builder-model-0.5.0-SNAPSHOT-sources.jar
new file mode 100644
index 0000000..786ec96
--- /dev/null
+++ b/android/lib/src/builder-model-0.5.0-SNAPSHOT-sources.jar
Binary files differ
diff --git a/android/lib/src/gradle-0.4-SNAPSHOT-sources.jar b/android/lib/src/gradle-0.4-SNAPSHOT-sources.jar
deleted file mode 100644
index 2bf11b1..0000000
--- a/android/lib/src/gradle-0.4-SNAPSHOT-sources.jar
+++ /dev/null
Binary files differ
diff --git a/android/lib/src/gradle-0.5.0-SNAPSHOT-sources.jar b/android/lib/src/gradle-0.5.0-SNAPSHOT-sources.jar
new file mode 100644
index 0000000..f1a0d83
--- /dev/null
+++ b/android/lib/src/gradle-0.5.0-SNAPSHOT-sources.jar
Binary files differ
diff --git a/android/lib/src/gradle-model-0.4-SNAPSHOT-sources.jar b/android/lib/src/gradle-model-0.4-SNAPSHOT-sources.jar
deleted file mode 100644
index b1cd165..0000000
--- a/android/lib/src/gradle-model-0.4-SNAPSHOT-sources.jar
+++ /dev/null
Binary files differ
diff --git a/android/resources/icons/AndroidIcons.java b/android/resources/icons/AndroidIcons.java
index 7a0adda..96a7b17 100644
--- a/android/resources/icons/AndroidIcons.java
+++ b/android/resources/icons/AndroidIcons.java
@@ -94,4 +94,78 @@
public static final Icon NewModuleSidePanel = load("/icons/wizards/newModule.png"); // 143x627
public static final Icon NewProjectSidePanel = load("/icons/wizards/newProject.png"); // 143x627
}
+
+ public static class Views {
+ public static final Icon AbsoluteLayout = load("/icons/views/AbsoluteLayout.png"); // 16x16
+ public static final Icon AdapterViewFlipper = load("/icons/views/AdapterViewFlipper.png"); // 16x16
+ public static final Icon AnalogClock = load("/icons/views/AnalogClock.png"); // 16x16
+ public static final Icon AutoCompleteTextView = load("/icons/views/AutoCompleteTextView.png"); // 16x16
+ public static final Icon Button = load("/icons/views/Button.png"); // 16x16
+ public static final Icon CalendarView = load("/icons/views/CalendarView.png"); // 16x16
+ public static final Icon CheckBox = load("/icons/views/CheckBox.png"); // 16x16
+ public static final Icon CheckedTextView = load("/icons/views/CheckedTextView.png"); // 16x16
+ public static final Icon Chronometer = load("/icons/views/Chronometer.png"); // 16x16
+ public static final Icon DatePicker = load("/icons/views/DatePicker.png"); // 16x16
+ public static final Icon DeviceScreen = load("/icons/views/DeviceScreen.png"); // 16x16
+ public static final Icon DialerFilter = load("/icons/views/DialerFilter.png"); // 16x16
+ public static final Icon DigitalClock = load("/icons/views/DigitalClock.png"); // 16x16
+ public static final Icon EditText = load("/icons/views/EditText.png"); // 16x16
+ public static final Icon ExpandableListView = load("/icons/views/ExpandableListView.png"); // 16x16
+ public static final Icon Fragment = load("/icons/views/fragment.png"); // 16x16
+ public static final Icon FrameLayout = load("/icons/views/FrameLayout.png"); // 16x16
+ public static final Icon Gallery = load("/icons/views/Gallery.png"); // 16x16
+ public static final Icon GestureOverlayView = load("/icons/views/GestureOverlayView.png"); // 16x16
+ public static final Icon GridLayout = load("/icons/views/GridLayout.png"); // 16x16
+ public static final Icon GridView = load("/icons/views/GridView.png"); // 16x16
+ public static final Icon HorizontalScrollView = load("/icons/views/HorizontalScrollView.png"); // 16x16
+ public static final Icon ImageButton = load("/icons/views/ImageButton.png"); // 16x16
+ public static final Icon ImageSwitcher = load("/icons/views/ImageSwitcher.png"); // 16x16
+ public static final Icon ImageView = load("/icons/views/ImageView.png"); // 16x16
+ public static final Icon Include = load("/icons/views/include.png"); // 16x16
+ public static final Icon LinearLayout = load("/icons/views/LinearLayout.png"); // 16x16
+ public static final Icon VerticalLinearLayout = load("/icons/views/VerticalLinearLayout.png"); // 16x16
+ public static final Icon LinearLayout3 = load("/icons/views/LinearLayout3.png"); // 16x16
+ public static final Icon ListView = load("/icons/views/ListView.png"); // 16x16
+ public static final Icon MediaController = load("/icons/views/MediaController.png"); // 16x16
+ public static final Icon MultiAutoCompleteTextView = load("/icons/views/MultiAutoCompleteTextView.png"); // 16x16
+ public static final Icon Merge = load("/icons/views/merge.png"); // 16x16
+ public static final Icon NumberPicker = load("/icons/views/NumberPicker.png"); // 16x16
+ public static final Icon ProgressBar = load("/icons/views/ProgressBar.png"); // 16x16
+ public static final Icon QuickContactBadge = load("/icons/views/QuickContactBadge.png"); // 16x16
+ public static final Icon RadioButton = load("/icons/views/RadioButton.png"); // 16x16
+ public static final Icon RadioGroup = load("/icons/views/RadioGroup.png"); // 16x16
+ public static final Icon RatingBar = load("/icons/views/RatingBar.png"); // 16x16
+ public static final Icon RelativeLayout = load("/icons/views/RelativeLayout.png"); // 16x16
+ public static final Icon RequestFocus = load("/icons/views/requestFocus.png"); // 16x16
+ public static final Icon ScrollView = load("/icons/views/ScrollView.png"); // 16x16
+ public static final Icon SearchView = load("/icons/views/SearchView.png"); // 16x16
+ public static final Icon SeekBar = load("/icons/views/SeekBar.png"); // 16x16
+ public static final Icon SlidingDrawer = load("/icons/views/SlidingDrawer.png"); // 16x16
+ public static final Icon Space = load("/icons/views/Space.png"); // 16x16
+ public static final Icon Spinner = load("/icons/views/Spinner.png"); // 16x16
+ public static final Icon StackView = load("/icons/views/StackView.png"); // 16x16
+ public static final Icon SurfaceView = load("/icons/views/SurfaceView.png"); // 16x16
+ public static final Icon Switch = load("/icons/views/Switch.png"); // 16x16
+ public static final Icon TabHost = load("/icons/views/TabHost.png"); // 16x16
+ public static final Icon TableLayout = load("/icons/views/TableLayout.png"); // 16x16
+ public static final Icon TableRow = load("/icons/views/TableRow.png"); // 16x16
+ public static final Icon TabWidget = load("/icons/views/TabWidget.png"); // 16x16
+ public static final Icon TextClock = load("/icons/views/TextClock.png"); // 16x16
+ public static final Icon TextSwitcher = load("/icons/views/TextSwitcher.png"); // 16x16
+ public static final Icon TextureView = load("/icons/views/TextureView.png"); // 16x16
+ public static final Icon TextView = load("/icons/views/TextView.png"); // 16x16
+ public static final Icon TimePicker = load("/icons/views/TimePicker.png"); // 16x16
+ public static final Icon ToggleButton = load("/icons/views/ToggleButton.png"); // 16x16
+ public static final Icon TwoLineListItem = load("/icons/views/TwoLineListItem.png"); // 16x16
+ public static final Icon Unknown = load("/icons/views/customView.png"); // 16x16
+ public static final Icon VideoView = load("/icons/views/VideoView.png"); // 16x13
+ public static final Icon View = load("/icons/views/View.png"); // 16x16
+ public static final Icon ViewAnimator = load("/icons/views/ViewAnimator.png"); // 16x16
+ public static final Icon ViewFlipper = load("/icons/views/ViewFlipper.png"); // 16x16
+ public static final Icon ViewStub = load("/icons/views/ViewStub.png"); // 16x16
+ public static final Icon ViewSwitcher = load("/icons/views/ViewSwitcher.png"); // 16x16
+ public static final Icon WebView = load("/icons/views/WebView.png"); // 16x16
+ public static final Icon ZoomButton = load("/icons/views/ZoomButton.png"); // 16x16
+ public static final Icon ZoomControls = load("/icons/views/ZoomControls.png"); // 16x16
+ }
}
diff --git a/android-designer/src/icons/dimension@2x.png b/android/resources/icons/dimension@2x.png
similarity index 100%
rename from android-designer/src/icons/dimension@2x.png
rename to android/resources/icons/dimension@2x.png
Binary files differ
diff --git a/android-designer/src/icons/dockmode@2x.png b/android/resources/icons/dockmode@2x.png
similarity index 100%
rename from android-designer/src/icons/dockmode@2x.png
rename to android/resources/icons/dockmode@2x.png
Binary files differ
diff --git a/android-designer/src/icons/dpi@2x.png b/android/resources/icons/dpi@2x.png
similarity index 100%
rename from android-designer/src/icons/dpi@2x.png
rename to android/resources/icons/dpi@2x.png
Binary files differ
diff --git a/android-designer/src/icons/height@2x.png b/android/resources/icons/height@2x.png
similarity index 100%
rename from android-designer/src/icons/height@2x.png
rename to android/resources/icons/height@2x.png
Binary files differ
diff --git a/android-designer/src/icons/keyboard@2x.png b/android/resources/icons/keyboard@2x.png
similarity index 100%
rename from android-designer/src/icons/keyboard@2x.png
rename to android/resources/icons/keyboard@2x.png
Binary files differ
diff --git a/android-designer/src/icons/language@2x.png b/android/resources/icons/language@2x.png
similarity index 100%
rename from android-designer/src/icons/language@2x.png
rename to android/resources/icons/language@2x.png
Binary files differ
diff --git a/android-designer/src/icons/mcc@2x.png b/android/resources/icons/mcc@2x.png
similarity index 100%
rename from android-designer/src/icons/mcc@2x.png
rename to android/resources/icons/mcc@2x.png
Binary files differ
diff --git a/android-designer/src/icons/mnc@2x.png b/android/resources/icons/mnc@2x.png
similarity index 100%
rename from android-designer/src/icons/mnc@2x.png
rename to android/resources/icons/mnc@2x.png
Binary files differ
diff --git a/android-designer/src/icons/navpad@2x.png b/android/resources/icons/navpad@2x.png
similarity index 100%
rename from android-designer/src/icons/navpad@2x.png
rename to android/resources/icons/navpad@2x.png
Binary files differ
diff --git a/android-designer/src/icons/navpad_method@2x.png b/android/resources/icons/navpad_method@2x.png
similarity index 100%
rename from android-designer/src/icons/navpad_method@2x.png
rename to android/resources/icons/navpad_method@2x.png
Binary files differ
diff --git a/android-designer/src/icons/nightmode@2x.png b/android/resources/icons/nightmode@2x.png
similarity index 100%
rename from android-designer/src/icons/nightmode@2x.png
rename to android/resources/icons/nightmode@2x.png
Binary files differ
diff --git a/android-designer/src/icons/orientation@2x.png b/android/resources/icons/orientation@2x.png
similarity index 100%
rename from android-designer/src/icons/orientation@2x.png
rename to android/resources/icons/orientation@2x.png
Binary files differ
diff --git a/android-designer/src/icons/ratio@2x.png b/android/resources/icons/ratio@2x.png
similarity index 100%
rename from android-designer/src/icons/ratio@2x.png
rename to android/resources/icons/ratio@2x.png
Binary files differ
diff --git a/android-designer/src/icons/region@2x.png b/android/resources/icons/region@2x.png
similarity index 100%
rename from android-designer/src/icons/region@2x.png
rename to android/resources/icons/region@2x.png
Binary files differ
diff --git a/android-designer/src/icons/size@2x.png b/android/resources/icons/size@2x.png
similarity index 100%
rename from android-designer/src/icons/size@2x.png
rename to android/resources/icons/size@2x.png
Binary files differ
diff --git a/android-designer/src/icons/swidth@2x.png b/android/resources/icons/swidth@2x.png
similarity index 100%
rename from android-designer/src/icons/swidth@2x.png
rename to android/resources/icons/swidth@2x.png
Binary files differ
diff --git a/android-designer/src/icons/text_input@2x.png b/android/resources/icons/text_input@2x.png
similarity index 100%
rename from android-designer/src/icons/text_input@2x.png
rename to android/resources/icons/text_input@2x.png
Binary files differ
diff --git a/android-designer/src/icons/touch@2x.png b/android/resources/icons/touch@2x.png
similarity index 100%
rename from android-designer/src/icons/touch@2x.png
rename to android/resources/icons/touch@2x.png
Binary files differ
diff --git a/android-designer/src/icons/AbsoluteLayout.png b/android/resources/icons/views/AbsoluteLayout.png
similarity index 100%
rename from android-designer/src/icons/AbsoluteLayout.png
rename to android/resources/icons/views/AbsoluteLayout.png
Binary files differ
diff --git a/android-designer/src/icons/AdapterViewFlipper.png b/android/resources/icons/views/AdapterViewFlipper.png
similarity index 100%
rename from android-designer/src/icons/AdapterViewFlipper.png
rename to android/resources/icons/views/AdapterViewFlipper.png
Binary files differ
diff --git a/android-designer/src/icons/AnalogClock.png b/android/resources/icons/views/AnalogClock.png
similarity index 100%
rename from android-designer/src/icons/AnalogClock.png
rename to android/resources/icons/views/AnalogClock.png
Binary files differ
diff --git a/android-designer/src/icons/AutoCompleteTextView.png b/android/resources/icons/views/AutoCompleteTextView.png
similarity index 100%
rename from android-designer/src/icons/AutoCompleteTextView.png
rename to android/resources/icons/views/AutoCompleteTextView.png
Binary files differ
diff --git a/android-designer/src/icons/AutoCompleteTextView@2x.png b/android/resources/icons/views/AutoCompleteTextView@2x.png
similarity index 100%
rename from android-designer/src/icons/AutoCompleteTextView@2x.png
rename to android/resources/icons/views/AutoCompleteTextView@2x.png
Binary files differ
diff --git a/android-designer/src/icons/Button.png b/android/resources/icons/views/Button.png
similarity index 100%
rename from android-designer/src/icons/Button.png
rename to android/resources/icons/views/Button.png
Binary files differ
diff --git a/android-designer/src/icons/Button@2x.png b/android/resources/icons/views/Button@2x.png
similarity index 100%
rename from android-designer/src/icons/Button@2x.png
rename to android/resources/icons/views/Button@2x.png
Binary files differ
diff --git a/android-designer/src/icons/CalendarView.png b/android/resources/icons/views/CalendarView.png
similarity index 100%
rename from android-designer/src/icons/CalendarView.png
rename to android/resources/icons/views/CalendarView.png
Binary files differ
diff --git a/android-designer/src/icons/CalendarView@2x.png b/android/resources/icons/views/CalendarView@2x.png
similarity index 100%
rename from android-designer/src/icons/CalendarView@2x.png
rename to android/resources/icons/views/CalendarView@2x.png
Binary files differ
diff --git a/android-designer/src/icons/CheckBox.png b/android/resources/icons/views/CheckBox.png
similarity index 100%
rename from android-designer/src/icons/CheckBox.png
rename to android/resources/icons/views/CheckBox.png
Binary files differ
diff --git a/android-designer/src/icons/CheckedTextView.png b/android/resources/icons/views/CheckedTextView.png
similarity index 100%
rename from android-designer/src/icons/CheckedTextView.png
rename to android/resources/icons/views/CheckedTextView.png
Binary files differ
diff --git a/android-designer/src/icons/CheckedTextView@2x.png b/android/resources/icons/views/CheckedTextView@2x.png
similarity index 100%
rename from android-designer/src/icons/CheckedTextView@2x.png
rename to android/resources/icons/views/CheckedTextView@2x.png
Binary files differ
diff --git a/android-designer/src/icons/Chronometer.png b/android/resources/icons/views/Chronometer.png
similarity index 100%
rename from android-designer/src/icons/Chronometer.png
rename to android/resources/icons/views/Chronometer.png
Binary files differ
diff --git a/android-designer/src/icons/Chronometer@2x.png b/android/resources/icons/views/Chronometer@2x.png
similarity index 100%
rename from android-designer/src/icons/Chronometer@2x.png
rename to android/resources/icons/views/Chronometer@2x.png
Binary files differ
diff --git a/android-designer/src/icons/DatePicker.png b/android/resources/icons/views/DatePicker.png
similarity index 100%
rename from android-designer/src/icons/DatePicker.png
rename to android/resources/icons/views/DatePicker.png
Binary files differ
diff --git a/android-designer/src/icons/DatePicker@2x.png b/android/resources/icons/views/DatePicker@2x.png
similarity index 100%
rename from android-designer/src/icons/DatePicker@2x.png
rename to android/resources/icons/views/DatePicker@2x.png
Binary files differ
diff --git a/android-designer/src/icons/DeviceScreen.png b/android/resources/icons/views/DeviceScreen.png
similarity index 100%
rename from android-designer/src/icons/DeviceScreen.png
rename to android/resources/icons/views/DeviceScreen.png
Binary files differ
diff --git a/android-designer/src/icons/DialerFilter.png b/android/resources/icons/views/DialerFilter.png
similarity index 100%
rename from android-designer/src/icons/DialerFilter.png
rename to android/resources/icons/views/DialerFilter.png
Binary files differ
diff --git a/android-designer/src/icons/DigitalClock.png b/android/resources/icons/views/DigitalClock.png
similarity index 100%
rename from android-designer/src/icons/DigitalClock.png
rename to android/resources/icons/views/DigitalClock.png
Binary files differ
diff --git a/android-designer/src/icons/DigitalClock@2x.png b/android/resources/icons/views/DigitalClock@2x.png
similarity index 100%
rename from android-designer/src/icons/DigitalClock@2x.png
rename to android/resources/icons/views/DigitalClock@2x.png
Binary files differ
diff --git a/android-designer/src/icons/EditText.png b/android/resources/icons/views/EditText.png
similarity index 100%
rename from android-designer/src/icons/EditText.png
rename to android/resources/icons/views/EditText.png
Binary files differ
diff --git a/android-designer/src/icons/EditText@2x.png b/android/resources/icons/views/EditText@2x.png
similarity index 100%
rename from android-designer/src/icons/EditText@2x.png
rename to android/resources/icons/views/EditText@2x.png
Binary files differ
diff --git a/android-designer/src/icons/ExpandableListView.png b/android/resources/icons/views/ExpandableListView.png
similarity index 100%
rename from android-designer/src/icons/ExpandableListView.png
rename to android/resources/icons/views/ExpandableListView.png
Binary files differ
diff --git a/android-designer/src/icons/ExpandableListView@2x.png b/android/resources/icons/views/ExpandableListView@2x.png
similarity index 100%
rename from android-designer/src/icons/ExpandableListView@2x.png
rename to android/resources/icons/views/ExpandableListView@2x.png
Binary files differ
diff --git a/android-designer/src/icons/FrameLayout.png b/android/resources/icons/views/FrameLayout.png
similarity index 100%
rename from android-designer/src/icons/FrameLayout.png
rename to android/resources/icons/views/FrameLayout.png
Binary files differ
diff --git a/android-designer/src/icons/Gallery.png b/android/resources/icons/views/Gallery.png
similarity index 100%
rename from android-designer/src/icons/Gallery.png
rename to android/resources/icons/views/Gallery.png
Binary files differ
diff --git a/android-designer/src/icons/Gallery@2x.png b/android/resources/icons/views/Gallery@2x.png
similarity index 100%
rename from android-designer/src/icons/Gallery@2x.png
rename to android/resources/icons/views/Gallery@2x.png
Binary files differ
diff --git a/android-designer/src/icons/GestureOverlayView.png b/android/resources/icons/views/GestureOverlayView.png
similarity index 100%
rename from android-designer/src/icons/GestureOverlayView.png
rename to android/resources/icons/views/GestureOverlayView.png
Binary files differ
diff --git a/android-designer/src/icons/GestureOverlayView@2x.png b/android/resources/icons/views/GestureOverlayView@2x.png
similarity index 100%
rename from android-designer/src/icons/GestureOverlayView@2x.png
rename to android/resources/icons/views/GestureOverlayView@2x.png
Binary files differ
diff --git a/android-designer/src/icons/GridLayout.png b/android/resources/icons/views/GridLayout.png
similarity index 100%
rename from android-designer/src/icons/GridLayout.png
rename to android/resources/icons/views/GridLayout.png
Binary files differ
diff --git a/android-designer/src/icons/GridView.png b/android/resources/icons/views/GridView.png
similarity index 100%
rename from android-designer/src/icons/GridView.png
rename to android/resources/icons/views/GridView.png
Binary files differ
diff --git a/android-designer/src/icons/HorizontalScrollView.png b/android/resources/icons/views/HorizontalScrollView.png
similarity index 100%
rename from android-designer/src/icons/HorizontalScrollView.png
rename to android/resources/icons/views/HorizontalScrollView.png
Binary files differ
diff --git a/android-designer/src/icons/ImageButton.png b/android/resources/icons/views/ImageButton.png
similarity index 100%
rename from android-designer/src/icons/ImageButton.png
rename to android/resources/icons/views/ImageButton.png
Binary files differ
diff --git a/android-designer/src/icons/ImageButton@2x.png b/android/resources/icons/views/ImageButton@2x.png
similarity index 100%
rename from android-designer/src/icons/ImageButton@2x.png
rename to android/resources/icons/views/ImageButton@2x.png
Binary files differ
diff --git a/android-designer/src/icons/ImageSwitcher.png b/android/resources/icons/views/ImageSwitcher.png
similarity index 100%
rename from android-designer/src/icons/ImageSwitcher.png
rename to android/resources/icons/views/ImageSwitcher.png
Binary files differ
diff --git a/android-designer/src/icons/ImageSwitcher@2x.png b/android/resources/icons/views/ImageSwitcher@2x.png
similarity index 100%
rename from android-designer/src/icons/ImageSwitcher@2x.png
rename to android/resources/icons/views/ImageSwitcher@2x.png
Binary files differ
diff --git a/android-designer/src/icons/ImageView.png b/android/resources/icons/views/ImageView.png
similarity index 100%
rename from android-designer/src/icons/ImageView.png
rename to android/resources/icons/views/ImageView.png
Binary files differ
diff --git a/android-designer/src/icons/ImageView@2x.png b/android/resources/icons/views/ImageView@2x.png
similarity index 100%
rename from android-designer/src/icons/ImageView@2x.png
rename to android/resources/icons/views/ImageView@2x.png
Binary files differ
diff --git a/android-designer/src/icons/LinearLayout.png b/android/resources/icons/views/LinearLayout.png
similarity index 100%
rename from android-designer/src/icons/LinearLayout.png
rename to android/resources/icons/views/LinearLayout.png
Binary files differ
diff --git a/android-designer/src/icons/LinearLayout2.png b/android/resources/icons/views/LinearLayout2.png
similarity index 100%
rename from android-designer/src/icons/LinearLayout2.png
rename to android/resources/icons/views/LinearLayout2.png
Binary files differ
diff --git a/android-designer/src/icons/LinearLayout3.png b/android/resources/icons/views/LinearLayout3.png
similarity index 100%
rename from android-designer/src/icons/LinearLayout3.png
rename to android/resources/icons/views/LinearLayout3.png
Binary files differ
diff --git a/android-designer/src/icons/ListView.png b/android/resources/icons/views/ListView.png
similarity index 100%
rename from android-designer/src/icons/ListView.png
rename to android/resources/icons/views/ListView.png
Binary files differ
diff --git a/android-designer/src/icons/ListView@2x.png b/android/resources/icons/views/ListView@2x.png
similarity index 100%
rename from android-designer/src/icons/ListView@2x.png
rename to android/resources/icons/views/ListView@2x.png
Binary files differ
diff --git a/android-designer/src/icons/MediaController.png b/android/resources/icons/views/MediaController.png
similarity index 100%
rename from android-designer/src/icons/MediaController.png
rename to android/resources/icons/views/MediaController.png
Binary files differ
diff --git a/android-designer/src/icons/Merge@2x.png b/android/resources/icons/views/Merge@2x.png
similarity index 100%
rename from android-designer/src/icons/Merge@2x.png
rename to android/resources/icons/views/Merge@2x.png
Binary files differ
diff --git a/android-designer/src/icons/MultiAutoCompleteTextView.png b/android/resources/icons/views/MultiAutoCompleteTextView.png
similarity index 100%
rename from android-designer/src/icons/MultiAutoCompleteTextView.png
rename to android/resources/icons/views/MultiAutoCompleteTextView.png
Binary files differ
diff --git a/android-designer/src/icons/MultiAutoCompleteTextView@2x.png b/android/resources/icons/views/MultiAutoCompleteTextView@2x.png
similarity index 100%
rename from android-designer/src/icons/MultiAutoCompleteTextView@2x.png
rename to android/resources/icons/views/MultiAutoCompleteTextView@2x.png
Binary files differ
diff --git a/android-designer/src/icons/NumberPicker.png b/android/resources/icons/views/NumberPicker.png
similarity index 100%
rename from android-designer/src/icons/NumberPicker.png
rename to android/resources/icons/views/NumberPicker.png
Binary files differ
diff --git a/android-designer/src/icons/NumberPicker@2x.png b/android/resources/icons/views/NumberPicker@2x.png
similarity index 100%
rename from android-designer/src/icons/NumberPicker@2x.png
rename to android/resources/icons/views/NumberPicker@2x.png
Binary files differ
diff --git a/android-designer/src/icons/ProgressBar.png b/android/resources/icons/views/ProgressBar.png
similarity index 100%
rename from android-designer/src/icons/ProgressBar.png
rename to android/resources/icons/views/ProgressBar.png
Binary files differ
diff --git a/android-designer/src/icons/ProgressBar@2x.png b/android/resources/icons/views/ProgressBar@2x.png
similarity index 100%
rename from android-designer/src/icons/ProgressBar@2x.png
rename to android/resources/icons/views/ProgressBar@2x.png
Binary files differ
diff --git a/android-designer/src/icons/QuickContactBadge.png b/android/resources/icons/views/QuickContactBadge.png
similarity index 100%
rename from android-designer/src/icons/QuickContactBadge.png
rename to android/resources/icons/views/QuickContactBadge.png
Binary files differ
diff --git a/android-designer/src/icons/QuickContactBadge@2x.png b/android/resources/icons/views/QuickContactBadge@2x.png
similarity index 100%
rename from android-designer/src/icons/QuickContactBadge@2x.png
rename to android/resources/icons/views/QuickContactBadge@2x.png
Binary files differ
diff --git a/android-designer/src/icons/RadioButton.png b/android/resources/icons/views/RadioButton.png
similarity index 100%
rename from android-designer/src/icons/RadioButton.png
rename to android/resources/icons/views/RadioButton.png
Binary files differ
diff --git a/android-designer/src/icons/RadioGroup.png b/android/resources/icons/views/RadioGroup.png
similarity index 100%
rename from android-designer/src/icons/RadioGroup.png
rename to android/resources/icons/views/RadioGroup.png
Binary files differ
diff --git a/android-designer/src/icons/RatingBar.png b/android/resources/icons/views/RatingBar.png
similarity index 100%
rename from android-designer/src/icons/RatingBar.png
rename to android/resources/icons/views/RatingBar.png
Binary files differ
diff --git a/android-designer/src/icons/RelativeLayout.png b/android/resources/icons/views/RelativeLayout.png
similarity index 100%
rename from android-designer/src/icons/RelativeLayout.png
rename to android/resources/icons/views/RelativeLayout.png
Binary files differ
diff --git a/android-designer/src/icons/ScrollView.png b/android/resources/icons/views/ScrollView.png
similarity index 100%
rename from android-designer/src/icons/ScrollView.png
rename to android/resources/icons/views/ScrollView.png
Binary files differ
diff --git a/android-designer/src/icons/SearchView.png b/android/resources/icons/views/SearchView.png
similarity index 100%
rename from android-designer/src/icons/SearchView.png
rename to android/resources/icons/views/SearchView.png
Binary files differ
diff --git a/android-designer/src/icons/SeekBar.png b/android/resources/icons/views/SeekBar.png
similarity index 100%
rename from android-designer/src/icons/SeekBar.png
rename to android/resources/icons/views/SeekBar.png
Binary files differ
diff --git a/android-designer/src/icons/SeekBar@2x.png b/android/resources/icons/views/SeekBar@2x.png
similarity index 100%
rename from android-designer/src/icons/SeekBar@2x.png
rename to android/resources/icons/views/SeekBar@2x.png
Binary files differ
diff --git a/android-designer/src/icons/SlidingDrawer.png b/android/resources/icons/views/SlidingDrawer.png
similarity index 100%
rename from android-designer/src/icons/SlidingDrawer.png
rename to android/resources/icons/views/SlidingDrawer.png
Binary files differ
diff --git a/android-designer/src/icons/Space.png b/android/resources/icons/views/Space.png
similarity index 100%
rename from android-designer/src/icons/Space.png
rename to android/resources/icons/views/Space.png
Binary files differ
diff --git a/android-designer/src/icons/Space@2x.png b/android/resources/icons/views/Space@2x.png
similarity index 100%
rename from android-designer/src/icons/Space@2x.png
rename to android/resources/icons/views/Space@2x.png
Binary files differ
diff --git a/android-designer/src/icons/Spinner.png b/android/resources/icons/views/Spinner.png
similarity index 100%
rename from android-designer/src/icons/Spinner.png
rename to android/resources/icons/views/Spinner.png
Binary files differ
diff --git a/android-designer/src/icons/Spinner@2x.png b/android/resources/icons/views/Spinner@2x.png
similarity index 100%
rename from android-designer/src/icons/Spinner@2x.png
rename to android/resources/icons/views/Spinner@2x.png
Binary files differ
diff --git a/android-designer/src/icons/StackView.png b/android/resources/icons/views/StackView.png
similarity index 100%
rename from android-designer/src/icons/StackView.png
rename to android/resources/icons/views/StackView.png
Binary files differ
diff --git a/android-designer/src/icons/SurfaceView.png b/android/resources/icons/views/SurfaceView.png
similarity index 100%
rename from android-designer/src/icons/SurfaceView.png
rename to android/resources/icons/views/SurfaceView.png
Binary files differ
diff --git a/android-designer/src/icons/Switch.png b/android/resources/icons/views/Switch.png
similarity index 100%
rename from android-designer/src/icons/Switch.png
rename to android/resources/icons/views/Switch.png
Binary files differ
diff --git a/android-designer/src/icons/Switch@2x.png b/android/resources/icons/views/Switch@2x.png
similarity index 100%
rename from android-designer/src/icons/Switch@2x.png
rename to android/resources/icons/views/Switch@2x.png
Binary files differ
diff --git a/android-designer/src/icons/TabHost.png b/android/resources/icons/views/TabHost.png
similarity index 100%
rename from android-designer/src/icons/TabHost.png
rename to android/resources/icons/views/TabHost.png
Binary files differ
diff --git a/android-designer/src/icons/TabHost@2x.png b/android/resources/icons/views/TabHost@2x.png
similarity index 100%
rename from android-designer/src/icons/TabHost@2x.png
rename to android/resources/icons/views/TabHost@2x.png
Binary files differ
diff --git a/android-designer/src/icons/TabWidget.png b/android/resources/icons/views/TabWidget.png
similarity index 100%
rename from android-designer/src/icons/TabWidget.png
rename to android/resources/icons/views/TabWidget.png
Binary files differ
diff --git a/android-designer/src/icons/TabWidget@2x.png b/android/resources/icons/views/TabWidget@2x.png
similarity index 100%
rename from android-designer/src/icons/TabWidget@2x.png
rename to android/resources/icons/views/TabWidget@2x.png
Binary files differ
diff --git a/android-designer/src/icons/TableLayout.png b/android/resources/icons/views/TableLayout.png
similarity index 100%
rename from android-designer/src/icons/TableLayout.png
rename to android/resources/icons/views/TableLayout.png
Binary files differ
diff --git a/android-designer/src/icons/TableRow.png b/android/resources/icons/views/TableRow.png
similarity index 100%
rename from android-designer/src/icons/TableRow.png
rename to android/resources/icons/views/TableRow.png
Binary files differ
diff --git a/android-designer/src/icons/TextClock.png b/android/resources/icons/views/TextClock.png
similarity index 100%
rename from android-designer/src/icons/TextClock.png
rename to android/resources/icons/views/TextClock.png
Binary files differ
diff --git a/android-designer/src/icons/TextSwitcher.png b/android/resources/icons/views/TextSwitcher.png
similarity index 100%
rename from android-designer/src/icons/TextSwitcher.png
rename to android/resources/icons/views/TextSwitcher.png
Binary files differ
diff --git a/android-designer/src/icons/TextSwitcher@2x.png b/android/resources/icons/views/TextSwitcher@2x.png
similarity index 100%
rename from android-designer/src/icons/TextSwitcher@2x.png
rename to android/resources/icons/views/TextSwitcher@2x.png
Binary files differ
diff --git a/android-designer/src/icons/TextView.png b/android/resources/icons/views/TextView.png
similarity index 100%
rename from android-designer/src/icons/TextView.png
rename to android/resources/icons/views/TextView.png
Binary files differ
diff --git a/android-designer/src/icons/TextureView.png b/android/resources/icons/views/TextureView.png
similarity index 100%
rename from android-designer/src/icons/TextureView.png
rename to android/resources/icons/views/TextureView.png
Binary files differ
diff --git a/android-designer/src/icons/TimePicker.png b/android/resources/icons/views/TimePicker.png
similarity index 100%
rename from android-designer/src/icons/TimePicker.png
rename to android/resources/icons/views/TimePicker.png
Binary files differ
diff --git a/android-designer/src/icons/TimePicker@2x.png b/android/resources/icons/views/TimePicker@2x.png
similarity index 100%
rename from android-designer/src/icons/TimePicker@2x.png
rename to android/resources/icons/views/TimePicker@2x.png
Binary files differ
diff --git a/android-designer/src/icons/ToggleButton.png b/android/resources/icons/views/ToggleButton.png
similarity index 100%
rename from android-designer/src/icons/ToggleButton.png
rename to android/resources/icons/views/ToggleButton.png
Binary files differ
diff --git a/android-designer/src/icons/ToggleButton@2x.png b/android/resources/icons/views/ToggleButton@2x.png
similarity index 100%
rename from android-designer/src/icons/ToggleButton@2x.png
rename to android/resources/icons/views/ToggleButton@2x.png
Binary files differ
diff --git a/android-designer/src/icons/TwoLineListItem.png b/android/resources/icons/views/TwoLineListItem.png
similarity index 100%
rename from android-designer/src/icons/TwoLineListItem.png
rename to android/resources/icons/views/TwoLineListItem.png
Binary files differ
diff --git a/android-designer/src/icons/TwoLineListItem@2x.png b/android/resources/icons/views/TwoLineListItem@2x.png
similarity index 100%
rename from android-designer/src/icons/TwoLineListItem@2x.png
rename to android/resources/icons/views/TwoLineListItem@2x.png
Binary files differ
diff --git a/android-designer/src/icons/VerticalLinearLayout.png b/android/resources/icons/views/VerticalLinearLayout.png
similarity index 100%
rename from android-designer/src/icons/VerticalLinearLayout.png
rename to android/resources/icons/views/VerticalLinearLayout.png
Binary files differ
diff --git a/android-designer/src/icons/VideoView.png b/android/resources/icons/views/VideoView.png
similarity index 100%
rename from android-designer/src/icons/VideoView.png
rename to android/resources/icons/views/VideoView.png
Binary files differ
diff --git a/android-designer/src/icons/VideoView@2x.png b/android/resources/icons/views/VideoView@2x.png
similarity index 100%
rename from android-designer/src/icons/VideoView@2x.png
rename to android/resources/icons/views/VideoView@2x.png
Binary files differ
diff --git a/android-designer/src/icons/View.png b/android/resources/icons/views/View.png
similarity index 100%
rename from android-designer/src/icons/View.png
rename to android/resources/icons/views/View.png
Binary files differ
diff --git a/android-designer/src/icons/ViewAnimator.png b/android/resources/icons/views/ViewAnimator.png
similarity index 100%
rename from android-designer/src/icons/ViewAnimator.png
rename to android/resources/icons/views/ViewAnimator.png
Binary files differ
diff --git a/android-designer/src/icons/ViewAnimator@2x.png b/android/resources/icons/views/ViewAnimator@2x.png
similarity index 100%
rename from android-designer/src/icons/ViewAnimator@2x.png
rename to android/resources/icons/views/ViewAnimator@2x.png
Binary files differ
diff --git a/android-designer/src/icons/ViewFlipper.png b/android/resources/icons/views/ViewFlipper.png
similarity index 100%
rename from android-designer/src/icons/ViewFlipper.png
rename to android/resources/icons/views/ViewFlipper.png
Binary files differ
diff --git a/android-designer/src/icons/ViewFlipper@2x.png b/android/resources/icons/views/ViewFlipper@2x.png
similarity index 100%
rename from android-designer/src/icons/ViewFlipper@2x.png
rename to android/resources/icons/views/ViewFlipper@2x.png
Binary files differ
diff --git a/android-designer/src/icons/ViewStub.png b/android/resources/icons/views/ViewStub.png
similarity index 100%
rename from android-designer/src/icons/ViewStub.png
rename to android/resources/icons/views/ViewStub.png
Binary files differ
diff --git a/android-designer/src/icons/ViewStub@2x.png b/android/resources/icons/views/ViewStub@2x.png
similarity index 100%
rename from android-designer/src/icons/ViewStub@2x.png
rename to android/resources/icons/views/ViewStub@2x.png
Binary files differ
diff --git a/android-designer/src/icons/ViewSwitcher.png b/android/resources/icons/views/ViewSwitcher.png
similarity index 100%
rename from android-designer/src/icons/ViewSwitcher.png
rename to android/resources/icons/views/ViewSwitcher.png
Binary files differ
diff --git a/android-designer/src/icons/ViewSwitcher@2x.png b/android/resources/icons/views/ViewSwitcher@2x.png
similarity index 100%
rename from android-designer/src/icons/ViewSwitcher@2x.png
rename to android/resources/icons/views/ViewSwitcher@2x.png
Binary files differ
diff --git a/android-designer/src/icons/WebView.png b/android/resources/icons/views/WebView.png
similarity index 100%
rename from android-designer/src/icons/WebView.png
rename to android/resources/icons/views/WebView.png
Binary files differ
diff --git a/android-designer/src/icons/WebView@2x.png b/android/resources/icons/views/WebView@2x.png
similarity index 100%
rename from android-designer/src/icons/WebView@2x.png
rename to android/resources/icons/views/WebView@2x.png
Binary files differ
diff --git a/android-designer/src/icons/ZoomButton.png b/android/resources/icons/views/ZoomButton.png
similarity index 100%
rename from android-designer/src/icons/ZoomButton.png
rename to android/resources/icons/views/ZoomButton.png
Binary files differ
diff --git a/android-designer/src/icons/ZoomButton@2x.png b/android/resources/icons/views/ZoomButton@2x.png
similarity index 100%
rename from android-designer/src/icons/ZoomButton@2x.png
rename to android/resources/icons/views/ZoomButton@2x.png
Binary files differ
diff --git a/android-designer/src/icons/ZoomControls.png b/android/resources/icons/views/ZoomControls.png
similarity index 100%
rename from android-designer/src/icons/ZoomControls.png
rename to android/resources/icons/views/ZoomControls.png
Binary files differ
diff --git a/android-designer/src/icons/ZoomControls@2x.png b/android/resources/icons/views/ZoomControls@2x.png
similarity index 100%
rename from android-designer/src/icons/ZoomControls@2x.png
rename to android/resources/icons/views/ZoomControls@2x.png
Binary files differ
diff --git a/android-designer/src/icons/ZoomReal.png b/android/resources/icons/views/ZoomReal.png
similarity index 100%
rename from android-designer/src/icons/ZoomReal.png
rename to android/resources/icons/views/ZoomReal.png
Binary files differ
diff --git a/android-designer/src/icons/customView.png b/android/resources/icons/views/customView.png
similarity index 100%
rename from android-designer/src/icons/customView.png
rename to android/resources/icons/views/customView.png
Binary files differ
diff --git a/android-designer/src/icons/customView@2x.png b/android/resources/icons/views/customView@2x.png
similarity index 100%
rename from android-designer/src/icons/customView@2x.png
rename to android/resources/icons/views/customView@2x.png
Binary files differ
diff --git a/android-designer/src/icons/fragment.png b/android/resources/icons/views/fragment.png
similarity index 100%
rename from android-designer/src/icons/fragment.png
rename to android/resources/icons/views/fragment.png
Binary files differ
diff --git a/android-designer/src/icons/Fragment@2x.png b/android/resources/icons/views/fragment@2x.png
similarity index 100%
rename from android-designer/src/icons/Fragment@2x.png
rename to android/resources/icons/views/fragment@2x.png
Binary files differ
diff --git a/android-designer/src/icons/include.png b/android/resources/icons/views/include.png
similarity index 100%
rename from android-designer/src/icons/include.png
rename to android/resources/icons/views/include.png
Binary files differ
diff --git a/android-designer/src/icons/Include@2x.png b/android/resources/icons/views/include@2x.png
similarity index 100%
rename from android-designer/src/icons/Include@2x.png
rename to android/resources/icons/views/include@2x.png
Binary files differ
diff --git a/android-designer/src/icons/merge.png b/android/resources/icons/views/merge.png
similarity index 100%
rename from android-designer/src/icons/merge.png
rename to android/resources/icons/views/merge.png
Binary files differ
diff --git a/android-designer/src/icons/requestFocus.png b/android/resources/icons/views/requestFocus.png
similarity index 100%
rename from android-designer/src/icons/requestFocus.png
rename to android/resources/icons/views/requestFocus.png
Binary files differ
diff --git a/android-designer/src/icons/RequestFocus@2x.png b/android/resources/icons/views/requestFocus@2x.png
similarity index 100%
rename from android-designer/src/icons/RequestFocus@2x.png
rename to android/resources/icons/views/requestFocus@2x.png
Binary files differ
diff --git a/android-designer/src/icons/width@2x.png b/android/resources/icons/width@2x.png
similarity index 100%
rename from android-designer/src/icons/width@2x.png
rename to android/resources/icons/width@2x.png
Binary files differ
diff --git a/android/resources/messages/AndroidBundle.properties b/android/resources/messages/AndroidBundle.properties
index a380687..ee36617 100644
--- a/android/resources/messages/AndroidBundle.properties
+++ b/android/resources/messages/AndroidBundle.properties
@@ -221,6 +221,7 @@
android.export.package.new.key.alias.label=&Alias:
android.key.password.label=Pa&ssword:
android.cannot.run.library.project.error=The module cannot be Android library
+android.cannot.run.library.project.in.this.buildtype=The currently selected Gradle build type does not support instrumentation tests
android.compilation.error.specify.platform=[{0}] Android SDK is not specified or cannot be parsed
android.compilation.error.manifest.not.found=[{0}] AndroidManifest.xml file not found. Please, check Android facet settings.
android.compilation.error.apt.gen.not.specified=AAPT destination directory not specified for module {0}
@@ -338,6 +339,7 @@
android.lint.inspections.commit.pref.edits=Missing commit() on SharedPreference editor
android.lint.inspections.content.description=Image without contentDescription
android.lint.inspections.cut.paste.id=Likely cut & paste mistakes
+android.lint.inspections.device.admin=Device Admin cannot be deactivated
android.lint.inspections.disable.baseline.alignment=Missing baselineAligned attribute
android.lint.inspections.draw.allocation=Memory allocations within drawing code
android.lint.inspections.duplicate.activity=Activity registered more than once
@@ -389,6 +391,7 @@
android.lint.inspections.missing.id=Fragments should specify an id or tag
android.lint.inspections.missing.prefix=Missing Android XML namespace
android.lint.inspections.missing.quantity=Missing quantity translation
+android.lint.inspections.missing.super.call=Missing Super Call
android.lint.inspections.missing.translation=Incomplete translation
android.lint.inspections.missing.version=Missing application name/version
android.lint.inspections.multiple.uses.sdk=Multiple <uses-sdk> elements in the manifest
@@ -472,8 +475,8 @@
android.lint.fix.remove.unnecessary.view=Remove unnecessary view
android.lint.fix.replace.with.suggested.characters=Replace with suggested characters
android.lint.fix.add.target.api=Add @TargetApi({0}) Annotation
-android.lint.fix.suppress.lint.api.annotation=Add @SuppressLint("{0}") annotation
-android.lint.fix.suppress.lint.api.attr=Add tools:ignore="{0}" attribute
+android.lint.fix.suppress.lint.api.annotation=Suppress: Add @SuppressLint("{0}") annotation
+android.lint.fix.suppress.lint.api.attr=Suppress: Add tools:ignore="{0}" attribute
android.lint.fix.replace.namespace=Replace with an auto resource namespace
android.export.unsigned.package.action.text=Export Unsigned Application Package
@@ -493,8 +496,6 @@
root.element.not.specified.error=Root element is not specified
directory.not.specified.error=Directory is not specified
android.manifest.merger.not.supported.error=Manifest merging is not supported. Please, reconfigure your manifest files
-invalid.file.resource.name.error=Resource file name must contain only lowercase a-z, 0-9, or _
-invalid.file.resource.name.error1=Invalid resource file name
invalid.resource.name.error=Invalid resource name ''{0}''
android.extract.style.title=Extract Android Style
android.inline.style.title=Inline Android Style
@@ -517,16 +518,27 @@
android.update.project.properties.dialog.title=Update Property Files
error.report.at.b.android=<html>Error Submitting Feedback: {0}<br>\
Consider creating an issue at \
- <a href="http://go/android-diamond-bug">Android Issue Tracker</a></html>
+ <a href="https://code.google.com/p/android/issues/list">Android Issue Tracker</a></html>
error.report.to.google.action=&Report to Google
android.startup.missing.jdk=<html>A Java Development Kit (JDK) installation is required. We could not locate a JDK installation on this machine.<br>\
If you have one, provide its location below. Otherwise, please download and install <a href\="http\://www.oracle.com/technetwork/java/javase/downloads/index.html">JDK 6</a>.</html>
-android.startup.missing.sdk=Provide the path to the Android SDK
+android.startup.missing.sdk=<html>Please provide the path to the Android SDK.<br>If you do not have the Android SDK, you can obtain it from <a href="http://d.android.com">d.android.com</a>.</html>
android.startup.missing.both=<html>A Java Development Kit (JDK) installation is required. We could not locate a JDK installation on this machine.<br>\
If you have one, provide its location below. Otherwise, please download and install <a href\="http\://www.oracle.com/technetwork/java/javase/downloads/index.html">JDK 6</a>.\
- If you do not have the Android SDK, you can obtain it from <a href="http://d.android.com">d.android.com</a>.</html>
+ <br>If you do not have the Android SDK, you can obtain it from <a href="http://d.android.com">d.android.com</a>.</html>
android.navigation.file.type.description=Android navigation Files
+android.version.check.too.old=<html>This version of Android Studio requires Android SDK Tools revision {0} or above.<br>Current revision is {1}.<br>Please update your SDK Tools to the latest version.</html>
+android.refactoring.rtl.addsupport.title=Add Right-To-Left (RTL) Support
+android.refactoring.rtl.addsupport.dialog.title=Add Right-To-Left (RTL) Support...
+android.refactoring.rtl.addsupport.dialog.ok.button.text=Run
+android.refactoring.rtl.addsupport.dialog.apply.button.text=Press the "Do RTL Refactor" button at the bottom of the search results panel to proceed with the right-to-left (RTL) refactoring\n
+android.refactoring.rtl.addsupport.dialog.label.text=This refactoring will add RTL support to your Android App.\n\nPlease check the following options:\n
+android.refactoring.rtl.addsupport.dialog.option.label.update.manifest.text=update AndroidManifest.xml
+android.refactoring.rtl.addsupport.dialog.option.label.update.layouts.text=update layouts files
+android.refactoring.rtl.addsupport.dialog.option.label.layouts.options.txt=Layout options
+android.refactoring.rtl.addsupport.dialog.option.label.layouts.options.replace.leftright.txt=replace left/right properties with start/end properties
+android.refactoring.rtl.addsupport.dialog.option.label.layouts.options.generate.v17.txt=generate v17 versions
create.on.click.handler.intention.text=Create onClick event handler
android.inspections.on.click.missing.name=onClick handler is missing in the related activity
android.inspections.on.click.missing.problem=Method ''{0}'' is missing in ''{1}'' or has incorrect signature
-android.inspections.on.click.missing.incorrect.signature=Method ''{0}'' in ''{1}'' has incorrect signature
\ No newline at end of file
+android.inspections.on.click.missing.incorrect.signature=Method ''{0}'' in ''{1}'' has incorrect signature
diff --git a/android/src/META-INF/androidstudio.xml b/android/src/META-INF/androidstudio.xml
index 409a0d9..6681060 100644
--- a/android/src/META-INF/androidstudio.xml
+++ b/android/src/META-INF/androidstudio.xml
@@ -31,5 +31,11 @@
<add-to-group group-id="MainToolBar" anchor="before" relative-to-action="HelpTopics" />
</group>
+
+ <action id="AndroidAddRTLSupport" class="com.android.tools.idea.actions.AndroidAddRtlSupportAction"
+ text="Add right-to-left (RTL) support where possible..." description="Add right-to-left (RTL) support where possible">
+ <add-to-group group-id="RefactoringMenu"/>
+ </action>
+
</actions>
</idea-plugin>
diff --git a/android/src/META-INF/plugin.xml b/android/src/META-INF/plugin.xml
index f430584..90d5513 100755
--- a/android/src/META-INF/plugin.xml
+++ b/android/src/META-INF/plugin.xml
@@ -24,6 +24,10 @@
<skipForDefaultProject/>
<headless-implementation-class></headless-implementation-class>
</component>
+ <component>
+ <implementation-class>com.android.tools.idea.gradle.project.AndroidGradleProjectComponent</implementation-class>
+ <skipForDefaultProject/>
+ </component>
</project-components>
<actions>
<action id="NewActivity" class="com.android.tools.idea.actions.AndroidNewActivityAction">
@@ -45,6 +49,10 @@
<action id="Android.ReImportProject" class="com.android.tools.idea.gradle.actions.ReImportProjectAction" icon="AndroidIcons.GradleSync">
<add-to-group group-id="AndroidToolsGroup" anchor="last"/>
</action>
+ <group id="Internal.Android" text="Android" popup="true" internal="true">
+ <action internal="true" id="Android.CleanImportProject" class="com.android.tools.idea.gradle.actions.CleanImportProjectAction" />
+ <add-to-group group-id="Internal"/>
+ </group>
<action id="Android.RunDdms" class="org.jetbrains.android.actions.AndroidRunDdmsAction" icon="AndroidIcons.Android">
<add-to-group group-id="AndroidToolsGroup" anchor="last"/>
</action>
@@ -107,17 +115,23 @@
<depends optional="true" config-file="eclipse.xml">org.jetbrains.idea.eclipse</depends>
<extensions defaultExtensionNs="com.intellij">
+ <externalSystemNotificationExtension implementation="com.android.tools.idea.gradle.service.notification.GradleNotificationExtension"/>
<buildProcess.parametersProvider implementation="com.android.tools.idea.gradle.compiler.AndroidGradleBuildProcessParametersProvider"/>
<externalProjectDataService implementation="com.android.tools.idea.gradle.service.AndroidProjectDataService" />
<externalProjectDataService implementation="com.android.tools.idea.gradle.service.GradleProjectDataService" />
+ <externalProjectDataService implementation="com.android.tools.idea.gradle.service.ProjectImportEventMessageDataService" />
<lang.foldingBuilder language="JAVA" implementationClass="com.android.tools.idea.folding.ResourceFoldingBuilder" />
<lang.foldingBuilder language="XML" implementationClass="com.android.tools.idea.folding.ResourceFoldingBuilder" />
<codeFoldingOptionsProvider instance="com.android.tools.idea.folding.AndroidCodeFoldingOptionsProvider"/>
<applicationService serviceInterface="com.android.tools.idea.folding.AndroidFoldingSettings"
serviceImplementation="com.android.tools.idea.folding.AndroidFoldingSettings"/>
- <applicationService serviceImplementation="com.android.tools.idea.gradle.GradleProjectImporter"/>
+ <applicationService serviceImplementation="com.android.tools.idea.gradle.project.GradleProjectImporter"/>
<exportable serviceInterface="com.android.tools.idea.folding.AndroidFoldingSettings"/>
+ <projectConfigurable instance="com.android.tools.idea.gradle.compiler.GradleCompilerSettingsConfigurable" id="gradle.compiler"
+ displayName="Gradle" parentId="project.propCompiler"/>
+ <compiler.optionsManager implementation="com.android.tools.idea.gradle.compiler.HideCompilerOptions" />
+
<errorHandler implementation="com.android.tools.idea.diagnostics.error.ErrorReporter"/>
<statisticsService implementationClass="com.android.tools.idea.stats.AndroidStatisticsService" key="android-studio" />
<dom.fileDescription implementation="org.jetbrains.android.dom.drawable.DrawableStateListDomFileDescription"/>
@@ -153,6 +167,7 @@
<lang.commenter language="AIDL" implementationClass="com.intellij.lang.java.JavaCommenter"/>
-->
<fileEditorProvider implementation="com.android.tools.idea.editors.NinePatchEditorProvider" />
+ <fileEditorProvider implementation="com.android.tools.idea.editors.vmtrace.VmTraceEditorProvider" />
<fileEditorProvider implementation="com.android.tools.idea.editors.navigation.NavigationEditorProvider" />
<runConfigurationProducer implementation="org.jetbrains.android.run.AndroidConfigurationProducer"/>
@@ -165,6 +180,7 @@
<framework.detector implementation="org.jetbrains.android.facet.AndroidFrameworkDetector"/>
<fileTemplateGroup implementation="org.jetbrains.android.AndroidFileTemplateProvider"/>
+ <projectTemplatesFactory implementation="com.android.tools.idea.wizard.TemplateWizardProjectTemplateFactory"/>
<projectTemplatesFactory implementation="org.jetbrains.android.newProject.AndroidProjectTemplatesFactory"/>
<compiler implementation="org.jetbrains.android.compiler.AndroidIncludingCompiler"/>
@@ -227,6 +243,7 @@
<globalInspection hasStaticDescription="true" shortName="AndroidLintCommitPrefEdits" displayName="Missing commit() on SharedPreference editor" groupKey="android.lint.inspections.group.name" bundle="messages.AndroidBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintCommitPrefEditsInspection"/>
<globalInspection hasStaticDescription="true" shortName="AndroidLintContentDescription" displayName="Image without contentDescription" groupKey="android.lint.inspections.group.name" bundle="messages.AndroidBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintContentDescriptionInspection"/>
<globalInspection hasStaticDescription="true" shortName="AndroidLintCutPasteId" displayName="Likely cut & paste mistakes" groupKey="android.lint.inspections.group.name" bundle="messages.AndroidBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintCutPasteIdInspection"/>
+ <globalInspection hasStaticDescription="true" shortName="AndroidLintDeviceAdmin" displayName="Device Admin cannot be deactivated" groupKey="android.lint.inspections.group.name" bundle="messages.AndroidBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintDeviceAdminInspection"/>
<globalInspection hasStaticDescription="true" shortName="AndroidLintDisableBaselineAlignment" displayName="Missing baselineAligned attribute" groupKey="android.lint.inspections.group.name" bundle="messages.AndroidBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintDisableBaselineAlignmentInspection"/>
<globalInspection hasStaticDescription="true" shortName="AndroidLintDrawAllocation" displayName="Memory allocations within drawing code" groupKey="android.lint.inspections.group.name" bundle="messages.AndroidBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintDrawAllocationInspection"/>
<globalInspection hasStaticDescription="true" shortName="AndroidLintDuplicateActivity" displayName="Activity registered more than once" groupKey="android.lint.inspections.group.name" bundle="messages.AndroidBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintDuplicateActivityInspection"/>
@@ -278,6 +295,7 @@
<globalInspection hasStaticDescription="true" shortName="AndroidLintMissingId" displayName="Fragments should specify an id or tag" groupKey="android.lint.inspections.group.name" bundle="messages.AndroidBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintMissingIdInspection"/>
<globalInspection hasStaticDescription="true" shortName="AndroidLintMissingPrefix" displayName="Missing Android XML namespace" groupKey="android.lint.inspections.group.name" bundle="messages.AndroidBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintMissingPrefixInspection"/>
<globalInspection hasStaticDescription="true" shortName="AndroidLintMissingQuantity" displayName="Missing quantity translation" groupKey="android.lint.inspections.group.name" bundle="messages.AndroidBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintMissingQuantityInspection"/>
+ <globalInspection hasStaticDescription="true" shortName="AndroidLintMissingSuperCall" displayName="Missing Super Call" groupKey="android.lint.inspections.group.name" bundle="messages.AndroidBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintMissingSuperCallInspection"/>
<globalInspection hasStaticDescription="true" shortName="AndroidLintMissingTranslation" displayName="Incomplete translation" groupKey="android.lint.inspections.group.name" bundle="messages.AndroidBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintMissingTranslationInspection"/>
<globalInspection hasStaticDescription="true" shortName="AndroidLintMissingVersion" displayName="Missing application name/version" groupKey="android.lint.inspections.group.name" bundle="messages.AndroidBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintMissingVersionInspection"/>
<globalInspection hasStaticDescription="true" shortName="AndroidLintMultipleUsesSdk" displayName="Multiple <uses-sdk> elements in the manifest" groupKey="android.lint.inspections.group.name" bundle="messages.AndroidBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintMultipleUsesSdkInspection"/>
@@ -404,6 +422,7 @@
<sdkType implementation="org.jetbrains.android.sdk.AndroidSdkType"/>
<gotoDeclarationHandler implementation="org.jetbrains.android.AndroidGotoDeclarationHandler"/>
+ <importFilter implementation="com.android.tools.idea.editors.AndroidImportFilter" />
<refactoring.safeDeleteProcessor id="android_component" order="before javaProcessor"
implementation="org.jetbrains.android.AndroidComponentSafeDeleteProcessor"/>
<refactoring.safeDeleteProcessor id="android_resource_file" implementation="org.jetbrains.android.AndroidResourceFileSafeDeleteProcessor"/>
@@ -437,6 +456,7 @@
<predefinedCodeStyle implementation="org.jetbrains.android.formatter.AndroidXmlPredefinedCodeStyle"/>
<editorNotificationProvider implementation="org.jetbrains.android.formatter.AndroidCodeStyleNotificationProvider"/>
<compiler.buildTargetScopeProvider implementation="org.jetbrains.android.compiler.AndroidBuildTargetScopeProvider"/>
+ <compiler.buildTargetScopeProvider implementation="com.android.tools.idea.gradle.compiler.AndroidGradleBuildTargetScopeProvider"/>
<!-- Temporarily disabled: need to display counts for files in multiple folders, and investigate bug
where cloned layout files do not appear etc.
@@ -446,7 +466,6 @@
-->
<spellchecker.bundledDictionaryProvider implementation="org.jetbrains.android.spellchecker.AndroidBundledDictionaryProvider"/>
<projectStructureDetector implementation="org.jetbrains.android.newProject.AndroidProjectStructureDetector"/>
- <postStartupActivity implementation="com.android.tools.idea.gradle.startup.GradleStartupActivity" order="first"/>
<filetype.stubBuilder filetype="JAVA" implementationClass="org.jetbrains.android.AndroidSdkSourcesStubBuilder"/>
<sdkResolveScopeProvider implementation="org.jetbrains.android.AndroidSdkResolveScopeProvider"/>
<lookup.charFilter implementation="org.jetbrains.android.dom.AndroidXmlCharFilter" order="before xml"/>
diff --git a/android/src/com/android/navigation/EventDispatcher.java b/android/src/com/android/navigation/EventDispatcher.java
index b923208..016a81d 100644
--- a/android/src/com/android/navigation/EventDispatcher.java
+++ b/android/src/com/android/navigation/EventDispatcher.java
@@ -15,13 +15,13 @@
*/
package com.android.navigation;
-import com.android.annotations.Nullable;
+import com.android.annotations.NonNull;
import java.util.ArrayList;
public class EventDispatcher<E> extends ArrayList<Listener<E>> implements Listener<E> {
@Override
- public void notify(@Nullable E event) {
+ public void notify(@NonNull E event) {
for (Listener<E> listener : this) {
listener.notify(event);
}
diff --git a/android/src/com/android/navigation/Listener.java b/android/src/com/android/navigation/Listener.java
index e693ce1..55c248d 100644
--- a/android/src/com/android/navigation/Listener.java
+++ b/android/src/com/android/navigation/Listener.java
@@ -15,8 +15,8 @@
*/
package com.android.navigation;
-import com.android.annotations.Nullable;
+import com.android.annotations.NonNull;
public interface Listener<E> {
- void notify(@Nullable E event);
+ void notify(@NonNull E event);
}
diff --git a/android/src/com/android/navigation/Locator.java b/android/src/com/android/navigation/Locator.java
new file mode 100644
index 0000000..50c3ea2
--- /dev/null
+++ b/android/src/com/android/navigation/Locator.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.navigation;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.annotations.Property;
+
+public class Locator {
+ @NonNull
+ private final State state;
+ private String viewName;
+
+ public Locator(@NonNull @Property("state") State state) {
+ this.state = state;
+ }
+
+ private Locator(@NonNull State state, @Nullable String viewName) {
+ this.state = state;
+ this.viewName = viewName;
+ }
+
+ public static Locator of(@NonNull State state, @Nullable String viewName) {
+ return new Locator(state, viewName);
+ }
+
+ @NonNull
+ public State getState() {
+ return state;
+ }
+
+ @SuppressWarnings("UnusedDeclaration")
+ public String getViewName() {
+ return viewName;
+ }
+
+ public void setViewName(@Nullable String viewName) {
+ this.viewName = viewName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Locator locator = (Locator)o;
+
+ if (!state.equals(locator.state)) return false;
+ if (viewName != null ? !viewName.equals(locator.viewName) : locator.viewName != null) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = state.hashCode();
+ result = 31 * result + (viewName != null ? viewName.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/android/src/com/android/navigation/NavigationModel.java b/android/src/com/android/navigation/NavigationModel.java
index d47b7da..9242311 100644
--- a/android/src/com/android/navigation/NavigationModel.java
+++ b/android/src/com/android/navigation/NavigationModel.java
@@ -15,28 +15,90 @@
*/
package com.android.navigation;
+import com.android.annotations.NonNull;
+
import java.util.ArrayList;
-public class NavigationModel extends ArrayList<Transition> {
- private static final Void NON_EVENT = null;
+public class NavigationModel {
+ public static class Event {
+ public enum Operation {INSERT, UPDATE, DELETE}
- private final EventDispatcher<Void> listeners = new EventDispatcher<Void>();
+ public final Operation operation;
+ public final Class<?> operandType;
- @Override
+ private Event(@NonNull Operation operation, @NonNull Class operandType) {
+ this.operation = operation;
+ this.operandType = operandType;
+ }
+
+ private static Event of(@NonNull Operation operation, @NonNull Class operandType) {
+ return new Event(operation, operandType);
+ }
+
+ public static Event insert(@NonNull Class operandType) {
+ return of(Operation.INSERT, operandType);
+ }
+
+ public static Event update(@NonNull Class operandType) {
+ return of(Operation.UPDATE, operandType);
+ }
+
+ public static Event delete(@NonNull Class operandType) {
+ return of(Operation.DELETE, operandType);
+ }
+ }
+
+ private final EventDispatcher<Event> listeners = new EventDispatcher<Event>();
+
+ private final ArrayList<State> states = new ArrayList<State>();
+ private final ArrayList<Transition> transitions = new ArrayList<Transition>();
+
+ // todo change return type to List<State>
+ public ArrayList<State> getStates() {
+ return states;
+ }
+
+ public ArrayList<Transition> getTransitions() {
+ return transitions;
+ }
+
+ public void addState(State state) {
+ states.add(state);
+ listeners.notify(Event.insert(State.class));
+ }
+
+ public void removeState(State state) {
+ states.remove(state);
+ for (Transition t : new ArrayList<Transition>(transitions)) {
+ if (t.getSource().getState() == state || t.getDestination().getState() == state) {
+ remove(t);
+ }
+ }
+ listeners.notify(Event.delete(State.class));
+ }
+
+ private void updateStates(State state) {
+ if (!states.contains(state)) {
+ states.add(state);
+ }
+ }
+
public boolean add(Transition transition) {
- boolean result = super.add(transition);
- listeners.notify(NON_EVENT);
+ boolean result = transitions.add(transition);
+ // todo remove this
+ updateStates(transition.getSource().getState());
+ updateStates(transition.getDestination().getState());
+ listeners.notify(Event.insert(Transition.class));
return result;
}
- @Override
- public boolean remove(Object o) {
- boolean result = super.remove(o);
- listeners.notify(NON_EVENT);
+ public boolean remove(Transition transition) {
+ boolean result = transitions.remove(transition);
+ listeners.notify(Event.delete(Transition.class));
return result;
}
- public EventDispatcher<Void> getListeners() {
+ public EventDispatcher<Event> getListeners() {
return listeners;
}
diff --git a/android/src/com/android/navigation/Point.java b/android/src/com/android/navigation/Point.java
new file mode 100644
index 0000000..f798063
--- /dev/null
+++ b/android/src/com/android/navigation/Point.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.navigation;
+
+import com.android.annotations.Property;
+
+public class Point {
+ public static final Point ORIGIN = new Point(0, 0);
+
+ public final int x;
+ public final int y;
+
+ public Point(@Property("x") int x, @Property("y") int y) {
+ this.x = x;
+ this.y = y;
+ }
+}
diff --git a/android/src/com/android/navigation/Properties.java b/android/src/com/android/navigation/Properties.java
new file mode 100644
index 0000000..2ffbdff
--- /dev/null
+++ b/android/src/com/android/navigation/Properties.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.navigation;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class Properties {
+ static class PropertyAccessException extends Exception {
+ private PropertyAccessException(Throwable throwable) {
+ super(throwable);
+ }
+ }
+
+ abstract static class Property<T> {
+ abstract String getName();
+
+ abstract Class getType();
+
+ abstract Object getValue(T o) throws PropertyAccessException;
+ }
+
+ static class FieldProperty<T> extends Property<T> {
+ private final Field field;
+
+ FieldProperty(Field field) {
+ this.field = field;
+ }
+
+ @Override
+ String getName() {
+ return field.getName();
+ }
+
+ @Override
+ Class getType() {
+ return field.getType();
+ }
+
+ @Override
+ Object getValue(T o) throws PropertyAccessException {
+ try {
+ return field.get(o);
+ }
+ catch (IllegalAccessException e) {
+ throw new PropertyAccessException(e);
+ }
+ }
+ }
+
+ static class MethodProperty<T> extends Property<T> {
+ private final Method method;
+
+ MethodProperty(Method method) {
+ this.method = method;
+ }
+
+ @Override
+ String getName() {
+ return Utilities.getPropertyName(method);
+ }
+
+ @Override
+ Class getType() {
+ return method.getReturnType();
+ }
+
+ @Override
+ Object getValue(T o) throws PropertyAccessException {
+ try {
+ return method.invoke(o);
+ }
+ catch (IllegalAccessException e) {
+ throw new PropertyAccessException(e);
+ }
+ catch (InvocationTargetException e) {
+ throw new PropertyAccessException(e);
+ }
+ }
+ }
+
+ private static Method[] findGetters(Class c) {
+ List<Method> methods = new ArrayList<Method>();
+ for (Method m : c.getMethods()) {
+ if (!Modifier.isStatic(m.getModifiers()) && m.getParameterTypes().length == 0 && m.getName().startsWith("get")) { // todo or "is"
+ if (m.getName().equals("getListeners")) { // todo remove
+ continue;
+ }
+ methods.add(m);
+ }
+ }
+ /*
+ // Put all the primitives first, and ensure that property order is not subject to unstable method ordering
+ Collections.sort(methods, new Comparator<Method>() {
+ @Override
+ public int compare(Method m1, Method m2) {
+ boolean p1 = isPrimitive(m1.getReturnType());
+ boolean p2 = isPrimitive(m2.getReturnType());
+ if (p1 != p2) {
+ return p1 ? -1 : 1;
+ }
+ return m1.getName().compareTo(m2.getName());
+ }
+ });
+ */
+ Collections.sort(methods, new Comparator<Method>() {
+ @Override
+ public int compare(Method m1, Method m2) {
+ return m1.getName().compareTo(m2.getName());
+ }
+ });
+ return methods.toArray(new Method[methods.size()]);
+ }
+
+ static Property[] computeProperties(Class c) {
+ List<Property> result = new ArrayList<Property>();
+ for (Field f : c.getFields()) {
+ if (!Modifier.isStatic(f.getModifiers())) {
+ result.add(new FieldProperty(f));
+ }
+ }
+ for (Method m : findGetters(c)) {
+ result.add(new MethodProperty(m));
+ }
+ return result.toArray(new Property[result.size()]);
+ }
+}
diff --git a/android/src/com/android/navigation/ReflectiveHandler.java b/android/src/com/android/navigation/ReflectiveHandler.java
index f21d4e1..fa398a3 100644
--- a/android/src/com/android/navigation/ReflectiveHandler.java
+++ b/android/src/com/android/navigation/ReflectiveHandler.java
@@ -15,38 +15,57 @@
*/
package com.android.navigation;
+import com.android.annotations.Nullable;
import com.android.annotations.Property;
import org.xml.sax.*;
+import org.xml.sax.Locator;
import org.xml.sax.helpers.DefaultHandler;
-
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
import java.util.*;
class ReflectiveHandler extends DefaultHandler {
// public static final List<String> DEFAULT_PACKAGES = Arrays.<String>asList("java.lang", "android.view", "android.widget");
public static final List<String> DEFAULT_PACKAGES = Arrays.asList();
public static final List<String> DEFAULT_CLASSES = Arrays.asList();
+ public static final String[] EMPTY_STRING_ARRAY = new String[0];
private final List<String> packagesImports = new ArrayList<String>(DEFAULT_PACKAGES);
private final List<String> classImports = new ArrayList<String>(DEFAULT_CLASSES);
- private final ErrorHandler errorHandler;
- private final Stack<Object> stack;
+ private final MyErrorHandler errorHandler;
+ private final Stack<ElementInfo> stack = new Stack<ElementInfo>();
private final Map<String, Object> idToValue = new HashMap<String, Object>();
-
- private Locator documentLocator;
+ private final Map<Class, Constructor> classToConstructor = new IdentityHashMap<Class, Constructor>();
+ private final Map<Constructor, String[]> constructorToParameterNames = new IdentityHashMap<Constructor, String[]>();
public Object result;
+ static class MyErrorHandler {
+ final ErrorHandler errorHandler;
+ Locator documentLocator;
+
+ MyErrorHandler(ErrorHandler errorHandler) {
+ this.errorHandler = errorHandler;
+ }
+
+ void handleWarning(Exception e) throws SAXException {
+ errorHandler.warning(new SAXParseException(e.getMessage(), documentLocator, e));
+ }
+
+ void handleError(Exception e) throws SAXException {
+ errorHandler.error(new SAXParseException(e.getMessage(), documentLocator, e));
+ }
+ }
+
public ReflectiveHandler(ErrorHandler errorHandler) {
- this.errorHandler = errorHandler;
- this.stack = new Stack<Object>();
+ this.errorHandler = new MyErrorHandler(errorHandler);
}
@Override
public void setDocumentLocator(Locator documentLocator) {
- this.documentLocator = documentLocator;
+ errorHandler.documentLocator = documentLocator;
}
private void processNameSpace(String nameSpace) {
@@ -66,15 +85,16 @@
}
public Class getClassForName(String tag) throws ClassNotFoundException {
+ String simpleName = Utilities.capitalize(tag);
ClassLoader classLoader = getClass().getClassLoader();
for (String clazz : classImports) {
- if (clazz.endsWith("." + tag)) {
+ if (clazz.endsWith("." + simpleName)) {
return classLoader.loadClass(clazz);
}
}
for (String pkg : packagesImports) {
try {
- return classLoader.loadClass(pkg + "." + tag);
+ return classLoader.loadClass(pkg + "." + simpleName);
}
catch (ClassNotFoundException e) {
// Class was not defined by this import, continue.
@@ -83,16 +103,14 @@
throw new ClassNotFoundException("Could not find class for tag: " + tag);
}
- private static class PropertyAnnotationNotFoundException extends Exception {
- }
-
- private static String getName(Annotation[] parameterAnnotation) throws PropertyAnnotationNotFoundException {
+ @Nullable
+ private static String getName(Annotation[] parameterAnnotation) {
for (Annotation a : parameterAnnotation) {
if (a instanceof Property) {
return ((Property)a).value();
}
}
- throw new PropertyAnnotationNotFoundException();
+ return null;
}
private static Object valueFor(Class<?> type, String stringValue)
@@ -100,10 +118,26 @@
if (type == String.class) {
return stringValue;
}
+ if (type.isPrimitive()) {
+ type = Utilities.wrapperForPrimitiveType(type);
+ }
return type.getConstructor(String.class).newInstance(stringValue);
}
- private static Object createInstance(Class clz, Map<String, String> attributes) throws InstantiationException {
+ private static String[] findParameterNames(@Nullable Constructor constructor) {
+ if (constructor == null) {
+ return EMPTY_STRING_ARRAY;
+ }
+ Annotation[][] annotations = constructor.getParameterAnnotations();
+ String[] result = new String[annotations.length];
+ for (int i = 0; i < annotations.length; i++) {
+ result[i] = getName(annotations[i]);
+ }
+ return result;
+ }
+
+ @Nullable
+ private static Constructor findConstructor(Class clz) {
Constructor[] constructors = clz.getConstructors();
Arrays.sort(constructors, new Comparator<Constructor>() {
@Override
@@ -112,56 +146,158 @@
}
});
for (Constructor constructor : constructors) {
- try {
- return constructor.newInstance(getArguments(constructor, attributes));
+ if (!Modifier.isPublic(constructor.getModifiers())) {
+ continue;
}
- catch (PropertyAnnotationNotFoundException e) {
- // ok, try next constructor
- }
- catch (IllegalAccessException e) {
- throw new RuntimeException(e);
- }
- catch (InstantiationException e) {
- throw new RuntimeException(e);
- }
- catch (NoSuchMethodException e) {
- throw new RuntimeException(e);
- }
- catch (InvocationTargetException e) {
- throw new RuntimeException(e);
- }
+ return constructor;
}
- throw new InstantiationException();
+ return null;
}
- private static Object[] getArguments(Constructor constructor, Map<String, String> attributes) throws
- NoSuchMethodException,
- IllegalAccessException,
- InvocationTargetException,
- InstantiationException,
- PropertyAnnotationNotFoundException {
- Class[] types = constructor.getParameterTypes();
- Annotation[][] annotations = constructor.getParameterAnnotations();
- Object[] result = new Object[annotations.length];
- for (int i = 0; i < annotations.length; i++) {
- result[i] = valueFor(types[i], attributes.remove(getName(annotations[i]))); // note destructive
+ @Nullable
+ private Constructor getConstructor(Class clazz) {
+ Constructor result = classToConstructor.get(clazz);
+ if (result == null) {
+ classToConstructor.put(clazz, result = findConstructor(clazz));
}
return result;
}
- private static void installInOuter(Object outer, String propertyName, Object instance)
- throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
- if (propertyName != null) {
- applySetter(outer, propertyName, instance);
+ private String[] getParameterNames(@Nullable Constructor constructor) {
+ String[] result = constructorToParameterNames.get(constructor);
+ if (result == null) {
+ constructorToParameterNames.put(constructor, result = findParameterNames(constructor));
}
- else {
- applyMethod(outer, "add", instance);
+ return result;
+ }
+
+ Object[] getParameterValues(Constructor constructor, Map<String, String> attributes, List<ElementInfo> elements)
+ throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, SAXException {
+ String[] parameterNames = getParameterNames(constructor);
+ Class[] types = constructor.getParameterTypes();
+ Object[] result = new Object[parameterNames.length];
+ for (int i = 0; i < parameterNames.length; i++) {
+ String parameterName = parameterNames[i];
+ String stringValue = attributes.get(parameterName);
+ if (stringValue != null) {
+ result[i] = valueFor(types[i], stringValue);
+ }
+ else {
+ ElementInfo param = getParam(elements, parameterName, constructor);
+ param.myValueAlreadySetInOuter = true;
+ result[i] = param.getValue();
+ }
+ }
+ return result;
+ }
+
+ private static ElementInfo getParam(List<ElementInfo> elements, String parameterName, Object constructor) throws SAXException {
+ for (ElementInfo element : elements) {
+ if (parameterName.equals(element.name)) {
+ return element;
+ }
+ }
+ throw new SAXException("Unspecified parameter, " + parameterName + ", in " + constructor);
+ }
+
+ static abstract class Evaluator {
+ abstract Object evaluate() throws SAXException;
+ }
+
+ static class LazyValue {
+ private static Object UNSET = new Object();
+
+ private Evaluator evaluator;
+ private Object value = UNSET;
+
+ Object getValue() throws SAXException {
+ if (value == UNSET) {
+ value = evaluator.evaluate();
+ }
+ return value;
+ }
+
+ void setEvaluator(Evaluator evaluator) {
+ this.evaluator = evaluator;
+ }
+
+ void setValue(Object value) {
+ this.value = value;
}
}
- private static void applySetter(Object outer, String propertyName, Object instance)
- throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
- getSetter(outer.getClass(), propertyName).invoke(outer, instance);
+ static class ElementInfo {
+ public Class type;
+ public String name;
+ public boolean myValueAlreadySetInOuter = false;
+ public Map<String, String> attributes;
+ public List<ElementInfo> elements = new ArrayList<ElementInfo>();
+ private LazyValue lazyValue = new LazyValue();
+
+ public Object getValue() throws SAXException {
+ return lazyValue.getValue();
+ }
+
+ public void setValue(Object value) {
+ lazyValue.setValue(value);
+ }
+
+ public void setEvaluator(Evaluator evaluator) {
+ lazyValue.setEvaluator(evaluator);
+ }
+
+ private void installAttributes(MyErrorHandler errorHandler, String[] constructorParameterNames) throws SAXException {
+ for (Map.Entry<String, String> entry : attributes.entrySet()) {
+ String attributeName = entry.getKey();
+ if (Utilities.RESERVED_ATTRIBUTES.contains(attributeName) || Utilities.contains(constructorParameterNames, attributeName)) {
+ continue;
+ }
+ try {
+ Method setter = getSetter(type, attributeName);
+ Class argType = Utilities.wrapperForPrimitiveType(setter.getParameterTypes()[0]);
+ setter.invoke(getValue(), valueFor(argType, entry.getValue()));
+ }
+ catch (NoSuchMethodException e) {
+ errorHandler.handleWarning(e);
+ }
+ catch (IllegalAccessException e) {
+ errorHandler.handleWarning(e);
+ }
+ catch (InvocationTargetException e) {
+ errorHandler.handleWarning(e);
+ }
+ catch (InstantiationException e) {
+ errorHandler.handleWarning(e);
+ }
+ }
+ }
+
+ private void installSubElements(MyErrorHandler errorHandler) throws SAXException {
+ for (ElementInfo element : elements) {
+ if (element.myValueAlreadySetInOuter) {
+ continue;
+ }
+ try {
+ Object outerValue = getValue();
+ if (!(Collection.class.isAssignableFrom(type))) { // todo remove Collection check
+ getSetter(outerValue.getClass(), element.name).invoke(outerValue, element.getValue());
+ }
+ else {
+ applyMethod(outerValue, "add", element.getValue());
+ }
+ }
+ catch (NoSuchMethodException e) {
+ errorHandler.handleError(e);
+ }
+ catch (IllegalAccessException e) {
+ errorHandler.handleError(e);
+ }
+ catch (InvocationTargetException e) {
+ errorHandler.handleError(e);
+ }
+ }
+ }
+
}
private static void applyMethod(Object target, String methodName, Object parameter)
@@ -179,83 +315,137 @@
}
}
- private void handleWarning(Exception e) throws SAXException {
- errorHandler.warning(new SAXParseException(e.getMessage(), documentLocator, e));
- }
-
- private void handleError(Exception e) throws SAXException {
- errorHandler.error(new SAXParseException(e.getMessage(), documentLocator, e));
+ private static Method getGetter(Class<?> type, String propertyName) throws NoSuchMethodException {
+ String getterMethodName = Utilities.getGetterMethodName(propertyName);
+ return type.getMethod(getterMethodName);
}
private static Method getSetter(Class<?> type, String propertyName) throws NoSuchMethodException {
- String getterMethodName = Utilities.getGetterMethodName(propertyName);
String setterMethodName = Utilities.getSetterMethodName(propertyName);
- Method getter = type.getMethod(getterMethodName);
- Class propertyType = getter.getReturnType();
+ Class propertyType = getGetter(type, propertyName).getReturnType();
return type.getMethod(setterMethodName, propertyType);
}
+ private String[] getConstructorParameterNames(Class type) {
+ if (type == null) {
+ return EMPTY_STRING_ARRAY;
+ }
+ return getParameterNames(getConstructor(type));
+ }
+
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
- Map<String, String> nameToValue = Utilities.toMap(attributes);
- String nameSpace = nameToValue.remove(Utilities.NAME_SPACE_TAG); // note destructive
+ final ElementInfo elementInfo = new ElementInfo();
+ elementInfo.name = qName;
+ elementInfo.attributes = Utilities.toMap(attributes);
+ String nameSpace = elementInfo.attributes.get(Utilities.NAME_SPACE_ATRIBUTE_NAME);
if (nameSpace != null) {
processNameSpace(nameSpace);
}
try {
- String idref = nameToValue.remove(Utilities.IDREF_ATTRIBUTE_NAME); // note destructive
- Object instance;
+ String idref = elementInfo.attributes.get(Utilities.IDREF_ATTRIBUTE_NAME);
if (idref != null) {
- instance = idToValue.get(idref);
+ if (!idToValue.containsKey(idref)) {
+ throw new SAXException("IDREF attribute, \"" + idref + "\" , was used before corresponding ID was defined.");
+ }
+ elementInfo.setValue(idToValue.get(idref));
}
else {
- instance = createInstance(getClassForName(qName), nameToValue);
- }
- String id = nameToValue.remove(Utilities.ID_ATTRIBUTE_NAME); // note destructive
- if (id != null) {
- idToValue.put(id, instance);
- }
-
- if (stack.size() != 0) {
- installInOuter(stack.getLast(), nameToValue.remove(Utilities.PROPERTY_ATTRIBUTE_NAME), instance); // note destructive
- }
- if (idref == null) {
- for (Map.Entry<String, String> entry : nameToValue.entrySet()) {
- try {
- Method setter = getSetter(instance.getClass(), entry.getKey());
- Class argType = Utilities.wrapperForPrimitiveType(setter.getParameterTypes()[0]);
- setter.invoke(instance, valueFor(argType, entry.getValue()));
- }
- catch (NoSuchMethodException e) {
- handleWarning(e);
- }
- catch (IllegalAccessException e) {
- handleWarning(e);
- }
- catch (InvocationTargetException e) {
- handleWarning(e);
- }
- catch (InstantiationException e) {
- handleWarning(e);
- }
+ elementInfo.type = getType(qName, elementInfo.attributes.get("class"));
+ if (elementInfo.type != null) {
+ elementInfo.setEvaluator(new Evaluator() {
+ @Override
+ Object evaluate() throws SAXException {
+ try {
+ Constructor constructor = getConstructor(elementInfo.type);
+ if (constructor == null) {
+ throw new SAXException("No Constructor found for " + elementInfo.name);
+ }
+ // note info.elements is changing under our feet
+ return constructor.newInstance(getParameterValues(constructor, elementInfo.attributes, elementInfo.elements));
+ }
+ catch (IllegalAccessException e) {
+ throw new SAXException(e);
+ }
+ catch (NoSuchMethodException e) {
+ throw new SAXException(e);
+ }
+ catch (InvocationTargetException e) {
+ throw new SAXException(e);
+ }
+ catch (InstantiationException e) {
+ throw new SAXException(e);
+ }
+ }
+ });
+ }
+ else {
+ final ElementInfo last = stack.getLast();
+ final Method getter = getGetter(last.type, qName);
+ elementInfo.myValueAlreadySetInOuter = true;
+ elementInfo.type = getter.getReturnType();
+ elementInfo.setEvaluator(new Evaluator() {
+ @Override
+ Object evaluate() throws SAXException {
+ try {
+ return getter.invoke(last.getValue());
+ }
+ catch (IllegalAccessException e) {
+ throw new SAXException(e);
+ }
+ catch (InvocationTargetException e) {
+ throw new SAXException(e);
+ }
+ }
+ });
}
}
- stack.push(instance);
+ String id = elementInfo.attributes.get(Utilities.ID_ATTRIBUTE_NAME);
+ if (id != null) {
+ idToValue.put(id, elementInfo.getValue());
+ }
+ stack.push(elementInfo);
}
catch (ClassNotFoundException e) {
- handleError(e);
- }
- catch (InvocationTargetException e) {
- handleError(e);
+ errorHandler.handleError(e);
}
catch (NoSuchMethodException e) {
- handleError(e);
+ errorHandler.handleError(e);
}
- catch (InstantiationException e) {
- handleError(e);
+ }
+
+ @Nullable
+ private Class getConstructorParameterType(@Nullable Constructor constructor, String name) {
+ if (constructor != null) {
+ String[] parameterNames = getParameterNames(constructor);
+ Class[] parameterTypes = constructor.getParameterTypes();
+ for (int i = 0; i < parameterNames.length; i++) {
+ if (parameterNames[i].equals(name)) {
+ return parameterTypes[i];
+ }
+ }
}
- catch (IllegalAccessException e) {
- handleError(e);
+ return null;
+ }
+
+ @Nullable
+ private Class getType(String qName, String className) throws ClassNotFoundException {
+ if (className != null) {
+ return getClass().getClassLoader().loadClass(className);
+ }
+ else {
+ try {
+ return getClassForName(qName);
+ }
+ catch (ClassNotFoundException e) {
+ Class outerType = stack.getLast().type;
+ try {
+ return getSetter(outerType, qName).getParameterTypes()[0];
+ }
+ catch (NoSuchMethodException e1) {
+ return getConstructorParameterType(getConstructor(outerType), qName);
+ }
+ }
}
}
@@ -265,7 +455,12 @@
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
- result = stack.pop();
+ ElementInfo elementInfo = stack.pop();
+ result = elementInfo.getValue();
+ elementInfo.installAttributes(errorHandler, getConstructorParameterNames(elementInfo.type));
+ elementInfo.installSubElements(errorHandler);
+ if (stack.size() != 0) {
+ stack.getLast().elements.add(elementInfo);
+ }
}
-
}
diff --git a/android/src/com/android/navigation/State.java b/android/src/com/android/navigation/State.java
index e2cb0a7..a77cfe0 100644
--- a/android/src/com/android/navigation/State.java
+++ b/android/src/com/android/navigation/State.java
@@ -15,11 +15,13 @@
*/
package com.android.navigation;
+import com.android.annotations.NonNull;
import com.android.annotations.Property;
public class State {
private final String controllerClassName;
private String xmlResourceName;
+ private Point location = Point.ORIGIN;
public State(@Property("controllerClassName") String controllerClassName) {
this.controllerClassName = controllerClassName;
@@ -37,6 +39,15 @@
this.xmlResourceName = xmlResourceName;
}
+ @NonNull
+ public Point getLocation() {
+ return location;
+ }
+
+ public void setLocation(@NonNull Point location) {
+ this.location = location;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/android/src/com/android/navigation/Transition.java b/android/src/com/android/navigation/Transition.java
index 5f35bbe..3db269e 100644
--- a/android/src/com/android/navigation/Transition.java
+++ b/android/src/com/android/navigation/Transition.java
@@ -16,28 +16,22 @@
package com.android.navigation;
import com.android.annotations.Property;
-import com.android.annotations.Nullable;
public class Transition {
private String type;
- private String viewIdentifier;
- private State source;
- private State destination;
+ private final Locator source;
+ private final Locator destination;
- public Transition(@Property("gesture") String type,
- @Property("source") State source,
- @Property("destination") State destination) {
+ public Transition(@Property("type") String type,
+ @Property("source") Locator source,
+ @Property("destination") Locator destination) {
this.type = type;
this.source = source;
this.destination = destination;
}
- public State getSource() {
- return source;
- }
-
- public void setSource(State source) {
- this.source = source;
+ public static Transition of(String type, State source, State destination) {
+ return new Transition(type, new Locator(source), new Locator(destination));
}
public String getType() {
@@ -48,28 +42,11 @@
this.type = type;
}
- public State getDestination() {
+ public Locator getSource() {
+ return source;
+ }
+
+ public Locator getDestination() {
return destination;
}
-
- public void setDestination(State destination) {
- this.destination = destination;
- }
-
- public String getViewIdentifier() {
- return viewIdentifier;
- }
-
- public void setViewIdentifier(@Nullable String viewIdentifier) {
- this.viewIdentifier = viewIdentifier;
- }
-
- @Override
- public String toString() {
- return "Navigation{" +
- "source='" + source + '\'' +
- ", gesture='" + type + '\'' +
- ", destination='" + destination + '\'' +
- '}';
- }
}
diff --git a/android/src/com/android/navigation/Utilities.java b/android/src/com/android/navigation/Utilities.java
index 10a17b9..5c1865e 100644
--- a/android/src/com/android/navigation/Utilities.java
+++ b/android/src/com/android/navigation/Utilities.java
@@ -18,14 +18,15 @@
import org.xml.sax.Attributes;
import java.lang.reflect.Method;
-import java.util.LinkedHashMap;
-import java.util.Map;
+import java.util.*;
class Utilities {
+ public static final String NAME_SPACE_ATRIBUTE_NAME = "ns";
public static final String ID_ATTRIBUTE_NAME = "id";
public static final String IDREF_ATTRIBUTE_NAME = "idref";
public static final String PROPERTY_ATTRIBUTE_NAME = "outer.property";
- public static final String NAME_SPACE_TAG = "ns";
+ public static final Set<String> RESERVED_ATTRIBUTES = new HashSet<String>(
+ Arrays.asList(NAME_SPACE_ATRIBUTE_NAME, ID_ATTRIBUTE_NAME, IDREF_ATTRIBUTE_NAME, PROPERTY_ATTRIBUTE_NAME));
public static Map<String, String> toMap(Attributes attributes) {
Map<String, String> nameToValue = new LinkedHashMap<String, String>();
@@ -88,4 +89,13 @@
}
throw new RuntimeException("Internal error");
}
+
+ static boolean contains(Object[] a, Object o) {
+ for (Object e : a) {
+ if (o.equals(e)) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/android/src/com/android/navigation/XMLWriter.java b/android/src/com/android/navigation/XMLWriter.java
index 30563cb..804c822 100644
--- a/android/src/com/android/navigation/XMLWriter.java
+++ b/android/src/com/android/navigation/XMLWriter.java
@@ -15,21 +15,40 @@
*/
package com.android.navigation;
+import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import java.io.OutputStream;
import java.io.PrintStream;
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.Map;
public class XMLWriter {
- public static final int UNDEFINED = -1;
- private final Map<Class, Property[]> classToProperties = new IdentityHashMap<Class, Property[]>();
+ public static final String UNDEFINED = null;
+ private final Map<Class, Properties.Property[]> classToProperties = new IdentityHashMap<Class, Properties.Property[]>();
private final PrintStream out;
private int level;
- private int idCount = 0;
+ private Map<Class, Integer> idCounts = new IdentityHashMap<Class, Integer>();
+
+ public XMLWriter(OutputStream out) {
+ this.out = new PrintStream(out);
+ }
+
+ private static boolean isPrimitive(Class type) {
+ return type.isPrimitive() || type == String.class;
+ }
+
+ private String nextId(Class c) {
+ Integer prev = idCounts.get(c);
+ if (prev == null) {
+ prev = -1;
+ }
+ int result = prev + 1;
+ idCounts.put(c, result);
+ return Utilities.decapitalize(c.getSimpleName()) + result;
+ }
private Map<Object, Info> objectToInfo = new IdentityHashMap<Object, Info>() {
@Override
@@ -42,226 +61,122 @@
}
};
- public XMLWriter(OutputStream out) {
- this.out = new PrintStream(out);
- }
-
- private static boolean isPrimitive(Class type) {
- return type.isPrimitive() || type == String.class;
- }
-
- static class PropertyAccessException extends Exception {
- private PropertyAccessException(Throwable throwable) {
- super(throwable);
- }
- }
-
static class Info {
- int id = UNDEFINED;
+ String id = UNDEFINED;
int count = 0;
}
- static class NameValue {
+ abstract class NameValue<T> {
public final String name;
- public final Object value;
+ public final T value;
- NameValue(String name, Object value) {
+ NameValue(String name, T value) {
this.name = name;
this.value = value;
}
+
+ public abstract void write();
+
+ public abstract void addToParent(Element parent);
}
- class NameValueList extends ArrayList<NameValue> {
- private final Object object;
-
- NameValueList(Object object) {
- this.object = object;
+ class Attribute<T> extends NameValue<T> {
+ Attribute(String name, T value) {
+ super(name, value);
}
- private void writeAttribute(String name, Object value) {
- add(new NameValue(name, value));
+ @Override
+ public void write() {
+ writeAttribute(name, value);
}
- private Class getElementClass() {
- for (NameValue p : this) {
- if (p.name == "class") {
- return (Class)p.value;
+ @Override
+ public void addToParent(Element parent) {
+ parent.attributes.add(this);
+ }
+ }
+
+
+ class ClassAttribute extends Attribute<Class> {
+ public ClassAttribute(String name, Class value) {
+ super(name, value);
+ }
+
+ /**
+ * This method is called when we are considering whether to add a class attribute to, for example,
+ * the value of the "state" property of a {@link Transition}. If the property is "get/setState()" we needn't record
+ * the fully qualified "State" class as {@link XMLReader} can be infer it from the type of the getter/setter pair.
+ */
+ @Override
+ public void addToParent(Element parent) {
+ if (parent.type != value) {
+ if (parent.tag == null) {
+ parent.tag = Utilities.decapitalize(value.getSimpleName());
+ }
+ else {
+ super.addToParent(parent);
}
}
- throw new RuntimeException("No class defined");
}
- private Collection<NameValue> attributes() {
- ArrayList<NameValue> result = new ArrayList<NameValue>();
- Info info = objectToInfo.get(object);
- if (info.count > 1) {
- NameValue nameValue = (info.id == UNDEFINED)
- ? new NameValue(Utilities.ID_ATTRIBUTE_NAME, info.id = idCount++)
- : new NameValue(Utilities.IDREF_ATTRIBUTE_NAME, info.id);
- result.add(nameValue);
- }
- for (NameValue p : this) {
- if (!(p.value instanceof NameValueList)) {
- result.add(p);
- }
- }
- return result;
+ @Override
+ public void write() {
+ writeAttribute(name, value.getName());
+ }
+ }
+
+ class Element extends NameValue<Object> {
+ public String tag;
+ public final Class type;
+ public final ArrayList<Attribute> attributes = new ArrayList<Attribute>();
+ public final ArrayList<Element> elements = new ArrayList<Element>();
+
+ Element(Class type, String name, Object value) {
+ super(name, value);
+ this.tag = name;
+ this.type = type;
}
- private Collection<NameValueList> body() {
- ArrayList<NameValueList> result = new ArrayList<NameValueList>();
- for (NameValue p : this) {
- Object value = p.value;
- if ((value instanceof NameValueList)) {
- result.add((NameValueList)value);
- }
- }
- return result;
- }
-
- public void writeElement() {
+ @Override
+ public void write() {
level++;
- Class aClass = getElementClass();
- String tag = aClass.getSimpleName();
-
- Collection<NameValue> attributes = attributes();
- Collection<NameValueList> body = body();
- boolean hasBody = body.size() != 0;
-
- if (attributes.size() == 0) {
- println("<" + tag + ">");
- }
- else {
- println("<" + tag);
- for (NameValue attribute : attributes) {
- if (attribute.name != "class") {
- XMLWriter.this.writeAttribute(attribute.name, attribute.value);
- }
+ String tag = this.tag == null ? "object" : this.tag;
+ print("<" + tag);
+ Info info = objectToInfo.get(value);
+ if (info.count > 1) {
+ if (info.id == UNDEFINED) {
+ writeAttribute(Utilities.ID_ATTRIBUTE_NAME, info.id = nextId(value.getClass()));
}
- if (hasBody) {
- println(">");
+ else {
+ writeAttribute(Utilities.IDREF_ATTRIBUTE_NAME, info.id);
}
}
-
- for (NameValueList element : body) {
- element.writeElement();
+ for (Attribute attribute : attributes) {
+ attribute.write();
}
-
- if (!hasBody) {
- println("/>");
- }
- else {
+ if (!elements.isEmpty()) {
+ out.println(">");
+ for (Element element : elements) {
+ element.write();
+ }
println("</" + tag + ">");
}
+ else {
+ out.println("/>");
+ }
level--;
}
- }
-
- abstract static class Property<T> {
- abstract String getName();
-
- abstract Class getType();
-
- abstract Object getValue(T o) throws PropertyAccessException;
- }
-
- static class FieldProperty<T> extends Property<T> {
- private final Field field;
-
- FieldProperty(Field field) {
- this.field = field;
- }
-
@Override
- String getName() {
- return field.getName();
- }
-
- @Override
- Class getType() {
- return field.getType();
- }
-
- @Override
- Object getValue(T o) throws PropertyAccessException {
- try {
- return field.get(o);
- }
- catch (IllegalAccessException e) {
- throw new PropertyAccessException(e);
- }
+ public void addToParent(Element parent) {
+ parent.elements.add(this);
}
}
- static class MethodProperty<T> extends Property<T> {
- private final Method method;
-
- MethodProperty(Method method) {
- this.method = method;
- }
-
- @Override
- String getName() {
- return Utilities.getPropertyName(method);
- }
-
- @Override
- Class getType() {
- return method.getReturnType();
- }
-
- @Override
- Object getValue(T o) throws PropertyAccessException {
- try {
- return method.invoke(o);
- }
- catch (IllegalAccessException e) {
- throw new PropertyAccessException(e);
- }
- catch (InvocationTargetException e) {
- throw new PropertyAccessException(e);
- }
- }
- }
-
- private static Method[] findGetters(Class c) {
- List<Method> methods = new ArrayList<Method>();
- for (Method m : c.getMethods()) {
- if (m.getName().startsWith("get")) { // todo or "is"
- methods.add(m);
- }
- }
- // Put all the primitives first, and ensure that property order is not subject to unstable method ordering
- Collections.sort(methods, new Comparator<Method>() {
- @Override
- public int compare(Method m1, Method m2) {
- boolean p1 = isPrimitive(m1.getReturnType());
- boolean p2 = isPrimitive(m2.getReturnType());
- if (p1 != p2) {
- return p1 ? -1 : 1;
- }
- return m1.getName().compareTo(m2.getName());
- }
- });
- return methods.toArray(new Method[methods.size()]);
- }
-
- private static Property[] computeProperties(Class c) {
- List<Property> result = new ArrayList<Property>();
- for (Field f : c.getFields()) {
- result.add(new FieldProperty(f));
- }
- for (Method m : findGetters(c)) {
- result.add(new MethodProperty(m));
- }
- return result.toArray(new Property[result.size()]);
- }
-
- public Property[] getProperties(Class c) {
- Property[] result = classToProperties.get(c);
+ public Properties.Property[] getProperties(Class c) {
+ Properties.Property[] result = classToProperties.get(c);
if (result == null) {
- classToProperties.put(c, result = computeProperties(c));
+ classToProperties.put(c, result = Properties.computeProperties(c));
}
return result;
}
@@ -269,12 +184,14 @@
public void write(Object o) {
println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
level = -1;
- traverse(o, null, true).writeElement();
+ NameValue traversal = traverse(Object.class, null, o, true);
+ traversal.write();
}
private void indent() {
for (int i = 0; i < level; i++) {
out.write(' ');
+ out.write(' ');
}
}
@@ -289,62 +206,53 @@
}
private void writeAttribute(String name, Object value) {
+ print("\n");
level++;
- println(name + " = \"" + value + "\"");
+ print(name + " = \"" + value + "\"");
level--;
}
- private NameValueList traverse(Object o, @Nullable String propertyName, boolean isTopLevel) {
- NameValueList result = new NameValueList(o);
- Class aClass = o.getClass();
+ private NameValue traverse(@NonNull Class type, String name, Object value, boolean isTopLevel) {
+ if (isPrimitive(type)) {
+ return new Attribute<Object>(name, value);
+ }
- result.writeAttribute("class", aClass);
+ if (type == Class.class) {
+ return new ClassAttribute(name, (Class)value);
+ }
+
+ Element result = new Element(type, name, value);
+ Class aClass = value.getClass();
if (isTopLevel) {
String packageName = aClass.getPackage().getName(); // todo deal with multiple packages
- result.writeAttribute(Utilities.NAME_SPACE_TAG, "http://schemas.android.com?import=" + packageName + ".*");
+ result.attributes.add(new Attribute<Object>(Utilities.NAME_SPACE_ATRIBUTE_NAME, "http://schemas.android.com?import=" + packageName + ".*"));
}
- if (propertyName != null) {
- result.writeAttribute(Utilities.PROPERTY_ATTRIBUTE_NAME, propertyName);
- }
-
- Info info = objectToInfo.get(o);
+ Info info = objectToInfo.get(value);
info.count++;
if (info.count > 1) {
return result;
}
- if (o instanceof Collection) {
- for (Object element : (Collection)o) {
- result.writeAttribute(null, traverse(element, null, false));
+ // recurse into each property, using reflection
+ for (Properties.Property p : getProperties(aClass)) {
+ try {
+ Object propertyValue = p.getValue(value);
+ if (propertyValue != null) {
+ traverse(p.getType(), p.getName(), propertyValue, false).addToParent(result);
+ }
+ }
+ catch (Properties.PropertyAccessException e) {
+ throw new RuntimeException(e);
}
}
- else {
- for (Property p : getProperties(aClass)) {
- if (p.getName().equals("class")) {
- continue;
- }
- String name = p.getName();
- try {
- Object value = p.getValue(o);
- if (value != null) {
- Class type = p.getType();
- if (isPrimitive(type)) {
- result.writeAttribute(name, value);
- }
- else {
- result.writeAttribute(name, traverse(value, name, false));
- }
- }
- }
- catch (PropertyAccessException e) {
- throw new RuntimeException(e);
- }
+ // special-case Collections
+ if (value instanceof Collection) {
+ for (Object element : (Collection)value) {
+ traverse(Object.class, null, element, false).addToParent(result);
}
}
return result;
}
-
-
}
diff --git a/android/src/com/android/tools/idea/actions/AndroidAddRtlSupportAction.java b/android/src/com/android/tools/idea/actions/AndroidAddRtlSupportAction.java
new file mode 100644
index 0000000..1660025
--- /dev/null
+++ b/android/src/com/android/tools/idea/actions/AndroidAddRtlSupportAction.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.idea.actions;
+
+import com.android.tools.idea.refactoring.rtl.RtlSupportManager;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.PlatformDataKeys;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+
+public class AndroidAddRtlSupportAction extends AnAction implements DumbAware {
+
+ public AndroidAddRtlSupportAction() {
+ super("Add right-to-left (RTL) support...");
+ }
+
+ @Override
+ public void actionPerformed(AnActionEvent e) {
+ Project project = PlatformDataKeys.PROJECT.getData(e.getDataContext());
+ if (project != null) {
+ new RtlSupportManager(project).showDialog();
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/actions/AndroidImportProjectAction.java b/android/src/com/android/tools/idea/actions/AndroidImportProjectAction.java
new file mode 100644
index 0000000..31b9634
--- /dev/null
+++ b/android/src/com/android/tools/idea/actions/AndroidImportProjectAction.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.actions;
+
+import com.android.SdkConstants;
+import com.google.common.collect.Lists;
+import com.intellij.ide.actions.OpenProjectFileChooserDescriptor;
+import com.intellij.ide.impl.NewProjectUtil;
+import com.intellij.ide.util.PropertiesComponent;
+import com.intellij.ide.util.newProjectWizard.AddModuleWizard;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.fileChooser.FileChooserDialog;
+import com.intellij.openapi.fileChooser.FileChooserFactory;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
+import com.intellij.projectImport.ProjectImportProvider;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Imports a new project into Android Studio.
+ * <p>
+ * This action replaces the default "Import Project..." changing the behavior of project imports. If the user selects a project's root
+ * directory of a Gradle project, this action will detect that the project is a Gradle project and it will direct the user to the Gradle
+ * "Import Project" wizard, instead of the intermediate wizard where users can choose to import a project from existing sources. This has
+ * been a source of confusion for our users.
+ * <p>
+ * The code in the original action cannot be extended or reused. It is implemented using static methods and the method where we change the
+ * behavior is at the bottom of the call chain.
+ */
+public class AndroidImportProjectAction extends AnAction {
+ @NonNls private static final String LAST_IMPORTED_LOCATION = "last.imported.location";
+
+ public AndroidImportProjectAction() {
+ super("Import Project...");
+ }
+
+ @Override
+ public void actionPerformed(AnActionEvent e) {
+ AddModuleWizard wizard = selectFileAndCreateWizard();
+ if (wizard != null) {
+ if (wizard.getStepCount() > 0) {
+ if (!wizard.showAndGet()) {
+ return;
+ }
+ //noinspection ConstantConditions
+ NewProjectUtil.createFromWizard(wizard, null);
+ }
+ }
+ }
+
+ @Nullable
+ private static AddModuleWizard selectFileAndCreateWizard() {
+ FileChooserDescriptor descriptor = new FileChooserDescriptor(true, true, true, true, false, false) {
+ FileChooserDescriptor myDelegate = new OpenProjectFileChooserDescriptor(true);
+ @Override
+ public Icon getIcon(VirtualFile file) {
+ Icon icon = myDelegate.getIcon(file);
+ return icon == null ? super.getIcon(file) : icon;
+ }
+ };
+ descriptor.setHideIgnored(false);
+ descriptor.setTitle("Select Gradle Project Import");
+ String description = "Select build.gradle or settings.gradle";
+ descriptor.setDescription(description);
+ return selectFileAndCreateWizard(descriptor);
+ }
+
+ @Nullable
+ private static AddModuleWizard selectFileAndCreateWizard(@NotNull FileChooserDescriptor descriptor) {
+ FileChooserDialog chooser = FileChooserFactory.getInstance().createFileChooser(descriptor, null, null);
+ VirtualFile toSelect = null;
+ String lastLocation = PropertiesComponent.getInstance().getValue(LAST_IMPORTED_LOCATION);
+ if (lastLocation != null) {
+ toSelect = LocalFileSystem.getInstance().refreshAndFindFileByPath(lastLocation);
+ }
+ VirtualFile[] files = chooser.choose(toSelect, null);
+ if (files.length == 0) {
+ return null;
+ }
+ VirtualFile file = files[0];
+ PropertiesComponent.getInstance().setValue(LAST_IMPORTED_LOCATION, file.getPath());
+ return createImportWizard(file);
+ }
+
+ @Nullable
+ private static AddModuleWizard createImportWizard(@NotNull VirtualFile file) {
+ VirtualFile target = findMatchingChild(file, SdkConstants.FN_BUILD_GRADLE, SdkConstants.FN_SETTINGS_GRADLE);
+ if (target == null) {
+ target = file;
+ }
+ List<ProjectImportProvider> available = Lists.newArrayList();
+ for (ProjectImportProvider provider : ProjectImportProvider.PROJECT_IMPORT_PROVIDER.getExtensions()) {
+ if (provider.canImport(target, null)) {
+ available.add(provider);
+ }
+ }
+ if (available.isEmpty()) {
+ Messages.showInfoMessage("Cannot import anything from " + file.getPath(), "Cannot Import");
+ return null;
+ }
+
+ String path;
+ if (available.size() == 1) {
+ path = available.get(0).getPathToBeImported(file);
+ }
+ else {
+ path = ProjectImportProvider.getDefaultPath(file);
+ }
+
+ ProjectImportProvider[] availableProviders = available.toArray(new ProjectImportProvider[available.size()]);
+ return new AddModuleWizard(null, path, availableProviders);
+ }
+
+ @Nullable
+ private static VirtualFile findMatchingChild(@NotNull VirtualFile parent, @NotNull String...validNames) {
+ if (parent.isDirectory()) {
+ for (VirtualFile child : getChildrenOf(parent)) {
+ for (String name : validNames) {
+ if (name.equals(child.getName())) {
+ return child;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ @NotNull
+ private static Collection<VirtualFile> getChildrenOf(@NotNull VirtualFile file) {
+ if (file instanceof NewVirtualFile) {
+ return ((NewVirtualFile)file).getCachedChildren();
+ }
+ return Lists.newArrayList(file.getChildren());
+ }
+
+}
diff --git a/android/src/com/android/tools/idea/actions/AndroidNewActivityAction.java b/android/src/com/android/tools/idea/actions/AndroidNewActivityAction.java
index 2aff3ec..a06597f 100644
--- a/android/src/com/android/tools/idea/actions/AndroidNewActivityAction.java
+++ b/android/src/com/android/tools/idea/actions/AndroidNewActivityAction.java
@@ -19,12 +19,13 @@
import com.android.tools.idea.gradle.facet.AndroidGradleFacet;
import com.android.tools.idea.wizard.NewTemplateObjectWizard;
import com.intellij.facet.FacetManager;
-import com.intellij.ide.IdeView;
-import com.intellij.openapi.actionSystem.*;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.actionSystem.LangDataKeys;
+import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.DumbAware;
import icons.AndroidIcons;
-import org.jetbrains.android.facet.AndroidFacet;
import static com.android.tools.idea.templates.Template.CATEGORY_ACTIVITIES;
@@ -50,7 +51,9 @@
@Override
public void actionPerformed(AnActionEvent e) {
NewTemplateObjectWizard dialog = new NewTemplateObjectWizard(PlatformDataKeys.PROJECT.getData(e.getDataContext()),
- LangDataKeys.MODULE.getData(e.getDataContext()), CATEGORY_ACTIVITIES);
+ LangDataKeys.MODULE.getData(e.getDataContext()),
+ PlatformDataKeys.VIRTUAL_FILE.getData(e.getDataContext()),
+ CATEGORY_ACTIVITIES);
dialog.show();
if (!dialog.isOK()) {
return;
diff --git a/android/src/com/android/tools/idea/configurations/Configuration.java b/android/src/com/android/tools/idea/configurations/Configuration.java
index 1090f21..30f3b4d 100644
--- a/android/src/com/android/tools/idea/configurations/Configuration.java
+++ b/android/src/com/android/tools/idea/configurations/Configuration.java
@@ -16,6 +16,7 @@
package com.android.tools.idea.configurations;
import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.rendering.LayoutLibrary;
import com.android.ide.common.rendering.api.Capability;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.resources.FrameworkResources;
@@ -121,8 +122,8 @@
/**
* The locale to use for this configuration
*/
- @NotNull
- private Locale myLocale = Locale.ANY;
+ @Nullable
+ private Locale myLocale = null;
/**
* UI mode
@@ -154,6 +155,8 @@
/** Dirty flags since last folder config sync: corresponds to constants in {@link ConfigurationListener} */
protected int myFolderConfigDirty = MASK_FOLDERCONFIG;
+ protected int myProjectStateVersion;
+
/**
* Creates a new {@linkplain Configuration}
*/
@@ -161,6 +164,13 @@
myManager = manager;
myFile = file;
myEditedConfig = editedConfig;
+
+ if (editedConfig.getLanguageQualifier() != null) {
+ myLocale = Locale.create(editedConfig);
+ }
+ if (editedConfig.getVersionQualifier() != null) {
+ myTarget = manager.getTarget(editedConfig.getVersionQualifier().getVersion());
+ }
}
/**
@@ -175,7 +185,6 @@
@NotNull FolderConfiguration editedConfig) {
Configuration configuration = new Configuration(manager, file, editedConfig);
configuration.myDevice = manager.getDefaultDevice();
- assert configuration.ensureValid();
return configuration;
}
@@ -202,18 +211,12 @@
@NotNull
public static Configuration create(@NotNull ConfigurationManager manager,
- @Nullable ConfigurationProjectState projectState,
@Nullable VirtualFile file,
@Nullable ConfigurationFileState fileState,
@NotNull FolderConfiguration editedConfig) {
Configuration configuration = new Configuration(manager, file, editedConfig);
configuration.startBulkEditing();
- if (projectState != null) {
- projectState.loadState(configuration);
- } else {
- configuration.myTarget = manager.getDefaultTarget();
- }
if (fileState != null) {
fileState.loadState(configuration);
} else {
@@ -225,7 +228,6 @@
}
configuration.finishBulkEditing();
- assert configuration.ensureValid();
return configuration;
}
@@ -242,12 +244,13 @@
Configuration copy = new Configuration(original.myManager, original.myFile, copiedConfig);
copy.myFullConfig.set(original.myFullConfig);
copy.myFolderConfigDirty = original.myFolderConfigDirty;
- copy.myTarget = original.getTarget();
+ copy.myProjectStateVersion = original.myProjectStateVersion;
+ copy.myTarget = original.myTarget; // avoid getTarget() since it fetches project state
+ copy.myLocale = original.myLocale; // avoid getLocale() since it fetches project state
copy.myTheme = original.getTheme();
copy.myDevice = original.getDevice();
copy.myState = original.getDeviceState();
copy.myActivity = original.getActivity();
- copy.myLocale = original.getLocale();
copy.myUiMode = original.getUiMode();
copy.myNightMode = original.getNightMode();
copy.myDisplayName = original.getDisplayName();
@@ -256,7 +259,6 @@
copy.myConfiguredProjectRes = original.myConfiguredProjectRes;
copy.myConfiguredFrameworkRes = original.myConfiguredFrameworkRes;
- assert copy.ensureValid();
return copy;
}
@@ -279,7 +281,7 @@
FolderConfiguration editedConfig = destination.getEditedConfig();
if (editedConfig.getVersionQualifier() == null) {
- destination.myTarget = source.getTarget();
+ destination.myTarget = source.myTarget; // avoid getTarget() since it fetches project state
}
if (editedConfig.getScreenSizeQualifier() == null) {
destination.myDevice = source.getDevice();
@@ -288,7 +290,7 @@
destination.myState = source.getDeviceState();
}
if (editedConfig.getLanguageQualifier() == null) {
- destination.myLocale = source.getLocale();
+ destination.myLocale = source.myLocale; // avoid getLocale() since it fetches project state
}
if (editedConfig.getUiModeQualifier() == null) {
destination.myUiMode = source.getUiMode();
@@ -304,8 +306,6 @@
destination.myConfiguredProjectRes = null;
destination.myConfiguredFrameworkRes = null;
- assert destination.ensureValid();
-
ProjectResources resources = ProjectResources.get(source.myManager.getModule(), true);
ConfigurationMatcher matcher = new ConfigurationMatcher(destination, resources, destination.myFile);
//if (!matcher.isCurrentFileBestMatchFor(editedConfig)) {
@@ -318,8 +318,6 @@
public void save() {
ConfigurationStateManager stateManager = ConfigurationStateManager.get(myManager.getModule().getProject());
- ConfigurationProjectState projectState = stateManager.getProjectState();
- projectState.saveState(this);
if (myFile != null) {
ConfigurationFileState fileState = new ConfigurationFileState();
@@ -328,18 +326,6 @@
}
}
- @SuppressWarnings("AssertWithSideEffects")
- protected boolean ensureValid() {
- // Asserting on getters rather than fields since some are initialized lazily
- assert getTheme() != null;
- assert getUiMode() != null;
- assert getNightMode() != null;
- assert getLocale() != null;
- // Not checking device, state and target since this causes problem if you open
- // projects without a proper SDK configured
- return true;
- }
-
/**
* Returns the associated {@link ConfigurationManager}
*
@@ -439,6 +425,9 @@
*/
@NotNull
public Locale getLocale() {
+ if (myLocale == null) {
+ return myManager.getLocale();
+ }
return myLocale;
}
@@ -484,7 +473,7 @@
@Nullable
public IAndroidTarget getTarget() {
if (myTarget == null) {
- myTarget = myManager.getDefaultTarget();
+ return myManager.getTarget();
}
return myTarget;
@@ -544,7 +533,7 @@
*/
@NotNull
public FolderConfiguration getFullConfig() {
- if ((myFolderConfigDirty & MASK_FOLDERCONFIG) != 0) {
+ if ((myFolderConfigDirty & MASK_FOLDERCONFIG) != 0 || myProjectStateVersion < myManager.getStateVersion()) {
syncFolderConfig();
}
@@ -808,6 +797,19 @@
Locale locale = getLocale();
myFullConfig.setLanguageQualifier(locale.language);
myFullConfig.setRegionQualifier(locale.region);
+ if (!locale.hasLanguage()) {
+ // Avoid getting the layout library if the locale doesn't have any language.
+ myFullConfig.setLayoutDirectionQualifier(new LayoutDirectionQualifier(LayoutDirection.LTR));
+ } else {
+ LayoutLibrary layoutLib = RenderService.getLayoutLibrary(getModule(), getTarget());
+ if (layoutLib != null) {
+ if (layoutLib.isRtl(locale.toLocaleId())) {
+ myFullConfig.setLayoutDirectionQualifier(new LayoutDirectionQualifier(LayoutDirection.RTL));
+ } else {
+ myFullConfig.setLayoutDirectionQualifier(new LayoutDirectionQualifier(LayoutDirection.LTR));
+ }
+ }
+ }
// Replace the UiMode with the selected one, if one is selected
UiMode uiMode = getUiMode();
@@ -825,6 +827,7 @@
}
myFolderConfigDirty = 0;
+ myProjectStateVersion = myManager.getStateVersion();
}
/** Returns the screen size required for this configuration */
@@ -979,6 +982,12 @@
myNotifyDirty |= flags;
myFolderConfigDirty |= flags;
+ if (myManager.getStateVersion() > myProjectStateVersion) {
+ myNotifyDirty |= MASK_PROJECT_STATE;
+ myFolderConfigDirty |= MASK_PROJECT_STATE;
+ // TODO: Update myProjectStateVersion?
+ }
+
if ((flags & MASK_RESOLVE_RESOURCES) != 0) {
myFrameworkResources = null;
myConfiguredFrameworkRes = null;
@@ -1136,10 +1145,15 @@
@NotNull
public Map<ResourceType, Map<String, ResourceValue>> getConfiguredProjectResources() {
- ProjectResources resources = ProjectResources.get(myManager.getModule(), true);
+ final ProjectResources resources = ProjectResources.get(myManager.getModule(), true);
if (myConfiguredProjectRes == null || myCachedGeneration < resources.getModificationCount()) {
// get the project resource values based on the current config
- myConfiguredProjectRes = resources.getConfiguredResources(getFullConfig());
+ ApplicationManager.getApplication().runReadAction(new Runnable() {
+ @Override
+ public void run() {
+ myConfiguredProjectRes = resources.getConfiguredResources(getFullConfig());
+ }
+ });
myCachedGeneration = resources.getModificationCount();
}
@@ -1147,6 +1161,7 @@
}
// For debugging only
+ @SuppressWarnings("SpellCheckingInspection")
@Override
public String toString() {
return Objects.toStringHelper(this.getClass()).add("display", getDisplayName()) //$NON-NLS-1$
diff --git a/android/src/com/android/tools/idea/configurations/ConfigurationAction.java b/android/src/com/android/tools/idea/configurations/ConfigurationAction.java
index e475ec0..2746e0a 100644
--- a/android/src/com/android/tools/idea/configurations/ConfigurationAction.java
+++ b/android/src/com/android/tools/idea/configurations/ConfigurationAction.java
@@ -78,6 +78,7 @@
protected void pickedBetterMatch(@NotNull VirtualFile file) {
// Switch files, and leave this configuration alone
Module module = myRenderContext.getModule();
+ assert module != null;
Project project = module.getProject();
OpenFileDescriptor descriptor = new OpenFileDescriptor(project, file, -1);
FileEditorManager.getInstance(project).openEditor(descriptor, true);
diff --git a/android/src/com/android/tools/idea/configurations/ConfigurationListener.java b/android/src/com/android/tools/idea/configurations/ConfigurationListener.java
index ac25448..35be1de 100644
--- a/android/src/com/android/tools/idea/configurations/ConfigurationListener.java
+++ b/android/src/com/android/tools/idea/configurations/ConfigurationListener.java
@@ -63,6 +63,9 @@
/** Attributes which affect rendering appearance */
int MASK_RENDERING = MASK_FILE_ATTRS | CFG_THEME;
+ /** Attributes which are edited project-wide */
+ int MASK_PROJECT_STATE = CFG_LOCALE|CFG_TARGET;
+
/**
* The configuration has changed. If the client returns false, it means that
* the change was rejected. This typically means that changing the
diff --git a/android/src/com/android/tools/idea/configurations/ConfigurationManager.java b/android/src/com/android/tools/idea/configurations/ConfigurationManager.java
index f2b3851..0ca55b7 100644
--- a/android/src/com/android/tools/idea/configurations/ConfigurationManager.java
+++ b/android/src/com/android/tools/idea/configurations/ConfigurationManager.java
@@ -15,17 +15,18 @@
*/
package com.android.tools.idea.configurations;
-import com.android.SdkConstants;
+import com.android.annotations.VisibleForTesting;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.ide.common.resources.configuration.LanguageQualifier;
import com.android.ide.common.resources.configuration.RegionQualifier;
-import com.android.resources.ResourceType;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.devices.Device;
import com.android.sdklib.devices.DeviceManager;
import com.android.sdklib.internal.avd.AvdInfo;
-import com.android.tools.idea.rendering.*;
import com.android.tools.idea.rendering.Locale;
+import com.android.tools.idea.rendering.ManifestInfo;
+import com.android.tools.idea.rendering.ProjectResources;
+import com.android.tools.idea.rendering.ResourceHelper;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.module.Module;
@@ -34,24 +35,23 @@
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.util.containers.HashMap;
-import com.intellij.util.containers.HashSet;
-import com.intellij.util.containers.WeakValueHashMap;
-import org.jetbrains.android.dom.resources.ResourceElement;
-import org.jetbrains.android.dom.resources.ResourceValue;
-import org.jetbrains.android.dom.resources.Style;
+import com.intellij.util.containers.SoftValueHashMap;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.facet.AndroidFacetConfiguration;
import org.jetbrains.android.sdk.*;
import org.jetbrains.android.uipreview.UserDeviceManager;
-import org.jetbrains.android.util.AndroidUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
import static com.android.sdklib.devices.DeviceManager.DEFAULT_DEVICES;
import static com.android.sdklib.devices.DeviceManager.VENDOR_DEVICES;
+import static com.android.tools.idea.configurations.ConfigurationListener.CFG_LOCALE;
+import static com.android.tools.idea.configurations.ConfigurationListener.CFG_TARGET;
/**
* A {@linkplain ConfigurationManager} is responsible for managing {@link Configuration}
@@ -67,12 +67,14 @@
public class ConfigurationManager implements Disposable {
@NotNull private final Module myModule;
private List<Device> myDevices;
- private List<String> myProjectThemes;
private List<IAndroidTarget> myTargets;
private final UserDeviceManager myUserDeviceManager;
- private final WeakValueHashMap<VirtualFile, Configuration> myCache = new WeakValueHashMap<VirtualFile, Configuration>();
+ private final SoftValueHashMap<VirtualFile, Configuration> myCache = new SoftValueHashMap<VirtualFile, Configuration>();
private List<Locale> myLocales;
private Device myDefaultDevice;
+ private Locale myLocale;
+ private IAndroidTarget myTarget;
+ private int myStateVersion;
private ConfigurationManager(@NotNull Module module) {
myModule = module;
@@ -103,6 +105,11 @@
return configuration;
}
+ @VisibleForTesting
+ boolean hasCachedConfiguration(@NotNull VirtualFile file) {
+ return myCache.get(file) != null;
+ }
+
/**
* Creates a new {@link Configuration} associated with this manager
* @return a new {@link Configuration}
@@ -110,13 +117,12 @@
@NotNull
private Configuration create(@NotNull VirtualFile file) {
ConfigurationStateManager stateManager = getStateManager();
- ConfigurationProjectState projectState = stateManager.getProjectState();
ConfigurationFileState fileState = stateManager.getConfigurationState(file);
FolderConfiguration config = FolderConfiguration.getConfigForFolder(file.getParent().getName());
if (config == null) {
config = new FolderConfiguration();
}
- Configuration configuration = Configuration.create(this, projectState, file, fileState, config);
+ Configuration configuration = Configuration.create(this, file, fileState, config);
ProjectResources projectResources = ProjectResources.get(myModule, true);
ConfigurationMatcher matcher = new ConfigurationMatcher(configuration, projectResources, file);
if (fileState != null) {
@@ -143,13 +149,12 @@
@NotNull
public Configuration createSimilar(@NotNull VirtualFile file, @NotNull VirtualFile baseFile) {
ConfigurationStateManager stateManager = getStateManager();
- ConfigurationProjectState projectState = stateManager.getProjectState();
ConfigurationFileState fileState = stateManager.getConfigurationState(baseFile);
FolderConfiguration config = FolderConfiguration.getConfigForFolder(file.getParent().getName());
if (config == null) {
config = new FolderConfiguration();
}
- Configuration configuration = Configuration.create(this, projectState, file, fileState, config);
+ Configuration configuration = Configuration.create(this, file, fileState, config);
ProjectResources projectResources = ProjectResources.get(myModule, true);
ConfigurationMatcher matcher = new ConfigurationMatcher(configuration, projectResources, file);
matcher.adaptConfigSelection(true /*needBestMatch*/);
@@ -281,40 +286,6 @@
}
@NotNull
- public List<String> getProjectThemes() {
- if (myProjectThemes == null) {
- // TODO: How do we invalidate this if the manifest theme set changes?
- myProjectThemes = computeProjectThemes();
- }
-
- return myProjectThemes;
- }
-
- @NotNull
- private List<String> computeProjectThemes() {
- final AndroidFacet facet = AndroidFacet.getInstance(myModule);
- if (facet == null) {
- return Collections.emptyList();
- }
-
- final List<String> themes = new ArrayList<String>();
- final Map<String, ResourceElement> styleMap = buildStyleMap(facet);
-
- for (ResourceElement style : styleMap.values()) {
- if (isTheme(style, styleMap, new HashSet<ResourceElement>())) {
- final String themeName = style.getName().getValue();
- if (themeName != null) {
- final String theme = SdkConstants.STYLE_RESOURCE_PREFIX + themeName;
- themes.add(theme);
- }
- }
- }
-
- Collections.sort(themes);
- return themes;
- }
-
- @NotNull
public Module getModule() {
return myModule;
}
@@ -329,66 +300,6 @@
myUserDeviceManager.dispose();
}
- private static Map<String, ResourceElement> buildStyleMap(AndroidFacet facet) {
- final Map<String, ResourceElement> result = new HashMap<String, ResourceElement>();
- final List<ResourceElement> styles = facet.getLocalResourceManager().getValueResources(ResourceType.STYLE.getName());
- for (ResourceElement style : styles) {
- final String styleName = style.getName().getValue();
- if (styleName != null) {
- result.put(styleName, style);
- }
- }
- return result;
- }
-
- private static boolean isTheme(ResourceElement resElement, Map<String, ResourceElement> styleMap, Set<ResourceElement> visitedElements) {
- if (!visitedElements.add(resElement)) {
- return false;
- }
-
- if (!(resElement instanceof Style)) {
- return false;
- }
-
- final String styleName = resElement.getName().getValue();
- if (styleName == null) {
- return false;
- }
-
- final ResourceValue parentStyleRef = ((Style)resElement).getParentStyle().getValue();
- String parentStyleName = null;
- boolean frameworkStyle = false;
-
- if (parentStyleRef != null) {
- final String s = parentStyleRef.getResourceName();
- if (s != null) {
- parentStyleName = s;
- frameworkStyle = AndroidUtils.SYSTEM_RESOURCE_PACKAGE.equals(parentStyleRef.getPackage());
- }
- }
-
- if (parentStyleRef == null) {
- final int index = styleName.indexOf('.');
- if (index >= 0) {
- parentStyleName = styleName.substring(0, index);
- }
- }
-
- if (parentStyleRef != null) {
- if (frameworkStyle) {
- return parentStyleName.equals("Theme") || parentStyleName.startsWith("Theme.");
- }
- else {
- final ResourceElement parentStyle = styleMap.get(parentStyleName);
- if (parentStyle != null) {
- return isTheme(parentStyle, styleMap, visitedElements);
- }
- }
- }
-
- return false;
- }
-
@Nullable
public Device getDefaultDevice() {
if (myDefaultDevice == null) {
@@ -457,30 +368,78 @@
@NotNull
public Locale getLocale() {
- String localeString = getStateManager().getProjectState().getLocale();
- if (localeString != null) {
- return ConfigurationProjectState.fromLocaleString(localeString);
+ if (myLocale == null) {
+ String localeString = getStateManager().getProjectState().getLocale();
+ if (localeString != null) {
+ myLocale = ConfigurationProjectState.fromLocaleString(localeString);
+ } else {
+ myLocale = Locale.ANY;
+ }
}
- return Locale.ANY;
+ return myLocale;
}
public void setLocale(@NotNull Locale locale) {
- getStateManager().getProjectState().setLocale(ConfigurationProjectState.toLocaleString(locale));
+ if (!locale.equals(myLocale)) {
+ myLocale = locale;
+ getStateManager().getProjectState().setLocale(ConfigurationProjectState.toLocaleString(locale));
+ for (Configuration configuration : myCache.values()) {
+ configuration.updated(CFG_LOCALE);
+ }
+ myStateVersion++;
+ }
}
@Nullable
public IAndroidTarget getTarget() {
- String targetString = getStateManager().getProjectState().getTarget();
- IAndroidTarget target = ConfigurationProjectState.fromTargetString(this, targetString);
- if (target == null) {
- target = getDefaultTarget();
+ if (myTarget == null) {
+ ConfigurationProjectState projectState = getStateManager().getProjectState();
+ if (projectState.isPickTarget()) {
+ myTarget = getDefaultTarget();
+ } else {
+ String targetString = projectState.getTarget();
+ myTarget = ConfigurationProjectState.fromTargetString(this, targetString);
+ if (myTarget == null) {
+ myTarget = getDefaultTarget();
+ }
+ }
+ return myTarget;
}
- return target;
+
+ return myTarget;
}
- public void setTarget(@NotNull IAndroidTarget target) {
- getStateManager().getProjectState().setTarget(ConfigurationProjectState.toTargetString(target));
+ /** Returns the best render target to use for the given minimum API level */
+ @Nullable
+ public IAndroidTarget getTarget(int min) {
+ IAndroidTarget target = getTarget();
+ if (target != null && target.getVersion().getApiLevel() >= min) {
+ return target;
+ }
+
+ List<IAndroidTarget> targetList = getTargets();
+ for (int i = targetList.size() - 1; i >= 0; i--) {
+ target = targetList.get(i);
+ if (target.hasRenderingLibrary() && target.getVersion().getApiLevel() >= min) {
+ return target;
+ }
+ }
+
+ return null;
+ }
+
+ public void setTarget(@Nullable IAndroidTarget target) {
+ if (target != myTarget) {
+ myTarget = target;
+ if (target != null) {
+ getStateManager().getProjectState().setTarget(ConfigurationProjectState.toTargetString(target));
+ for (Configuration configuration : myCache.values()) {
+ configuration.updated(CFG_TARGET);
+ }
+ myStateVersion++;
+ }
+ }
}
/**
@@ -512,10 +471,10 @@
}
}
- private void doSyncToVariations(int flags, VirtualFile updatedFile, boolean includeSelf,
+ private void doSyncToVariations(@SuppressWarnings("UnusedParameters") int flags,
+ VirtualFile updatedFile, boolean includeSelf,
Configuration base) {
// Synchronize the given changes to other configurations as well
- Project project = getProject();
List<VirtualFile> files = ResourceHelper.getResourceVariations(updatedFile, includeSelf);
for (VirtualFile file : files) {
Configuration configuration = getConfiguration(file);
@@ -523,4 +482,8 @@
configuration.save();
}
}
+
+ public int getStateVersion() {
+ return myStateVersion;
+ }
}
diff --git a/android/src/com/android/tools/idea/configurations/ConfigurationMatcher.java b/android/src/com/android/tools/idea/configurations/ConfigurationMatcher.java
index f7a3964..2d73ed2 100644
--- a/android/src/com/android/tools/idea/configurations/ConfigurationMatcher.java
+++ b/android/src/com/android/tools/idea/configurations/ConfigurationMatcher.java
@@ -15,17 +15,15 @@
*/
package com.android.tools.idea.configurations;
-import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.resources.configuration.*;
import com.android.io.IAbstractFile;
import com.android.resources.*;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.devices.Device;
import com.android.sdklib.devices.State;
-import com.android.sdklib.util.SparseIntArray;
import com.android.tools.idea.rendering.Locale;
import com.android.tools.idea.rendering.ProjectResources;
-import com.android.tools.idea.rendering.ResourceHelper;
+import com.android.utils.SparseIntArray;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
@@ -127,10 +125,9 @@
*/
public boolean isCurrentFileBestMatchFor(@NotNull FolderConfiguration config) {
if (myResources != null && myFile != null) {
- ResourceFile match = myResources.getMatchingFile(ResourceHelper.getResourceName(myFile), ResourceType.LAYOUT, config);
-
+ VirtualFile match = myResources.getMatchingFile(myFile, ResourceType.LAYOUT, config);
if (match != null) {
- return myFile.equals(LocalFileSystem.getInstance().findFileByIoFile(match.getFile()));
+ return myFile.equals(match);
}
else {
// if we stop here that means the current file is not even a match!
@@ -149,7 +146,7 @@
BufferingFileWrapper wrapper = (BufferingFileWrapper)file;
File ioFile = wrapper.getFile();
return LocalFileSystem.getInstance().findFileByIoFile(ioFile);
- } else if (file != null) {
+ } else {
LOG.warn("Unexpected type of match file: " + file.getClass().getName());
}
return null;
@@ -163,13 +160,8 @@
@Nullable
public VirtualFile getBestFileMatch() {
if (myResources != null && myFile != null) {
- String name = ResourceHelper.getResourceName(myFile);
FolderConfiguration config = myConfiguration.getFullConfig();
- ResourceFile match = myResources.getMatchingFile(name, ResourceType.LAYOUT, config);
-
- if (match != null) {
- return LocalFileSystem.getInstance().findFileByIoFile(match.getFile());
- }
+ return myResources.getMatchingFile(myFile, ResourceType.LAYOUT, config);
}
return null;
@@ -257,8 +249,6 @@
if (matchState != null) {
myConfiguration.startBulkEditing();
myConfiguration.setDeviceState(matchState);
- Locale locale = localeList.get(localeIndex);
- myConfiguration.setLocale(locale);
myConfiguration.finishBulkEditing();
}
else {
@@ -385,7 +375,6 @@
myConfiguration.startBulkEditing();
myConfiguration.setDevice(match.device, false);
myConfiguration.setDeviceState(match.state);
- myConfiguration.setLocale(localeList.get(match.bundle.localeIndex));
myConfiguration.setUiMode(UiMode.getByIndex(match.bundle.dockModeIndex));
myConfiguration.setNightMode(NightMode.getByIndex(match.bundle.nightModeIndex));
myConfiguration.finishBulkEditing();
@@ -410,7 +399,6 @@
myConfiguration.startBulkEditing();
myConfiguration.setDevice(match.device, false);
myConfiguration.setDeviceState(match.state);
- myConfiguration.setLocale(localeList.get(match.bundle.localeIndex));
myConfiguration.setUiMode(UiMode.getByIndex(match.bundle.dockModeIndex));
myConfiguration.setNightMode(NightMode.getByIndex(match.bundle.nightModeIndex));
myConfiguration.finishBulkEditing();
diff --git a/android/src/com/android/tools/idea/configurations/ConfigurationProjectState.java b/android/src/com/android/tools/idea/configurations/ConfigurationProjectState.java
index 5afcd06..d21c411 100644
--- a/android/src/com/android/tools/idea/configurations/ConfigurationProjectState.java
+++ b/android/src/com/android/tools/idea/configurations/ConfigurationProjectState.java
@@ -28,6 +28,7 @@
public class ConfigurationProjectState {
@Nullable private String myLocale;
@Nullable private String myTarget;
+ private boolean myPickTarget = true;
@Tag("locale")
@Nullable
@@ -49,22 +50,13 @@
myTarget = target;
}
- public void saveState(@NotNull Configuration configuration) {
- setLocale(toLocaleString(configuration.getLocale()));
- setTarget(toTargetString(configuration.getTarget()));
+ @Tag("pickBest")
+ public boolean isPickTarget() {
+ return myPickTarget;
}
- public void loadState(@NotNull Configuration configuration) {
- ConfigurationManager manager = configuration.getConfigurationManager();
- IAndroidTarget target = fromTargetString(manager, myTarget);
- configuration.startBulkEditing();
- if (target != null) {
- configuration.setTarget(target);
- }
- if (myLocale != null) {
- configuration.setLocale(Locale.create(myLocale));
- }
- configuration.finishBulkEditing();
+ public void setPickTarget(boolean pickTarget) {
+ myPickTarget = pickTarget;
}
@Nullable
diff --git a/android/src/com/android/tools/idea/configurations/LocaleMenuAction.java b/android/src/com/android/tools/idea/configurations/LocaleMenuAction.java
index 7f0e929..2d6f9de 100644
--- a/android/src/com/android/tools/idea/configurations/LocaleMenuAction.java
+++ b/android/src/com/android/tools/idea/configurations/LocaleMenuAction.java
@@ -61,7 +61,9 @@
// allowing typing to filter, etc.
List<Locale> locales = getRelevantLocales();
- if (locales.size() > 0) {
+
+ Configuration configuration = myRenderContext.getConfiguration();
+ if (configuration != null && configuration.getEditedConfig().getLanguageQualifier() == null && locales.size() > 0) {
group.add(new SetLocaleAction(myRenderContext, getLocaleLabel(Locale.ANY, false), Locale.ANY));
group.addSeparator();
@@ -264,8 +266,9 @@
protected void updateConfiguration(@NotNull Configuration configuration) {
if (configuration == myRenderContext.getConfiguration()) {
setProjectWideLocale();
+ } else {
+ configuration.setLocale(myLocale);
}
- configuration.setLocale(myLocale);
}
@Override
@@ -275,12 +278,6 @@
if (configuration != null) {
// Save project-wide configuration; not done by regular listening scheme since the previous configuration was not switched
setProjectWideLocale();
- Module module = myRenderContext.getModule();
- if (module != null) {
- ConfigurationStateManager stateManager = ConfigurationStateManager.get(module.getProject());
- ConfigurationProjectState projectState = stateManager.getProjectState();
- projectState.saveState(configuration);
- }
}
}
@@ -289,6 +286,7 @@
if (configuration != null) {
// Also set the project-wide locale, since locales (and rendering targets) are project wide
configuration.getConfigurationManager().setLocale(myLocale);
+ myRenderContext.requestRender();
}
}
}
diff --git a/android/src/com/android/tools/idea/configurations/RenderContext.java b/android/src/com/android/tools/idea/configurations/RenderContext.java
index 950c2cb..a4e568c 100644
--- a/android/src/com/android/tools/idea/configurations/RenderContext.java
+++ b/android/src/com/android/tools/idea/configurations/RenderContext.java
@@ -15,6 +15,7 @@
*/
package com.android.tools.idea.configurations;
+import com.android.tools.idea.rendering.RenderResult;
import com.android.tools.idea.rendering.multi.RenderPreviewManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.vfs.VirtualFile;
@@ -23,6 +24,7 @@
import org.jetbrains.annotations.Nullable;
import java.awt.*;
+import java.awt.image.BufferedImage;
/**
* A {@link RenderContext} can provide an optional configuration.
@@ -171,6 +173,10 @@
*/
void setDeviceFramesEnabled(boolean on);
+ /** Returns the most recent rendered image, if any */
+ @Nullable
+ BufferedImage getRenderedImage();
+
/**
* Types of uses of the {@link com.android.tools.idea.rendering.RenderService} which can drive some decisions, such as how and whether
* to report {@code <fragment/>} tags without a known preview layout, and so on.
diff --git a/android/src/com/android/tools/idea/configurations/TargetMenuAction.java b/android/src/com/android/tools/idea/configurations/TargetMenuAction.java
index 0d1b5ea..c1b3cad 100644
--- a/android/src/com/android/tools/idea/configurations/TargetMenuAction.java
+++ b/android/src/com/android/tools/idea/configurations/TargetMenuAction.java
@@ -18,9 +18,7 @@
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.intellij.icons.AllIcons;
-import com.intellij.openapi.actionSystem.AnActionEvent;
-import com.intellij.openapi.actionSystem.DefaultActionGroup;
-import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.vfs.VirtualFile;
import icons.AndroidIcons;
import org.jetbrains.annotations.NotNull;
@@ -50,8 +48,7 @@
Configuration configuration = myRenderContext.getConfiguration();
boolean visible = configuration != null;
if (visible) {
- IAndroidTarget target = configuration.isTargetSpecificLayout()
- ? configuration.getTarget() : configuration.getConfigurationManager().getTarget();
+ IAndroidTarget target = configuration.getTarget();
String brief = getRenderingTargetLabel(target, true);
presentation.setText(brief);
}
@@ -69,6 +66,10 @@
if (configuration == null) {
return group;
}
+
+ group.add(new TogglePickBestAction(configuration.getConfigurationManager()));
+ group.addSeparator();
+
IAndroidTarget current = configuration.getTarget();
List<IAndroidTarget> targets = configuration.getConfigurationManager().getTargets();
@@ -122,6 +123,34 @@
return String.format("API %1$d: %2$s", version.getApiLevel(), target.getShortClasspathName());
}
+ private static class TogglePickBestAction extends ToggleAction {
+ private final ConfigurationManager myManager;
+
+ TogglePickBestAction(ConfigurationManager manager) {
+ super("Automatically Pick Best");
+
+ myManager = manager;
+
+ if (manager.getStateManager().getProjectState().isPickTarget()) {
+ getTemplatePresentation().setIcon(AllIcons.Actions.Checked);
+ }
+ }
+
+ @Override
+ public boolean isSelected(AnActionEvent e) {
+ return myManager.getStateManager().getProjectState().isPickTarget();
+ }
+
+ @Override
+ public void setSelected(AnActionEvent e, boolean state) {
+ myManager.getStateManager().getProjectState().setPickTarget(state);
+ if (state) {
+ // Make sure we have the best target: force recompute on next getTarget()
+ myManager.setTarget(null);
+ }
+ }
+ }
+
private static class SetTargetAction extends ConfigurationAction {
private final IAndroidTarget myTarget;
@@ -138,8 +167,9 @@
protected void updateConfiguration(@NotNull Configuration configuration) {
if (configuration == myRenderContext.getConfiguration()) {
setProjectWideTarget();
+ } else {
+ configuration.setTarget(myTarget);
}
- configuration.setTarget(myTarget);
}
@Override
@@ -149,9 +179,6 @@
if (configuration != null) {
// Save project-wide configuration; not done by regular listening scheme since the previous configuration was not switched
setProjectWideTarget();
- ConfigurationStateManager stateManager = ConfigurationStateManager.get(myRenderContext.getModule().getProject());
- ConfigurationProjectState projectState = stateManager.getProjectState();
- projectState.saveState(configuration);
}
}
@@ -160,6 +187,7 @@
Configuration configuration = myRenderContext.getConfiguration();
if (configuration != null) {
configuration.getConfigurationManager().setTarget(myTarget);
+ myRenderContext.requestRender();
}
}
}
diff --git a/android/src/com/android/tools/idea/configurations/ThemeSelectionPanel.java b/android/src/com/android/tools/idea/configurations/ThemeSelectionPanel.java
index 7de4567..48ffed5 100644
--- a/android/src/com/android/tools/idea/configurations/ThemeSelectionPanel.java
+++ b/android/src/com/android/tools/idea/configurations/ThemeSelectionPanel.java
@@ -16,7 +16,9 @@
package com.android.tools.idea.configurations;
import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.rendering.api.StyleResourceValue;
import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.ResourceResolver;
import com.android.resources.ResourceType;
import com.android.tools.idea.rendering.ManifestInfo;
import com.android.tools.idea.rendering.ProjectResources;
@@ -45,6 +47,8 @@
import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
+import static com.android.ide.common.resources.ResourceResolver.THEME_NAME;
+import static com.android.ide.common.resources.ResourceResolver.THEME_NAME_DOT;
/**
* Theme selection dialog.
@@ -64,8 +68,6 @@
private static final String PROJECT_THEME_PREFIX = STYLE_RESOURCE_PREFIX + "Theme.";
private static final String DIALOG_SUFFIX = ".Dialog";
private static final String DIALOG_PART = ".Dialog.";
- private static final String THEME = "Theme";
- private static final String THEME_DOT = "Theme.";
private static final SimpleTextAttributes SEARCH_HIGHLIGHT_ATTRIBUTES =
new SimpleTextAttributes(null, JBColor.MAGENTA, null, SimpleTextAttributes.STYLE_BOLD);
@@ -107,8 +109,11 @@
else if (style.startsWith(PROJECT_THEME_PREFIX)) {
style = style.substring(PROJECT_THEME_PREFIX.length());
}
+ else if (style.startsWith(STYLE_RESOURCE_PREFIX)) {
+ style = style.substring(STYLE_RESOURCE_PREFIX.length());
+ }
else if (style.equals(ANDROID_THEME) || style.equals(PROJECT_THEME)) {
- style = THEME;
+ style = THEME_NAME;
}
if (!filter.isEmpty()) {
@@ -352,7 +357,7 @@
if (myProjectThemes == null) {
ProjectResources repository = ProjectResources.get(myConfiguration.getModule(), true);
Map<ResourceType, Map<String, ResourceValue>> resources = repository.getConfiguredResources(myConfiguration.getFullConfig());
- myProjectThemes = getThemes(resources, false /*isFramework*/);
+ myProjectThemes = getThemes(myConfiguration, resources, false /*isFramework*/);
}
return myProjectThemes;
@@ -505,10 +510,10 @@
}
Map<ResourceType, Map<String, ResourceValue>> resources = repository.getConfiguredResources(myConfiguration.getFullConfig());
- return getThemes(resources, isFramework);
+ return getThemes(myConfiguration, resources, isFramework);
}
- private static List<String> getThemes(Map<ResourceType, Map<String, ResourceValue>> resources, boolean isFramework) {
+ private static List<String> getThemes(Configuration configuration, Map<ResourceType, Map<String, ResourceValue>> resources, boolean isFramework) {
String prefix = isFramework ? ANDROID_STYLE_RESOURCE_PREFIX : STYLE_RESOURCE_PREFIX;
// get the styles.
Map<String, ResourceValue> styles = resources.get(ResourceType.STYLE);
@@ -516,13 +521,35 @@
// Collect the themes out of all the styles.
Collection<ResourceValue> values = styles.values();
List<String> themes = new ArrayList<String>(values.size());
- for (ResourceValue value : values) {
- String name = value.getName();
- if (name.startsWith(THEME_DOT) || name.equals(THEME)) {
- themes.add(prefix + name);
+
+ if (!isFramework) {
+ // Try a little harder to see if the user has themes that don't have the normal naming convention
+ ResourceResolver resolver = configuration.getResourceResolver();
+ if (resolver != null) {
+ Map<ResourceValue, Boolean> cache = Maps.newHashMapWithExpectedSize(values.size());
+ for (ResourceValue value : values) {
+ if (value instanceof StyleResourceValue) {
+ StyleResourceValue styleValue = (StyleResourceValue)value;
+ boolean isTheme = resolver.isTheme(styleValue, cache);
+ if (isTheme) {
+ String name = value.getName();
+ themes.add(prefix + name);
+ }
+ }
+ }
+
+ Collections.sort(themes);
+ return themes;
}
}
+ // For the framework (and projects if resolver can't be computed) the computation is easier
+ for (ResourceValue value : values) {
+ String name = value.getName();
+ if (name.startsWith(THEME_NAME_DOT) || name.equals(THEME_NAME)) {
+ themes.add(prefix + name);
+ }
+ }
Collections.sort(themes);
return themes;
}
diff --git a/android/src/com/android/tools/idea/configurations/TranslationDialog.java b/android/src/com/android/tools/idea/configurations/TranslationDialog.java
index b30e1d3..7e4e0bf 100644
--- a/android/src/com/android/tools/idea/configurations/TranslationDialog.java
+++ b/android/src/com/android/tools/idea/configurations/TranslationDialog.java
@@ -67,6 +67,7 @@
String localeLabel = LocaleMenuAction.getLocaleLabel(myLocale, false);
setTitle(String.format("Add Translation for %1$s", localeLabel));
init();
+ setOKActionEnabled(false);
}
@Nullable
@@ -183,7 +184,11 @@
@Override
public void setValueAt(Object aValue, int row, int col) {
- myTranslations.put(myKeys[row], aValue.toString());
+ String string = aValue.toString();
+ if (!string.isEmpty() && !isOKActionEnabled()) {
+ setOKActionEnabled(true);
+ }
+ myTranslations.put(myKeys[row], string);
}
@Override
diff --git a/android/src/com/android/tools/idea/ddms/DevicePanel.java b/android/src/com/android/tools/idea/ddms/DevicePanel.java
index 8c81eea..538f0ae 100644
--- a/android/src/com/android/tools/idea/ddms/DevicePanel.java
+++ b/android/src/com/android/tools/idea/ddms/DevicePanel.java
@@ -413,7 +413,8 @@
File backingFile = FileUtil.createTempFile("screenshot", SdkConstants.DOT_PNG, true);
ImageIO.write(getScreenshot(), SdkConstants.EXT_PNG, backingFile);
- ScreenshotViewer viewer = new ScreenshotViewer(project, getScreenshot(), backingFile, d);
+ ScreenshotViewer viewer = new ScreenshotViewer(project, getScreenshot(), backingFile, d,
+ d.getPropertyCacheOrSync(IDevice.PROP_DEVICE_MODEL));
if (viewer.showAndGet()) {
File screenshot = viewer.getScreenshot();
VirtualFile vf = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(screenshot);
diff --git a/android/src/com/android/tools/idea/ddms/screenshot/ScreenshotViewer.java b/android/src/com/android/tools/idea/ddms/screenshot/ScreenshotViewer.java
index 3e02672..17accab 100644
--- a/android/src/com/android/tools/idea/ddms/screenshot/ScreenshotViewer.java
+++ b/android/src/com/android/tools/idea/ddms/screenshot/ScreenshotViewer.java
@@ -31,9 +31,7 @@
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
-import com.intellij.openapi.vfs.LocalFileSystem;
-import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.openapi.vfs.VirtualFileWrapper;
+import com.intellij.openapi.vfs.*;
import com.intellij.ui.components.JBScrollPane;
import org.intellij.images.editor.ImageEditor;
import org.intellij.images.editor.ImageFileEditor;
@@ -57,6 +55,8 @@
public class ScreenshotViewer extends DialogWrapper implements DataProvider {
@NonNls private static final String SCREENSHOT_VIEWER_DIMENSIONS_KEY = "ScreenshotViewer.Dimensions";
+ private static VirtualFile ourLastSavedFolder = null;
+
private final Project myProject;
private final IDevice myDevice;
@@ -64,7 +64,7 @@
private final ImageFileEditor myImageFileEditor;
private final FileEditorProvider myProvider;
- private final List<DeviceArtDescriptor> myDeviceArtSpecs;
+ private final List<DeviceArtDescriptor> myDeviceArtDescriptors;
private JPanel myPanel;
private JButton myRefreshButton;
@@ -93,7 +93,8 @@
public ScreenshotViewer(@NotNull Project project,
@NotNull BufferedImage image,
@NotNull File backingFile,
- @NotNull IDevice device) {
+ @Nullable IDevice device,
+ @Nullable String deviceModel) {
super(project, true);
myProject = project;
@@ -105,6 +106,7 @@
assert myBackingVirtualFile != null;
myRefreshButton.setIcon(AllIcons.Actions.Refresh);
+ myRefreshButton.setEnabled(device != null);
myRotateButton.setIcon(AllIcons.Actions.AllRight);
myProvider = getImageFileEditorProvider();
@@ -134,18 +136,54 @@
myDropShadowCheckBox.addActionListener(l);
myScreenGlareCheckBox.addActionListener(l);
- myDeviceArtSpecs = DeviceArtDescriptor.getDescriptors(null);
- String[] titles = new String[myDeviceArtSpecs.size()];
- for (int i = 0; i < myDeviceArtSpecs.size(); i++) {
- titles[i] = myDeviceArtSpecs.get(i).getName();
+ myDeviceArtDescriptors = DeviceArtDescriptor.getDescriptors(null);
+ String[] titles = new String[myDeviceArtDescriptors.size()];
+ for (int i = 0; i < myDeviceArtDescriptors.size(); i++) {
+ titles[i] = myDeviceArtDescriptors.get(i).getName();
}
DefaultComboBoxModel model = new DefaultComboBoxModel(titles);
myDeviceArtCombo.setModel(model);
myDeviceArtCombo.setSelectedIndex(0);
+ // Set the default device art descriptor selection
+ myDeviceArtCombo.setSelectedIndex(getDefaultDescriptor(myDeviceArtDescriptors, image, deviceModel));
+
init();
}
+ private static int getDefaultDescriptor(List<DeviceArtDescriptor> deviceArtDescriptors, BufferedImage image,
+ @Nullable String deviceModel) {
+ int index = -1;
+
+ if (deviceModel != null) {
+ index = findDescriptorIndexForProduct(deviceArtDescriptors, deviceModel);
+ }
+
+ if (index < 0) {
+ // Assume that if the min resolution is > 1280, then we are on a tablet
+ String defaultDevice = Math.min(image.getWidth(), image.getHeight()) > 1280 ? "Generic Tablet" : "Generic Phone";
+ index = findDescriptorIndexForProduct(deviceArtDescriptors, defaultDevice);
+ }
+
+ // If we can't find anything (which shouldn't happen since we should get the Generic Phone/Tablet),
+ // default to the first one.
+ if (index < 0) {
+ index = 0;
+ }
+
+ return index;
+ }
+
+ private static int findDescriptorIndexForProduct(List<DeviceArtDescriptor> descriptors, String deviceModel) {
+ for (int i = 0; i < descriptors.size(); i++) {
+ DeviceArtDescriptor d = descriptors.get(i);
+ if (d.getName().equalsIgnoreCase(deviceModel)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
@Override
protected void dispose() {
myProvider.disposeEditor(myImageFileEditor);
@@ -153,6 +191,7 @@
}
private void doRefreshScreenshot() {
+ assert myDevice != null;
new ScreenshotTask(myProject, myDevice) {
@Override
public void onSuccess() {
@@ -164,14 +203,14 @@
BufferedImage image = getScreenshot();
mySourceImageRef.set(image);
- frameScreenshot(myRotationAngle);
+ processScreenshot(myFrameScreenshotCheckBox.isSelected(), myRotationAngle);
}
}.queue();
}
private void doRotateScreenshot() {
myRotationAngle = (myRotationAngle + 90) % 360;
- frameScreenshot(90);
+ processScreenshot(myFrameScreenshotCheckBox.isSelected(), 90);
}
private void doFrameScreenshot() {
@@ -182,17 +221,17 @@
myScreenGlareCheckBox.setEnabled(shouldFrame);
if (shouldFrame) {
- frameScreenshot(0);
+ processScreenshot(shouldFrame, 0);
} else {
myDisplayedImageRef.set(mySourceImageRef.get());
updateEditorImage();
}
}
- private void frameScreenshot(int rotateByAngle) {
- DeviceArtDescriptor spec = myDeviceArtSpecs.get(myDeviceArtCombo.getSelectedIndex());
- boolean shadow = myDropShadowCheckBox.isSelected();
- boolean reflection = myScreenGlareCheckBox.isSelected();
+ private void processScreenshot(boolean addFrame, int rotateByAngle) {
+ DeviceArtDescriptor spec = addFrame ? myDeviceArtDescriptors.get(myDeviceArtCombo.getSelectedIndex()) : null;
+ boolean shadow = addFrame && myDropShadowCheckBox.isSelected();
+ boolean reflection = addFrame && myScreenGlareCheckBox.isSelected();
new ImageProcessorTask(myProject, mySourceImageRef.get(), rotateByAngle, spec, shadow, reflection) {
@Override
@@ -315,7 +354,8 @@
FileSaverDescriptor descriptor =
new FileSaverDescriptor(AndroidBundle.message("android.ddms.screenshot.save.title"), "", SdkConstants.EXT_PNG);
FileSaverDialog saveFileDialog = FileChooserFactory.getInstance().createSaveFileDialog(descriptor, myProject);
- VirtualFileWrapper fileWrapper = saveFileDialog.save(myProject.getBaseDir(), getDefaultFileName());
+ VirtualFile baseDir = ourLastSavedFolder != null ? ourLastSavedFolder : myProject.getBaseDir();
+ VirtualFileWrapper fileWrapper = saveFileDialog.save(baseDir, getDefaultFileName());
if (fileWrapper == null) {
return;
}
@@ -331,12 +371,18 @@
return;
}
+ VirtualFile virtualFile = fileWrapper.getVirtualFile();
+ if (virtualFile != null) {
+ //noinspection AssignmentToStaticFieldFromInstanceMethod
+ ourLastSavedFolder = virtualFile.getParent();
+ }
+
super.doOKAction();
}
- private static String getDefaultFileName() {
+ private String getDefaultFileName() {
Calendar now = Calendar.getInstance();
- return String.format("device-%tF-%tH%tM%tS.png", now, now, now, now);
+ return String.format("%s-%tF-%tH%tM%tS.png", myDevice != null ? "device" : "layout", now, now, now, now);
}
public File getScreenshot() {
diff --git a/android/src/com/android/tools/idea/diagnostics/error/AnonymousFeedbackTask.java b/android/src/com/android/tools/idea/diagnostics/error/AnonymousFeedbackTask.java
index c9faa37..f76d0ab 100644
--- a/android/src/com/android/tools/idea/diagnostics/error/AnonymousFeedbackTask.java
+++ b/android/src/com/android/tools/idea/diagnostics/error/AnonymousFeedbackTask.java
@@ -33,6 +33,7 @@
public class AnonymousFeedbackTask extends Task.Backgroundable {
private final Consumer<String> myCallback;
private final Consumer<Exception> myErrorCallback;
+ private final Throwable myThrowable;
private final Map<String, String> myParams;
private final String myErrorMessage;
private final String myErrorDescription;
@@ -41,6 +42,7 @@
public AnonymousFeedbackTask(@Nullable Project project,
@NotNull String title,
boolean canBeCancelled,
+ @Nullable Throwable throwable,
Map<String, String> params,
String errorMessage,
String errorDescription,
@@ -49,6 +51,7 @@
final Consumer<Exception> errorCallback) {
super(project, title, canBeCancelled);
+ myThrowable = throwable;
myParams = params;
myErrorMessage = errorMessage;
myErrorDescription = errorDescription;
@@ -61,7 +64,7 @@
public void run(@NotNull ProgressIndicator indicator) {
indicator.setIndeterminate(true);
try {
- String token = sendFeedback(new ProxyHttpConnectionFactory(), myParams,
+ String token = sendFeedback(new ProxyHttpConnectionFactory(), myThrowable, myParams,
myErrorMessage, myErrorDescription, myAppVersion);
myCallback.consume(token);
}
diff --git a/android/src/com/android/tools/idea/diagnostics/error/ErrorReporter.java b/android/src/com/android/tools/idea/diagnostics/error/ErrorReporter.java
index 7552bf6..0e6a12f 100644
--- a/android/src/com/android/tools/idea/diagnostics/error/ErrorReporter.java
+++ b/android/src/com/android/tools/idea/diagnostics/error/ErrorReporter.java
@@ -87,15 +87,8 @@
final String description) {
final DataContext dataContext = DataManager.getInstance().getDataContext(parentComponent);
- bean.setDescription(description != null ? description : event.getThrowableText());
- String message = event.getMessage();
- if (message == null && event.getThrowable() != null) {
- message = event.getThrowable().toString();
- }
- if (message == null) {
- message = "Unclassified error";
- }
- bean.setMessage(message);
+ bean.setDescription(description);
+ bean.setMessage(event.getMessage());
Throwable t = event.getThrowable();
if (t != null) {
@@ -150,7 +143,7 @@
}
};
AnonymousFeedbackTask task =
- new AnonymousFeedbackTask(project, "Submitting error report", true, pair2map(kv),
+ new AnonymousFeedbackTask(project, "Submitting error report", true, t, pair2map(kv),
bean.getMessage(), bean.getDescription(),
ApplicationInfo.getInstance().getFullVersion(),
successCallback, errorCallback);
diff --git a/android/src/com/android/tools/idea/editors/AndroidImportFilter.java b/android/src/com/android/tools/idea/editors/AndroidImportFilter.java
new file mode 100644
index 0000000..483d515
--- /dev/null
+++ b/android/src/com/android/tools/idea/editors/AndroidImportFilter.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.editors;
+
+import com.android.resources.ResourceType;
+import com.intellij.codeInsight.ImportFilter;
+import org.jetbrains.annotations.NotNull;
+
+import static com.android.SdkConstants.CLASS_R;
+
+public class AndroidImportFilter extends ImportFilter {
+ /** Never import android.R, or inner classes of application R or android.R classes */
+ @Override
+ public boolean shouldUseFullyQualifiedName(@NotNull String classQualifiedName) {
+ if (classQualifiedName.endsWith(".R")) {
+ return CLASS_R.equals(classQualifiedName);
+ }
+ int index = classQualifiedName.lastIndexOf('.');
+ if (index > 2 && classQualifiedName.charAt(index - 1) == 'R' && classQualifiedName.charAt(index - 2) == '.') {
+ // Only accept R inner classes that look like they really are resource classes, e.g.
+ // foo.bar.R.string and foo.bar.R.layout, but not my.weird.R.pkg
+ return classQualifiedName.startsWith(CLASS_R) || ResourceType.getEnum(classQualifiedName.substring(index + 1)) != null;
+ }
+
+ return false;
+ }
+}
diff --git a/android/src/com/android/tools/idea/editors/GeneratedFileNotificationProvider.java b/android/src/com/android/tools/idea/editors/GeneratedFileNotificationProvider.java
index 51907a8..936a1c1 100644
--- a/android/src/com/android/tools/idea/editors/GeneratedFileNotificationProvider.java
+++ b/android/src/com/android/tools/idea/editors/GeneratedFileNotificationProvider.java
@@ -29,6 +29,8 @@
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.Nullable;
+import static com.android.tools.idea.gradle.project.AndroidContentRoot.BUILD_DIR;
+
public class GeneratedFileNotificationProvider extends EditorNotifications.Provider<EditorNotificationPanel> {
private static final Key<EditorNotificationPanel> KEY = Key.create("android.generated.file.ro");
private final Project myProject;
@@ -60,14 +62,23 @@
}
// TODO: Look up build folder via Gradle project metadata.
- if (!file.getPath().contains("build")) { // fast fail
+ if (!file.getPath().contains(BUILD_DIR)) { // fast fail
return null;
}
for (VirtualFile baseDir : ModuleRootManager.getInstance(module).getContentRoots()) {
- VirtualFile build = baseDir.findChild("build");
+ VirtualFile build = baseDir.findChild(BUILD_DIR);
if (build != null && VfsUtilCore.isAncestor(build, file, true)) {
+ VirtualFile explodedBundled = build.findChild("exploded-bundles");
+ boolean inAar = explodedBundled != null && VfsUtilCore.isAncestor(explodedBundled, file, true);
+ String text;
+ if (inAar) {
+ text = "Resource files inside Android library archive files (.aar) should not be edited";
+ } else {
+ text = "Files under the build folder are generated and should not be edited.";
+ }
+
EditorNotificationPanel panel = new EditorNotificationPanel();
- panel.setText("Files under the build folder are generated and should not be edited.");
+ panel.setText(text);
return panel;
}
}
diff --git a/android/src/com/android/tools/idea/editors/NinePatchEditor.java b/android/src/com/android/tools/idea/editors/NinePatchEditor.java
index cba3787..66ad803 100644
--- a/android/src/com/android/tools/idea/editors/NinePatchEditor.java
+++ b/android/src/com/android/tools/idea/editors/NinePatchEditor.java
@@ -180,6 +180,7 @@
if (myImageEditorPanel != null) {
myImageEditorPanel.getViewer().removePatchUpdateListener(this);
+ myImageEditorPanel.dispose();
myImageEditorPanel = null;
}
}
diff --git a/android/src/com/android/tools/idea/editors/NinePatchEditorProvider.java b/android/src/com/android/tools/idea/editors/NinePatchEditorProvider.java
index 917d5a8..dfb73c5 100644
--- a/android/src/com/android/tools/idea/editors/NinePatchEditorProvider.java
+++ b/android/src/com/android/tools/idea/editors/NinePatchEditorProvider.java
@@ -27,7 +27,6 @@
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vfs.VirtualFile;
import org.jdom.Element;
-import com.android.tools.idea.fileTypes.AndroidNinePatchFileType;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
diff --git a/android/src/com/android/tools/idea/editors/navigation/AndroidRootComponent.java b/android/src/com/android/tools/idea/editors/navigation/AndroidRootComponent.java
index e0e2e30..c0eddf5 100644
--- a/android/src/com/android/tools/idea/editors/navigation/AndroidRootComponent.java
+++ b/android/src/com/android/tools/idea/editors/navigation/AndroidRootComponent.java
@@ -20,37 +20,55 @@
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
-import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
-import com.intellij.psi.PsiManager;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
+import java.awt.Point;
import java.awt.image.BufferedImage;
+import java.util.List;
public class AndroidRootComponent extends JComponent {
- private static final Object RENDERING_LOCK = new Object();
- private RenderResult myRenderResult = null;
- private double myScale;
+ //private static final Object RENDERING_LOCK = new Object();
- public AndroidRootComponent() {
- myScale = 1;
+ private final RenderingParameters myRenderingParameters;
+ private final PsiFile myPsiFile;
+
+ @NotNull
+ Transform transform = new Transform(1);
+ private Image myScaledImage;
+ private RenderResult myRenderResult = null;
+
+ public AndroidRootComponent(@NotNull final RenderingParameters renderingParameters, @Nullable final PsiFile psiFile) {
+ this.myRenderingParameters = renderingParameters;
+ this.myPsiFile = psiFile;
}
@Nullable
- public RenderResult getRenderResult() {
+ private RenderResult getRenderResult() {
return myRenderResult;
}
- public double getScale() {
- return myScale;
+ private void setRenderResult(@Nullable RenderResult renderResult) {
+ myRenderResult = renderResult;
+ revalidate(); // once we have finished rendering we will know where our internal views are, need to relayout otherwise
+ repaint();
}
- public void setScale(double scale) {
- myScale = scale;
+ public float getScale() {
+ return transform.myScale;
+ }
+
+ private void invalidate2() {
+ myScaledImage = null;
+ }
+
+ public void setScale(float scale) {
+ transform = new Transform(scale);
+ invalidate2();
}
@Nullable
@@ -60,13 +78,14 @@
}
@Override
- public void paintComponent(Graphics g) {
- //ScalableImage image = myRenderResult.getImage();
- //if (image != null) {
- // image.paint(g);
- //}
- BufferedImage image = getImage();
- if (image != null) {
+ public Dimension getPreferredSize() {
+ return myRenderingParameters.getDeviceScreenSizeFor(transform);
+ }
+
+ //ScalableImage image = myRenderResult.getImage();
+ //if (image != null) {
+ // image.paint(g);
+ //}
/*
if (false) {
Graphics2D g2 = (Graphics2D)g;
@@ -80,39 +99,98 @@
g.drawImage(image, 0, 0, getWidth(), getHeight(), null);
}
*/
- g.drawImage(ImageUtils.scale(image, myScale, myScale, 0, 0), 0, 0, getWidth(), getHeight(), null);
+
+ private Image getScaledImage() {
+ if (myScaledImage == null ||
+ myScaledImage.getWidth(null) != getWidth() ||
+ myScaledImage.getHeight(null) != getHeight()) {
+ BufferedImage image = getImage();
+ if (image != null) {
+ myScaledImage = ImageUtils.scale(image, transform.myScale, transform.myScale, 0, 0);
+ }
}
+ return myScaledImage;
}
@Override
- public Dimension getPreferredSize() {
- //return myRenderResult.getImage().getRequiredSize();
- BufferedImage image = getImage();
- return image == null ? new Dimension() : new Dimension(image.getWidth(), image.getHeight());
+ public void paintComponent(Graphics g) {
+ Image scaledImage = getScaledImage();
+ if (scaledImage != null) {
+ g.drawImage(scaledImage, 0, 0, null);
+ }
+ else {
+ g.setColor(Color.WHITE);
+ g.fillRect(0, 0, getWidth(), getHeight());
+ g.setColor(Color.GRAY);
+ String message = "Initialising...";
+ Font font = g.getFont();
+ int messageWidth = getFontMetrics(font).stringWidth(message);
+ g.drawString(message, (getWidth() - messageWidth) / 2, getHeight() / 2);
+ render();
+ }
}
- public void render(@NotNull final Project project, @NotNull final VirtualFile file) {
- ApplicationManager.getApplication().runReadAction(new Runnable() {
+ private void render() {
+ if (myPsiFile == null) {
+ return;
+ }
+ Project project = myRenderingParameters.myProject;
+ AndroidFacet facet = myRenderingParameters.myFacet;
+ Configuration configuration = myRenderingParameters.myConfiguration;
+
+ if (project.isDisposed()) {
+ return;
+ }
+ Module module = facet.getModule();
+ RenderLogger logger = new RenderLogger(myPsiFile.getName(), module);
+ //synchronized (RENDERING_LOCK) {
+ final RenderService service = RenderService.create(facet, module, myPsiFile, configuration, logger, null);
+ // The rendering service takes long enough to initialise that we don't want to do this from the EDT.
+ // Further, IntellJ's helper classes don't not allow read access from outside EDT, so we need nested runnables.
+ ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
@Override
public void run() {
- PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
- AndroidFacet facet = AndroidFacet.getInstance(psiFile);
- synchronized (RENDERING_LOCK) {
- Module module = facet.getModule();
- Configuration configuration = facet.getConfigurationManager().getConfiguration(file);
- configuration.setTheme("@android:style/Theme.Holo");
- final RenderLogger logger = new RenderLogger(file.getName(), module);
- RenderService service = RenderService.create(facet, module, psiFile, configuration, logger, null);
- if (service != null) {
- myRenderResult = service.render();
- service.dispose();
+ ApplicationManager.getApplication().runReadAction(new Runnable() {
+ @Override
+ public void run() {
+ if (service != null) {
+ setRenderResult(service.render());
+ service.dispose();
+ }
+ //}
}
- else {
- myRenderResult = new RenderResult(null, null, psiFile, logger);
- }
- repaint();
- }
+ });
}
});
}
+
+ @Nullable
+ public RenderedView getRootView() {
+ RenderResult renderResult = getRenderResult();
+ if (renderResult == null) {
+ return null;
+ }
+ RenderedViewHierarchy hierarchy = renderResult.getHierarchy();
+ if (hierarchy == null) {
+ return null;
+ }
+ List<RenderedView> roots = hierarchy.getRoots();
+ if (roots.isEmpty()) {
+ return null;
+ }
+ return roots.get(0);
+ }
+
+ @Nullable
+ public RenderedView getRenderedView(Point p) {
+ RenderResult renderResult = getRenderResult();
+ if (renderResult == null) {
+ return null;
+ }
+ RenderedViewHierarchy hierarchy = renderResult.getHierarchy();
+ if (hierarchy == null) {
+ return null;
+ }
+ return hierarchy.findLeafAt(transform.viewToModel(p.x), transform.viewToModel(p.y));
+ }
}
diff --git a/android/src/com/android/tools/idea/editors/navigation/Assoc.java b/android/src/com/android/tools/idea/editors/navigation/Assoc.java
new file mode 100644
index 0000000..2e94104
--- /dev/null
+++ b/android/src/com/android/tools/idea/editors/navigation/Assoc.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.editors.navigation;
+
+import java.util.HashMap;
+import java.util.Map;
+
+class Assoc<K, V> {
+ public final Map<K, V> keyToValue = new HashMap<K, V>();
+ public final Map<V, K> valueToKey = new HashMap<V, K>();
+
+ public void add(K key, V value) {
+ keyToValue.put(key, value);
+ valueToKey.put(value, key);
+ }
+
+ public void remove(K key, V value) {
+ keyToValue.remove(key);
+ valueToKey.remove(value);
+ }
+
+ public void clear() {
+ keyToValue.clear();
+ valueToKey.clear();
+ }
+}
diff --git a/android/src/com/android/tools/idea/editors/navigation/NavigationEditor.java b/android/src/com/android/tools/idea/editors/navigation/NavigationEditor.java
index 0166fe3..c084ba2 100644
--- a/android/src/com/android/tools/idea/editors/navigation/NavigationEditor.java
+++ b/android/src/com/android/tools/idea/editors/navigation/NavigationEditor.java
@@ -23,31 +23,38 @@
import com.intellij.AppTopics;
import com.intellij.codeHighlighting.BackgroundEditorHighlighter;
import com.intellij.ide.structureView.StructureViewBuilder;
+import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.*;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.IdeBorderFactory;
+import com.intellij.ui.SideBorder;
import com.intellij.ui.components.JBScrollPane;
+import icons.AndroidIcons;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
+import java.awt.*;
import java.beans.PropertyChangeListener;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class NavigationEditor implements FileEditor {
+ public static final String TOOLBAR = "NavigationEditorToolbar";
private static final Logger LOG = Logger.getInstance("#" + NavigationEditor.class.getName());
private static final String NAME = "Navigation";
- public static final int INITIAL_FILE_BUFFER_SIZE = 1000;
+ private static final int INITIAL_FILE_BUFFER_SIZE = 1000;
+ private static final int SCROLL_UNIT_INCREMENT = 20;
private final UserDataHolderBase myUserDataHolder = new UserDataHolderBase();
- private final NavigationModel myNavigationModel;
- private final Listener<Void> myNavigationModelListener;
+ private NavigationModel myNavigationModel;
+ private final Listener<NavigationModel.Event> myNavigationModelListener;
private VirtualFile myFile;
- private final JComponent myComponent;
+ private JComponent myComponent;
private boolean myDirty;
public NavigationEditor(Project project, VirtualFile file) {
@@ -66,24 +73,93 @@
project.getMessageBus().connect(this).subscribe(AppTopics.FILE_DOCUMENT_SYNC, saveListener);
myFile = file;
- myNavigationModel = read(file);
- // component = new NavigationModelEditorPanel1(project, file, read(file));
- myComponent = new JBScrollPane(new NavigationEditorPanel2(project, file, myNavigationModel));
- myNavigationModelListener = new Listener<Void>() {
+ try {
+ myNavigationModel = read(file);
+ // component = new NavigationModelEditorPanel1(project, file, read(file));
+ NavigationEditorPanel editor = new NavigationEditorPanel(project, file, myNavigationModel);
+ JBScrollPane scrollPane = new JBScrollPane(editor);
+ scrollPane.getVerticalScrollBar().setUnitIncrement(SCROLL_UNIT_INCREMENT);
+ JPanel p = new JPanel(new BorderLayout());
+
+ JComponent controls = createToolbar(editor);
+ p.add(controls, BorderLayout.NORTH);
+ p.add(scrollPane);
+ myComponent = p;
+ }
+ catch (FileReadException e) {
+ myNavigationModel = new NavigationModel();
+ {
+ JPanel panel = new JPanel(new BorderLayout());
+ JLabel message = new JLabel("Invalid Navigation File");
+ Font font = message.getFont();
+ message.setFont(font.deriveFont(30f));
+ panel.add(message, BorderLayout.NORTH);
+ panel.add(new JLabel(e.getMessage()), BorderLayout.CENTER);
+ myComponent = new JBScrollPane(panel);
+ }
+ }
+ myNavigationModelListener = new Listener<NavigationModel.Event>() {
@Override
- public void notify(Void unused) {
+ public void notify(@NotNull NavigationModel.Event event) {
myDirty = true;
}
};
myNavigationModel.getListeners().add(myNavigationModelListener);
}
- private static NavigationModel read(VirtualFile file) {
+ // See AndroidDesignerActionPanel
+ protected JComponent createToolbar(NavigationEditorPanel myDesigner) {
+ JPanel panel = new JPanel(new BorderLayout());
+ panel.setBorder(IdeBorderFactory.createBorder(SideBorder.BOTTOM));
+
+ ActionManager actionManager = ActionManager.getInstance();
+ ActionToolbar zoomToolBar = actionManager.createActionToolbar(TOOLBAR, getActions(myDesigner), true);
+ JPanel bottom = new JPanel(new BorderLayout());
+ //bottom.add(layoutToolBar.getComponent(), BorderLayout.WEST);
+ bottom.add(zoomToolBar.getComponent(), BorderLayout.EAST);
+ panel.add(bottom, BorderLayout.SOUTH);
+
+ return panel;
+ }
+
+ private static class FileReadException extends Exception {
+ private FileReadException(Throwable throwable) {
+ super(throwable);
+ }
+ }
+
+ // See AndroidDesignerActionPanel
+ private static ActionGroup getActions(final NavigationEditorPanel myDesigner) {
+ DefaultActionGroup group = new DefaultActionGroup();
+
+ group.add(new AnAction(null, "Zoom Out (-)", AndroidIcons.ZoomOut) {
+ @Override
+ public void actionPerformed(AnActionEvent e) {
+ myDesigner.zoom(false);
+ }
+ });
+ group.add(new AnAction(null, "Reset Zoom to 100% (1)", AndroidIcons.ZoomActual) {
+ @Override
+ public void actionPerformed(AnActionEvent e) {
+ myDesigner.setScale(1);
+ }
+ });
+ group.add(new AnAction(null, "Zoom In (+)", AndroidIcons.ZoomIn) {
+ @Override
+ public void actionPerformed(AnActionEvent e) {
+ myDesigner.zoom(true);
+ }
+ });
+
+ return group;
+ }
+
+ private static NavigationModel read(VirtualFile file) throws FileReadException {
try {
return (NavigationModel)new XMLReader(file.getInputStream()).read();
}
- catch (IOException e) {
- throw new RuntimeException(e);
+ catch (Exception e) {
+ throw new FileReadException(e);
}
}
diff --git a/android/src/com/android/tools/idea/editors/navigation/NavigationEditorPanel.java b/android/src/com/android/tools/idea/editors/navigation/NavigationEditorPanel.java
new file mode 100644
index 0000000..fc46708
--- /dev/null
+++ b/android/src/com/android/tools/idea/editors/navigation/NavigationEditorPanel.java
@@ -0,0 +1,922 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.editors.navigation;
+
+import com.android.navigation.*;
+import com.android.tools.idea.configurations.Configuration;
+import com.android.tools.idea.rendering.RenderedView;
+import com.android.tools.idea.rendering.ShadowPainter;
+import com.intellij.ide.dnd.DnDEvent;
+import com.intellij.ide.dnd.DnDManager;
+import com.intellij.ide.dnd.DnDTarget;
+import com.intellij.ide.dnd.TransferableWrapper;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Condition;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileSystem;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.PsiQualifiedNamedElement;
+import com.intellij.psi.xml.XmlTag;
+import com.intellij.ui.Gray;
+import com.intellij.util.ui.UIUtil;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.Point;
+import java.awt.event.*;
+import java.awt.image.BufferedImage;
+import java.util.*;
+
+import static com.android.tools.idea.editors.navigation.Utilities.*;
+
+public class NavigationEditorPanel extends JComponent {
+ private static final Dimension GAP = new Dimension(150, 50);
+ private static final Color BACKGROUND_COLOR = Gray.get(192);
+ private static final Color SNAP_GRID_LINE_COLOR_MINOR = Gray.get(180);
+ private static final Color SNAP_GRID_LINE_COLOR_MIDDLE = Gray.get(170);
+ private static final Color SNAP_GRID_LINE_COLOR_MAJOR = Gray.get(160);
+
+ private static final float ZOOM_FACTOR = 1.1f;
+
+ // Snap grid
+ private static final int MINOR_SNAP = 32;
+ private static final int MIDDLE_COUNT = 5;
+ private static final int MAJOR_COUNT = 10;
+
+ public static final Dimension MINOR_SNAP_GRID = new Dimension(MINOR_SNAP, MINOR_SNAP);
+ public static final Dimension MIDDLE_SNAP_GRID = scale(MINOR_SNAP_GRID, MIDDLE_COUNT);
+ public static final Dimension MAJOR_SNAP_GRID = scale(MINOR_SNAP_GRID, MAJOR_COUNT);
+ public static final int MIN_GRID_LINE_SEPARATION = 8;
+
+ public static final int LINE_WIDTH = 12;
+ private static final Point MULTIPLE_DROP_STRIDE = point(MAJOR_SNAP_GRID);
+ private static final String ID_PREFIX = "@+id/";
+ private static final Color TRANSITION_LINE_COLOR = new Color(80, 80, 255);
+ private static final Condition<Component> SCREENS = instanceOf(AndroidRootComponent.class);
+ private static final Condition<Component> EDITORS = not(SCREENS);
+
+ private final RenderingParameters myMyRenderingParams;
+ private final VirtualFile myFile;
+ private final NavigationModel myNavigationModel;
+
+ private final Assoc<State, AndroidRootComponent> myStateComponentAssociation = new Assoc<State, AndroidRootComponent>();
+ private final Assoc<Transition, Component> myTransitionEditorAssociation = new Assoc<Transition, Component>();
+
+ private boolean myStateCacheIsValid;
+ private boolean myTransitionEditorCacheIsValid;
+ @NotNull private Selections.Selection mySelection = Selections.NULL;
+ private Map<State, Map<String, RenderedView>> myLocationToRenderedView = new IdentityHashMap<State, Map<String, RenderedView>>();
+ private Image myBackgroundImage;
+ private Point myMouseLocation;
+ private Transform myTransform = new Transform(1 / 4f);
+
+ // Configuration
+
+ private boolean showRollover = false;
+
+ @Nullable
+ private static RenderingParameters getRenderingParams(Project project, VirtualFile file) {
+ if (file != null) {
+ PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
+ if (psiFile != null) {
+ AndroidFacet facet = AndroidFacet.getInstance(psiFile);
+ if (facet != null) {
+ Configuration configuration = facet.getConfigurationManager().getConfiguration(file);
+ return new RenderingParameters(project, configuration, facet);
+ }
+ }
+ }
+ return null;
+ }
+
+ public NavigationEditorPanel(Project project, VirtualFile file, NavigationModel model) {
+ myMyRenderingParams = getRenderingParams(project, file);
+ myFile = file;
+ myNavigationModel = model;
+
+ setFocusable(true);
+ setBackground(BACKGROUND_COLOR);
+ setLayout(null);
+
+ // Mouse listener
+ {
+ MouseAdapter mouseListener = new MyMouseListener();
+ addMouseListener(mouseListener);
+ addMouseMotionListener(mouseListener);
+ }
+
+ // Focus listener
+ {
+ addFocusListener(new FocusListener() {
+ @Override
+ public void focusGained(FocusEvent focusEvent) {
+ repaint();
+ }
+
+ @Override
+ public void focusLost(FocusEvent focusEvent) {
+ repaint();
+ }
+ });
+ }
+
+ // Drag and Drop listener
+ {
+ final DnDManager dndManager = DnDManager.getInstance();
+ dndManager.registerTarget(new MyDnDTarget(), this);
+ }
+
+ // Key listeners
+ {
+ Action remove = new AbstractAction() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ mySelection.remove();
+ setSelection(Selections.NULL);
+ }
+ };
+ registerKeyBinding(KeyEvent.VK_DELETE, "delete", remove);
+ registerKeyBinding(KeyEvent.VK_BACK_SPACE, "backspace", remove);
+ }
+
+ // Model listener
+ {
+ myNavigationModel.getListeners().add(new Listener<NavigationModel.Event>() {
+ @Override
+ public void notify(@NotNull NavigationModel.Event event) {
+ if (event.operation != NavigationModel.Event.Operation.UPDATE) {
+ if (event.operandType.isAssignableFrom(State.class)) {
+ myStateCacheIsValid = false;
+ }
+ if (event.operandType.isAssignableFrom(Transition.class)) {
+ myTransitionEditorCacheIsValid = false;
+ }
+ }
+ repaint();
+ }
+ });
+ }
+ }
+
+ @Nullable
+ private static RenderedView getRenderedView(AndroidRootComponent c, Point location) {
+ return c.getRenderedView(diff(location, c.getLocation()));
+ }
+
+ @Nullable
+ Transition getTransition(AndroidRootComponent sourceComponent, @Nullable RenderedView namedSourceLeaf, Point mouseUpLocation) {
+ Component destComponent = getComponentAt(mouseUpLocation);
+ if (sourceComponent != destComponent) {
+ if (destComponent instanceof AndroidRootComponent) {
+ AndroidRootComponent destinationRoot = (AndroidRootComponent)destComponent;
+ RenderedView endLeaf = getRenderedView(destinationRoot, mouseUpLocation);
+ RenderedView namedEndLeaf = getNamedParent(endLeaf);
+
+ Map<AndroidRootComponent, State> rootComponentToState = getStateComponentAssociation().valueToKey;
+ Locator sourceLocator = Locator.of(rootComponentToState.get(sourceComponent), getViewId(namedSourceLeaf));
+ Locator destinationLocator = Locator.of(rootComponentToState.get(destComponent), getViewId(namedEndLeaf));
+ return new Transition("", sourceLocator, destinationLocator);
+ }
+ }
+ return null;
+ }
+
+ static Rectangle getBounds(AndroidRootComponent c, @Nullable RenderedView leaf) {
+ if (leaf == null) {
+ return c.getBounds();
+ }
+ Rectangle r = c.transform.getBounds(leaf);
+ return new Rectangle(c.getX() + r.x, c.getY() + r.y, r.width, r.height);
+ }
+
+ Rectangle getNamedLeafBoundsAt(Component sourceComponent, Point location) {
+ Component destComponent = getComponentAt(location);
+ if (sourceComponent != destComponent) {
+ if (destComponent instanceof AndroidRootComponent) {
+ AndroidRootComponent destinationRoot = (AndroidRootComponent)destComponent;
+ RenderedView endLeaf = getRenderedView(destinationRoot, location);
+ RenderedView namedEndLeaf = getNamedParent(endLeaf);
+ return getBounds(destinationRoot, namedEndLeaf);
+ }
+ }
+ return new Rectangle(location);
+ }
+
+ public float getScale() {
+ return myTransform.myScale;
+ }
+
+ public void setScale(float scale) {
+ myTransform = new Transform(scale);
+ myBackgroundImage = null;
+ for (AndroidRootComponent root : getStateComponentAssociation().keyToValue.values()) {
+ root.setScale(scale);
+ }
+ setPreferredSize();
+
+ revalidate();
+ repaint();
+ }
+
+ public void zoom(boolean in) {
+ setScale(myTransform.myScale * (in ? ZOOM_FACTOR : 1 / ZOOM_FACTOR));
+ }
+
+ private Assoc<State, AndroidRootComponent> getStateComponentAssociation() {
+ if (!myStateCacheIsValid) {
+ syncStateCache(myStateComponentAssociation);
+ myStateCacheIsValid = true;
+ }
+ return myStateComponentAssociation;
+ }
+
+ private Assoc<Transition, Component> getTransitionEditorAssociation() {
+ if (!myTransitionEditorCacheIsValid) {
+ syncTransitionCache(myTransitionEditorAssociation);
+ myTransitionEditorCacheIsValid = true;
+ }
+ return myTransitionEditorAssociation;
+ }
+
+ @Nullable
+ static String getViewId(@Nullable RenderedView leaf) {
+ if (leaf != null) {
+ XmlTag tag = leaf.tag;
+ if (tag != null) {
+ String attributeValue = tag.getAttributeValue("android:id");
+ if (attributeValue != null && attributeValue.startsWith(ID_PREFIX)) {
+ return attributeValue.substring(ID_PREFIX.length());
+ }
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ static RenderedView getNamedParent(@Nullable RenderedView view) {
+ while (view != null && getViewId(view) == null) {
+ view = view.getParent();
+ }
+ return view;
+ }
+
+ private Map<String, RenderedView> getNameToRenderedView(State state) {
+ Map<String, RenderedView> result = myLocationToRenderedView.get(state);
+ if (result == null) {
+ RenderedView root = getStateComponentAssociation().keyToValue.get(state).getRootView();
+ if (root != null) {
+ myLocationToRenderedView.put(state, result = createViewNameToRenderedView(root));
+ }
+ else {
+ return Collections.emptyMap(); // rendering library hasn't loaded, temporarily return an empty map
+ }
+ }
+ return result;
+ }
+
+ private static Map<String, RenderedView> createViewNameToRenderedView(@NotNull RenderedView root) {
+ final Map<String, RenderedView> result = new HashMap<String, RenderedView>();
+ new Object() {
+ void walk(RenderedView parent) {
+ for (RenderedView child : parent.getChildren()) {
+ String id = getViewId(child);
+ if (id != null) {
+ result.put(id, child);
+ }
+ walk(child);
+ }
+ }
+ }.walk(root);
+ return result;
+ }
+
+ static void paintLeaf(Graphics g, @Nullable RenderedView leaf, Color color, AndroidRootComponent component) {
+ if (leaf != null) {
+ Color oldColor = g.getColor();
+ g.setColor(color);
+ drawRectangle(g, getBounds(component, leaf));
+ g.setColor(oldColor);
+ }
+ }
+
+ private void registerKeyBinding(int keyCode, String name, Action action) {
+ InputMap inputMap = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
+ inputMap.put(KeyStroke.getKeyStroke(keyCode, 0), name);
+ getActionMap().put(name, action);
+ }
+
+ private void setSelection(@NotNull Selections.Selection selection) {
+ mySelection = selection;
+ // the re-validate() call shouldn't be necessary but removing it causes orphaned
+ // combo-boxes to remain visible (and click-able) after a 'remove' operation
+ revalidate();
+ repaint();
+ }
+
+ private void moveSelection(Point location) {
+ mySelection.moveTo(location);
+ revalidate();
+ repaint();
+ }
+
+ private void setMouseLocation(Point mouseLocation) {
+ myMouseLocation = mouseLocation;
+ if (showRollover) {
+ repaint();
+ }
+ }
+
+ private void finaliseSelectionLocation(Point location) {
+ mySelection = mySelection.finaliseSelectionLocation(location);
+ revalidate();
+ repaint();
+ }
+
+ /*
+ private List<State> findDestinationsFor(State state, Set<State> exclude) {
+ List<State> result = new ArrayList<State>();
+ for (Transition transition : myNavigationModel) {
+ State source = transition.getSource();
+ if (source.equals(state)) {
+ State destination = transition.getDestination();
+ if (!exclude.contains(destination)) {
+ result.add(destination);
+ }
+ }
+ }
+ return result;
+ }
+ */
+
+ private void drawGrid(Graphics g, Color c, Dimension modelSize, int width, int height) {
+ g.setColor(c);
+ Dimension viewSize = myTransform.modelToView(modelSize);
+ if (viewSize.width < MIN_GRID_LINE_SEPARATION || viewSize.height < MIN_GRID_LINE_SEPARATION) {
+ return;
+ }
+ for (int x = 0; x < myTransform.viewToModel(width); x += modelSize.width) {
+ int vx = myTransform.modelToView(x);
+ g.drawLine(vx, 0, vx, getHeight());
+ }
+ for (int y = 0; y < myTransform.viewToModel(height); y += modelSize.height) {
+ int vy = myTransform.modelToView(y);
+ g.drawLine(0, vy, getWidth(), vy);
+ }
+ }
+
+ private void drawBackground(Graphics g, int width, int height) {
+ g.setColor(BACKGROUND_COLOR);
+ g.fillRect(0, 0, width, height);
+
+ drawGrid(g, SNAP_GRID_LINE_COLOR_MINOR, MINOR_SNAP_GRID, width, height);
+ drawGrid(g, SNAP_GRID_LINE_COLOR_MIDDLE, MIDDLE_SNAP_GRID, width, height);
+ drawGrid(g, SNAP_GRID_LINE_COLOR_MAJOR, MAJOR_SNAP_GRID, width, height);
+ }
+
+ private Image getBackGroundImage() {
+ if (myBackgroundImage == null ||
+ myBackgroundImage.getWidth(null) != getWidth() ||
+ myBackgroundImage.getHeight(null) != getHeight()) {
+ myBackgroundImage = UIUtil.createImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
+ drawBackground(myBackgroundImage.getGraphics(), getWidth(), getHeight());
+ }
+ return myBackgroundImage;
+ }
+
+ @Override
+ protected void paintComponent(Graphics g) {
+ super.paintComponent(g);
+
+ // draw background
+ g.drawImage(getBackGroundImage(), 0, 0, null);
+
+ // draw component shadows
+ for (Component c : getStateComponentAssociation().keyToValue.values()) {
+ Rectangle r = c.getBounds();
+ ShadowPainter.drawRectangleShadow(g, r.x, r.y, r.width, r.height);
+ }
+ }
+
+ public static Graphics2D createLineGraphics(Graphics g, int lineWidth) {
+ Graphics2D g2D = (Graphics2D)g.create();
+ g2D.setColor(TRANSITION_LINE_COLOR);
+ g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g2D.setStroke(new BasicStroke(lineWidth));
+ return g2D;
+ }
+
+ private static Rectangle getCorner(Point a, int cornerDiameter) {
+ int cornerRadius = cornerDiameter / 2;
+ return new Rectangle(a.x - cornerRadius, a.y - cornerRadius, cornerDiameter, cornerDiameter);
+ }
+
+ private static void drawLine(Graphics g, Point a, Point b) {
+ g.drawLine(a.x, a.y, b.x, b.y);
+ }
+
+ private static void drawArrow(Graphics g, Point a, Point b, int lineWidth) {
+ Utilities.drawArrow(g, a.x, a.y, b.x, b.y, lineWidth);
+ }
+
+ private static void drawRectangle(Graphics g, Rectangle r) {
+ g.drawRect(r.x, r.y, r.width, r.height);
+ }
+
+ private static int x1(Rectangle src) {
+ return src.x;
+ }
+
+ private static int x2(Rectangle dst) {
+ return dst.x + dst.width;
+ }
+
+ private static int y1(Rectangle src) {
+ return src.y;
+ }
+
+ private static int y2(Rectangle dst) {
+ return dst.y + dst.height;
+ }
+
+ static class Line {
+ public final Point a;
+ public final Point b;
+
+ Line(Point a, Point b) {
+ this.a = a;
+ this.b = b;
+ }
+
+ Point project(Point p) {
+ boolean horizontal = a.x == b.x;
+ return horizontal ? new Point(a.x, p.y) : new Point(p.x, a.y);
+ }
+ }
+
+ static Line getMidLine(Rectangle src, Rectangle dst) {
+ Point midSrc = centre(src);
+ Point midDst = centre(dst);
+
+ int dx = Math.abs(midSrc.x - midDst.x);
+ int dy = Math.abs(midSrc.y - midDst.y);
+ boolean horizontal = dx >= dy;
+
+ int middle;
+ if (horizontal) {
+ middle = x1(src) - x2(dst) > 0 ? (x2(dst) + x1(src)) / 2 : (x2(src) + x1(dst)) / 2;
+ }
+ else {
+ middle = y1(src) - y2(dst) > 0 ? (y2(dst) + y1(src)) / 2 : (y2(src) + y1(dst)) / 2;
+ }
+
+ Point a = horizontal ? new Point(middle, midSrc.y) : new Point(midSrc.x, middle);
+ Point b = horizontal ? new Point(middle, midDst.y) : new Point(midDst.x, middle);
+
+ return new Line(a, b);
+ }
+
+ private Line getMidLine(Transition t) {
+ Map<State, AndroidRootComponent> m = getStateComponentAssociation().keyToValue;
+ State src = t.getSource().getState();
+ State dst = t.getDestination().getState();
+ return getMidLine(m.get(src).getBounds(), m.get(dst).getBounds());
+ }
+
+ static Point[] getControlPoints(Rectangle src, Rectangle dst, Line midLine) {
+ Point a = midLine.project(centre(src));
+ Point b = midLine.project(centre(dst));
+ return new Point[]{project(a, src), a, b, project(b, dst)};
+ }
+
+ private Point[] getControlPoints(Transition t) {
+ return getControlPoints(getBounds(t.getSource()), getBounds(t.getDestination()), getMidLine(t));
+ }
+
+ private static int getTurnLength(Point[] points, float scale) {
+ int N = points.length;
+ int cornerDiameter = (int)(Math.min(MAJOR_SNAP_GRID.width, MAJOR_SNAP_GRID.height) * scale);
+
+ for (int i = 0; i < N - 1; i++) {
+ Point a = points[i];
+ Point b = points[i + 1];
+
+ int length = (int)length(diff(b, a));
+ if (i != 0 && i != N - 2) {
+ length /= 2;
+ }
+ cornerDiameter = Math.min(cornerDiameter, length);
+ }
+ return cornerDiameter;
+ }
+
+ private static void drawCurve(Graphics g, Point[] points, float scale) {
+ final int N = points.length;
+ final int cornerDiameter = getTurnLength(points, scale);
+
+ boolean horizontal = points[0].x != points[1].x;
+ Point previous = points[0];
+ for (int i = 1; i < N - 1; i++) {
+ Rectangle turn = getCorner(points[i], cornerDiameter);
+ Point startTurn = project(previous, turn);
+ drawLine(g, previous, startTurn);
+ Point endTurn = project(points[i + 1], turn);
+ drawCorner(g, startTurn, endTurn, horizontal);
+ previous = endTurn;
+ horizontal = !horizontal;
+ }
+
+ Point endPoint = points[N - 1];
+ if (length(diff(previous, endPoint)) > 1) { //
+ drawArrow(g, previous, endPoint, (int)(LINE_WIDTH * scale));
+ }
+ }
+
+ public void drawTransition(Graphics g, Rectangle src, Rectangle dst, Point[] controlPoints) {
+ // draw source rect
+ drawRectangle(g, src);
+
+ // draw curved 'Manhattan route' from source to destination
+ drawCurve(g, controlPoints, myTransform.myScale);
+
+ // draw destination rect
+ Color oldColor = g.getColor();
+ g.setColor(Color.CYAN);
+ drawRectangle(g, dst);
+ g.setColor(oldColor);
+ }
+
+ private void drawTransition(Graphics g, Transition t) {
+ drawTransition(g, getBounds(t.getSource()), getBounds(t.getDestination()), getControlPoints(t));
+ }
+
+ public void paintTransitions(Graphics g) {
+ for (Transition transition : myNavigationModel.getTransitions()) {
+ drawTransition(g, transition);
+ }
+ }
+
+ private static int angle(Point p) {
+ //if ((p.x == 0) == (p.y == 0)) {
+ // throw new IllegalArgumentException();
+ //}
+ return p.x > 0 ? 0 : p.y < 0 ? 90 : p.x < 0 ? 180 : 270;
+ }
+
+ private static void drawCorner(Graphics g, Point a, Point b, boolean horizontal) {
+ int radiusX = Math.abs(a.x - b.x);
+ int radiusY = Math.abs(a.y - b.y);
+ Point centre = horizontal ? new Point(a.x, b.y) : new Point(b.x, a.y);
+ int startAngle = angle(diff(a, centre));
+ int endAngle = angle(diff(b, centre));
+ int dangle = endAngle - startAngle;
+ int angle = dangle - (Math.abs(dangle) <= 180 ? 0 : 360 * sign(dangle));
+ g.drawArc(centre.x - radiusX, centre.y - radiusY, radiusX * 2, radiusY * 2, startAngle, angle);
+ }
+
+ private RenderedView getRenderedView(Locator locator) {
+ return getNameToRenderedView(locator.getState()).get(locator.getViewName());
+ }
+
+ private void paintRollover(Graphics2D lineGraphics) {
+ if (myMouseLocation == null || !showRollover) {
+ return;
+ }
+ Component component = getComponentAt(myMouseLocation);
+ if (component instanceof AndroidRootComponent) {
+ Stroke oldStroke = lineGraphics.getStroke();
+ lineGraphics.setStroke(new BasicStroke(1));
+ AndroidRootComponent androidRootComponent = (AndroidRootComponent)component;
+ RenderedView leaf = getRenderedView(androidRootComponent, myMouseLocation);
+ RenderedView namedLeaf = getNamedParent(leaf);
+ paintLeaf(lineGraphics, leaf, Color.RED, androidRootComponent);
+ paintLeaf(lineGraphics, namedLeaf, Color.BLUE, androidRootComponent);
+ lineGraphics.setStroke(oldStroke);
+ }
+ }
+
+ private void paintSelection(Graphics g) {
+ mySelection.paint(g, hasFocus());
+ mySelection.paintOver(g);
+ }
+
+ private void paintChildren(Graphics g, Condition<Component> condition) {
+ Rectangle bounds = new Rectangle();
+ for (int i = getComponentCount() - 1; i >= 0; i--) {
+ Component child = getComponent(i);
+ if (condition.value(child)) {
+ child.getBounds(bounds);
+ Graphics cg = g.create(bounds.x, bounds.y, bounds.width, bounds.height);
+ child.paint(cg);
+ }
+ }
+ }
+
+ @Override
+ protected void paintChildren(Graphics g) {
+ paintChildren(g, SCREENS);
+ Graphics2D lineGraphics = createLineGraphics(g, myTransform.modelToView(LINE_WIDTH));
+ paintTransitions(lineGraphics);
+ paintRollover(lineGraphics);
+ paintSelection(g);
+ paintChildren(g, EDITORS);
+ }
+
+ private Rectangle getBounds(Locator source) {
+ Map<State, AndroidRootComponent> stateToComponent = getStateComponentAssociation().keyToValue;
+ AndroidRootComponent component = stateToComponent.get(source.getState());
+ return getBounds(component, getRenderedView(source));
+ }
+
+ @Override
+ public void doLayout() {
+ Map<Transition, Component> transitionToEditor = getTransitionEditorAssociation().keyToValue;
+
+ Map<State, AndroidRootComponent> stateToComponent = getStateComponentAssociation().keyToValue;
+ for (State state : stateToComponent.keySet()) {
+ AndroidRootComponent root = stateToComponent.get(state);
+ root.setLocation(myTransform.modelToView(state.getLocation()));
+ root.setSize(root.getPreferredSize());
+ }
+
+ for (Transition transition : myNavigationModel.getTransitions()) {
+ String gesture = transition.getType();
+ if (gesture != null) {
+ Component editor = transitionToEditor.get(transition);
+ Dimension preferredSize = editor.getPreferredSize();
+ Point[] points = getControlPoints(transition);
+ Point location = diff(midPoint(points[1], points[2]), midPoint(preferredSize));
+ editor.setLocation(location);
+ editor.setSize(preferredSize);
+ }
+ }
+ }
+
+ /*
+ private void addChildrenOld(Collection<State> states) {
+ final Set<State> visited = new HashSet<State>();
+ final Point location = new Point(GAP.width, GAP.height);
+ final Point maxLocation = new Point(0, 0);
+ final int gridWidth = PREVIEW_SIZE.width + GAP.width;
+ final int gridHeight = PREVIEW_SIZE.height + GAP.height;
+ getStateComponentAssociation().clear();
+ for (State state : states) {
+ if (visited.contains(state)) {
+ continue;
+ }
+ new Object() {
+ public void addChildrenFor(State source) {
+ visited.add(source);
+ add(createRootComponentFor(source, location));
+ List<State> children = findDestinationsFor(source, visited);
+ location.x += gridWidth;
+ maxLocation.x = Math.max(maxLocation.x, location.x);
+ if (children.isEmpty()) {
+ location.y += gridHeight;
+ maxLocation.y = Math.max(maxLocation.y, location.y);
+ }
+ else {
+ for (State child : children) {
+ addChildrenFor(child);
+ }
+ }
+ location.x -= gridWidth;
+ }
+ }.addChildrenFor(state);
+ }
+ setPreferredSize(new Dimension(maxLocation.x, maxLocation.y));
+ }
+ */
+
+ private <K, V extends Component> void removeLeftovers(Assoc<K, V> assoc, Collection<K> a) {
+ for (Map.Entry<K, V> e : new ArrayList<Map.Entry<K, V>>(assoc.keyToValue.entrySet())) {
+ K k = e.getKey();
+ V v = e.getValue();
+ if (!a.contains(k)) {
+ assoc.remove(k, v);
+ remove(v);
+ repaint();
+ }
+ }
+ }
+
+ private JComboBox createEditorFor(final Transition transition) {
+ String gesture = transition.getType();
+ JComboBox c = new JComboBox(new Object[]{"", "click", "list", "menu", "contains", "update"});
+ c.setSelectedItem(gesture);
+ c.setForeground(getForeground());
+ //c.setBorder(LABEL_BORDER);
+ //c.setOpaque(true);
+ c.setBackground(BACKGROUND_COLOR);
+ c.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent itemEvent) {
+ transition.setType((String)itemEvent.getItem());
+ myNavigationModel.getListeners().notify(NavigationModel.Event.update(Transition.class));
+ }
+ });
+ return c;
+ }
+
+ private void syncTransitionCache(Assoc<Transition, Component> assoc) {
+ // add anything that is in the model but not in our cache
+ for (Transition transition : myNavigationModel.getTransitions()) {
+ if (!assoc.keyToValue.containsKey(transition)) {
+ Component editor = createEditorFor(transition);
+ add(editor);
+ assoc.add(transition, editor);
+ }
+ }
+ // remove anything that is in our cache but not in the model
+ removeLeftovers(assoc, myNavigationModel.getTransitions());
+ }
+
+ @Nullable
+ private static VirtualFile getFile(State state, VirtualFileSystem fileSystem, String path, String dir) {
+ return fileSystem.findFileByPath(path + dir + state.getXmlResourceName() + ".xml");
+ }
+
+ private AndroidRootComponent createRootComponentFor(State state) {
+ VirtualFileSystem fileSystem = myFile.getFileSystem();
+ String path = myFile.getParent().getParent().getPath();
+ String directoryName = myFile.getParent().getName();
+ int index = directoryName.indexOf('-');
+ String qualifier = index == -1 ? "" : directoryName.substring(index + 1);
+ VirtualFile qualifiedFile = getFile(state, fileSystem, path, "/layout" + qualifier + "/");
+ VirtualFile file = qualifiedFile != null ? qualifiedFile : getFile(state, fileSystem, path, "/layout/");
+ PsiFile psiFile = file == null ? null : PsiManager.getInstance(myMyRenderingParams.myProject).findFile(file);
+ AndroidRootComponent result = new AndroidRootComponent(myMyRenderingParams, psiFile);
+ result.setScale(myTransform.myScale);
+ return result;
+ }
+
+ private void syncStateCache(Assoc<State, AndroidRootComponent> assoc) {
+ // add anything that is in the model but not in our cache
+ for (State state : myNavigationModel.getStates()) {
+ if (!assoc.keyToValue.containsKey(state)) {
+ AndroidRootComponent root = createRootComponentFor(state);
+ assoc.add(state, root);
+ add(root);
+ }
+ }
+ // remove anything that is in our cache but not in the model
+ removeLeftovers(assoc, myNavigationModel.getStates());
+
+ setPreferredSize();
+ }
+
+ private static com.android.navigation.Point getMaxLoc(ArrayList<State> states) {
+ int maxX = 0;
+ int maxY = 0;
+ for (State state : states) {
+ com.android.navigation.Point loc = state.getLocation();
+ maxX = Math.max(maxX, loc.x);
+ maxY = Math.max(maxY, loc.y);
+ }
+ return new com.android.navigation.Point(maxX, maxY);
+ }
+
+ private void setPreferredSize() {
+ Dimension size = myMyRenderingParams.getDeviceScreenSize();
+ Dimension gridSize = new Dimension(size.width + GAP.width, size.height + GAP.height);
+ com.android.navigation.Point maxLoc = getMaxLoc(myNavigationModel.getStates());
+ Point max = myTransform.modelToView(new com.android.navigation.Point(maxLoc.x + gridSize.width, maxLoc.y + gridSize.height));
+ setPreferredSize(new Dimension(max.x, max.y));
+ }
+
+ private Selections.Selection createSelection(Point mouseDownLocation, boolean shiftDown) {
+ Component component = getComponentAt(mouseDownLocation);
+ if (component instanceof NavigationEditorPanel) {
+ return Selections.NULL;
+ }
+ Transition transition = getTransitionEditorAssociation().valueToKey.get(component);
+ if (component instanceof AndroidRootComponent) {
+ AndroidRootComponent androidRootComponent = (AndroidRootComponent)component;
+ if (!shiftDown) {
+ return new Selections.AndroidRootComponentSelection(myNavigationModel, androidRootComponent, mouseDownLocation, transition,
+ getStateComponentAssociation().valueToKey.get(androidRootComponent));
+ }
+ else {
+ RenderedView leaf = getRenderedView(androidRootComponent, mouseDownLocation);
+ return new Selections.RelationSelection(myNavigationModel, androidRootComponent, mouseDownLocation, getNamedParent(leaf), this);
+ }
+ }
+ else {
+ return new Selections.ComponentSelection<Component>(myNavigationModel, component, transition);
+ }
+ }
+
+ private class MyMouseListener extends MouseAdapter {
+ @Override
+ public void mousePressed(MouseEvent e) {
+ Point location = e.getPoint();
+ boolean modified = (e.isShiftDown() || e.isControlDown() || e.isMetaDown()) && !e.isPopupTrigger();
+ setSelection(createSelection(location, modified));
+ requestFocus();
+ }
+
+ @Override
+ public void mouseMoved(MouseEvent e) {
+ setMouseLocation(e.getPoint());
+ }
+
+ @Override
+ public void mouseDragged(MouseEvent mouseEvent) {
+ moveSelection(mouseEvent.getPoint());
+ }
+
+ @Override
+ public void mouseReleased(MouseEvent mouseEvent) {
+ finaliseSelectionLocation(mouseEvent.getPoint());
+ }
+ }
+
+ private class MyDnDTarget implements DnDTarget {
+ private int applicableDropCount = 0;
+
+ private void dropOrPrepareToDrop(DnDEvent anEvent, boolean execute) {
+ Object attachedObject = anEvent.getAttachedObject();
+ if (attachedObject instanceof TransferableWrapper) {
+ TransferableWrapper wrapper = (TransferableWrapper)attachedObject;
+ PsiElement[] psiElements = wrapper.getPsiElements();
+ Point dropLocation = diff(anEvent.getPointOn(NavigationEditorPanel.this),
+ midPoint(myMyRenderingParams.getDeviceScreenSizeFor(myTransform)));
+
+ if (psiElements != null) {
+ for (PsiElement element : psiElements) {
+ if (element instanceof PsiQualifiedNamedElement) {
+ PsiQualifiedNamedElement namedElement = (PsiQualifiedNamedElement)element;
+ String qualifiedName = namedElement.getQualifiedName();
+ if (qualifiedName != null) {
+ State state = new State(qualifiedName);
+ state.setLocation(myTransform.viewToModel(snap(dropLocation, MIDDLE_SNAP_GRID)));
+ String name = namedElement.getName();
+ if (name != null) {
+ state.setXmlResourceName(getXmlFileNameFromJavaFileName(name));
+ }
+ if (!getStateComponentAssociation().keyToValue.containsKey(state)) {
+ if (execute) {
+ myNavigationModel.addState(state);
+ }
+ else {
+ applicableDropCount++;
+ }
+ }
+ dropLocation = Utilities.add(dropLocation, MULTIPLE_DROP_STRIDE);
+ }
+ }
+ }
+ }
+ }
+ if (execute) {
+ revalidate();
+ repaint();
+ }
+ }
+
+ @Override
+ public boolean update(DnDEvent anEvent) {
+ applicableDropCount = 0;
+ dropOrPrepareToDrop(anEvent, false);
+ anEvent.setDropPossible(applicableDropCount > 0);
+ return false;
+ }
+
+ @Override
+ public void drop(DnDEvent anEvent) {
+ dropOrPrepareToDrop(anEvent, true);
+ }
+
+
+ @Override
+ public void cleanUpOnLeave() {
+ }
+
+ @Override
+ public void updateDraggedImage(Image image, Point dropPoint, Point imageOffset) {
+ }
+
+ }
+
+ private static String getXmlFileNameFromJavaFileName(String name) {
+ return Utilities.getXmlFileNameFromJavaFileName(name);
+ }
+
+}
diff --git a/android/src/com/android/tools/idea/editors/navigation/NavigationEditorPanel1.java b/android/src/com/android/tools/idea/editors/navigation/NavigationEditorPanel1.java
deleted file mode 100644
index 07b5441..0000000
--- a/android/src/com/android/tools/idea/editors/navigation/NavigationEditorPanel1.java
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tools.idea.editors.navigation;
-
-import com.android.navigation.Transition;
-import com.android.navigation.NavigationModel;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.openapi.vfs.VirtualFileSystem;
-
-import javax.swing.*;
-import java.awt.*;
-import java.awt.event.*;
-
-/*
-This class is currently unused.
-*/
-
-public class NavigationEditorPanel1 extends JPanel {
- private final NavigationModel myNavigationModel;
- private final AndroidRootComponent mySourcePreviewPanel;
- private final AndroidRootComponent myDestPreviewPanel;
- private final Project myProject;
- private final JComboBox myGestureComboBox;
- private int myCursor = 0;
- private VirtualFileSystem myFileSystem;
- private String myPath;
-
- private void update() {
- Transition current = getCurrentNavigation();
- VirtualFile source = myFileSystem.findFileByPath(myPath + "/" + current.getSource());
- VirtualFile destination = myFileSystem.findFileByPath(myPath + "/" + current.getDestination());
- mySourcePreviewPanel.render(myProject, source);
- myGestureComboBox.setSelectedItem(current.getType());
- myDestPreviewPanel.render(myProject, destination);
- }
-
- public void setCursor(int cursor) {
- myCursor = Math.min(Math.max(0, cursor), myNavigationModel.size() - 1);
- update();
- }
-
- private Transition getCurrentNavigation() {
- return myNavigationModel.get(myCursor);
- }
-
- private int findFirstNavWith(String name, boolean source) {
- for(int i = 0 ; i < myNavigationModel.size(); i++) {
- Transition nav = myNavigationModel.get(i);
- String field = (source ? nav.getSource() : nav.getDestination()).getControllerClassName();
- if (field.equals(name)) {
- return i;
- }
- }
- return -1;
- }
-
- public NavigationEditorPanel1(Project project, VirtualFile file, NavigationModel navigationModel) {
- myProject = project;
- myFileSystem = file.getFileSystem();
- myPath = file.getParent().getPath();
- myCursor = 0;
- myNavigationModel = navigationModel;
-
- mySourcePreviewPanel = new AndroidRootComponent();
- mySourcePreviewPanel.addMouseListener(new MouseAdapter() {
- @Override
- public void mouseClicked(MouseEvent mouseEvent) {
- right();
- }
- });
- myGestureComboBox = new JComboBox(new Object[]{"", "touch", "swipe"});
- myDestPreviewPanel = new AndroidRootComponent();
- myDestPreviewPanel.addMouseListener(new MouseAdapter() {
- @Override
- public void mouseClicked(MouseEvent mouseEvent) {
- left();
- }
- });
- Color background = Color.LIGHT_GRAY;
-
- setLayout(new BorderLayout());
- setBackground(background);
- {
- JPanel panel = new JPanel(new FlowLayout());
- panel.setBackground(background);
- panel.add(mySourcePreviewPanel);
- panel.add(myGestureComboBox);
- panel.add(myDestPreviewPanel);
-
- add(panel, BorderLayout.CENTER);
- }
- /*
- mySourcePreviewPanel.setFocusable(true);
- mySourcePreviewPanel.addKeyListener(new KeyAdapter() {
- @Override
- public void keyTyped(KeyEvent keyEvent) {
- super.keyTyped(keyEvent);
- System.out.println("keyEvent = " + keyEvent);
- }
- });
- */
- {
- JButton b = new JButton("Previous");
- add(b, BorderLayout.NORTH);
- b.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent actionEvent) {
- up();
- }
- });
- }
- {
- JButton b = new JButton("Next");
- add(b, BorderLayout.SOUTH);
- b.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent actionEvent) {
- down();
- }
- });
- }
- update();
- }
-
- private void up() {
- setCursor(myCursor + 1);
- }
-
- private void down() {
- setCursor(myCursor - 1);
- }
-
- private void left() {
- int next = findFirstNavWith(getCurrentNavigation().getSource().getControllerClassName(), false);
- if (next != -1) {
- setCursor(next);
- } else {
- Toolkit.getDefaultToolkit().beep();
- }
- }
-
- private void right() {
- int next = findFirstNavWith(getCurrentNavigation().getDestination().getControllerClassName(), true);
- if (next != -1) {
- setCursor(next);
- } else {
- Toolkit.getDefaultToolkit().beep();
- }
- }
-}
diff --git a/android/src/com/android/tools/idea/editors/navigation/NavigationEditorPanel2.java b/android/src/com/android/tools/idea/editors/navigation/NavigationEditorPanel2.java
deleted file mode 100644
index a620882..0000000
--- a/android/src/com/android/tools/idea/editors/navigation/NavigationEditorPanel2.java
+++ /dev/null
@@ -1,575 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tools.idea.editors.navigation;
-
-import com.android.ide.common.rendering.api.ViewInfo;
-import com.android.navigation.NavigationModel;
-import com.android.navigation.State;
-import com.android.navigation.Transition;
-import com.android.tools.idea.rendering.RenderResult;
-import com.android.tools.idea.rendering.RenderedView;
-import com.android.tools.idea.rendering.RenderedViewHierarchy;
-import com.android.tools.idea.rendering.ShadowPainter;
-import com.intellij.ide.dnd.DnDEvent;
-import com.intellij.ide.dnd.DnDManager;
-import com.intellij.ide.dnd.DnDTarget;
-import com.intellij.ide.dnd.TransferableWrapper;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.openapi.vfs.VirtualFileSystem;
-import com.intellij.psi.PsiElement;
-import com.intellij.psi.PsiQualifiedNamedElement;
-import com.intellij.psi.xml.XmlTag;
-import com.intellij.util.containers.HashSet;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import javax.swing.*;
-import javax.swing.border.EmptyBorder;
-import java.awt.*;
-import java.awt.event.ItemEvent;
-import java.awt.event.ItemListener;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-import java.awt.geom.AffineTransform;
-import java.util.*;
-import java.util.List;
-
-public class NavigationEditorPanel2 extends JComponent {
- private static final Dimension GAP = new Dimension(150, 50);
- private static final double SCALE = 0.333333;
- private static final EmptyBorder LABEL_BORDER = new EmptyBorder(0, 5, 0, 5);
- private static final Dimension ORIGINAL_SIZE = new Dimension(480, 800);
- private static final Dimension PREVIEW_SIZE = new Dimension((int)(ORIGINAL_SIZE.width * SCALE), (int)(ORIGINAL_SIZE.height * SCALE));
- private static final Dimension ARROW_HEAD_SIZE = new Dimension(10, 5);
- public static final Color LINE_COLOR = Color.GRAY;
- public static final Color BACKGROUND_COLOR = Color.LIGHT_GRAY;
- public static final Point MULTIPLE_DROP_STRIDE = new Point(50, 50);
- private static final String ID_PREFIX = "@+id/";
-
- private final NavigationModel myNavigationModel;
- private final Project myProject;
- private VirtualFileSystem myFileSystem;
- private String myPath;
- private final Map<State, AndroidRootComponent> myStateToComponent = new HashMap<State, AndroidRootComponent>();
- private final Map<AndroidRootComponent, State> myComponentToState = new HashMap<AndroidRootComponent, State>();
- private Selection mySelection = Selection.NULL;
- private Map<Transition, Component> myNavigationToComponent = new IdentityHashMap<Transition, Component>();
-
- private abstract static class Selection {
-
- private static Selection NULL = new EmptySelection();
-
- protected abstract void moveTo(Point location);
-
- protected abstract Selection finaliseSelectionLocation(Point location);
-
- protected abstract void paint(Graphics g);
-
- protected abstract void paintOver(Graphics g);
-
- private static Selection create(NavigationEditorPanel2 editor, Point mouseDownLocation, boolean relation) {
- Component component = editor.getComponentAt(mouseDownLocation);
- return component != editor ? !(relation && component instanceof AndroidRootComponent)
- ? new ComponentSelection(component, mouseDownLocation)
- : new RelationSelection(editor, (AndroidRootComponent)component, mouseDownLocation) : NULL;
- }
- }
-
- private static class EmptySelection extends Selection {
- @Override
- protected void moveTo(Point location) {
- }
-
- @Override
- protected void paint(Graphics g) {
- }
-
- @Override
- protected void paintOver(Graphics g) {
- }
-
- @Override
- protected Selection finaliseSelectionLocation(Point location) {
- return this;
- }
- }
-
- private static class ComponentSelection extends Selection {
- private final Point myMouseDownLocation;
- private final Point myOrigComponentLocation;
- private final Component myComponent;
-
- private ComponentSelection(Component component, Point mouseDownLocation) {
- myComponent = component;
- myMouseDownLocation = mouseDownLocation;
- myOrigComponentLocation = myComponent.getLocation();
- }
-
- @Override
- protected void moveTo(Point location) {
- myComponent.setLocation(Utilities.add(Utilities.diff(location, myMouseDownLocation), myOrigComponentLocation));
- }
-
- @Override
- protected void paint(Graphics g) {
- g.setColor(Color.BLUE);
- Rectangle selection = myComponent.getBounds();
- int l = 4;
- selection.grow(l, l);
- g.fillRoundRect(selection.x, selection.y, selection.width, selection.height, l, l);
- }
-
- @Override
- protected void paintOver(Graphics g) {
- }
-
- @Override
- protected Selection finaliseSelectionLocation(Point location) {
- return this;
- }
- }
-
- private static class RelationSelection extends Selection {
- @NotNull private final NavigationEditorPanel2 myOverViewPanel;
- @NotNull private final Component myComponent;
- @NotNull private Point myLocation;
- @Nullable private final RenderedView myLeaf;
- @Nullable private final RenderedView myNamedLeaf;
- private final float myKx;
- private final float myKy;
-
- private RelationSelection(@NotNull NavigationEditorPanel2 myNavigationEditorPanel2,
- @NotNull AndroidRootComponent component,
- @NotNull Point mouseDownLocation) {
- myOverViewPanel = myNavigationEditorPanel2;
- myComponent = component;
- myLocation = mouseDownLocation;
- RenderResult renderResult = component.getRenderResult();
- RenderedViewHierarchy hierarchy = renderResult.getHierarchy();
- ViewInfo root = renderResult.getRootViews().get(0);
- int b = root.getBottom() + 100; // todo this accounts for the button bar at the bottom of the rendered view; remove
- int r = root.getRight();
-
- int cW = component.getWidth();
- int cH = component.getHeight();
-
- myKx = (float)r / cW;
- myKy = (float)b / cH;
-
- int dx = mouseDownLocation.x - component.getX();
- int dy = mouseDownLocation.y - component.getY();
- myLeaf = hierarchy.findLeafAt((int)(dx * myKx), (int)(dy * myKy));
- myNamedLeaf = getNamedParent(myLeaf);
- }
-
- @Nullable
- private static RenderedView getNamedParent(@Nullable RenderedView view) {
- while (view != null && getViewId(view) == null) {
- view = view.getParent();
- }
- return view;
- }
-
- @Nullable
- private static String getViewId(@Nullable RenderedView leaf) {
- if (leaf != null) {
- XmlTag tag = leaf.tag;
- if (tag != null) {
- String attributeValue = tag.getAttributeValue("android:id");
- if (attributeValue != null && attributeValue.startsWith(ID_PREFIX)) {
- return attributeValue.substring(ID_PREFIX.length());
- }
- }
- }
- return null;
- }
-
- @Override
- protected void moveTo(Point location) {
- myLocation = location;
- }
-
- @Override
- protected void paint(Graphics g) {
- g.setColor(LINE_COLOR);
- Point start = Utilities.centre(myComponent);
- drawArrow(g, start.x, start.y, myLocation.x, myLocation.y);
- }
-
- private void paintLeaf(Graphics g, @Nullable RenderedView leaf, Color color) {
- if (leaf != null) {
- g.setColor(color);
- g.drawRect(myComponent.getX() + ((int)(leaf.x / myKx)), myComponent.getY() + ((int)(leaf.y / myKy)), ((int)(leaf.w / myKx)),
- ((int)(leaf.h / myKy)));
- }
- }
-
- @Override
- protected void paintOver(Graphics g) {
- paintLeaf(g, myLeaf, Color.RED);
- paintLeaf(g, myNamedLeaf, Color.BLUE);
- }
-
- @Override
- protected Selection finaliseSelectionLocation(Point location) {
- Component componentAt = myOverViewPanel.getComponentAt(location);
- if (myComponent instanceof AndroidRootComponent && componentAt instanceof AndroidRootComponent) {
- if (myComponent != componentAt) {
- Map<AndroidRootComponent, State> m = myOverViewPanel.myComponentToState;
- myOverViewPanel.addRelation(m.get(myComponent), getViewId(myNamedLeaf), m.get(componentAt));
- }
- }
- return Selection.NULL;
- }
-
- }
-
- public NavigationEditorPanel2(Project project, VirtualFile file, NavigationModel navigationModel) {
- myProject = project;
- myFileSystem = file.getFileSystem();
- myPath = file.getParent().getParent().getPath();
- myNavigationModel = navigationModel;
-
- setBackground(BACKGROUND_COLOR);
- setForeground(LINE_COLOR);
-
- if (navigationModel.size() > 0) {
- addChildren(getStates(navigationModel));
- }
- addAllRelations();
-
- {
- MouseAdapter mouseListener = new MyMouseListener();
- addMouseListener(mouseListener);
- addMouseMotionListener(mouseListener);
- }
-
- {
- final DnDManager dndManager = DnDManager.getInstance();
- dndManager.registerTarget(new MyDnDTarget(), this);
- }
- }
-
- private static void addIfAbsent(Collection<State> result, Set<State> added, State source) {
- if (!added.contains(source)) {
- result.add(source);
- }
- }
-
- private static Collection<State> getStates(NavigationModel model) {
- Collection<State> result = new ArrayList<State>(); // use set and list to preserve order (for no particular reason)
- Set<State> added = new HashSet<State>();
- for (Transition t : model) {
- addIfAbsent(result, added, t.getSource());
- addIfAbsent(result, added, t.getDestination());
- }
- return result;
- }
-
- private void setSelection(Selection selection) {
- mySelection = selection;
- repaint();
- }
-
- private void moveSelection(Point location) {
- mySelection.moveTo(location);
- revalidate();
- repaint();
- }
-
- private void finaliseSelectionLocation(Point location) {
- mySelection = mySelection.finaliseSelectionLocation(location);
- revalidate();
- repaint();
- }
-
- private List<State> findDestinationsFor(State state, Set<State> exclude) {
- List<State> result = new ArrayList<State>();
- for (Transition transition : myNavigationModel) {
- State source = transition.getSource();
- if (source.equals(state)) {
- State destination = transition.getDestination();
- if (!exclude.contains(destination)) {
- result.add(destination);
- }
- }
- }
- return result;
- }
-
- private static void drawArrow(Graphics g1, int x1, int y1, int x2, int y2) {
- // x1 and y1 are coordinates of circle or rectangle
- // x2 and y2 are coordinates of circle or rectangle, to this point is directed the arrow
- Graphics2D g = (Graphics2D)g1.create();
- double dx = x2 - x1;
- double dy = y2 - y1;
- double angle = Math.atan2(dy, dx);
- int len = (int)Math.sqrt(dx * dx + dy * dy);
- AffineTransform t = AffineTransform.getTranslateInstance(x1, y1);
- t.concatenate(AffineTransform.getRotateInstance(angle));
- g.transform(t);
- g.drawLine(0, 0, len, 0);
- int basePosition = len - ARROW_HEAD_SIZE.width;
- int height = ARROW_HEAD_SIZE.height;
- g.fillPolygon(new int[]{len, basePosition, basePosition, len}, new int[]{0, -height, height, 0}, 4);
- }
-
- @Override
- protected void paintComponent(Graphics g) {
- super.paintComponent(g);
-
- g.setColor(BACKGROUND_COLOR);
- g.fillRect(0, 0, getWidth(), getHeight());
-
- Graphics2D g2d = (Graphics2D)g;
- Object oldRenderingHint = g2d.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
- g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
- g.setColor(getForeground());
- for (Transition transition : myNavigationModel) {
- AndroidRootComponent sourceComponent = myStateToComponent.get(transition.getSource());
- AndroidRootComponent destinationComponent = myStateToComponent.get(transition.getDestination());
- if (sourceComponent != null && destinationComponent != null) {
- Rectangle scb = sourceComponent.getBounds();
- Rectangle dcb = destinationComponent.getBounds();
- Point sc = Utilities.centre(scb);
- Point dc = Utilities.centre(dcb);
- Point scp = Utilities.project(scb, dc);
- Point dcp = Utilities.project(dcb, sc);
- drawArrow(g, scp.x, scp.y, dcp.x, dcp.y);
- }
- }
- g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldRenderingHint);
- for (Component c : myStateToComponent.values()) {
- Rectangle r = c.getBounds();
- ShadowPainter.drawRectangleShadow(g, r.x, r.y, r.width, r.height);
- }
- mySelection.paint(g);
- }
-
- @Override
- protected void paintChildren(Graphics graphics) {
- super.paintChildren(graphics);
- mySelection.paintOver(graphics);
- }
-
- private void addRelation(State source, @Nullable String viewIdentifier, State dest) {
- Transition transition = new Transition("", source, dest);
- transition.setViewIdentifier(viewIdentifier);
- myNavigationModel.add(transition);
- addRelationView(transition);
- }
-
- private void addRelationView(final Transition transition) {
- String gesture = transition.getType();
- JComboBox c = new JComboBox(new Object[]{"", "click", "list", "menu", "contains"});
- c.addItemListener(new ItemListener() {
- @Override
- public void itemStateChanged(ItemEvent itemEvent) {
- transition.setType((String)itemEvent.getItem());
- myNavigationModel.getListeners().notify(null);
- }
- });
- c.setSelectedItem(gesture);
- c.setForeground(getForeground());
- //c.setBorder(LABEL_BORDER);
- //c.setOpaque(true);
- c.setBackground(BACKGROUND_COLOR);
- add(c);
- myNavigationToComponent.put(transition, c);
- }
-
- private void addAllRelations() {
- for (Transition transition : myNavigationModel) {
- addRelationView(transition);
- }
- }
-
- @Override
- public void doLayout() {
- for (Transition transition : myNavigationModel) {
- AndroidRootComponent sourceComponent = myStateToComponent.get(transition.getSource());
- AndroidRootComponent destinationComponent = myStateToComponent.get(transition.getDestination());
- if (sourceComponent != null && destinationComponent != null) {
- Point sl = Utilities.centre(sourceComponent);
- Point dl = Utilities.centre(destinationComponent);
- String gesture = transition.getType();
- if (gesture != null) {
- Component c = myNavigationToComponent.get(transition);
- c.setSize(c.getPreferredSize());
- int sx = (sl.x + dl.x - c.getWidth()) / 2;
- int sy = (sl.y + dl.y - c.getHeight()) / 2;
- c.setLocation(sx, sy);
- }
- }
- }
- }
-
- private void addChildren(Collection<State> states) {
- final Set<State> visited = new HashSet<State>();
- final Point location = new Point(GAP.width, GAP.height);
- final Point maxLocation = new Point(0, 0);
- final int gridWidth = PREVIEW_SIZE.width + GAP.width;
- final int gridHeight = PREVIEW_SIZE.height + GAP.height;
- myStateToComponent.clear();
- myComponentToState.clear();
- for (State state : states) {
- if (visited.contains(state)) {
- continue;
- }
- new Object() {
- public void addChildrenFor(State source) {
- visited.add(source);
- add(createActivityPanel(source, location));
- List<State> children = findDestinationsFor(source, visited);
- location.x += gridWidth;
- maxLocation.x = Math.max(maxLocation.x, location.x);
- if (children.isEmpty()) {
- location.y += gridHeight;
- maxLocation.y = Math.max(maxLocation.y, location.y);
- }
- else {
- for (State child : children) {
- addChildrenFor(child);
- }
- }
- location.x -= gridWidth;
- }
- }.addChildrenFor(state);
- }
- setPreferredSize(new Dimension(maxLocation.x, maxLocation.y));
- }
-
- private AndroidRootComponent createActivityPanel(State state, Point location) {
- AndroidRootComponent result = new AndroidRootComponent();
- result.setScale(SCALE);
- VirtualFile file = myFileSystem.findFileByPath(myPath + "/layout/" + state.getXmlResourceName() + ".xml");
- if (file != null) {
- result.render(myProject, file);
- }
- result.setLocation(location);
- result.setSize(PREVIEW_SIZE);
- myStateToComponent.put(state, result);
- myComponentToState.put(result, state);
- return result;
- }
-
- private class MyMouseListener extends MouseAdapter {
- @Override
- public void mousePressed(MouseEvent mouseEvent) {
- Point location = mouseEvent.getPoint();
- setSelection(Selection.create(NavigationEditorPanel2.this, location, mouseEvent.isShiftDown()));
- }
-
- /*
- @Override
- public void mouseMoved(MouseEvent mouseEvent) {
- moveSelection(mouseEvent.getPoint());
- }
- */
-
- @Override
- public void mouseDragged(MouseEvent mouseEvent) {
- moveSelection(mouseEvent.getPoint());
- }
-
- @Override
- public void mouseReleased(MouseEvent mouseEvent) {
- finaliseSelectionLocation(mouseEvent.getPoint());
- }
- }
-
- private class MyDnDTarget implements DnDTarget {
-
- @Override
- public boolean update(DnDEvent aEvent) {
- /*
- setHoverIndex(-1);
- if (aEvent.getAttachedObject() instanceof PaletteItem) {
- setDropTargetIndex(locationToTargetIndex(aEvent.getPoint()));
- aEvent.setDropPossible(true);
- }
- else {
- setDropTargetIndex(-1);
- aEvent.setDropPossible(false);
- }
- */
- aEvent.setDropPossible(true);
- //System.out.println("aEvent = " + aEvent);
- return false;
- }
-
- @Override
- public void drop(DnDEvent aEvent) {
- /*
- setDropTargetIndex(-1);
- if (aEvent.getAttachedObject() instanceof PaletteItem) {
- int index = locationToTargetIndex(aEvent.getPoint());
- if (index >= 0) {
- myGroup.handleDrop(myProject, (PaletteItem) aEvent.getAttachedObject(), index);
- }
- }
- */
- Object attachedObject = aEvent.getAttachedObject();
- if (attachedObject instanceof TransferableWrapper) {
- TransferableWrapper wrapper = (TransferableWrapper)attachedObject;
- PsiElement[] psiElements = wrapper.getPsiElements();
- Point dropLocation = aEvent.getPointOn(NavigationEditorPanel2.this);
-
- if (psiElements != null) {
- for (PsiElement element : psiElements) {
- if (element instanceof PsiQualifiedNamedElement) {
- PsiQualifiedNamedElement namedElement = (PsiQualifiedNamedElement)element;
- String qualifiedName = namedElement.getQualifiedName();
- if (qualifiedName != null) {
- State state = new State(qualifiedName);
- String name = namedElement.getName();
- if (name != null) {
- state.setXmlResourceName(getXmlFileNameFromJavaFileName(name));
- }
- if (!myStateToComponent.containsKey(state)) {
- add(createActivityPanel(state, dropLocation));
- dropLocation = Utilities.add(dropLocation, MULTIPLE_DROP_STRIDE);
- }
- }
- }
- }
- }
- revalidate();
- repaint();
- }
- }
-
- @Override
- public void cleanUpOnLeave() {
- //setDropTargetIndex(-1);
- }
-
- @Override
- public void updateDraggedImage(Image image, Point dropPoint, Point imageOffset) {
- //System.out.println("image = " + image);
- }
- }
-
- private static String getXmlFileNameFromJavaFileName(String name) {
- //if (name.contains("ListFragment")) {
- // return "";
- //}
-
- return Utilities.getXmlFileNameFromJavaFileName(name);
- }
-
-}
diff --git a/android/src/com/android/tools/idea/editors/navigation/RenderingParameters.java b/android/src/com/android/tools/idea/editors/navigation/RenderingParameters.java
new file mode 100644
index 0000000..49324ac
--- /dev/null
+++ b/android/src/com/android/tools/idea/editors/navigation/RenderingParameters.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.editors.navigation;
+
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.State;
+import com.android.tools.idea.configurations.Configuration;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.annotations.NotNull;
+
+import java.awt.*;
+
+import static com.android.tools.idea.editors.navigation.Utilities.ZERO_SIZE;
+import static com.android.tools.idea.editors.navigation.Utilities.notNull;
+
+class RenderingParameters {
+ @NotNull final Project myProject;
+ @NotNull final Configuration myConfiguration;
+ @NotNull final AndroidFacet myFacet;
+
+ public RenderingParameters(@NotNull Project project, @NotNull Configuration configuration, @NotNull AndroidFacet facet) {
+ this.myProject = project;
+ this.myConfiguration = configuration;
+ this.myFacet = facet;
+ }
+
+ Dimension getDeviceScreenSize() {
+ Configuration configuration = myConfiguration;
+ Device device = configuration.getDevice();
+ if (device == null) {
+ return ZERO_SIZE;
+ }
+ State deviceState = configuration.getDeviceState();
+ if (deviceState == null) {
+ deviceState = device.getDefaultState();
+ }
+ return notNull(device.getScreenSize(deviceState.getOrientation()));
+ }
+
+ Dimension getDeviceScreenSizeFor(Transform transform) {
+ return transform.modelToView(getDeviceScreenSize());
+ }
+}
diff --git a/android/src/com/android/tools/idea/editors/navigation/Selections.java b/android/src/com/android/tools/idea/editors/navigation/Selections.java
new file mode 100644
index 0000000..77c5532
--- /dev/null
+++ b/android/src/com/android/tools/idea/editors/navigation/Selections.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.editors.navigation;
+
+import com.android.navigation.NavigationModel;
+import com.android.navigation.State;
+import com.android.navigation.Transition;
+import com.android.tools.idea.rendering.RenderedView;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.awt.*;
+
+import static com.android.tools.idea.editors.navigation.NavigationEditorPanel.Line;
+import static com.android.tools.idea.editors.navigation.Utilities.diff;
+
+class Selections {
+ private static final Color SELECTION_COLOR = Color.BLUE;
+ private static final int SELECTION_RECTANGLE_LINE_WIDTH = 4;
+
+ public static Selection NULL = new EmptySelection();
+
+ abstract static class Selection {
+
+ protected abstract void moveTo(Point location);
+
+ protected abstract Selection finaliseSelectionLocation(Point location);
+
+ protected abstract void paint(Graphics g, boolean hasFocus);
+
+ protected abstract void paintOver(Graphics g);
+
+ protected abstract void remove();
+ }
+
+ private static class EmptySelection extends Selection {
+ @Override
+ protected void moveTo(Point location) {
+ }
+
+ @Override
+ protected void paint(Graphics g, boolean hasFocus) {
+ }
+
+ @Override
+ protected void paintOver(Graphics g) {
+ }
+
+ @Override
+ protected Selection finaliseSelectionLocation(Point location) {
+ return this;
+ }
+
+ @Override
+ protected void remove() {
+ }
+ }
+
+ static class ComponentSelection<T extends Component> extends Selection {
+ protected final T myComponent;
+ protected final Transition myTransition;
+ protected final NavigationModel myNavigationModel;
+
+ ComponentSelection(NavigationModel navigationModel, T component, Transition transition) {
+ myNavigationModel = navigationModel;
+ myComponent = component;
+ myTransition = transition;
+ }
+
+ @Override
+ protected void moveTo(Point location) {
+ }
+
+ @Override
+ protected void paint(Graphics g, boolean hasFocus) {
+ if (hasFocus) {
+ Graphics2D g2D = (Graphics2D)g.create();
+ g2D.setStroke(new BasicStroke(SELECTION_RECTANGLE_LINE_WIDTH));
+ g2D.setColor(SELECTION_COLOR);
+ Rectangle selection = myComponent.getBounds();
+ int l = SELECTION_RECTANGLE_LINE_WIDTH / 2;
+ selection.grow(l, l);
+ g2D.drawRect(selection.x, selection.y, selection.width, selection.height);
+ }
+ }
+
+ @Override
+ protected void paintOver(Graphics g) {
+ }
+
+ @Override
+ protected Selection finaliseSelectionLocation(Point location) {
+ return this;
+ }
+
+ @Override
+ protected void remove() {
+ myNavigationModel.remove(myTransition);
+ }
+ }
+
+ static class AndroidRootComponentSelection extends ComponentSelection<AndroidRootComponent> {
+ protected final Point myMouseDownLocation;
+ protected final Point myOrigComponentLocation;
+ private final State myState;
+
+ AndroidRootComponentSelection(NavigationModel navigationModel,
+ AndroidRootComponent component,
+ Point mouseDownLocation,
+ Transition transition,
+ State state) {
+ super(navigationModel, component, transition);
+ myMouseDownLocation = mouseDownLocation;
+ myOrigComponentLocation = myComponent.getLocation();
+ myState = state;
+ }
+
+ private void moveTo(Point location, boolean snap) {
+ Point newLocation = Utilities.add(diff(location, myMouseDownLocation), myOrigComponentLocation);
+ if (snap) {
+ newLocation = Utilities.snap(newLocation, myComponent.transform.modelToView(NavigationEditorPanel.MIDDLE_SNAP_GRID));
+ }
+ myComponent.setLocation(newLocation);
+ myState.setLocation(myComponent.transform.viewToModel(newLocation));
+ myNavigationModel.getListeners().notify(NavigationModel.Event.update(State.class));
+ }
+
+ @Override
+ protected void moveTo(Point location) {
+ moveTo(location, false);
+ }
+
+ @Override
+ protected void remove() {
+ myNavigationModel.removeState(myState);
+ }
+
+ @Override
+ protected Selection finaliseSelectionLocation(Point location) {
+ moveTo(location, true);
+ return this;
+ }
+ }
+
+ static class RelationSelection extends Selection {
+ private final AndroidRootComponent mySourceComponent;
+ private final NavigationEditorPanel myNavigationEditor;
+ private final NavigationModel myNavigationModel;
+ private final RenderedView myNamedLeaf;
+ @NotNull private Point myMouseLocation;
+
+ RelationSelection(NavigationModel navigationModel,
+ @NotNull AndroidRootComponent sourceComponent,
+ @NotNull Point mouseDownLocation,
+ @Nullable RenderedView namedLeaf,
+ @NotNull NavigationEditorPanel navigationEditor) {
+ myNavigationModel = navigationModel;
+ mySourceComponent = sourceComponent;
+ myMouseLocation = mouseDownLocation;
+ myNamedLeaf = namedLeaf;
+ myNavigationEditor = navigationEditor;
+ }
+
+ @Override
+ protected void moveTo(Point location) {
+ myMouseLocation = location;
+ }
+
+ @Override
+ protected void paint(Graphics g, boolean hasFocus) {
+ }
+
+ @Override
+ protected void paintOver(Graphics g) {
+ int lineWidth = mySourceComponent.transform.modelToView(NavigationEditorPanel.LINE_WIDTH);
+ Graphics2D lineGraphics = NavigationEditorPanel.createLineGraphics(g, lineWidth);
+ Rectangle sourceBounds = NavigationEditorPanel.getBounds(mySourceComponent, myNamedLeaf);
+ Rectangle destBounds = myNavigationEditor.getNamedLeafBoundsAt(mySourceComponent, myMouseLocation);
+ Line midLine = NavigationEditorPanel.getMidLine(mySourceComponent.getBounds(), new Rectangle(myMouseLocation));
+ Point[] controlPoints = NavigationEditorPanel.getControlPoints(sourceBounds, destBounds, midLine);
+ myNavigationEditor.drawTransition(lineGraphics, sourceBounds, destBounds, controlPoints);
+ }
+
+ @Override
+ protected Selection finaliseSelectionLocation(Point mouseUpLocation) {
+ Transition transition = myNavigationEditor.getTransition(mySourceComponent, myNamedLeaf, mouseUpLocation);
+ if (transition != null) {
+ myNavigationModel.add(transition);
+ }
+ return NULL;
+ }
+
+ @Override
+ protected void remove() {
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/editors/navigation/Transform.java b/android/src/com/android/tools/idea/editors/navigation/Transform.java
new file mode 100644
index 0000000..695ac10
--- /dev/null
+++ b/android/src/com/android/tools/idea/editors/navigation/Transform.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.editors.navigation;
+
+import com.android.tools.idea.rendering.RenderedView;
+
+import java.awt.*;
+
+public class Transform {
+ public final float myScale;
+
+ public Transform(float scale) {
+ myScale = scale;
+ }
+
+ public Dimension modelToView(Dimension size) {
+ return Utilities.scale(size, myScale);
+ }
+
+ public int modelToView(int d) {
+ return ((int)(d * myScale));
+ }
+
+ public int viewToModel(int d) {
+ return (int)(d / myScale);
+ }
+
+ public Point modelToView(com.android.navigation.Point loc) {
+ return new Point(modelToView(loc.x), modelToView(loc.y));
+ }
+
+ public com.android.navigation.Point viewToModel(Point loc) {
+ return new com.android.navigation.Point(viewToModel(loc.x), viewToModel(loc.y));
+ }
+
+ public Rectangle getBounds(RenderedView v) {
+ return new Rectangle(modelToView(v.x), modelToView(v.y), modelToView(v.w), modelToView(v.h));
+ }
+}
diff --git a/android/src/com/android/tools/idea/editors/navigation/Utilities.java b/android/src/com/android/tools/idea/editors/navigation/Utilities.java
index 4c2ebb2..28a4937 100644
--- a/android/src/com/android/tools/idea/editors/navigation/Utilities.java
+++ b/android/src/com/android/tools/idea/editors/navigation/Utilities.java
@@ -15,11 +15,16 @@
*/
package com.android.tools.idea.editors.navigation;
+import com.intellij.openapi.util.Condition;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.awt.*;
+import java.awt.geom.AffineTransform;
public class Utilities {
+ public static final Dimension ZERO_SIZE = new Dimension(0, 0);
+
public static Point add(Point p1, Point p2) {
return new Point(p1.x + p2.x, p1.y + p2.y);
}
@@ -28,11 +33,47 @@
return new Point(p1.x - p2.x, p1.y - p2.y);
}
+ public static double length(Point p) {
+ return Math.sqrt(p.x * p.x + p.y * p.y);
+ }
+
+ public static Point max(Point p1, Point p2) {
+ return new Point(Math.max(p1.x, p2.x), Math.max(p1.y, p2.y));
+ }
+
public static Point scale(Point p, float k) {
return new Point((int)(k * p.x), (int)(k * p.y));
}
- public static Point project(Rectangle r, Point p) {
+ public static Dimension scale(Dimension d, float k) {
+ return new Dimension((int)(k * d.width), (int)(k * d.height));
+ }
+
+ private static int snap(int i, int d) {
+ return ((int)Math.round((double)i / d)) * d;
+ }
+
+ public static Point snap(Point p, Dimension gridSize) {
+ return new Point(snap(p.x, gridSize.width), snap(p.y, gridSize.height));
+ }
+
+ public static Point midPoint(Point p1, Point p2) {
+ return scale(add(p1, p2), 0.5f);
+ }
+
+ public static Point midPoint(Dimension size) {
+ return point(scale(size, 0.5f));
+ }
+
+ public static Point point(Dimension d) {
+ return new Point(d.width, d.height);
+ }
+
+ public static Dimension dimension(Point p) {
+ return new Dimension(Math.abs(p.x), Math.abs(p.y));
+ }
+
+ public static Point project(Point p, Rectangle r) {
Point centre = centre(r);
Point diff = diff(p, centre);
boolean horizontal = Math.abs((float)diff.y / diff.x) < Math.abs((float)r.height / r.width);
@@ -44,14 +85,10 @@
return new Point(r.x + r.width / 2, r.y + r.height / 2);
}
- public static Point centre(@NotNull Component c) {
- return centre(c.getBounds());
- }
-
/**
* Translates a Java file name to a XML file name according
* to Android naming convention.
- *
+ * <p/>
* Doesn't append .xml extension
*
* @return XML file name associated with Java file name
@@ -78,11 +115,12 @@
/**
* Translates a XML file name to a Java file name according
* to Android naming convention.
- *
+ * <p/>
* Doesn't append .java extension
*
* @return Java file name associated with XML file name
*/
+ @SuppressWarnings("AssignmentToForLoopParameter")
public static String getJavaFileNameFromXmlFileName(String xmlFileName) {
if (xmlFileName.endsWith(".xml")) {
@@ -101,10 +139,55 @@
// skip '_' and add the next char as upper case
char toAppend = Character.toUpperCase(charsXml[++i]);
stringBuilder.append(toAppend);
- } else {
+ }
+ else {
stringBuilder.append(currentChar);
}
}
return stringBuilder.toString();
}
+
+ static void drawArrow(Graphics g1, int x1, int y1, int x2, int y2, int lineWidth) {
+ // x1 and y1 are coordinates of circle or rectangle
+ // x2 and y2 are coordinates of circle or rectangle, to this point is directed the arrow
+ Graphics2D g = (Graphics2D)g1.create();
+ double dx = x2 - x1;
+ double dy = y2 - y1;
+ double angle = Math.atan2(dy, dx);
+ int len = (int)Math.sqrt(dx * dx + dy * dy);
+ AffineTransform t = AffineTransform.getTranslateInstance(x1, y1);
+ t.concatenate(AffineTransform.getRotateInstance(angle));
+ g.transform(t);
+ g.drawLine(0, 0, len, 0);
+ Dimension arrowHeadSize = new Dimension(lineWidth * 6, lineWidth * 3);
+ int basePosition = len - arrowHeadSize.width;
+ int height = arrowHeadSize.height;
+ g.fillPolygon(new int[]{len, basePosition, basePosition, len}, new int[]{0, -height, height, 0}, 4);
+ }
+
+ static <T> Condition<T> not(final Condition<T> condition) {
+ return new Condition<T>() {
+ @Override
+ public boolean value(T t) {
+ return !condition.value(t);
+ }
+ };
+ }
+
+ static <T> Condition<T> instanceOf(final Class<?> type) {
+ return new Condition<T>() {
+ @Override
+ public boolean value(Object o) {
+ return type.isAssignableFrom(o.getClass());
+ }
+ };
+ }
+
+ static int sign(int x) {
+ return x > 0 ? 1 : x < 0 ? -1 : 0;
+ }
+
+ static Dimension notNull(@Nullable Dimension d) {
+ return d == null ? ZERO_SIZE : d;
+ }
}
diff --git a/android/src/com/android/tools/idea/editors/vmtrace/TraceViewPanel.form b/android/src/com/android/tools/idea/editors/vmtrace/TraceViewPanel.form
new file mode 100644
index 0000000..3cabef2
--- /dev/null
+++ b/android/src/com/android/tools/idea/editors/vmtrace/TraceViewPanel.form
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.android.tools.idea.editors.vmtrace.TraceViewPanel">
+ <grid id="27dc6" binding="myContainer" layout-manager="BorderLayout" hgap="0" vgap="0">
+ <constraints>
+ <xy x="20" y="20" width="500" height="400"/>
+ </constraints>
+ <properties/>
+ <border type="none"/>
+ <children>
+ <component id="12431" class="com.android.tools.perflib.vmtrace.viz.TraceViewCanvas" binding="myTraceViewCanvas">
+ <constraints border-constraint="Center"/>
+ <properties/>
+ </component>
+ <grid id="1acea" layout-manager="GridLayoutManager" row-count="1" column-count="5" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+ <margin top="0" left="0" bottom="0" right="0"/>
+ <constraints border-constraint="North"/>
+ <properties/>
+ <border type="none"/>
+ <children>
+ <component id="2e6cb" class="com.intellij.ui.components.JBLabel">
+ <constraints>
+ <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <labelFor value="63838"/>
+ <text value="Thread: "/>
+ </properties>
+ </component>
+ <hspacer id="c735d">
+ <constraints>
+ <grid row="0" column="4" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ </hspacer>
+ <component id="63838" class="javax.swing.JComboBox" binding="myThreadCombo">
+ <constraints>
+ <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <enabled value="false"/>
+ </properties>
+ </component>
+ <component id="e50c" class="com.intellij.ui.components.JBLabel">
+ <constraints>
+ <grid row="0" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <labelFor value="a8a8"/>
+ <text value="x-axis: "/>
+ </properties>
+ </component>
+ <component id="a8a8" class="javax.swing.JComboBox" binding="myRenderClockSelectorCombo">
+ <constraints>
+ <grid row="0" column="3" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <enabled value="false"/>
+ </properties>
+ </component>
+ </children>
+ </grid>
+ </children>
+ </grid>
+</form>
diff --git a/android/src/com/android/tools/idea/editors/vmtrace/TraceViewPanel.java b/android/src/com/android/tools/idea/editors/vmtrace/TraceViewPanel.java
new file mode 100644
index 0000000..9a651d8
--- /dev/null
+++ b/android/src/com/android/tools/idea/editors/vmtrace/TraceViewPanel.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.idea.editors.vmtrace;
+
+import com.android.tools.perflib.vmtrace.Call;
+import com.android.tools.perflib.vmtrace.ClockType;
+import com.android.tools.perflib.vmtrace.ThreadInfo;
+import com.android.tools.perflib.vmtrace.VmTraceData;
+import com.android.tools.perflib.vmtrace.viz.TraceViewCanvas;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public class TraceViewPanel {
+ /** Default name for main thread in Android apps. */
+ @NonNls private static final String MAIN_THREAD_NAME = "main";
+
+ private TraceViewCanvas myTraceViewCanvas;
+ private JPanel myContainer;
+ private JComboBox myThreadCombo;
+ private JComboBox myRenderClockSelectorCombo;
+
+ private static final String[] ourRenderClockOptions = new String[] {
+ "Wall Clock Time",
+ "Thread Time",
+ };
+
+ private static final ClockType[] ourRenderClockTypes = new ClockType[] {
+ ClockType.GLOBAL,
+ ClockType.THREAD,
+ };
+
+ public TraceViewPanel() {
+ myRenderClockSelectorCombo.setModel(new DefaultComboBoxModel(ourRenderClockOptions));
+ myRenderClockSelectorCombo.setSelectedIndex(0);
+
+ ActionListener l = new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ if (e.getSource() == myThreadCombo) {
+ myTraceViewCanvas.displayThread((String)myThreadCombo.getSelectedItem());
+ } else if (e.getSource() == myRenderClockSelectorCombo) {
+ myTraceViewCanvas.setRenderClock(getCurrentRenderClock());
+ }
+ }
+ };
+
+ myThreadCombo.addActionListener(l);
+ myRenderClockSelectorCombo.addActionListener(l);
+ }
+
+ public void setTrace(@NotNull VmTraceData trace) {
+ List<String> threadNames = getThreadsWithTraces(trace);
+ String defaultThread = getDefaultThreadName(threadNames);
+ myTraceViewCanvas.setTrace(trace, defaultThread, getCurrentRenderClock());
+ myThreadCombo.setModel(new DefaultComboBoxModel(threadNames.toArray()));
+ myThreadCombo.setSelectedIndex(threadNames.indexOf(defaultThread));
+
+ myThreadCombo.setEnabled(true);
+ myRenderClockSelectorCombo.setEnabled(true);
+ }
+
+ @NotNull
+ private String getDefaultThreadName(@NotNull List<String> threadNames) {
+ if (threadNames.isEmpty()) {
+ return "";
+ }
+
+ // default to displaying info from main thread
+ return threadNames.contains(MAIN_THREAD_NAME) ? MAIN_THREAD_NAME : threadNames.get(0);
+ }
+
+ private ClockType getCurrentRenderClock() {
+ return ourRenderClockTypes[myRenderClockSelectorCombo.getSelectedIndex()];
+ }
+
+ private static List<String> getThreadsWithTraces(@NotNull VmTraceData trace) {
+ Collection<ThreadInfo> threads = trace.getThreads();
+ List<String> threadNames = new ArrayList<String>(threads.size());
+
+ for (ThreadInfo thread : threads) {
+ Call topLevelCall = thread.getTopLevelCall();
+ if (topLevelCall != null) {
+ threadNames.add(thread.getName());
+ }
+ }
+ return threadNames;
+ }
+
+ @NotNull
+ public JComponent getComponent() {
+ return myContainer;
+ }
+}
diff --git a/android/src/com/android/tools/idea/editors/vmtrace/VmTraceEditor.java b/android/src/com/android/tools/idea/editors/vmtrace/VmTraceEditor.java
new file mode 100644
index 0000000..a63fbe3
--- /dev/null
+++ b/android/src/com/android/tools/idea/editors/vmtrace/VmTraceEditor.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.idea.editors.vmtrace;
+
+import com.android.tools.perflib.vmtrace.VmTraceData;
+import com.android.tools.perflib.vmtrace.VmTraceParser;
+import com.google.common.base.Throwables;
+import com.intellij.codeHighlighting.BackgroundEditorHighlighter;
+import com.intellij.ide.structureView.StructureViewBuilder;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.ModalityState;
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.fileEditor.FileEditorLocation;
+import com.intellij.openapi.fileEditor.FileEditorState;
+import com.intellij.openapi.fileEditor.FileEditorStateLevel;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.Task;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.android.dom.manifest.Application;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.beans.PropertyChangeListener;
+import java.io.File;
+import java.io.IOException;
+
+public class VmTraceEditor implements FileEditor {
+ private final TraceViewPanel myTraceViewPanel;
+
+ public VmTraceEditor(@NotNull final Project project, @NotNull final VirtualFile file) {
+ myTraceViewPanel = new TraceViewPanel();
+ parseTraceFileInBackground(project, file);
+ }
+
+ private void parseTraceFileInBackground(@NotNull final Project project, @NotNull final VirtualFile file) {
+ final Task.Modal parseTask = new Task.Modal(project, "Parsing trace file", false) {
+ @Override
+ public void run(@NotNull ProgressIndicator indicator) {
+ indicator.setIndeterminate(true);
+
+ File traceFile = VfsUtilCore.virtualToIoFile(file);
+ VmTraceParser parser = new VmTraceParser(traceFile);
+ try {
+ parser.parse();
+ }
+ catch (final IOException e) {
+ ApplicationManager.getApplication().invokeAndWait(new Runnable() {
+ @Override
+ public void run() {
+ //noinspection ThrowableResultOfMethodCallIgnored
+ Messages.showErrorDialog(project, "Unexpected error while parsing trace file: " + Throwables.getRootCause(e).getMessage(),
+ getName());
+ }
+ }, ModalityState.defaultModalityState());
+ return;
+ }
+
+ final VmTraceData vmTraceData = parser.getTraceData();
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ myTraceViewPanel.setTrace(vmTraceData);
+ }
+ });
+ }
+ };
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ parseTask.queue();
+ }
+ });
+ }
+
+ @NotNull
+ @Override
+ public JComponent getComponent() {
+ return myTraceViewPanel.getComponent();
+ }
+
+ @Nullable
+ @Override
+ public JComponent getPreferredFocusedComponent() {
+ return null;
+ }
+
+ @NotNull
+ @Override
+ public String getName() {
+ return "Traceview";
+ }
+
+ @NotNull
+ @Override
+ public FileEditorState getState(@NotNull FileEditorStateLevel level) {
+ return FileEditorState.INSTANCE;
+ }
+
+ @Override
+ public void setState(@NotNull FileEditorState state) {
+ }
+
+ @Override
+ public boolean isModified() {
+ return false;
+ }
+
+ @Override
+ public boolean isValid() {
+ return true;
+ }
+
+ @Override
+ public void selectNotify() {
+ }
+
+ @Override
+ public void deselectNotify() {
+ }
+
+ @Override
+ public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) {
+ }
+
+ @Override
+ public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) {
+ }
+
+ @Nullable
+ @Override
+ public BackgroundEditorHighlighter getBackgroundHighlighter() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public FileEditorLocation getCurrentLocation() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public StructureViewBuilder getStructureViewBuilder() {
+ return null;
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Nullable
+ @Override
+ public <T> T getUserData(@NotNull Key<T> key) {
+ return null;
+ }
+
+ @Override
+ public <T> void putUserData(@NotNull Key<T> key, @Nullable T value) {
+ }
+}
diff --git a/android/src/com/android/tools/idea/editors/vmtrace/VmTraceEditorProvider.java b/android/src/com/android/tools/idea/editors/vmtrace/VmTraceEditorProvider.java
new file mode 100644
index 0000000..f32b6fa
--- /dev/null
+++ b/android/src/com/android/tools/idea/editors/vmtrace/VmTraceEditorProvider.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.idea.editors.vmtrace;
+
+import com.android.utils.SdkUtils;
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.fileEditor.FileEditorPolicy;
+import com.intellij.openapi.fileEditor.FileEditorProvider;
+import com.intellij.openapi.fileEditor.FileEditorState;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jdom.Element;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+
+public class VmTraceEditorProvider implements FileEditorProvider, DumbAware {
+ @NonNls private static final String ID = "vmtrace-editor";
+ @NonNls private static final String DOT_TRACE = ".trace";
+
+ @Override
+ public boolean accept(@NotNull Project project, @NotNull VirtualFile file) {
+ return SdkUtils.endsWithIgnoreCase(file.getPath(), DOT_TRACE);
+ }
+
+ @NotNull
+ @Override
+ public FileEditor createEditor(@NotNull Project project, @NotNull VirtualFile file) {
+ return new VmTraceEditor(project, file);
+ }
+
+ @Override
+ public void disposeEditor(@NotNull FileEditor editor) {
+ Disposer.dispose(editor);
+ }
+
+ @NotNull
+ @Override
+ public FileEditorState readState(@NotNull Element sourceElement, @NotNull Project project, @NotNull VirtualFile file) {
+ return FileEditorState.INSTANCE;
+ }
+
+ @Override
+ public void writeState(@NotNull FileEditorState state, @NotNull Project project, @NotNull Element targetElement) {
+ }
+
+ @NotNull
+ @Override
+ public String getEditorTypeId() {
+ return ID;
+ }
+
+ @NotNull
+ @Override
+ public FileEditorPolicy getPolicy() {
+ return FileEditorPolicy.HIDE_DEFAULT_EDITOR;
+ }
+}
diff --git a/android/src/com/android/tools/idea/folding/ResourceFoldingBuilder.java b/android/src/com/android/tools/idea/folding/ResourceFoldingBuilder.java
index fc76301..136858c 100644
--- a/android/src/com/android/tools/idea/folding/ResourceFoldingBuilder.java
+++ b/android/src/com/android/tools/idea/folding/ResourceFoldingBuilder.java
@@ -102,7 +102,10 @@
public void visitXmlAttributeValue(XmlAttributeValue value) {
ResourceString resourceString = getResolvedString(value);
if (resourceString != NONE) {
- result.add(resourceString.getDescriptor());
+ FoldingDescriptor descriptor = resourceString.getDescriptor();
+ if (descriptor != null) {
+ result.add(descriptor);
+ }
}
super.visitXmlAttributeValue(value);
}
diff --git a/android/src/com/android/tools/idea/folding/ResourceString.java b/android/src/com/android/tools/idea/folding/ResourceString.java
index 7ede8a4..fea140e 100644
--- a/android/src/com/android/tools/idea/folding/ResourceString.java
+++ b/android/src/com/android/tools/idea/folding/ResourceString.java
@@ -23,6 +23,7 @@
import com.intellij.lang.ASTNode;
import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.openapi.util.ModificationTracker;
+import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
@@ -189,7 +190,12 @@
if (myDescriptor != null && myElement != null) {
ASTNode node = myElement.getNode();
Set<Object> dependencies = myDescriptor.getDependencies();
- myDescriptor = new FoldingDescriptor(node, node.getTextRange(), null, dependencies);
+ TextRange textRange = node.getTextRange();
+ if (!textRange.isEmpty()) {
+ myDescriptor = new FoldingDescriptor(node, textRange, null, dependencies);
+ } else {
+ myDescriptor = null;
+ }
}
}
}
diff --git a/android/src/com/android/tools/idea/gradle/AndroidProjectKeys.java b/android/src/com/android/tools/idea/gradle/AndroidProjectKeys.java
index b160234..04aa2d1 100644
--- a/android/src/com/android/tools/idea/gradle/AndroidProjectKeys.java
+++ b/android/src/com/android/tools/idea/gradle/AndroidProjectKeys.java
@@ -17,7 +17,6 @@
import com.intellij.openapi.externalSystem.model.Key;
import com.intellij.openapi.externalSystem.model.ProjectKeys;
-import org.gradle.tooling.model.GradleProject;
import org.gradle.tooling.model.idea.IdeaModule;
import org.jetbrains.annotations.NotNull;
@@ -25,12 +24,16 @@
* Common project entity {@link Key keys}.
*/
public class AndroidProjectKeys {
- @NotNull public static final Key<IdeaAndroidProject> IDE_ANDROID_PROJECT = Key.create(IdeaAndroidProject.class,
- ProjectKeys.PROJECT.getProcessingWeight() + 5);
- @NotNull public static final Key<IdeaModule> IDEA_MODULE = Key.create(IdeaModule.class,
- ProjectKeys.MODULE.getProcessingWeight() + 5);
- @NotNull public static final Key<IdeaGradleProject> GRADLE_PROJECT = Key.create(IdeaGradleProject.class,
- IDE_ANDROID_PROJECT.getProcessingWeight() + 10);
+ @NotNull public static final Key<IdeaAndroidProject> IDE_ANDROID_PROJECT =
+ Key.create(IdeaAndroidProject.class, ProjectKeys.PROJECT.getProcessingWeight() + 5);
+
+ @NotNull public static final Key<IdeaModule> IDEA_MODULE = Key.create(IdeaModule.class, ProjectKeys.MODULE.getProcessingWeight() + 5);
+
+ @NotNull public static final Key<ProjectImportEventMessage> IMPORT_EVENT_MSG =
+ Key.create(ProjectImportEventMessage.class, IDE_ANDROID_PROJECT.getProcessingWeight() + 5);
+
+ @NotNull public static final Key<IdeaGradleProject> IDE_GRADLE_PROJECT =
+ Key.create(IdeaGradleProject.class, IDE_ANDROID_PROJECT.getProcessingWeight() + 10);
private AndroidProjectKeys() {
}
diff --git a/android/src/com/android/tools/idea/gradle/GradleImportNotificationListener.java b/android/src/com/android/tools/idea/gradle/GradleImportNotificationListener.java
index 75a49db..13375dd 100644
--- a/android/src/com/android/tools/idea/gradle/GradleImportNotificationListener.java
+++ b/android/src/com/android/tools/idea/gradle/GradleImportNotificationListener.java
@@ -17,6 +17,7 @@
import com.android.tools.idea.gradle.util.Projects;
import com.android.tools.idea.gradle.variant.view.BuildVariantView;
+import com.android.tools.idea.rendering.ModuleSetResourceRepository;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId;
@@ -111,6 +112,7 @@
Project project = Projects.getCurrentGradleProject();
if (project != null) {
BuildVariantView.getInstance(project).updateContents();
+ ModuleSetResourceRepository.moduleRootsChanged(project);
}
}
});
diff --git a/android/src/com/android/tools/idea/gradle/IdeaAndroidProject.java b/android/src/com/android/tools/idea/gradle/IdeaAndroidProject.java
index 25784aa..27479c4 100644
--- a/android/src/com/android/tools/idea/gradle/IdeaAndroidProject.java
+++ b/android/src/com/android/tools/idea/gradle/IdeaAndroidProject.java
@@ -15,8 +15,8 @@
*/
package com.android.tools.idea.gradle;
-import com.android.build.gradle.model.AndroidProject;
-import com.android.build.gradle.model.Variant;
+import com.android.builder.model.AndroidProject;
+import com.android.builder.model.Variant;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import org.jetbrains.annotations.NotNull;
@@ -32,7 +32,6 @@
public class IdeaAndroidProject implements Serializable {
@NotNull private final String myModuleName;
@NotNull private final String myRootDirPath;
- @NotNull private final String myRootGradleProjectFilePath;
@NotNull private final AndroidProject myDelegate;
@NotNull private String mySelectedVariantName;
@@ -41,18 +40,15 @@
*
* @param moduleName the name of the IDEA module, created from {@code delegate}.
* @param rootDirPath absolute path of the root directory of the imported Android-Gradle project.
- * @param rootGradleProjectFilePath path of the build.gradle file used to import the project.
* @param delegate imported Android-Gradle project.
* @param selectedVariantName name of the selected build variant.
*/
public IdeaAndroidProject(@NotNull String moduleName,
@NotNull String rootDirPath,
- @NotNull String rootGradleProjectFilePath,
@NotNull AndroidProject delegate,
@NotNull String selectedVariantName) {
myModuleName = moduleName;
myRootDirPath = rootDirPath;
- myRootGradleProjectFilePath = rootGradleProjectFilePath;
myDelegate = delegate;
setSelectedVariantName(selectedVariantName);
}
@@ -110,12 +106,4 @@
public Collection<String> getVariantNames() {
return myDelegate.getVariants().keySet();
}
-
- /**
- * @return the path of the build.gradle file used to import the project.
- */
- @NotNull
- public String getRootGradleProjectFilePath() {
- return myRootGradleProjectFilePath;
- }
}
diff --git a/android/src/com/android/tools/idea/gradle/ProjectImportEventMessage.java b/android/src/com/android/tools/idea/gradle/ProjectImportEventMessage.java
new file mode 100644
index 0000000..aa2ce26
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/ProjectImportEventMessage.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.Serializable;
+
+/**
+ * Describes an unusual event that occurred during a project import.
+ */
+public class ProjectImportEventMessage implements Serializable {
+ @NotNull private final String myCategory;
+ @NotNull private final String myText;
+
+ public ProjectImportEventMessage(@NotNull String category, @NotNull String text) {
+ myCategory = category;
+ myText = text;
+ }
+
+ @NotNull
+ public String getCategory() {
+ return myCategory;
+ }
+
+ @NotNull
+ public String getText() {
+ return myText;
+ }
+
+ @Override
+ public String toString() {
+ if (myCategory.isEmpty()) {
+ return myText;
+ }
+ return myCategory + " " + myText;
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/actions/CleanImportProjectAction.java b/android/src/com/android/tools/idea/gradle/actions/CleanImportProjectAction.java
new file mode 100644
index 0000000..e28e8e8
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/actions/CleanImportProjectAction.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.actions;
+
+import com.android.tools.idea.gradle.project.GradleProjectImporter;
+import com.android.tools.idea.gradle.util.Projects;
+import com.google.common.collect.Lists;
+import com.intellij.ide.RecentProjectsManagerBase;
+import com.intellij.ide.impl.ProjectUtil;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.openapi.progress.Task;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ProjectManager;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeFrame;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Closes, removes all IDEA-related files (.idea folder and .iml files) and imports a project.
+ */
+public class CleanImportProjectAction extends AnAction {
+ private static final String MESSAGE_FORMAT = "This action will:\n" +
+ "1. Close project '%1$s'\n" +
+ "2. Delete all project files (.idea folder and .iml files)\n" +
+ "3. Import the project\n\n" +
+ "You will lose custom project-wide settings. Are you sure you want to continue?";
+
+ private static final String TITLE = "Close, Clean and Re-Import Project";
+
+ private static final Logger LOG = Logger.getInstance(CleanImportProjectAction.class);
+
+ public CleanImportProjectAction() {
+ super(TITLE);
+ }
+
+ @Override
+ public void actionPerformed(AnActionEvent e) {
+ Project project = e.getProject();
+ if (project != null && isGradleProject(project)) {
+ String projectName = project.getName();
+ int answer = Messages.showYesNoDialog(project, String.format(MESSAGE_FORMAT, projectName), TITLE, null);
+ if (answer == Messages.YES) {
+ LOG.info(String.format("Closing, cleaning and re-importing project '%1$s'...", projectName));
+ List<File> filesToDelete = collectFilesToDelete(project);
+ File projectDir = new File(project.getBasePath());
+ close(project);
+ delete(filesToDelete, projectName);
+ try {
+ LOG.info(String.format("About to import project '%1$s'.", projectName));
+ GradleProjectImporter.getInstance().importProject(projectName, projectDir, null);
+ LOG.info(String.format("Done importing project '%1$s'.", projectName));
+ }
+ catch (Exception error) {
+ String title = getErrorMessageTitle(error);
+ Messages.showErrorDialog(error.getMessage(), title);
+ LOG.info(String.format("Failed to import project '%1$s'.", projectName));
+ }
+ }
+ }
+ }
+ @NotNull
+ private static List<File> collectFilesToDelete(@NotNull Project project) {
+ VirtualFile projectFile = project.getProjectFile();
+ if (projectFile == null) {
+ // This is the default project. This will NEVER happen.
+ return Collections.emptyList();
+ }
+ List<File> filesToDelete = Lists.newArrayList();
+ filesToDelete.add(VfsUtilCore.virtualToIoFile(projectFile.getParent()));
+ ModuleManager moduleManager = ModuleManager.getInstance(project);
+ for (Module module : moduleManager.getModules()) {
+ VirtualFile moduleFile = module.getModuleFile();
+ if (moduleFile != null) {
+ filesToDelete.add(VfsUtilCore.virtualToIoFile(moduleFile));
+ }
+ }
+ return filesToDelete;
+ }
+
+ private static void close(@NotNull Project project) {
+ String projectName = project.getName();
+ ProjectUtil.closeAndDispose(project);
+ RecentProjectsManagerBase.getInstance().updateLastProjectPath();
+ WelcomeFrame.showIfNoProjectOpened();
+ LOG.info(String.format("Closed project '%1$s'.", projectName));
+ }
+
+ private static void delete(@NotNull final List<File> files, @NotNull String projectName) {
+ Project project = ProjectManager.getInstance().getDefaultProject();
+ String title = String.format("Cleaning up project '%1$s", projectName);
+ ProgressManager.getInstance().run(new Task.Modal(project, title, false) {
+ @Override
+ public void run(@NotNull ProgressIndicator indicator) {
+ indicator.setFraction(0d);
+ int fileCount = files.size();
+ for (int i = 0; i < fileCount; i++) {
+ File file = files.get(i);
+ String path = file.getPath();
+ LOG.info(String.format("About to delete file '%1$s'", path));
+ if (!FileUtil.delete(file)) {
+ LOG.info(String.format("Failed to delete file '%1$s'", path));
+ }
+ indicator.setFraction(i / fileCount);
+ }
+ indicator.setFraction(1d);
+ }
+ });
+ }
+
+ @NotNull
+ private static String getErrorMessageTitle(@NotNull Exception e) {
+ if (e instanceof ConfigurationException) {
+ return ((ConfigurationException)e).getTitle();
+ }
+ return TITLE;
+ }
+
+ @Override
+ public void update(AnActionEvent e) {
+ boolean isGradleProject = isGradleProject(e.getProject());
+ Presentation presentation = e.getPresentation();
+ presentation.setVisible(isGradleProject);
+ presentation.setEnabled(isGradleProject);
+ }
+
+ private static boolean isGradleProject(@Nullable Project project) {
+ return project != null && Projects.isGradleProject(project);
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/actions/ReImportProjectAction.java b/android/src/com/android/tools/idea/gradle/actions/ReImportProjectAction.java
index a578631..ab275b7 100644
--- a/android/src/com/android/tools/idea/gradle/actions/ReImportProjectAction.java
+++ b/android/src/com/android/tools/idea/gradle/actions/ReImportProjectAction.java
@@ -16,7 +16,7 @@
package com.android.tools.idea.gradle.actions;
import com.android.tools.idea.gradle.GradleImportNotificationListener;
-import com.android.tools.idea.gradle.GradleProjectImporter;
+import com.android.tools.idea.gradle.project.GradleProjectImporter;
import com.android.tools.idea.gradle.util.Projects;
import com.android.tools.idea.gradle.variant.view.BuildVariantView;
import com.intellij.openapi.actionSystem.AnAction;
@@ -41,17 +41,17 @@
@Override
public void actionPerformed(final AnActionEvent e) {
Project project = e.getProject();
- if (isGradleProject(project)) {
+ if (project != null && isGradleProject(project)) {
GradleImportNotificationListener.detachFromManager();
BuildVariantView.getInstance(project).projectImportStarted();
Presentation presentation = e.getPresentation();
presentation.setEnabled(false);
try {
- GradleProjectImporter.getInstance().reImportProject(project);
+ GradleProjectImporter.getInstance().reImportProject(project, null);
}
catch (ConfigurationException ex) {
Messages.showErrorDialog(ex.getMessage(), ex.getTitle());
- LOG.error(ex);
+ LOG.info(ex);
}
finally {
GradleImportNotificationListener.attachToManager();
diff --git a/android/src/com/android/tools/idea/gradle/compiler/AndroidGradleBuildProcessParametersProvider.java b/android/src/com/android/tools/idea/gradle/compiler/AndroidGradleBuildProcessParametersProvider.java
index 399f61f..881167b 100644
--- a/android/src/com/android/tools/idea/gradle/compiler/AndroidGradleBuildProcessParametersProvider.java
+++ b/android/src/com/android/tools/idea/gradle/compiler/AndroidGradleBuildProcessParametersProvider.java
@@ -21,6 +21,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.intellij.compiler.server.BuildProcessParametersProvider;
+import com.intellij.execution.configurations.CommandLineTokenizer;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.externalSystem.model.ProjectSystemId;
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
@@ -28,7 +29,6 @@
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.util.PathUtil;
import org.gradle.tooling.ProjectConnection;
-import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.gradle.settings.GradleExecutionSettings;
@@ -41,6 +41,8 @@
import java.util.List;
import java.util.Set;
+import static com.android.tools.idea.gradle.util.AndroidGradleSettings.createJvmArg;
+
/**
* Adds Gradle jars to the build process' classpath and adds extra Gradle-related configuration options.
*/
@@ -52,9 +54,6 @@
private List<String> myClasspath;
- @NonNls private static final String JVM_ARG_FORMAT = "-D%1$s=%2$s";
- @NonNls private static final String JVM_ARG_WITH_QUOTED_VALUE_FORMAT = "-D%1$s=\"%2$s\"";
-
public AndroidGradleBuildProcessParametersProvider(@NotNull Project project) {
myProject = project;
}
@@ -116,11 +115,18 @@
LOG.info(msg);
return Collections.emptyList();
}
+ List<String> jvmArgs = Lists.newArrayList();
GradleExecutionSettings executionSettings =
ExternalSystemApiUtil.getExecutionSettings(myProject, projectSettings.getExternalProjectPath(), SYSTEM_ID);
+ //noinspection TestOnlyProblems
+ populateJvmArgs(executionSettings, jvmArgs);
- return getGradleExecutionSettingsAsVmArgs(executionSettings);
+ if (Projects.generateSourceOnlyOnCompile(myProject)) {
+ jvmArgs.add(createJvmArg(BuildProcessJvmArgs.GENERATE_SOURCE_ONLY_ON_COMPILE, "true"));
+ }
+
+ return jvmArgs;
}
@Nullable
@@ -134,37 +140,40 @@
}
@VisibleForTesting
- @NotNull
- List<String> getGradleExecutionSettingsAsVmArgs(@NotNull GradleExecutionSettings executionSettings) {
- List<String> vmArgs = Lists.newArrayList();
-
+ void populateJvmArgs(@NotNull GradleExecutionSettings executionSettings, @NotNull List<String> jvmArgs) {
long daemonMaxIdleTimeInMs = executionSettings.getRemoteProcessIdleTtlInMs();
- vmArgs.add(createVmArg(BuildProcessJvmArgs.GRADLE_DAEMON_MAX_IDLE_TIME_IN_MS, String.valueOf(daemonMaxIdleTimeInMs)));
+ jvmArgs.add(createJvmArg(BuildProcessJvmArgs.GRADLE_DAEMON_MAX_IDLE_TIME_IN_MS, String.valueOf(daemonMaxIdleTimeInMs)));
String gradleHome = executionSettings.getGradleHome();
if (gradleHome != null && !gradleHome.isEmpty()) {
- vmArgs.add(createVmArg(BuildProcessJvmArgs.GRADLE_HOME_DIR_PATH, gradleHome));
+ jvmArgs.add(createJvmArg(BuildProcessJvmArgs.GRADLE_HOME_DIR_PATH, gradleHome));
}
String serviceDirectory = executionSettings.getServiceDirectory();
if (serviceDirectory != null && !serviceDirectory.isEmpty()) {
- vmArgs.add(createVmArg(BuildProcessJvmArgs.GRADLE_SERVICE_DIR_PATH, serviceDirectory));
+ jvmArgs.add(createJvmArg(BuildProcessJvmArgs.GRADLE_SERVICE_DIR_PATH, serviceDirectory));
}
- vmArgs.add(createVmArg(BuildProcessJvmArgs.PROJECT_DIR_PATH, myProject.getBasePath()));
+ String javaHome = executionSettings.getJavaHome();
+ if (javaHome != null && !javaHome.isEmpty()) {
+ jvmArgs.add(createJvmArg(BuildProcessJvmArgs.GRADLE_JAVA_HOME_DIR_PATH, javaHome));
+ }
+
+ jvmArgs.add(createJvmArg(BuildProcessJvmArgs.PROJECT_DIR_PATH, myProject.getBasePath()));
boolean verboseProcessing = executionSettings.isVerboseProcessing();
- vmArgs.add(createVmArg(BuildProcessJvmArgs.USE_GRADLE_VERBOSE_LOGGING, String.valueOf(verboseProcessing)));
+ jvmArgs.add(createJvmArg(BuildProcessJvmArgs.USE_GRADLE_VERBOSE_LOGGING, String.valueOf(verboseProcessing)));
- return vmArgs;
- }
-
- @NotNull
- private static String createVmArg(@NotNull String name, @NotNull String value) {
- String format = JVM_ARG_FORMAT;
- if (value.contains(" ")) {
- format = JVM_ARG_WITH_QUOTED_VALUE_FORMAT;
+ String vmOptions = executionSettings.getDaemonVmOptions();
+ int vmOptionCount = 0;
+ if (vmOptions != null && !vmOptions.isEmpty()) {
+ CommandLineTokenizer tokenizer = new CommandLineTokenizer(vmOptions);
+ while(tokenizer.hasMoreTokens()) {
+ String vmOption = tokenizer.nextToken();
+ jvmArgs.add(createJvmArg(BuildProcessJvmArgs.GRADLE_DAEMON_VM_OPTION_DOT + vmOptionCount, vmOption));
+ vmOptionCount++;
+ }
}
- return String.format(format, name, value);
+ jvmArgs.add(createJvmArg(BuildProcessJvmArgs.GRADLE_DAEMON_VM_OPTION_COUNT, String.valueOf(vmOptionCount)));
}
}
diff --git a/android/src/com/android/tools/idea/gradle/compiler/AndroidGradleBuildTargetScopeProvider.java b/android/src/com/android/tools/idea/gradle/compiler/AndroidGradleBuildTargetScopeProvider.java
new file mode 100644
index 0000000..46d2cbb
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/compiler/AndroidGradleBuildTargetScopeProvider.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.compiler;
+
+import com.android.tools.idea.gradle.util.Projects;
+import com.intellij.compiler.impl.BuildTargetScopeProvider;
+import com.intellij.openapi.compiler.CompileScope;
+import com.intellij.openapi.compiler.CompilerFilter;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.jps.api.CmdlineProtoUtil;
+import org.jetbrains.jps.api.CmdlineRemoteProto.Message.ControllerMessage.ParametersMessage.TargetTypeBuildScope;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Instructs the JPS builder to use Gradle to build the project.
+ */
+public class AndroidGradleBuildTargetScopeProvider extends BuildTargetScopeProvider {
+ public static final String TARGET_ID = "android_gradle_build_target";
+ public static final String TARGET_TYPE_ID = "android_gradle_build_target_type";
+
+ @Override
+ @NotNull
+ public List<TargetTypeBuildScope> getBuildTargetScopes(@NotNull CompileScope baseScope,
+ @NotNull CompilerFilter filter,
+ @NotNull Project project,
+ boolean forceBuild) {
+ if (!Projects.isGradleProject(project)) {
+ return Collections.emptyList();
+ }
+ TargetTypeBuildScope scope = CmdlineProtoUtil.createTargetsScope(TARGET_TYPE_ID, Collections.singletonList(TARGET_ID), forceBuild);
+ return Collections.singletonList(scope);
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/compiler/BuildProcessJvmArgs.java b/android/src/com/android/tools/idea/gradle/compiler/BuildProcessJvmArgs.java
index 784f6b3..e00f782f 100644
--- a/android/src/com/android/tools/idea/gradle/compiler/BuildProcessJvmArgs.java
+++ b/android/src/com/android/tools/idea/gradle/compiler/BuildProcessJvmArgs.java
@@ -22,10 +22,13 @@
*/
public class BuildProcessJvmArgs {
@NonNls public static final String GRADLE_DAEMON_MAX_IDLE_TIME_IN_MS = "com.android.studio.gradle.daemon.max.idle.time";
- @NonNls public static final String GRADLE_DAEMON_MAX_MEMORY_IN_MB = "com.android.studio.gradle.max.memory";
+ @NonNls public static final String GRADLE_DAEMON_VM_OPTION_COUNT = "com.android.studio.daemon.gradle.vm.option.count";
+ @NonNls public static final String GRADLE_DAEMON_VM_OPTION_DOT = "com.android.studio.daemon.gradle.vm.option.";
+ @NonNls public static final String GRADLE_JAVA_HOME_DIR_PATH = "com.android.studio.gradle.java.home.path";
@NonNls public static final String GRADLE_HOME_DIR_PATH = "com.android.studio.gradle.home.path";
@NonNls public static final String GRADLE_SERVICE_DIR_PATH = "com.android.studio.gradle.service.dir.path";
@NonNls public static final String PROJECT_DIR_PATH = "com.android.studio.gradle.project.path";
@NonNls public static final String USE_EMBEDDED_GRADLE_DAEMON = "com.android.studio.gradle.use.embedded.daemon";
@NonNls public static final String USE_GRADLE_VERBOSE_LOGGING = "com.android.studio.gradle.use.verbose.logging";
+ @NonNls public static final String GENERATE_SOURCE_ONLY_ON_COMPILE = "com.android.studio.gradle.generate.source.only.on.compile";
}
diff --git a/android/src/com/android/tools/idea/gradle/compiler/GradleCompilerSettingsConfigurable.form b/android/src/com/android/tools/idea/gradle/compiler/GradleCompilerSettingsConfigurable.form
new file mode 100644
index 0000000..6836c4f
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/compiler/GradleCompilerSettingsConfigurable.form
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.android.tools.idea.gradle.compiler.GradleCompilerSettingsConfigurable">
+ <grid id="27dc6" binding="myContentPanel" layout-manager="GridLayoutManager" row-count="10" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+ <margin top="0" left="0" bottom="0" right="0"/>
+ <constraints>
+ <xy x="20" y="20" width="580" height="400"/>
+ </constraints>
+ <properties/>
+ <border type="none"/>
+ <children>
+ <vspacer id="2169c">
+ <constraints>
+ <grid row="9" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
+ </constraints>
+ </vspacer>
+ <component id="34bf5" class="javax.swing.JCheckBox" binding="myParallelBuildCheckBox">
+ <constraints>
+ <grid row="2" column="0" row-span="1" col-span="3" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value="Compile independent modules in parallel (may require larger heap size)"/>
+ </properties>
+ </component>
+ <component id="98309" class="com.intellij.ui.HyperlinkLabel" binding="myParallelBuildDocHyperlinkLabel" custom-create="true">
+ <constraints>
+ <grid row="3" column="0" row-span="1" col-span="3" vsize-policy="3" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties/>
+ </component>
+ <component id="83516" class="com.intellij.ui.components.JBLabel">
+ <constraints>
+ <grid row="4" column="0" row-span="1" col-span="3" vsize-policy="0" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value="<html><br></html>"/>
+ </properties>
+ </component>
+ <component id="6792" class="com.intellij.ui.components.JBLabel">
+ <constraints>
+ <grid row="0" column="0" row-span="1" col-span="3" vsize-policy="0" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value="<html><b>Note:</b> These settings are used for <b>compiling</b> Gradle-based Android projects.</html>"/>
+ </properties>
+ </component>
+ <component id="5c48d" class="com.intellij.ui.components.JBLabel">
+ <constraints>
+ <grid row="1" column="0" row-span="1" col-span="3" vsize-policy="0" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value="<html><br></html>"/>
+ </properties>
+ </component>
+ <component id="a0575" class="com.intellij.ui.components.JBLabel">
+ <constraints>
+ <grid row="5" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value="VM Options:"/>
+ </properties>
+ </component>
+ <component id="9227b" class="com.intellij.ui.RawCommandLineEditor" binding="myVmOptionsEditor" custom-create="true">
+ <constraints>
+ <grid row="5" column="1" row-span="1" col-span="2" vsize-policy="0" hsize-policy="7" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <dialogCaption value="Gradle VM Options"/>
+ </properties>
+ </component>
+ <component id="e683f" class="com.intellij.ui.components.JBLabel">
+ <constraints>
+ <grid row="6" column="0" row-span="1" col-span="3" vsize-policy="0" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value="Example: -Xmx2048m -XX:MaxPermSize=512m "/>
+ </properties>
+ </component>
+ <component id="1028c" class="com.intellij.ui.components.JBLabel">
+ <constraints>
+ <grid row="7" column="0" row-span="1" col-span="3" vsize-policy="0" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value="<html><br></html>"/>
+ </properties>
+ </component>
+ <component id="ad345" class="javax.swing.JCheckBox" binding="myAutoMakeCheckBox">
+ <constraints>
+ <grid row="8" column="0" row-span="1" col-span="3" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value="Make project automatically (only works while not running / debugging)"/>
+ </properties>
+ </component>
+ </children>
+ </grid>
+</form>
diff --git a/android/src/com/android/tools/idea/gradle/compiler/GradleCompilerSettingsConfigurable.java b/android/src/com/android/tools/idea/gradle/compiler/GradleCompilerSettingsConfigurable.java
new file mode 100644
index 0000000..1a80a29
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/compiler/GradleCompilerSettingsConfigurable.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.compiler;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.intellij.compiler.CompilerWorkspaceConfiguration;
+import com.intellij.openapi.options.Configurable;
+import com.intellij.openapi.options.SearchableConfigurable;
+import com.intellij.openapi.project.Project;
+import com.intellij.ui.HyperlinkLabel;
+import com.intellij.ui.RawCommandLineEditor;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.gradle.settings.GradleSettings;
+
+import javax.swing.*;
+
+/**
+ * Configuration page for Gradle compiler settings.
+ */
+public class GradleCompilerSettingsConfigurable implements SearchableConfigurable, Configurable.NoScroll {
+ private final CompilerWorkspaceConfiguration myCompilerConfiguration;
+ private final GradleSettings myGradleSettings;
+
+ private JCheckBox myParallelBuildCheckBox;
+ private HyperlinkLabel myParallelBuildDocHyperlinkLabel;
+ private RawCommandLineEditor myVmOptionsEditor;
+ private JCheckBox myAutoMakeCheckBox;
+ private JPanel myContentPanel;
+
+ public GradleCompilerSettingsConfigurable(@NotNull Project project) {
+ myCompilerConfiguration = CompilerWorkspaceConfiguration.getInstance(project);
+ myGradleSettings = GradleSettings.getInstance(project);
+ }
+
+ @Override
+ @NotNull
+ public String getId() {
+ return "gradle.compiler";
+ }
+
+ @Override
+ @Nullable
+ public Runnable enableSearch(String option) {
+ return null;
+ }
+
+ @Override
+ @Nls
+ public String getDisplayName() {
+ return "Gradle";
+ }
+
+ @Override
+ @Nullable
+ public String getHelpTopic() {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public JComponent createComponent() {
+ return myContentPanel;
+ }
+
+ @Override
+ public boolean isModified() {
+ return myCompilerConfiguration.PARALLEL_COMPILATION != isParallelBuildsEnabled() ||
+ !Objects.equal(getVmOptions(), myGradleSettings.getGradleVmOptions()) ||
+ myCompilerConfiguration.MAKE_PROJECT_ON_SAVE != isAutoMakeEnabled();
+ }
+
+ @Override
+ public void apply() {
+ myCompilerConfiguration.PARALLEL_COMPILATION = isParallelBuildsEnabled();
+ myGradleSettings.setGradleVmOptions(getVmOptions());
+ myCompilerConfiguration.MAKE_PROJECT_ON_SAVE = isAutoMakeEnabled();
+ }
+
+ private boolean isParallelBuildsEnabled() {
+ return myParallelBuildCheckBox.isSelected();
+ }
+
+ private boolean isAutoMakeEnabled() {
+ return myAutoMakeCheckBox.isSelected();
+ }
+
+ @Nullable
+ private String getVmOptions() {
+ return Strings.emptyToNull(myVmOptionsEditor.getText().trim());
+ }
+
+ @Override
+ public void reset() {
+ myParallelBuildCheckBox.setSelected(myCompilerConfiguration.PARALLEL_COMPILATION);
+ String vmOptions = Strings.nullToEmpty(myGradleSettings.getGradleVmOptions());
+ myVmOptionsEditor.setText(vmOptions);
+ myAutoMakeCheckBox.setSelected(myCompilerConfiguration.MAKE_PROJECT_ON_SAVE);
+ }
+
+ @Override
+ public void disposeUIResources() {
+ }
+
+ private void createUIComponents() {
+ myParallelBuildDocHyperlinkLabel = new HyperlinkLabel();
+ myParallelBuildDocHyperlinkLabel
+ .setHyperlinkText("This option is in \"incubation\" and should only be used with ", "decoupled projects", ".");
+ myParallelBuildDocHyperlinkLabel
+ .setHyperlinkTarget("http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects");
+
+ myVmOptionsEditor = new RawCommandLineEditor();
+ myVmOptionsEditor.setDialogCaption("Gradle VM Options");
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/compiler/HideCompilerOptions.java b/android/src/com/android/tools/idea/gradle/compiler/HideCompilerOptions.java
new file mode 100644
index 0000000..ff110cc
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/compiler/HideCompilerOptions.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.compiler;
+
+import com.android.tools.idea.gradle.util.Projects;
+import com.intellij.compiler.options.CompilerOptionsFilter;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Hides options in the "Compiler" preference page that are either redundant or not applicable to Gradle-based Android projects:
+ * <ul>
+ * <li>"Add @NotNull assertions"</li>
+ * <li>"Use external build" and children</li>
+ * </ul>
+ */
+public class HideCompilerOptions implements CompilerOptionsFilter {
+ @Override
+ public boolean isAvailable(@NotNull Setting setting, @NotNull Project project) {
+ if (!Projects.isGradleProject(project)) {
+ return true;
+ }
+ return Setting.EXTERNAL_BUILD != setting && Setting.ADD_NOT_NULL_ASSERTIONS != setting;
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/customizer/AndroidFacetModuleCustomizer.java b/android/src/com/android/tools/idea/gradle/customizer/AndroidFacetModuleCustomizer.java
index fcb033a..e02d273 100755
--- a/android/src/com/android/tools/idea/gradle/customizer/AndroidFacetModuleCustomizer.java
+++ b/android/src/com/android/tools/idea/gradle/customizer/AndroidFacetModuleCustomizer.java
@@ -15,13 +15,16 @@
*/
package com.android.tools.idea.gradle.customizer;
-import com.android.build.gradle.model.AndroidProject;
+import com.android.builder.model.AndroidProject;
import com.android.builder.model.SourceProvider;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.android.tools.idea.gradle.util.Facets;
+import com.android.tools.idea.gradle.variant.view.BuildVariantView;
import com.google.common.base.Strings;
import com.intellij.facet.FacetManager;
import com.intellij.facet.ModifiableFacetModel;
+import com.intellij.openapi.application.Application;
+import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.io.FileUtilRt;
@@ -36,7 +39,7 @@
import java.util.Set;
/**
- * Adds the Android facet to modules imported from {@link com.android.build.gradle.model.AndroidProject}s.
+ * Adds the Android facet to modules imported from {@link com.android.builder.model.AndroidProject}s.
*/
public class AndroidFacetModuleCustomizer implements ModuleCustomizer {
private static final String EMPTY_PATH = "";
@@ -45,9 +48,12 @@
private static final String SEPARATOR = "/";
@Override
- public void customizeModule(@NotNull Module module, @NotNull Project project, @Nullable IdeaAndroidProject ideaAndroidProject) {
- if (ideaAndroidProject != null) {
- AndroidFacet facet = Facets.getFirstFacet(module, AndroidFacet.ID);
+ public void customizeModule(@NotNull Module module, @NotNull final Project project, @Nullable IdeaAndroidProject ideaAndroidProject) {
+ if (ideaAndroidProject == null) {
+ Facets.removeAllFacetsOfType(module, AndroidFacet.ID);
+ }
+ else {
+ AndroidFacet facet = Facets.getFirstFacetOfType(module, AndroidFacet.ID);
if (facet != null) {
configureFacet(facet, ideaAndroidProject);
}
@@ -64,6 +70,19 @@
}
}
}
+ Runnable updateBuildVariantViewTask = new Runnable() {
+ @Override
+ public void run() {
+ BuildVariantView buildVariantView = BuildVariantView.getInstance(project);
+ buildVariantView.updateContents();
+ }
+ };
+ Application application = ApplicationManager.getApplication();
+ if (application.isDispatchThread()) {
+ updateBuildVariantViewTask.run();
+ } else {
+ application.invokeLater(updateBuildVariantViewTask);
+ }
}
private static void configureFacet(@NotNull AndroidFacet facet, @NotNull IdeaAndroidProject ideaAndroidProject) {
@@ -76,7 +95,6 @@
SourceProvider sourceProvider = delegate.getDefaultConfig().getSourceProvider();
syncSelectedVariant(facetState, ideaAndroidProject);
- facet.syncSelectedVariant();
String moduleDirPath = ideaAndroidProject.getRootDirPath();
File manifestFile = sourceProvider.getManifestFile();
@@ -89,11 +107,13 @@
facetState.ASSETS_FOLDER_RELATIVE_PATH = getRelativePath(moduleDirPath, assetsDirs);
facet.setIdeaAndroidProject(ideaAndroidProject);
+ facet.syncSelectedVariant();
}
private static void syncSelectedVariant(@NotNull JpsAndroidModuleProperties facetState, @NotNull IdeaAndroidProject ideaAndroidProject) {
- if (!Strings.isNullOrEmpty(facetState.SELECTED_BUILD_VARIANT)) {
- ideaAndroidProject.setSelectedVariantName(facetState.SELECTED_BUILD_VARIANT);
+ String variantStoredInFacet = facetState.SELECTED_BUILD_VARIANT;
+ if (!Strings.isNullOrEmpty(variantStoredInFacet) && ideaAndroidProject.getVariantNames().contains(variantStoredInFacet)) {
+ ideaAndroidProject.setSelectedVariantName(variantStoredInFacet);
}
}
diff --git a/android/src/com/android/tools/idea/gradle/customizer/AndroidSdkModuleCustomizer.java b/android/src/com/android/tools/idea/gradle/customizer/AndroidSdkModuleCustomizer.java
index d7a1ffa..b65d898 100644
--- a/android/src/com/android/tools/idea/gradle/customizer/AndroidSdkModuleCustomizer.java
+++ b/android/src/com/android/tools/idea/gradle/customizer/AndroidSdkModuleCustomizer.java
@@ -20,7 +20,10 @@
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.io.FileUtil;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -28,7 +31,7 @@
import java.io.IOException;
/**
- * Sets an Android SDK to a module imported from an {@link com.android.build.gradle.model.AndroidProject}.
+ * Sets an Android SDK to a module imported from an {@link com.android.builder.model.AndroidProject}.
*/
public class AndroidSdkModuleCustomizer implements ModuleCustomizer {
private static final Logger LOG = Logger.getInstance(AndroidSdkModuleCustomizer.class);
@@ -36,8 +39,8 @@
/**
* Sets an Android SDK to the given module only if:
* <ol>
- * <li>the given module was created by importing an {@code AndroidProject}</li>
- * <li>there is a matching Android SDK already defined in IDEA</li>
+ * <li>the given module was created by importing an {@code AndroidProject}</li>
+ * <li>there is a matching Android SDK already defined in IDEA</li>
* </ol>
*
* @param module module to customize.
@@ -49,9 +52,9 @@
if (ideaAndroidProject == null) {
return;
}
- String androidSdkPath;
+ LocalProperties localProperties;
try {
- androidSdkPath = LocalProperties.getAndroidSdkPath(project);
+ localProperties = new LocalProperties(project);
}
catch (IOException e) {
String msg = String.format("Unable to read local.properties file in project '%1$s'", project.getBasePath());
@@ -59,15 +62,41 @@
showErrorDialog(msg);
return;
}
+ String androidSdkInProperties = localProperties.getAndroidSdkPath();
String compileTarget = ideaAndroidProject.getDelegate().getCompileTarget();
- boolean sdkSet = AndroidSdkUtils.findAndSetSdk(module, compileTarget, androidSdkPath, true);
- if (!sdkSet) {
+ boolean sdkSet = AndroidSdkUtils.findAndSetSdk(module, compileTarget, androidSdkInProperties, true);
+ if (sdkSet) {
+ // Check that the SDK set is the same as the one in the local.properties.
+ String sdkPath = getSdkPath(module);
+ assert sdkPath != null;
+ boolean shouldSetAndroidSdkInLocalProperties;
+ if (androidSdkInProperties == null || androidSdkInProperties.isEmpty()) {
+ shouldSetAndroidSdkInLocalProperties = true;
+ }
+ else {
+ shouldSetAndroidSdkInLocalProperties = !areEqualPaths(sdkPath, androidSdkInProperties);
+ }
+ if (shouldSetAndroidSdkInLocalProperties) {
+ // Changing the SDK path in local.properties to match the one set, so Studio and command line builds are consistent.
+ localProperties.setAndroidSdkPath(sdkPath);
+ try {
+ localProperties.save();
+ }
+ catch (IOException e) {
+ // An unlikely thing to happen on top of another unlikely thing to happen.
+ String msg = String.format("Unable to set SDK path in local.properties file");
+ LOG.error(msg, e);
+ showErrorDialog(msg);
+ }
+ }
+ }
+ else {
// This should never, ever happen.
// We already either attempted to create an Android SDK (even prompted the user for its path) or downloaded the matching platform.
String msg;
- if (androidSdkPath != null) {
+ if (androidSdkInProperties != null) {
String format = "Unable to set the Android SDK at '%1$s', with compile target '%2$s', to module '%3$s'";
- msg = String.format(format, androidSdkPath, compileTarget, module.getName());
+ msg = String.format(format, androidSdkInProperties, compileTarget, module.getName());
}
else {
String format = "Unable to set an Android SDK, with compile target '%1$s', to module '%2$s'";
@@ -82,4 +111,15 @@
private static void showErrorDialog(@NotNull String msg) {
Messages.showErrorDialog(msg, "Android SDK Configuration");
}
+
+ @Nullable
+ private static String getSdkPath(@NotNull Module module) {
+ Sdk sdk = ModuleRootManager.getInstance(module).getSdk();
+ return sdk != null ? sdk.getHomePath() : null;
+ }
+
+ private static boolean areEqualPaths(@NotNull String sdkPath1, @NotNull String sdkPath2) {
+ return FileUtil.pathsEqual(FileUtil.toSystemIndependentName(sdkPath1),
+ FileUtil.toSystemIndependentName(sdkPath2));
+ }
}
diff --git a/android/src/com/android/tools/idea/gradle/customizer/CompilerOutputPathModuleCustomizer.java b/android/src/com/android/tools/idea/gradle/customizer/CompilerOutputPathModuleCustomizer.java
index 3dd83fc..0832851 100644
--- a/android/src/com/android/tools/idea/gradle/customizer/CompilerOutputPathModuleCustomizer.java
+++ b/android/src/com/android/tools/idea/gradle/customizer/CompilerOutputPathModuleCustomizer.java
@@ -15,6 +15,7 @@
*/
package com.android.tools.idea.gradle.customizer;
+import com.android.builder.model.Variant;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.google.common.base.Strings;
import com.intellij.openapi.module.Module;
@@ -29,7 +30,7 @@
import java.io.File;
/**
- * Sets the compiler output folder to a module imported from an {@link com.android.build.gradle.model.AndroidProject}.
+ * Sets the compiler output folder to a module imported from an {@link com.android.builder.model.AndroidProject}.
*/
public class CompilerOutputPathModuleCustomizer implements ModuleCustomizer {
@Override
@@ -40,7 +41,8 @@
// We are dealing with old model that does not have 'class' folder.
return;
}
- File outputFile = ideaAndroidProject.getSelectedVariant().getClassesFolder();
+ Variant selectedVariant = ideaAndroidProject.getSelectedVariant();
+ File outputFile = selectedVariant.getMainArtifactInfo().getClassesFolder();
String url = VfsUtil.pathToUrl(outputFile.getAbsolutePath());
ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module);
diff --git a/android/src/com/android/tools/idea/gradle/customizer/ContentRootModuleCustomizer.java b/android/src/com/android/tools/idea/gradle/customizer/ContentRootModuleCustomizer.java
index 9007950..846f51a 100644
--- a/android/src/com/android/tools/idea/gradle/customizer/ContentRootModuleCustomizer.java
+++ b/android/src/com/android/tools/idea/gradle/customizer/ContentRootModuleCustomizer.java
@@ -16,8 +16,8 @@
package com.android.tools.idea.gradle.customizer;
import com.android.tools.idea.gradle.IdeaAndroidProject;
-import com.android.tools.idea.gradle.model.AndroidContentRoot;
-import com.android.tools.idea.gradle.model.AndroidContentRoot.ContentRootStorage;
+import com.android.tools.idea.gradle.project.AndroidContentRoot;
+import com.android.tools.idea.gradle.project.AndroidContentRoot.ContentRootStorage;
import com.google.common.base.Preconditions;
import com.intellij.openapi.externalSystem.model.project.ExternalSystemSourceType;
import com.intellij.openapi.module.Module;
@@ -26,7 +26,7 @@
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.util.io.FileUtil;
-import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -34,7 +34,7 @@
import java.io.File;
/**
- * Sets the content roots of an IDEA module imported from an {@link com.android.build.gradle.model.AndroidProject}.
+ * Sets the content roots of an IDEA module imported from an {@link com.android.builder.model.AndroidProject}.
*/
public class ContentRootModuleCustomizer implements ModuleCustomizer {
/**
@@ -99,7 +99,7 @@
return;
}
boolean isTestSource = sourceType.equals(ExternalSystemSourceType.TEST);
- String url = VfsUtil.pathToUrl(dir.getAbsolutePath());
+ String url = VfsUtilCore.pathToUrl(dir.getAbsolutePath());
contentEntry.addSourceFolder(url, isTestSource);
}
};
diff --git a/android/src/com/android/tools/idea/gradle/customizer/DependenciesModuleCustomizer.java b/android/src/com/android/tools/idea/gradle/customizer/DependenciesModuleCustomizer.java
index a1a9023..98ef81f 100644
--- a/android/src/com/android/tools/idea/gradle/customizer/DependenciesModuleCustomizer.java
+++ b/android/src/com/android/tools/idea/gradle/customizer/DependenciesModuleCustomizer.java
@@ -16,11 +16,13 @@
package com.android.tools.idea.gradle.customizer;
import com.android.tools.idea.gradle.IdeaAndroidProject;
-import com.android.tools.idea.gradle.model.AndroidDependencies;
-import com.android.tools.idea.gradle.model.AndroidDependencies.DependencyFactory;
-import com.google.common.collect.Maps;
+import com.android.tools.idea.gradle.dependency.*;
+import com.android.tools.idea.gradle.facet.AndroidGradleFacet;
+import com.android.tools.idea.gradle.util.Facets;
+import com.google.common.base.Objects;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable;
@@ -33,12 +35,12 @@
import org.jetbrains.annotations.Nullable;
import java.io.File;
-import java.util.Map;
+import java.util.Collection;
/**
- * Sets the dependencies of a module imported from an {@link com.android.build.gradle.model.AndroidProject}.
+ * Sets the dependencies of a module imported from an {@link com.android.builder.model.AndroidProject}.
*/
-public class DependenciesModuleCustomizer implements ModuleCustomizer {
+public class DependenciesModuleCustomizer extends DependencyUpdater<ModifiableRootModel> implements ModuleCustomizer {
private static final Logger LOG = Logger.getInstance(DependenciesModuleCustomizer.class);
@Override
@@ -46,105 +48,118 @@
if (ideaAndroidProject == null) {
return;
}
- // first pass we update existing libraries or import any missing project-level library
- updateProjectLibraries(project, ideaAndroidProject);
-
ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module);
ModifiableRootModel model = moduleRootManager.getModifiableModel();
try {
- // remove existing dependencies.
removeExistingDependencies(model);
- populateDependencies(model, project, ideaAndroidProject);
+ Collection<Dependency> dependencies = Dependency.extractFrom(ideaAndroidProject);
+ updateDependencies(model, dependencies);
} finally {
model.commit();
}
}
- private static void updateProjectLibraries(@NotNull Project project, @NotNull IdeaAndroidProject ideaAndroidProject) {
- final LibraryTable libraryTable = ProjectLibraryTable.getInstance(project);
- final LibraryTable.ModifiableModel model = libraryTable.getModifiableModel();
- final Map<File, Library> libraries = Maps.newHashMap();
- try {
- AndroidDependencies.populate(ideaAndroidProject, new DependencyFactory() {
- @Override
- public void addDependency(@NotNull DependencyScope scope, @NotNull String name, @NotNull File binaryPath) {
- Library library = libraryTable.getLibraryByName(name);
- if (library == null) {
- library = model.createLibrary(name);
- }
- libraries.put(binaryPath, library);
- }
- });
- }
- finally {
- model.commit();
- }
- registerPaths(OrderRootType.CLASSES, libraries);
- }
-
- private static void registerPaths(@NotNull OrderRootType type, @NotNull Map<File, Library> libraries) {
- for (Map.Entry<File, Library> entry : libraries.entrySet()) {
- Library library = entry.getValue();
- Library.ModifiableModel model = library.getModifiableModel();
- try {
- File file = entry.getKey();
- VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file);
- if (virtualFile == null) {
- String msg = String.format("Unable to find file at path '%1$s', library '%2$s'", file.getAbsolutePath(), library.getName());
- LOG.warn(msg);
- continue;
- }
- if (virtualFile.isDirectory()) {
- model.addRoot(virtualFile, type);
- continue;
- }
- VirtualFile jarRoot = JarFileSystem.getInstance().getJarRootForLocalFile(virtualFile);
- if (jarRoot == null) {
- String msg =
- String.format("Unable to parse contents of jar file '%1$s', library '%2$s'", file.getAbsolutePath(), library.getName());
- LOG.warn(msg);
- continue;
- }
- model.addRoot(jarRoot, type);
- }
- finally {
- model.commit();
- }
- }
- }
-
- private static void removeExistingDependencies(@NotNull final ModifiableRootModel model) {
+ private static void removeExistingDependencies(@NotNull final ModifiableRootModel moduleModel) {
RootPolicy<Object> dependencyRemover = new RootPolicy<Object>() {
@Override
public Object visitLibraryOrderEntry(LibraryOrderEntry libraryOrderEntry, Object value) {
- model.removeOrderEntry(libraryOrderEntry);
+ moduleModel.removeOrderEntry(libraryOrderEntry);
return value;
}
@Override
public Object visitModuleOrderEntry(ModuleOrderEntry moduleOrderEntry, Object value) {
- model.removeOrderEntry(moduleOrderEntry);
+ moduleModel.removeOrderEntry(moduleOrderEntry);
return value;
}
};
- for (OrderEntry orderEntry : model.getOrderEntries()) {
+ for (OrderEntry orderEntry : moduleModel.getOrderEntries()) {
orderEntry.accept(dependencyRemover, null);
}
}
- private static void populateDependencies(@NotNull final ModifiableRootModel model,
- @NotNull Project project,
- @NotNull IdeaAndroidProject ideaAndroidProject) {
- final LibraryTable libraryTable = ProjectLibraryTable.getInstance(project);
- AndroidDependencies.populate(ideaAndroidProject, new DependencyFactory() {
- @Override
- public void addDependency(@NotNull DependencyScope scope, @NotNull String name, @NotNull File binaryPath) {
- Library library = libraryTable.getLibraryByName(name);
- if (library != null) {
- LibraryOrderEntry orderEntry = model.addLibraryEntry(library);
- orderEntry.setScope(scope);
+ @Override
+ protected void updateDependency(@NotNull ModifiableRootModel moduleModel, @NotNull LibraryDependency dependency) {
+ LibraryTable libraryTable = ProjectLibraryTable.getInstance(moduleModel.getProject());
+ Library library = libraryTable.getLibraryByName(dependency.getName());
+ if (library == null) {
+ // Create library.
+ LibraryTable.ModifiableModel libraryTableModel = libraryTable.getModifiableModel();
+ try {
+ library = libraryTableModel.createLibrary(dependency.getName());
+ updateLibraryPaths(library, dependency);
+ }
+ finally {
+ libraryTableModel.commit();
+ }
+ }
+ LibraryOrderEntry orderEntry = moduleModel.addLibraryEntry(library);
+ orderEntry.setScope(dependency.getScope());
+ orderEntry.setExported(true);
+ }
+
+ private static void updateLibraryPaths(@NotNull Library library, @NotNull LibraryDependency dependency) {
+ Library.ModifiableModel libraryModel = library.getModifiableModel();
+ try {
+ Collection<String> binaryPaths = dependency.getPaths(LibraryDependency.PathType.BINARY);
+ for (String binaryPath : binaryPaths) {
+ File file = new File(binaryPath);
+ VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file);
+ if (virtualFile == null) {
+ // TODO: log and show balloon
+ String msg = String.format("Unable to find file at path '%1$s', library '%2$s'", file.getPath(), library.getName());
+ LOG.warn(msg);
+ continue;
+ }
+ if (virtualFile.isDirectory()) {
+ libraryModel.addRoot(virtualFile, OrderRootType.CLASSES);
+ continue;
+ }
+ VirtualFile jarRoot = JarFileSystem.getInstance().getJarRootForLocalFile(virtualFile);
+ if (jarRoot == null) {
+ // TODO: log and show balloon
+ String msg = String.format("Unable to parse contents of jar file '%1$s', library '%2$s'", file.getPath(), library.getName());
+ LOG.warn(msg);
+ continue;
+ }
+ libraryModel.addRoot(jarRoot, OrderRootType.CLASSES);
+ }
+ }
+ finally {
+ libraryModel.commit();
+ }
+ }
+
+ @Override
+ protected boolean tryUpdating(@NotNull ModifiableRootModel moduleModel, @NotNull ModuleDependency dependency) {
+ ModuleManager moduleManager = ModuleManager.getInstance(moduleModel.getProject());
+ Module moduleDependency = null;
+ for (Module module : moduleManager.getModules()) {
+ AndroidGradleFacet androidGradleFacet = Facets.getFirstFacetOfType(module, AndroidGradleFacet.TYPE_ID);
+ if (androidGradleFacet != null) {
+ String gradlePath = androidGradleFacet.getConfiguration().GRADLE_PROJECT_PATH;
+ if (Objects.equal(gradlePath, dependency.getGradlePath())) {
+ moduleDependency = module;
+ break;
}
}
- });
+ }
+ if (moduleDependency != null) {
+ ModuleOrderEntry orderEntry = moduleModel.addModuleOrderEntry(moduleDependency);
+ orderEntry.setExported(true);
+ return true;
+ }
+ return false;
+ }
+
+ @NotNull
+ @Override
+ protected String getNameOf(@NotNull ModifiableRootModel moduleModel) {
+ return moduleModel.getModule().getName();
+ }
+
+ @Override
+ protected void log(ModifiableRootModel moduleModel, String category, String msg) {
+ // TODO: log and show balloon.
}
}
diff --git a/android/src/com/android/tools/idea/gradle/customizer/RunConfigModuleCustomizer.java b/android/src/com/android/tools/idea/gradle/customizer/RunConfigModuleCustomizer.java
index 230c1f4..20e53a0 100755
--- a/android/src/com/android/tools/idea/gradle/customizer/RunConfigModuleCustomizer.java
+++ b/android/src/com/android/tools/idea/gradle/customizer/RunConfigModuleCustomizer.java
@@ -33,14 +33,14 @@
import java.util.List;
/**
- * Creates run configurations for modules imported from {@link com.android.build.gradle.model.AndroidProject}s.
+ * Creates run configurations for modules imported from {@link com.android.builder.model.AndroidProject}s.
*/
public class RunConfigModuleCustomizer implements ModuleCustomizer {
@Override
public void customizeModule(@NotNull Module module, @NotNull Project project, @Nullable IdeaAndroidProject ideaAndroidProject) {
if (ideaAndroidProject != null) {
- AndroidFacet facet = Facets.getFirstFacet(module, AndroidFacet.ID);
- if (facet != null && !facet.getConfiguration().getState().LIBRARY_PROJECT) {
+ AndroidFacet facet = Facets.getFirstFacetOfType(module, AndroidFacet.ID);
+ if (facet != null && !facet.isLibraryProject()) {
RunManager runManager = RunManager.getInstance(project);
ConfigurationFactory configurationFactory = AndroidRunConfigurationType.getInstance().getFactory();
List<RunConfiguration> configs = runManager.getConfigurationsList(configurationFactory.getType());
diff --git a/android/src/com/android/tools/idea/gradle/dependency/Dependency.java b/android/src/com/android/tools/idea/gradle/dependency/Dependency.java
new file mode 100644
index 0000000..66c21f5
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/dependency/Dependency.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.dependency;
+
+import com.android.builder.model.AndroidLibrary;
+import com.android.builder.model.ArtifactInfo;
+import com.android.builder.model.Variant;
+import com.android.tools.idea.gradle.IdeaAndroidProject;
+import com.google.common.collect.Lists;
+import com.intellij.openapi.roots.DependencyScope;
+import com.intellij.openapi.util.io.FileUtil;
+import org.gradle.tooling.model.idea.*;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+
+import static com.android.SdkConstants.DOT_AAR;
+import static com.android.SdkConstants.FD_RES;
+
+/**
+ * An IDEA module's dependency on an artifact (e.g. a jar file or another IDEA module.)
+ */
+public abstract class Dependency {
+ /**
+ * The Android Gradle plug-in only supports "compile" and "test" scopes. This list is sorted by width of the scope, being "compile" a
+ * wider scope than "test."
+ */
+ static final List<DependencyScope> SUPPORTED_SCOPES = Lists.newArrayList(DependencyScope.COMPILE, DependencyScope.TEST);
+
+ @NotNull private final String myName;
+ @NotNull private DependencyScope myScope;
+
+ /**
+ * Creates a new {@link Dependency}. This constructor sets the scope to {@link DependencyScope#COMPILE}.
+ *
+ * @param name the name of the artifact to depend on.
+ */
+ Dependency(@NotNull String name) {
+ this(name, DependencyScope.COMPILE);
+ }
+
+ /**
+ * Creates a new {@link Dependency}.
+ *
+ * @param name the name of the artifact to depend on.
+ * @param scope the scope of the dependency. Supported values are {@link DependencyScope#COMPILE} and {@link DependencyScope#TEST}.
+ * @throws IllegalArgumentException if the given scope is not supported.
+ */
+ Dependency(@NotNull String name, @NotNull DependencyScope scope) throws IllegalArgumentException {
+ myName = name;
+ setScope(scope);
+ }
+
+ @NotNull
+ public final String getName() {
+ return myName;
+ }
+
+ @NotNull
+ public final DependencyScope getScope() {
+ return myScope;
+ }
+
+ /**
+ * Sets the scope of this dependency.
+ *
+ * @param scope the scope of the dependency. Supported values are {@link DependencyScope#COMPILE} and {@link DependencyScope#TEST}.
+ * @throws IllegalArgumentException if the given scope is not supported.
+ */
+ void setScope(@NotNull DependencyScope scope) throws IllegalArgumentException {
+ if (!SUPPORTED_SCOPES.contains(scope)) {
+ String msg = String.format("'%1$s' is not a supported scope. Supported scopes are %2$s.", scope, SUPPORTED_SCOPES);
+ throw new IllegalArgumentException(msg);
+ }
+ myScope = scope;
+ }
+
+ @NotNull
+ public static Collection<Dependency> extractFrom(@NotNull IdeaAndroidProject androidProject) {
+ DependencySet dependencies = new DependencySet();
+ Variant selectedVariant = androidProject.getSelectedVariant();
+
+ ArtifactInfo testArtifactInfo = selectedVariant.getTestArtifactInfo();
+ if (testArtifactInfo != null) {
+ populate(dependencies, testArtifactInfo, DependencyScope.TEST);
+ }
+ ArtifactInfo mainArtifactInfo = selectedVariant.getMainArtifactInfo();
+ populate(dependencies, mainArtifactInfo, DependencyScope.COMPILE);
+
+ return dependencies.getValues();
+ }
+
+ private static void populate(@NotNull DependencySet dependencies, @NotNull ArtifactInfo artifactInfo, @NotNull DependencyScope scope) {
+ populate(dependencies, artifactInfo.getDependencies().getJars(), scope);
+
+ for (AndroidLibrary lib : artifactInfo.getDependencies().getLibraries()) {
+ ModuleDependency mainDependency = null;
+ String gradleProjectPath = lib.getProject();
+ if (gradleProjectPath != null && !gradleProjectPath.isEmpty()) {
+ //noinspection TestOnlyProblems
+ mainDependency = new ModuleDependency(gradleProjectPath, scope);
+ dependencies.add(mainDependency);
+ }
+ File jar = lib.getJarFile();
+ File aar = jar.getParentFile();
+ String name = aar != null ? aar.getName() : FileUtil.getNameWithoutExtension(jar);
+ if (mainDependency == null) {
+ LibraryDependency dependency = new LibraryDependency(name, scope);
+ dependency.addPath(LibraryDependency.PathType.BINARY, jar);
+ dependencies.add(dependency);
+
+ // The model does not yet provide pointers to resources in AAR files, so
+ // manually look for them where they are known to be and add them manually
+ if (aar != null && aar.getName().endsWith(DOT_AAR)) {
+ File res = new File(aar, FD_RES);
+ if (res.exists()) {
+ dependency.addPath(LibraryDependency.PathType.BINARY, res);
+ }
+ }
+ }
+ else {
+ // add the aar as dependency in case there is a module dependency that cannot be satisfied (e.g. the module is outside of the
+ // project.) If we cannot set the module dependency, we set a library dependency instead.
+ LibraryDependency backupDependency = new LibraryDependency(name);
+ backupDependency.addPath(LibraryDependency.PathType.BINARY, jar);
+ //noinspection TestOnlyProblems
+ mainDependency.setBackupDependency(backupDependency);
+ }
+
+ populate(dependencies, lib.getLocalJars(), scope);
+ }
+
+ for (String gradleProjectPath : artifactInfo.getDependencies().getProjects()) {
+ if (gradleProjectPath != null && !gradleProjectPath.isEmpty()) {
+ //noinspection TestOnlyProblems
+ ModuleDependency dependency = new ModuleDependency(gradleProjectPath, scope);
+ dependencies.add(dependency);
+ }
+ }
+ }
+
+ private static void populate(@NotNull DependencySet dependencies, @NotNull List<File> jars, @NotNull DependencyScope scope) {
+ for (File jar : jars) {
+ //noinspection TestOnlyProblems
+ dependencies.add(new LibraryDependency(jar, scope));
+ }
+ }
+
+ @NotNull
+ public static Collection<Dependency> extractFrom(@NotNull IdeaModule module) {
+ DependencySet dependencies = new DependencySet();
+ for (IdeaDependency ideaDependency : module.getDependencies()) {
+ DependencyScope scope = parseScope(ideaDependency.getScope());
+
+ if (ideaDependency instanceof IdeaModuleDependency) {
+ IdeaModule ideaModule = ((IdeaModuleDependency)ideaDependency).getDependencyModule();
+ String moduleName = ideaModule.getName();
+ String gradlePath = ideaModule.getGradleProject().getPath();
+ Dependency dependency = new ModuleDependency(moduleName, gradlePath, scope);
+ dependencies.add(dependency);
+ }
+ else if (ideaDependency instanceof IdeaSingleEntryLibraryDependency) {
+ IdeaSingleEntryLibraryDependency ideaLibrary = (IdeaSingleEntryLibraryDependency)ideaDependency;
+ //noinspection TestOnlyProblems
+ LibraryDependency dependency = new LibraryDependency(ideaLibrary.getFile(), scope);
+ File javadoc = ideaLibrary.getJavadoc();
+ if (javadoc != null) {
+ dependency.addPath(LibraryDependency.PathType.DOC, javadoc);
+ }
+ File source = ideaLibrary.getSource();
+ if (source != null) {
+ dependency.addPath(LibraryDependency.PathType.SOURCE, source);
+ }
+ dependencies.add(dependency);
+ }
+ }
+ return dependencies.getValues();
+ }
+
+ @NotNull
+ private static DependencyScope parseScope(@Nullable IdeaDependencyScope scope) {
+ if (scope != null) {
+ String scopeAsString = scope.getScope();
+ if (scopeAsString != null) {
+ for (DependencyScope dependencyScope : DependencyScope.values()) {
+ if (scopeAsString.equalsIgnoreCase(dependencyScope.toString())) {
+ return dependencyScope;
+ }
+ }
+ }
+ }
+ return DependencyScope.COMPILE;
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/dependency/DependencySet.java b/android/src/com/android/tools/idea/gradle/dependency/DependencySet.java
new file mode 100644
index 0000000..5495009
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/dependency/DependencySet.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.dependency;
+
+import com.google.common.collect.Maps;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collection;
+import java.util.Map;
+
+import static com.android.tools.idea.gradle.dependency.Dependency.SUPPORTED_SCOPES;
+
+/**
+ * Collection of an IDEA module's dependencies.
+ */
+class DependencySet {
+ private final Map<String, Dependency> myDependenciesByName = Maps.newHashMap();
+
+ /**
+ * Adds the given dependency to this collection. If this collection already has a dependency under the same name, the dependency with the
+ * wider scope is stored: {@link com.intellij.openapi.roots.DependencyScope#COMPILE} has wider scope than
+ * {@link com.intellij.openapi.roots.DependencyScope#TEST}.
+ * <p>
+ * It is not uncommon that the Android Gradle plug-in lists the same dependency as explicitly having both "compile" and "test" scopes. In
+ * IDEA there is no such distinction, a dependency with "compile" scope is also available to test code.
+ *
+ * @param dependency the dependency to add.
+ */
+ void add(@NotNull Dependency dependency) {
+ Dependency storedDependency = myDependenciesByName.get(dependency.getName());
+ if (storedDependency == null ||
+ SUPPORTED_SCOPES.indexOf(dependency.getScope()) < SUPPORTED_SCOPES.indexOf(storedDependency.getScope())) {
+ myDependenciesByName.put(dependency.getName(), dependency);
+ }
+ }
+
+ Collection<Dependency> getValues() {
+ return myDependenciesByName.values();
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/dependency/DependencyUpdater.java b/android/src/com/android/tools/idea/gradle/dependency/DependencyUpdater.java
new file mode 100644
index 0000000..f14a84f
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/dependency/DependencyUpdater.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.dependency;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collection;
+
+/**
+ * Updates an IDEA module's dependencies on artifacts (e.g. libraries and other IDEA modules.)
+ */
+public abstract class DependencyUpdater<T> {
+ public final void updateDependencies(@NotNull T module, @NotNull Collection<? extends Dependency> dependencies) {
+ for (Dependency dependency : dependencies) {
+ if (dependency instanceof LibraryDependency) {
+ updateDependency(module, (LibraryDependency)dependency);
+ }
+ else if (dependency instanceof ModuleDependency) {
+ updateDependency(module, (ModuleDependency)dependency);
+ }
+ else {
+ // This will NEVER happen.
+ String className = dependency == null ? "null" : dependency.getClass().getName();
+ throw new IllegalArgumentException("Unsupported dependency: " + className);
+ }
+ }
+ }
+
+ protected abstract void updateDependency(@NotNull T module, LibraryDependency dependency);
+
+ private void updateDependency(@NotNull T module, ModuleDependency dependency) {
+ if (!tryUpdating(module, dependency)) {
+ logModuleNotFound(module, dependency);
+ // fall back to library dependency, if available.
+ LibraryDependency backup = dependency.getBackupDependency();
+ if (backup != null) {
+ updateDependency(module, backup);
+ }
+ }
+ }
+
+ protected abstract boolean tryUpdating(@NotNull T module, @NotNull ModuleDependency dependency);
+
+ private void logModuleNotFound(@NotNull T module, @NotNull ModuleDependency dependency) {
+ String moduleName = getNameOf(module);
+ LibraryDependency backup = dependency.getBackupDependency();
+ String severity = backup != null ? "Warning" : "Error";
+ String category = String.format("%1$s(s) found while populating dependencies of module '%2$s'.", severity, moduleName);
+ String msg = String.format("Unable fo find module '%1$s'.", dependency.getName());
+ if (backup != null) {
+ msg += String.format(" Linking to library '%1$s' instead.", backup.getName());
+ }
+ log(module, category, msg);
+ }
+
+ @NotNull
+ protected abstract String getNameOf(@NotNull T module);
+
+ protected abstract void log(T module, String category, String msg);
+}
diff --git a/android/src/com/android/tools/idea/gradle/dependency/LibraryDependency.java b/android/src/com/android/tools/idea/gradle/dependency/LibraryDependency.java
new file mode 100644
index 0000000..b600495
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/dependency/LibraryDependency.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.dependency;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.intellij.openapi.roots.DependencyScope;
+import com.intellij.openapi.util.io.FileUtil;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * An IDEA module's dependency on a library (e.g. a jar file.)
+ */
+public class LibraryDependency extends Dependency {
+ @NotNull private final Map<PathType, Collection<String>> myPathsByType = Maps.newEnumMap(PathType.class);
+
+ /**
+ * Creates a new {@link LibraryDependency}.
+ *
+ * @param binaryPath the path, in the file system, of the binary file that represents the library to depend on.
+ * @param scope the scope of the dependency. Supported values are {@link DependencyScope#COMPILE} and {@link DependencyScope#TEST}.
+ * @throws IllegalArgumentException if the given scope is not supported.
+ */
+ @VisibleForTesting
+ public LibraryDependency(@NotNull File binaryPath, @NotNull DependencyScope scope) {
+ this(FileUtil.getNameWithoutExtension(binaryPath), scope);
+ addPath(PathType.BINARY, binaryPath);
+ }
+
+ /**
+ * Creates a new {@link LibraryDependency}. This constructor sets the scope to {@link DependencyScope#COMPILE}.
+ *
+ * @param name the name of the library to depend on.
+ */
+ LibraryDependency(@NotNull String name) {
+ super(name);
+ }
+
+ /**
+ * Creates a new {@link LibraryDependency}.
+ *
+ * @param name the name of the library to depend on.
+ * @param scope the scope of the dependency. Supported values are {@link DependencyScope#COMPILE} and {@link DependencyScope#TEST}.
+ * @throws IllegalArgumentException if the given scope is not supported.
+ */
+ LibraryDependency(@NotNull String name, @NotNull DependencyScope scope) {
+ super(name, scope);
+ }
+
+ void addPath(@NotNull PathType type, @NotNull File path) {
+ Collection<String> paths = myPathsByType.get(type);
+ if (paths == null) {
+ paths = Sets.newHashSet();
+ myPathsByType.put(type, paths);
+ }
+ paths.add(path.getPath());
+ }
+
+ @NotNull
+ public Collection<String> getPaths(@NotNull PathType type) {
+ Collection<String> paths = myPathsByType.get(type);
+ return paths == null ? Collections.<String>emptyList() : paths;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "[" +
+ "name='" + getName() + '\'' +
+ ", scope=" + getScope() +
+ ", pathsByType=" + myPathsByType +
+ "]";
+ }
+
+ public enum PathType {
+ BINARY, SOURCE, DOC
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/dependency/ModuleDependency.java b/android/src/com/android/tools/idea/gradle/dependency/ModuleDependency.java
new file mode 100644
index 0000000..01883f2
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/dependency/ModuleDependency.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.dependency;
+
+import com.android.SdkConstants;
+import com.google.common.annotations.VisibleForTesting;
+import com.intellij.openapi.roots.DependencyScope;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * An IDEA module's dependency on another IDEA module.
+ */
+public class ModuleDependency extends Dependency {
+ @NotNull private final String myGradlePath;
+
+ @Nullable private LibraryDependency myBackupDependency;
+
+ /**
+ * Creates a new {@link ModuleDependency}.
+ *
+ * @param gradlePath the Gradle path of the project that maps to the IDEA module to depend on.
+ * @param scope the scope of the dependency. Supported values are {@link DependencyScope#COMPILE} and {@link DependencyScope#TEST}.
+ * @throws IllegalArgumentException if the given scope is not supported.
+ */
+ @VisibleForTesting
+ public ModuleDependency(@NotNull String gradlePath, @NotNull DependencyScope scope) {
+ this(extractModuleName(gradlePath), gradlePath, scope);
+ }
+
+ @NotNull
+ private static String extractModuleName(@NotNull String gradlePath) {
+ String[] pathSegments = gradlePath.split(SdkConstants.GRADLE_PATH_SEPARATOR);
+ return pathSegments[pathSegments.length - 1];
+ }
+
+ /**
+ * Creates a new {@link ModuleDependency}. This constructor sets the scope to {@link DependencyScope#COMPILE}.
+ *
+ * @param moduleName the name of the IDEA module to depend on.
+ * @param gradlePath the Gradle path of the project that maps to the IDEA module to depend on.
+ * @param scope the scope of the dependency. Supported values are {@link DependencyScope#COMPILE} and {@link DependencyScope#TEST}.
+ * @throws IllegalArgumentException if the given scope is not supported.
+ */
+ ModuleDependency(@NotNull String moduleName, @NotNull String gradlePath, @NotNull DependencyScope scope) {
+ super(moduleName, scope);
+ myGradlePath = gradlePath;
+ }
+
+ @NotNull
+ public String getGradlePath() {
+ return myGradlePath;
+ }
+
+ /**
+ * @return the backup library that can be used as dependency in case it is not possible to use the module dependency (e.g. the module is
+ * outside the project and we don't have the path of the module folder.)
+ */
+ @Nullable
+ public LibraryDependency getBackupDependency() {
+ return myBackupDependency;
+ }
+
+ @VisibleForTesting
+ public void setBackupDependency(@Nullable LibraryDependency backupDependency) {
+ myBackupDependency = backupDependency;
+ updateBackupDependencyScope();
+ }
+
+ /**
+ * Sets the scope of this dependency. It also updates the scope of this dependency's backup dependency if it is not {@code null}.
+ *
+ * @param scope the scope of the dependency. Supported values are {@link DependencyScope#COMPILE} and {@link DependencyScope#TEST}.
+ * @throws IllegalArgumentException if the given scope is not supported.
+ */
+ @Override
+ void setScope(@NotNull DependencyScope scope) throws IllegalArgumentException {
+ super.setScope(scope);
+ updateBackupDependencyScope();
+ }
+
+ private void updateBackupDependencyScope() {
+ if (myBackupDependency != null) {
+ myBackupDependency.setScope(getScope());
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "[" +
+ "name='" + getName() + '\'' +
+ ", gradlePath=" + myGradlePath +
+ ", scope=" + getScope() +
+ ", backUpDependency=" + myBackupDependency +
+ "]";
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/model/AndroidDependencies.java b/android/src/com/android/tools/idea/gradle/model/AndroidDependencies.java
deleted file mode 100644
index c827955..0000000
--- a/android/src/com/android/tools/idea/gradle/model/AndroidDependencies.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tools.idea.gradle.model;
-
-import com.android.build.gradle.model.*;
-import com.android.builder.model.AndroidLibrary;
-import com.android.tools.idea.gradle.IdeaAndroidProject;
-import com.intellij.openapi.roots.DependencyScope;
-import com.intellij.openapi.util.io.FileUtil;
-import org.jetbrains.annotations.NotNull;
-
-import java.io.File;
-
-/**
- * Configures a module's dependencies from an {@link AndroidProject}.
- */
-public final class AndroidDependencies {
- private AndroidDependencies() {
- }
-
- /**
- * Populates the dependencies of a module based on the given {@link AndroidProject}.
- *
- * @param androidProject structure of the Android-Gradle project.
- * @param dependencyFactory creates and adds dependencies to a module.
- */
- public static void populate(@NotNull IdeaAndroidProject androidProject, @NotNull DependencyFactory dependencyFactory) {
- AndroidProject delegate = androidProject.getDelegate();
-
- Variant selectedVariant = androidProject.getSelectedVariant();
- for (String flavorName : selectedVariant.getProductFlavors()) {
- ProductFlavorContainer productFlavor = delegate.getProductFlavors().get(flavorName);
- populateDependencies(productFlavor, dependencyFactory);
- }
-
- ProductFlavorContainer defaultConfig = delegate.getDefaultConfig();
- populateDependencies(defaultConfig, dependencyFactory);
-
- String buildTypeName = selectedVariant.getBuildType();
- BuildTypeContainer buildType = delegate.getBuildTypes().get(buildTypeName);
- if (buildType != null) {
- populateDependencies(DependencyScope.COMPILE, buildType.getDependency(), dependencyFactory);
- }
- }
-
- private static void populateDependencies(@NotNull ProductFlavorContainer productFlavor, @NotNull DependencyFactory dependencyFactory) {
- populateDependencies(DependencyScope.COMPILE, productFlavor.getDependencies(), dependencyFactory);
- populateDependencies(DependencyScope.TEST, productFlavor.getTestDependencies(), dependencyFactory);
- }
-
- private static void populateDependencies(@NotNull DependencyScope scope,
- @NotNull Dependencies dependencies,
- @NotNull DependencyFactory dependencyFactory) {
- for (File jar : dependencies.getJars()) {
- addDependency(scope, dependencyFactory, jar);
- }
- for (AndroidLibrary lib : dependencies.getLibraries()) {
- File jar = lib.getJarFile();
- File parentFile = jar.getParentFile();
- String name = parentFile != null ? parentFile.getName() : FileUtil.getNameWithoutExtension(jar);
- dependencyFactory.addDependency(scope, name, jar);
- for (File localJar : lib.getLocalJars()) {
- addDependency(scope, dependencyFactory, localJar);
- }
- }
- }
-
- private static void addDependency(@NotNull DependencyScope scope, @NotNull DependencyFactory dependencyFactory, @NotNull File jar) {
- dependencyFactory.addDependency(scope, FileUtil.getNameWithoutExtension(jar), jar);
- }
-
- /**
- * Adds a new dependency to a module.
- */
- public interface DependencyFactory {
- /**
- * Adds a new dependency to a module.
- *
- * @param scope scope of the dependency.
- * @param name name of the dependency.
- * @param binaryPath absolute path of the dependency's jar file.
- */
- void addDependency(@NotNull DependencyScope scope, @NotNull String name, @NotNull File binaryPath);
- }
-}
diff --git a/android/src/com/android/tools/idea/gradle/parser/GradleBuildFile.java b/android/src/com/android/tools/idea/gradle/parser/GradleBuildFile.java
new file mode 100644
index 0000000..e5e961f
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/parser/GradleBuildFile.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.parser;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElement;
+import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral;
+import org.jetbrains.plugins.groovy.lang.psi.api.util.GrStatementOwner;
+
+/**
+ * GradleBuildFile uses PSI to parse build.gradle files and provides high-level methods to read and mutate the file. For many things in
+ * the file it uses a simple key/value interface to set and retrieve values. Since a user can potentially edit a build.gradle file by
+ * hand and make changes that we are unable to parse, there is also a
+ * {@link #canParseValue(com.android.tools.idea.gradle.parser.GradleBuildFile.BuildSettingKey)} method that will query if the value can
+ * be edited by this class or not.
+ *
+ * Note that if you do any mutations on the PSI structure you must be inside a write action. See
+ * {@link com.intellij.util.ActionRunner#runInsideWriteAction}.
+ */
+public class GradleBuildFile extends GradleGroovyFile {
+ public static final String GRADLE_PLUGIN_CLASSPATH = "com.android.tools.build:gradle:";
+
+ /**
+ * BuildSettingKey enumerates the values we know how to parse out of the build file. This includes values that only occur in one place
+ * and are always rooted at the file root (e.g. android/buildToolsVersion) and values that can occur at different places (e.g.
+ * signingConfig, which can occur in defaultConfig, a build type, or a flavor. When retrieving keys that are of the former type, you
+ * can call {@link #getValue(com.android.tools.idea.gradle.parser.GradleBuildFile.BuildSettingKey)}, which uses the build file itself as
+ * the root; in the case of the latter, call
+ * {@link #getValue(org.jetbrains.plugins.groovy.lang.psi.api.util.GrStatementOwner, com.android.tools.idea.gradle.parser.GradleBuildFile.BuildSettingKey)}
+ * and pass in the block that is the root of the key's path.
+ */
+ public enum BuildSettingKey {
+ // Buildscript block
+ PLUGIN_CLASSPATH("buildscript/dependencies/classpath"),
+ PLUGIN_REPOSITORY("buildscript/repositories"), // TODO: Implement properly. This is not a simple literal.
+ PLUGIN_VERSION("buildscript/dependencies/classpath") {
+ @Override
+ public Object getValue(@NotNull GroovyPsiElement[] args) {
+ String s = (String)PLUGIN_CLASSPATH.getValue(args);
+ if (s != null && s.startsWith(GRADLE_PLUGIN_CLASSPATH)) {
+ return s.substring(GRADLE_PLUGIN_CLASSPATH.length());
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void setValue(@NotNull Project project, @NotNull GroovyPsiElement[] args, @NotNull Object value) {
+ PLUGIN_CLASSPATH.setValue(project, args, GRADLE_PLUGIN_CLASSPATH + value);
+ }
+ },
+
+ // Repositories block
+ // TODO: Implement
+
+ // Dependencies block
+ // TODO: Implement
+
+ // Android block
+ BUILD_TOOLS_VERSION("android/buildToolsVersion"),
+ COMPILE_SDK_VERSION("android/compileSdkVersion"),
+ IGNORE_ASSETS_PATTERN("android/aaptOptions/ignoreAssetsPattern"),
+ INCREMENTAL("android/dexOptions/incremental"),
+ NO_COMPRESS("android/aaptOptions/noCompress"), // TODO: Implement properly. This is not a simple literal.
+ SOURCE_COMPATIBILITY("android/compileOptions/sourceCompatibility"), // TODO: Does this work? This is an assignment, not a method call.
+ TARGET_COMPATIBILITY("android/compileOptions/targetCompatibility"), // TODO: Does this work? This is an assignment, not a method call.
+
+ // defaultConfig or build flavor
+ MIN_SDK_VERSION("minSdkVersion"),
+ PACKAGE_NAME("packageName"),
+ PROGUARD_FILE("proguardFile"),
+ SIGNING_CONFIG("signingConfig"), // TODO: Implement properly. This is not a simple literal.
+ TARGET_SDK_VERSION("targetSdkVersion"),
+ TEST_INSTRUMENTATION_RUNNER("testInstrumentationRunner"),
+ TEST_PACKAGE_NAME("testPackageName"),
+ VERSION_CODE("versionCode"),
+ VERSION_NAME("versionName"),
+
+ // Build type
+ DEBUGGABLE("debuggable"),
+ JNI_DEBUG_BUILD("jniDebugBuild"),
+ RENDERSCRIPT_DEBUG_BUILD("renderscriptDebugBuild"),
+ RENDERSCRIPT_OPTIM_LEVEL("renderscriptOptimLevel"),
+ RUN_PROGUARD("runProguard"),
+ PACKAGE_NAME_SUFFIX("packageNameSuffix"),
+ VERSION_NAME_SUFFIX("versionNameSuffix"),
+ ZIP_ALIGN("zipAlign"),
+
+ // Signing config
+ KEY_ALIAS("keyAlias"),
+ KEY_PASSWORD("keyPassword"),
+ STORE_FILE("storeFile"),
+ STORE_PASSWORD("storePassword");
+
+ private final String myPath;
+
+ BuildSettingKey(@NotNull String path) {
+ myPath = path;
+ }
+
+ protected boolean canParseValue(@NotNull GroovyPsiElement[] args) {
+ if (args.length != 1) {
+ return false;
+ }
+ return args[0] != null && args[0] instanceof GrLiteral;
+ }
+
+ protected @Nullable Object getValue(@NotNull GroovyPsiElement[] args) {
+ if (!canParseValue(args)) {
+ return null;
+ }
+ return ((GrLiteral) args[0]).getValue();
+ }
+
+ protected void setValue(@NotNull Project project, @NotNull GroovyPsiElement[] args, @NotNull Object value) {
+ // TODO: create path to the value if it's not there
+ if (canParseValue(args)) {
+ GrLiteral literal;
+ GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(project);
+ if (value instanceof String || value instanceof Boolean) {
+ literal = factory.createLiteralFromValue(value);
+ } else {
+ // e.g. Integer
+ literal = (GrLiteral)factory.createExpressionFromText(value.toString());
+ }
+ args[0].replace(literal);
+ }
+ }
+ }
+
+ public GradleBuildFile(@NotNull VirtualFile buildFile, @NotNull Project project) {
+ super(buildFile, project);
+ }
+
+ /**
+ * @return true if the build file has a value for this key that we know how to safely parse and modify; false if it has user modifications
+ * and should be left alone.
+ */
+ public boolean canParseValue(@NotNull BuildSettingKey key) {
+ checkInitialized();
+ return canParseValue(myGroovyFile, key);
+ }
+
+ /**
+ * @return true if the build file has a value for this key that we know how to safely parse and modify; false if it has user modifications
+ * and should be left alone.
+ */
+ public boolean canParseValue(@NotNull GrStatementOwner root, @NotNull BuildSettingKey key) {
+ checkInitialized();
+ GrMethodCall method = getMethodCallByPath(root, key.myPath);
+ if (method == null) {
+ return false;
+ }
+ return key.canParseValue(getArguments(method));
+ }
+
+ /**
+ * Returns the value in the file for the given key, or null if not present.
+ */
+ public @Nullable Object getValue(@NotNull BuildSettingKey key) {
+ checkInitialized();
+ return getValue(myGroovyFile, key);
+ }
+
+ /**
+ * Returns the value in the file for the given key, or null if not present.
+ */
+ public @Nullable Object getValue(@NotNull GrStatementOwner root, @NotNull BuildSettingKey key) {
+ checkInitialized();
+ GrMethodCall method = getMethodCallByPath(root, key.myPath);
+ if (method == null) {
+ return null;
+ }
+ return key.getValue(getArguments(method));
+ }
+
+ /**
+ * Given a path to a method, returns the first argument of that method that is a closure, or null.
+ */
+ public @Nullable GrStatementOwner getClosure(String path) {
+ checkInitialized();
+ GrMethodCall method = getMethodCallByPath(myGroovyFile, path);
+ if (method == null) {
+ return null;
+ }
+ return getMethodClosureArgument(method);
+ }
+
+ /**
+ * Modifies the value in the file. Must be run inside a write action.
+ */
+ public void setValue(@NotNull BuildSettingKey key, @NotNull Object value) {
+ checkInitialized();
+ commitDocumentChanges();
+ setValue(myGroovyFile, key, value);
+ }
+
+ /**
+ * Modifies the value in the file. Must be run inside a write action.
+ */
+ public void setValue(@NotNull GrStatementOwner root, @NotNull BuildSettingKey key, @NotNull Object value) {
+ checkInitialized();
+ commitDocumentChanges();
+ GrMethodCall method = getMethodCallByPath(root, key.myPath);
+ if (method != null) {
+ key.setValue(myProject, getArguments(method), value);
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/parser/GradleGroovyFile.java b/android/src/com/android/tools/idea/gradle/parser/GradleGroovyFile.java
new file mode 100644
index 0000000..d62a0ea
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/parser/GradleGroovyFile.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.parser;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.startup.StartupManager;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.*;
+import com.intellij.psi.impl.source.tree.LeafPsiElement;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.groovy.lang.psi.GroovyFile;
+import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElement;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral;
+import org.jetbrains.plugins.groovy.lang.psi.api.util.GrStatementOwner;
+
+import java.util.Arrays;
+
+/**
+ * Base class for classes that parse Gradle Groovy files (e.g. settings.gradle, build.gradle). It provides a number of convenience
+ * methods for its subclasses to extract interesting pieces from Gradle files.
+ *
+ * Note that if you do any mutations on the PSI structure you must be inside a write action. See
+ * {@link com.intellij.util.ActionRunner#runInsideWriteAction}.
+ */
+class GradleGroovyFile {
+ private static final Logger LOG = Logger.getInstance(GradleGroovyFile.class);
+ protected static final GroovyPsiElement[] EMPTY_ELEMENT_ARRAY = new GroovyPsiElement[0];
+ protected static final Iterable<GrLiteral> EMPTY_LITERAL_ITERABLE = Arrays.asList(new GrLiteral[0]);
+
+ protected final Project myProject;
+ protected final VirtualFile myFile;
+ protected GroovyFile myGroovyFile = null;
+
+ public GradleGroovyFile(@NotNull VirtualFile file, @NotNull Project project) {
+ myProject = project;
+ myFile = file;
+ StartupManager.getInstance(project).runWhenProjectIsInitialized(new Runnable() {
+ @Override
+ public void run() {
+ PsiFile psiFile = PsiManager.getInstance(myProject).findFile(myFile);
+ if (psiFile == null) {
+ LOG.warn("Could not find PsiFile for " + myFile.getPath());
+ return;
+ }
+ if (!(psiFile instanceof GroovyFile)) {
+ LOG.warn("PsiFile " + psiFile.getName() + " is not a Groovy file");
+ return;
+ }
+ myGroovyFile = (GroovyFile)psiFile;
+ onPsiFileAvailable();
+ }
+ });
+ }
+
+ public VirtualFile getFile() {
+ return myFile;
+ }
+
+ /**
+ * @throws IllegalStateException if the instance has not parsed its PSI file yet in a
+ * {@link StartupManager#runWhenProjectIsInitialized(Runnable)} callback. To resolve this, wait until {@link #onPsiFileAvailable()}
+ * is called before invoking methods.
+ */
+ protected void checkInitialized() {
+ if (myGroovyFile == null) {
+ throw new IllegalStateException("PsiFile not parsed for file " + myFile.getPath() +". Wait until onPsiFileAvailable() is called.");
+ }
+ }
+
+ /**
+ * Commits any {@link Document} changes outstanding to the document that underlies the PSI representation. Call this before manipulating
+ * PSI to ensure the PSI model of the document is in sync with what's on disk. Must be run inside a write action.
+ */
+ protected void commitDocumentChanges() {
+ PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myProject);
+ Document document = documentManager.getDocument(myGroovyFile);
+ if (document != null) {
+ documentManager.commitDocument(document);
+ }
+ }
+
+ /**
+ * Finds a method identified by the given path. It descends into nested closure arguments of method calls to find the leaf method.
+ * For example, if you have this code in Groovy:
+ *
+ * method_a {
+ * method_b {
+ * method_c 'literal'
+ * }
+ * }
+ *
+ * you can find method_c using the path "method_a/method_b/method_c"
+ *
+ * It returns the first eligible result. If you have code like this:
+ *
+ * method_a {
+ * method_b 'literal1'
+ * method_b 'literal2'
+ * }
+ *
+ * a search for "method_a/method_b" will only return the first invocation of method_b.
+ *
+ * It continues searching until it has exhausted all possibilities of finding the path. If you have code like this:
+ *
+ * method_a {
+ * method_b 'literal1'
+ * }
+ *
+ * method_a {
+ * method_c 'literal2'
+ * }
+ *
+ * a search for "method_a/method_c" will succeed: it will not give up just because it doesn't find it in the first method_a block.
+ *
+ * @param root the block to use as the root of the path
+ * @param path the slash-delimited chain of methods with closure arguments to navigate to find the final leaf.
+ * @return the resultant method, or null if it could not be found.
+ */
+ protected static @Nullable GrMethodCall getMethodCallByPath(@NotNull GrStatementOwner root, @NotNull String path) {
+ if (path.isEmpty() || path.endsWith("/")) {
+ return null;
+ }
+ int slash = path.indexOf('/');
+ String pathElement = slash == -1 ? path : path.substring(0, slash);
+ for (GrMethodCall gmc : getMethodCalls(root, pathElement)) {
+ if (slash == -1) {
+ return gmc;
+ }
+ if (gmc == null) {
+ return null;
+ }
+ GrClosableBlock[] blocks = gmc.getClosureArguments();
+ if (blocks.length != 1) {
+ return null;
+ }
+ GrMethodCall subresult = getMethodCallByPath(blocks[0], path.substring(slash + 1));
+ if (subresult != null) {
+ return subresult;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns an array of arguments for the given method call. Note that it returns only regular arguments (via the
+ * {@link org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall#getArgumentList()} call), not closure arguments
+ * (via the {@link org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall#getClosureArguments()} call).
+ *
+ * @return the array of arguments. This method never returns null; even in the case of a nonexistent argument list or error, it will
+ * return an empty array.
+ */
+ protected static @NotNull GroovyPsiElement[] getArguments(@NotNull GrMethodCall gmc) {
+ GrArgumentList argList = gmc.getArgumentList();
+ if (argList == null) {
+ return EMPTY_ELEMENT_ARRAY;
+ }
+ return argList.getAllArguments();
+ }
+
+ /**
+ * Returns the first method call of the given method name in the given parent statement block, or null if one could not be found.
+ */
+ protected static @Nullable GrMethodCall getMethodCall(@NotNull GrStatementOwner parent, @NotNull String methodName) {
+ return Iterables.getFirst(getMethodCalls(parent, methodName), null);
+ }
+
+ /**
+ * Returns all statements in the parent statement block that are method calls.
+ */
+ protected static @NotNull Iterable<GrMethodCall> getMethodCalls(@NotNull GrStatementOwner parent) {
+ return Iterables.filter(Arrays.asList(parent.getStatements()), GrMethodCall.class);
+ }
+
+ /**
+ * Returns all statements in the parent statement block that are method calls with the given method name
+ */
+ protected static @NotNull Iterable<GrMethodCall> getMethodCalls(@NotNull GrStatementOwner parent, @NotNull final String methodName) {
+ return Iterables.filter(getMethodCalls(parent), new Predicate<GrMethodCall>() {
+ @Override
+ public boolean apply(@Nullable GrMethodCall input) {
+ return input != null && methodName.equals(getMethodCallName(input));
+ }
+ });
+ }
+
+ /**
+ * Returns the name of the given method call
+ */
+ protected static @NotNull String getMethodCallName(@NotNull GrMethodCall gmc) {
+ return (gmc.getInvokedExpression() != null && gmc.getInvokedExpression().getText() != null) ? gmc.getInvokedExpression().getText() : "";
+ }
+
+ /**
+ * Returns all arguments in the given argument list that are literals.
+ */
+ protected static @NotNull Iterable<GrLiteral> getLiteralArguments(@NotNull GrArgumentList args) {
+ return Iterables.filter(Arrays.asList(args.getAllArguments()), GrLiteral.class);
+ }
+
+ /**
+ * Returns all arguments of the given method call that are literals.
+ */
+ protected static @NotNull Iterable<GrLiteral> getLiteralArguments(@NotNull GrMethodCall gmc) {
+ GrArgumentList argumentList = gmc.getArgumentList();
+ if (argumentList == null) {
+ return EMPTY_LITERAL_ITERABLE;
+ }
+ return getLiteralArguments(argumentList);
+ }
+
+ /**
+ * Returns values of all literal-typed arguments of the given method call.
+ */
+ protected static @NotNull Iterable<Object> getLiteralArgumentValues(@NotNull GrMethodCall gmc) {
+ return Iterables.transform(getLiteralArguments(gmc), new Function<GrLiteral, Object>() {
+ @Override
+ public Object apply(@Nullable GrLiteral input) {
+ return (input != null) ? input.getValue() : null;
+ }
+ });
+ }
+
+ /**
+ * Returns the value of the first literal argument in the given method call's argument list.
+ */
+ protected static @Nullable Object getFirstLiteralArgumentValue(@NotNull GrMethodCall gmc) {
+ GrLiteral lit = getFirstLiteralArgument(gmc);
+ return lit != null ? lit.getValue() : null;
+ }
+
+ /**
+ * Returns the first literal argument in the given method call's argument list.
+ */
+ protected static @Nullable GrLiteral getFirstLiteralArgument(@NotNull GrMethodCall gmc) {
+ return Iterables.getFirst(getLiteralArguments(gmc), null);
+ }
+
+ /**
+ * Returns the first argument of the method call with the given name in the given parent that is a closure, or null if the method
+ * or its closure could not be found.
+ */
+ protected static @Nullable GrClosableBlock getMethodClosureArgument(@NotNull GrStatementOwner parent, @NotNull String methodName) {
+ GrMethodCall methodCall = getMethodCall(parent, methodName);
+ if (methodCall == null) {
+ return null;
+ }
+ return getMethodClosureArgument(methodCall);
+ }
+
+ /**
+ * Returns the first argument of the given method call that is a closure, or null if the closure could not be found.
+ */
+ protected static GrClosableBlock getMethodClosureArgument(@NotNull GrMethodCall methodCall) {
+ return Iterables.getFirst(Arrays.asList(methodCall.getClosureArguments()), null);
+ }
+
+ /**
+ * Override this method if you wish to be called when the underlying PSI file is available and you would like to parse it.
+ */
+ protected void onPsiFileAvailable() {
+ }
+
+ @Override
+ public @NotNull String toString() {
+ if (myGroovyFile == null) {
+ return "<uninitialized>";
+ } else {
+ ToStringPsiVisitor visitor = new ToStringPsiVisitor();
+ myGroovyFile.accept(visitor);
+ return myFile.getPath() + ":\n" + visitor.toString();
+ }
+ }
+
+ private static class ToStringPsiVisitor extends PsiRecursiveElementVisitor {
+ private StringBuilder myString = new StringBuilder();
+ @Override
+ public void visitElement(final @NotNull PsiElement element) {
+ PsiElement e = element;
+ while (e.getParent() != null) {
+ myString.append(" ");
+ e = e.getParent();
+ }
+ myString.append(element.getClass().getName());
+ myString.append(": ");
+ myString.append(element.toString());
+ if (element instanceof LeafPsiElement) {
+ myString.append(" ");
+ myString.append(element.getText());
+ }
+ myString.append("\n");
+ super.visitElement(element);
+ }
+
+ public @NotNull String toString() {
+ return myString.toString();
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/parser/GradleSettingsFile.java b/android/src/com/android/tools/idea/gradle/parser/GradleSettingsFile.java
new file mode 100644
index 0000000..c505bd9
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/parser/GradleSettingsFile.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.parser;
+
+import com.android.tools.idea.gradle.facet.AndroidGradleFacet;
+import com.android.tools.idea.gradle.util.Facets;
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrStatement;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral;
+
+import java.util.Arrays;
+
+/**
+ * GradleSettingsFile uses PSI to parse settings.gradle files and provides high-level methods to read and mutate the file.
+ *
+ * Note that if you do any mutations on the PSI structure you must be inside a write action. See
+ * {@link com.intellij.util.ActionRunner#runInsideWriteAction}.
+ */
+public class GradleSettingsFile extends GradleGroovyFile {
+
+ public static final String INCLUDE_METHOD = "include";
+ private static final Iterable<String> EMPTY_ITERABLE = Arrays.asList(new String[] {});
+
+ public GradleSettingsFile(@NotNull VirtualFile file, @NotNull Project project) {
+ super(file, project);
+ }
+
+ /**
+ * Adds a reference to the module to the settings file, if there is not already one. Must be run inside a write action.
+ */
+ public void addModule(@NotNull Module module) {
+ checkInitialized();
+ String moduleGradlePath = getModuleGradlePath(module);
+ if (moduleGradlePath != null) {
+ addModule(moduleGradlePath);
+ }
+ }
+
+ /**
+ * Adds a reference to the module to the settings file, if there is not already one. The module path must be colon separated, with a
+ * leading colon, e.g. ":project:subproject". Must be run inside a write action.
+ */
+ public void addModule(@NotNull String modulePath) {
+ checkInitialized();
+ commitDocumentChanges();
+ for (GrMethodCall includeStatement : getMethodCalls(myGroovyFile, INCLUDE_METHOD)) {
+ for (GrLiteral lit : getLiteralArguments(includeStatement)) {
+ if (modulePath.equals(lit.getValue())) {
+ return;
+ }
+ }
+ }
+ GrMethodCall includeStatement = getMethodCall(myGroovyFile, INCLUDE_METHOD);
+ if (includeStatement != null) {
+ GrArgumentList argList = includeStatement.getArgumentList();
+ if (argList != null) {
+ GrLiteral literal = GroovyPsiElementFactory.getInstance(myProject).createLiteralFromValue(modulePath);
+ argList.addAfter(literal, argList.getLastChild());
+ return;
+ }
+ }
+ GrStatement statement =
+ GroovyPsiElementFactory.getInstance(myProject).createStatementFromText(INCLUDE_METHOD + " '" + modulePath + "'");
+ myGroovyFile.add(statement);
+ }
+
+ /**
+ * Removes the reference to the module from the settings file, if present. Must be run inside a write action.
+ */
+ public void removeModule(@NotNull Module module) {
+ checkInitialized();
+ removeModule(getModuleGradlePath(module));
+ }
+
+ /**
+ * Removes the reference to the module from the settings file, if present. The module path must be colon separated, with a
+ * leading colon, e.g. ":project:subproject". Must be run inside a write action.
+ */
+ public void removeModule(String modulePath) {
+ checkInitialized();
+ commitDocumentChanges();
+ for (GrMethodCall includeStatement : getMethodCalls(myGroovyFile, INCLUDE_METHOD)) {
+ for (GrLiteral lit : getLiteralArguments(includeStatement)) {
+ if (modulePath.equals(lit.getValue())) {
+ lit.delete();
+ if (getArguments(includeStatement).length == 0) {
+ includeStatement.delete();
+ // If this happens we will fall through both for loops before we get into iteration trouble. We want to keep iterating in
+ // case the module is added more than once (via hand-editing of the file).
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns all of the literal-typed arguments of all include statements in the file.
+ */
+ public Iterable<String> getModules() {
+ checkInitialized();
+ return Iterables.concat(Iterables.transform(getMethodCalls(myGroovyFile, INCLUDE_METHOD),
+ new Function<GrMethodCall, Iterable<String>>() {
+ @Override
+ public Iterable<String> apply(@Nullable GrMethodCall input) {
+ if (input != null) {
+ return Iterables.transform(getLiteralArgumentValues(input), new Function<Object, String>() {
+ @Override
+ public String apply(@Nullable Object input) {
+ return input != null ? input.toString() : null;
+ }
+ });
+ } else {
+ return EMPTY_ITERABLE;
+ }
+ }
+ }));
+ }
+
+ @Nullable
+ private static String getModuleGradlePath(@NotNull Module module) {
+ AndroidGradleFacet androidGradleFacet = Facets.getFirstFacetOfType(module, AndroidGradleFacet.TYPE_ID);
+ if (androidGradleFacet == null) {
+ return null;
+ }
+ return androidGradleFacet.getConfiguration().GRADLE_PROJECT_PATH;
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/model/AndroidContentRoot.java b/android/src/com/android/tools/idea/gradle/project/AndroidContentRoot.java
similarity index 84%
rename from android/src/com/android/tools/idea/gradle/model/AndroidContentRoot.java
rename to android/src/com/android/tools/idea/gradle/project/AndroidContentRoot.java
index ded4415..51b4c71 100644
--- a/android/src/com/android/tools/idea/gradle/model/AndroidContentRoot.java
+++ b/android/src/com/android/tools/idea/gradle/project/AndroidContentRoot.java
@@ -13,33 +13,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.tools.idea.gradle.model;
+package com.android.tools.idea.gradle.project;
-import com.android.build.gradle.model.AndroidProject;
-import com.android.build.gradle.model.BuildTypeContainer;
-import com.android.build.gradle.model.ProductFlavorContainer;
-import com.android.build.gradle.model.Variant;
-import com.android.builder.model.SourceProvider;
+import com.android.builder.model.*;
import com.android.tools.idea.gradle.IdeaAndroidProject;
-import com.google.common.collect.ImmutableList;
import com.intellij.openapi.externalSystem.model.project.ExternalSystemSourceType;
import com.intellij.openapi.util.io.FileUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
-import java.util.List;
import java.util.Map;
/**
* Configures a module's content root from an {@link AndroidProject}.
*/
public final class AndroidContentRoot {
- private static final String OUTPUT_DIR_NAME = "build";
+ public static final String BUILD_DIR = "build";
// TODO: Retrieve this information from Gradle.
private static final String[] EXCLUDED_OUTPUT_DIR_NAMES =
- {"apk", "assets", "bundles", "classes", "dependency-cache", "exploded-bundles", "incremental", "libs", "manifests", "symbols", "tmp"};
+ // Note that build/exploded-bundles should *not* be excluded
+ {"apk", "assets", "bundles", "classes", "dependency-cache", "incremental", "libs", "manifests", "symbols", "tmp"};
private AndroidContentRoot() {
}
@@ -77,10 +72,20 @@
}
private static void storePaths(@NotNull Variant variant, @NotNull ContentRootStorage storage) {
- storePaths(ExternalSystemSourceType.SOURCE, variant.getGeneratedSourceFolders(), storage);
- storePaths(ExternalSystemSourceType.SOURCE, variant.getGeneratedResourceFolders(), storage);
- storePaths(ExternalSystemSourceType.TEST, variant.getGeneratedTestSourceFolders(), storage);
- storePaths(ExternalSystemSourceType.TEST, variant.getGeneratedTestResourceFolders(), storage);
+ ArtifactInfo mainArtifactInfo = variant.getMainArtifactInfo();
+ storePaths(ExternalSystemSourceType.SOURCE, mainArtifactInfo, storage);
+
+ ArtifactInfo testArtifactInfo = variant.getTestArtifactInfo();
+ if (testArtifactInfo != null) {
+ storePaths(ExternalSystemSourceType.TEST, testArtifactInfo, storage);
+ }
+ }
+
+ private static void storePaths(@NotNull ExternalSystemSourceType sourceType,
+ @NotNull ArtifactInfo artifactInfo,
+ @NotNull ContentRootStorage storage) {
+ storePaths(sourceType, artifactInfo.getGeneratedSourceFolders(), storage);
+ storePaths(sourceType, artifactInfo.getGeneratedResourceFolders(), storage);
}
private static void storePaths(@NotNull ProductFlavorContainer flavor, @NotNull ContentRootStorage storage) {
@@ -118,7 +123,7 @@
exclude(child, storage);
}
}
- File outputDir = new File(rootDir, OUTPUT_DIR_NAME);
+ File outputDir = new File(rootDir, BUILD_DIR);
for (String childName : EXCLUDED_OUTPUT_DIR_NAMES) {
File child = new File(outputDir, childName);
storage.storePath(ExternalSystemSourceType.EXCLUDED, child);
@@ -129,7 +134,6 @@
String name = dir.getName();
if (name.startsWith(".")) {
storage.storePath(ExternalSystemSourceType.EXCLUDED, dir);
- return;
}
}
diff --git a/android/src/com/android/tools/idea/gradle/project/AndroidGradleProjectComponent.java b/android/src/com/android/tools/idea/gradle/project/AndroidGradleProjectComponent.java
new file mode 100644
index 0000000..a6e253f
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/project/AndroidGradleProjectComponent.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.project;
+
+import com.android.tools.idea.gradle.GradleImportNotificationListener;
+import com.android.tools.idea.gradle.util.Projects;
+import com.android.tools.idea.gradle.variant.view.BuildVariantView;
+import com.google.common.collect.Lists;
+import com.intellij.ProjectTopics;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.compiler.CompileContext;
+import com.intellij.openapi.compiler.CompileTask;
+import com.intellij.openapi.compiler.CompilerManager;
+import com.intellij.openapi.components.AbstractProjectComponent;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.externalSystem.model.ExternalSystemDataKeys;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.project.ModuleListener;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.util.Function;
+import com.intellij.util.messages.MessageBusConnection;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class AndroidGradleProjectComponent extends AbstractProjectComponent {
+ private static final Logger LOG = Logger.getInstance(AndroidGradleProjectComponent.class);
+
+ @Nullable private Disposable myDisposable;
+
+ public AndroidGradleProjectComponent(Project project) {
+ super(project);
+ // Register a task that refreshes Studio's view of the file system after a compile.
+ // This is necessary for Studio to see generated code.
+ CompilerManager.getInstance(project).addAfterTask(new CompileTask() {
+ @Override
+ public boolean execute(CompileContext context) {
+ Project contextProject = context.getProject();
+ if (Projects.isGradleProject(contextProject)) {
+ String rootDirPath = contextProject.getBasePath();
+ VirtualFile rootDir = LocalFileSystem.getInstance().findFileByPath(rootDirPath);
+ if (rootDir != null && rootDir.isDirectory()) {
+ rootDir.refresh(true, true);
+ }
+ }
+ return true;
+ }
+ });
+ }
+
+ /**
+ * This method is called when a project is created and when it is opened.
+ */
+ @Override
+ public void projectOpened() {
+ if (Projects.isGradleProject(myProject)) {
+ configureGradleProject(true);
+ }
+ }
+
+ public void configureGradleProject(boolean reImportProject) {
+ if (myDisposable != null) {
+ return;
+ }
+ myDisposable = new Disposable() {
+ @Override
+ public void dispose() {
+ }
+ };
+
+ listenForProjectChanges(myProject, myDisposable);
+
+ GradleImportNotificationListener.attachToManager();
+ Projects.ensureExternalBuildIsEnabledForGradleProject(myProject);
+
+ if (reImportProject) {
+ Projects.setProjectBuildAction(myProject, Projects.BuildAction.SOURCE_GEN);
+ try {
+ // Prevent IDEA from refreshing project. We want to do it ourselves.
+ myProject.putUserData(ExternalSystemDataKeys.NEWLY_IMPORTED_PROJECT, Boolean.TRUE);
+
+ GradleProjectImporter.getInstance().reImportProject(myProject, null);
+ }
+ catch (ConfigurationException e) {
+ Messages.showErrorDialog(e.getMessage(), e.getTitle());
+ LOG.info(e);
+ }
+ }
+ }
+
+ private static void listenForProjectChanges(@NotNull Project project, @NotNull Disposable disposable) {
+ GradleBuildFileUpdater buildFileUpdater = new GradleBuildFileUpdater(project);
+
+ GradleModuleListener moduleListener = new GradleModuleListener();
+ moduleListener.addModuleListener(buildFileUpdater);
+
+ MessageBusConnection connection = project.getMessageBus().connect(disposable);
+ connection.subscribe(ProjectTopics.MODULES, moduleListener);
+ connection.subscribe(VirtualFileManager.VFS_CHANGES, buildFileUpdater);
+ }
+
+ @Override
+ public void projectClosed() {
+ if (myDisposable != null) {
+ Disposer.dispose(myDisposable);
+ }
+ }
+
+ private static class GradleModuleListener implements ModuleListener {
+ @NotNull private final List<ModuleListener> additionalListeners = Lists.newArrayList();
+
+ @Override
+ public void moduleAdded(Project project, Module module) {
+ updateBuildVariantView(project);
+ for (ModuleListener listener : additionalListeners) {
+ listener.moduleAdded(project, module);
+ }
+ }
+
+ @Override
+ public void beforeModuleRemoved(Project project, Module module) {
+ for (ModuleListener listener : additionalListeners) {
+ listener.beforeModuleRemoved(project, module);
+ }
+ }
+
+ @Override
+ public void modulesRenamed(Project project, List<Module> modules, Function<Module, String> oldNameProvider) {
+ updateBuildVariantView(project);
+ for (ModuleListener listener : additionalListeners) {
+ listener.modulesRenamed(project, modules, oldNameProvider);
+ }
+ }
+
+ @Override
+ public void moduleRemoved(Project project, Module module) {
+ updateBuildVariantView(project);
+ for (ModuleListener listener : additionalListeners) {
+ listener.moduleRemoved(project, module);
+ }
+ }
+
+ private static void updateBuildVariantView(@NotNull Project project) {
+ BuildVariantView.getInstance(project).updateContents();
+ }
+
+ void addModuleListener(@NotNull ModuleListener listener) {
+ additionalListeners.add(listener);
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/project/AndroidGradleProjectResolver.java b/android/src/com/android/tools/idea/gradle/project/AndroidGradleProjectResolver.java
index 2086939..0da521a 100644
--- a/android/src/com/android/tools/idea/gradle/project/AndroidGradleProjectResolver.java
+++ b/android/src/com/android/tools/idea/gradle/project/AndroidGradleProjectResolver.java
@@ -15,74 +15,104 @@
*/
package com.android.tools.idea.gradle.project;
-import com.android.build.gradle.internal.tasks.BaseTask;
-import com.android.build.gradle.model.AndroidProject;
-import com.android.builder.AndroidBuilder;
-import com.android.builder.model.ProductFlavor;
+import com.android.SdkConstants;
+import com.android.builder.model.AndroidProject;
+import com.android.builder.model.Variant;
+import com.android.tools.idea.gradle.AndroidProjectKeys;
import com.android.tools.idea.gradle.GradleImportNotificationListener;
+import com.android.tools.idea.gradle.IdeaAndroidProject;
+import com.android.tools.idea.gradle.IdeaGradleProject;
+import com.android.tools.idea.gradle.dependency.Dependency;
+import com.android.tools.idea.gradle.util.AndroidGradleSettings;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Strings;
import com.google.common.collect.Lists;
-import com.intellij.execution.configurations.ParametersList;
import com.intellij.execution.configurations.SimpleJavaParameters;
-import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.externalSystem.JavaProjectData;
import com.intellij.openapi.externalSystem.model.DataNode;
+import com.intellij.openapi.externalSystem.model.Key;
+import com.intellij.openapi.externalSystem.model.ProjectKeys;
+import com.intellij.openapi.externalSystem.model.project.ContentRootData;
+import com.intellij.openapi.externalSystem.model.project.ExternalSystemSourceType;
+import com.intellij.openapi.externalSystem.model.project.ModuleData;
import com.intellij.openapi.externalSystem.model.project.ProjectData;
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId;
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListener;
+import com.intellij.openapi.externalSystem.model.task.TaskData;
+import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
+import com.intellij.openapi.module.StdModuleTypes;
import com.intellij.openapi.projectRoots.ProjectJdkTable;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.util.KeyValue;
import com.intellij.util.Function;
-import com.intellij.util.PathUtil;
import com.intellij.util.containers.ContainerUtil;
-import com.intellij.util.lang.UrlClassLoader;
import com.intellij.util.net.HttpConfigurable;
+import org.gradle.tooling.ModelBuilder;
import org.gradle.tooling.ProjectConnection;
+import org.gradle.tooling.UnknownModelException;
+import org.gradle.tooling.model.DomainObjectSet;
+import org.gradle.tooling.model.GradleProject;
+import org.gradle.tooling.model.GradleTask;
+import org.gradle.tooling.model.idea.IdeaContentRoot;
+import org.gradle.tooling.model.idea.IdeaModule;
+import org.gradle.tooling.model.idea.IdeaProject;
import org.jetbrains.android.sdk.AndroidPlatform;
-import org.jetbrains.android.sdk.AndroidSdkUtils;
+import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.gradle.service.project.GradleExecutionHelper;
import org.jetbrains.plugins.gradle.service.project.GradleProjectResolverExtension;
import org.jetbrains.plugins.gradle.settings.GradleExecutionSettings;
+import org.jetbrains.plugins.gradle.util.GradleConstants;
+import org.jetbrains.plugins.gradle.util.GradleUtil;
+import java.io.File;
import java.net.URL;
+import java.util.Collection;
+import java.util.Collections;
import java.util.List;
+import java.util.Map;
/**
* Imports Android-Gradle projects into IDEA.
*/
public class AndroidGradleProjectResolver implements GradleProjectResolverExtension {
- private static final Logger LOG = Logger.getInstance(AndroidGradleProjectResolver.class);
+ @NonNls private static final String ANDROID_TASK_NAME_PREFIX = "android";
+ @NonNls private static final String COMPILE_JAVA_TASK_NAME = "compileJava";
+ @NonNls private static final String CLASSES_TASK_NAME = "classes";
+ @NonNls private static final String JAR_TASK_NAME = "jar";
+
+ @NonNls private static final String UNSUPPORTED_MODEL_VERSION_ERROR = String.format(
+ "Project is using an old version of the Android Gradle plug-in. The minimum supported version is %1$s.\n\n" +
+ "Please update the version of the dependency 'com.android.tools.build:gradle' in your build.gradle files.",
+ GradleModelVersionCheck.MINIMUM_SUPPORTED_VERSION.toString());
@NotNull private final GradleExecutionHelper myHelper;
- @NotNull private final ProjectResolverFunctionFactory myFunctionFactory;
+ @NotNull private final ProjectImportErrorHandler myErrorHandler;
// This constructor is called by the IDE. See this module's plugin.xml file, implementation of extension 'projectResolve'.
@SuppressWarnings("UnusedDeclaration")
public AndroidGradleProjectResolver() {
- myHelper = new GradleExecutionHelper();
- myFunctionFactory = new ProjectResolverFunctionFactory(new ProjectResolver(myHelper));
+ //noinspection TestOnlyProblems
+ this(new GradleExecutionHelper(), new ProjectImportErrorHandler());
}
@VisibleForTesting
- AndroidGradleProjectResolver(@NotNull GradleExecutionHelper helper, @NotNull ProjectResolverFunctionFactory functionFactory) {
+ AndroidGradleProjectResolver(@NotNull GradleExecutionHelper helper, @NotNull ProjectImportErrorHandler errorHandler) {
myHelper = helper;
- myFunctionFactory = functionFactory;
+ myErrorHandler = errorHandler;
}
/**
* Imports an Android-Gradle project into IDEA.
- *
+ * <p/>
* </p>Two types of projects are supported:
* <ol>
- * <li>A single {@link AndroidProject}</li>
- * <li>A multi-project has at least one {@link AndroidProject} child</li>
+ * <li>A single {@link AndroidProject}</li>
+ * <li>A multi-project has at least one {@link AndroidProject} child</li>
* </ol>
*
* @param id id of the current 'resolve project info' task.
- * @param projectPath absolute path of the build.gradle file. It includes the file name.
+ * @param projectPath absolute path of the parent folder of the build.gradle file.
* @param downloadLibraries a hint that specifies if third-party libraries that are not available locally should be resolved (downloaded.)
* @param settings settings to use for the project resolving; {@code null} as indication that no specific settings are required.
* @param listener callback to be notified about the execution
@@ -90,87 +120,394 @@
*/
@Nullable
@Override
- public DataNode<ProjectData> resolveProjectInfo(@NotNull ExternalSystemTaskId id,
- @NotNull String projectPath,
+ public DataNode<ProjectData> resolveProjectInfo(@NotNull final ExternalSystemTaskId id,
+ @NotNull final String projectPath,
boolean downloadLibraries,
- @Nullable GradleExecutionSettings settings,
- @NotNull ExternalSystemTaskNotificationListener listener) {
- Function<ProjectConnection, DataNode<ProjectData>> function = myFunctionFactory.createFunction(id, projectPath, settings, listener);
- return myHelper.execute(projectPath, settings, function);
+ @Nullable final GradleExecutionSettings settings,
+ @NotNull final ExternalSystemTaskNotificationListener listener) {
+ return myHelper.execute(projectPath, settings, new Function<ProjectConnection, DataNode<ProjectData>>() {
+ @Nullable
+ @Override
+ public DataNode<ProjectData> fun(ProjectConnection connection) {
+ try {
+ List<String> extraJvmArgs = getExtraJvmArgs(projectPath);
+ //noinspection TestOnlyProblems
+ return resolveProjectInfo(id, projectPath, connection, listener, extraJvmArgs, settings);
+ }
+ catch (RuntimeException e) {
+ throw myErrorHandler.getUserFriendlyError(e, projectPath, null);
+ }
+ }
+ });
+ }
+
+ @NotNull
+ private static List<String> getExtraJvmArgs(@NotNull String projectPath) {
+ if (ExternalSystemApiUtil.isInProcessMode(GradleConstants.SYSTEM_ID)) {
+ List<String> args = Lists.newArrayList();
+ if (!AndroidGradleSettings.isAndroidSdkDirInLocalPropertiesFile(new File(projectPath))) {
+ String androidHome = getAndroidSdkPathFromIde();
+ if (androidHome != null) {
+ String arg = AndroidGradleSettings.createAndroidHomeJvmArg(androidHome);
+ args.add(arg);
+ }
+ }
+ List<KeyValue<String,String>> proxyProperties = HttpConfigurable.getJvmPropertiesList(false, null);
+ for (KeyValue<String, String> proxyProperty : proxyProperties) {
+ String arg = AndroidGradleSettings.createJvmArg(proxyProperty.getKey(), proxyProperty.getValue());
+ args.add(arg);
+ }
+ return args;
+ }
+ return Collections.emptyList();
+ }
+
+ @Nullable
+ private static String getAndroidSdkPathFromIde() {
+ for (Sdk sdk : ProjectJdkTable.getInstance().getAllJdks()) {
+ AndroidPlatform androidPlatform = AndroidPlatform.parse(sdk);
+ String sdkHomePath = sdk.getHomePath();
+ if (androidPlatform != null && sdkHomePath != null) {
+ return sdkHomePath;
+ }
+ }
+ return null;
}
/**
- * <ol>
- * <li>Adds the paths of the 'android' module and jar files of the Android-Gradle project to the classpath of the slave process that
- * performs the Gradle project import.</li>
- * <li>Sets the value of the environment variable "ANDROID_HOME" with the path of the first found Android SDK, if the environment
- * variable has not been set.</li>
- * </ol>
+ * Imports multiple Android-Gradle projects. The set of projects to import may include regular Java projects as well.
+ * <p/>
+ * </p>Since the Android Gradle model does not support multiple projects, we query the Gradle Tooling API for a regular Java
+ * multi-project. Then, for each of the modules in the imported project, we query for an (@link AndroidProject Android Gradle model.) If
+ * we get one we create an IDE module from it, otherwise we just use the regular Java module. Unfortunately, this process requires
+ * creation of multiple {@link ProjectConnection}s.
*
- * @param parameters parameters to be applied to the slave process which will be used for external system communication.
+ * @param id id of the current 'resolve project info' task.
+ * @param projectPath absolute path of the parent folder of the build.gradle file.
+ * @param connection Gradle Tooling API connection to the project to import.
+ * @param listener callback to be notified about the execution.
+ * @param extraJvmArgs extra JVM arguments to pass to Gradle tooling API.
+ * @param settings settings to use for the project resolving; {@code null} as indication that no specific settings are required.
+ * @return the imported project, or {@link null} if the project to import is not an Android-Gradle project.
*/
- @Override
- public void enhanceRemoteProcessing(@NotNull SimpleJavaParameters parameters) {
- GradleImportNotificationListener.attachToManager();
- List<String> jarPaths = getJarPathsOf(getClass(), AndroidBuilder.class, AndroidProject.class, BaseTask.class, ProductFlavor.class);
- LOG.info("Added to RMI/Gradle process classpath: " + jarPaths);
- for (String jarPath : jarPaths) {
- parameters.getClassPath().add(jarPath);
+ @VisibleForTesting
+ @Nullable
+ DataNode<ProjectData> resolveProjectInfo(@NotNull ExternalSystemTaskId id,
+ @NotNull String projectPath,
+ @NotNull ProjectConnection connection,
+ @NotNull ExternalSystemTaskNotificationListener listener,
+ @NotNull List<String> extraJvmArgs,
+ @Nullable GradleExecutionSettings settings) {
+ ModelBuilder<IdeaProject> modelBuilder = myHelper.getModelBuilder(IdeaProject.class, id, settings, connection, listener, extraJvmArgs);
+ IdeaProject ideaProject = modelBuilder.get();
+ if (ideaProject == null || ideaProject.getModules().isEmpty()) {
+ return null;
}
- String androidHome = System.getenv(AndroidSdkUtils.ANDROID_HOME_ENV);
- if (Strings.isNullOrEmpty(androidHome)) {
- for (Sdk sdk : ProjectJdkTable.getInstance().getAllJdks()) {
- AndroidPlatform androidPlatform = AndroidPlatform.parse(sdk);
- String sdkHomePath = sdk.getHomePath();
- if (androidPlatform != null && sdkHomePath != null) {
- parameters.addEnv(AndroidSdkUtils.ANDROID_HOME_ENV, sdkHomePath);
- break;
+
+ String name = new File(projectPath).getName();
+ DataNode<ProjectData> projectInfo = createProjectInfo(projectPath, name);
+
+ AndroidProject first = null;
+
+ DomainObjectSet<? extends IdeaModule> modules = ideaProject.getModules();
+ for (IdeaModule module : modules) {
+ IdeaGradleProject gradleProject = new IdeaGradleProject(module.getName(), module.getGradleProject().getPath());
+ String relativePath = getRelativePath(gradleProject);
+ File moduleDir;
+ if (relativePath.isEmpty()) {
+ moduleDir = new File(projectPath);
+ }
+ else {
+ moduleDir = new File(projectPath, relativePath);
+ }
+ File gradleBuildFile = new File(moduleDir, SdkConstants.FN_BUILD_GRADLE);
+ if (!gradleBuildFile.isFile()) {
+ continue;
+ }
+ String moduleDirPath = moduleDir.getPath();
+ if (isAndroidProject(module.getGradleProject())) {
+ AndroidProject androidProject = getAndroidProject(id, moduleDirPath, gradleBuildFile, listener, extraJvmArgs, settings);
+ if (androidProject == null || !GradleModelVersionCheck.isSupportedVersion(androidProject)) {
+ throw new IllegalStateException(UNSUPPORTED_MODEL_VERSION_ERROR);
+ }
+ createModuleInfo(module, androidProject, projectInfo, moduleDirPath, gradleProject);
+ if (first == null) {
+ first = androidProject;
+ }
+ }
+ else if (isJavaLibrary(module.getGradleProject())) {
+ createModuleInfo(module, projectInfo, moduleDirPath, gradleProject);
+ }
+ else {
+ File gradleSettingsFile = new File(moduleDir, SdkConstants.FN_SETTINGS_GRADLE);
+ if (gradleSettingsFile.isFile()) {
+ // This is just a root folder for a group of Gradle projects. Set the Gradle project to null so the JPS builder won't try to
+ // compile it using Gradle. We still need to create the module to display files inside it.
+ createModuleInfo(module, projectInfo, moduleDirPath, null);
}
}
}
- List<KeyValue<String,String>> proxyProperties = HttpConfigurable.getJvmPropertiesList(false, null);
- ParametersList vmParameters = parameters.getVMParametersList();
- for (KeyValue<String, String> proxyProperty : proxyProperties) {
- vmParameters.defineProperty(proxyProperty.getKey(), proxyProperty.getValue());
+
+ if (first == null) {
+ // Don't import project if we don't have at least one AndroidProject.
+ return null;
}
+
+ populateDependencies(projectInfo);
+ return projectInfo;
+ }
+
+ @NotNull
+ private static DataNode<ProjectData> createProjectInfo(@NotNull String projectDirPath, @NotNull String name) {
+ ProjectData projectData = new ProjectData(GradleConstants.SYSTEM_ID, projectDirPath, projectDirPath);
+ projectData.setName(name);
+
+ DataNode<ProjectData> projectInfo = new DataNode<ProjectData>(ProjectKeys.PROJECT, projectData, null);
+
+ // Gradle API doesn't expose project compile output path yet.
+ JavaProjectData javaProjectData = new JavaProjectData(GradleConstants.SYSTEM_ID, projectDirPath + "/build/classes");
+ projectInfo.createChild(JavaProjectData.KEY, javaProjectData);
+
+ return projectInfo;
+ }
+
+ @NotNull
+ private static String getRelativePath(@NotNull IdeaGradleProject gradleProject) {
+ String separator = File.separator;
+ if (separator.equals("\\")) {
+ separator = "\\\\";
+ }
+ String gradleProjectPath = gradleProject.getGradleProjectPath();
+ if (SdkConstants.GRADLE_PATH_SEPARATOR.equals(gradleProjectPath)) {
+ return "";
+ }
+ return gradleProjectPath.replaceAll(SdkConstants.GRADLE_PATH_SEPARATOR, separator);
+ }
+
+ private static void addModuleTasks(@NotNull DataNode<ModuleData> moduleInfo,
+ @NotNull IdeaModule module,
+ @NotNull DataNode<ProjectData> projectInfo) {
+ String rootProjectPath = projectInfo.getData().getLinkedExternalProjectPath();
+ String moduleConfigPath = GradleUtil.getConfigPath(module.getGradleProject(), rootProjectPath);
+
+ DataNode<?> target = moduleConfigPath.equals(rootProjectPath) ? projectInfo : moduleInfo;
+
+ for (GradleTask task : module.getGradleProject().getTasks()) {
+ String taskName = task.getName();
+ //noinspection TestOnlyProblems
+ if (taskName == null || taskName.trim().isEmpty() || isIdeaTask(taskName)) {
+ continue;
+ }
+ TaskData taskData = new TaskData(GradleConstants.SYSTEM_ID, taskName, moduleConfigPath, task.getDescription());
+ target.createChild(ProjectKeys.TASK, taskData);
+ }
+ }
+
+ @VisibleForTesting
+ static boolean isIdeaTask(@NotNull String taskName) {
+ return taskName.equals("idea") ||
+ (taskName.startsWith("idea") && taskName.length() > 5 && Character.isUpperCase(taskName.charAt(4))) ||
+ taskName.endsWith("Idea") ||
+ taskName.endsWith("IdeaModule") ||
+ taskName.endsWith("IdeaProject") ||
+ taskName.endsWith("IdeaWorkspace");
+ }
+
+ @Nullable
+ private AndroidProject getAndroidProject(@NotNull final ExternalSystemTaskId id,
+ @NotNull final String projectPath,
+ @NotNull final File gradleBuildFile,
+ @NotNull final ExternalSystemTaskNotificationListener listener,
+ @NotNull final List<String> extraJvmArgs,
+ @Nullable final GradleExecutionSettings settings) {
+ return myHelper.execute(projectPath, settings, new Function<ProjectConnection, AndroidProject>() {
+ @Nullable
+ @Override
+ public AndroidProject fun(ProjectConnection connection) {
+ try {
+ ModelBuilder<AndroidProject> modelBuilder =
+ myHelper.getModelBuilder(AndroidProject.class, id, settings, connection, listener, extraJvmArgs);
+ return modelBuilder.get();
+ }
+ catch (UnknownModelException e) {
+ // Safe to ignore. It means that the Gradle project does not have an AndroidProject (e.g. a Java library project.)
+ return null;
+ }
+ catch (RuntimeException e) {
+ // This code should go away once we have one-pass project resolution in Gradle 1.8.
+ // Once that version of Gradle is out, we don't need to pass the project path because we won't be iterating through each
+ // sub-project looking for an AndroidProject. The current problem is: in this particular call to Gradle we don't get the location
+ // of the build.gradle file that has a problem.
+ throw myErrorHandler.getUserFriendlyError(e, projectPath, gradleBuildFile.getPath());
+ }
+ }
+ });
+ }
+
+ private static boolean isAndroidProject(@NotNull GradleProject gradleProject) {
+ // A Gradle project is an Android project is if has at least one task with name starting with 'android'.
+ for (GradleTask task : gradleProject.getTasks()) {
+ String taskName = task.getName();
+ if (taskName != null && taskName.startsWith(ANDROID_TASK_NAME_PREFIX)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isJavaLibrary(@NotNull GradleProject gradleProject) {
+ // A Gradle project is a Java library if it has the tasks 'compileJava', 'classes' and 'jar'.
+ List<String> javaTasks = Lists.newArrayList(COMPILE_JAVA_TASK_NAME, CLASSES_TASK_NAME, JAR_TASK_NAME);
+ for (GradleTask task : gradleProject.getTasks()) {
+ String taskName = task.getName();
+ if (taskName != null && javaTasks.remove(taskName) && javaTasks.isEmpty()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @NotNull
+ private static DataNode<ModuleData> createModuleInfo(@NotNull IdeaModule module,
+ @NotNull AndroidProject androidProject,
+ @NotNull DataNode<ProjectData> projectInfo,
+ @NotNull String moduleDirPath,
+ @NotNull IdeaGradleProject gradleProject) {
+ String moduleName = module.getName();
+ ModuleData moduleData = createModuleData(module, projectInfo, moduleName, moduleDirPath);
+ DataNode<ModuleData> moduleInfo = projectInfo.createChild(ProjectKeys.MODULE, moduleData);
+
+ Variant selectedVariant = getFirstVariant(androidProject);
+ IdeaAndroidProject ideaAndroidProject = new IdeaAndroidProject(moduleName, moduleDirPath, androidProject, selectedVariant.getName());
+ addContentRoot(ideaAndroidProject, moduleInfo, moduleDirPath);
+
+ moduleInfo.createChild(AndroidProjectKeys.IDE_ANDROID_PROJECT, ideaAndroidProject);
+ moduleInfo.createChild(AndroidProjectKeys.IDE_GRADLE_PROJECT, gradleProject);
+
+ addModuleTasks(moduleInfo, module, projectInfo);
+ return moduleInfo;
+ }
+
+ @NotNull
+ private static Variant getFirstVariant(@NotNull AndroidProject androidProject) {
+ Map<String, Variant> variants = androidProject.getVariants();
+ if (variants.size() == 1) {
+ Variant variant = ContainerUtil.getFirstItem(variants.values());
+ assert variant != null;
+ return variant;
+ }
+ List<String> variantNames = Lists.newArrayList(variants.keySet());
+ Collections.sort(variantNames);
+ return variants.get(variantNames.get(0));
+ }
+
+ private static void addContentRoot(@NotNull IdeaAndroidProject androidProject,
+ @NotNull DataNode<ModuleData> moduleInfo,
+ @NotNull String moduleDirPath) {
+ final ContentRootData contentRootData = new ContentRootData(GradleConstants.SYSTEM_ID, moduleDirPath);
+ AndroidContentRoot.ContentRootStorage storage = new AndroidContentRoot.ContentRootStorage() {
+ @Override
+ @NotNull
+ public String getRootDirPath() {
+ return contentRootData.getRootPath();
+ }
+
+ @Override
+ public void storePath(@NotNull ExternalSystemSourceType sourceType, @NotNull File directory) {
+ contentRootData.storePath(sourceType, directory.getAbsolutePath());
+ }
+ };
+ AndroidContentRoot.storePaths(androidProject, storage);
+ moduleInfo.createChild(ProjectKeys.CONTENT_ROOT, contentRootData);
+ }
+
+ @NotNull
+ private static DataNode<ModuleData> createModuleInfo(@NotNull IdeaModule module,
+ @NotNull DataNode<ProjectData> projectInfo,
+ @NotNull String moduleDirPath,
+ @Nullable IdeaGradleProject gradleProject) {
+ ModuleData moduleData = createModuleData(module, projectInfo, module.getName(), moduleDirPath);
+ DataNode<ModuleData> moduleInfo = projectInfo.createChild(ProjectKeys.MODULE, moduleData);
+
+ // Populate content roots.
+ for (IdeaContentRoot from : module.getContentRoots()) {
+ if (from == null || from.getRootDirectory() == null) {
+ continue;
+ }
+ ContentRootData contentRootData = new ContentRootData(GradleConstants.SYSTEM_ID, from.getRootDirectory().getAbsolutePath());
+ GradleContentRoot.storePaths(from, contentRootData);
+ moduleInfo.createChild(ProjectKeys.CONTENT_ROOT, contentRootData);
+ }
+
+ moduleInfo.createChild(AndroidProjectKeys.IDEA_MODULE, module);
+ if (gradleProject != null) {
+ moduleInfo.createChild(AndroidProjectKeys.IDE_GRADLE_PROJECT, gradleProject);
+ }
+
+ addModuleTasks(moduleInfo, module, projectInfo);
+ return moduleInfo;
+ }
+
+ private static ModuleData createModuleData(@NotNull IdeaModule module,
+ @NotNull DataNode<ProjectData> projectInfo,
+ @NotNull String name,
+ @NotNull String dirPath) {
+ String moduleConfigPath = GradleUtil.getConfigPath(module.getGradleProject(), projectInfo.getData().getLinkedExternalProjectPath());
+ return new ModuleData(GradleConstants.SYSTEM_ID, StdModuleTypes.JAVA.getId(), name, dirPath, moduleConfigPath);
+ }
+
+ private static void populateDependencies(@NotNull DataNode<ProjectData> projectInfo) {
+ Collection<DataNode<ModuleData>> modules = ExternalSystemApiUtil.getChildren(projectInfo, ProjectKeys.MODULE);
+ ImportedDependencyUpdater importer = new ImportedDependencyUpdater(projectInfo);
+ for (DataNode<ModuleData> moduleInfo : modules) {
+ IdeaAndroidProject androidProject = getIdeaAndroidProject(moduleInfo);
+ Collection<Dependency> dependencies = Collections.emptyList();
+ if (androidProject != null) {
+ dependencies = Dependency.extractFrom(androidProject);
+ }
+ else {
+ IdeaModule module = extractIdeaModule(moduleInfo);
+ if (module != null) {
+ dependencies = Dependency.extractFrom(module);
+ }
+ }
+ if (!dependencies.isEmpty()) {
+ importer.updateDependencies(moduleInfo, dependencies);
+ }
+ }
+ }
+
+ @Nullable
+ private static IdeaAndroidProject getIdeaAndroidProject(@NotNull DataNode<ModuleData> moduleInfo) {
+ return getFirstNodeData(moduleInfo, AndroidProjectKeys.IDE_ANDROID_PROJECT);
+ }
+
+ @Nullable
+ private static <T> T getFirstNodeData(@NotNull DataNode<ModuleData> moduleInfo, @NotNull Key<T> key) {
+ Collection<DataNode<T>> children = ExternalSystemApiUtil.getChildren(moduleInfo, key);
+ return getFirstNodeData(children);
+ }
+
+ @Nullable
+ private static IdeaModule extractIdeaModule(@NotNull DataNode<ModuleData> moduleInfo) {
+ Collection<DataNode<IdeaModule>> modules = ExternalSystemApiUtil.getChildren(moduleInfo, AndroidProjectKeys.IDEA_MODULE);
+ // it is safe to remove this node. We only needed it to resolve dependencies.
+ moduleInfo.getChildren().removeAll(modules);
+ return getFirstNodeData(modules);
+ }
+
+ @Nullable
+ private static <T> T getFirstNodeData(Collection<DataNode<T>> nodes) {
+ DataNode<T> node = ContainerUtil.getFirstItem(nodes);
+ return node != null ? node.getData() : null;
+ }
+
+ @Override
+ public void enhanceRemoteProcessing(@NotNull SimpleJavaParameters parameters) {
}
@Override
public void enhanceLocalProcessing(@NotNull List<URL> urls) {
- }
-
- @NotNull
- private static List<String> getJarPathsOf(@NotNull Class<?>... types) {
- List<String> jarPaths = Lists.newArrayList();
- for (Class<?> type : types) {
- ContainerUtil.addIfNotNull(PathUtil.getJarPathForClass(type), jarPaths);
- }
- return jarPaths;
- }
-
- static class ProjectResolverFunctionFactory {
- @NotNull private final ProjectResolver myResolver;
-
- ProjectResolverFunctionFactory(@NotNull ProjectResolver resolver) {
- myResolver = resolver;
- }
-
- @NotNull
- Function<ProjectConnection, DataNode<ProjectData>> createFunction(@NotNull final ExternalSystemTaskId id,
- @NotNull final String projectPath,
- @Nullable final GradleExecutionSettings settings,
- @NotNull final ExternalSystemTaskNotificationListener listener) {
- return new Function<ProjectConnection, DataNode<ProjectData>>() {
- @Nullable
- @Override
- public DataNode<ProjectData> fun(ProjectConnection connection) {
- DataNode<ProjectData> projectInfo = myResolver.resolveProjectInfo(id, projectPath, settings, connection, listener);
- if (projectInfo != null) {
- return projectInfo;
- }
- return null;
- }
- };
- }
+ GradleImportNotificationListener.attachToManager();
}
}
diff --git a/android/src/com/android/tools/idea/gradle/project/GradleBuildFileUpdater.java b/android/src/com/android/tools/idea/gradle/project/GradleBuildFileUpdater.java
new file mode 100644
index 0000000..d1be193
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/project/GradleBuildFileUpdater.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.project;
+
+import com.android.SdkConstants;
+import com.android.tools.idea.gradle.facet.AndroidGradleFacet;
+import com.android.tools.idea.gradle.parser.GradleBuildFile;
+import com.android.tools.idea.gradle.parser.GradleSettingsFile;
+import com.android.tools.idea.gradle.util.Facets;
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.project.ModuleAdapter;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.newvfs.BulkFileListener;
+import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
+import com.intellij.openapi.vfs.newvfs.events.VFilePropertyChangeEvent;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * GradleBuildFileUpdater listens for module-level events and updates the settings.gradle and build.gradle files to reflect any changes it
+ * sees.
+ */
+public class GradleBuildFileUpdater extends ModuleAdapter implements BulkFileListener {
+ private static final Logger LOG = Logger.getInstance(GradleBuildFileUpdater.class);
+
+ // Module doesn't implement hashCode(); we can't use a map
+ private final Collection<Pair<Module, GradleBuildFile>> myBuildFiles = Lists.newArrayList();
+ private final GradleSettingsFile mySettingsFile;
+ private final Project myProject;
+
+ public GradleBuildFileUpdater(@NotNull Project project) {
+ myProject = project;
+ VirtualFile settingsFile = project.getBaseDir().findFileByRelativePath(SdkConstants.FN_SETTINGS_GRADLE);
+ if (settingsFile != null) {
+ mySettingsFile = new GradleSettingsFile(settingsFile, project);
+ } else {
+ mySettingsFile = null;
+ LOG.warn("Unable to find settings.gradle file for project " + project.getName());
+ }
+ findAndAddAllBuildFiles();
+ }
+
+ @Override
+ public void moduleAdded(@NotNull final Project project, @NotNull final Module module) {
+ // At the time we're called, module.getModuleFile() is null, but getModuleFilePath returns the path where it will be created.
+ File moduleFile = new File(module.getModuleFilePath());
+ File buildFile = new File(moduleFile.getParentFile(), SdkConstants.FN_BUILD_GRADLE);
+ VirtualFile vBuildFile = LocalFileSystem.getInstance().findFileByIoFile(buildFile);
+ if (vBuildFile != null) {
+ put(module, new GradleBuildFile(vBuildFile, project));
+ }
+
+ // The module has probably already been added to the settings file but let's call this to be safe.
+ mySettingsFile.addModule(module);
+ }
+
+ @Override
+ public void moduleRemoved(@NotNull Project project, @NotNull final Module module) {
+ remove(module);
+ mySettingsFile.removeModule(module);
+ }
+
+ @Override
+ public void before(@NotNull List<? extends VFileEvent> events) {
+ }
+
+ /**
+ * This gets called on all file system changes, but we're interested in changes to module root directories. When we see them, we'll update
+ * the settings.gradle file. Note that users can also refactor modules by renaming them, which just changes their display name and not
+ * the filesystem directory -- when that happens, this class gets a
+ * {@link ModuleAdapter#modulesRenamed(com.intellij.openapi.project.Project, java.util.List)} callback. However, it's not appropriate to
+ * update settings.gradle in that case since Gradle doesn't case about IJ's display name of the module.
+ */
+ @Override
+ public void after(@NotNull List<? extends VFileEvent> events) {
+ for (VFileEvent event : events) {
+ if (!(event instanceof VFilePropertyChangeEvent)) {
+ continue;
+ }
+ VFilePropertyChangeEvent propChangeEvent = (VFilePropertyChangeEvent) event;
+ if (!(VirtualFile.PROP_NAME.equals(propChangeEvent.getPropertyName())) || propChangeEvent.getFile() == null) {
+ continue;
+ }
+
+ VirtualFile eventFile = propChangeEvent.getFile();
+ if (!eventFile.isDirectory()) {
+ continue;
+ }
+
+ // If this listener is installed, it is because that the project is a Gradle-based project. If we don't have any build.gradle files
+ // registered, it is because this listener was created before a project was fully created. This is common during creation of new
+ // Gradle-based Android projects. ProjectComponent#projectOpened is called when the project is created, instead of when the project
+ // is actually opened. It may be a bug in IJ.
+ if (myBuildFiles.isEmpty()) {
+ findAndAddAllBuildFiles();
+ }
+
+ // Dig through our modules and find the one that matches the change event's path (the module will already have its path updated by
+ // now).
+ Module module = null;
+ for (Pair<Module, GradleBuildFile> pair : myBuildFiles) {
+ VirtualFile moduleFile = pair.first.getModuleFile();
+ if (moduleFile == null || moduleFile.getParent() == null) {
+ continue;
+ }
+
+ VirtualFile moduleDir = moduleFile.getParent();
+ if (FileUtil.pathsEqual(eventFile.getPath(), moduleDir.getPath())) {
+ module = pair.first;
+ break;
+ }
+ }
+
+ // If we found the module, then remove the old reference from the settings.gradle file and from our data structures, and put in new
+ // references.
+ if (module != null) {
+ remove(module);
+ AndroidGradleFacet androidGradleFacet = Facets.getFirstFacetOfType(module, AndroidGradleFacet.TYPE_ID);
+ if (androidGradleFacet == null) {
+ continue;
+ }
+ String oldPath = androidGradleFacet.getConfiguration().GRADLE_PROJECT_PATH;
+ String newPath = updateProjectNameInGradlePath(androidGradleFacet, eventFile);
+
+ if (oldPath.equals(newPath)) {
+ continue;
+ }
+
+ mySettingsFile.removeModule(oldPath);
+
+ File modulePath = new File(newPath);
+ File buildFile = new File(modulePath, SdkConstants.FN_BUILD_GRADLE);
+ VirtualFile vBuildFile = LocalFileSystem.getInstance().findFileByIoFile(buildFile);
+ if (vBuildFile != null) {
+ put(module, new GradleBuildFile(vBuildFile, myProject));
+ }
+
+ mySettingsFile.addModule(newPath);
+ }
+ }
+ }
+
+ private void findAndAddAllBuildFiles() {
+ Module[] modules = ModuleManager.getInstance(myProject).getModules();
+ for (Module module : modules) {
+ VirtualFile vf;
+ if ((vf = module.getModuleFile()) != null &&
+ (vf = vf.getParent()) != null &&
+ (vf = vf.findChild(SdkConstants.FN_BUILD_GRADLE)) != null) {
+ put(module, new GradleBuildFile(vf, myProject));
+ } else {
+ LOG.warn("Unable to find build.gradle file for module " + module.getName());
+ }
+ }
+ }
+
+ @NotNull
+ private static String updateProjectNameInGradlePath(@NotNull AndroidGradleFacet androidGradleFacet, @NotNull VirtualFile moduleDir) {
+ String gradlePath = androidGradleFacet.getConfiguration().GRADLE_PROJECT_PATH;
+ if (gradlePath.equals(SdkConstants.GRADLE_PATH_SEPARATOR)) {
+ // This is root project, renaming folder does not affect it since the path is just ":".
+ return gradlePath;
+ }
+ List<String> pathSegments = Lists.newArrayList(gradlePath.split(SdkConstants.GRADLE_PATH_SEPARATOR));
+ pathSegments.remove(pathSegments.size() - 1);
+ pathSegments.add(moduleDir.getName());
+
+ String newPath = Joiner.on(SdkConstants.GRADLE_PATH_SEPARATOR).join(pathSegments);
+ androidGradleFacet.getConfiguration().GRADLE_PROJECT_PATH = newPath;
+ return newPath;
+ }
+
+ private void put(@NotNull Module module, @NotNull GradleBuildFile file) {
+ remove(module);
+ myBuildFiles.add(new Pair<Module, GradleBuildFile>(module, file));
+ }
+
+ private void remove(@NotNull Module module) {
+ for (Pair<Module, GradleBuildFile> pair : myBuildFiles) {
+ if (pair.first == module) {
+ myBuildFiles.remove(pair);
+ return;
+ }
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/project/GradleDependencies.java b/android/src/com/android/tools/idea/gradle/project/GradleDependencies.java
deleted file mode 100644
index b36c59b..0000000
--- a/android/src/com/android/tools/idea/gradle/project/GradleDependencies.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tools.idea.gradle.project;
-
-import com.google.common.base.Preconditions;
-import com.intellij.openapi.externalSystem.model.DataNode;
-import com.intellij.openapi.externalSystem.model.project.LibraryPathType;
-import com.intellij.openapi.externalSystem.model.project.ModuleData;
-import com.intellij.openapi.externalSystem.model.project.ProjectData;
-import com.intellij.openapi.roots.DependencyScope;
-import org.gradle.tooling.model.DomainObjectSet;
-import org.gradle.tooling.model.idea.*;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.io.File;
-
-/**
- * Populates an IDEA module with dependencies created from an {@link IdeaModule}.
- */
-final class GradleDependencies {
- private GradleDependencies() {
- }
-
- static void populate(@NotNull DataNode<ModuleData> moduleInfo, @NotNull DataNode<ProjectData> projectInfo, @NotNull IdeaModule module) {
- for (IdeaDependency dep : module.getDependencies()) {
- DependencyScope scope = parseScope(dep.getScope());
-
- if (dep instanceof IdeaModuleDependency) {
- IdeaModule dependencyModule = ((IdeaModuleDependency)dep).getDependencyModule();
- Preconditions.checkNotNull(dependencyModule);
- String dependencyName = Preconditions.checkNotNull(dependencyModule.getName());
-
- ModuleDependency dependency = new ModuleDependency(dependencyName);
- dependency.setScope(scope);
- dependency.addTo(moduleInfo, projectInfo);
- continue;
- }
-
- if (dep instanceof IdeaSingleEntryLibraryDependency) {
- IdeaSingleEntryLibraryDependency gradleDependency = (IdeaSingleEntryLibraryDependency)dep;
- File binaryPath = gradleDependency.getFile();
- Preconditions.checkNotNull(binaryPath);
-
- LibraryDependency dependency = new LibraryDependency(binaryPath);
-
- dependency.setScope(scope);
- dependency.addPath(LibraryPathType.BINARY, binaryPath);
- dependency.addPath(LibraryPathType.SOURCE, gradleDependency.getSource());
- dependency.addPath(LibraryPathType.DOC, gradleDependency.getJavadoc());
-
- dependency.addTo(moduleInfo, projectInfo);
- }
- }
- }
-
- @NotNull
- private static DependencyScope parseScope(@Nullable IdeaDependencyScope scope) {
- if (scope != null) {
- String scopeAsString = scope.getScope();
- if (scopeAsString != null) {
- for (DependencyScope dependencyScope : DependencyScope.values()) {
- if (scopeAsString.equalsIgnoreCase(dependencyScope.toString())) {
- return dependencyScope;
- }
- }
- }
- }
- return DependencyScope.COMPILE;
- }
-}
diff --git a/android/src/com/android/tools/idea/gradle/project/GradleModelVersionCheck.java b/android/src/com/android/tools/idea/gradle/project/GradleModelVersionCheck.java
new file mode 100644
index 0000000..88baba7
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/project/GradleModelVersionCheck.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.project;
+
+import com.android.builder.model.AndroidProject;
+import com.android.sdklib.repository.FullRevision;
+import com.google.common.base.Strings;
+import com.intellij.openapi.diagnostic.Logger;
+import org.jetbrains.annotations.NotNull;
+
+final class GradleModelVersionCheck {
+ private static final Logger LOG = Logger.getInstance(GradleModelVersionCheck.class);
+
+ static final FullRevision MINIMUM_SUPPORTED_VERSION = FullRevision.parseRevision("0.5.0");
+
+ static boolean isSupportedVersion(@NotNull AndroidProject androidProject) {
+ String modelVersion = androidProject.getModelVersion();
+ if (Strings.isNullOrEmpty(modelVersion)) {
+ return false;
+ }
+ int snapshotIndex = modelVersion.indexOf("-");
+ if (snapshotIndex != -1) {
+ modelVersion = modelVersion.substring(0, snapshotIndex);
+ }
+ try {
+ FullRevision modelRevision = FullRevision.parseRevision(modelVersion);
+ return modelRevision.compareTo(MINIMUM_SUPPORTED_VERSION) >= 0;
+ } catch (NumberFormatException e) {
+ LOG.info(String.format("Unable to parse Gradle model version '%1$s'", modelVersion), e);
+ return false;
+ }
+ }
+
+ private GradleModelVersionCheck() {
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/GradleProjectImporter.java b/android/src/com/android/tools/idea/gradle/project/GradleProjectImporter.java
similarity index 63%
rename from android/src/com/android/tools/idea/gradle/GradleProjectImporter.java
rename to android/src/com/android/tools/idea/gradle/project/GradleProjectImporter.java
index 76687c2..143ea3f 100644
--- a/android/src/com/android/tools/idea/gradle/GradleProjectImporter.java
+++ b/android/src/com/android/tools/idea/gradle/project/GradleProjectImporter.java
@@ -13,24 +13,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.tools.idea.gradle;
+package com.android.tools.idea.gradle.project;
+import com.android.SdkConstants;
+import com.android.tools.idea.gradle.GradleImportNotificationListener;
+import com.android.tools.idea.gradle.IdeaAndroidProject;
+import com.android.tools.idea.gradle.customizer.CompilerOutputPathModuleCustomizer;
+import com.android.tools.idea.gradle.customizer.ContentRootModuleCustomizer;
+import com.android.tools.idea.gradle.customizer.DependenciesModuleCustomizer;
+import com.android.tools.idea.gradle.customizer.ModuleCustomizer;
import com.android.tools.idea.gradle.util.Facets;
import com.android.tools.idea.gradle.util.LocalProperties;
import com.android.tools.idea.gradle.util.Projects;
+import com.android.tools.idea.sdk.Jdks;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
-import com.intellij.ProjectTopics;
import com.intellij.ide.impl.NewProjectUtil;
import com.intellij.ide.impl.ProjectUtil;
+import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.externalSystem.model.DataNode;
-import com.intellij.openapi.externalSystem.model.ExternalSystemDataKeys;
-import com.intellij.openapi.externalSystem.model.ProjectKeys;
-import com.intellij.openapi.externalSystem.model.ProjectSystemId;
+import com.intellij.openapi.externalSystem.model.*;
import com.intellij.openapi.externalSystem.model.project.ModuleData;
import com.intellij.openapi.externalSystem.model.project.ProjectData;
import com.intellij.openapi.externalSystem.service.project.ExternalProjectRefreshCallback;
@@ -38,6 +43,7 @@
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
import com.intellij.openapi.externalSystem.util.ExternalSystemBundle;
import com.intellij.openapi.externalSystem.util.ExternalSystemUtil;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.options.ConfigurationException;
@@ -46,11 +52,8 @@
import com.intellij.openapi.project.ex.ProjectManagerEx;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.CompilerProjectExtension;
-import com.intellij.openapi.roots.ModuleRootAdapter;
-import com.intellij.openapi.roots.ModuleRootEvent;
import com.intellij.openapi.roots.ex.ProjectRootManagerEx;
import com.intellij.openapi.startup.StartupManager;
-import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.vfs.VfsUtilCore;
@@ -60,7 +63,6 @@
import com.intellij.openapi.wm.ex.IdeFrameEx;
import com.intellij.openapi.wm.impl.IdeFrameImpl;
import com.intellij.util.SystemProperties;
-import com.intellij.util.messages.MessageBusConnection;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -80,6 +82,9 @@
private static final Logger LOG = Logger.getInstance(GradleProjectImporter.class);
private static final ProjectSystemId SYSTEM_ID = GradleConstants.SYSTEM_ID;
+ private final ModuleCustomizer[] myModuleCustomizers =
+ {new ContentRootModuleCustomizer(), new DependenciesModuleCustomizer(), new CompilerOutputPathModuleCustomizer()};
+
private final ImporterDelegate myDelegate;
@NotNull
@@ -99,67 +104,54 @@
/**
* Re-imports an existing Android-Gradle project.
*
- * @param project the given project. This method does nothing if the project is not an Android-Gradle project.
+ * @param project the given project. This method does nothing if the project is not an Android-Gradle project.
+ * @param callback called after the project has been imported.
* @throws ConfigurationException if any required configuration option is missing (e.g. Gradle home directory path.)
*/
- public void reImportProject(@NotNull Project project) throws ConfigurationException {
- String gradleProjectFilePath = findGradleProjectFilePath(project);
- if (gradleProjectFilePath != null) {
- doImport(project, gradleProjectFilePath, null);
+ public void reImportProject(@NotNull final Project project, @Nullable Callback callback) throws ConfigurationException {
+ if (Projects.isGradleProject(project)) {
+ FileDocumentManager.getInstance().saveAllDocuments();
+ doImport(project, false /* existing project */, false /* asynchronous import */, callback);
}
}
- @Nullable
- private static String findGradleProjectFilePath(@NotNull Project project) {
- for (Module module : ModuleManager.getInstance(project).getModules()) {
- AndroidFacet androidFacet = Facets.getFirstFacet(module, AndroidFacet.ID);
- if (androidFacet == null || androidFacet.getIdeaAndroidProject() == null) {
- continue;
- }
- return androidFacet.getIdeaAndroidProject().getRootGradleProjectFilePath();
- }
- return null;
- }
-
/**
* Imports and opens the newly created Android project.
*
+ *
* @param projectName name of the project.
* @param projectRootDir root directory of the project.
- * @param androidSdk Android SDK to set.
* @param callback called after the project has been imported.
* @throws IOException if any file I/O operation fails (e.g. creating the '.idea' directory.)
* @throws ConfigurationException if any required configuration option is missing (e.g. Gradle home directory path.)
*/
- public void importProject(@NotNull String projectName,
- @NotNull File projectRootDir,
- @NotNull Sdk androidSdk,
- @Nullable final Callback callback) throws IOException, ConfigurationException {
+ public void importProject(@NotNull String projectName, @NotNull File projectRootDir, @Nullable Callback callback) throws IOException, ConfigurationException {
GradleImportNotificationListener.attachToManager();
- File projectFile = createTopLevelBuildFile(projectRootDir);
- String projectFilePath = projectFile.getAbsolutePath();
+ createTopLevelBuildFileIfNotExisting(projectRootDir);
createIdeaProjectDir(projectRootDir);
- Project newProject = createProject(projectName, projectFilePath);
- setUpProject(newProject, projectFilePath, androidSdk);
+ final Project newProject = createProject(projectName, projectRootDir.getPath());
+ setUpProject(newProject);
+
if (!ApplicationManager.getApplication().isUnitTestMode()) {
newProject.save();
}
- LocalProperties.createFile(newProject, androidSdk);
+ Projects.setProjectBuildAction(newProject, Projects.BuildAction.REBUILD);
- doImport(newProject, projectFilePath, callback);
+ doImport(newProject, true /* new project */, true /* synchronous import */, callback);
}
- @NotNull
- private static File createTopLevelBuildFile(@NotNull File projectRootDir) throws IOException {
- File projectFile = new File(projectRootDir, "build.gradle");
+ private static void createTopLevelBuildFileIfNotExisting(@NotNull File projectRootDir) throws IOException {
+ File projectFile = new File(projectRootDir, SdkConstants.FN_BUILD_GRADLE);
+ if (projectFile.isFile()) {
+ return;
+ }
FileUtilRt.createIfNotExists(projectFile);
String contents = "// Top-level build file where you can add configuration options common to all sub-projects/modules." +
SystemProperties.getLineSeparator();
FileUtil.writeToFile(projectFile, contents);
- return projectFile;
}
private static void createIdeaProjectDir(@NotNull File projectRootDir) throws IOException {
@@ -168,118 +160,98 @@
}
@NotNull
- private static Project createProject(@NotNull String projectName, @NotNull String projectFilePath) throws ConfigurationException {
+ private static Project createProject(@NotNull String projectName, @NotNull String projectPath) throws ConfigurationException {
ProjectManager projectManager = ProjectManager.getInstance();
- Project newProject = projectManager.createProject(projectName, projectFilePath);
+ Project newProject = projectManager.createProject(projectName, projectPath);
if (newProject == null) {
throw new NullPointerException("Failed to create a new IDEA project");
}
return newProject;
}
- private static void setUpProject(@NotNull final Project newProject,
- @NotNull final String projectFilePath,
- @NotNull final Sdk androidSdk) {
+ private static void setUpProject(@NotNull final Project newProject) {
CommandProcessor.getInstance().executeCommand(newProject, new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
- NewProjectUtil.applyJdkToProject(newProject, androidSdk);
+ Sdk sdk = Jdks.chooseOrCreateJavaSdk();
+ if (sdk != null) {
+ NewProjectUtil.applyJdkToProject(newProject, sdk);
+ }
// In practice, it really does not matter where the compiler output folder is. Gradle handles that. This is done just to please
// IDEA.
String compileOutputUrl = VfsUtilCore.pathToUrl(newProject.getBasePath() + "/build/classes");
CompilerProjectExtension compilerProjectExt = CompilerProjectExtension.getInstance(newProject);
assert compilerProjectExt != null;
compilerProjectExt.setCompilerOutputUrl(compileOutputUrl);
- setUpGradleSettings(newProject, projectFilePath);
+ setUpGradleSettings(newProject);
}
});
}
}, null, null);
}
- private static void setUpGradleSettings(@NotNull Project newProject, @NotNull String projectFilePath) {
- GradleSettings gradleSettings = GradleSettings.getInstance(newProject);
+ private static void setUpGradleSettings(@NotNull Project newProject) {
GradleProjectSettings projectSettings = new GradleProjectSettings();
projectSettings.setDistributionType(DistributionType.WRAPPED);
- projectSettings.setExternalProjectPath(projectFilePath);
+ projectSettings.setExternalProjectPath(newProject.getBasePath());
projectSettings.setUseAutoImport(true);
+
+ GradleSettings gradleSettings = GradleSettings.getInstance(newProject);
gradleSettings.setLinkedProjectsSettings(ImmutableList.of(projectSettings));
}
- private void doImport(@NotNull final Project project,
- @NotNull final String projectFilePath,
- @Nullable final Callback callback) throws ConfigurationException {
- Projects.setBuildAction(project, Projects.BuildAction.REBUILD);
-
- final Ref<ConfigurationException> errorRef = new Ref<ConfigurationException>();
-
- myDelegate.importProject(project, projectFilePath, new ExternalProjectRefreshCallback() {
+ private void doImport(@NotNull final Project project, final boolean newProject, boolean modal, @Nullable final Callback callback)
+ throws ConfigurationException {
+ myDelegate.importProject(project, new ExternalProjectRefreshCallback() {
@Override
public void onSuccess(@Nullable final DataNode<ProjectData> projectInfo) {
assert projectInfo != null;
+ final Application application = ApplicationManager.getApplication();
Runnable runnable = new Runnable() {
@Override
public void run() {
populateProject(project, projectInfo);
- open(project, projectFilePath);
+ if (newProject) {
+ open(project);
+ }
+ else {
+ updateStructureAccordingToBuildVariants(project);
+ }
- if (!ApplicationManager.getApplication().isUnitTestMode()) {
+ if (!application.isUnitTestMode()) {
project.save();
}
+ if (newProject) {
+ configureGradleProject(project);
+ }
+ if (callback != null) {
+ callback.projectImported(project);
+ }
}
};
- if (ApplicationManager.getApplication().isUnitTestMode()) {
+ if (application.isUnitTestMode()) {
runnable.run();
}
else {
- ApplicationManager.getApplication().invokeLater(runnable);
+ application.invokeLater(runnable);
}
}
@Override
public void onFailure(@NotNull final String errorMessage, @Nullable String errorDetails) {
- ConfigurationException error = handleImportFailure(errorMessage, errorDetails);
- errorRef.set(error);
- }
- });
-
- ConfigurationException errorCause = errorRef.get();
- if (errorCause != null) {
- throw errorCause;
- }
-
- // Since importing is synchronous we should have modules now. Notify callback.
- if (notifyCallback(project, callback)) {
- return;
- }
-
- // If we got here, there is some bad timing and the module creation got delayed somehow. Notify callback as soon as the project roots
- // are created.
- final MessageBusConnection connection = project.getMessageBus().connect();
- connection.subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootAdapter() {
- @Override
- public void rootsChanged(ModuleRootEvent event) {
- Module[] modules = ModuleManager.getInstance(project).getModules();
- if (modules.length > 0) {
- connection.disconnect();
- // TODO: Consider moving callback to AndroidProjectDataService. It can reliably notify when a project has modules.
- notifyCallback(project, callback);
+ if (errorDetails != null) {
+ LOG.warn(errorDetails);
+ }
+ String newMessage = ExternalSystemBundle.message("error.resolve.with.reason", errorMessage);
+ LOG.info(newMessage);
+ if (callback != null) {
+ callback.importFailed(project, newMessage);
}
}
- });
- }
-
- @NotNull
- private static ConfigurationException handleImportFailure(@NotNull String errorMessage, @Nullable String errorDetails) {
- if (errorDetails != null) {
- LOG.warn(errorDetails);
- }
- String reason = "Failed to import Gradle project: " + errorMessage;
- return new ConfigurationException(ExternalSystemBundle.message("error.resolve.with.reason", reason),
- ExternalSystemBundle.message("error.resolve.generic"));
+ }, modal);
}
private static void populateProject(@NotNull final Project newProject, @NotNull final DataNode<ProjectData> projectInfo) {
@@ -304,9 +276,9 @@
});
}
- private static void open(@NotNull final Project newProject, @NotNull String projectFilePath) {
+ private static void open(@NotNull final Project newProject) {
ProjectManagerEx projectManager = ProjectManagerEx.getInstanceEx();
- ProjectUtil.updateLastProjectLocation(projectFilePath);
+ ProjectUtil.updateLastProjectLocation(newProject.getBasePath());
if (WindowManager.getInstance().isFullScreenSupportedInCurrentOS()) {
IdeFocusManager instance = IdeFocusManager.findInstance();
IdeFrame lastFocusedFrame = instance.getLastFocusedFrame();
@@ -320,23 +292,40 @@
projectManager.openProject(newProject);
}
- private static boolean notifyCallback(@NotNull Project newProject, @Nullable Callback callback) {
- Module[] modules = ModuleManager.getInstance(newProject).getModules();
- if (modules.length == 0) {
- return false;
- }
- if (callback != null) {
- callback.projectImported(newProject);
- }
- return true;
+ private void updateStructureAccordingToBuildVariants(final Project project) {
+ // Update module dependencies, content roots and output paths. This needs to be done in case the selected variant is not
+ // the same one as the default (an by "default" we mean the first in the drop-down.)
+ ExternalSystemApiUtil.executeProjectChangeAction(true, new Runnable() {
+ @Override
+ public void run() {
+ ModuleManager moduleManager = ModuleManager.getInstance(project);
+ for (Module module : moduleManager.getModules()) {
+ AndroidFacet facet = Facets.getFirstFacetOfType(module, AndroidFacet.ID);
+ if (facet != null) {
+ IdeaAndroidProject ideaAndroidProject = facet.getIdeaAndroidProject();
+ for (ModuleCustomizer customizer : myModuleCustomizers) {
+ customizer.customizeModule(module, project, ideaAndroidProject);
+ }
+ }
+ }
+ }
+ });
+ }
+
+ private static void configureGradleProject(@NotNull Project project) {
+ // We need to do this because AndroidGradleProjectComponent#projectOpened is being called when the project is created, instead of when
+ // the project is opened. When 'projectOpened' is called, the project is not fully configured, and it does not looks like it is
+ // Gradle-based, resulting in listeners (e.g. modules added events) not being registered. Here we force the listeners to be registered.
+ AndroidGradleProjectComponent projectComponent = ServiceManager.getService(project, AndroidGradleProjectComponent.class);
+ projectComponent.configureGradleProject(false);
}
// Makes it possible to mock invocations to the Gradle Tooling API.
static class ImporterDelegate {
- void importProject(@NotNull Project newProject, @NotNull String projectFilePath, @NotNull ExternalProjectRefreshCallback callback)
+ void importProject(@NotNull Project project, @NotNull ExternalProjectRefreshCallback callback, boolean modal)
throws ConfigurationException {
try {
- ExternalSystemUtil.refreshProject(newProject, SYSTEM_ID, projectFilePath, callback, true, true);
+ ExternalSystemUtil.refreshProject(project, SYSTEM_ID, project.getBasePath(), callback, true, modal, true);
}
catch (RuntimeException e) {
String externalSystemName = SYSTEM_ID.getReadableName();
@@ -352,5 +341,7 @@
* @param project the IDEA project created from the Gradle one.
*/
void projectImported(@NotNull Project project);
+
+ void importFailed(@NotNull Project project, @NotNull String errorMessage);
}
}
diff --git a/android/src/com/android/tools/idea/gradle/project/ImportedDependencyUpdater.java b/android/src/com/android/tools/idea/gradle/project/ImportedDependencyUpdater.java
new file mode 100644
index 0000000..f16e63d
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/project/ImportedDependencyUpdater.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.project;
+
+import com.android.tools.idea.gradle.AndroidProjectKeys;
+import com.android.tools.idea.gradle.IdeaGradleProject;
+import com.android.tools.idea.gradle.ProjectImportEventMessage;
+import com.android.tools.idea.gradle.dependency.*;
+import com.android.tools.idea.gradle.dependency.LibraryDependency;
+import com.android.tools.idea.gradle.dependency.ModuleDependency;
+import com.google.common.base.Objects;
+import com.intellij.openapi.externalSystem.model.DataNode;
+import com.intellij.openapi.externalSystem.model.Key;
+import com.intellij.openapi.externalSystem.model.ProjectKeys;
+import com.intellij.openapi.externalSystem.model.project.*;
+import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
+import com.intellij.util.BooleanFunction;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.gradle.util.GradleConstants;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+class ImportedDependencyUpdater extends DependencyUpdater<DataNode<ModuleData>> {
+ @NotNull private final DataNode<ProjectData> myProjectInfo;
+ @NotNull private final Collection<DataNode<ModuleData>> myModules;
+
+ ImportedDependencyUpdater(@NotNull DataNode<ProjectData> projectInfo) {
+ myProjectInfo = projectInfo;
+ myModules = ExternalSystemApiUtil.getChildren(projectInfo, ProjectKeys.MODULE);
+ }
+
+ @Override
+ protected void updateDependency(@NotNull DataNode<ModuleData> moduleInfo, @NotNull LibraryDependency dependency) {
+ final LibraryData library = createLibraryData(dependency);
+ DataNode<LibraryData> libraryInfo =
+ ExternalSystemApiUtil.find(myProjectInfo, ProjectKeys.LIBRARY, new BooleanFunction<DataNode<LibraryData>>() {
+ @Override
+ public boolean fun(DataNode<LibraryData> node) {
+ // Match only by name and binary path. Source and Javadoc paths are not relevant for comparison.
+ LibraryData other = node.getData();
+ return library.getName().equals(other.getName()) &&
+ library.getPaths(LibraryPathType.BINARY).equals(other.getPaths(LibraryPathType.BINARY));
+ }
+ });
+ if (libraryInfo == null) {
+ libraryInfo = myProjectInfo.createChild(ProjectKeys.LIBRARY, library);
+ }
+ LibraryDependencyData dependencyInfo = new LibraryDependencyData(moduleInfo.getData(), libraryInfo.getData(), LibraryLevel.PROJECT);
+ dependencyInfo.setScope(dependency.getScope());
+ dependencyInfo.setExported(true);
+ moduleInfo.createChild(ProjectKeys.LIBRARY_DEPENDENCY, dependencyInfo);
+ }
+
+ @NotNull
+ private static LibraryData createLibraryData(@NotNull LibraryDependency dependency) {
+ LibraryData data = new LibraryData(GradleConstants.SYSTEM_ID, dependency.getName());
+ for (LibraryDependency.PathType type : LibraryDependency.PathType.values()) {
+ LibraryPathType newPathType = convertPathType(type);
+ for (String path : dependency.getPaths(type)) {
+ data.addPath(newPathType, path);
+ }
+ }
+ return data;
+ }
+
+ @NotNull
+ private static LibraryPathType convertPathType(@NotNull com.android.tools.idea.gradle.dependency.LibraryDependency.PathType pathType) {
+ String pathTypeAsString = pathType.toString();
+ LibraryPathType[] allTypes = LibraryPathType.values();
+ for (LibraryPathType type : allTypes) {
+ if (type.toString().equalsIgnoreCase(pathTypeAsString)) {
+ return type;
+ }
+ }
+ String msg = String.format("Unable to find a counterpart for '%1$s' in %2$s", pathTypeAsString, Arrays.toString(allTypes));
+ throw new IllegalArgumentException(msg);
+ }
+
+ @Override
+ protected boolean tryUpdating(@NotNull DataNode<ModuleData> moduleInfo, @NotNull ModuleDependency dependency) {
+ String dependencyName = findDependencyName(moduleInfo, dependency);
+ for (DataNode<ModuleData> module : myModules) {
+ String name = getNameOf(module);
+ if (name.equals(dependencyName)) {
+ ModuleDependencyData dependencyInfo = new ModuleDependencyData(moduleInfo.getData(), module.getData());
+ dependencyInfo.setScope(dependency.getScope());
+ dependencyInfo.setExported(true);
+ moduleInfo.createChild(ProjectKeys.MODULE_DEPENDENCY, dependencyInfo);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @NotNull
+ private String findDependencyName(@NotNull DataNode<ModuleData> moduleInfo, @NotNull ModuleDependency dependency) {
+ String dependencyName = dependency.getName();
+ String dependencyGradlePath = dependency.getGradlePath();
+ for (DataNode<ModuleData> module : myModules) {
+ String moduleName = getNameOf(module);
+ if (moduleName.equals(getNameOf(moduleInfo))) {
+ // this is the same module as the one we are configuring.
+ continue;
+ }
+ IdeaGradleProject gradleProject = getFirstNodeData(module, AndroidProjectKeys.IDE_GRADLE_PROJECT);
+ if (gradleProject != null && Objects.equal(dependencyGradlePath, gradleProject.getGradleProjectPath())) {
+ dependencyName = moduleName;
+ break;
+ }
+ }
+ return dependencyName;
+ }
+
+ @Nullable
+ private static <T> T getFirstNodeData(@NotNull DataNode<ModuleData> moduleInfo, @NotNull Key<T> key) {
+ Collection<DataNode<T>> children = ExternalSystemApiUtil.getChildren(moduleInfo, key);
+ return getFirstNodeData(children);
+ }
+
+ @Nullable
+ private static <T> T getFirstNodeData(Collection<DataNode<T>> nodes) {
+ DataNode<T> node = ContainerUtil.getFirstItem(nodes);
+ return node != null ? node.getData() : null;
+ }
+
+ @NotNull
+ @Override
+ protected String getNameOf(@NotNull DataNode<ModuleData> moduleInfo) {
+ return moduleInfo.getData().getName();
+ }
+
+ @Override
+ protected void log(@NotNull DataNode<ModuleData> moduleInfo, @NotNull String category, @NotNull String message) {
+ moduleInfo.createChild(AndroidProjectKeys.IMPORT_EVENT_MSG, new ProjectImportEventMessage(category, message));
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/project/LibraryDependency.java b/android/src/com/android/tools/idea/gradle/project/LibraryDependency.java
deleted file mode 100644
index 07c7f9e..0000000
--- a/android/src/com/android/tools/idea/gradle/project/LibraryDependency.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tools.idea.gradle.project;
-
-import com.intellij.openapi.externalSystem.model.DataNode;
-import com.intellij.openapi.externalSystem.model.ProjectKeys;
-import com.intellij.openapi.externalSystem.model.project.*;
-import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
-import com.intellij.openapi.roots.DependencyScope;
-import com.intellij.openapi.util.io.FileUtil;
-import com.intellij.util.BooleanFunction;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.jetbrains.plugins.gradle.util.GradleConstants;
-
-import java.io.File;
-
-/**
- * Dependency of an IDEA module on a Java library.
- */
-class LibraryDependency {
- @NotNull private final LibraryData myLibraryData;
-
- @NotNull private DependencyScope myScope = DependencyScope.COMPILE;
-
- /**
- * Creates a new {@link LibraryDependency}.
- *
- * @param binaryPath the path of the library file to depend on.
- */
- LibraryDependency(@NotNull File binaryPath) {
- this(FileUtil.getNameWithoutExtension(binaryPath));
- }
-
- /**
- * Creates a new {@link LibraryDependency}.
- *
- * @param name the name of the library.
- */
- LibraryDependency(@NotNull String name) {
- myLibraryData = new LibraryData(GradleConstants.SYSTEM_ID, name);
- }
-
- void addPath(@NotNull LibraryPathType pathType, @Nullable File path) {
- if (path != null) {
- myLibraryData.addPath(pathType, path.getAbsolutePath());
- }
- }
-
- void setScope(@NotNull DependencyScope scope) {
- myScope = scope;
- }
-
- void addTo(@NotNull DataNode<ModuleData> moduleInfo, @NotNull DataNode<ProjectData> projectInfo) {
- DataNode<LibraryData> libraryInfo =
- ExternalSystemApiUtil.find(projectInfo, ProjectKeys.LIBRARY, new BooleanFunction<DataNode<LibraryData>>() {
- @Override
- public boolean fun(DataNode<LibraryData> node) {
- // Match only by name and binary path. Source and Javadoc paths are not relevant for comparison.
- LibraryData other = node.getData();
- return myLibraryData.getName().equals(other.getName()) &&
- myLibraryData.getPaths(LibraryPathType.BINARY).equals(other.getPaths(LibraryPathType.BINARY));
- }
- });
- if (libraryInfo == null) {
- libraryInfo = projectInfo.createChild(ProjectKeys.LIBRARY, myLibraryData);
- }
- LibraryDependencyData dependencyInfo = new LibraryDependencyData(moduleInfo.getData(), libraryInfo.getData(), LibraryLevel.PROJECT);
- dependencyInfo.setScope(myScope);
- moduleInfo.createChild(ProjectKeys.LIBRARY_DEPENDENCY, dependencyInfo);
- }
-}
diff --git a/android/src/com/android/tools/idea/gradle/project/ModuleDependency.java b/android/src/com/android/tools/idea/gradle/project/ModuleDependency.java
deleted file mode 100644
index cc22933..0000000
--- a/android/src/com/android/tools/idea/gradle/project/ModuleDependency.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tools.idea.gradle.project;
-
-import com.google.common.collect.Sets;
-import com.intellij.openapi.externalSystem.model.DataNode;
-import com.intellij.openapi.externalSystem.model.ProjectKeys;
-import com.intellij.openapi.externalSystem.model.project.ModuleData;
-import com.intellij.openapi.externalSystem.model.project.ModuleDependencyData;
-import com.intellij.openapi.externalSystem.model.project.ProjectData;
-import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
-import com.intellij.openapi.roots.DependencyScope;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.Collection;
-import java.util.Set;
-
-/**
- * Dependency of an IDEA module on another module.
- */
-class ModuleDependency {
- @NotNull private final String myModuleName;
-
- @NotNull private DependencyScope myScope = DependencyScope.COMPILE;
-
- /**
- * Creates a new {@link ModuleDependency}.
- *
- * @param moduleName the name of the IDEA module to depend on.
- */
- ModuleDependency(@NotNull String moduleName) {
- myModuleName = moduleName;
- }
-
- void setScope(@NotNull DependencyScope scope) {
- myScope = scope;
- }
-
- void addTo(@NotNull DataNode<ModuleData> moduleInfo, @NotNull DataNode<ProjectData> projectInfo) {
- Set<String> registeredModuleNames = Sets.newHashSet();
- Collection<DataNode<ModuleData>> modules = ExternalSystemApiUtil.getChildren(projectInfo, ProjectKeys.MODULE);
- for (DataNode<ModuleData> m : modules) {
- String name = m.getData().getName();
- registeredModuleNames.add(name);
- if (name.equals(myModuleName)) {
- ModuleDependencyData dependencyInfo = new ModuleDependencyData(moduleInfo.getData(), m.getData());
- dependencyInfo.setScope(myScope);
- moduleInfo.createChild(ProjectKeys.MODULE_DEPENDENCY, dependencyInfo);
- return;
- }
- }
- String format = "Unable to find module with name '%1$s'. Registered modules: %2$s";
- String msg = String.format(format, myModuleName, registeredModuleNames);
- throw new IllegalStateException(msg);
- }
-}
diff --git a/android/src/com/android/tools/idea/gradle/project/ProjectImportErrorHandler.java b/android/src/com/android/tools/idea/gradle/project/ProjectImportErrorHandler.java
new file mode 100644
index 0000000..c8ad4a2
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/project/ProjectImportErrorHandler.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.project;
+
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.externalSystem.model.ExternalSystemException;
+import com.intellij.openapi.util.Pair;
+import org.gradle.api.internal.LocationAwareException;
+import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.net.ConnectException;
+import java.net.UnknownHostException;
+
+/**
+ * Provides better error messages for project import failures.
+ */
+public class ProjectImportErrorHandler {
+ private static final Logger LOG = Logger.getInstance(ProjectImportErrorHandler.class);
+
+ @NonNls private static final String MINIMUM_GRADLE_SUPPORTED_VERSION = "1.6";
+
+ private static final String EMPTY_LINE = "\n\n";
+ private static final String UNSUPPORTED_GRADLE_VERSION_ERROR = "Gradle version " + MINIMUM_GRADLE_SUPPORTED_VERSION + " is required";
+
+ public interface NotificationHints {
+ String OPEN_GRADLE_SETTINGS = "Please fix the project's Gradle settings.";
+ String FAILED_TO_PARSE_SDK = "failed to parse SDK";
+ String INSTALL_ANDROID_SUPPORT_REPO = "Please install the Android Support Repository from the Android SDK Manager.";
+ String SET_UP_HTTP_PROXY = "If you are behind an HTTP proxy, please configure the proxy settings either in Android Studio or Gradle.";
+ String UNEXPECTED_ERROR_FILE_BUG = "This is an unexpected error. Please file a bug containing the idea.log file.";
+ }
+
+ @NotNull
+ ExternalSystemException getUserFriendlyError(@NotNull Throwable error, @NotNull String projectPath, @Nullable String buildFilePath) {
+ if (error instanceof ExternalSystemException) {
+ // This is already a user-friendly error.
+ return (ExternalSystemException)error;
+ }
+
+ LOG.info(String.format("Failed to import Gradle project at '%1$s'", projectPath), error);
+
+ Pair<Throwable, String> rootCauseAndLocation = getRootCauseAndLocation(error);
+
+ Throwable rootCause = rootCauseAndLocation.getFirst();
+
+ String location = rootCauseAndLocation.getSecond();
+ if (location == null && !Strings.isNullOrEmpty(buildFilePath)) {
+ location = String.format("Build file: '%1$s'", buildFilePath);
+ }
+
+ if (isOldGradleVersion(rootCause)) {
+ String msg = String.format("You are using an old, unsupported version of Gradle. Please use version %1$s or greater.",
+ MINIMUM_GRADLE_SUPPORTED_VERSION);
+ // Location of build.gradle is useless for this error. Omitting it.
+ return createUserFriendlyError(msg, null);
+ }
+
+ if (rootCause instanceof OutOfMemoryError) {
+ // The OutOfMemoryError happens in the Gradle daemon process.
+ String originalMessage = rootCause.getMessage();
+ String msg = "Out of memory";
+ if (originalMessage != null && !originalMessage.isEmpty()) {
+ msg = msg + ": " + originalMessage;
+ }
+ if (msg.endsWith("Java heap space")) {
+ msg += ". Configure Gradle memory settings using '-Xmx' JVM option (e.g. '-Xmx2048m'.)";
+ } else if (!msg.endsWith(".")) {
+ msg += ".";
+ }
+ msg += EMPTY_LINE + NotificationHints.OPEN_GRADLE_SETTINGS;
+ // Location of build.gradle is useless for this error. Omitting it.
+ return createUserFriendlyError(msg, null);
+ }
+
+ if (rootCause instanceof ClassNotFoundException) {
+ String msg = String.format("Unable to load class '%1$s'.", rootCause.getMessage()) + EMPTY_LINE +
+ NotificationHints.UNEXPECTED_ERROR_FILE_BUG;
+ // Location of build.gradle is useless for this error. Omitting it.
+ return createUserFriendlyError(msg, null);
+ }
+
+ if (rootCause instanceof UnknownHostException) {
+ String msg = String.format("Unknown host '%1$s'.", rootCause.getMessage()) +
+ EMPTY_LINE + "Please ensure the host name is correct. " +
+ NotificationHints.SET_UP_HTTP_PROXY;
+ // Location of build.gradle is useless for this error. Omitting it.
+ return createUserFriendlyError(msg, null);
+ }
+
+ if (rootCause instanceof ConnectException) {
+ String msg = rootCause.getMessage();
+ if (msg != null && msg.contains("timed out")) {
+ msg += msg.endsWith(".") ? " " : ". ";
+ msg += NotificationHints.SET_UP_HTTP_PROXY;
+ return createUserFriendlyError(msg, null);
+ }
+ }
+
+ if (rootCause instanceof RuntimeException) {
+ String msg = rootCause.getMessage();
+
+ if (msg != null && msg.startsWith(UNSUPPORTED_GRADLE_VERSION_ERROR)) {
+ if (!msg.endsWith(".")) {
+ msg += ".";
+ }
+ msg += EMPTY_LINE + NotificationHints.OPEN_GRADLE_SETTINGS;
+ // Location of build.gradle is useless for this error. Omitting it.
+ return createUserFriendlyError(msg, null);
+ }
+
+ // With this condition we cover 2 similar messages about the same problem.
+ if (msg != null && msg.contains("Could not find") && msg.contains("com.android.support:support")) {
+ // We keep the original error message and we append a hint about how to fix the missing dependency.
+ String newMsg = msg + EMPTY_LINE + NotificationHints.INSTALL_ANDROID_SUPPORT_REPO;
+ // Location of build.gradle is useless for this error. Omitting it.
+ return createUserFriendlyError(newMsg, null);
+ }
+
+ if (msg != null && msg.contains(NotificationHints.FAILED_TO_PARSE_SDK)) {
+ String newMsg = msg + EMPTY_LINE + "The Android SDK may be missing the directory 'add-ons'.";
+ // Location of build.gradle is useless for this error. Omitting it.
+ return createUserFriendlyError(newMsg, null);
+ }
+ }
+
+ return createUserFriendlyError(rootCause.getMessage(), location);
+ }
+
+ @NotNull
+ private static Pair<Throwable, String> getRootCauseAndLocation(@NotNull Throwable error) {
+ Throwable rootCause = error;
+ String location = null;
+ while (true) {
+ if (location == null) {
+ location = getLocationFrom(rootCause);
+ }
+ if (rootCause.getCause() == null || rootCause.getCause().getMessage() == null) {
+ break;
+ }
+ rootCause = rootCause.getCause();
+ }
+ //noinspection ConstantConditions
+ return Pair.create(rootCause, location);
+ }
+
+ @Nullable
+ private static String getLocationFrom(@NotNull Throwable error) {
+ String errorToString = error.toString();
+ if (errorToString != null && errorToString.startsWith(LocationAwareException.class.getName())) {
+ // LocationAwareException is never passed, but converted into a PlaceholderException that has the toString value of the original
+ // LocationAwareException.
+ String location = error.getMessage();
+ if (location != null && location.startsWith("Build file '")) {
+ // Only the first line contains the location of the error. Discard the rest.
+ Iterable<String> lines = Splitter.on('\n').split(location);
+ return lines.iterator().next();
+ }
+ }
+ return null;
+ }
+
+ private static boolean isOldGradleVersion(@NotNull Throwable error) {
+ if (error instanceof ClassNotFoundException) {
+ String msg = error.getMessage();
+ if (msg != null && msg.contains(ToolingModelBuilderRegistry.class.getName())) {
+ return true;
+ }
+ }
+ String errorToString = error.toString();
+ return errorToString != null && errorToString.startsWith("org.gradle.api.internal.MissingMethodException");
+ }
+
+ @NotNull
+ private static ExternalSystemException createUserFriendlyError(@NotNull String msg, @Nullable String location) {
+ String newMsg = msg;
+ if (!newMsg.isEmpty() && Character.isLowerCase(newMsg.charAt(0))) {
+ // Message starts with lower case letter. Sentences should start with uppercase.
+ newMsg = "Cause: " + newMsg;
+ }
+
+ if (!Strings.isNullOrEmpty(location)) {
+ StringBuilder msgBuilder = new StringBuilder();
+ msgBuilder.append(newMsg).append(EMPTY_LINE).append(location);
+ newMsg = msgBuilder.toString();
+ }
+ return new ExternalSystemException(newMsg);
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/project/ProjectImportEventLogger.java b/android/src/com/android/tools/idea/gradle/project/ProjectImportEventLogger.java
new file mode 100644
index 0000000..37d5166
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/project/ProjectImportEventLogger.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.project;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Logs event that occur during project import. Such events are displayed to the user once project import is complete.
+ */
+public interface ProjectImportEventLogger {
+ void log(@NotNull String category, @NotNull String message);
+}
diff --git a/android/src/com/android/tools/idea/gradle/project/ProjectResolver.java b/android/src/com/android/tools/idea/gradle/project/ProjectResolver.java
deleted file mode 100755
index 6336533..0000000
--- a/android/src/com/android/tools/idea/gradle/project/ProjectResolver.java
+++ /dev/null
@@ -1,357 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tools.idea.gradle.project;
-
-import com.android.build.gradle.model.AndroidProject;
-import com.android.build.gradle.model.Variant;
-import com.android.tools.idea.gradle.AndroidProjectKeys;
-import com.android.tools.idea.gradle.IdeaAndroidProject;
-import com.android.tools.idea.gradle.IdeaGradleProject;
-import com.android.tools.idea.gradle.model.AndroidContentRoot;
-import com.android.tools.idea.gradle.model.AndroidDependencies;
-import com.google.common.collect.Lists;
-import com.intellij.externalSystem.JavaProjectData;
-import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.externalSystem.model.DataNode;
-import com.intellij.openapi.externalSystem.model.ProjectKeys;
-import com.intellij.openapi.externalSystem.model.project.*;
-import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId;
-import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListener;
-import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
-import com.intellij.openapi.module.StdModuleTypes;
-import com.intellij.openapi.roots.DependencyScope;
-import com.intellij.openapi.util.io.FileUtil;
-import com.intellij.util.ExceptionUtil;
-import com.intellij.util.Function;
-import com.intellij.util.PathUtil;
-import com.intellij.util.containers.ContainerUtil;
-import org.gradle.tooling.BuildException;
-import org.gradle.tooling.ModelBuilder;
-import org.gradle.tooling.ProjectConnection;
-import org.gradle.tooling.UnknownModelException;
-import org.gradle.tooling.model.idea.IdeaContentRoot;
-import org.gradle.tooling.model.idea.IdeaModule;
-import org.gradle.tooling.model.idea.IdeaProject;
-import org.jetbrains.annotations.NonNls;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.jetbrains.plugins.gradle.service.project.GradleExecutionHelper;
-import org.jetbrains.plugins.gradle.settings.GradleExecutionSettings;
-import org.jetbrains.plugins.gradle.util.GradleConstants;
-import org.jetbrains.plugins.gradle.util.GradleUtil;
-
-import java.io.File;
-import java.io.FilenameFilter;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Imports a Android-Gradle projects into IDEA. The set of projects to import may include regular Java projects as well.
- */
-class ProjectResolver {
- private static final Logger LOG = Logger.getInstance(ProjectResolver.class);
-
- @NonNls private static final String GRADLE_PATH_SEPARATOR = ":";
-
- @NotNull final GradleExecutionHelper myHelper;
-
- ProjectResolver(@NotNull GradleExecutionHelper helper) {
- myHelper = helper;
- }
-
- /**
- * Imports multiple Android-Gradle projects. The set of projects to import may include regular Java projects as well.
- *
- * </p>Since the Android Gradle model does not support multiple projects, we query the Gradle Tooling API for a regular Java
- * multi-project. Then, for each of the modules in the imported project, we query for an (@link AndroidProject Android Gradle model.) If
- * we get one we create an IDE module from it, otherwise we just use the regular Java module. Unfortunately, this process requires
- * creation of multiple {@link ProjectConnection}s.
- *
- * @param id id of the current 'resolve project info' task.
- * @param projectPath absolute path of the build.gradle file. It includes the file name.
- * @param settings settings to use for the project resolving; {@code null} as indication that no specific settings are required.
- * @param connection Gradle Tooling API connection to the project to import.
- * @param listener callback to be notified about the execution
- * @return the imported project, or {@link null} if the project to import is not an Android-Gradle project.
- */
- @Nullable
- DataNode<ProjectData> resolveProjectInfo(@NotNull ExternalSystemTaskId id,
- @NotNull String projectPath,
- @Nullable GradleExecutionSettings settings,
- @NotNull ProjectConnection connection,
- @NotNull ExternalSystemTaskNotificationListener listener) {
- String projectDirPath = PathUtil.getParentPath(projectPath);
-
- ModelBuilder<IdeaProject> modelBuilder = myHelper.getModelBuilder(IdeaProject.class,
- id,
- settings,
- connection,
- listener,
- Collections.<String>emptyList());
- IdeaProject ideaProject = modelBuilder.get();
- if (ideaProject == null || ideaProject.getModules().isEmpty()) {
- return null;
- }
-
- String name = new File(projectDirPath).getName();
- DataNode<ProjectData> projectInfo = createProjectInfo(projectDirPath, projectPath, name);
-
- AndroidProject first = null;
-
- for (IdeaModule module : ideaProject.getModules()) {
- IdeaGradleProject gradleProject = new IdeaGradleProject(module.getName(), module.getGradleProject().getPath());
- String relativePath = getRelativePath(gradleProject);
- File moduleDir = new File(projectDirPath, relativePath);
- String gradleBuildFilePath = getGradleBuildFilePath(moduleDir);
- if (gradleBuildFilePath == null) {
- continue;
- }
- String moduleDirPath = moduleDir.getAbsolutePath();
- AndroidProject androidProject = getAndroidProject(id, gradleBuildFilePath, settings, listener);
- if (androidProject != null) {
- createModuleInfo(module, androidProject, projectInfo, moduleDirPath, projectPath, gradleProject);
- if (first == null) {
- first = androidProject;
- }
- continue;
- }
- createModuleInfo(module, projectInfo, moduleDirPath, gradleProject);
- }
-
- if (first == null) {
- // Don't import project if we don't have at least one AndroidProject.
- return null;
- }
-
- populateDependencies(projectInfo);
- return projectInfo;
- }
-
- @NotNull
- private static DataNode<ProjectData> createProjectInfo(@NotNull String projectDirPath,
- @NotNull String projectPath,
- @NotNull String name) {
- ProjectData projectData = new ProjectData(GradleConstants.SYSTEM_ID, projectDirPath, projectPath);
- projectData.setName(name);
-
- DataNode<ProjectData> projectInfo = new DataNode<ProjectData>(ProjectKeys.PROJECT, projectData, null);
-
- // Gradle API doesn't expose project compile output path yet.
- JavaProjectData javaProjectData = new JavaProjectData(GradleConstants.SYSTEM_ID, projectDirPath + "/build/classes");
- projectInfo.createChild(JavaProjectData.KEY, javaProjectData);
-
- return projectInfo;
- }
-
- @NotNull
- private static String getRelativePath(@NotNull IdeaGradleProject gradleProject) {
- String separator = File.separator;
- if (separator.equals("\\")) {
- separator = "\\\\";
- }
- String gradleProjectPath = gradleProject.getGradleProjectPath();
- return gradleProjectPath.replaceAll(GRADLE_PATH_SEPARATOR, separator);
- }
-
- @Nullable
- private static String getGradleBuildFilePath(@NotNull final File projectDir) {
- File[] children = projectDir.listFiles(new FilenameFilter() {
- @Override
- public boolean accept(File dir, String name) {
- return "build.gradle".equals(name) && FileUtil.filesEqual(projectDir, dir);
- }
- });
- if (children != null && children.length == 1) {
- return children[0].getAbsolutePath();
- }
- return null;
- }
-
- @Nullable
- private AndroidProject getAndroidProject(@NotNull final ExternalSystemTaskId id,
- @NotNull String projectPath,
- @Nullable final GradleExecutionSettings settings,
- @NotNull final ExternalSystemTaskNotificationListener listener) {
- return myHelper.execute(projectPath, settings, new Function<ProjectConnection, AndroidProject>() {
- @Nullable
- @Override
- public AndroidProject fun(ProjectConnection connection) {
- try {
- ModelBuilder<AndroidProject> modelBuilder = myHelper.getModelBuilder(AndroidProject.class,
- id,
- settings,
- connection,
- listener,
- Collections.<String>emptyList());
- return modelBuilder.get();
- }
- catch (RuntimeException e) {
- handleProjectImportError(e);
- }
- return null;
- }
- });
- }
-
- private static void handleProjectImportError(@NotNull RuntimeException e) {
- if (e instanceof UnknownModelException) {
- return;
- }
- Throwable root = e;
- if (e instanceof BuildException) {
- root = ExceptionUtil.getRootCause(e);
- }
- LOG.error(root);
- }
-
- @Nullable
- static IdeaAndroidProject getIdeaAndroidProject(@NotNull DataNode<ModuleData> moduleInfo) {
- Collection<DataNode<IdeaAndroidProject>> projects =
- ExternalSystemApiUtil.getChildren(moduleInfo, AndroidProjectKeys.IDE_ANDROID_PROJECT);
- return getFirstNodeData(projects);
- }
-
- @Nullable
- static <T> T getFirstNodeData(Collection<DataNode<T>> nodes) {
- DataNode<T> node = ContainerUtil.getFirstItem(nodes);
- return node != null ? node.getData() : null;
- }
-
- @NotNull
- private static DataNode<ModuleData> createModuleInfo(@NotNull IdeaModule module,
- @NotNull AndroidProject androidProject,
- @NotNull DataNode<ProjectData> projectInfo,
- @NotNull String moduleDirPath,
- @NotNull String rootGradleProjectPath,
- @NotNull IdeaGradleProject gradleProject) {
- String moduleName = module.getName();
- ModuleData moduleData = createModuleData(module, projectInfo, moduleName, moduleDirPath);
- DataNode<ModuleData> moduleInfo = projectInfo.createChild(ProjectKeys.MODULE, moduleData);
-
- Variant selectedVariant = getFirstVariant(androidProject);
- IdeaAndroidProject ideaAndroidProject =
- new IdeaAndroidProject(moduleName, moduleDirPath, rootGradleProjectPath, androidProject, selectedVariant.getName());
- addContentRoot(ideaAndroidProject, moduleInfo, moduleDirPath);
-
- moduleInfo.createChild(AndroidProjectKeys.IDE_ANDROID_PROJECT, ideaAndroidProject);
- moduleInfo.createChild(AndroidProjectKeys.GRADLE_PROJECT, gradleProject);
- return moduleInfo;
- }
-
- @NotNull
- private static Variant getFirstVariant(@NotNull AndroidProject androidProject) {
- Map<String, Variant> variants = androidProject.getVariants();
- if (variants.size() == 1) {
- Variant variant = ContainerUtil.getFirstItem(variants.values());
- assert variant != null;
- return variant;
- }
- List<String> variantNames = Lists.newArrayList(variants.keySet());
- Collections.sort(variantNames);
- return variants.get(variantNames.get(0));
- }
-
- private static void addContentRoot(@NotNull IdeaAndroidProject androidProject,
- @NotNull DataNode<ModuleData> moduleInfo,
- @NotNull String moduleDirPath) {
- final ContentRootData contentRootData = new ContentRootData(GradleConstants.SYSTEM_ID, moduleDirPath);
- AndroidContentRoot.ContentRootStorage storage = new AndroidContentRoot.ContentRootStorage() {
- @Override
- @NotNull
- public String getRootDirPath() {
- return contentRootData.getRootPath();
- }
-
- @Override
- public void storePath(@NotNull ExternalSystemSourceType sourceType, @NotNull File directory) {
- contentRootData.storePath(sourceType, directory.getAbsolutePath());
- }
- };
- AndroidContentRoot.storePaths(androidProject, storage);
- moduleInfo.createChild(ProjectKeys.CONTENT_ROOT, contentRootData);
- }
-
- @NotNull
- private static DataNode<ModuleData> createModuleInfo(@NotNull IdeaModule module,
- @NotNull DataNode<ProjectData> projectInfo,
- @NotNull String moduleDirPath,
- @NotNull IdeaGradleProject gradleProject) {
- ModuleData moduleData = createModuleData(module, projectInfo, module.getName(), moduleDirPath);
- DataNode<ModuleData> moduleInfo = projectInfo.createChild(ProjectKeys.MODULE, moduleData);
-
- // Populate content roots.
- for (IdeaContentRoot from : module.getContentRoots()) {
- if (from == null || from.getRootDirectory() == null) {
- continue;
- }
- ContentRootData contentRootData = new ContentRootData(GradleConstants.SYSTEM_ID, from.getRootDirectory().getAbsolutePath());
- GradleContentRoot.storePaths(from, contentRootData);
- moduleInfo.createChild(ProjectKeys.CONTENT_ROOT, contentRootData);
- }
-
- moduleInfo.createChild(AndroidProjectKeys.IDEA_MODULE, module);
- moduleInfo.createChild(AndroidProjectKeys.GRADLE_PROJECT, gradleProject);
- return moduleInfo;
- }
-
- private static ModuleData createModuleData(@NotNull IdeaModule module,
- @NotNull DataNode<ProjectData> projectInfo,
- @NotNull String name,
- @NotNull String dirPath) {
- String moduleConfigPath = GradleUtil.getConfigPath(module.getGradleProject(), projectInfo.getData().getLinkedExternalProjectPath());
- return new ModuleData(GradleConstants.SYSTEM_ID, StdModuleTypes.JAVA.getId(), name, dirPath, moduleConfigPath);
- }
-
- private static void populateDependencies(@NotNull DataNode<ProjectData> projectInfo) {
- Collection<DataNode<ModuleData>> modules = ExternalSystemApiUtil.getChildren(projectInfo, ProjectKeys.MODULE);
- for (DataNode<ModuleData> moduleInfo : modules) {
- IdeaAndroidProject androidProject = getIdeaAndroidProject(moduleInfo);
- if (androidProject != null) {
- populateDependencies(projectInfo, moduleInfo, androidProject);
- continue;
- }
- IdeaModule module = extractIdeaModule(moduleInfo);
- if (module != null) {
- GradleDependencies.populate(moduleInfo, projectInfo, module);
- }
- }
- }
-
- private static void populateDependencies(@NotNull final DataNode<ProjectData> projectInfo,
- @NotNull final DataNode<ModuleData> moduleInfo,
- @NotNull IdeaAndroidProject ideaAndroidProject) {
- AndroidDependencies.DependencyFactory dependencyFactory = new AndroidDependencies.DependencyFactory() {
- @Override
- public void addDependency(@NotNull DependencyScope scope, @NotNull String name, @NotNull File binaryPath) {
- LibraryDependency dependency = new LibraryDependency(name);
- dependency.setScope(scope);
- dependency.addPath(LibraryPathType.BINARY, binaryPath);
- dependency.addTo(moduleInfo, projectInfo);
- }
- };
- AndroidDependencies.populate(ideaAndroidProject, dependencyFactory);
- }
-
- @Nullable
- private static IdeaModule extractIdeaModule(@NotNull DataNode<ModuleData> moduleInfo) {
- Collection<DataNode<IdeaModule>> modules = ExternalSystemApiUtil.getChildren(moduleInfo, AndroidProjectKeys.IDEA_MODULE);
- // it is safe to remove this node. We only needed it to resolve dependencies.
- moduleInfo.getChildren().removeAll(modules);
- return getFirstNodeData(modules);
- }
-}
diff --git a/android/src/com/android/tools/idea/gradle/service/AndroidProjectDataService.java b/android/src/com/android/tools/idea/gradle/service/AndroidProjectDataService.java
index 4f0c159..ecf06d6 100644
--- a/android/src/com/android/tools/idea/gradle/service/AndroidProjectDataService.java
+++ b/android/src/com/android/tools/idea/gradle/service/AndroidProjectDataService.java
@@ -18,13 +18,9 @@
import com.android.tools.idea.gradle.AndroidProjectKeys;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.android.tools.idea.gradle.customizer.*;
-import com.android.tools.idea.gradle.util.Projects;
-import com.android.tools.idea.gradle.variant.view.BuildVariantView;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
-import com.intellij.openapi.application.Application;
-import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.externalSystem.model.DataNode;
import com.intellij.openapi.externalSystem.model.Key;
import com.intellij.openapi.externalSystem.service.project.manage.ProjectDataService;
@@ -47,15 +43,13 @@
// This constructor is called by the IDE. See this module's plugin.xml file, implementation of extension 'externalProjectDataService'.
public AndroidProjectDataService() {
- myCustomizers =
- new ModuleCustomizer[]{
- new AndroidSdkModuleCustomizer(), new AndroidFacetModuleCustomizer(), new RunConfigModuleCustomizer(),
- new ContentRootModuleCustomizer(), new CompilerOutputPathModuleCustomizer()
- };
+ //noinspection TestOnlyProblems
+ this(new AndroidSdkModuleCustomizer(), new AndroidFacetModuleCustomizer(), new RunConfigModuleCustomizer(),
+ new CompilerOutputPathModuleCustomizer());
}
@VisibleForTesting
- AndroidProjectDataService(@NotNull ModuleCustomizer...customizers) {
+ AndroidProjectDataService(@NotNull ModuleCustomizer... customizers) {
myCustomizers = customizers;
}
@@ -81,7 +75,6 @@
}
final List<Module> modules = ImmutableList.copyOf(ModuleManager.getInstance(project).getModules());
- final Application application = ApplicationManager.getApplication();
ExternalSystemApiUtil.executeProjectChangeAction(synchronous, new Runnable() {
@Override
@@ -91,37 +84,8 @@
IdeaAndroidProject androidProject = androidProjectsByModuleName.get(module.getName());
customizeModule(module, project, androidProject);
}
- application.invokeLater(new Runnable() {
- @Override
- public void run() {
- BuildVariantView buildVariantView = BuildVariantView.getInstance(project);
- buildVariantView.updateContents();
- }
- });
}
});
- if (!application.isUnitTestMode()) {
- application.invokeLater(new Runnable() {
- @Override
- public void run() {
- Projects.BuildAction buildAction = Projects.getBuildAction(project);
- if (buildAction == null) {
- // This happens when the project is imported and this is the first pass of the 2-pass import. Rebuild on second pass.
- Projects.setBuildAction(project, Projects.BuildAction.REBUILD);
- }
- else {
- switch (buildAction) {
- case COMPILE:
- Projects.compile(project, project.getBasePath());
- break;
- case REBUILD:
- Projects.rebuild(project, project.getBasePath());
- }
- Projects.removeBuildAction(project);
- }
- }
- });
- }
}
@NotNull
diff --git a/android/src/com/android/tools/idea/gradle/service/GradleProjectDataService.java b/android/src/com/android/tools/idea/gradle/service/GradleProjectDataService.java
index e4adf0e..899f40d 100644
--- a/android/src/com/android/tools/idea/gradle/service/GradleProjectDataService.java
+++ b/android/src/com/android/tools/idea/gradle/service/GradleProjectDataService.java
@@ -19,10 +19,13 @@
import com.android.tools.idea.gradle.IdeaGradleProject;
import com.android.tools.idea.gradle.facet.AndroidGradleFacet;
import com.android.tools.idea.gradle.util.Facets;
+import com.android.tools.idea.gradle.util.Projects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.intellij.facet.FacetManager;
import com.intellij.facet.ModifiableFacetModel;
+import com.intellij.openapi.application.Application;
+import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.externalSystem.model.DataNode;
import com.intellij.openapi.externalSystem.model.Key;
import com.intellij.openapi.externalSystem.service.project.manage.ProjectDataService;
@@ -43,38 +46,61 @@
@NotNull
@Override
public Key<IdeaGradleProject> getTargetDataKey() {
- return AndroidProjectKeys.GRADLE_PROJECT;
+ return AndroidProjectKeys.IDE_GRADLE_PROJECT;
}
@Override
public void importData(@NotNull final Collection<DataNode<IdeaGradleProject>> toImport,
@NotNull final Project project,
boolean synchronous) {
- if (toImport.isEmpty()) {
- return;
- }
- ModuleManager moduleManager = ModuleManager.getInstance(project);
- final List<Module> modules = ImmutableList.copyOf(moduleManager.getModules());
- ExternalSystemApiUtil.executeProjectChangeAction(synchronous, new Runnable() {
- @Override
- public void run() {
- Map<String, IdeaGradleProject> gradleProjectsByName = indexByModuleName(toImport);
- for (Module module : modules) {
- IdeaGradleProject gradleProject = gradleProjectsByName.get(module.getName());
- customizeModule(module, gradleProject);
+ if (!toImport.isEmpty()) {
+ ModuleManager moduleManager = ModuleManager.getInstance(project);
+ final List<Module> modules = ImmutableList.copyOf(moduleManager.getModules());
+ ExternalSystemApiUtil.executeProjectChangeAction(synchronous, new Runnable() {
+ @Override
+ public void run() {
+ Map<String, IdeaGradleProject> gradleProjectsByName = indexByModuleName(toImport);
+ for (Module module : modules) {
+ IdeaGradleProject gradleProject = gradleProjectsByName.get(module.getName());
+ if (gradleProject == null) {
+ // This happens when there is an orphan IDEA module that does not map to a Gradle project. One way for this to happen is when
+ // opening a project created in another machine, and Gradle import assigns a different name to a module. Then, user decides not
+ // to delete the orphan module when Studio prompts to do so.
+ Facets.removeAllFacetsOfType(module, AndroidGradleFacet.TYPE_ID);
+ } else {
+ customizeModule(module, gradleProject);
+ }
+ }
}
- }
- });
+ });
+ }
+ Projects.ensureExternalBuildIsEnabledForGradleProject(project);
+ Application application = ApplicationManager.getApplication();
+ if (!application.isUnitTestMode()) {
+ application.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ Projects.BuildAction buildAction = Projects.getBuildActionFrom(project);
+ if (buildAction == null) {
+ // This happens when the project is imported and this is the first pass of the 2-pass import. Rebuild on second pass.
+ Projects.setProjectBuildAction(project, Projects.BuildAction.REBUILD);
+ }
+ else {
+ Projects.make(project);
+ }
+ }
+ });
+ }
}
@NotNull
private static Map<String, IdeaGradleProject> indexByModuleName(@NotNull Collection<DataNode<IdeaGradleProject>> dataNodes) {
- Map<String, IdeaGradleProject> index = Maps.newHashMap();
+ Map<String, IdeaGradleProject> gradleProjectsByModuleName = Maps.newHashMap();
for (DataNode<IdeaGradleProject> d : dataNodes) {
IdeaGradleProject gradleProject = d.getData();
- index.put(gradleProject.getModuleName(), gradleProject);
+ gradleProjectsByModuleName.put(gradleProject.getModuleName(), gradleProject);
}
- return index;
+ return gradleProjectsByModuleName;
}
private static void customizeModule(@NotNull Module module, @NotNull IdeaGradleProject gradleProject) {
@@ -90,7 +116,7 @@
*/
@NotNull
private static AndroidGradleFacet setAndGetAndroidGradleFacet(Module module) {
- AndroidGradleFacet facet = Facets.getFirstFacet(module, AndroidGradleFacet.TYPE_ID);
+ AndroidGradleFacet facet = Facets.getFirstFacetOfType(module, AndroidGradleFacet.TYPE_ID);
if (facet != null) {
return facet;
}
diff --git a/android/src/com/android/tools/idea/gradle/service/ProjectImportEventMessageDataService.java b/android/src/com/android/tools/idea/gradle/service/ProjectImportEventMessageDataService.java
new file mode 100644
index 0000000..abdb796
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/service/ProjectImportEventMessageDataService.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.service;
+
+import com.android.tools.idea.gradle.AndroidProjectKeys;
+import com.android.tools.idea.gradle.ProjectImportEventMessage;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+import com.intellij.notification.NotificationType;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.externalSystem.model.DataNode;
+import com.intellij.openapi.externalSystem.model.Key;
+import com.intellij.openapi.externalSystem.service.notification.ExternalSystemIdeNotificationManager;
+import com.intellij.openapi.externalSystem.service.project.manage.ProjectDataService;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.plugins.gradle.util.GradleConstants;
+
+import java.util.Collection;
+
+/**
+ * Presents to the user any unexpected events that occurred during project import.
+ */
+public class ProjectImportEventMessageDataService implements ProjectDataService<ProjectImportEventMessage, Void> {
+ private static final Logger LOG = Logger.getInstance(ProjectImportEventMessageDataService.class);
+
+ @NotNull
+ @Override
+ public Key<ProjectImportEventMessage> getTargetDataKey() {
+ return AndroidProjectKeys.IMPORT_EVENT_MSG;
+ }
+
+ @Override
+ public void importData(@NotNull Collection<DataNode<ProjectImportEventMessage>> toImport,
+ @NotNull final Project project,
+ boolean synchronous) {
+ final ExternalSystemIdeNotificationManager notificationManager = ServiceManager.getService(ExternalSystemIdeNotificationManager.class);
+ if (notificationManager == null) {
+ return;
+ }
+
+ Multimap<String, String> messagesByCategory = ArrayListMultimap.create();
+ for (DataNode<ProjectImportEventMessage> node : toImport) {
+ ProjectImportEventMessage message = node.getData();
+ String category = message.getCategory();
+ messagesByCategory.put(category, message.getText());
+ LOG.info(message.toString());
+ }
+ final StringBuilder builder = new StringBuilder();
+ builder.append("<html>");
+ for (String category : messagesByCategory.keySet()) {
+ Collection<String> messages = messagesByCategory.get(category);
+ if (category.isEmpty()) {
+ Joiner.on("<br>").join(messages);
+ }
+ else {
+ // If the category is not an empty String, we show the category and each message as a list.
+ builder.append(category).append("<ul>");
+ for (String message : messages) {
+ builder.append("<li>").append(message).append("</li>");
+ }
+ builder.append("</ul>");
+ }
+ }
+ builder.append("</html>");
+
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ String title = "Unexpected events:";
+ String messageToShow = builder.toString();
+ notificationManager.showNotification(title, messageToShow, NotificationType.ERROR, project, GradleConstants.SYSTEM_ID, null);
+ }
+ });
+ }
+
+ @Override
+ public void removeData(@NotNull Collection<? extends Void> toRemove, @NotNull Project project, boolean synchronous) {
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/service/notification/FileBugHyperlink.java b/android/src/com/android/tools/idea/gradle/service/notification/FileBugHyperlink.java
new file mode 100644
index 0000000..14be746
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/service/notification/FileBugHyperlink.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.service.notification;
+
+import com.intellij.ide.actions.SendFeedbackAction;
+
+class FileBugHyperlink extends NotificationHyperlink {
+ FileBugHyperlink() {
+ super("fileBug", "File a bug");
+ }
+
+ @Override
+ void execute() {
+ SendFeedbackAction.launchBrowser();
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/service/notification/GradleNotificationExtension.java b/android/src/com/android/tools/idea/gradle/service/notification/GradleNotificationExtension.java
new file mode 100644
index 0000000..b43f4cc
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/service/notification/GradleNotificationExtension.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.service.notification;
+
+import com.android.SdkConstants;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.intellij.facet.ProjectFacetManager;
+import com.intellij.notification.Notification;
+import com.intellij.notification.NotificationListener;
+import com.intellij.notification.NotificationType;
+import com.intellij.openapi.externalSystem.model.ExternalSystemException;
+import com.intellij.openapi.externalSystem.model.ProjectSystemId;
+import com.intellij.openapi.externalSystem.service.notification.ExternalSystemNotificationExtension;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.roots.ModuleRootManager;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.util.SystemProperties;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.android.sdk.AndroidSdkType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.gradle.util.GradleConstants;
+
+import javax.swing.event.HyperlinkEvent;
+import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static com.android.tools.idea.gradle.project.ProjectImportErrorHandler.*;
+
+public class GradleNotificationExtension implements ExternalSystemNotificationExtension {
+ private static final Pattern ERROR_LOCATION_IN_FILE_PATTERN = Pattern.compile("Build file '(.*)' line: ([\\d]+)");
+ private static final Pattern ERROR_IN_FILE_PATTERN = Pattern.compile("Build file '(.*)'");
+ private static final Pattern MISSING_DEPENDENCY_PATTERN = Pattern.compile("Could not find (.*)\\.");
+
+ private static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.ERROR;
+
+ @NotNull
+ @Override
+ public ProjectSystemId getTargetExternalSystemId() {
+ return GradleConstants.SYSTEM_ID;
+ }
+
+ @Nullable
+ @Override
+ public CustomizationResult customize(@NotNull Project project, @NotNull Throwable error, @Nullable UsageHint hint) {
+ Throwable cause = error;
+ if (error instanceof UndeclaredThrowableException) {
+ cause = ((UndeclaredThrowableException)error).getUndeclaredThrowable();
+ if (cause instanceof InvocationTargetException) {
+ cause = ((InvocationTargetException)cause).getTargetException();
+ }
+ }
+ if (cause instanceof ExternalSystemException) {
+ return createNotification(project, (ExternalSystemException)cause);
+ }
+ return null;
+ }
+
+ @Nullable
+ private static CustomizationResult createNotification(@NotNull Project project, @NotNull ExternalSystemException error) {
+ String msg = error.getMessage();
+ if (msg != null && !msg.isEmpty()) {
+ if (msg.startsWith("Project is using an old version of the Android Gradle plug-in")) {
+ //noinspection TestOnlyProblems
+ return createNotification(project, msg, new SearchInBuildFilesHyperlink(project, "com.android.tools.build:gradle"));
+ }
+
+ if (msg.contains(NotificationHints.FAILED_TO_PARSE_SDK)) {
+ String pathOfBrokenSdk = findPathOfSdkMissingOrEmptyAddonsFolder(project);
+ String newMsg;
+ if (pathOfBrokenSdk != null) {
+ newMsg = String.format("The directory '%1$s', in the Android SDK at '%2$s', is either missing or empty", SdkConstants.FD_ADDONS,
+ pathOfBrokenSdk);
+ File sdkHomeDir = new File(pathOfBrokenSdk);
+ if (!sdkHomeDir.canWrite()) {
+ newMsg += String.format("\n\nCurrent user (%1$s) does not have write access to the SDK directory.", SystemProperties.getUserName());
+ }
+ }
+ else {
+ newMsg = splitLines(msg).get(0);
+ }
+ //noinspection TestOnlyProblems
+ return createNotification(project, newMsg);
+ }
+
+ List<String> lines = splitLines(msg);
+ String firstLine = lines.get(0);
+ String lastLine = lines.get(lines.size() - 1);
+
+ if (lastLine != null && lastLine.equals(NotificationHints.OPEN_GRADLE_SETTINGS)) {
+ //noinspection TestOnlyProblems
+ return createNotification(project, msg, new GradleSettingsHyperlink(project));
+ }
+
+ if (lastLine != null && lastLine.contains(NotificationHints.INSTALL_ANDROID_SUPPORT_REPO)) {
+ List<AndroidFacet> facets = ProjectFacetManager.getInstance(project).getFacets(AndroidFacet.ID);
+ if (!facets.isEmpty()) {
+ // We can only open SDK manager if the project has an Android facet. Android facet has a reference to the Android SDK manager.
+ //noinspection TestOnlyProblems
+ return createNotification(project, msg, new OpenAndroidSdkManagerHyperlink(project));
+ }
+ //noinspection TestOnlyProblems
+ return createNotification(project, msg);
+ }
+
+ if (lastLine != null && lastLine.contains(NotificationHints.SET_UP_HTTP_PROXY)) {
+ //noinspection TestOnlyProblems
+ return createNotification(project, msg, new OpenHttpSettingsHyperlink(project), new OpenUrlHyperlink(
+ "http://www.gradle.org/docs/current/userguide/userguide_single.html#sec:accessing_the_web_via_a_proxy",
+ "Open Gradle documentation"));
+ }
+
+ Matcher matcher = MISSING_DEPENDENCY_PATTERN.matcher(firstLine);
+ if (matcher.matches() && lines.size() > 1 && lines.get(1).startsWith("Required by:")) {
+ String dependency = matcher.group(1);
+ if (!Strings.isNullOrEmpty(dependency)) {
+ if (lastLine != null) {
+ Pair<String, Integer> errorLocation = getErrorLocation(lastLine);
+ if (errorLocation != null) {
+ // We have a location in file, show the "Open File" hyperlink.
+ String filePath = errorLocation.getFirst();
+ int line = errorLocation.getSecond();
+ //noinspection TestOnlyProblems
+ return createNotification(project, msg, new OpenFileHyperlink(project, filePath, line),
+ new SearchInBuildFilesHyperlink(project, dependency));
+ }
+ }
+ //noinspection TestOnlyProblems
+ return createNotification(project, msg, new SearchInBuildFilesHyperlink(project, dependency));
+ }
+ }
+
+ if (lastLine != null) {
+ if (lastLine.contains(NotificationHints.UNEXPECTED_ERROR_FILE_BUG)) {
+ //noinspection TestOnlyProblems
+ return createNotification(project, msg, new FileBugHyperlink(), new ShowLogHyperlink());
+ }
+
+ Pair<String, Integer> errorLocation = getErrorLocation(lastLine);
+ if (errorLocation != null) {
+ String filePath = errorLocation.getFirst();
+ int line = errorLocation.getSecond();
+ //noinspection TestOnlyProblems
+ return createNotification(project, msg, new OpenFileHyperlink(project, filePath, line));
+ }
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ private static Pair<String, Integer> getErrorLocation(@NotNull String msg) {
+ Matcher matcher = ERROR_LOCATION_IN_FILE_PATTERN.matcher(msg);
+ if (matcher.matches()) {
+ String filePath = matcher.group(1);
+ int line = -1;
+ try {
+ line = Integer.parseInt(matcher.group(2));
+ }
+ catch (NumberFormatException e) {
+ // ignored.
+ }
+ return Pair.create(filePath, line);
+ }
+
+ matcher = ERROR_IN_FILE_PATTERN.matcher(msg);
+ if (matcher.matches()) {
+ String filePath = matcher.group(1);
+ return Pair.create(filePath, -1);
+ }
+ return null;
+ }
+
+ @Nullable
+ private static String findPathOfSdkMissingOrEmptyAddonsFolder(@NotNull Project project) {
+ ModuleManager moduleManager = ModuleManager.getInstance(project);
+ for (Module module : moduleManager.getModules()) {
+ Sdk moduleSdk = ModuleRootManager.getInstance(module).getSdk();
+ if (moduleSdk != null && moduleSdk.getSdkType().equals(AndroidSdkType.getInstance())) {
+ String sdkHomeDirPath = moduleSdk.getHomePath();
+ File addonsDir = new File(sdkHomeDirPath, SdkConstants.FD_ADDONS);
+ if (!addonsDir.isDirectory() || FileUtil.notNullize(addonsDir.listFiles()).length == 0) {
+ return sdkHomeDirPath;
+ }
+ }
+ }
+ return null;
+ }
+
+ @NotNull
+ private static List<String> splitLines(@NotNull String s) {
+ return Lists.newArrayList(Splitter.on('\n').split(s));
+ }
+
+ @NotNull
+ @VisibleForTesting
+ static CustomizationResult createNotification(@NotNull Project project,
+ @NotNull String errorMsg,
+ @NotNull NotificationHyperlink... hyperlinks) {
+ String text = "";
+ NotificationListener notificationListener = null;
+ int hyperlinkCount = hyperlinks.length;
+ if (hyperlinkCount > 0) {
+ StringBuilder b = new StringBuilder();
+ for (int i = 0; i < hyperlinkCount; i++) {
+ b.append(hyperlinks[i].toString());
+ if (i < hyperlinkCount - 1) {
+ b.append(" ");
+ }
+ }
+ text = b.toString();
+ notificationListener = new CustomNotificationListener(hyperlinks);
+ }
+ String title = createNotificationTitle(project, errorMsg);
+ return new CustomizationResult(title, text, DEFAULT_NOTIFICATION_TYPE, notificationListener);
+ }
+
+ @NotNull
+ private static String createNotificationTitle(@NotNull Project project, @NotNull String msg) {
+ return String.format("Failed to refresh Gradle project '%1$s':\n", project.getName()) + msg;
+ }
+
+ @VisibleForTesting
+ static class CustomNotificationListener extends NotificationListener.Adapter {
+ @NotNull private final NotificationHyperlink[] myHyperlinks;
+
+ CustomNotificationListener(@NotNull NotificationHyperlink...hyperlinks) {
+ myHyperlinks = hyperlinks;
+ }
+
+ @Override
+ protected void hyperlinkActivated(@NotNull Notification notification, @NotNull HyperlinkEvent e) {
+ if (myHyperlinks.length == 1) {
+ myHyperlinks[0].executeIfClicked(e);
+ return;
+ }
+ for (NotificationHyperlink hyperlink : myHyperlinks) {
+ if (hyperlink.executeIfClicked(e)) {
+ return;
+ }
+ }
+ }
+
+ @NotNull
+ NotificationHyperlink[] getHyperlinks() {
+ return myHyperlinks;
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/service/notification/GradleSettingsHyperlink.java b/android/src/com/android/tools/idea/gradle/service/notification/GradleSettingsHyperlink.java
new file mode 100644
index 0000000..6537c63
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/service/notification/GradleSettingsHyperlink.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.service.notification;
+
+import com.intellij.openapi.externalSystem.ExternalSystemManager;
+import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
+import com.intellij.openapi.options.Configurable;
+import com.intellij.openapi.options.ShowSettingsUtil;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.plugins.gradle.GradleManager;
+import org.jetbrains.plugins.gradle.util.GradleConstants;
+
+class GradleSettingsHyperlink extends NotificationHyperlink {
+ @NotNull private final Project myProject;
+
+ GradleSettingsHyperlink(@NotNull Project project) {
+ super("openGradleSettings", "Gradle settings");
+ myProject = project;
+ }
+
+ @Override
+ void execute() {
+ ExternalSystemManager<?,?,?,?,?> manager = ExternalSystemApiUtil.getManager(GradleConstants.SYSTEM_ID);
+ assert manager instanceof GradleManager;
+ GradleManager gradleManager = (GradleManager)manager;
+ Configurable configurable = gradleManager.getConfigurable(myProject);
+ ShowSettingsUtil.getInstance().editConfigurable(myProject, configurable);
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/service/notification/NotificationHyperlink.java b/android/src/com/android/tools/idea/gradle/service/notification/NotificationHyperlink.java
new file mode 100644
index 0000000..c4097ae
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/service/notification/NotificationHyperlink.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.service.notification;
+
+import com.intellij.openapi.util.text.StringUtil;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.event.HyperlinkEvent;
+
+abstract class NotificationHyperlink {
+ @NotNull private final String myUrl;
+ @NotNull private final String myValue;
+
+ NotificationHyperlink(@NotNull String url, @NotNull String text) {
+ myUrl = url;
+ myValue = String.format("<a href=\"%1$s\">%2$s</a>", StringUtil.escapeXml(url), text);
+ }
+
+ abstract void execute();
+
+ boolean executeIfClicked(@NotNull HyperlinkEvent event) {
+ if (myUrl.equals(event.getDescription())) {
+ execute();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return myValue;
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/service/notification/OpenAndroidSdkManagerHyperlink.java b/android/src/com/android/tools/idea/gradle/service/notification/OpenAndroidSdkManagerHyperlink.java
new file mode 100644
index 0000000..88fb1e1
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/service/notification/OpenAndroidSdkManagerHyperlink.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.service.notification;
+
+import com.intellij.openapi.project.Project;
+import org.jetbrains.android.actions.RunAndroidSdkManagerAction;
+import org.jetbrains.annotations.NotNull;
+
+class OpenAndroidSdkManagerHyperlink extends NotificationHyperlink {
+ @NotNull private final Project myProject;
+
+ OpenAndroidSdkManagerHyperlink(@NotNull final Project project) {
+ super("openAndroidSdkManager", "Open Android SDK Manager");
+ myProject = project;
+ }
+
+ @Override
+ void execute() {
+ RunAndroidSdkManagerAction action = new RunAndroidSdkManagerAction();
+ action.doAction(myProject);
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/service/notification/OpenFileHyperlink.java b/android/src/com/android/tools/idea/gradle/service/notification/OpenFileHyperlink.java
new file mode 100644
index 0000000..a923df2
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/service/notification/OpenFileHyperlink.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.service.notification;
+
+import com.intellij.openapi.fileEditor.OpenFileDescriptor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.pom.Navigatable;
+import org.jetbrains.annotations.NotNull;
+
+class OpenFileHyperlink extends NotificationHyperlink {
+ @NotNull private final Project myProject;
+ @NotNull private final String myFilePath;
+ private final int myLine;
+
+ OpenFileHyperlink(@NotNull final Project project, @NotNull final String filePath, final int line) {
+ super("openFile", "Open File");
+ myProject = project;
+ myFilePath = filePath;
+ myLine = line;
+ }
+
+ @Override
+ void execute() {
+ VirtualFile projectFile = myProject.getProjectFile();
+ if (projectFile == null) {
+ // This is the default project. This will NEVER happen.
+ return;
+ }
+ VirtualFile file = projectFile.getParent().getFileSystem().findFileByPath(myFilePath);
+ if (file != null) {
+ Navigatable openFile = new OpenFileDescriptor(myProject, file, myLine, -1, false);
+ if (openFile.canNavigate()) {
+ openFile.navigate(true);
+ }
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/service/notification/OpenHttpSettingsHyperlink.java b/android/src/com/android/tools/idea/gradle/service/notification/OpenHttpSettingsHyperlink.java
new file mode 100644
index 0000000..8abebc3
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/service/notification/OpenHttpSettingsHyperlink.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.service.notification;
+
+import com.intellij.openapi.options.ShowSettingsUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.util.net.HTTPProxySettingsPanel;
+import com.intellij.util.net.HttpConfigurable;
+import org.jetbrains.annotations.NotNull;
+
+class OpenHttpSettingsHyperlink extends NotificationHyperlink {
+ @NotNull private final Project myProject;
+
+ OpenHttpSettingsHyperlink(@NotNull final Project project) {
+ super("openHttpSettings", "HTTP proxy settings");
+ myProject = project;
+ }
+
+ @Override
+ void execute() {
+ ShowSettingsUtil.getInstance().editConfigurable(myProject, new HTTPProxySettingsPanel(HttpConfigurable.getInstance()));
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/service/notification/OpenUrlHyperlink.java b/android/src/com/android/tools/idea/gradle/service/notification/OpenUrlHyperlink.java
new file mode 100644
index 0000000..5d7d193
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/service/notification/OpenUrlHyperlink.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.service.notification;
+
+import com.intellij.ide.BrowserUtil;
+import com.intellij.openapi.options.ShowSettingsUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.util.net.HTTPProxySettingsPanel;
+import com.intellij.util.net.HttpConfigurable;
+import org.jetbrains.annotations.NotNull;
+
+class OpenUrlHyperlink extends NotificationHyperlink {
+ @NotNull private final String myUrl;
+
+ OpenUrlHyperlink(@NotNull String url, @NotNull String text) {
+ super(url, text);
+ myUrl = url;
+ }
+
+ @Override
+ void execute() {
+ BrowserUtil.launchBrowser(myUrl);
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/service/notification/SearchInBuildFilesHyperlink.java b/android/src/com/android/tools/idea/gradle/service/notification/SearchInBuildFilesHyperlink.java
new file mode 100644
index 0000000..56d2467
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/service/notification/SearchInBuildFilesHyperlink.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.service.notification;
+
+import com.android.SdkConstants;
+import com.intellij.find.FindManager;
+import com.intellij.find.FindModel;
+import com.intellij.find.FindSettings;
+import com.intellij.find.impl.FindInProjectUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Factory;
+import com.intellij.usageView.UsageInfo;
+import com.intellij.usages.*;
+import com.intellij.util.AdapterProcessor;
+import com.intellij.util.Processor;
+import org.jetbrains.annotations.NotNull;
+
+class SearchInBuildFilesHyperlink extends NotificationHyperlink {
+ @NotNull private final Project myProject;
+ @NotNull private final String myTextToFind;
+
+ SearchInBuildFilesHyperlink(@NotNull final Project project, @NotNull final String textToFind) {
+ super("searchInBuildFiles", "Search in build.gradle files");
+ myProject = project;
+ myTextToFind = textToFind;
+ }
+
+ @Override
+ void execute() {
+ FindManager findManager = FindManager.getInstance(myProject);
+ UsageViewManager usageViewManager = UsageViewManager.getInstance(myProject);
+
+ FindModel findModel = (FindModel)findManager.getFindInProjectModel().clone();
+ findModel.setStringToFind(myTextToFind);
+ findModel.setReplaceState(false);
+ findModel.setOpenInNewTabVisible(true);
+ findModel.setOpenInNewTabEnabled(true);
+ findModel.setOpenInNewTab(true);
+ findModel.setFileFilter(SdkConstants.FN_BUILD_GRADLE);
+
+ findManager.getFindInProjectModel().copyFrom(findModel);
+ final FindModel findModelCopy = (FindModel)findModel.clone();
+
+ UsageViewPresentation presentation = FindInProjectUtil.setupViewPresentation(findModel.isOpenInNewTabEnabled(), findModelCopy);
+ boolean showPanelIfOnlyOneUsage = !FindSettings.getInstance().isSkipResultsWithOneUsage();
+ final FindUsagesProcessPresentation processPresentation =
+ FindInProjectUtil.setupProcessPresentation(myProject, showPanelIfOnlyOneUsage, presentation);
+ UsageTarget usageTarget = new FindInProjectUtil.StringUsageTarget(findModel.getStringToFind());
+ usageViewManager.searchAndShowUsages(new UsageTarget[]{usageTarget}, new Factory<UsageSearcher>() {
+ @Override
+ public UsageSearcher create() {
+ return new UsageSearcher() {
+ @Override
+ public void generate(@NotNull final Processor<Usage> processor) {
+ AdapterProcessor<UsageInfo, Usage> consumer =
+ new AdapterProcessor<UsageInfo, Usage>(processor, UsageInfo2UsageAdapter.CONVERTER);
+ //noinspection ConstantConditions
+ FindInProjectUtil.findUsages(findModelCopy, null, myProject, true, consumer, processPresentation);
+ }
+ };
+ }
+ }, processPresentation, presentation, null);
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/service/notification/ShowLogHyperlink.java b/android/src/com/android/tools/idea/gradle/service/notification/ShowLogHyperlink.java
new file mode 100644
index 0000000..a7ad19f
--- /dev/null
+++ b/android/src/com/android/tools/idea/gradle/service/notification/ShowLogHyperlink.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.service.notification;
+
+import com.intellij.ide.actions.ShowFilePathAction;
+import com.intellij.openapi.application.PathManager;
+import org.jetbrains.annotations.NonNls;
+
+import java.io.File;
+
+class ShowLogHyperlink extends NotificationHyperlink {
+ @NonNls private static final String IDEA_LOG_FILE_NAME = "idea.log";
+
+ ShowLogHyperlink() {
+ super("showLogFile", "Show log file");
+ }
+
+ @Override
+ void execute() {
+ File logFile = new File(PathManager.getLogPath(), IDEA_LOG_FILE_NAME);
+ ShowFilePathAction.openFile(logFile);
+ }
+}
diff --git a/android/src/com/android/tools/idea/gradle/startup/GradleStartupActivity.java b/android/src/com/android/tools/idea/gradle/startup/GradleStartupActivity.java
deleted file mode 100644
index b8544c4..0000000
--- a/android/src/com/android/tools/idea/gradle/startup/GradleStartupActivity.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tools.idea.gradle.startup;
-
-import com.android.tools.idea.gradle.GradleImportNotificationListener;
-import com.android.tools.idea.gradle.util.Projects;
-import com.intellij.openapi.project.DumbAware;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.startup.StartupActivity;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * Sets up any Gradle-related state when the IDE starts.
- */
-public class GradleStartupActivity implements StartupActivity, DumbAware {
- @Override
- public void runActivity(@NotNull Project project) {
- if (Projects.isGradleProject(project)) {
- GradleImportNotificationListener.attachToManager();
- Projects.setBuildAction(project, Projects.BuildAction.COMPILE);
- }
- }
-}
diff --git a/android/src/com/android/tools/idea/gradle/util/Facets.java b/android/src/com/android/tools/idea/gradle/util/Facets.java
index 1022e55..53b46fd 100644
--- a/android/src/com/android/tools/idea/gradle/util/Facets.java
+++ b/android/src/com/android/tools/idea/gradle/util/Facets.java
@@ -15,7 +15,6 @@
*/
package com.android.tools.idea.gradle.util;
-import com.android.tools.idea.gradle.facet.AndroidGradleFacet;
import com.intellij.facet.Facet;
import com.intellij.facet.FacetManager;
import com.intellij.facet.FacetTypeId;
@@ -43,9 +42,26 @@
* @return the first found facet in the given module.
*/
@Nullable
- public static <T extends Facet> T getFirstFacet(@NotNull Module module, @NotNull FacetTypeId<T> typeId) {
+ public static <T extends Facet> T getFirstFacetOfType(@NotNull Module module, @NotNull FacetTypeId<T> typeId) {
FacetManager facetManager = FacetManager.getInstance(module);
Collection<T> facets = facetManager.getFacetsByType(typeId);
return ContainerUtil.getFirstItem(facets);
}
+
+ public static <T extends Facet> void removeAllFacetsOfType(@NotNull Module module, @NotNull FacetTypeId<T> typeId) {
+ FacetManager facetManager = FacetManager.getInstance(module);
+ Collection<T> facets = facetManager.getFacetsByType(typeId);
+ if (!facets.isEmpty()) {
+ ModifiableFacetModel model = facetManager.createModifiableModel();
+ try {
+ for (T facet : facets) {
+ model.removeFacet(facet);
+ }
+ }
+ finally {
+ model.commit();
+ }
+ }
+ }
}
+
diff --git a/android/src/com/android/tools/idea/gradle/util/LocalProperties.java b/android/src/com/android/tools/idea/gradle/util/LocalProperties.java
index dffdf9f..1db44a0 100644
--- a/android/src/com/android/tools/idea/gradle/util/LocalProperties.java
+++ b/android/src/com/android/tools/idea/gradle/util/LocalProperties.java
@@ -20,7 +20,6 @@
import com.google.common.io.Closeables;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
-import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.util.SystemProperties;
import org.jetbrains.annotations.NotNull;
@@ -28,7 +27,7 @@
import java.io.File;
import java.io.FileInputStream;
-import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;
@@ -36,66 +35,13 @@
* Utility methods related to a Gradle project's local.properties file.
*/
public final class LocalProperties {
- public static final String SDK_DIR_PROPERTY = "sdk.dir";
+ private static final String HEADER_COMMENT = getHeaderComment();
- private LocalProperties() {
- }
+ @NotNull private final File myFilePath;
+ @NotNull private final Properties myProperties;
- /**
- * Returns the path of the Android SDK specified in the project's local.properties file.
- *
- * @param project the given project.
- * @return the path of the Android SDK specified in the project's local.properties file; or {@code null} if the given project does not
- * have a local.properties file or if the file does not specify the path of the Android SDK to use.
- * @throws IOException if an I/O error occurs while reading the file.
- */
- @Nullable
- public static String getAndroidSdkPath(@NotNull Project project) throws IOException {
- Properties properties = readFile(project);
- if (properties == null) {
- return null;
- }
- return properties.getProperty(SDK_DIR_PROPERTY);
- }
-
- /**
- * Returns the contents of the local.properties file in the given project.
- *
- * @param project the given project.
- * @return the contents of the local.properties file in the given project, or {@code null} if such file does not exist.
- * @throws IOException if an I/O error occurs while reading the file.
- */
- @Nullable
- public static Properties readFile(@NotNull Project project) throws IOException {
- File filePath = localPropertiesFilePath(project);
- if (!filePath.isFile()) {
- return null;
- }
- Properties properties = new Properties();
- FileInputStream fileInputStream = null;
- try {
- //noinspection IOResourceOpenedButNotSafelyClosed
- fileInputStream = new FileInputStream(filePath);
- properties.load(fileInputStream);
- } catch (FileNotFoundException e) {
- return null;
- } finally {
- Closeables.closeQuietly(fileInputStream);
- }
- return properties;
- }
-
- /**
- * Creates a local.properties file, containing the path of the given Android SDK, inside the root directory of the given project.
- *
- * @param project the given project.
- * @param androidSdk the Android SDK.
- * @throws IOException if an I/O error occurs while writing the contents of the file.
- */
- public static void createFile(@NotNull Project project, @NotNull Sdk androidSdk) throws IOException {
- File filePath = localPropertiesFilePath(project);
- FileUtilRt.createIfNotExists(filePath);
- // TODO: create this file using a template and just populate the path of Android SDK.
+ @NotNull
+ private static String getHeaderComment() {
String[] lines = {
"# This file is automatically generated by Android Studio.",
"# Do not modify this file -- YOUR CHANGES WILL BE ERASED!",
@@ -105,15 +51,82 @@
"",
"# Location of the SDK. This is only used by Gradle.",
"# For customization when using a Version Control System, please read the",
- "# header note.",
- SDK_DIR_PROPERTY + "=" + androidSdk.getHomePath()
+ "# header note."
};
- String contents = Joiner.on(SystemProperties.getLineSeparator()).join(lines);
- FileUtil.writeToFile(filePath, contents);
+ return Joiner.on(SystemProperties.getLineSeparator()).join(lines);
+ }
+
+ /**
+ * Creates a new {@link LocalProperties}. This constructor creates a new file at the given path if a local.properties file does not exist.
+ *
+ * @param project the Android project.
+ * @throws IOException if an I/O error occurs while reading the file.
+ * @throws IllegalArgumentException if there is already a directory called "local.properties" in the given project.
+ */
+ public LocalProperties(@NotNull Project project) throws IOException {
+ this(new File(project.getBasePath()));
+ }
+
+ /**
+ * Creates a new {@link LocalProperties}. This constructor creates a new file at the given path if a local.properties file does not exist.
+ *
+ * @param projectDirPath the path of the Android project's root directory.
+ * @throws IOException if an I/O error occurs while reading the file.
+ * @throws IllegalArgumentException if there is already a directory called "local.properties" at the given path.
+ */
+ public LocalProperties(@NotNull File projectDirPath) throws IOException {
+ myFilePath = new File(projectDirPath, SdkConstants.FN_LOCAL_PROPERTIES);
+ myProperties = readFile(myFilePath);
}
@NotNull
- private static File localPropertiesFilePath(@NotNull Project project) {
- return new File(project.getBasePath(), SdkConstants.FN_LOCAL_PROPERTIES);
+ private static Properties readFile(@NotNull File filePath) throws IOException {
+ if (filePath.isDirectory()) {
+ // There is a directory named "local.properties". Unlikely to happen, but worth checking.
+ throw new IllegalArgumentException(String.format("The path '%1$s' belongs to a directory!", filePath.getPath()));
+ }
+ if (!filePath.exists()) {
+ return new Properties();
+ }
+ Properties properties = new Properties();
+ FileInputStream fileInputStream = null;
+ try {
+ //noinspection IOResourceOpenedButNotSafelyClosed
+ fileInputStream = new FileInputStream(filePath);
+ properties.load(fileInputStream);
+ } finally {
+ Closeables.closeQuietly(fileInputStream);
+ }
+ return properties;
+ }
+
+ /**
+ * @return the path of the Android SDK specified in this local.properties file; or {@code null} if such property is not specified.
+ */
+ @Nullable
+ public String getAndroidSdkPath() {
+ return myProperties.getProperty(SdkConstants.SDK_DIR_PROPERTY);
+ }
+
+ public void setAndroidSdkPath(@NotNull Sdk androidSdk) {
+ String androidSdkPath = androidSdk.getHomePath();
+ assert androidSdkPath != null;
+ setAndroidSdkPath(androidSdkPath);
+ }
+
+ public void setAndroidSdkPath(@NotNull String androidSdkPath) {
+ myProperties.setProperty(SdkConstants.SDK_DIR_PROPERTY, androidSdkPath);
+ }
+
+ public void save() throws IOException {
+ FileUtilRt.createParentDirs(myFilePath);
+ FileOutputStream out = null;
+ try {
+ //noinspection IOResourceOpenedButNotSafelyClosed
+ out = new FileOutputStream(myFilePath);
+ myProperties.store(out, HEADER_COMMENT);
+ } finally {
+ Closeables.closeQuietly(out);
+ }
}
}
diff --git a/android/src/com/android/tools/idea/gradle/util/Projects.java b/android/src/com/android/tools/idea/gradle/util/Projects.java
index d2184e9..3bd262f 100644
--- a/android/src/com/android/tools/idea/gradle/util/Projects.java
+++ b/android/src/com/android/tools/idea/gradle/util/Projects.java
@@ -16,17 +16,18 @@
package com.android.tools.idea.gradle.util;
import com.android.tools.idea.gradle.facet.AndroidGradleFacet;
+import com.intellij.compiler.CompilerWorkspaceConfiguration;
+import com.intellij.compiler.options.ExternalBuildOptionListener;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
-import com.intellij.openapi.compiler.CompileContext;
-import com.intellij.openapi.compiler.CompileStatusNotification;
import com.intellij.openapi.compiler.CompilerManager;
+import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
-import com.intellij.openapi.vfs.LocalFileSystem;
-import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.messages.MessageBus;
+import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -35,46 +36,83 @@
*/
public final class Projects {
private static final Key<BuildAction> PROJECT_BUILD_ACTION_KEY = Key.create("android.gradle.project.build.action");
+ private static final Key<Boolean> GENERATE_SOURCE_ONLY_ON_COMPILE = Key.create("android.gradle.generate.source.only.on.compile");
+
+ private static final Logger LOG = Logger.getInstance(Projects.class);
private Projects() {
}
/**
+ * Takes a project and compiles it, rebuilds it or simply generates source code based on the {@link BuildAction} set on the given project.
+ * This method does nothing if the project does not have a {@link BuildAction}.
+ *
+ * @param project the given project.
+ */
+ public static void make(@NotNull Project project) {
+ BuildAction buildAction = getBuildActionFrom(project);
+ if (buildAction != null) {
+ switch (buildAction) {
+ case COMPILE:
+ compile(project);
+ break;
+ case REBUILD:
+ rebuild(project);
+ break;
+ case SOURCE_GEN:
+ generateSourcesOnly(project);
+ break;
+ }
+ removeBuildActionFrom(project);
+ }
+ }
+
+ /**
* Compiles the given project and refreshes the directory at the given path after compilation is finished. This method refreshes the
* directory asynchronously and recursively.
*
- * @param project the given project.
- * @param dirToRefreshPath the path of the directory to refresh after compilation is finished.
+ * @param project the given project.
*/
- public static void compile(@NotNull Project project, @NotNull String dirToRefreshPath) {
- CompilerManager.getInstance(project).make(new RefreshProjectAfterCompilation(dirToRefreshPath));
+ public static void compile(@NotNull Project project) {
+ CompilerManager.getInstance(project).make(null);
}
/**
* Rebuilds the given project and refreshes the directory at the given path after compilation is finished. This method refreshes the
* directory asynchronously and recursively. Rebuilding cleans the output directories and then compiles the project.
*
- * @param project the given project.
- * @param dirToRefreshPath the path of the directory to refresh after compilation is finished.
+ * @param project the given project.
*/
- public static void rebuild(@NotNull Project project, @NotNull String dirToRefreshPath) {
- CompilerManager.getInstance(project).rebuild(new RefreshProjectAfterCompilation(dirToRefreshPath));
+ public static void rebuild(@NotNull Project project) {
+ CompilerManager.getInstance(project).rebuild(null);
}
- private static class RefreshProjectAfterCompilation implements CompileStatusNotification {
- @NotNull private final String myDirToRefreshPath;
-
- RefreshProjectAfterCompilation(@NotNull String dirToRefreshPath) {
- myDirToRefreshPath = dirToRefreshPath;
+ /**
+ * Generates source code instead of a full compilation. This method does nothing if the Gradle model does not specify the name of the
+ * Gradle task to invoke.
+ *
+ * @param project the given project.
+ */
+ public static void generateSourcesOnly(@NotNull Project project) {
+ if (hasSourceGenTasks(project)) {
+ project.putUserData(GENERATE_SOURCE_ONLY_ON_COMPILE, true);
+ compile(project);
+ } else {
+ String msg = String.format("Unable to find tasks for generating source code for project '%1$s'", project.getName());
+ LOG.info(msg);
}
+ }
- @Override
- public void finished(boolean aborted, int errors, int warnings, CompileContext compileContext) {
- VirtualFile rootDir = LocalFileSystem.getInstance().findFileByPath(myDirToRefreshPath);
- if (rootDir != null && rootDir.isDirectory()) {
- rootDir.refresh(true, true);
+ private static boolean hasSourceGenTasks(@NotNull Project project) {
+ Module[] modules = ModuleManager.getInstance(project).getModules();
+ for (Module module : modules) {
+ AndroidFacet androidFacet = Facets.getFirstFacetOfType(module, AndroidFacet.ID);
+ if (androidFacet != null) {
+ String sourceGenTaskName = androidFacet.getConfiguration().getState().SOURCE_GEN_TASK_NAME;
+ return !sourceGenTaskName.isEmpty() && !"TODO".equalsIgnoreCase(sourceGenTaskName);
}
}
+ return false;
}
/**
@@ -86,7 +124,7 @@
public static boolean isGradleProject(@NotNull Project project) {
ModuleManager moduleManager = ModuleManager.getInstance(project);
for (Module module : moduleManager.getModules()) {
- if (Facets.getFirstFacet(module, AndroidGradleFacet.TYPE_ID) != null) {
+ if (Facets.getFirstFacetOfType(module, AndroidGradleFacet.TYPE_ID) != null) {
return true;
}
}
@@ -105,24 +143,58 @@
return isGradleProject ? project : null;
}
- public static void removeBuildAction(@NotNull Project project) {
- setBuildAction(project, null);
+ /**
+ * Ensures that "External Build" is enabled for the given Gradle-based project. External build is the type of build that delegates project
+ * building to Gradle.
+ *
+ * @param project the given project. This method does not do anything if the given project is not a Gradle-based project.
+ */
+ public static void ensureExternalBuildIsEnabledForGradleProject(@NotNull Project project) {
+ if (isGradleProject(project)) {
+ CompilerWorkspaceConfiguration workspaceConfiguration = CompilerWorkspaceConfiguration.getInstance(project);
+ boolean wasUsingExternalMake = workspaceConfiguration.USE_COMPILE_SERVER;
+ if (!wasUsingExternalMake) {
+ String format = "Enabled 'External Build' for Android project '%1$s'. Otherwise, the project will not be built with Gradle";
+ String msg = String.format(format, project.getName());
+ LOG.info(msg);
+ workspaceConfiguration.USE_COMPILE_SERVER = true;
+ MessageBus messageBus = project.getMessageBus();
+ messageBus.syncPublisher(ExternalBuildOptionListener.TOPIC).externalBuildOptionChanged(workspaceConfiguration.USE_COMPILE_SERVER);
+ }
+ }
}
- public static void setBuildAction(@NotNull Project project, @Nullable BuildAction action) {
+ public static void removeBuildActionFrom(@NotNull Project project) {
+ setProjectBuildAction(project, null);
+ }
+
+ public static void setProjectBuildAction(@NotNull Project project, @Nullable BuildAction action) {
project.putUserData(PROJECT_BUILD_ACTION_KEY, action);
}
@Nullable
- public static BuildAction getBuildAction(@NotNull Project project) {
+ public static BuildAction getBuildActionFrom(@NotNull Project project) {
return project.getUserData(PROJECT_BUILD_ACTION_KEY);
}
/**
+ * Indicates whether the given project has the setting 'generate source code only'. Note that the setting is turned off after being
+ * checked making subsequent calls to this method always return {@code false}.
+ *
+ * @param project the given project.
+ * @return {@code true}
+ */
+ public static boolean generateSourceOnlyOnCompile(@NotNull Project project) {
+ Boolean generateSourceCodeOnCompile = project.getUserData(GENERATE_SOURCE_ONLY_ON_COMPILE);
+ project.putUserData(GENERATE_SOURCE_ONLY_ON_COMPILE, null);
+ return generateSourceCodeOnCompile == Boolean.TRUE;
+ }
+
+ /**
* Indicates whether a project should be built or not after a Gradle model refresh. "Building" means either compiling or rebuilding a
* project.
*/
public enum BuildAction {
- COMPILE, REBUILD
+ COMPILE, REBUILD, SOURCE_GEN
}
}
diff --git a/android/src/com/android/tools/idea/gradle/variant/view/BuildVariantUpdater.java b/android/src/com/android/tools/idea/gradle/variant/view/BuildVariantUpdater.java
index c72a1a5..cb19df7 100644
--- a/android/src/com/android/tools/idea/gradle/variant/view/BuildVariantUpdater.java
+++ b/android/src/com/android/tools/idea/gradle/variant/view/BuildVariantUpdater.java
@@ -74,7 +74,7 @@
logAndShowUpdateFailure(buildVariantName, reason);
return null;
}
- AndroidFacet facet = Facets.getFirstFacet(moduleToUpdate, AndroidFacet.ID);
+ AndroidFacet facet = Facets.getFirstFacetOfType(moduleToUpdate, AndroidFacet.ID);
if (facet == null) {
// Reason is not capitalized because it is a sentence fragment.
String reason = String.format("cannot find 'Android' facet in module '%1$s', project '%2$s'", moduleName, project.getName());
@@ -95,10 +95,10 @@
customizer.customizeModule(moduleToUpdate, project, androidProject);
}
- // We changed the way we build projects: now we build only the selected variant. If user changes variant, we need to rebuild the project
+ // We changed the way we build projects: now we build only the selected variant. If user changes variant, we need to re-generate sources
// since the generated sources may not be there.
if (!ApplicationManager.getApplication().isUnitTestMode()) {
- Projects.compile(project, androidProject.getRootDirPath());
+ Projects.generateSourcesOnly(project);
}
return facet;
diff --git a/android/src/com/android/tools/idea/gradle/variant/view/BuildVariantView.java b/android/src/com/android/tools/idea/gradle/variant/view/BuildVariantView.java
index a7785d5..bdf00f42 100644
--- a/android/src/com/android/tools/idea/gradle/variant/view/BuildVariantView.java
+++ b/android/src/com/android/tools/idea/gradle/variant/view/BuildVariantView.java
@@ -22,18 +22,16 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
-import com.intellij.ProjectTopics;
+import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
-import com.intellij.openapi.project.ModuleAdapter;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentFactory;
import com.intellij.ui.table.JBTable;
-import com.intellij.util.messages.MessageBusConnection;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -110,18 +108,6 @@
Content content = contentFactory.createContent(myToolWindowPanel, "", false);
toolWindow.getContentManager().addContent(content);
updateContents();
- MessageBusConnection connection = myProject.getMessageBus().connect();
- connection.subscribe(ProjectTopics.MODULES, new ModuleAdapter() {
- @Override
- public void moduleAdded(Project project, Module module) {
- updateContents();
- }
-
- @Override
- public void moduleRemoved(Project project, Module module) {
- updateContents();
- }
- });
}
public void updateContents() {
@@ -135,11 +121,11 @@
ModuleManager moduleManager = ModuleManager.getInstance(myProject);
for (Module module : moduleManager.getModules()) {
- AndroidFacet androidFacet = Facets.getFirstFacet(module, AndroidFacet.ID);
+ AndroidFacet androidFacet = Facets.getFirstFacetOfType(module, AndroidFacet.ID);
if (androidFacet == null) {
continue;
}
- if (Facets.getFirstFacet(module, AndroidGradleFacet.TYPE_ID) == null) {
+ if (Facets.getFirstFacetOfType(module, AndroidGradleFacet.TYPE_ID) == null) {
// If the module does not have an Android-Gradle facet, just skip it.
continue;
}
@@ -158,12 +144,18 @@
rows.add(row);
}
}
- ApplicationManager.getApplication().invokeLater(new Runnable() {
+ Runnable setModelTask = new Runnable() {
@Override
public void run() {
getVariantsTable().setModel(rows, variantNamesPerRow);
}
- });
+ };
+ Application application = ApplicationManager.getApplication();
+ if (application.isDispatchThread()) {
+ setModelTask.run();
+ } else {
+ application.invokeLater(setModelTask);
+ }
}
@NotNull
@@ -189,7 +181,7 @@
@Nullable
private static IdeaAndroidProject getAndroidProject(@NotNull Module module) {
- AndroidFacet androidFacet = Facets.getFirstFacet(module, AndroidFacet.ID);
+ AndroidFacet androidFacet = Facets.getFirstFacetOfType(module, AndroidFacet.ID);
return androidFacet != null ? androidFacet.getIdeaAndroidProject() : null;
}
@@ -260,13 +252,8 @@
void setLoading(boolean loading) {
myLoading = loading;
setPaintBusy(myLoading);
- String text;
- if (myLoading) {
- clearContents();
- text = "Loading...";
- } else {
- text = "Nothing to Show";
- }
+ clearContents();
+ String text = myLoading ? "Loading..." : "Nothing to Show";
getEmptyText().setText(text);
}
diff --git a/android/src/com/android/tools/idea/javadoc/AndroidJavaDocRenderer.java b/android/src/com/android/tools/idea/javadoc/AndroidJavaDocRenderer.java
index dbb2628..c6bff44 100644
--- a/android/src/com/android/tools/idea/javadoc/AndroidJavaDocRenderer.java
+++ b/android/src/com/android/tools/idea/javadoc/AndroidJavaDocRenderer.java
@@ -17,15 +17,24 @@
package com.android.tools.idea.javadoc;
import com.android.SdkConstants;
+import com.android.builder.model.*;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
import com.android.ide.common.resources.configuration.DensityQualifier;
import com.android.resources.Density;
import com.android.resources.ResourceType;
-import com.android.tools.idea.rendering.HtmlBuilder;
-import com.android.tools.idea.rendering.ProjectResources;
+import com.android.tools.idea.gradle.IdeaAndroidProject;
+import com.android.tools.idea.rendering.*;
+import com.android.utils.HtmlBuilder;
import com.android.utils.XmlUtils;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -39,69 +48,242 @@
import java.net.URL;
import java.util.*;
import java.util.List;
+import java.util.Locale;
+
+import static org.jetbrains.android.util.AndroidUtils.hasImageExtension;
public class AndroidJavaDocRenderer {
/** Renders the Javadoc for a resource of given type and name. */
- @Nullable
- public static String render(ProjectResources projectResources, ResourceType type, String name) {
+ @Nullable public static String render(Module module, ResourceType type, String name) {
+ ProjectResources projectResources = ProjectResources.get(module, true, true);
+ if (projectResources == null) {
+ return null;
+ }
+
if (ResourceType.STRING.equals(type) || ResourceType.DIMEN.equals(type)
|| ResourceType.INTEGER.equals(type) || ResourceType.BOOL.equals(type)) {
- List<ResourceItem> items = projectResources.getResourceItem(type, name);
- if (items != null) {
- return renderKeyValues(sort(items), type, name, new TextValueRenderer(), projectResources);
- }
+ return renderKeyValues(module, new TextValueRenderer(), projectResources, type, name);
} else if (ResourceType.DRAWABLE.equals(type)) {
- List<ResourceItem> items = projectResources.getResourceItem(type, name);
- if (items != null) {
- return renderKeyValues(sort(items), type, name, new DrawableValueRenderer(), projectResources);
- }
+ return renderKeyValues(module, new DrawableValueRenderer(), projectResources, type, name);
}
return null;
}
- private static List<ResourceItem> sort(@NotNull List<ResourceItem> resourceFiles) {
- List<ResourceItem> copy = new ArrayList<ResourceItem>(resourceFiles);
- Collections.sort(copy, new Comparator<ResourceItem>() {
- @Override
- public int compare(ResourceItem item1, ResourceItem item2) {
- ResourceFile file1 = item1.getSource();
- ResourceFile file2 = item2.getSource();
- String parent1 = file1 != null ? file1.getFile().getParentFile().getName() : "";
- String parent2 = file2 != null ? file2.getFile().getParentFile().getName() : "";
- return parent1.compareTo(parent2);
- }
- });
- return copy;
+ @Nullable
+ private static String renderKeyValues(Module module, ResourceValueRenderer renderer,
+ ProjectResources resources, ResourceType type, String name) {
+ List<ItemInfo> items = gatherItems(module, resources, type, name);
+ if (items != null) {
+ Collections.sort(items);
+ return renderKeyValues(items, renderer, resources);
+ }
+
+ return null;
}
@Nullable
- private static String renderKeyValues(List<ResourceItem> items, ResourceType type, String name,
- ResourceValueRenderer renderer, ProjectResources resources) {
+ private static List<ItemInfo> gatherItems(Module module, ProjectResources resources, ResourceType type, String name) {
+ AndroidFacet facet = AndroidFacet.getInstance(module);
+ if (facet == null) {
+ return null;
+ }
+
+ List<ItemInfo> results = Lists.newArrayList();
+
+ IdeaAndroidProject ideaAndroidProject = facet.getIdeaAndroidProject();
+ if (ideaAndroidProject != null) {
+ assert facet.isGradleProject();
+ AndroidProject delegate = ideaAndroidProject.getDelegate();
+ Variant selectedVariant = ideaAndroidProject.getSelectedVariant();
+ Set<SourceProvider> selectedProviders = Sets.newHashSet();
+
+ SourceProvider buildType = delegate.getBuildTypes().get(selectedVariant.getBuildType()).getSourceProvider();
+ String buildTypeName = selectedVariant.getName();
+ int rank = 0;
+ addItemsFromSourceSet(buildTypeName, MASK_FLAVOR_SELECTED, rank++, buildType, type, name, results, facet);
+ selectedProviders.add(buildType);
+
+ List<String> productFlavors = selectedVariant.getProductFlavors();
+ // Iterate in *reverse* order
+ for (int i = productFlavors.size() - 1; i >= 0; i--) {
+ String flavor = productFlavors.get(i);
+ SourceProvider provider = delegate.getProductFlavors().get(flavor).getSourceProvider();
+ addItemsFromSourceSet(flavor, MASK_FLAVOR_SELECTED, rank++, provider, type, name, results, facet);
+ selectedProviders.add(provider);
+ }
+
+ SourceProvider main = delegate.getDefaultConfig().getSourceProvider();
+ addItemsFromSourceSet("main", MASK_FLAVOR_SELECTED, rank++, main, type, name, results, facet);
+ selectedProviders.add(main);
+
+ // Next display any source sets that are *not* in the selected flavors or build types!
+ Collection<BuildTypeContainer> buildTypes = delegate.getBuildTypes().values();
+ for (BuildTypeContainer container : buildTypes) {
+ SourceProvider provider = container.getSourceProvider();
+ if (!selectedProviders.contains(provider)) {
+ addItemsFromSourceSet(container.getBuildType().getName(), MASK_NORMAL, rank++, provider, type, name, results, facet);
+ selectedProviders.add(provider);
+ }
+ }
+
+ Map<String,ProductFlavorContainer> flavors = delegate.getProductFlavors();
+ for (Map.Entry<String,ProductFlavorContainer> entry : flavors.entrySet()) {
+ ProductFlavorContainer container = entry.getValue();
+ SourceProvider provider = container.getSourceProvider();
+ if (!selectedProviders.contains(provider)) {
+ addItemsFromSourceSet(entry.getKey(), MASK_NORMAL, rank++, provider, type, name, results, facet);
+ selectedProviders.add(provider);
+ }
+ }
+
+ // Also pull in items from libraries; this will include items from the current module as well,
+ // so add them to a temporary list so we can only add the items that are missing
+ if (resources instanceof MultiResourceRepository) {
+ ProjectResources primary = ProjectResources.get(module, false);
+ MultiResourceRepository multi = (MultiResourceRepository)resources;
+ for (ProjectResources dependency : multi.getChildren()) {
+ if (dependency != primary) {
+ addItemsFromRepository(dependency.getDisplayName(), MASK_NORMAL, rank++, dependency, type, name, results);
+ }
+ }
+ }
+ } else {
+ addItemsFromRepository(null, MASK_NORMAL, 0, resources, type, name, results);
+ }
+
+ return results;
+ }
+
+ private static void addItemsFromSourceSet(@Nullable String flavor,
+ int mask,
+ int rank,
+ @NotNull SourceProvider sourceProvider,
+ @NotNull ResourceType type,
+ @NotNull String name,
+ @NotNull List<ItemInfo> results,
+ @NotNull AndroidFacet facet) {
+ Set<File> resDirectories = sourceProvider.getResDirectories();
+ LocalFileSystem fileSystem = LocalFileSystem.getInstance();
+ for (File dir : resDirectories) {
+ VirtualFile virtualFile = fileSystem.findFileByIoFile(dir);
+ if (virtualFile != null) {
+ ResourceFolderRepository resources = ResourceFolderRegistry.get(facet, virtualFile);
+ addItemsFromRepository(flavor, mask, rank, resources, type, name, results);
+ }
+ }
+ }
+
+ private static void addItemsFromRepository(@Nullable String flavor,
+ int mask,
+ int rank,
+ @NotNull ProjectResources resources,
+ @NotNull ResourceType type,
+ @NotNull String name,
+ @NotNull List<ItemInfo> results) {
+ List<ResourceItem> items = resources.getResourceItem(type, name);
+ if (items != null) {
+ for (ResourceItem item : items) {
+ String folderName = "?";
+ ResourceFile source = item.getSource();
+ if (source != null) {
+ folderName = source.getFile().getParentFile().getName();
+ }
+ String folder = renderFolderName(folderName);
+ ItemInfo info = new ItemInfo(item, folder, flavor, rank, mask);
+ results.add(info);
+ }
+ }
+ }
+
+ @Nullable
+ private static String renderKeyValues(List<ItemInfo> items, ResourceValueRenderer renderer,
+ ProjectResources resources) {
if (items.isEmpty()) {
return null;
}
+ markHidden(items);
+
HtmlBuilder builder = new HtmlBuilder();
+ builder.openHtmlBody();
if (items.size() == 1) {
- String value = renderer.renderToHtml(resources, items.get(0));
+ String value = renderer.renderToHtml(resources, items.get(0).item);
builder.addHtml(value);
} else {
+ //noinspection SpellCheckingInspection
builder.beginTable("valign=\"top\"");
- builder.addTableRow(true, "Configuration", "Value");
- for (ResourceItem f : items) {
- String v = renderer.renderToHtml(resources, f);
- String folderName = "?";
- ResourceFile source = f.getSource();
- if (source != null) {
- folderName = source.getFile().getParentFile().getName();
+ boolean haveFlavors = haveFlavors(items);
+ if (haveFlavors) {
+ builder.addTableRow(true, "Flavor/Library", "Configuration", "Value");
+ } else {
+ builder.addTableRow(true, "Configuration", "Value");
+ }
+
+ String prevFlavor = null;
+ for (ItemInfo info : items) {
+ String value = renderer.renderToHtml(resources, info.item);
+ String folder = info.folder;
+ String flavor = StringUtil.notNullize(info.flavor);
+ if (flavor.equals(prevFlavor)) {
+ flavor = "";
+ } else {
+ prevFlavor = flavor;
}
- builder.addTableRow(renderFolderName(folderName), v);
+
+ builder.addHtml("<tr>");
+ if (haveFlavors) {
+ // Bold selected flavors?
+ String style = ( (info.displayMask & MASK_FLAVOR_SELECTED) != 0) ? "b" : null;
+ addTableCell(builder, flavor, style);
+ }
+ addTableCell(builder, folder, null);
+ String style = ( (info.displayMask & MASK_ITEM_HIDDEN) != 0) ? "s" : null;
+ addTableCell(builder, value, style);
+ builder.addHtml("</tr>");
}
builder.endTable();
}
- return String.format("<html><body>%s</body></html>", builder.getHtml());
+ builder.addHtml("</body></html>");
+ return builder.getHtml();
+ }
+
+ private static boolean haveFlavors(List<ItemInfo> items) {
+ for (ItemInfo info : items) {
+ if (info.flavor != null) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static void markHidden(List<ItemInfo> items) {
+ Set<String> hiddenQualifiers = Sets.newHashSet();
+ for (ItemInfo info : items) {
+ String folder = info.folder;
+
+ if (hiddenQualifiers.contains(folder)) {
+ info.displayMask |= MASK_ITEM_HIDDEN;
+ }
+ hiddenQualifiers.add(folder);
+ }
+ }
+
+ private static void addTableCell(HtmlBuilder builder, String text, @Nullable String attribute) {
+ //noinspection SpellCheckingInspection
+ builder.addHtml("<td valign=\"top\">");
+ if (attribute != null) {
+ builder.addHtml("<").addHtml(attribute).addHtml(">");
+ }
+
+ builder.addHtml(text);
+
+ if (attribute != null) {
+ builder.addHtml("</").addHtml(attribute).addHtml(">");
+ }
+ builder.addHtml("</td>");
}
private static String renderFolderName(String name) {
@@ -138,7 +320,7 @@
private static String getStringValue(@NotNull ProjectResources resources, @NotNull ResourceItem item) {
String v = item.getValueText();
if (v == null) {
- ResourceValue value = resources.getConfiguredValue(item.getType(), item.getName(), item.getConfiguration());
+ ResourceValue value = item.getResourceValue(resources.isFramework());
if (value != null) {
return value.getValue();
}
@@ -154,14 +336,14 @@
return "";
}
String v = source.getFile().getPath();
- if (!isBitmapDrawable(v)) {
+ if (!hasImageExtension(v)) {
v = getStringValue(resources, item);
if (v == null) {
return "";
}
}
- if (isBitmapDrawable(v)) {
+ if (hasImageExtension(v)) {
File bitmap = new File(v);
if (bitmap.exists()) {
URL url = null;
@@ -199,20 +381,13 @@
private static int px2dp(int px, Density density) {
return (int)((float)px * Density.MEDIUM.getDpiValue() / density.getDpiValue());
}
-
- private static boolean isBitmapDrawable(String v) {
- return v.endsWith(SdkConstants.DOT_PNG)
- || v.endsWith(SdkConstants.DOT_9PNG)
- || v.endsWith(SdkConstants.DOT_GIF)
- || v.endsWith(SdkConstants.DOT_JPEG)
- || v.endsWith(SdkConstants.DOT_JPG);
- }
}
/**
* Returns the dimensions of an Image if it can be obtained without fully reading it into memory.
* This is a copy of the method in {@link com.android.tools.lint.checks.IconDetector}.
*/
+ @Nullable
private static Dimension getSize(File file) {
try {
ImageInputStream input = ImageIO.createImageInputStream(file);
@@ -229,9 +404,7 @@
}
}
} finally {
- if (input != null) {
- input.close();
- }
+ input.close();
}
}
@@ -248,4 +421,61 @@
return null;
}
}
+
+ /** Normal display style */
+ private static final int MASK_NORMAL = 0;
+ /** Display style for flavor folders that are selected */
+ private static final int MASK_FLAVOR_SELECTED = 1;
+ /** Display style for items that are hidden by later resource folders */
+ private static final int MASK_ITEM_HIDDEN = 2;
+
+ /**
+ * Information about {@link ResourceItem} instances to be displayed; in addition to the item and the
+ * folder name, we can also record the flavor or library name, as well as display attributes indicating
+ * whether the item is from a selected flavor, or whether the item is masked by a higher priority repository
+ */
+ private static class ItemInfo implements Comparable<ItemInfo> {
+ @NotNull public final ResourceItem item;
+ @Nullable public final String flavor;
+ @NotNull public final String folder;
+ public final int rank;
+ public int displayMask;
+
+ private ItemInfo(@NotNull ResourceItem item, @NotNull String folder, @Nullable String flavor, int rank, int initialMask) {
+ this.item = item;
+ this.flavor = flavor;
+ this.folder = folder;
+ this.displayMask = initialMask;
+ this.rank = rank;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ItemInfo line = (ItemInfo)o;
+
+ if (!item.equals(line.item)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return item.hashCode();
+ }
+
+ @Override
+ public int compareTo(@NotNull ItemInfo other) {
+ if (rank != other.rank) {
+ return rank - other.rank;
+ }
+ ResourceFile file1 = item.getSource();
+ ResourceFile file2 = other.item.getSource();
+ String parent1 = file1 != null ? file1.getFile().getParentFile().getName() : "";
+ String parent2 = file2 != null ? file2.getFile().getParentFile().getName() : "";
+ return parent1.compareTo(parent2);
+ }
+ }
}
diff --git a/android/src/com/android/tools/idea/refactoring/rtl/RtlRefactoringUsageInfo.java b/android/src/com/android/tools/idea/refactoring/rtl/RtlRefactoringUsageInfo.java
new file mode 100644
index 0000000..c44df7a
--- /dev/null
+++ b/android/src/com/android/tools/idea/refactoring/rtl/RtlRefactoringUsageInfo.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.refactoring.rtl;
+
+import com.intellij.psi.PsiElement;
+import com.intellij.usageView.UsageInfo;
+import org.jetbrains.annotations.NotNull;
+
+class RtlRefactoringUsageInfo extends UsageInfo {
+
+ private RtlRefactoringType myType = RtlRefactoringType.UNDEFINED;
+ private boolean myCreateV17;
+ private int myAndroidManifestMinSdkVersion;
+
+ public enum RtlRefactoringType {
+ UNDEFINED,
+ MANIFEST_SUPPORTS_RTL,
+ MANIFEST_TARGET_SDK,
+ LAYOUT_FILE_ATTRIBUTE,
+ STYLE
+ }
+
+ public RtlRefactoringUsageInfo(@NotNull PsiElement element, int startOffset, int endOffset) {
+ super(element, startOffset, endOffset);
+ }
+
+ RtlRefactoringType getType() {
+ return myType;
+ }
+
+ void setType(RtlRefactoringType type) {
+ myType = type;
+ }
+
+ boolean isCreateV17() {
+ return myCreateV17;
+ }
+
+ void setCreateV17(boolean createV17) {
+ myCreateV17 = createV17;
+ }
+
+ int getAndroidManifestMinSdkVersion() {
+ return myAndroidManifestMinSdkVersion;
+ }
+
+ void setAndroidManifestMinSdkVersion(int version) {
+ myAndroidManifestMinSdkVersion = version;
+ }
+}
diff --git a/android/src/com/android/tools/idea/refactoring/rtl/RtlSupportDialog.form b/android/src/com/android/tools/idea/refactoring/rtl/RtlSupportDialog.form
new file mode 100644
index 0000000..f9476a0
--- /dev/null
+++ b/android/src/com/android/tools/idea/refactoring/rtl/RtlSupportDialog.form
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.android.tools.idea.refactoring.rtl.RtlSupportDialog">
+ <grid id="cbd77" binding="myPanel" layout-manager="BorderLayout" hgap="0" vgap="0">
+ <constraints>
+ <xy x="48" y="54" width="389" height="221"/>
+ </constraints>
+ <properties/>
+ <border type="none"/>
+ <children>
+ <grid id="a3954" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+ <margin top="0" left="0" bottom="0" right="0"/>
+ <constraints border-constraint="North"/>
+ <properties/>
+ <border type="none"/>
+ <children>
+ <component id="8b81e" class="javax.swing.JTextArea" binding="myLabel">
+ <constraints>
+ <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <background swing-color="Button.background"/>
+ <text resource-bundle="messages/AndroidBundle" key="android.refactoring.rtl.addsupport.dialog.label.text"/>
+ </properties>
+ </component>
+ </children>
+ </grid>
+ <grid id="902d1" layout-manager="GridLayoutManager" row-count="3" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+ <margin top="0" left="0" bottom="0" right="0"/>
+ <constraints border-constraint="Center"/>
+ <properties/>
+ <border type="none"/>
+ <children>
+ <component id="7a3c5" class="javax.swing.JCheckBox" binding="myAndroidManifestCheckBox" default-binding="true">
+ <constraints>
+ <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text resource-bundle="messages/AndroidBundle" key="android.refactoring.rtl.addsupport.dialog.option.label.update.manifest.text"/>
+ </properties>
+ </component>
+ <component id="6895b" class="javax.swing.JCheckBox" binding="myLayoutsCheckBox" default-binding="true">
+ <constraints>
+ <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text resource-bundle="messages/AndroidBundle" key="android.refactoring.rtl.addsupport.dialog.option.label.update.layouts.text"/>
+ </properties>
+ </component>
+ <grid id="2ae50" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+ <margin top="0" left="0" bottom="0" right="0"/>
+ <constraints>
+ <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties/>
+ <border type="none" title-resource-bundle="messages/AndroidBundle" title-key="android.refactoring.rtl.addsupport.dialog.option.label.layouts.options.txt"/>
+ <children>
+ <component id="6e04c" class="javax.swing.JCheckBox" binding="myReplaceLeftRightPropertiesCheckBox" default-binding="true">
+ <constraints>
+ <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false">
+ <preferred-size width="251" height="23"/>
+ </grid>
+ </constraints>
+ <properties>
+ <text resource-bundle="messages/AndroidBundle" key="android.refactoring.rtl.addsupport.dialog.option.label.layouts.options.replace.leftright.txt"/>
+ </properties>
+ </component>
+ <component id="3e044" class="javax.swing.JCheckBox" binding="myGenerateV17VersionsCheckBox" default-binding="true">
+ <constraints>
+ <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false">
+ <preferred-size width="251" height="23"/>
+ </grid>
+ </constraints>
+ <properties>
+ <text resource-bundle="messages/AndroidBundle" key="android.refactoring.rtl.addsupport.dialog.option.label.layouts.options.generate.v17.txt"/>
+ </properties>
+ </component>
+ </children>
+ </grid>
+ </children>
+ </grid>
+ </children>
+ </grid>
+ <buttonGroups>
+ <group name="myLayoutOptionsButtonGroup">
+ <member id="af949"/>
+ <member id="a9ab"/>
+ </group>
+ </buttonGroups>
+</form>
diff --git a/android/src/com/android/tools/idea/refactoring/rtl/RtlSupportDialog.java b/android/src/com/android/tools/idea/refactoring/rtl/RtlSupportDialog.java
new file mode 100644
index 0000000..e3186c9
--- /dev/null
+++ b/android/src/com/android/tools/idea/refactoring/rtl/RtlSupportDialog.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.idea.refactoring.rtl;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.DialogWrapper;
+import org.jetbrains.android.util.AndroidBundle;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+
+public class RtlSupportDialog extends DialogWrapper {
+ private JPanel myPanel;
+ private JCheckBox myAndroidManifestCheckBox;
+ private JCheckBox myLayoutsCheckBox;
+ private JTextArea myLabel;
+ private JCheckBox myReplaceLeftRightPropertiesCheckBox;
+ private JCheckBox myGenerateV17VersionsCheckBox;
+
+ private final RtlSupportProperties myProperties;
+
+ public RtlSupportDialog(Project project) {
+ super(project, true);
+
+ myProperties = new RtlSupportProperties();
+
+ setTitle(AndroidBundle.message("android.refactoring.rtl.addsupport.dialog.title"));
+ setOKButtonText(AndroidBundle.message("android.refactoring.rtl.addsupport.dialog.ok.button.text"));
+
+ myLayoutsCheckBox.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent itemEvent) {
+ final boolean isSelected = myLayoutsCheckBox.isSelected();
+ myReplaceLeftRightPropertiesCheckBox.setEnabled(isSelected);
+ myGenerateV17VersionsCheckBox.setEnabled(isSelected);
+ }
+ });
+
+ setDefaultValues();
+
+ init();
+ }
+
+ private void setDefaultValues() {
+ myAndroidManifestCheckBox.setSelected(true);
+ myLayoutsCheckBox.setSelected(true);
+ }
+
+ @Override
+ @NotNull
+ protected Action[] createActions() {
+ return new Action[]{getOKAction(), getCancelAction(), getHelpAction()};
+ }
+
+ @Override
+ @Nullable
+ protected JComponent createCenterPanel() {
+ return myPanel;
+ }
+
+ @Override
+ public JComponent getPreferredFocusedComponent() {
+ return myAndroidManifestCheckBox;
+ }
+
+ public final RtlSupportProperties getProperties() {
+ myProperties.updateAndroidManifest = myAndroidManifestCheckBox.isSelected();
+ myProperties.updateLayouts = myLayoutsCheckBox.isSelected();
+ myProperties.replaceLeftRightPropertiesOption = myReplaceLeftRightPropertiesCheckBox.isSelected();
+ myProperties.generateV17resourcesOption = myGenerateV17VersionsCheckBox.isSelected();
+
+ // When generating v17 layout file, we force replacing left/right properties by start/end properties
+ if (myProperties.generateV17resourcesOption) {
+ myProperties.replaceLeftRightPropertiesOption = true;
+ }
+
+ return myProperties;
+ }
+}
diff --git a/android/src/com/android/tools/idea/refactoring/rtl/RtlSupportManager.java b/android/src/com/android/tools/idea/refactoring/rtl/RtlSupportManager.java
new file mode 100644
index 0000000..d962452
--- /dev/null
+++ b/android/src/com/android/tools/idea/refactoring/rtl/RtlSupportManager.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.idea.refactoring.rtl;
+
+import com.intellij.openapi.project.Project;
+
+public class RtlSupportManager {
+ private final Project myProject;
+
+ public RtlSupportManager(Project project) {
+ myProject = project;
+ }
+
+ public void showDialog() {
+ final RtlSupportDialog dialog = new RtlSupportDialog(myProject);
+ dialog.show();
+ if (!dialog.isOK()) {
+ return;
+ }
+ new RtlSupportProcessor(myProject, dialog.getProperties()).run();
+ }
+}
diff --git a/android/src/com/android/tools/idea/refactoring/rtl/RtlSupportProcessor.java b/android/src/com/android/tools/idea/refactoring/rtl/RtlSupportProcessor.java
new file mode 100644
index 0000000..ccb328d
--- /dev/null
+++ b/android/src/com/android/tools/idea/refactoring/rtl/RtlSupportProcessor.java
@@ -0,0 +1,563 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.idea.refactoring.rtl;
+
+import com.android.SdkConstants;
+import com.android.resources.ResourceFolderType;
+import com.android.tools.idea.rendering.ManifestInfo;
+import com.android.xml.AndroidManifest;
+import com.google.common.collect.Maps;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.*;
+import com.intellij.psi.xml.XmlAttribute;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.psi.xml.XmlTag;
+import com.intellij.refactoring.BaseRefactoringProcessor;
+import com.intellij.usageView.UsageInfo;
+import com.intellij.usageView.UsageViewDescriptor;
+import com.intellij.util.xml.DomElement;
+import com.intellij.util.xml.DomManager;
+import org.jetbrains.android.dom.layout.LayoutDomFileDescription;
+import org.jetbrains.android.dom.layout.LayoutViewElement;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.android.facet.AndroidRootUtil;
+import org.jetbrains.android.util.AndroidBundle;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static com.android.SdkConstants.*;
+import static com.android.tools.idea.refactoring.rtl.RtlRefactoringUsageInfo.RtlRefactoringType.*;
+import static com.android.xml.AndroidManifest.*;
+
+public class RtlSupportProcessor extends BaseRefactoringProcessor {
+
+ private static final Logger LOG = Logger.getInstance("#com.android.tools.idea.refactoring.AddRTLSupportProcessor");
+ private static final String REFACTORING_NAME = AndroidBundle.message("android.refactoring.rtl.addsupport.title");
+
+ public static final String RES_V_QUALIFIER = "-v";
+ public static final String RES_V17_QUALIFIER = "-v17";
+
+ final private RtlSupportProperties myProperties;
+ final private Project myProject;
+
+ // This is the API level corresponding to the first public release for RTL support
+ final static int RTL_TARGET_SDK_START = 17;
+
+ private static Map<String, String> ourMapMirroredAttributeName = Maps.newHashMapWithExpectedSize(12);
+
+ static {
+ initMapMirroredAttributes();
+ }
+
+ private static void initMapMirroredAttributes() {
+ ourMapMirroredAttributeName.put(ATTR_PADDING_LEFT, ATTR_PADDING_START);
+ ourMapMirroredAttributeName.put(ATTR_PADDING_RIGHT, ATTR_PADDING_END);
+ ourMapMirroredAttributeName.put(ATTR_LAYOUT_MARGIN_LEFT, ATTR_LAYOUT_MARGIN_START);
+ ourMapMirroredAttributeName.put(ATTR_LAYOUT_MARGIN_RIGHT, ATTR_LAYOUT_MARGIN_END);
+ ourMapMirroredAttributeName.put(ATTR_DRAWABLE_LEFT, ATTR_DRAWABLE_START);
+ ourMapMirroredAttributeName.put(ATTR_DRAWABLE_RIGHT, ATTR_DRAWABLE_END);
+ ourMapMirroredAttributeName.put(ATTR_LAYOUT_TO_LEFT_OF, ATTR_LAYOUT_TO_START_OF);
+ ourMapMirroredAttributeName.put(ATTR_LAYOUT_TO_RIGHT_OF, ATTR_LAYOUT_TO_END_OF);
+ ourMapMirroredAttributeName.put(ATTR_LAYOUT_ALIGN_LEFT, ATTR_LAYOUT_ALIGN_START);
+ ourMapMirroredAttributeName.put(ATTR_LAYOUT_ALIGN_RIGHT, ATTR_LAYOUT_ALIGN_END);
+ ourMapMirroredAttributeName.put(ATTR_LAYOUT_ALIGN_PARENT_LEFT, ATTR_LAYOUT_ALIGN_PARENT_START);
+ ourMapMirroredAttributeName.put(ATTR_LAYOUT_ALIGN_PARENT_RIGHT, ATTR_LAYOUT_ALIGN_PARENT_END);
+
+ // Gravity is a special case that we will handled separately as we will mirror its value instead of its name
+ }
+
+ protected RtlSupportProcessor(Project project, @NotNull RtlSupportProperties properties) {
+ super(project);
+ myProject = project;
+ myProperties = properties;
+ setPreviewUsages(true);
+ }
+
+ @NotNull
+ @Override
+ protected UsageViewDescriptor createUsageViewDescriptor(UsageInfo[] usages) {
+ return new RtlSupportUsageViewDescriptor();
+ }
+
+ @NotNull
+ @Override
+ protected UsageInfo[] findUsages() {
+ if (!myProperties.hasSomethingToDo()) {
+ return UsageInfo.EMPTY_ARRAY;
+ }
+ final List<UsageInfo> list = new ArrayList<UsageInfo>();
+
+ if (myProperties.updateAndroidManifest) {
+ addManifestRefactoring(list);
+ }
+
+ if (myProperties.updateLayouts) {
+ addLayoutRefactoring(list);
+ }
+
+ final int size = list.size();
+ return list.toArray(new UsageInfo[size]);
+ }
+
+ @Override
+ protected void performRefactoring(UsageInfo[] usages) {
+ for (UsageInfo usageInfo : usages) {
+ RtlRefactoringUsageInfo refactoring = (RtlRefactoringUsageInfo)usageInfo;
+ switch (refactoring.getType()) {
+ case MANIFEST_SUPPORTS_RTL:
+ performRefactoringForAndroidManifestApplicationTag(refactoring);
+ break;
+ case MANIFEST_TARGET_SDK:
+ performRefactoringForAndroidManifestTargetSdk(refactoring);
+ break;
+ case LAYOUT_FILE_ATTRIBUTE:
+ performRefactoringForLayoutFile(refactoring);
+ break;
+ case UNDEFINED:
+ break;
+ default:
+ assert false : refactoring.getType();
+ }
+ }
+ }
+
+ @Override
+ protected void performPsiSpoilingRefactoring() {
+ PsiDocumentManager.getInstance(myProject).commitAllDocuments();
+ }
+
+ private void addManifestRefactoring(List<UsageInfo> list) {
+ // For all non library modules in our project
+ for (Module module : ModuleManager.getInstance(myProject).getModules()) {
+ AndroidFacet facet = AndroidFacet.getInstance(module);
+ if (facet != null && !facet.isLibraryProject()) {
+ final VirtualFile manifestFile = AndroidRootUtil.getManifestFile(facet);
+
+ XmlFile myManifestFile = (XmlFile)PsiManager.getInstance(myProject).findFile(manifestFile);
+ try {
+ XmlTag root = myManifestFile.getRootTag();
+ if (root == null) {
+ return;
+ }
+
+ // First, deal with "supportsRtl" into the <application> tag
+ XmlTag[] applicationNodes = root.findSubTags(NODE_APPLICATION);
+ if (applicationNodes.length > 0) {
+ assert applicationNodes.length == 1;
+
+ XmlTag applicationTag = applicationNodes[0];
+ XmlAttribute supportsRtlAttribute = applicationTag.getAttribute(AndroidManifest.ATTRIBUTE_SUPPORTS_RTL, ANDROID_URI);
+ if (supportsRtlAttribute == null || supportsRtlAttribute.getValue().equals(SdkConstants.VALUE_FALSE)) {
+ final int startOffset;
+ final int endOffset;
+ if (supportsRtlAttribute == null) {
+ XmlAttribute[] applicationTagAttributes = applicationTag.getAttributes();
+ XmlAttribute lastAttribute = applicationTagAttributes[applicationTagAttributes.length - 1];
+ PsiElement nextSibling = lastAttribute.getNextSibling();
+ assert nextSibling != null;
+
+ // Will position the caret just before the ">" for the application tag
+ startOffset = nextSibling.getStartOffsetInParent() + nextSibling.getTextLength();
+ endOffset = startOffset;
+ }
+ else {
+ // Will position the caret at the beginning of the "supportsRtl" attribute
+ startOffset = supportsRtlAttribute.getStartOffsetInParent();
+ endOffset = startOffset + supportsRtlAttribute.getTextLength();
+ }
+
+ RtlRefactoringUsageInfo usageInfo = new RtlRefactoringUsageInfo(applicationTag, startOffset, endOffset);
+ usageInfo.setType(MANIFEST_SUPPORTS_RTL);
+
+ list.add(usageInfo);
+ }
+ }
+
+ // Second, deal with targetSdkVersion / minSdkVersion
+ XmlTag[] usesSdkNodes = root.findSubTags(NODE_USES_SDK);
+ if (usesSdkNodes.length > 0) {
+ assert usesSdkNodes.length == 1;
+
+ XmlTag usesSdkTag = usesSdkNodes[0];
+ XmlAttribute targetSdkAttribute = usesSdkTag.getAttribute(ATTRIBUTE_TARGET_SDK_VERSION, ANDROID_URI);
+ int targetSdk = (targetSdkAttribute != null) ? Integer.parseInt(targetSdkAttribute.getValue()) : 0;
+
+ // Will need to set existing targetSdkVersion to 17
+ if (targetSdk == 0 || targetSdk < RTL_TARGET_SDK_START) {
+ // Will position the caret just at the start of
+ final int startOffset = (targetSdkAttribute != null)
+ ? targetSdkAttribute.getStartOffsetInParent()
+ : usesSdkTag.getStartOffsetInParent();
+ final int endOffset = startOffset +
+ ((targetSdkAttribute != null)
+ ? targetSdkAttribute.getTextLength()
+ : usesSdkTag.getTextLength());
+
+ RtlRefactoringUsageInfo usageInfo = new RtlRefactoringUsageInfo(usesSdkTag, startOffset, endOffset);
+ usageInfo.setType(MANIFEST_TARGET_SDK);
+
+ list.add(usageInfo);
+ }
+ }
+ }
+ catch (Exception e) {
+ LOG.error("Could not read Manifest data", e);
+ }
+ }
+ }
+ }
+
+ private static String quote(String str) {
+ return quoteWith(str, "'");
+ }
+
+ private static String quoteWith(String str, String quote) {
+ return quote + str + quote;
+ }
+
+ @Nullable
+ private VirtualFile getLayoutV17(final VirtualFile oneLayoutRes, boolean bCreateIfNeeded) {
+ final String resName = oneLayoutRes.getName();
+ if (resName.contains(RES_V_QUALIFIER)) {
+ return null;
+ }
+ final String resNameWithV17 = resName + RES_V17_QUALIFIER;
+ final VirtualFile parent = oneLayoutRes.getParent();
+ assert parent != null;
+ VirtualFile layoutV17Dir = parent.findChild(resNameWithV17);
+
+ if ((layoutV17Dir == null || !layoutV17Dir.exists()) && bCreateIfNeeded) {
+ try {
+ layoutV17Dir = parent.createChildDirectory(this, resNameWithV17);
+ }
+ catch (IOException e) {
+ LOG.error("Cannot create " + quote(resNameWithV17) + " directory in resource directory: " + parent.getName());
+ }
+ }
+
+ if (layoutV17Dir != null) {
+ assert layoutV17Dir.isDirectory() : layoutV17Dir;
+ }
+ return layoutV17Dir;
+ }
+
+ private List<UsageInfo> getLayoutRefactoringForOneDir(@NotNull VirtualFile layoutDir, boolean createV17, int minSdk) {
+ List<UsageInfo> result = new ArrayList<UsageInfo>();
+
+ final VirtualFile[] layoutChildren = layoutDir.getChildren();
+ for (final VirtualFile oneLayoutFile : layoutChildren) {
+ result.addAll(getLayoutRefactoringForOneFile(oneLayoutFile, createV17, minSdk));
+ }
+
+ return result;
+ }
+
+ private List<UsageInfo> getLayoutRefactoringForOneFile(@NotNull VirtualFile layoutFile, boolean createV17, int minSdk) {
+ final PsiFile psiFile = PsiManager.getInstance(myProject).findFile(layoutFile);
+ assert psiFile != null;
+ return getLayoutRefactoringForFile(psiFile, createV17, minSdk);
+ }
+
+ private void addLayoutRefactoring(List<UsageInfo> list) {
+ // For all non library modules in our project
+ for (Module module : ModuleManager.getInstance(myProject).getModules()) {
+ AndroidFacet facet = AndroidFacet.getInstance(module);
+ if (facet != null && !facet.isLibraryProject()) {
+ final int minSdk = ManifestInfo.get(module).getMinSdkVersion();
+
+ if (myProperties.generateV17resourcesOption) {
+ // First get all the "res" directories
+ final List<VirtualFile> allRes = facet.getAllResourceDirectories();
+
+ // Then, need to get all the "layout-XXX" sub directories
+ final List<VirtualFile> allLayoutDir = new ArrayList<VirtualFile>();
+
+ for (VirtualFile oneRes : allRes) {
+ final VirtualFile[] children = oneRes.getChildren();
+ // Check every children if they are a layout dir but not a "-v17" one
+ for (VirtualFile oneChild : children) {
+ final String childName = oneChild.getName();
+ if (childName.startsWith(FD_RES_LAYOUT) && !childName.contains(RES_V_QUALIFIER)) {
+ allLayoutDir.add(oneChild);
+ }
+ }
+ }
+
+ // For all "layout-XXX" entries, process all the contained files
+ for (final VirtualFile layoutDir : allLayoutDir) {
+ final VirtualFile layoutV17Dir = getLayoutV17(layoutDir, false /* no creation */);
+
+ // The corresponding "v17" directory already exists
+ if (layoutV17Dir != null) {
+ // ... so add refactoring for all files in the "v17" directory if needed
+ if (layoutV17Dir.getChildren().length != 0) {
+ list.addAll(getLayoutRefactoringForOneDir(layoutV17Dir, false /* do not create v17 version */, minSdk));
+ }
+ else {
+ list.addAll(getLayoutRefactoringForOneDir(layoutDir, true /* create v17 version */, minSdk));
+ }
+ }
+ else {
+ // otherwise all refactoring for all the non "v17" file and will create the "v17" file later on (we *cannot*
+ // create them here even with a ApplicationManager.getApplication().runWriteAction(...)
+ list.addAll(getLayoutRefactoringForOneDir(layoutDir, true /* create the v17 version */, minSdk));
+ }
+ }
+ }
+ else {
+ final List<PsiFile> files = facet.getLocalResourceManager().findResourceFiles(ResourceFolderType.LAYOUT.getName());
+
+ for (PsiFile psiFile : files) {
+ list.addAll(getLayoutRefactoringForFile(psiFile, false /* do not create the v17 version */, minSdk));
+ }
+ }
+ }
+ }
+ }
+
+ private List<UsageInfo> getLayoutRefactoringForFile(@NotNull final PsiFile layoutFile, final boolean createV17, final int minSdk) {
+ final List<UsageInfo> result = new ArrayList<UsageInfo>();
+
+ if (layoutFile instanceof XmlFile &&
+ DomManager.getDomManager(myProject).getDomFileDescription((XmlFile)layoutFile) instanceof LayoutDomFileDescription) {
+ layoutFile.accept(new XmlRecursiveElementVisitor() {
+ @Override
+ public void visitXmlTag(XmlTag tag) {
+ super.visitXmlTag(tag);
+
+ List<UsageInfo> usageInfos = getLayoutRefactoringForTag(tag, createV17, minSdk);
+ if (usageInfos.isEmpty()) {
+ return;
+ }
+ result.addAll(usageInfos);
+ }
+ });
+ }
+
+ return result;
+ }
+
+ private List<UsageInfo> getLayoutRefactoringForTag(@NotNull XmlTag tag, boolean createV17, int minSdk) {
+ final DomElement domElement = DomManager.getDomManager(myProject).getDomElement(tag);
+
+ if (!(domElement instanceof LayoutViewElement)) {
+ return Collections.emptyList();
+ }
+
+ final List<UsageInfo> result = new ArrayList<UsageInfo>();
+
+ final XmlAttribute[] attributes = tag.getAttributes();
+ for (XmlAttribute attributeToMirror : attributes) {
+ final String localName = attributeToMirror.getLocalName();
+ final String namespacePrefix = attributeToMirror.getNamespacePrefix();
+
+ final String mirroredLocalName = ourMapMirroredAttributeName.get(localName);
+ // Check if this is a RTL attribute to mirror or if it is a Gravity attribute
+ if (mirroredLocalName != null) {
+ // Mirror only attributes that has not been mirrored before
+ final XmlAttribute attributeMirrored = tag.getAttribute(namespacePrefix + ":" + mirroredLocalName);
+ if (attributeMirrored == null) {
+ final int startOffset = 0;
+ final int endOffset = attributeToMirror.getTextLength();
+ RtlRefactoringUsageInfo usageInfoForAttribute = new RtlRefactoringUsageInfo(attributeToMirror, startOffset, endOffset);
+ usageInfoForAttribute.setType(LAYOUT_FILE_ATTRIBUTE);
+ usageInfoForAttribute.setCreateV17(createV17);
+ usageInfoForAttribute.setAndroidManifestMinSdkVersion(minSdk);
+ result.add(usageInfoForAttribute);
+ }
+ }
+ else if (localName.equals(ATTR_GRAVITY) || localName.equals(ATTR_LAYOUT_GRAVITY)) {
+ final String value = attributeToMirror.getValue();
+ if (value != null && (value.contains(GRAVITY_VALUE_LEFT) || value.contains(GRAVITY_VALUE_RIGHT))) {
+ final int startOffset = 0;
+ final int endOffset = attributeToMirror.getTextLength();
+ RtlRefactoringUsageInfo usageInfoForAttribute = new RtlRefactoringUsageInfo(attributeToMirror, startOffset, endOffset);
+ usageInfoForAttribute.setType(LAYOUT_FILE_ATTRIBUTE);
+ usageInfoForAttribute.setCreateV17(createV17);
+ result.add(usageInfoForAttribute);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private static void performRefactoringForAndroidManifestApplicationTag(@NotNull UsageInfo usageInfo) {
+ PsiElement element = usageInfo.getElement();
+ assert element != null;
+ XmlTag applicationTag = (XmlTag)element;
+
+ XmlAttribute supportsRtlAttribute = applicationTag.getAttribute(ATTRIBUTE_SUPPORTS_RTL, ANDROID_URI);
+ if (supportsRtlAttribute != null) {
+ supportsRtlAttribute.setValue(SdkConstants.VALUE_TRUE);
+ }
+ else {
+ applicationTag.setAttribute(ATTRIBUTE_SUPPORTS_RTL, ANDROID_URI, SdkConstants.VALUE_TRUE);
+ }
+ }
+
+ private static void performRefactoringForAndroidManifestTargetSdk(@NotNull UsageInfo usageInfo) {
+ PsiElement element = usageInfo.getElement();
+ assert element != null;
+ XmlTag usesSdkTag = (XmlTag)element;
+
+ XmlAttribute targetSdkAttribute = usesSdkTag.getAttribute(ATTRIBUTE_TARGET_SDK_VERSION, ANDROID_URI);
+ if (targetSdkAttribute != null) {
+ targetSdkAttribute.setValue(Integer.toString(RTL_TARGET_SDK_START));
+ }
+ else {
+ usesSdkTag.setAttribute(ATTRIBUTE_TARGET_SDK_VERSION, ANDROID_URI, Integer.toString(RTL_TARGET_SDK_START));
+ }
+ }
+
+ private void performRefactoringForLayoutFile(@NotNull final RtlRefactoringUsageInfo usageInfo) {
+ final PsiElement element = usageInfo.getElement();
+ assert element != null;
+
+ final XmlAttribute attribute = (XmlAttribute)element;
+ final int minSdk = usageInfo.getAndroidManifestMinSdkVersion();
+
+ if (!usageInfo.isCreateV17()) {
+ updateAttributeForElement(attribute, minSdk);
+ }
+ else {
+ // We need first to create the v17 layout file, so first get our initial layout file
+ final PsiFile psiFile = element.getContainingFile();
+
+ final VirtualFile layoutFile = psiFile.getVirtualFile();
+ assert layoutFile != null;
+
+ final VirtualFile layoutDir = layoutFile.getParent();
+ assert layoutDir != null;
+
+ final VirtualFile layoutV17Dir = getLayoutV17(layoutDir, true /* create if needed */);
+ assert layoutV17Dir != null;
+
+ final String layoutFileName = layoutFile.getName();
+
+ // Create the v17 file if needed (should be done only once)
+ if (layoutV17Dir.findChild(layoutFileName) == null) {
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ layoutFile.copy(this, layoutV17Dir, layoutFileName);
+ }
+ catch (IOException e) {
+ LOG.error("Cannot copy layout file " + quote(layoutFileName) + " from " +
+ quote(layoutDir.getName()) + " directory to " + quote(layoutV17Dir.getName()) +
+ " directory");
+ }
+ }
+ });
+ }
+
+ final VirtualFile layoutV17File = layoutV17Dir.findChild(layoutFileName);
+ assert layoutV17File != null;
+
+ final XmlFile xmlV17File = (XmlFile)PsiManager.getInstance(myProject).findFile(layoutV17File);
+ assert xmlV17File != null;
+
+ LOG.info("Processing refactoring for attribute: " + attribute.getName() + " into file: " + layoutV17File.getPath());
+
+ if (DomManager.getDomManager(myProject).getDomFileDescription((XmlFile)xmlV17File) instanceof LayoutDomFileDescription) {
+ xmlV17File.accept(new XmlRecursiveElementVisitor() {
+ @Override
+ public void visitXmlTag(XmlTag tag) {
+ super.visitXmlTag(tag);
+
+ final XmlAttribute attribute = tag.getAttribute(((XmlAttribute)element).getName());
+ if (attribute == null) {
+ return;
+ }
+ updateAttributeForElement(attribute, minSdk);
+ }
+ });
+ }
+
+ layoutV17File.refresh(true /* asynchronous */, false /* not recursive */);
+ }
+ }
+
+ private void updateAttributeForElement(@NotNull XmlAttribute attribute, int minSdk) {
+ final String attributeLocalName = attribute.getLocalName();
+ LOG.info("Updating attribute name: " + attributeLocalName + " value: " + attribute.getValue());
+
+ if (attributeLocalName.equals(ATTR_GRAVITY) || attributeLocalName.equals(ATTR_LAYOUT_GRAVITY)) {
+ // Special case for android:gravity and android:layout_gravity
+ final String value = StringUtil.notNullize(attribute.getValue());
+ final String newValue = value.replace(GRAVITY_VALUE_LEFT, GRAVITY_VALUE_START).replace(GRAVITY_VALUE_RIGHT, GRAVITY_VALUE_END);
+ attribute.setValue(newValue);
+ LOG.info("Changing gravity from: " + value + " to: " + newValue);
+ }
+ else {
+ // General case for RTL attributes
+ final String mirroredAttributeLocalName = ourMapMirroredAttributeName.get(attributeLocalName);
+ if (mirroredAttributeLocalName == null) {
+ LOG.warn("Cannot mirror attribute: " + attribute.toString());
+ return;
+ }
+ final String mirroredAttributeName = attribute.getNamespacePrefix() + ":" + mirroredAttributeLocalName;
+ XmlAttribute attributeForUpdatingValue;
+ if (myProperties.replaceLeftRightPropertiesOption) {
+ attribute.setName(mirroredAttributeName);
+ LOG.info("Replacing attribute name from: " + attributeLocalName + " to: " + mirroredAttributeLocalName);
+ attributeForUpdatingValue = attribute;
+ }
+ else {
+ XmlTag parent = attribute.getParent();
+ attributeForUpdatingValue = parent.setAttribute(mirroredAttributeName, StringUtil.notNullize(attribute.getValue()));
+ LOG.info("Adding attribute name: " + mirroredAttributeName + " value: " + attribute.getValue());
+ }
+ // Special case for updating attribute value
+ updateAttributeValueIfNeeded(attributeForUpdatingValue, minSdk);
+ }
+ }
+
+ private static void updateAttributeValueIfNeeded(@NotNull XmlAttribute attribute, int minSdk) {
+ final String attributeLocalName = attribute.getLocalName();
+ final String value = StringUtil.notNullize(attribute.getValue());
+ if (attributeLocalName.equals(ATTR_PADDING_LEFT) || attributeLocalName.equals(ATTR_PADDING_RIGHT) ||
+ attributeLocalName.equals(ATTR_PADDING_START) || attributeLocalName.equals(ATTR_PADDING_END)) {
+ if (minSdk >= RTL_TARGET_SDK_START &&
+ (value.contains(ATTR_LIST_PREFERRED_ITEM_PADDING_LEFT) || value.contains(ATTR_LIST_PREFERRED_ITEM_PADDING_RIGHT))) {
+ final String newValue = value.replace(ATTR_LIST_PREFERRED_ITEM_PADDING_LEFT, ATTR_LIST_PREFERRED_ITEM_PADDING_START).
+ replace(ATTR_LIST_PREFERRED_ITEM_PADDING_RIGHT, ATTR_LIST_PREFERRED_ITEM_PADDING_END);
+ attribute.setValue(newValue);
+ LOG.info("Changing attribute value from: " + value + " to: " + newValue);
+ }
+ }
+ }
+
+ @Override
+ protected String getCommandName() {
+ return REFACTORING_NAME;
+ }
+
+}
diff --git a/android/src/com/android/tools/idea/refactoring/rtl/RtlSupportProperties.java b/android/src/com/android/tools/idea/refactoring/rtl/RtlSupportProperties.java
new file mode 100644
index 0000000..aebc246
--- /dev/null
+++ b/android/src/com/android/tools/idea/refactoring/rtl/RtlSupportProperties.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.idea.refactoring.rtl;
+
+public class RtlSupportProperties {
+
+ public boolean updateAndroidManifest = true;
+ public boolean updateLayouts = false;
+ public boolean replaceLeftRightPropertiesOption = true;
+ public boolean generateV17resourcesOption = false;
+
+ public boolean hasSomethingToDo() {
+ return updateAndroidManifest || updateLayouts;
+ }
+}
diff --git a/android/src/com/android/tools/idea/refactoring/rtl/RtlSupportUsageViewDescriptor.java b/android/src/com/android/tools/idea/refactoring/rtl/RtlSupportUsageViewDescriptor.java
new file mode 100644
index 0000000..5a4907b
--- /dev/null
+++ b/android/src/com/android/tools/idea/refactoring/rtl/RtlSupportUsageViewDescriptor.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.idea.refactoring.rtl;
+
+import com.intellij.psi.PsiElement;
+import com.intellij.usageView.UsageViewBundle;
+import com.intellij.usageView.UsageViewDescriptor;
+import org.jetbrains.android.util.AndroidBundle;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class RtlSupportUsageViewDescriptor implements UsageViewDescriptor {
+ public RtlSupportUsageViewDescriptor() {
+ }
+
+ @NotNull
+ @Override
+ public PsiElement[] getElements() {
+ return PsiElement.EMPTY_ARRAY;
+ }
+
+ @Override
+ public String getProcessedElementsHeader() {
+ return "Items to be converted";
+ }
+
+ @Override
+ public String getCodeReferencesText(int usagesCount, int filesCount) {
+ return String.format("RTL References in code %1$s", UsageViewBundle.getReferencesString(usagesCount, filesCount));
+ }
+
+ @Nullable
+ @Override
+ public String getCommentReferencesText(int usagesCount, int filesCount) {
+ return null;
+ }
+
+ public String getInfo() {
+ return AndroidBundle.message("android.refactoring.rtl.addsupport.dialog.apply.button.text");
+ }
+}
diff --git a/android/src/com/android/tools/idea/rendering/AddMissingAttributesFix.java b/android/src/com/android/tools/idea/rendering/AddMissingAttributesFix.java
index 6ee1f2c..baf9e83 100644
--- a/android/src/com/android/tools/idea/rendering/AddMissingAttributesFix.java
+++ b/android/src/com/android/tools/idea/rendering/AddMissingAttributesFix.java
@@ -26,6 +26,7 @@
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import org.jetbrains.annotations.NotNull;
@@ -81,9 +82,17 @@
}
public static boolean definesHeight(@NotNull XmlTag tag, @Nullable ResourceResolver resourceResolver) {
- boolean definesHeight = tag.getAttribute(ATTR_LAYOUT_HEIGHT, ANDROID_URI) != null;
+ XmlAttribute height = tag.getAttribute(ATTR_LAYOUT_HEIGHT, ANDROID_URI);
+ boolean definesHeight = height != null;
- if (!definesHeight && resourceResolver != null) {
+ if (definesHeight) {
+ String value = height.getValue();
+ if (value != null && !Character.isDigit(value.charAt(0))) {
+ return value.equals(VALUE_WRAP_CONTENT) || value.equals(VALUE_FILL_PARENT) || value.equals(VALUE_MATCH_PARENT);
+ }
+
+ return value != null;
+ } else if (resourceResolver != null) {
String style = tag.getAttributeValue(ATTR_STYLE);
if (style != null) {
ResourceValue st = resourceResolver.findResValue(style, false);
@@ -98,9 +107,17 @@
}
public static boolean definesWidth(@NotNull XmlTag tag, @Nullable ResourceResolver resourceResolver) {
- boolean definesWidth = tag.getAttribute(ATTR_LAYOUT_WIDTH, ANDROID_URI) != null;
+ XmlAttribute width = tag.getAttribute(ATTR_LAYOUT_WIDTH, ANDROID_URI);
+ boolean definesWidth = width != null;
- if (!definesWidth && resourceResolver != null) {
+ if (definesWidth) {
+ String value = width.getValue();
+ if (value != null && !Character.isDigit(value.charAt(0))) {
+ return value.equals(VALUE_WRAP_CONTENT) || value.equals(VALUE_FILL_PARENT) || value.equals(VALUE_MATCH_PARENT);
+ }
+
+ return value != null;
+ } else if (resourceResolver != null) {
String style = tag.getAttributeValue(ATTR_STYLE);
if (style != null) {
ResourceValue st = resourceResolver.findResValue(style, false);
@@ -173,15 +190,9 @@
}
String tagName = tag.getName();
- if (tagName.equals(REQUEST_FOCUS) || tagName.equals(VIEW_FRAGMENT) || tagName.equals(VIEW_MERGE)) {
+ if (tagName.equals(REQUEST_FOCUS) || tagName.equals(VIEW_MERGE) || tagName.equals(VIEW_INCLUDE)) {
return false;
}
- // TODO: What are the other exceptions?
-
- if (tagName.equals(VIEW_INCLUDE)) {
- return false;
- }
-
return true;
}
diff --git a/android/src/com/android/tools/idea/rendering/ContextPullParser.java b/android/src/com/android/tools/idea/rendering/ContextPullParser.java
index cd10256..935589b 100644
--- a/android/src/com/android/tools/idea/rendering/ContextPullParser.java
+++ b/android/src/com/android/tools/idea/rendering/ContextPullParser.java
@@ -19,6 +19,7 @@
import com.android.ide.common.rendering.api.ILayoutPullParser;
import com.android.ide.common.rendering.api.IProjectCallback;
import com.google.common.collect.Maps;
+import com.intellij.openapi.util.text.StringUtil;
import org.jetbrains.annotations.Nullable;
import org.kxml2.io.KXmlParser;
@@ -138,9 +139,15 @@
return VALUE_FILL_PARENT;
}
- // Handle unicode escapes
- if (value != null && value.indexOf('\\') != -1) {
- value = XmlTagPullParser.replaceUnicodeEscapes(value);
+ if (value != null) {
+ if (value.indexOf('&') != -1) {
+ value = StringUtil.unescapeXml(value);
+ }
+
+ // Handle unicode escapes
+ if (value.indexOf('\\') != -1) {
+ value = XmlTagPullParser.replaceUnicodeEscapes(value);
+ }
}
return value;
diff --git a/android/src/com/android/tools/idea/rendering/DelegatingProjectResources.java b/android/src/com/android/tools/idea/rendering/DelegatingProjectResources.java
deleted file mode 100644
index 834628a..0000000
--- a/android/src/com/android/tools/idea/rendering/DelegatingProjectResources.java
+++ /dev/null
@@ -1,434 +0,0 @@
-package com.android.tools.idea.rendering;
-
-import com.android.annotations.NonNull;
-import com.android.ide.common.rendering.api.ResourceValue;
-import com.android.ide.common.res2.MergeConsumer;
-import com.android.ide.common.res2.ResourceFile;
-import com.android.ide.common.res2.ResourceItem;
-import com.android.ide.common.resources.IntArrayWrapper;
-import com.android.ide.common.resources.configuration.FolderConfiguration;
-import com.android.resources.ResourceType;
-import com.android.util.Pair;
-import com.google.common.collect.*;
-import gnu.trove.TIntObjectHashMap;
-import gnu.trove.TObjectIntHashMap;
-import org.jetbrains.android.facet.AndroidFacet;
-import org.jetbrains.android.util.AndroidUtils;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.util.*;
-
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-@SuppressWarnings("deprecation") // Deprecated com.android.util.Pair is required by ProjectCallback interface
-class DelegatingProjectResources extends ProjectResources {
- private final List<ProjectResources> myDelegates;
- private final long[] myModificationCounts;
-
- DelegatingProjectResources(@NotNull List<ProjectResources> delegates) {
- super();
- myDelegates = delegates;
- assert delegates.size() >= 2; // factory should delegate to plain FileProjectResourceRepository if not
- myModificationCounts = new long[delegates.size()];
- for (int i = myDelegates.size() - 1; i >= 0; i--) {
- ProjectResources resources = myDelegates.get(i);
- myModificationCounts[i] = resources.getModificationCount();
- }
- }
-
- @NotNull
- public static ProjectResources create(@NotNull AndroidFacet facet) {
- List<AndroidFacet> libraries = AndroidUtils.getAllAndroidDependencies(facet.getModule(), true);
- boolean includeLibraries = false;
-
- ProjectResources main = get(facet.getModule(), includeLibraries);
- if (libraries.isEmpty()) {
- return main;
- }
- List<ProjectResources> resources = Lists.newArrayListWithExpectedSize(libraries.size());
- for (AndroidFacet f : libraries) {
- ProjectResources r = get(f.getModule(), includeLibraries);
- resources.add(r);
- }
-
- resources.add(main);
-
- return new DelegatingProjectResources(resources);
- }
-
- @Nullable
- @Override
- public Pair<ResourceType, String> resolveResourceId(int id) {
- for (int i = myDelegates.size() - 1; i >= 0; i--) {
- ProjectResources resources = myDelegates.get(i);
- resources.sync();
- Pair<ResourceType, String> resolved = resources.resolveResourceId(id);
- if (resolved != null) {
- return resolved;
- }
- }
- return null;
- }
-
- @Nullable
- @Override
- public String resolveStyleable(int[] id) {
- for (int i = myDelegates.size() - 1; i >= 0; i--) {
- ProjectResources resources = myDelegates.get(i);
- resources.sync();
- String resolved = resources.resolveStyleable(id);
- if (resolved != null) {
- return resolved;
- }
- }
- return null;
- }
-
- @Nullable
- @Override
- public Integer getResourceId(ResourceType type, String name) {
- for (int i = myDelegates.size() - 1; i >= 0; i--) {
- ProjectResources resources = myDelegates.get(i);
- resources.sync();
- Integer resolved = resources.getResourceId(type, name);
- if (resolved != null) {
- return resolved;
- }
- }
- return null;
- }
-
- @Override
- public void setCompiledResources(TIntObjectHashMap<Pair<ResourceType, String>> id2res,
- Map<IntArrayWrapper, String> styleableId2name,
- Map<ResourceType, TObjectIntHashMap<String>> res2id) {
- for (int i = myDelegates.size() - 1; i >= 0; i--) {
- ProjectResources resources = myDelegates.get(i);
- resources.setCompiledResources(id2res, styleableId2name, res2id);
- }
- }
-
- @Override
- public void sync() {
- for (int i = myDelegates.size() - 1; i >= 0; i--) {
- ProjectResources resources = myDelegates.get(i);
- resources.sync();
- }
- }
-
- @SuppressWarnings("ForLoopReplaceableByForEach")
- @NonNull
- @Override
- public Map<ResourceType, Map<String, ResourceValue>> getConfiguredResources(@NonNull FolderConfiguration referenceConfig) {
- Map<ResourceType, Map<String, ResourceValue>> typeMap = Maps.newEnumMap(ResourceType.class);
- for (ResourceType type : ResourceType.values()) {
- // create the map
- Map<String, ResourceValue> map = Maps.newHashMapWithExpectedSize(100);
- typeMap.put(type, map);
-
- for (int i = 0, n = myDelegates.size(); i < n; i++) {
- ProjectResources resources = myDelegates.get(i);
- resources.sync();
-
- // get the local results and put them in the map
- ListMultimap<String, ResourceItem> items = resources.getItems().get(type);
- if (items == null) {
- continue;
- }
-
- boolean framework = resources.isFramework();
- Set<String> keys = items.keySet();
- for (String key : keys) {
- List<ResourceItem> keyItems = items.get(key);
-
- // look for the best match for the given configuration
- // the match has to be of type ResourceFile since that's what the input list contains
- ResourceItem match = (ResourceItem) referenceConfig.findMatchingConfigurable(keyItems);
- if (match != null) {
- ResourceValue value = match.getResourceValue(framework);
- if (value != null) {
- map.put(match.getName(), value);
- }
- }
- }
- }
- }
-
- return typeMap;
- }
-
- @SuppressWarnings("ForLoopReplaceableByForEach")
- @NonNull
- @Override
- public Map<String, ResourceValue> getConfiguredResources(@NonNull ResourceType type, @NonNull FolderConfiguration referenceConfig) {
-
- // create the map
- Map<String, ResourceValue> map = Maps.newHashMapWithExpectedSize(100);
-
- for (int i = 0, n = myDelegates.size(); i < n; i++) {
- ProjectResources resources = myDelegates.get(i);
- resources.sync();
-
- // get the resource item for the given type
- ListMultimap<String, ResourceItem> items = resources.getItems().get(type);
- if (items == null) {
- continue;
- }
-
- boolean framework = resources.isFramework();
- Set<String> keys = items.keySet();
- for (String key : keys) {
- List<ResourceItem> keyItems = items.get(key);
-
- // look for the best match for the given configuration
- // the match has to be of type ResourceFile since that's what the input list contains
- ResourceItem match = (ResourceItem) referenceConfig.findMatchingConfigurable(keyItems);
- if (match != null) {
- ResourceValue value = match.getResourceValue(framework);
- if (value != null) {
- map.put(match.getName(), value);
- }
- }
- }
- }
-
- return map;
- }
-
- @Nullable
- @Override
- public ResourceValue getConfiguredValue(@NonNull ResourceType type,
- @NonNull String name,
- @NonNull FolderConfiguration referenceConfig) {
-
- for (int i = myDelegates.size() - 1; i >= 0; i--) {
- ProjectResources resources = myDelegates.get(i);
- resources.sync();
- ResourceValue value = resources.getConfiguredValue(type, name, referenceConfig);
- if (value != null) {
- return value;
- }
- }
-
- return null;
- }
-
- @Override
- public long getModificationCount() {
- // See if any of the delegates have changed
- boolean changed = false;
- for (int i = myDelegates.size() - 1; i >= 0; i--) {
- ProjectResources resources = myDelegates.get(i);
- long rev = resources.getModificationCount();
- if (rev != myModificationCounts[i]) {
- myModificationCounts[i] = rev;
- changed = true;
- }
- }
- if (changed) {
- myGeneration++;
- }
-
- return myGeneration;
- }
-
- @NonNull
- @Override
- public Collection<String> getItemsOfType(@NonNull ResourceType type) {
- Set<String> items = Sets.newHashSet();
- for (int i = myDelegates.size() - 1; i >= 0; i--) {
- ProjectResources resources = myDelegates.get(i);
- items.addAll(resources.getItemsOfType(type));
- }
-
- return items;
- }
-
- @NonNull
- @Override
- public Map<ResourceType, ListMultimap<String, ResourceItem>> getItems() {
- // TODO: I should *cache* this, and reuse it since it's needed a lot; that would make
- // synthesizing query answers much faster, as long as I can invalidate it!
- return combineItems();
- }
-
- private Map<ResourceType, ListMultimap<String, ResourceItem>> combineItems() {
- Map<ResourceType, ListMultimap<String, ResourceItem>> allItems = Maps.newEnumMap(ResourceType.class);
- for (ProjectResources resources : myDelegates) {
- Map<ResourceType, ListMultimap<String, ResourceItem>> items = resources.getItems();
- for (Map.Entry<ResourceType, ListMultimap<String, ResourceItem>> entry : items.entrySet()) {
- ResourceType type = entry.getKey();
- ListMultimap<String, ResourceItem> map = entry.getValue();
-
- // TODO: Do a proper/full merge here!
- if (map == null) {
- continue;
- }
-
- ListMultimap<String, ResourceItem> fullMap = allItems.get(type);
- if (fullMap == null) {
- fullMap = ArrayListMultimap.create();
- allItems.put(type, fullMap);
- }
-
- for (Map.Entry<String, ResourceItem> mapEntry : map.entries()) {
- // TODO: Implement overlay/shadowing here!!!
- String key = mapEntry.getKey();
- ResourceItem value = mapEntry.getValue();
- fullMap.put(key, value);
- }
- }
- }
-
- return allItems;
- }
-
- @Nullable
- @Override
- public List<ResourceItem> getResourceItem(@NonNull ResourceType resourceType, @NonNull String resourceName) {
- for (int i = myDelegates.size() - 1; i >= 0; i--) {
- ProjectResources resources = myDelegates.get(i);
- List<ResourceItem> item = resources.getResourceItem(resourceType, resourceName);
- if (item != null) {
- return item;
- }
- }
-
- return null;
- }
-
- @Override
- public boolean hasResourceItem(@NonNull String url) {
- for (int i = myDelegates.size() - 1; i >= 0; i--) {
- ProjectResources resources = myDelegates.get(i);
- if (resources.hasResourceItem(url)) {
- return true;
- }
- }
-
- return false;
- }
-
- @Override
- public boolean hasResourceItem(@NonNull ResourceType resourceType, @NonNull String resourceName) {
- for (int i = myDelegates.size() - 1; i >= 0; i--) {
- ProjectResources resources = myDelegates.get(i);
- if (resources.hasResourceItem(resourceType, resourceName)) {
- return true;
- }
- }
-
- return false;
- }
-
- @Override
- public boolean hasResourcesOfType(@NonNull ResourceType resourceType) {
- for (int i = myDelegates.size() - 1; i >= 0; i--) {
- ProjectResources resources = myDelegates.get(i);
- if (resources.hasResourcesOfType(resourceType)) {
- return true;
- }
- }
-
- return false;
- }
-
- @NonNull
- @Override
- public List<ResourceType> getAvailableResourceTypes() {
- Set<ResourceType> types = EnumSet.noneOf(ResourceType.class);
- for (int i = myDelegates.size() - 1; i >= 0; i--) {
- ProjectResources resources = myDelegates.get(i);
- types.addAll(resources.getAvailableResourceTypes());
- }
-
- return Lists.newArrayList(types);
- }
-
- @Nullable
- @Override
- public ResourceFile getMatchingFile(@NonNull String name, @NonNull ResourceType type, @NonNull FolderConfiguration config) {
- for (int i = myDelegates.size() - 1; i >= 0; i--) {
- ProjectResources resources = myDelegates.get(i);
- ResourceFile matchingFile = resources.getMatchingFile(name, type, config);
- if (matchingFile != null) {
- return matchingFile;
- }
- }
-
- return null;
- }
-
- @NonNull
- @Override
- public SortedSet<String> getLanguages() {
- SortedSet<String> languages = myDelegates.get(0).getLanguages();
- for (int i = myDelegates.size() - 1; i >= 1; i--) { // deliberately skipping i == 0
- ProjectResources resources = myDelegates.get(i);
- languages.addAll(resources.getLanguages());
- }
-
- return languages;
- }
-
- @NonNull
- @Override
- public SortedSet<String> getRegions(@NonNull String currentLanguage) {
- SortedSet<String> regions = myDelegates.get(0).getRegions(currentLanguage);
- for (int i = myDelegates.size() - 1; i >= 1; i--) { // deliberately skipping i == 0
- ProjectResources resources = myDelegates.get(i);
- regions.addAll(resources.getRegions(currentLanguage));
- }
-
- return regions;
- }
-
- @Override
- public void refresh() {
- // TODO: If in the future we cache information, such as mItems, clear them here
- myGeneration++;
- }
-
- @NonNull
- @Override
- public MergeConsumer<ResourceItem> getMergeConsumer() {
- throw new IllegalStateException(); // Merging is only done on individual project resources
- }
-
- @Nullable
- @Override
- protected Collection<String> getMergeIds() {
- // Should be called on individual delegated project resources
- return null;
- }
-
- @Override
- public void mergeIds() {
- for (int i = myDelegates.size() - 1; i >= 0; i--) {
- ProjectResources resources = myDelegates.get(i);
- resources.mergeIds();
- }
- }
-
- @Override
- public void dispose() {
- for (int i = myDelegates.size() - 1; i >= 0; i--) {
- ProjectResources resources = myDelegates.get(i);
- resources.dispose();
- }
- }
-}
diff --git a/android/src/com/android/tools/idea/rendering/FileProjectResourceRepository.java b/android/src/com/android/tools/idea/rendering/FileProjectResourceRepository.java
index f99bbf8..f6fb3af 100644
--- a/android/src/com/android/tools/idea/rendering/FileProjectResourceRepository.java
+++ b/android/src/com/android/tools/idea/rendering/FileProjectResourceRepository.java
@@ -16,154 +16,84 @@
package com.android.tools.idea.rendering;
import com.android.annotations.NonNull;
-import com.android.builder.model.SourceProvider;
-import com.android.ide.common.res2.DuplicateDataException;
-import com.android.ide.common.res2.FileStatus;
-import com.android.ide.common.res2.ResourceMerger;
-import com.android.ide.common.res2.ResourceSet;
-import com.android.ide.common.resources.IntArrayWrapper;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.res2.*;
import com.android.resources.ResourceType;
-import com.android.tools.idea.gradle.IdeaAndroidProject;
-import com.android.tools.idea.gradle.variant.view.BuildVariantView;
-import com.android.util.Pair;
import com.android.utils.ILogger;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Maps;
import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.fileTypes.FileType;
-import com.intellij.openapi.fileTypes.FileTypeManager;
-import com.intellij.openapi.fileTypes.StdFileTypes;
-import com.intellij.openapi.module.Module;
-import com.intellij.openapi.vfs.VfsUtilCore;
-import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.psi.*;
-import gnu.trove.TIntObjectHashMap;
-import gnu.trove.TObjectIntHashMap;
-import org.jetbrains.android.facet.AndroidFacet;
-import org.jetbrains.android.resourceManagers.LocalResourceManager;
+import com.intellij.util.containers.SoftValueHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
-import java.util.List;
import java.util.Map;
-import java.util.Set;
-import java.util.SortedSet;
-import static com.android.SdkConstants.*;
-import static com.android.tools.idea.gradle.variant.view.BuildVariantView.BuildVariantSelectionChangeListener;
-
-@SuppressWarnings("deprecation") // Deprecated com.android.util.Pair is required by ProjectCallback interface
+/**
+ * A {@link AbstractResourceRepository} for plain java.io Files; this is needed for repositories
+ * in output folders such as build, where Studio will not create PsiDirectories, and
+ * as a result cannot use the normal {@link ResourceFolderRepository}. This is the case
+ * for example for the expanded {@code .aar} directories.
+ */
class FileProjectResourceRepository extends ProjectResources {
private static final Logger LOG = Logger.getInstance(FileProjectResourceRepository.class);
- protected final Module myModule;
+ protected final Map<ResourceType, ListMultimap<String, ResourceItem>> mItems = Maps.newEnumMap(ResourceType.class);
+ private final File myFile;
- // Project resource ints are defined as 0x7FXX#### where XX is the resource type (layout, drawable,
- // etc...). Using FF as the type allows for 255 resource types before we get a collision
- // which should be fine.
- private static final int DYNAMIC_ID_SEED_START = 0x7fff0000;
- private ResourceMerger myResourceMerger;
+ private final static SoftValueHashMap<File, FileProjectResourceRepository> ourCache =
+ new SoftValueHashMap<File, FileProjectResourceRepository>();
- private final IntArrayWrapper myWrapper = new IntArrayWrapper(null);
- /** Map of (name, id) for resources of type {@link ResourceType#ID} coming from R.java */
- private Map<ResourceType, TObjectIntHashMap<String>> myResourceValueMap;
- /** Map of (id, [name, resType]) for all resources coming from R.java */
- private TIntObjectHashMap<Pair<ResourceType, String>> myResIdValueToNameMap;
- /** Map of (int[], name) for styleable resources coming from R.java */
- private Map<IntArrayWrapper, String> myStyleableValueToNameMap;
-
- private final TObjectIntHashMap<String> myName2DynamicIdMap = new TObjectIntHashMap<String>();
- private final TIntObjectHashMap<Pair<ResourceType, String>> myDynamicId2ResourceMap =
- new TIntObjectHashMap<Pair<ResourceType, String>>();
- private int myDynamicSeed = DYNAMIC_ID_SEED_START;
-
- private final PsiListener myListener;
-
- private FileProjectResourceRepository(@NotNull Module module, @NotNull ResourceMerger resourceMerger) {
- myModule = module;
- myResourceMerger = resourceMerger;
-
- myListener = new PsiListener();
-
- // TODO: There should only be one of these per project, not per module!
- PsiManager.getInstance(module.getProject()).addPsiTreeChangeListener(myListener);
- // TODO: Register with Disposer.register
+ private FileProjectResourceRepository(@NotNull File file) {
+ super(file.getName());
+ myFile = file;
}
@NotNull
- static FileProjectResourceRepository create(@NotNull final AndroidFacet facet) {
- boolean refresh = facet.isGradleProject() && facet.getIdeaAndroidProject() == null;
- ResourceMerger resourceMerger = createResourceMerger(facet);
- final FileProjectResourceRepository repository = new FileProjectResourceRepository(facet.getModule(), resourceMerger);
- try {
- resourceMerger.mergeData(repository.getMergeConsumer(), true /*doCleanUp*/);
- }
- catch (Exception e) {
- LOG.error("Failed to initialize resources", e);
- }
-
- // If the model is not yet ready, we may get an incomplete set of resource
- // directories, so in that case update the repository when the model is available.
- // TODO: Only refresh if the set of resource directories has actually changed!
- if (refresh) {
- facet.addListener(new AndroidFacet.GradleProjectAvailableListener() {
- @Override
- public void gradleProjectAvailable(@NotNull IdeaAndroidProject project) {
- facet.removeListener(this);
- repository.refresh();
- }
- });
- }
-
- // Also refresh the project resources whenever the variant changes
- if (facet.isGradleProject()) {
- BuildVariantView.getInstance(facet.getModule().getProject()).addListener(new BuildVariantSelectionChangeListener() {
- @Override
- public void buildVariantSelected(@NotNull AndroidFacet facet) {
- repository.refresh();
- }
- });
+ static FileProjectResourceRepository get(@NotNull final File file) {
+ FileProjectResourceRepository repository = ourCache.get(file);
+ if (repository == null) {
+ repository = create(file);
+ ourCache.put(file, repository);
}
return repository;
}
- private static ResourceMerger createResourceMerger(AndroidFacet facet) {
- ResourceMerger resourceMerger = new ResourceMerger();
- addAllSources(resourceMerger, facet);
- return resourceMerger;
+ @Nullable
+ @VisibleForTesting
+ static FileProjectResourceRepository getCached(@NotNull final File file) {
+ return ourCache.get(file);
}
- private static void addAllSources(ResourceMerger resourceMerger, AndroidFacet facet) {
+ @NotNull
+ private static FileProjectResourceRepository create(@NotNull final File file) {
+ final FileProjectResourceRepository repository = new FileProjectResourceRepository(file);
+ try {
+ ResourceMerger resourceMerger = createResourceMerger(file);
+ resourceMerger.mergeData(repository.createMergeConsumer(), true /*doCleanUp*/);
+ }
+ catch (Exception e) {
+ LOG.error("Failed to initialize resources", e);
+ }
+
+ return repository;
+ }
+
+ private static ResourceMerger createResourceMerger(File file) {
ILogger logger = new LogWrapper(LOG);
- addSources(resourceMerger, facet.getMainSourceSet(), "main", logger);
- List<com.intellij.openapi.util.Pair<String,SourceProvider>> flavors = facet.getFlavorSourceSetsAndNames();
- if (flavors != null) {
- for (com.intellij.openapi.util.Pair<String,SourceProvider> pair : flavors) {
- String flavorName = pair.getFirst();
- SourceProvider provider = pair.getSecond();
- addSources(resourceMerger, provider, flavorName, logger);
- }
- }
- String buildTypeName = facet.getBuildTypeName();
- if (buildTypeName != null) {
- SourceProvider provider = facet.getBuildTypeSourceSet();
- if (provider != null) {
- addSources(resourceMerger, provider, buildTypeName, logger);
- }
- }
- }
+ ResourceMerger merger = new ResourceMerger();
- private static void addSources(ResourceMerger merger, SourceProvider provider, String name, ILogger logger) {
- ResourceSet resourceSet = new ResourceSet(name) {
+ ResourceSet resourceSet = new ResourceSet(file.getName()) {
@Override
protected void checkItems() throws DuplicateDataException {
// No checking in ProjectResources; duplicates can happen, but
// the project resources shouldn't abort initialization
}
};
- resourceSet.addSources(provider.getResDirectories());
+ resourceSet.addSource(file);
try {
resourceSet.loadFromFiles(logger);
}
@@ -175,440 +105,29 @@
LOG.error("Failed to initialize resources", e);
}
merger.addDataSet(resourceSet);
+ return merger;
}
@Override
- public void dispose() {
- PsiManager.getInstance(myModule.getProject()).addPsiTreeChangeListener(myListener);
- }
-
- @Override
- @Nullable
- public Pair<ResourceType, String> resolveResourceId(int id) {
- Pair<ResourceType, String> result = null;
- if (myResIdValueToNameMap != null) {
- result = myResIdValueToNameMap.get(id);
- }
-
- if (result == null) {
- final Pair<ResourceType, String> pair = myDynamicId2ResourceMap.get(id);
- if (pair != null) {
- result = pair;
- }
- }
-
- return result;
- }
-
- @Override
- @Nullable
- public String resolveStyleable(int[] id) {
- if (myStyleableValueToNameMap != null) {
- myWrapper.set(id);
- // A normal map lookup on int[] would only consider object identity, but the IntArrayWrapper
- // will check all the individual elements for equality. We reuse an instance for all the lookups
- // since we don't need a new one each time.
- return myStyleableValueToNameMap.get(myWrapper);
- }
-
- return null;
- }
-
- @Override
- @Nullable
- public Integer getResourceId(ResourceType type, String name) {
- final TObjectIntHashMap<String> map = myResourceValueMap != null ? myResourceValueMap.get(type) : null;
-
- if (map == null || !map.containsKey(name)) {
- return getDynamicId(type, name);
- }
- return map.get(name);
- }
-
- @Override
- public void refresh() {
- AndroidFacet facet = AndroidFacet.getInstance(myModule);
- assert facet != null;
- ResourceMerger resourceMerger = createResourceMerger(facet);
- clear();
- try {
- resourceMerger.mergeData(getMergeConsumer(), true /*doCleanUp*/);
- mergeIds();
- }
- catch (Exception e) {
- LOG.error("Failed to initialize resources", e);
- }
-
- myResourceMerger = resourceMerger;
- myGeneration++;
- }
-
- @Override
- public void sync() {
- if (!myHaveDirtyFiles) {
- return;
- }
-
- // Longer term I want to *directly* merge from PSI elements; no XML parsing etc.
- List<File> addedFiles = null;
- if (!myAddedFiles.isEmpty()) {
- addedFiles = Lists.newArrayListWithExpectedSize(myAddedFiles.size());
- for (VirtualFile file : myAddedFiles) {
- myChangedFiles.remove(file);
- addedFiles.add(VfsUtilCore.virtualToIoFile(file));
- }
- }
- List<File> changedFiles = null;
- if (!myChangedFiles.isEmpty()) {
- changedFiles = Lists.newArrayListWithExpectedSize(myChangedFiles.size());
- for (VirtualFile file : myChangedFiles) {
- changedFiles.add(VfsUtilCore.virtualToIoFile(file));
- }
- }
-
- ILogger logger = new LogWrapper(LOG);
- boolean newGeneration = false;
-
- for (ResourceSet set : myResourceMerger.getDataSets()) {
- for (File root : set.getSourceFiles()) {
- String rootPath = root.getPath();
-
- if (addedFiles != null) {
- assert !addedFiles.isEmpty();
- newGeneration = true;
- for (File file : addedFiles) {
- if (file.getPath().startsWith(rootPath)) {
- try {
- set.updateWith(root, file, FileStatus.NEW, logger);
- }
- catch (IOException e) {
- LOG.error("Can't update new file " + file, e);
- }
- }
- }
- }
-
- if (changedFiles != null) {
- for (File file : changedFiles) {
- if (file.getPath().startsWith(rootPath)) {
- try {
- set.updateWith(root, file, FileStatus.CHANGED, logger);
- if (!newGeneration) {
- String parentName = file.getParentFile().getName();
- if (parentName.startsWith(FD_RES_VALUES)) {
- newGeneration = true;
- }
- }
- }
- catch (IOException e) {
- LOG.error("Can't update changed file " + file, e);
- }
- }
- }
- }
-
- if (!myDeletedFiles.isEmpty()) {
- newGeneration = true;
- for (File file : myDeletedFiles) {
- if (file.getPath().startsWith(rootPath)) {
- try {
- set.updateWith(root, file, FileStatus.REMOVED, logger);
- }
- catch (IOException e) {
- LOG.error("Can't update deleted file " + file, e);
- }
- }
- }
- }
- }
- }
-
- try {
- myResourceMerger.mergeData(getMergeConsumer(), true /*doCleanUp*/);
- mergeIds();
- }
- catch (Exception e) {
- LOG.error("Failed to initialize resources", e);
- }
-
- myHaveDirtyFiles = false;
- myAddedFiles.clear();
- myChangedFiles.clear();
- myDeletedFiles.clear();
-
- if (newGeneration) {
- myGeneration++;
- }
- }
-
- private int getDynamicId(ResourceType type, String name) {
- synchronized (myName2DynamicIdMap) {
- if (myName2DynamicIdMap.containsKey(name)) {
- return myName2DynamicIdMap.get(name);
- }
- final int value = ++myDynamicSeed;
- myName2DynamicIdMap.put(name, value);
- myDynamicId2ResourceMap.put(value, Pair.of(type, name));
- return value;
- }
- }
-
- @Override
- public void setCompiledResources(TIntObjectHashMap<Pair<ResourceType, String>> id2res,
- Map<IntArrayWrapper, String> styleableId2name,
- Map<ResourceType, TObjectIntHashMap<String>> res2id) {
- // Regularly clear dynamic seed such that we don't run out of numbers (we only have 255)
- myDynamicSeed = DYNAMIC_ID_SEED_START;
- myName2DynamicIdMap.clear();
- myDynamicId2ResourceMap.clear();
-
- myResourceValueMap = res2id;
- myResIdValueToNameMap = id2res;
- myStyleableValueToNameMap = styleableId2name;
- sync();
- mergeIds();
- }
-
-
@NonNull
+ protected Map<ResourceType, ListMultimap<String, ResourceItem>> getMap() {
+ return mItems;
+ }
+
@Override
- public SortedSet<String> getRegions(@NonNull String currentLanguage) {
- sync();
- return super.getRegions(currentLanguage);
+ @Nullable
+ protected ListMultimap<String, ResourceItem> getMap(ResourceType type, boolean create) {
+ ListMultimap<String, ResourceItem> multimap = mItems.get(type);
+ if (multimap == null && create) {
+ multimap = ArrayListMultimap.create();
+ mItems.put(type, multimap);
+ }
+ return multimap;
}
- // Code related to updating the resources
-
- private boolean myHaveDirtyFiles;
- private Set<VirtualFile> myAddedFiles = Sets.newHashSet();
- private Set<VirtualFile> myChangedFiles = Sets.newHashSet();
-
- // For deleted files we need to store the path, since the file
- // no longer exist (if you for example rename a file, the VirtualFile
- // will stay the same and its name change to the new name instead,
- // so if we store the "old" file in the deleted list, we'll really look
- // up the new path when processing it.
- private Set<File> myDeletedFiles = Sets.newHashSet();
-
- private boolean isResourceFolder(@Nullable PsiElement parent) {
- // Returns true if the given element represents a resource folder (e.g. res/values-en-rUS or layout-land, *not* the root res/ folder)
- if (parent instanceof PsiDirectory) {
- PsiDirectory directory = (PsiDirectory)parent;
- PsiDirectory parentDirectory = directory.getParentDirectory();
- if (parentDirectory != null) {
- VirtualFile dir = parentDirectory.getVirtualFile();
- AndroidFacet facet = AndroidFacet.getInstance(myModule);
- if (facet != null) {
- return facet.getLocalResourceManager().isResourceDir(dir);
- }
- }
- }
- return false;
- }
-
- private static boolean isRelevantFileType(@NotNull FileType fileType) {
- return fileType == StdFileTypes.XML ||
- (fileType.isBinary() && fileType == FileTypeManager.getInstance().getFileTypeByExtension(EXT_PNG));
- }
-
- private static boolean isRelevantFile(@NotNull VirtualFile file) {
- return isRelevantFileType(file.getFileType());
- }
-
- private static boolean isRelevantFile(@NotNull PsiFile file) {
- return isRelevantFileType(file.getFileType());
- }
-
- private final class PsiListener implements PsiTreeChangeListener {
- private boolean myIgnoreChildrenChanged;
-
- @Override
- public void childAdded(@NotNull PsiTreeChangeEvent event) {
- PsiFile psiFile = event.getFile();
- if (psiFile == null) {
- PsiElement child = event.getChild();
- if (child instanceof PsiFile) {
- VirtualFile file = ((PsiFile)child).getVirtualFile();
- if (file != null && isRelevantFile(file) && isResourceFolder(event.getParent())) {
- myAddedFiles.add(file);
- myHaveDirtyFiles = true;
- }
- } else if (child instanceof PsiDirectory) {
- PsiDirectory directory = (PsiDirectory)child;
- if (isResourceFolder(directory)) {
- for (PsiFile file : directory.getFiles()) {
- VirtualFile virtualFile = file.getVirtualFile();
- if (virtualFile != null) {
- myAddedFiles.add(virtualFile);
- myHaveDirtyFiles = true;
- }
- }
- }
- }
- } else if (isRelevantFile(psiFile)) {
- VirtualFile virtualFile = psiFile.getVirtualFile();
- if (virtualFile != null) {
- myChangedFiles.add(virtualFile);
- myHaveDirtyFiles = true;
- }
- }
-
- myIgnoreChildrenChanged = true;
- }
-
- @Override
- public void childRemoved(@NotNull PsiTreeChangeEvent event) {
- PsiFile psiFile = event.getFile();
- if (psiFile == null) {
- PsiElement child = event.getChild();
- if (child instanceof PsiFile) {
- VirtualFile file = ((PsiFile)child).getVirtualFile();
- if (file != null && isRelevantFile(file) && isResourceFolder(event.getParent())) {
- myDeletedFiles.add(VfsUtilCore.virtualToIoFile(file));
- myHaveDirtyFiles = true;
- }
- }// else if (child instanceof PsiDirectory) {
- // TODO: We can't iterate the children here and record them in myDeletedFiles because dir is empty
- // (even if we do this in #beforeChildRemoval.
- // Fix this during the full PSI rewrite.
- //}
- } else if (isRelevantFile(psiFile)) {
- VirtualFile file = psiFile.getVirtualFile();
- if (file != null) {
- myChangedFiles.add(file);
- myHaveDirtyFiles = true;
- }
- }
-
- myIgnoreChildrenChanged = true;
- }
-
- @Override
- public void childReplaced(@NotNull PsiTreeChangeEvent event) {
- // TODO: If getOldChild() is a PsiWhiteSpace and getNewChild() is PsiWhiteSpace, do nothing
- PsiFile psiFile = event.getFile();
- if (psiFile != null) {
- if (isRelevantFile(psiFile)) {
- VirtualFile file = psiFile.getVirtualFile();
- if (file != null) {
- myChangedFiles.add(file);
- myHaveDirtyFiles = true;
- }
- }
- } else {
- // TODO: Check how renaming is affected by this. Do I get children change notifications for
- // children of a PsiDirectory?
- Throwable throwable = new Throwable();
- throwable.fillInStackTrace();
- LOG.debug("Got childReplaced event for inter-file operations; TODO: investigate", throwable);
- }
-
- myIgnoreChildrenChanged = true;
- }
-
- @Override
- public void childMoved(@NotNull PsiTreeChangeEvent event) {
- PsiElement child = event.getChild();
- PsiFile psiFile = event.getFile();
- if (psiFile == null) {
- if (child instanceof PsiFile && isRelevantFile((PsiFile)child)) {
- if (isResourceFolder(event.getNewParent())) {
- VirtualFile file = ((PsiFile)child).getVirtualFile();
- if (file != null) {
- myAddedFiles.add(file);
- myHaveDirtyFiles = true;
- }
- }
-
- PsiElement oldParent = event.getOldParent();
- if (oldParent instanceof PsiDirectory) {
- PsiDirectory directory = (PsiDirectory)oldParent;
- VirtualFile dir = directory.getVirtualFile();
- myDeletedFiles.add(new File(VfsUtilCore.virtualToIoFile(dir), ((PsiFile)child).getName()));
- myHaveDirtyFiles = true;
- }
- }
- } else {
- // Change inside a file
- VirtualFile file = psiFile.getVirtualFile();
- if (file != null && isRelevantFile(file)) {
- myChangedFiles.add(file);
- myHaveDirtyFiles = true;
- }
- }
-
- myIgnoreChildrenChanged = true;
- }
-
- @Override
- public final void beforeChildrenChange(@NotNull PsiTreeChangeEvent event) {
- myIgnoreChildrenChanged = false;
- }
-
- @Override
- public void childrenChanged(@NotNull PsiTreeChangeEvent event) {
- if (myIgnoreChildrenChanged) {
- // We've already processed this change as one or more individual childMoved, childAdded, childRemoved etc calls
- return;
- }
-
- PsiFile psiFile = event.getFile();
- if (psiFile != null && isRelevantFile(psiFile)) {
- VirtualFile file = psiFile.getVirtualFile();
- if (file != null) {
- myChangedFiles.add(file);
- myHaveDirtyFiles = true;
- }
- } else {
- Throwable throwable = new Throwable();
- throwable.fillInStackTrace();
- LOG.debug("Got childrenChanged event for inter-file operations; TODO: investigate", throwable);
- }
- }
-
- @Override
- public final void beforePropertyChange(@NotNull PsiTreeChangeEvent event) {
- if (PsiTreeChangeEvent.PROP_FILE_NAME == event.getPropertyName()) {
- PsiElement child = event.getChild();
- if (child instanceof PsiFile) {
- PsiFile psiFile = (PsiFile)child;
- if (isRelevantFile(psiFile) && isResourceFolder(event.getParent())) {
- VirtualFile file = psiFile.getVirtualFile();
- if (file != null) {
- myDeletedFiles.add(VfsUtilCore.virtualToIoFile(file));
- myHaveDirtyFiles = true;
- }
- }
- }
- // The new name will be added in the post hook (propertyChanged rather than beforePropertyChange)
- }
- }
-
- @Override
- public void propertyChanged(@NotNull PsiTreeChangeEvent event) {
- if (PsiTreeChangeEvent.PROP_FILE_NAME == event.getPropertyName() && isResourceFolder(event.getParent())) {
- PsiElement child = event.getElement();
- if (child instanceof PsiFile) {
- PsiFile psiFile = (PsiFile)child;
- if (isRelevantFile(psiFile) && isResourceFolder(event.getParent())) {
- VirtualFile file = psiFile.getVirtualFile();
- if (file != null) {
- myAddedFiles.add(file);
- myHaveDirtyFiles = true;
- }
- }
- }
- }
-
- // TODO: Do we need to handle PROP_DIRECTORY_NAME for users renaming any of the resource folders?
- // and what about PROP_FILE_TYPES -- can users change the type of an XML File to something else?
- }
-
- // Before-hooks: We don't care about these.
-
- @Override public final void beforeChildAddition(@NotNull PsiTreeChangeEvent event) { }
- @Override public final void beforeChildRemoval(@NotNull PsiTreeChangeEvent event) { }
- @Override public final void beforeChildReplacement(@NotNull PsiTreeChangeEvent event) { }
- @Override public final void beforeChildMovement(@NotNull PsiTreeChangeEvent event) { }
+ // For debugging only
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + " for " + myFile + ": @" + Integer.toHexString(System.identityHashCode(this));
}
}
diff --git a/android/src/com/android/tools/idea/rendering/GutterIconCache.java b/android/src/com/android/tools/idea/rendering/GutterIconCache.java
index aa4ddce..664b444 100644
--- a/android/src/com/android/tools/idea/rendering/GutterIconCache.java
+++ b/android/src/com/android/tools/idea/rendering/GutterIconCache.java
@@ -32,7 +32,7 @@
public class GutterIconCache {
private static final Logger LOG = Logger.getInstance("#" + GutterIconCache.class.getName());
- private static final int MAX_WIDTH = 12;
+ private static final int MAX_WIDTH = 16;
private static final int MAX_HEIGHT = 16;
private static final Icon NONE = AndroidIcons.Android; // placeholder
diff --git a/android/src/com/android/tools/idea/rendering/HtmlBuilder.java b/android/src/com/android/tools/idea/rendering/HtmlBuilder.java
deleted file mode 100644
index 6fb8eaa..0000000
--- a/android/src/com/android/tools/idea/rendering/HtmlBuilder.java
+++ /dev/null
@@ -1,329 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tools.idea.rendering;
-
-import com.android.sdklib.util.SparseArray;
-import com.android.utils.XmlUtils;
-import com.intellij.icons.AllIcons;
-import com.intellij.util.ui.UIUtil;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.net.URL;
-
-public class HtmlBuilder {
- @NotNull private final StringBuilder myStringBuilder;
- private SparseArray<Runnable> myLinkRunnables;
- private int myNextLinkId = 0;
- private String myTableDataExtra;
-
- public HtmlBuilder(@NotNull StringBuilder stringBuilder) {
- myStringBuilder = stringBuilder;
- }
-
- public HtmlBuilder() {
- myStringBuilder = new StringBuilder(100);
- }
-
- public HtmlBuilder addHtml(@NotNull String html) {
- myStringBuilder.append(html);
-
- return this;
- }
-
- public HtmlBuilder addNbsp() {
- myStringBuilder.append(" ");
-
- return this;
- }
-
- public HtmlBuilder addNbsps(int count) {
- for (int i = 0; i < count; i++) {
- addNbsp();
- }
-
- return this;
- }
-
- public HtmlBuilder newline() {
- myStringBuilder.append("<BR/>\n");
-
- return this;
- }
-
- public HtmlBuilder addLink(@Nullable String textBefore,
- @NotNull String linkText,
- @Nullable String textAfter,
- @NotNull String url) {
- if (textBefore != null) {
- add(textBefore);
- }
-
- addLink(linkText, url);
-
- if (textAfter != null) {
- add(textAfter);
- }
-
- return this;
- }
-
- public HtmlBuilder addLink(@NotNull String text, @NotNull String url) {
- int begin = 0;
- int length = text.length();
- for (; begin < length; begin++) {
- char c = text.charAt(begin);
- if (Character.isWhitespace(c)) {
- myStringBuilder.append(c);
- } else {
- break;
- }
- }
- myStringBuilder.append("<A HREF=\"");
- myStringBuilder.append(url);
- myStringBuilder.append("\">");
-
- XmlUtils.appendXmlTextValue(myStringBuilder, text.trim());
- myStringBuilder.append("</A>");
-
- int end = length - 1;
- for (; end > begin; end--) {
- char c = text.charAt(begin);
- if (Character.isWhitespace(c)) {
- myStringBuilder.append(c);
- }
- }
-
- return this;
- }
-
- public HtmlBuilder add(@NotNull String text) {
- XmlUtils.appendXmlTextValue(myStringBuilder, text);
-
- return this;
- }
-
- @NotNull
- public String getHtml() {
- return myStringBuilder.toString();
- }
-
- public HtmlBuilder beginBold() {
- myStringBuilder.append("<B>");
-
- return this;
- }
-
- public HtmlBuilder endBold() {
- myStringBuilder.append("</B>");
-
- return this;
- }
-
- public HtmlBuilder addBold(String text) {
- beginBold();
- add(text);
- endBold();
-
- return this;
- }
-
- public HtmlBuilder beginDiv() {
- return beginDiv(null);
- }
-
- public HtmlBuilder beginDiv(@Nullable String cssStyle) {
- myStringBuilder.append("<div");
- if (cssStyle != null) {
- myStringBuilder.append(" style=\"");
- myStringBuilder.append(cssStyle);
- myStringBuilder.append("\"");
- }
- myStringBuilder.append('>');
- return this;
- }
-
- public HtmlBuilder endDiv() {
- myStringBuilder.append("</div>");
- return this;
- }
-
- public HtmlBuilder addHeading(String text) {
- // See om.intellij.codeInspection.HtmlComposer.addHeading
- // (which operates on StringBuffers)
- myStringBuilder.append("<font style=\"font-weight:bold; color:")
- .append(UIUtil.isUnderDarcula() ? "#A5C25C" : "#005555").append(";\">");
- add(text);
- myStringBuilder.append("</font>");
-
- return this;
- }
-
- /**
- * The JEditorPane HTML renderer creates really ugly bulleted lists; the
- * size is hardcoded to use a giant heavy bullet. So, use a definition
- * list instead.
- */
- private static final boolean USE_DD_LISTS = true;
-
- public HtmlBuilder beginList() {
- if (USE_DD_LISTS) {
- myStringBuilder.append("<DL>");
- } else {
- myStringBuilder.append("<UL>");
- }
-
- return this;
- }
-
- public HtmlBuilder endList() {
- if (USE_DD_LISTS) {
- myStringBuilder.append("\n</DL>");
- } else {
- myStringBuilder.append("\n</UL>");
- }
-
- return this;
- }
-
- public HtmlBuilder listItem() {
- if (USE_DD_LISTS) {
- myStringBuilder.append("\n<DD>");
- myStringBuilder.append("-&NBSP;");
- } else {
- myStringBuilder.append("\n<LI>");
- }
-
- return this;
- }
-
- public HtmlBuilder addImage(URL url, @Nullable String altText) {
- String link = "";
- try {
- link = url.toURI().toURL().toExternalForm();
- }
- catch (Throwable t) {
- // pass
- }
- myStringBuilder.append("<img src='");
- myStringBuilder.append(link);
-
- if (altText != null) {
- myStringBuilder.append("' alt=\"");
- myStringBuilder.append(altText);
- myStringBuilder.append("\"");
- }
- myStringBuilder.append(" />");
-
- return this;
- }
-
- private void addIcon(String relative) {
- try {
- // TODO: Find a way to do this more efficiently; not referencing assets but the corresponding
- // AllIcons constants, and loading them into HTML class loader contexts?
- URL resource = AllIcons.class.getClassLoader().getResource(relative);
- if (resource != null) {
- String src = resource.toURI().toURL().toExternalForm();
- myStringBuilder.append("<img src='");
- myStringBuilder.append(src);
- myStringBuilder.append("' width=16 height=16></img>");
- }
- } catch (Throwable t) {
- // pass
- }
- }
-
- public HtmlBuilder addTipIcon() {
- addIcon("/actions/createFromUsage.png");
-
- return this;
- }
-
- public HtmlBuilder addWarningIcon() {
- addIcon("/actions/warning.png");
-
- return this;
- }
-
- public HtmlBuilder addErrorIcon() {
- addIcon("/actions/error.png");
-
- return this;
- }
-
- public HtmlBuilder beginTable(@Nullable String tdExtra) {
- myStringBuilder.append("<table>");
- myTableDataExtra = tdExtra;
- return this;
- }
-
- public HtmlBuilder beginTable() {
- return beginTable(null);
- }
-
- public HtmlBuilder endTable() {
- myStringBuilder.append("</table>");
- return this;
- }
-
- public HtmlBuilder beginTableRow() {
- myStringBuilder.append("<tr>");
- return this;
- }
-
- public HtmlBuilder endTableRow() {
- myStringBuilder.append("</tr>");
- return this;
- }
-
- public HtmlBuilder addTableRow(boolean isHeader, String... columns) {
- if (columns == null || columns.length == 0) {
- return this;
- }
-
- String tag = "t" + (isHeader ? 'h' : 'd');
-
- beginTableRow();
- for (String c : columns) {
- myStringBuilder.append('<');
- myStringBuilder.append(tag);
- if (myTableDataExtra != null) {
- myStringBuilder.append(' ');
- myStringBuilder.append(myTableDataExtra);
- }
- myStringBuilder.append('>');
-
- myStringBuilder.append(c);
-
- myStringBuilder.append("</");
- myStringBuilder.append(tag);
- myStringBuilder.append('>');
- }
- endTableRow();
-
- return this;
- }
-
- public HtmlBuilder addTableRow(String... columns) {
- return addTableRow(false, columns);
- }
-
- @NotNull
- public StringBuilder getStringBuilder() {
- return myStringBuilder;
- }
-}
diff --git a/android/src/com/android/tools/idea/rendering/HtmlLinkManager.java b/android/src/com/android/tools/idea/rendering/HtmlLinkManager.java
index 47ed8d7..34d9111 100644
--- a/android/src/com/android/tools/idea/rendering/HtmlLinkManager.java
+++ b/android/src/com/android/tools/idea/rendering/HtmlLinkManager.java
@@ -16,9 +16,9 @@
package com.android.tools.idea.rendering;
import com.android.resources.ResourceType;
-import com.android.sdklib.util.SparseArray;
import com.android.tools.idea.configurations.RenderContext;
import com.android.tools.lint.detector.api.LintUtils;
+import com.android.utils.SparseArray;
import com.intellij.codeInsight.daemon.impl.quickfix.CreateClassKind;
import com.intellij.codeInsight.intention.impl.CreateClassDialog;
import com.intellij.compiler.actions.CompileDirtyAction;
@@ -73,6 +73,8 @@
private static final String URL_ASSIGN_FRAGMENT_URL = "assignFragmentUrl:";
private static final String URL_ASSIGN_LAYOUT_URL = "assignLayoutUrl:";
private static final String URL_EDIT_ATTRIBUTE = "editAttribute:";
+ private static final String URL_REPLACE_ATTRIBUTE_VALUE = "replaceAttributeValue:";
+ static final String URL_ACTION_CLOSE = "action:close";
private SparseArray<Runnable> myLinkRunnables;
private SparseArray<WriteCommandAction> myLinkCommands;
@@ -83,7 +85,7 @@
public void handleUrl(@NotNull String url, @Nullable Module module, @Nullable PsiFile file, @Nullable DataContext dataContext,
@Nullable RenderResult result) {
- if (url.startsWith("http:")) {
+ if (url.startsWith("http:") || url.startsWith("https:")) {
UrlOpener.launchBrowser(null, url);
} else if (url.startsWith(URL_REPLACE_TAGS)) {
assert module != null;
@@ -122,6 +124,9 @@
} else if (url.startsWith(URL_EDIT_ATTRIBUTE)) {
assert result != null;
handleEditAttribute(url, module, file);
+ } else if (url.startsWith(URL_REPLACE_ATTRIBUTE_VALUE)) {
+ assert result != null;
+ handleReplaceAttributeValue(url, module, file);
} else if (url.startsWith(URL_RUNNABLE)) {
Runnable linkRunnable = getLinkRunnable(url);
if (linkRunnable != null) {
@@ -601,7 +606,7 @@
return URL_EDIT_ATTRIBUTE + attribute + '/' + value;
}
- private void handleEditAttribute(@NotNull String url, @NotNull Module module, @NotNull final PsiFile file) {
+ private static void handleEditAttribute(@NotNull String url, @NotNull Module module, @NotNull final PsiFile file) {
assert url.startsWith(URL_EDIT_ATTRIBUTE);
int attributeStart = URL_EDIT_ATTRIBUTE.length();
int valueStart = url.indexOf('/');
@@ -612,8 +617,8 @@
@Override
@Nullable
public XmlAttribute compute() {
- Collection<XmlAttribute> xmlTags = PsiTreeUtil.findChildrenOfType(file, XmlAttribute.class);
- for (XmlAttribute attribute : xmlTags) {
+ Collection<XmlAttribute> attributes = PsiTreeUtil.findChildrenOfType(file, XmlAttribute.class);
+ for (XmlAttribute attribute : attributes) {
if (attributeName.equals(attribute.getLocalName()) && value.equals(attribute.getValue())) {
return attribute;
}
@@ -630,4 +635,47 @@
openEditor(module.getProject(), file, 0, -1);
}
}
+
+ public String createReplaceAttributeValueUrl(String attribute, String oldValue, String newValue) {
+ return URL_REPLACE_ATTRIBUTE_VALUE + attribute + '/' + oldValue + '/' + newValue;
+ }
+
+ private static void handleReplaceAttributeValue(@NotNull String url, @NotNull Module module, @NotNull final PsiFile file) {
+ assert url.startsWith(URL_REPLACE_ATTRIBUTE_VALUE);
+ int attributeStart = URL_REPLACE_ATTRIBUTE_VALUE.length();
+ int valueStart = url.indexOf('/');
+ int newValueStart = url.indexOf('/', valueStart + 1);
+ final String attributeName = url.substring(attributeStart, valueStart);
+ final String oldValue = url.substring(valueStart + 1, newValueStart);
+ final String newValue = url.substring(newValueStart + 1);
+
+ WriteCommandAction<Void> action = new WriteCommandAction<Void>(module.getProject(), "Set Attribute Value", file) {
+ @Override
+ protected void run(Result<Void> result) throws Throwable {
+ Collection<XmlAttribute> attributes = PsiTreeUtil.findChildrenOfType(file, XmlAttribute.class);
+ int oldValueLen = oldValue.length();
+ for (XmlAttribute attribute : attributes) {
+ if (attributeName.equals(attribute.getLocalName())) {
+ String attributeValue = attribute.getValue();
+ if (attributeValue == null) {
+ continue;
+ }
+ if (oldValue.equals(attributeValue)) {
+ attribute.setValue(newValue);
+ } else {
+ int index = attributeValue.indexOf(oldValue);
+ if (index != -1) {
+ if ((index == 0 || attributeValue.charAt(index - 1) == '|') &&
+ (index + oldValueLen == attributeValue.length() || attributeValue.charAt(index + oldValueLen) == '|')) {
+ attributeValue = attributeValue.substring(0, index) + newValue + attributeValue.substring(index + oldValueLen);
+ attribute.setValue(attributeValue);
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+ action.execute();
+ }
}
diff --git a/android/src/com/android/tools/idea/rendering/Locale.java b/android/src/com/android/tools/idea/rendering/Locale.java
index aa301c6..f891721 100644
--- a/android/src/com/android/tools/idea/rendering/Locale.java
+++ b/android/src/com/android/tools/idea/rendering/Locale.java
@@ -16,6 +16,7 @@
package com.android.tools.idea.rendering;
import com.android.ide.common.resources.LocaleManager;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.ide.common.resources.configuration.LanguageQualifier;
import com.android.ide.common.resources.configuration.RegionQualifier;
import com.google.common.base.Objects;
@@ -102,6 +103,25 @@
}
/**
+ * Constructs a new {@linkplain Locale} for the given folder configuration
+ *
+ * @param folder the folder configuration
+ * @return a locale with the given language and region
+ */
+ public static Locale create(FolderConfiguration folder) {
+ LanguageQualifier language = folder.getLanguageQualifier();
+ RegionQualifier region = folder.getRegionQualifier();
+ if (language == null && region == null) {
+ return ANY;
+ } else if (region == null) {
+ return create(language);
+ } else {
+ assert language != null;
+ return create(language, region);
+ }
+ }
+
+ /**
* Constructs a new {@linkplain Locale} for the given locale string, e.g. "zh" or "en-rUS".
*
* @param localeString the locale description
@@ -171,6 +191,15 @@
return region != ANY_REGION;
}
+ /**
+ * Returns the locale formatted as language-region. If region is not set,
+ * language is returned. If language is not set, empty string is returned.
+ */
+ public String toLocaleId() {
+ // Return lang-reg only if both lang and reg are present. Else return lang.
+ return hasLanguage() && hasRegion() ? language.getValue() + "-" + region.getValue() : hasLanguage() ? language.getValue() : "";
+ }
+
@Override
public int hashCode() {
final int prime = 31;
diff --git a/android/src/com/android/tools/idea/rendering/ManifestInfo.java b/android/src/com/android/tools/idea/rendering/ManifestInfo.java
index d46c1db..13aec01 100644
--- a/android/src/com/android/tools/idea/rendering/ManifestInfo.java
+++ b/android/src/com/android/tools/idea/rendering/ManifestInfo.java
@@ -31,11 +31,13 @@
import org.jetbrains.android.facet.AndroidRootUtil;
import org.jetbrains.annotations.NotNull;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.VALUE_TRUE;
import static com.android.xml.AndroidManifest.*;
/**
@@ -59,6 +61,7 @@
private int myTargetSdk;
private String myApplicationIcon;
private String myApplicationLabel;
+ private boolean myApplicationSupportsRtl;
/**
* Key for the per-project non-persistent property storing the {@link ManifestInfo} for
@@ -155,6 +158,7 @@
myPackage = ""; //$NON-NLS-1$
myApplicationIcon = null;
myApplicationLabel = null;
+ myApplicationSupportsRtl = false;
try {
XmlTag root = myManifestFile.getRootTag();
@@ -171,6 +175,7 @@
myApplicationIcon = application.getAttributeValue(ATTRIBUTE_ICON, ANDROID_URI);
myApplicationLabel = application.getAttributeValue(ATTRIBUTE_LABEL, ANDROID_URI);
myManifestTheme = application.getAttributeValue(ATTRIBUTE_THEME, ANDROID_URI);
+ myApplicationSupportsRtl = VALUE_TRUE.equals(application.getAttributeValue(ATTRIBUTE_SUPPORTS_RTL, ANDROID_URI));
XmlTag[] activities = application.findSubTags(NODE_ACTIVITY);
for (XmlTag activity : activities) {
@@ -251,7 +256,7 @@
public Map<String, String> getActivityThemes() {
sync();
if (myActivityThemes == null) {
- sync();
+ return Collections.emptyMap();
}
return myActivityThemes;
}
@@ -325,6 +330,16 @@
}
/**
+ * Returns true if the application has RTL support.
+ *
+ * @return true if the application has RTL support.
+ */
+ public boolean isRtlSupported() {
+ sync();
+ return myApplicationSupportsRtl;
+ }
+
+ /**
* Returns the target SDK version
*
* @return the target SDK version
diff --git a/android/src/com/android/tools/idea/rendering/ModuleResourceRepository.java b/android/src/com/android/tools/idea/rendering/ModuleResourceRepository.java
new file mode 100644
index 0000000..91fe80a
--- /dev/null
+++ b/android/src/com/android/tools/idea/rendering/ModuleResourceRepository.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.rendering;
+
+import com.android.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.android.facet.ResourceFolderManager;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+
+/**
+ * Resource repository for a single module (which can possibly have multiple resource folders)
+ */
+final class ModuleResourceRepository extends MultiResourceRepository {
+ private final AndroidFacet myFacet;
+
+ private ModuleResourceRepository(@NotNull AndroidFacet facet,
+ @NotNull List<ResourceFolderRepository> delegates) {
+ super(facet.getModule().getName(), delegates);
+ myFacet = facet;
+ }
+
+ /**
+ * Creates a new resource repository for the given module, <b>not</b> including its dependent modules.
+ *
+ * @param facet the facet for the module
+ * @return the resource repository
+ */
+ @NotNull
+ public static ProjectResources create(@NotNull final AndroidFacet facet) {
+ boolean gradleProject = facet.isGradleProject();
+ if (!gradleProject) {
+ // Always just a single resource folder: simple
+ VirtualFile primaryResourceDir = facet.getPrimaryResourceDir();
+ if (primaryResourceDir == null) {
+ return new EmptyRepository();
+ }
+ return ResourceFolderRegistry.get(facet, primaryResourceDir);
+ }
+
+ ResourceFolderManager folderManager = facet.getResourceFolderManager();
+ List<VirtualFile> resourceDirectories = folderManager.getFolders();
+ List<ResourceFolderRepository> resources = Lists.newArrayListWithExpectedSize(resourceDirectories.size());
+ for (VirtualFile resourceDirectory : resourceDirectories) {
+ ResourceFolderRepository repository = ResourceFolderRegistry.get(facet, resourceDirectory);
+ resources.add(repository);
+ }
+
+ // We create a ModuleResourceRepository even if resources.isEmpty(), because we may
+ // dynamically add children to it later (in updateRoots)
+ final ModuleResourceRepository repository = new ModuleResourceRepository(facet, resources);
+
+ // If the model is not yet ready, we may get an incomplete set of resource
+ // directories, so in that case update the repository when the model is available.
+
+ folderManager.addListener(new ResourceFolderManager.ResourceFolderListener() {
+ @Override
+ public void resourceFoldersChanged(@NotNull AndroidFacet facet, @NotNull List<VirtualFile> folders,
+ @NotNull Collection<VirtualFile> added, @NotNull Collection<VirtualFile> removed) {
+ repository.updateRoots();
+ }
+ });
+
+ return repository;
+ }
+
+ private void updateRoots() {
+ updateRoots(myFacet.getResourceFolderManager().getFolders());
+ }
+
+ @VisibleForTesting
+ void updateRoots(List<VirtualFile> resourceDirectories) {
+ // Compute current roots
+ Map<VirtualFile, ResourceFolderRepository> map = Maps.newHashMap();
+ for (ProjectResources resources : myChildren) {
+ ResourceFolderRepository repository = (ResourceFolderRepository)resources;
+ VirtualFile resourceDir = repository.getResourceDir();
+ map.put(resourceDir, repository);
+ }
+
+ // Compute new resource directories (it's possible for just the order to differ, or
+ // for resource dirs to have been added and/or removed)
+ Set<VirtualFile> newDirs = Sets.newHashSet(resourceDirectories);
+ List<ResourceFolderRepository> resources = Lists.newArrayListWithExpectedSize(newDirs.size());
+ for (VirtualFile dir : resourceDirectories) {
+ ResourceFolderRepository repository = map.get(dir);
+ if (repository == null) {
+ repository = ResourceFolderRegistry.get(myFacet, dir);
+ }
+ else {
+ map.remove(dir);
+ }
+ resources.add(repository);
+ }
+
+ if (resources.equals(myChildren)) {
+ // Nothing changed (including order); nothing to do
+ assert map.isEmpty(); // shouldn't have created any new ones
+ return;
+ }
+
+ for (ResourceFolderRepository removed : map.values()) {
+ removed.removeParent(this);
+ }
+
+ setChildren(resources);
+ }
+
+ /**
+ * For testing: creates a project with a given set of resource roots; this allows tests to check
+ * this repository without creating a gradle project setup etc
+ */
+ @VisibleForTesting
+ @NotNull
+ static ModuleResourceRepository createForTest(@NotNull final AndroidFacet facet, @NotNull List<VirtualFile> resourceDirectories) {
+ assert ApplicationManager.getApplication().isUnitTestMode();
+ List<ResourceFolderRepository> resources = Lists.newArrayListWithExpectedSize(resourceDirectories.size());
+ for (VirtualFile resourceDirectory : resourceDirectories) {
+ ResourceFolderRepository repository = ResourceFolderRegistry.get(facet, resourceDirectory);
+ resources.add(repository);
+ }
+
+ return new ModuleResourceRepository(facet, resources);
+ }
+
+ private static class EmptyRepository extends MultiResourceRepository {
+ public EmptyRepository() {
+ super("", Collections.<ProjectResources>emptyList());
+ }
+
+ @Override
+ protected void setChildren(@NotNull List<? extends ProjectResources> children) {
+ myChildren = children;
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/rendering/ModuleSetResourceRepository.java b/android/src/com/android/tools/idea/rendering/ModuleSetResourceRepository.java
new file mode 100644
index 0000000..6181dff
--- /dev/null
+++ b/android/src/com/android/tools/idea/rendering/ModuleSetResourceRepository.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.rendering;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.builder.model.AndroidLibrary;
+import com.android.tools.idea.gradle.IdeaAndroidProject;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.LibraryOrSdkOrderEntry;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.android.util.AndroidUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import static com.android.SdkConstants.DOT_AAR;
+import static org.jetbrains.android.facet.ResourceFolderManager.addAarsFromModuleLibraries;
+
+/** Resource repository for a module along with all its library dependencies */
+public final class ModuleSetResourceRepository extends MultiResourceRepository {
+ private final AndroidFacet myFacet;
+
+ private ModuleSetResourceRepository(@NotNull AndroidFacet facet, @NotNull List<? extends ProjectResources> delegates) {
+ super(facet.getModule().getName() + " with libraries", delegates);
+ myFacet = facet;
+ }
+
+ @NotNull
+ public static ProjectResources create(@NotNull final AndroidFacet facet) {
+ boolean refresh = facet.getIdeaAndroidProject() == null;
+
+ List<ProjectResources> resources = computeRepositories(facet);
+ final ModuleSetResourceRepository repository = new ModuleSetResourceRepository(facet, resources);
+
+ // If the model is not yet ready, we may get an incomplete set of resource
+ // directories, so in that case update the repository when the model is available.
+ if (refresh) {
+ facet.addListener(new AndroidFacet.GradleProjectAvailableListener() {
+ @Override
+ public void gradleProjectAvailable(@NotNull IdeaAndroidProject project) {
+ facet.removeListener(this);
+ repository.updateRoots();
+ }
+ });
+ }
+
+ return repository;
+ }
+
+ private static List<ProjectResources> computeRepositories(@NotNull final AndroidFacet facet) {
+ ProjectResources main = get(facet.getModule(), false /*includeLibraries */);
+
+ // List of module facets the given module depends on
+ List<AndroidFacet> dependentFacets = AndroidUtils.getAllAndroidDependencies(facet.getModule(), true);
+ List<File> aarDirs = findAarLibraries(facet, dependentFacets);
+ if (dependentFacets.isEmpty() && aarDirs.isEmpty()) {
+ return Collections.singletonList(main);
+ }
+
+ List<ProjectResources> resources = Lists.newArrayListWithExpectedSize(dependentFacets.size() + aarDirs.size());
+
+ for (File root : aarDirs) {
+ resources.add(FileProjectResourceRepository.get(root));
+ }
+
+ for (AndroidFacet f : dependentFacets) {
+ ProjectResources r = get(f.getModule(), false /*includeLibraries */);
+ resources.add(r);
+ }
+
+ resources.add(main);
+
+ return resources;
+ }
+
+ @NotNull
+ private static List<File> findAarLibraries(AndroidFacet facet, List<AndroidFacet> dependentFacets) {
+ if (facet.isGradleProject()) {
+ // Use the gradle model if available, but if not, fall back to using plain IntelliJ library dependencies
+ // which have been persisted since the most recent sync
+ if (facet.getIdeaAndroidProject() != null) {
+ List<AndroidLibrary> libraries = Lists.newArrayList();
+ addGradleLibraries(libraries, facet);
+ for (AndroidFacet f : dependentFacets) {
+ addGradleLibraries(libraries, f);
+ }
+ return findAarLibrariesFromGradle(dependentFacets, libraries);
+ } else {
+ return findAarLibrariesFromIntelliJ(facet, dependentFacets);
+ }
+ }
+
+ return Collections.emptyList();
+ }
+
+ /**
+ * Reads IntelliJ library definitions ({@link LibraryOrSdkOrderEntry}) and if possible, finds a corresponding
+ * {@code .aar} resource library to include. This works before the Gradle project has been initialized.
+ */
+ private static List<File> findAarLibrariesFromIntelliJ(AndroidFacet facet, List<AndroidFacet> dependentFacets) {
+ // Find .aar libraries from old IntelliJ library definitions
+ Set<File> dirs = Sets.newHashSet();
+ addAarsFromModuleLibraries(facet, dirs);
+ for (AndroidFacet f : dependentFacets) {
+ addAarsFromModuleLibraries(f, dirs);
+ }
+ List<File> sorted = new ArrayList<File>(dirs);
+ // Sort to ensure consistent results between pre-model sync order of resources and
+ // the post-sync order. (Also see sort comment in the method below.)
+ Collections.sort(sorted);
+ return sorted;
+ }
+
+ /**
+ * Looks up the library dependencies from the Gradle tools model and returns the corresponding {@code .aar}
+ * resource directories.
+ */
+ @NotNull
+ private static List<File> findAarLibrariesFromGradle(List<AndroidFacet> dependentFacets, List<AndroidLibrary> libraries) {
+ // Pull out the unique directories, in case multiple modules point to the same .aar folder
+ Set<File> files = Sets.newHashSetWithExpectedSize(dependentFacets.size());
+
+ Set<String> moduleNames = Sets.newHashSet();
+ for (AndroidFacet f : dependentFacets) {
+ moduleNames.add(f.getModule().getName());
+ }
+ for (AndroidLibrary library : libraries) {
+ // We should only add .aar dependencies if they aren't already provided as modules.
+ // For now, the way we associate them with each other is via the library name;
+ // in the future the model will provide this for us
+ String libraryName = null;
+ String projectName = library.getProject();
+ if (projectName != null && !projectName.isEmpty()) {
+ libraryName = projectName.substring(projectName.lastIndexOf(':') + 1);
+ // Since this library has project!=null, it exists in module form; don't
+ // add it here.
+ moduleNames.add(libraryName);
+ continue;
+ } else {
+ File folder = library.getFolder();
+ String name = folder.getName();
+ if (name.endsWith(DOT_AAR)) {
+ libraryName = name.substring(0, name.length() - DOT_AAR.length());
+ }
+ }
+ if (libraryName != null && !moduleNames.contains(libraryName)) {
+ File resFolder = library.getResFolder();
+ if (resFolder.exists()) {
+ files.add(resFolder);
+
+ // Don't add it again!
+ moduleNames.add(libraryName);
+ }
+ }
+ }
+
+ List<File> dirs = Lists.newArrayList();
+ for (File resFolder : files) {
+ dirs.add(resFolder);
+ }
+
+ // Sort alphabetically to ensure that we keep a consistent order of these libraries;
+ // otherwise when we jump from libraries initialized from IntelliJ library binary paths
+ // to gradle project state, the order difference will cause the merged project resource
+ // maps to have to be recomputed
+ Collections.sort(dirs);
+ return dirs;
+ }
+
+ private static void addGradleLibraries(List<AndroidLibrary> list, AndroidFacet facet) {
+ IdeaAndroidProject gradleProject = facet.getIdeaAndroidProject();
+ if (gradleProject != null) {
+ list.addAll(gradleProject.getSelectedVariant().getMainArtifactInfo().getDependencies().getLibraries());
+ }
+ }
+
+ void updateRoots() {
+ List<ProjectResources> repositories = computeRepositories(myFacet);
+ updateRoots(repositories);
+ }
+
+ @VisibleForTesting
+ void updateRoots(List<ProjectResources> resourceDirectories) {
+ if (resourceDirectories.equals(myChildren)) {
+ // Nothing changed (including order); nothing to do
+ return;
+ }
+
+ setChildren(resourceDirectories);
+ }
+
+ /**
+ * Called when module roots have changed in the given project. Locates all
+ * the {@linkplain ModuleSetResourceRepository} instances (but only those that
+ * have already been initialized) and updates the roots, if necessary.
+ *
+ * @param project the project whose module roots changed.
+ */
+ public static void moduleRootsChanged(@NotNull Project project) {
+ ModuleManager moduleManager = ModuleManager.getInstance(project);
+ for (Module module : moduleManager.getModules()) {
+ moduleRootsChanged(module);
+ }
+ }
+
+ /**
+ * Called when module roots have changed in the given module. Locates the
+ * {@linkplain ModuleSetResourceRepository} instance (but only if it has
+ * already been initialized) and updates its roots, if necessary.
+ * <p>
+ * TODO: Currently, this method is only called during a Gradle project import.
+ * We should call it for non-Gradle projects after modules are changed in the
+ * project structure dialog etc. with
+ * project.getMessageBus().connect().subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootListener() { ... }
+ *
+ *
+ * @param module the module whose roots changed
+ */
+ public static void moduleRootsChanged(@NotNull Module module) {
+ AndroidFacet facet = AndroidFacet.getInstance(module);
+ if (facet != null) {
+ if (facet.isGradleProject() && facet.getIdeaAndroidProject() == null) {
+ // Project not yet fully initialized; no need to do a sync now because our
+ // GradleProjectAvailableListener will be called as soon as it is and do a proper sync
+ return;
+ }
+ ProjectResources resources = facet.getProjectResources(true, false);
+ if (resources instanceof ModuleSetResourceRepository) {
+ ModuleSetResourceRepository moduleSetRepository = (ModuleSetResourceRepository)resources;
+ moduleSetRepository.updateRoots();
+ }
+ }
+ }
+
+ @VisibleForTesting
+ @NotNull
+ static ProjectResources create(AndroidFacet facet, List<ProjectResources> modules) {
+ return new ModuleSetResourceRepository(facet, modules);
+ }
+}
diff --git a/android/src/com/android/tools/idea/rendering/MultiResourceRepository.java b/android/src/com/android/tools/idea/rendering/MultiResourceRepository.java
new file mode 100644
index 0000000..d95fc48
--- /dev/null
+++ b/android/src/com/android/tools/idea/rendering/MultiResourceRepository.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.rendering;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.res2.ResourceFile;
+import com.android.ide.common.res2.ResourceItem;
+import com.android.ide.common.resources.IntArrayWrapper;
+import com.android.resources.ResourceType;
+import com.android.util.Pair;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Maps;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.psi.PsiFile;
+import gnu.trove.TIntObjectHashMap;
+import gnu.trove.TObjectIntHashMap;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Map;
+
+@SuppressWarnings("deprecation") // Deprecated com.android.util.Pair is required by ProjectCallback interface
+public abstract class MultiResourceRepository extends ProjectResources {
+ protected List<? extends ProjectResources> myChildren;
+ private long[] myModificationCounts;
+ private Map<ResourceType, ListMultimap<String, ResourceItem>> myItems = Maps.newEnumMap(ResourceType.class);
+ private final Map<ResourceType, ListMultimap<String, ResourceItem>> myCachedTypeMaps = Maps.newEnumMap(ResourceType.class);
+
+ MultiResourceRepository(@NotNull String displayName, @NotNull List<? extends ProjectResources> children) {
+ super(displayName);
+ setChildren(children);
+ }
+
+ protected void setChildren(@NotNull List<? extends ProjectResources> children) {
+ if (myChildren != null) {
+ for (int i = myChildren.size() - 1; i >= 0; i--) {
+ ProjectResources resources = myChildren.get(i);
+ resources.removeParent(this);
+ }
+ }
+ myChildren = children;
+ myModificationCounts = new long[children.size()];
+ for (int i = myChildren.size() - 1; i >= 0; i--) {
+ ProjectResources resources = myChildren.get(i);
+ resources.addParent(this);
+ myModificationCounts[i] = resources.getModificationCount();
+ }
+ myGeneration++;
+ clearCache();
+ invalidateItemCaches();
+ }
+
+ private void clearCache() {
+ myItems = null;
+ myCachedTypeMaps.clear();
+ }
+
+ public List<? extends ProjectResources> getChildren() {
+ return myChildren;
+ }
+
+ @Nullable
+ @Override
+ public Pair<ResourceType, String> resolveResourceId(int id) {
+ Pair<ResourceType, String> pair = super.resolveResourceId(id);
+ if (pair != null) {
+ return pair;
+ }
+
+ for (int i = myChildren.size() - 1; i >= 0; i--) {
+ ProjectResources resources = myChildren.get(i);
+ Pair<ResourceType, String> resolved = resources.resolveResourceId(id);
+ if (resolved != null) {
+ return resolved;
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String resolveStyleable(int[] id) {
+ String resourceName = super.resolveStyleable(id);
+ if (resourceName != null) {
+ return resourceName;
+ }
+
+ for (int i = myChildren.size() - 1; i >= 0; i--) {
+ ProjectResources resources = myChildren.get(i);
+ String resolved = resources.resolveStyleable(id);
+ if (resolved != null) {
+ return resolved;
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Integer getResourceId(ResourceType type, String name) {
+ Integer id = super.getResourceId(type, name);
+ if (id != null) {
+ return id;
+ }
+
+ for (int i = myChildren.size() - 1; i >= 0; i--) {
+ ProjectResources resources = myChildren.get(i);
+ Integer resolved = resources.getResourceId(type, name);
+ if (resolved != null) {
+ return resolved;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void setCompiledResources(TIntObjectHashMap<Pair<ResourceType, String>> id2res,
+ Map<IntArrayWrapper, String> styleableId2name,
+ Map<ResourceType, TObjectIntHashMap<String>> res2id) {
+ super.setCompiledResources(id2res, styleableId2name, res2id);
+ for (int i = myChildren.size() - 1; i >= 0; i--) {
+ ProjectResources resources = myChildren.get(i);
+ resources.setCompiledResources(id2res, styleableId2name, res2id);
+ }
+ }
+
+ @Override
+ public long getModificationCount() {
+ if (myChildren.size() == 1) {
+ return myChildren.get(0).getModificationCount();
+ }
+
+ // See if any of the delegates have changed
+ boolean changed = false;
+ for (int i = myChildren.size() - 1; i >= 0; i--) {
+ ProjectResources resources = myChildren.get(i);
+ long rev = resources.getModificationCount();
+ if (rev != myModificationCounts[i]) {
+ myModificationCounts[i] = rev;
+ changed = true;
+ }
+ }
+ if (changed) {
+ myGeneration++;
+ }
+
+ return myGeneration;
+ }
+
+ @NonNull
+ @Override
+ protected Map<ResourceType, ListMultimap<String, ResourceItem>> getMap() {
+ if (myItems == null) {
+ if (myChildren.size() == 1) {
+ myItems = myChildren.get(0).getItems();
+ }
+ else {
+ Map<ResourceType, ListMultimap<String, ResourceItem>> map = Maps.newEnumMap(ResourceType.class);
+ for (ResourceType type : ResourceType.values()) {
+ map.put(type, getMap(type, false)); // should pass create is true, but as described below we interpret this differently
+ }
+ myItems = map;
+ }
+ }
+
+ return myItems;
+ }
+
+ @Nullable
+ @Override
+ protected ListMultimap<String, ResourceItem> getMap(ResourceType type, boolean create) {
+ // Should I assert !create here? If we try to manipulate the cache it won't work right...
+ ListMultimap<String, ResourceItem> map = myCachedTypeMaps.get(type);
+ if (map != null) {
+ return map;
+ }
+
+ if (myChildren.size() == 1) {
+ return myChildren.get(0).getItems().get(type);
+ }
+
+ map = ArrayListMultimap.create();
+ myCachedTypeMaps.put(type, map);
+
+ // Merge all items of the given type
+ for (int i = myChildren.size() - 1; i >= 0; i--) {
+ ProjectResources resources = myChildren.get(i);
+ Map<ResourceType, ListMultimap<String, ResourceItem>> items = resources.getItems();
+ ListMultimap<String, ResourceItem> m = items.get(type);
+ if (m == null) {
+ continue;
+ }
+
+ // TODO: Start with JUST the first map here (which often contains most of the keys) and then
+ // only merge in 1...n
+ for (ResourceItem item : m.values()) {
+ String name = item.getName();
+ if (map.containsKey(name)) {
+ // The item already exists in this map; only add if there isn't an item with the
+ // same qualifiers
+ ResourceFile itemSource = item.getSource();
+ String qualifiers = itemSource != null ? itemSource.getQualifiers() : "";
+ boolean contains = false;
+ List<ResourceItem> list = map.get(name);
+ assert list != null;
+ for (ResourceItem existing : list) {
+ ResourceFile source = existing.getSource();
+ if (source != null && qualifiers.equals(source.getQualifiers())) {
+ contains = true;
+ break;
+ }
+ }
+ if (!contains) {
+ map.put(name, item);
+ }
+ }
+ else {
+ map.put(name, item);
+ }
+ }
+ }
+
+ return map;
+ }
+
+ @NonNull
+ @Override
+ protected ListMultimap<String, ResourceItem> getMap(ResourceType type) {
+ return super.getMap(type);
+ }
+
+ @NonNull
+ @Override
+ public Map<ResourceType, ListMultimap<String, ResourceItem>> getItems() {
+ return getMap();
+ }
+
+ @Override
+ public void dispose() {
+ for (int i = myChildren.size() - 1; i >= 0; i--) {
+ ProjectResources resources = myChildren.get(i);
+ resources.removeParent(this);
+ resources.dispose();
+ }
+ }
+
+ /**
+ * Notifies this delegating repository that the given dependent repository has invalidated
+ * resources of the given types (empty means all)
+ */
+ public void invalidateCache(@NotNull ProjectResources repository, @Nullable ResourceType... types) {
+ assert myChildren.contains(repository) : repository;
+
+ if (types == null || types.length == 0) {
+ myCachedTypeMaps.clear();
+ }
+ else {
+ for (ResourceType type : types) {
+ myCachedTypeMaps.remove(type);
+ }
+ }
+ myItems = null;
+ myGeneration++;
+
+ invalidateItemCaches(types);
+ }
+
+ @Override
+ @VisibleForTesting
+ boolean isScanPending(@NonNull PsiFile psiFile) {
+ assert ApplicationManager.getApplication().isUnitTestMode();
+ for (int i = myChildren.size() - 1; i >= 0; i--) {
+ ProjectResources resources = myChildren.get(i);
+ if (resources.isScanPending(psiFile)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @VisibleForTesting
+ int getChildCount() {
+ return myChildren.size();
+ }
+}
diff --git a/android/src/com/android/tools/idea/rendering/ProjectCallback.java b/android/src/com/android/tools/idea/rendering/ProjectCallback.java
index 0f92b9e..a412abd 100644
--- a/android/src/com/android/tools/idea/rendering/ProjectCallback.java
+++ b/android/src/com/android/tools/idea/rendering/ProjectCallback.java
@@ -110,6 +110,12 @@
@SuppressWarnings("unchecked")
public Object loadView(@NotNull String className, @NotNull Class[] constructorSignature, @NotNull Object[] constructorParameters)
throws Exception {
+ if (className.indexOf('.') == -1 && !VIEW_FRAGMENT.equals(className) && !VIEW_INCLUDE.equals(className)) {
+ // When something is *really* wrong we get asked to load core Android classes.
+ // Ignore these; custom views should always have fully qualified names.
+ throw new ClassNotFoundException(className);
+ }
+
myUsed = true;
return myClassLoader.loadView(className, constructorSignature, constructorParameters);
diff --git a/android/src/com/android/tools/idea/rendering/ProjectResources.java b/android/src/com/android/tools/idea/rendering/ProjectResources.java
index 853e34b..1bd5a10 100644
--- a/android/src/com/android/tools/idea/rendering/ProjectResources.java
+++ b/android/src/com/android/tools/idea/rendering/ProjectResources.java
@@ -15,37 +15,158 @@
*/
package com.android.tools.idea.rendering;
+import com.android.SdkConstants;
import com.android.annotations.NonNull;
+import com.android.annotations.VisibleForTesting;
import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
-import com.android.ide.common.res2.ResourceRepository;
import com.android.ide.common.resources.IntArrayWrapper;
import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.resources.FolderTypeRelationship;
+import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.util.Pair;
-import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.util.ModificationTracker;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.psi.xml.XmlTag;
import gnu.trove.TIntObjectHashMap;
import gnu.trove.TObjectIntHashMap;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import java.util.Collection;
+import java.io.File;
import java.util.List;
import java.util.Map;
-import java.util.SortedSet;
-public abstract class ProjectResources extends ResourceRepository implements Disposable, ModificationTracker {
+
+/**
+ * Repository for Android application resources, e.g. those that show up in {@code R}, not {@code android.R}
+ * (which are referred to as framework resources.)
+ * <p>
+ * For a given Android module, you can obtain either the resources for the module itself, or for a module and all
+ * its libraries. Most clients should use the module with all its dependencies included; when a user is
+ * using code completion for example, they expect to be offered not just the drawables in this module, but
+ * all the drawables available in this module which includes the libraries.
+ * </p>
+ * <p>
+ * The module repository is implemented using several layers. Consider a Gradle project where the main module has
+ * two flavors, and depends on a library module. In this case, the {@linkplain ProjectResources} for the
+ * module with dependencies will contain these components:
+ * <ul>
+ * <li> A {@link ModuleSetResourceRepository} representing the collection of module repositories</li>
+ * <li> For each module (e.g. the main module and library module}, a {@link ModuleResourceRepository}</li>
+ * <li> For each resource directory in each module, a {@link ResourceFolderRepository}</li>
+ * </ul>
+ * These different repositories are merged together by the {@link MultiResourceRepository} class,
+ * which represents a repository that just combines the resources from each of its children.
+ * Both {@linkplain ModuleResourceRepository} and {@linkplain ModuleSetResourceRepository} are instances
+ * of a {@linkplain MultiResourceRepository}.
+ * </p>
+ * <p>
+ * The {@link ResourceFolderRepository} is the lowest level of repository. It is associated with just
+ * a single resource folder. Therefore, it does not have to worry about trying to mask resources between
+ * different flavors; that task is done by the {@link ModuleResourceRepository} which combines
+ * {@linkplain ResourceFolderRepository} instances. Instead, the {@linkplain ResourceFolderRepository} just
+ * needs to compute the resource items for the resource folders, including qualifier variations.
+ * </p>
+ * <p>
+ * The resource repository automatically stays up to date. You can call {@linkplain #getModificationCount()}
+ * to see whether anything has changed since your last data fetch. This is for example how the resource
+ * string folding in the source editors work; they fetch the current values of the resource strings, and
+ * store those along with the current project resource modification count into the folding data structures.
+ * When the editor wants to see if the folding sections are up to date, those are compared with the current
+ * {@linkplain #getModificationCount()} version, and only if they differ is the folding structure updated.
+ * </p>
+ * <p>
+ * Only the {@linkplain ResourceFolderRepository} needs to listen for user edits and file changes. It
+ * uses {@linkplain PsiProjectListener}, a single listener which is shared by all repositories in the
+ * same project, to get notified when something in one of its resource files changes, and it uses the
+ * PSI change event to selectively update the repository data structures, if possible.
+ * </p>
+ * <p>
+ * The {@linkplain ResourceFolderRepository} can also have a pointer to its parent. This is possible
+ * since a resource folder can only be in a single module. The parent reference is used to quickly
+ * invalidate the cache of the parent {@link MultiResourceRepository}. For example, let's say the
+ * project has two flavors. When the PSI change event is used to update the name of a string resource,
+ * the repository will also notify the parent that its {@link ResourceType#ID} map is out of date.
+ * The {@linkplain MultiResourceRepository} will use this to null out its map cache of strings, and
+ * on the next read, it will merge in the string maps from all its {@linkplain ResourceFolderRepository}
+ * children.
+ * </p>
+ * <p>
+ * One common type of "update" is changing the current variant in the IDE. With the above scheme,
+ * this just means reordering the {@linkplain ResourceFolderRepository} instances in the
+ * {@linkplain ModuleResourceRepository}; it does not have to rescan the resources as it did in the
+ * previous implementation.
+ * </p>
+ * <p>
+ * The {@linkplain ModuleSetResourceRepository} is similar, but it combines {@link ModuleResourceRepository}
+ * instances rather than {@link ResourceFolderRepository} instances. Note also that the way these
+ * resource repositories work is slightly different from the way the resource items are used by
+ * the builder: The builder will bail if it encounters duplicate declarations unless they are in alternative
+ * folders of the same flavor. For the resource repository we never want to bail on merging; the repository
+ * is kept up to date and live as the user is editing, so it is normal for the repository to sometimes
+ * reflect invalid user edits (in the same way a Java editor in an IDE sometimes is showing uncompilable
+ * source code) and it needs to be able to handle this case and offer a state that is as close to possible
+ * as the intended meaning. Error handling is done by another part of the IDE.
+ * </p>
+ * <p>
+ * Finally, note that the resource repository is showing the current state of the resources for the
+ * currently selected variant. Note however that the above approach also lets us query resources for
+ * example for <b>all</b> flavors, not just the currently selected flavor. We can offer APIs to iterate
+ * through all available {@link ResourceFolderRepository} instances, not just the set of instances for
+ * the current module's current flavor. This will allow us to for example preview the string translations
+ * for a given resource name not just for the current flavor but for all other flavors as well.
+ * </p>
+ * TODO: Rename this class to ModuleResources, or maybe LocalResources or ApplicationResources
+ */
+@SuppressWarnings("deprecation") // Deprecated com.android.util.Pair is required by ProjectCallback interface
+public abstract class ProjectResources extends AbstractResourceRepository implements Disposable, ModificationTracker {
protected static final Logger LOG = Logger.getInstance(ProjectResources.class);
+ private final String myDisplayName;
+
+ @Nullable private List<MultiResourceRepository> myParents;
+
+ // Project resource ints are defined as 0x7FXX#### where XX is the resource type (layout, drawable,
+ // etc...). Using FF as the type allows for 255 resource types before we get a collision
+ // which should be fine.
+ private static final int DYNAMIC_ID_SEED_START = 0x7fff0000;
+
+ /** Map of (name, id) for resources of type {@link com.android.resources.ResourceType#ID} coming from R.java */
+ private Map<ResourceType, TObjectIntHashMap<String>> myResourceValueMap;
+ /** Map of (id, [name, resType]) for all resources coming from R.java */
+ private TIntObjectHashMap<Pair<ResourceType, String>> myResIdValueToNameMap;
+ /** Map of (int[], name) for styleable resources coming from R.java */
+ private Map<IntArrayWrapper, String> myStyleableValueToNameMap;
+
+ private final TObjectIntHashMap<String> myName2DynamicIdMap = new TObjectIntHashMap<String>();
+ private final TIntObjectHashMap<Pair<ResourceType, String>> myDynamicId2ResourceMap =
+ new TIntObjectHashMap<Pair<ResourceType, String>>();
+ private int myDynamicSeed = DYNAMIC_ID_SEED_START;
+ private final IntArrayWrapper myWrapper = new IntArrayWrapper(null);
+
protected long myGeneration;
- protected ProjectResources() {
+ protected ProjectResources(@NotNull String displayName) {
super(false);
+ myDisplayName = displayName;
+ }
+
+ @NotNull
+ public String getDisplayName() {
+ return myDisplayName;
}
@NotNull
@@ -68,9 +189,9 @@
@NotNull
public static ProjectResources create(@NotNull AndroidFacet facet, boolean includeLibraries) {
if (includeLibraries) {
- return DelegatingProjectResources.create(facet);
+ return ModuleSetResourceRepository.create(facet);
} else {
- return FileProjectResourceRepository.create(facet);
+ return ModuleResourceRepository.create(facet);
}
}
@@ -83,22 +204,95 @@
return false;
}
+ public void addParent(@NonNull MultiResourceRepository parent) {
+ if (myParents == null) {
+ myParents = Lists.newArrayListWithExpectedSize(2); // Don't expect many parents
+ }
+ myParents.add(parent);
+ }
+
+ public void removeParent(@NonNull MultiResourceRepository parent) {
+ if (myParents != null) {
+ myParents.remove(parent);
+ }
+ }
+
+ protected void invalidateItemCaches(@Nullable ResourceType... types) {
+ if (myParents != null) {
+ for (MultiResourceRepository parent : myParents) {
+ parent.invalidateCache(this, types);
+ }
+ }
+ }
+
// For ProjectCallback
@Nullable
- public abstract Pair<ResourceType, String> resolveResourceId(int id);
+ public Pair<ResourceType, String> resolveResourceId(int id) {
+ Pair<ResourceType, String> result = null;
+ if (myResIdValueToNameMap != null) {
+ result = myResIdValueToNameMap.get(id);
+ }
+
+ if (result == null) {
+ final Pair<ResourceType, String> pair = myDynamicId2ResourceMap.get(id);
+ if (pair != null) {
+ result = pair;
+ }
+ }
+
+ return result;
+ }
@Nullable
- public abstract String resolveStyleable(int[] id);
+ public String resolveStyleable(int[] id) {
+ if (myStyleableValueToNameMap != null) {
+ myWrapper.set(id);
+ // A normal map lookup on int[] would only consider object identity, but the IntArrayWrapper
+ // will check all the individual elements for equality. We reuse an instance for all the lookups
+ // since we don't need a new one each time.
+ return myStyleableValueToNameMap.get(myWrapper);
+ }
+
+ return null;
+ }
@Nullable
- public abstract Integer getResourceId(ResourceType type, String name);
+ public Integer getResourceId(ResourceType type, String name) {
+ final TObjectIntHashMap<String> map = myResourceValueMap != null ? myResourceValueMap.get(type) : null;
- public abstract void setCompiledResources(TIntObjectHashMap<Pair<ResourceType, String>> id2res,
- Map<IntArrayWrapper, String> styleableId2name,
- Map<ResourceType, TObjectIntHashMap<String>> res2id);
+ if (map == null || !map.containsKey(name)) {
+ return getDynamicId(type, name);
+ }
+ return map.get(name);
+ }
- public abstract void sync();
+ private int getDynamicId(ResourceType type, String name) {
+ synchronized (myName2DynamicIdMap) {
+ if (myName2DynamicIdMap.containsKey(name)) {
+ return myName2DynamicIdMap.get(name);
+ }
+ final int value = ++myDynamicSeed;
+ myName2DynamicIdMap.put(name, value);
+ myDynamicId2ResourceMap.put(value, Pair.of(type, name));
+ return value;
+ }
+ }
+
+ public void setCompiledResources(TIntObjectHashMap<Pair<ResourceType, String>> id2res,
+ Map<IntArrayWrapper, String> styleableId2name,
+ Map<ResourceType, TObjectIntHashMap<String>> res2id) {
+ // Regularly clear dynamic seed such that we don't run out of numbers (we only have 255)
+ synchronized (myName2DynamicIdMap) {
+ myDynamicSeed = DYNAMIC_ID_SEED_START;
+ myName2DynamicIdMap.clear();
+ myDynamicId2ResourceMap.clear();
+ }
+
+ myResourceValueMap = res2id;
+ myResIdValueToNameMap = id2res;
+ myStyleableValueToNameMap = styleableId2name;
+ }
// ---- Implements ModificationCount ----
@@ -107,116 +301,107 @@
* the generation increases. This can be used to force refreshing of layouts etc (which will cache
* configured project resources) when the project resources have changed since last render.
* <p>
- * Note that the generation is not a change count. If you change the contents of a layout drawable XML file,
+ * Note that the generation is not a simple change count. If you change the contents of a layout drawable XML file,
* that will not affect the {@link ResourceItem} and {@link ResourceValue} results returned from
* this repository; we only store the presence of file based resources like layouts, menus, and drawables.
* Therefore, only additions or removals of these files will cause a generation change.
* <p>
- * Value resource files, such as string files, will cause generation changes when they are edited.
- * Later we should consider only updating the generation when the actual values are changed (such that
- * we can ignore whitespace changes, comment changes, reordering changes (outside of arrays), and so on.
- * The natural time to implement this is when we reimplement this class to directly work on top of
- * the PSI data structures, rather than simply using a PSI listener and calling super methods to
- * process ResourceFile objects as is currently done.
+ * Value resource files, such as string files, will cause generation changes when they are edited (unless
+ * the change is determined to not be relevant to resource values, such as a change in an XML comment, etc.
*
* @return the generation id
*/
@Override
public long getModificationCount() {
- sync();
- // First sync in case there are pending changes which will rev the generation
return myGeneration;
}
- // Code related to updating the resources
+ @Nullable
+ public VirtualFile getMatchingFile(@NonNull VirtualFile file, @NonNull ResourceType type, @NonNull FolderConfiguration config) {
+ @SuppressWarnings("deprecation")
+ ResourceFile best = super.getMatchingFile(ResourceHelper.getResourceName(file), type, config);
+ if (best != null) {
+ if (best instanceof PsiResourceFile) {
+ PsiResourceFile prf = (PsiResourceFile)best;
+ return prf.getPsiFile().getVirtualFile();
+ }
- @NonNull
- @Override
- public Collection<String> getItemsOfType(@NonNull ResourceType type) {
- sync();
- return super.getItemsOfType(type);
+ return LocalFileSystem.getInstance().findFileByIoFile(best.getFile());
+ }
+
+ return null;
}
- @NonNull
- @Override
- public Map<ResourceType, ListMultimap<String, ResourceItem>> getItems() {
- sync();
- return super.getItems();
- }
-
+ /** @deprecated Use {@link #getMatchingFile(VirtualFile, ResourceType, FolderConfiguration)} in the plugin code */
@Nullable
@Override
- public List<ResourceItem> getResourceItem(@NonNull ResourceType resourceType, @NonNull String resourceName) {
- sync();
- return super.getResourceItem(resourceType, resourceName);
- }
-
- @Override
- public boolean hasResourceItem(@NonNull String url) {
- sync();
- return super.hasResourceItem(url);
- }
-
- @Override
- public boolean hasResourceItem(@NonNull ResourceType resourceType, @NonNull String resourceName) {
- sync();
- return super.hasResourceItem(resourceType, resourceName);
- }
-
- @Override
- public boolean hasResourcesOfType(@NonNull ResourceType resourceType) {
- sync();
- return super.hasResourcesOfType(resourceType);
- }
-
- @NonNull
- @Override
- public List<ResourceType> getAvailableResourceTypes() {
- sync();
- return super.getAvailableResourceTypes();
- }
-
- @Nullable
- @Override
+ @Deprecated
public ResourceFile getMatchingFile(@NonNull String name, @NonNull ResourceType type, @NonNull FolderConfiguration config) {
- sync();
+ assert name.indexOf('.') == -1 : name;
return super.getMatchingFile(name, type, config);
}
- @NonNull
- @Override
- public Map<ResourceType, Map<String, ResourceValue>> getConfiguredResources(@NonNull FolderConfiguration referenceConfig) {
- sync();
- return super.getConfiguredResources(referenceConfig);
+ @VisibleForTesting
+ boolean isScanPending(@NonNull PsiFile psiFile) {
+ return false;
}
- @NonNull
- @Override
- public Map<String, ResourceValue> getConfiguredResources(@NonNull ResourceType type, @NonNull FolderConfiguration referenceConfig) {
- sync();
- return super.getConfiguredResources(type, referenceConfig);
- }
-
+ /** Returns the {@link PsiFile} corresponding to the source of the given resource item, if possible */
@Nullable
- @Override
- public ResourceValue getConfiguredValue(@NonNull ResourceType type, @NonNull String name, @NonNull FolderConfiguration referenceConfig) {
- sync();
- return super.getConfiguredValue(type, name, referenceConfig);
+ public static PsiFile getItemPsiFile(@NonNull AndroidFacet facet, @NonNull ResourceItem item) {
+ if (item instanceof PsiResourceItem) {
+ PsiResourceItem psiResourceItem = (PsiResourceItem)item;
+ return psiResourceItem.getPsiFile();
+ }
+
+ ResourceFile source = item.getSource();
+ assert source != null : item.getName();
+
+ if (source instanceof PsiResourceFile) {
+ PsiResourceFile prf = (PsiResourceFile)source;
+ return prf.getPsiFile();
+ }
+
+ File file = source.getFile();
+ VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByIoFile(file);
+ if (virtualFile != null) {
+ PsiManager psiManager = PsiManager.getInstance(facet.getModule().getProject());
+ return psiManager.findFile(virtualFile);
+ }
+
+ return null;
}
- @NonNull
- @Override
- public SortedSet<String> getLanguages() {
- sync();
- return super.getLanguages();
- }
+ /**
+ * Returns the {@link XmlTag} corresponding to the given resource item. This is only
+ * defined for resource items in value files.
+ */
+ @Nullable
+ public static XmlTag getItemTag(@NonNull AndroidFacet facet, @NonNull ResourceItem item) {
+ if (item instanceof PsiResourceItem) {
+ PsiResourceItem psiResourceItem = (PsiResourceItem)item;
+ return psiResourceItem.getTag();
+ }
- @NonNull
- @Override
- public SortedSet<String> getRegions(@NonNull String currentLanguage) {
- sync();
- return super.getRegions(currentLanguage);
- }
+ PsiFile psiFile = getItemPsiFile(facet, item);
+ if (psiFile instanceof XmlFile) {
+ String resourceName = item.getName();
+ XmlFile xmlFile = (XmlFile)psiFile;
+ ApplicationManager.getApplication().assertReadAccessAllowed();
+ XmlTag rootTag = xmlFile.getRootTag();
+ if (rootTag != null) {
+ XmlTag[] subTags = rootTag.getSubTags();
+ for (XmlTag tag : subTags) {
+ if (resourceName.equals(tag.getAttributeValue(SdkConstants.ATTR_NAME))) {
+ return tag;
+ }
+ }
+ }
- public abstract void refresh();
+ // This method should only be called on value resource types
+ assert FolderTypeRelationship.getRelatedFolders(item.getType()).contains(ResourceFolderType.VALUES) : item.getType();
+ }
+
+ return null;
+ }
}
diff --git a/android/src/com/android/tools/idea/rendering/PsiProjectListener.java b/android/src/com/android/tools/idea/rendering/PsiProjectListener.java
new file mode 100644
index 0000000..492b9d3
--- /dev/null
+++ b/android/src/com/android/tools/idea/rendering/PsiProjectListener.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.rendering;
+
+import com.android.resources.ResourceFolderType;
+import com.google.common.collect.Maps;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.fileTypes.FileTypeManager;
+import com.intellij.openapi.fileTypes.StdFileTypes;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.*;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Map;
+
+import static com.android.SdkConstants.EXT_PNG;
+import static com.android.SdkConstants.FD_RES_RAW;
+
+public class PsiProjectListener extends PsiTreeChangeAdapter {
+
+ @NotNull private static Map<Project, PsiProjectListener> ourListeners = Maps.newHashMap();
+ @NotNull private final Map<VirtualFile, ResourceFolderRepository> myListeners = Maps.newHashMap();
+
+ private PsiProjectListener(@NotNull Project project) {
+ PsiManager.getInstance(project).addPsiTreeChangeListener(this);
+ }
+
+ public static void addRoot(@NotNull Project project, @NotNull VirtualFile root, @NotNull ResourceFolderRepository repository) {
+ synchronized (PsiProjectListener.class) {
+ getListener(project).addRoot(root, repository);
+ }
+ }
+
+ public static void removeRoot(@NotNull Project project, @NotNull VirtualFile root, @NotNull ResourceFolderRepository repository) {
+ synchronized (PsiProjectListener.class) {
+ getListener(project).removeRoot(root, repository);
+ }
+ }
+
+ @NotNull
+ private static PsiProjectListener getListener(@NotNull Project project) {
+ PsiProjectListener listener = ourListeners.get(project);
+ if (listener == null) {
+ listener = new PsiProjectListener(project);
+ ourListeners.put(project, listener);
+ }
+
+ return listener;
+ }
+
+ private void addRoot(@NotNull VirtualFile root, @NotNull ResourceFolderRepository repository) {
+ assert myListeners.get(root) == null; // Repositories should be unique.
+ // TODO: Walk up in the chain and make sure they aren't nested either!
+
+ myListeners.put(root, repository);
+ }
+
+ private void removeRoot(@NotNull VirtualFile root, @NotNull ResourceFolderRepository repository) {
+ assert myListeners.get(root) == repository : repository;
+ myListeners.remove(root);
+ }
+
+ static boolean isRelevantFileType(@NotNull FileType fileType) {
+ if (fileType == StdFileTypes.JAVA) { // fail fast for vital file type
+ return false;
+ }
+ return fileType == StdFileTypes.XML ||
+ (fileType.isBinary() && fileType == FileTypeManager.getInstance().getFileTypeByExtension(EXT_PNG));
+ }
+
+ static boolean isRelevantFile(@NotNull VirtualFile file) {
+ FileType fileType = file.getFileType();
+ if (fileType == StdFileTypes.JAVA) {
+ return false;
+ }
+
+ if (isRelevantFileType(fileType)) {
+ return true;
+ } else {
+ VirtualFile parent = file.getParent();
+ if (parent != null) {
+ String parentName = parent.getName();
+ if (parentName.startsWith(FD_RES_RAW)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ static boolean isRelevantFile(@NotNull PsiFile file) {
+ FileType fileType = file.getFileType();
+ if (fileType == StdFileTypes.JAVA) {
+ return false;
+ }
+
+ if (isRelevantFileType(fileType)) {
+ return true;
+ } else {
+ PsiDirectory parent = file.getParent();
+ if (parent != null) {
+ String parentName = parent.getName();
+ if (parentName.startsWith(FD_RES_RAW)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+
+ @Nullable
+ private ResourceFolderRepository findRepository(@Nullable VirtualFile file) {
+ if (file == null) {
+ return null;
+ }
+ while (file != null) {
+ ResourceFolderRepository repository = myListeners.get(file);
+ if (repository != null) {
+ return repository;
+ }
+
+ file = file.getParent();
+ }
+
+ return null;
+ }
+
+ @Override
+ public void childAdded(@NotNull PsiTreeChangeEvent event) {
+ PsiFile psiFile = event.getFile();
+ if (psiFile == null) {
+ PsiElement child = event.getChild();
+ if (child instanceof PsiFile) {
+ VirtualFile file = ((PsiFile)child).getVirtualFile();
+ if (file != null && isRelevantFile(file)) {
+ dispatchChildAdded(event, file);
+ }
+ } else if (child instanceof PsiDirectory) {
+ PsiDirectory directory = (PsiDirectory)child;
+ dispatchChildAdded(event, directory.getVirtualFile());
+ }
+ } else if (isRelevantFile(psiFile)) {
+ dispatchChildAdded(event, psiFile.getVirtualFile());
+ }
+ }
+
+ private void dispatchChildAdded(@NotNull PsiTreeChangeEvent event, @Nullable VirtualFile virtualFile) {
+ ResourceFolderRepository repository = findRepository(virtualFile);
+ if (repository != null) {
+ repository.getPsiListener().childAdded(event);
+ }
+ }
+
+ @Override
+ public void childRemoved(@NotNull PsiTreeChangeEvent event) {
+ PsiFile psiFile = event.getFile();
+ if (psiFile == null) {
+ PsiElement child = event.getChild();
+ if (child instanceof PsiFile) {
+ VirtualFile file = ((PsiFile)child).getVirtualFile();
+ if (file != null && isRelevantFile(file)) {
+ dispatchChildRemoved(event, file);
+ }
+ } else if (child instanceof PsiDirectory) {
+ PsiDirectory directory = (PsiDirectory)child;
+ if (ResourceFolderType.getFolderType(directory.getName()) != null) {
+ VirtualFile file = directory.getVirtualFile();
+ dispatchChildRemoved(event, file);
+ }
+ }
+ } else if (isRelevantFile(psiFile)) {
+ VirtualFile file = psiFile.getVirtualFile();
+ dispatchChildRemoved(event, file);
+ }
+ }
+
+ private void dispatchChildRemoved(@NotNull PsiTreeChangeEvent event, @Nullable VirtualFile virtualFile) {
+ ResourceFolderRepository repository = findRepository(virtualFile);
+ if (repository != null) {
+ repository.getPsiListener().childRemoved(event);
+ }
+ }
+
+ @Override
+ public void childReplaced(@NotNull PsiTreeChangeEvent event) {
+ PsiFile psiFile = event.getFile();
+ if (psiFile != null) {
+ if (isRelevantFile(psiFile)) {
+ dispatchChildReplaced(event, psiFile.getVirtualFile());
+ }
+ } else {
+ PsiElement parent = event.getParent();
+ if (parent instanceof PsiDirectory) {
+ PsiDirectory directory = (PsiDirectory)parent;
+ dispatchChildReplaced(event, directory.getVirtualFile());
+ }
+ }
+ }
+
+ private void dispatchChildReplaced(@NotNull PsiTreeChangeEvent event, @Nullable VirtualFile virtualFile) {
+ ResourceFolderRepository repository = findRepository(virtualFile);
+ if (repository != null) {
+ repository.getPsiListener().childReplaced(event);
+ }
+ }
+
+ @Override
+ public void childrenChanged(@NotNull PsiTreeChangeEvent event) {
+ PsiFile psiFile = event.getFile();
+ if (psiFile != null && isRelevantFile(psiFile)) {
+ VirtualFile file = psiFile.getVirtualFile();
+ dispatchChildrenChanged(event, file);
+ }
+ }
+
+ private void dispatchChildrenChanged(@NotNull PsiTreeChangeEvent event, @Nullable VirtualFile virtualFile) {
+ ResourceFolderRepository repository = findRepository(virtualFile);
+ if (repository != null) {
+ repository.getPsiListener().childrenChanged(event);
+ }
+ }
+
+ @Override
+ public void childMoved(@NotNull PsiTreeChangeEvent event) {
+ PsiElement child = event.getChild();
+ PsiFile psiFile = event.getFile();
+ if (psiFile == null) {
+ if (child instanceof PsiFile && isRelevantFile((PsiFile)child)) {
+ VirtualFile file = ((PsiFile)child).getVirtualFile();
+ if (file != null) {
+ dispatchChildMoved(event, file);
+ return;
+ }
+
+ PsiElement oldParent = event.getOldParent();
+ if (oldParent instanceof PsiDirectory) {
+ PsiDirectory directory = (PsiDirectory)oldParent;
+ VirtualFile dir = directory.getVirtualFile();
+ dispatchChildMoved(event, dir);
+ }
+ }
+ } else {
+ // Change inside a file
+ VirtualFile file = psiFile.getVirtualFile();
+ if (file != null && isRelevantFile(file)) {
+ dispatchChildMoved(event, file);
+ }
+ }
+ }
+
+ private void dispatchChildMoved(@NotNull PsiTreeChangeEvent event, @Nullable VirtualFile virtualFile) {
+ ResourceFolderRepository repository = findRepository(virtualFile);
+ if (repository != null) {
+ repository.getPsiListener().childMoved(event);
+ }
+
+ // If you moved the file between resource directories, potentially notify that previous repository as well
+ if (event.getFile() == null) {
+ PsiElement oldParent = event.getOldParent();
+ if (oldParent instanceof PsiDirectory) {
+ PsiDirectory sourceDir = (PsiDirectory)oldParent;
+ ResourceFolderRepository targetRepository = findRepository(sourceDir.getVirtualFile());
+ if (targetRepository != null && targetRepository != repository) {
+ targetRepository.getPsiListener().childMoved(event);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void beforePropertyChange(@NotNull PsiTreeChangeEvent event) {
+ if (PsiTreeChangeEvent.PROP_FILE_NAME == event.getPropertyName()) {
+ PsiElement child = event.getChild();
+ if (child instanceof PsiFile) {
+ PsiFile psiFile = (PsiFile)child;
+ if (isRelevantFile(psiFile)) {
+ VirtualFile file = psiFile.getVirtualFile();
+ dispatchBeforePropertyChange(event, file);
+ }
+ }
+ }
+ }
+
+ private void dispatchBeforePropertyChange(@NotNull PsiTreeChangeEvent event, @Nullable VirtualFile virtualFile) {
+ ResourceFolderRepository repository = findRepository(virtualFile);
+ if (repository != null) {
+ repository.getPsiListener().beforePropertyChange(event);
+ }
+ }
+
+ @Override
+ public void propertyChanged(@NotNull PsiTreeChangeEvent event) {
+ if (PsiTreeChangeEvent.PROP_FILE_NAME == event.getPropertyName()) {
+ PsiElement child = event.getElement();
+ if (child instanceof PsiFile) {
+ PsiFile psiFile = (PsiFile)child;
+ if (isRelevantFile(psiFile)) {
+ VirtualFile file = psiFile.getVirtualFile();
+ dispatchPropertyChange(event, file);
+ }
+ }
+ }
+
+ // TODO: Do we need to handle PROP_DIRECTORY_NAME for users renaming any of the resource folders?
+ // and what about PROP_FILE_TYPES -- can users change the type of an XML File to something else?
+ }
+
+ private void dispatchPropertyChange(@NotNull PsiTreeChangeEvent event, @Nullable VirtualFile virtualFile) {
+ ResourceFolderRepository repository = findRepository(virtualFile);
+ if (repository != null) {
+ repository.getPsiListener().propertyChanged(event);
+ }
+ }
+}
+
diff --git a/android/src/com/android/tools/idea/rendering/PsiResourceFile.java b/android/src/com/android/tools/idea/rendering/PsiResourceFile.java
new file mode 100644
index 0000000..b7f5a5a
--- /dev/null
+++ b/android/src/com/android/tools/idea/rendering/PsiResourceFile.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.rendering;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.res2.ResourceFile;
+import com.android.ide.common.res2.ResourceItem;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.resources.ResourceFolderType;
+import com.google.common.base.Splitter;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiFile;
+
+import java.io.File;
+import java.util.List;
+
+class PsiResourceFile extends ResourceFile {
+ private static final File DUMMY_FILE = new File("");
+ private PsiFile myFile;
+ private String myName;
+ private ResourceFolderType myFolderType;
+ private FolderConfiguration myFolderConfiguration;
+
+ public PsiResourceFile(@NonNull PsiFile file, @NonNull ResourceItem item, @NonNull String qualifiers,
+ @NonNull ResourceFolderType folderType, @NonNull FolderConfiguration folderConfiguration) {
+ super(DUMMY_FILE, item, qualifiers);
+ myFile = file;
+ myName = file.getName();
+ myFolderType = folderType;
+ myFolderConfiguration = folderConfiguration;
+ }
+
+ public PsiResourceFile(@NonNull PsiFile file, @NonNull List<ResourceItem> items, @NonNull String qualifiers,
+ @NonNull ResourceFolderType folderType, @NonNull FolderConfiguration folderConfiguration) {
+ super(DUMMY_FILE, items, qualifiers);
+ myFile = file;
+ myName = file.getName();
+ myFolderType = folderType;
+ myFolderConfiguration = folderConfiguration;
+ }
+
+ @NonNull
+ public PsiFile getPsiFile() {
+ return myFile;
+ }
+
+ @NonNull
+ @Override
+ public File getFile() {
+ if (mFile == null || mFile == DUMMY_FILE) {
+ VirtualFile virtualFile = myFile.getVirtualFile();
+ if (virtualFile != null) {
+ mFile = VfsUtilCore.virtualToIoFile(virtualFile);
+ } else {
+ mFile = super.getFile();
+ }
+ }
+
+ return mFile;
+ }
+
+ public String getName() {
+ return myName;
+ }
+
+ ResourceFolderType getFolderType() {
+ return myFolderType;
+ }
+
+ FolderConfiguration getFolderConfiguration() {
+ return myFolderConfiguration;
+ }
+
+ public void setPsiFile(@NonNull PsiFile psiFile, String qualifiers) {
+ mFile = null;
+ myFile = psiFile;
+ setQualifiers(qualifiers);
+ myFolderConfiguration = FolderConfiguration.getConfigFromQualifiers(Splitter.on('-').split(qualifiers));
+ PsiDirectory parent = psiFile.getParent();
+ assert parent != null : psiFile;
+ myFolderType = ResourceFolderType.getFolderType(parent.getName());
+ }
+}
diff --git a/android/src/com/android/tools/idea/rendering/PsiResourceItem.java b/android/src/com/android/tools/idea/rendering/PsiResourceItem.java
new file mode 100644
index 0000000..d5bc274
--- /dev/null
+++ b/android/src/com/android/tools/idea/rendering/PsiResourceItem.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.rendering;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.rendering.api.*;
+import com.android.ide.common.res2.ResourceFile;
+import com.android.ide.common.res2.ResourceItem;
+import com.android.ide.common.res2.ValueXmlHelper;
+import com.android.ide.common.resources.configuration.DensityQualifier;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.resources.Density;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.xml.XmlElementType;
+import com.intellij.psi.xml.XmlTag;
+import com.intellij.psi.xml.XmlText;
+import com.intellij.psi.xml.XmlTokenType;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+
+import static com.android.SdkConstants.*;
+
+class PsiResourceItem extends ResourceItem {
+ private final XmlTag myTag;
+ private PsiFile myFile;
+
+ PsiResourceItem(@NonNull String name, @NonNull ResourceType type, @Nullable XmlTag tag, @NonNull PsiFile file) {
+ super(name, type, null);
+ myTag = tag;
+ myFile = file;
+ }
+
+ @Override
+ public FolderConfiguration getConfiguration() {
+ PsiResourceFile source = (PsiResourceFile)super.getSource();
+
+ // Temporary safety workaround
+ if (source == null) {
+ if (myFile != null) {
+ PsiDirectory parent = myFile.getParent();
+ if (parent != null) {
+ String name = parent.getName();
+ FolderConfiguration configuration = FolderConfiguration.getConfigForFolder(name);
+ if (configuration != null) {
+ return configuration;
+ }
+ }
+ }
+ return new FolderConfiguration();
+ }
+ return source.getFolderConfiguration();
+ }
+
+ @Nullable
+ @Override
+ public ResourceFile getSource() {
+ ResourceFile source = super.getSource();
+
+ // Temporary safety workaround
+ if (source == null && myFile != null && myFile.getParent() != null) {
+ PsiDirectory parent = myFile.getParent();
+ if (parent != null) {
+ String name = parent.getName();
+ ResourceFolderType folderType = ResourceFolderType.getFolderType(name);
+ FolderConfiguration configuration = FolderConfiguration.getConfigForFolder(name);
+ int index = name.indexOf('-');
+ String qualifiers = index == -1 ? "" : name.substring(index + 1);
+ source = new PsiResourceFile(myFile, Collections.<ResourceItem>singletonList(this), qualifiers, folderType,
+ configuration);
+ setSource(source);
+ }
+ }
+
+ return source;
+ }
+
+ @Nullable
+ @Override
+ public ResourceValue getResourceValue(boolean isFrameworks) {
+ if (mResourceValue == null) {
+ //noinspection VariableNotUsedInsideIf
+ if (myTag == null) {
+ // Density based resource value?
+ ResourceType type = getType();
+ Density density = type == ResourceType.DRAWABLE ? getFolderDensity() : null;
+ if (density != null) {
+ mResourceValue = new DensityBasedResourceValue(type, getName(), getSource().getFile().getAbsolutePath(), density, isFrameworks);
+ } else {
+ mResourceValue = new ResourceValue(type, getName(), getSource().getFile().getAbsolutePath(), isFrameworks);
+ }
+ } else {
+ mResourceValue = parseXmlToResourceValue(isFrameworks);
+ }
+ }
+
+ return mResourceValue;
+ }
+
+ @Nullable
+ private Density getFolderDensity() {
+ FolderConfiguration configuration = getConfiguration();
+ if (configuration != null) {
+ DensityQualifier densityQualifier = configuration.getDensityQualifier();
+ if (densityQualifier != null) {
+ return densityQualifier.getValue();
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ private ResourceValue parseXmlToResourceValue(boolean isFrameworks) {
+ assert myTag != null;
+
+ if (!myTag.isValid()) {
+ return null;
+ }
+
+ ResourceType type = getType();
+ String name = getName();
+
+ ResourceValue value;
+ switch (type) {
+ case STYLE:
+ String parent = getAttributeValue(myTag, ATTR_PARENT);
+ value = parseStyleValue(new StyleResourceValue(type, name, parent, isFrameworks));
+ break;
+ case DECLARE_STYLEABLE:
+ //noinspection deprecation
+ value = parseDeclareStyleable(new DeclareStyleableResourceValue(type, name, isFrameworks));
+ break;
+ case ATTR:
+ value = parseAttrValue(new AttrResourceValue(type, name, isFrameworks));
+ break;
+ case ARRAY:
+ value = parseArrayValue(new ArrayResourceValue(name, isFrameworks) {
+ // Allow the user to specify a specific element to use via tools:index
+ @Override
+ protected int getDefaultIndex() {
+ String index = myTag.getAttributeValue(ATTR_INDEX, TOOLS_URI);
+ if (index != null) {
+ return Integer.parseInt(index);
+ }
+ return super.getDefaultIndex();
+ }
+ });
+ break;
+ case PLURALS:
+ value = parsePluralsValue(new PluralsResourceValue(name, isFrameworks) {
+ // Allow the user to specify a specific quantity to use via tools:quantity
+ @Override
+ public String getValue() {
+ String quantity = myTag.getAttributeValue(ATTR_QUANTITY, TOOLS_URI);
+ if (quantity != null) {
+ String value = getValue(quantity);
+ if (value != null) {
+ return value;
+ }
+ }
+ return super.getValue();
+ }
+ });
+ break;
+ default:
+ value = parseValue(new ResourceValue(type, name, isFrameworks));
+ break;
+ }
+
+ return value;
+ }
+
+ @Nullable
+ private static String getAttributeValue(XmlTag tag, String attributeName) {
+ return tag.getAttributeValue(attributeName);
+ }
+
+ @SuppressWarnings("deprecation") // support for deprecated (but supported) API
+ @NonNull
+ private ResourceValue parseDeclareStyleable(@NonNull DeclareStyleableResourceValue declareStyleable) {
+ assert myTag != null;
+ for (XmlTag child : myTag.getSubTags()) {
+ String name = getAttributeValue(child, ATTR_NAME);
+ if (name != null) {
+ // is the attribute in the android namespace?
+ boolean isFrameworkAttr = declareStyleable.isFramework();
+ if (name.startsWith(ANDROID_NS_NAME_PREFIX)) {
+ name = name.substring(ANDROID_NS_NAME_PREFIX_LEN);
+ isFrameworkAttr = true;
+ }
+
+ AttrResourceValue attr = parseAttrValue(child, new AttrResourceValue(ResourceType.ATTR, name, isFrameworkAttr));
+ declareStyleable.addValue(attr);
+ }
+ }
+
+ return declareStyleable;
+ }
+
+ @NonNull
+ private ResourceValue parseStyleValue(@NonNull StyleResourceValue styleValue) {
+ assert myTag != null;
+ for (XmlTag child : myTag.getSubTags()) {
+ String name = getAttributeValue(child, ATTR_NAME);
+ if (name != null) {
+ // is the attribute in the android namespace?
+ boolean isFrameworkAttr = styleValue.isFramework();
+ if (name.startsWith(ANDROID_NS_NAME_PREFIX)) {
+ name = name.substring(ANDROID_NS_NAME_PREFIX_LEN);
+ isFrameworkAttr = true;
+ }
+
+ ResourceValue resValue = new ResourceValue(null, name, styleValue.isFramework());
+ resValue.setValue(ValueXmlHelper.unescapeResourceString(getTextContent(child), true, true));
+ styleValue.addValue(resValue, isFrameworkAttr);
+ }
+ }
+
+ return styleValue;
+ }
+
+ @NonNull
+ private AttrResourceValue parseAttrValue(@NonNull AttrResourceValue attrValue) {
+ assert myTag != null;
+ return parseAttrValue(myTag, attrValue);
+ }
+
+ @NonNull
+ private static AttrResourceValue parseAttrValue(@NonNull XmlTag myTag, @NonNull AttrResourceValue attrValue) {
+ for (XmlTag child : myTag.getSubTags()) {
+ String name = getAttributeValue(child, ATTR_NAME);
+ if (name != null) {
+ String value = getAttributeValue(child, ATTR_VALUE);
+ if (value != null) {
+ try {
+ // Integer.decode/parseInt can't deal with hex value > 0x7FFFFFFF so we
+ // use Long.decode instead.
+ attrValue.addValue(name, (int)(long)Long.decode(value));
+ } catch (NumberFormatException e) {
+ // pass, we'll just ignore this value
+ }
+ }
+ }
+ }
+
+ return attrValue;
+ }
+
+ private ResourceValue parseArrayValue(ArrayResourceValue arrayValue) {
+ assert myTag != null;
+ for (XmlTag child : myTag.getSubTags()) {
+ String text = ValueXmlHelper.unescapeResourceString(getTextContent(child), true, true);
+ arrayValue.addElement(text);
+ }
+
+ return arrayValue;
+ }
+
+ private ResourceValue parsePluralsValue(PluralsResourceValue value) {
+ assert myTag != null;
+ for (XmlTag child : myTag.getSubTags()) {
+ String quantity = child.getAttributeValue(ATTR_QUANTITY);
+ if (quantity != null) {
+ String text = ValueXmlHelper.unescapeResourceString(getTextContent(child), true, true);
+ value.addPlural(quantity, text);
+ }
+ }
+
+ return value;
+ }
+
+ @NonNull
+ private ResourceValue parseValue(@NonNull ResourceValue value) {
+ assert myTag != null;
+ String text = getTextContent(myTag);
+ text = ValueXmlHelper.unescapeResourceString(text, true, true);
+ value.setValue(text);
+ return value;
+ }
+
+ private static String getTextContent(@NonNull XmlTag tag) {
+ // We can't just use tag.getValue().getTrimmedText() here because we need to remove
+ // intermediate elements such as <xliff> text:
+ // TODO: Make sure I correct handle HTML content for XML items in <string> nodes!
+ // For example, for the following string we want to compute "Share with %s":
+ // <string name="share">Share with <xliff:g id="application_name" example="Bluetooth">%s</xliff:g></string>
+ XmlTag[] subTags = tag.getSubTags();
+ XmlText[] textElements = tag.getValue().getTextElements();
+ if (subTags.length == 0) {
+ if (textElements.length == 1) {
+ return getXmlTextValue(textElements[0]);
+ } else if (textElements.length == 0) {
+ return "";
+ }
+ }
+ StringBuilder sb = new StringBuilder(40);
+ appendText(sb, tag);
+ return sb.toString();
+ }
+
+ private static String getXmlTextValue(XmlText element) {
+ PsiElement current = element.getFirstChild();
+ if (current != null) {
+ if (current.getNextSibling() != null) {
+ StringBuilder sb = new StringBuilder();
+ for (; current != null; current = current.getNextSibling()) {
+ IElementType type = current.getNode().getElementType();
+ if (type == XmlElementType.XML_CDATA) {
+ PsiElement[] children = current.getChildren();
+ if (children.length == 3) { // XML_CDATA_START, XML_DATA_CHARACTERS, XML_CDATA_END
+ assert children[1].getNode().getElementType() == XmlTokenType.XML_DATA_CHARACTERS;
+ sb.append(children[1].getText());
+ }
+ continue;
+ }
+ sb.append(current.getText());
+ }
+ return sb.toString();
+ } else if (current.getNode().getElementType() == XmlElementType.XML_CDATA) {
+ PsiElement[] children = current.getChildren();
+ if (children.length == 3) { // XML_CDATA_START, XML_DATA_CHARACTERS, XML_CDATA_END
+ assert children[1].getNode().getElementType() == XmlTokenType.XML_DATA_CHARACTERS;
+ return children[1].getText();
+ }
+ }
+ }
+
+ return element.getText();
+ }
+
+ private static void appendText(@NonNull StringBuilder sb, @NonNull XmlTag tag) {
+ PsiElement[] children = tag.getChildren();
+ for (PsiElement child : children) {
+ if (child instanceof XmlText) {
+ XmlText text = (XmlText)child;
+ sb.append(getXmlTextValue(text));
+ } else if (child instanceof XmlTag) {
+ appendText(sb, (XmlTag)child);
+ }
+ }
+ }
+
+ @NonNull
+ PsiFile getPsiFile() {
+ return myFile;
+ }
+
+ /** Clears the cached value, if any, and returns true if the value was cleared */
+ public boolean recomputeValue() {
+ if (mResourceValue != null) {
+ // Force recompute in getResourceValue
+ mResourceValue = null;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Nullable
+ XmlTag getTag() {
+ return myTag;
+ }
+}
diff --git a/android/src/com/android/tools/idea/rendering/RenderErrorPanel.java b/android/src/com/android/tools/idea/rendering/RenderErrorPanel.java
index 36666a1..0571991 100644
--- a/android/src/com/android/tools/idea/rendering/RenderErrorPanel.java
+++ b/android/src/com/android/tools/idea/rendering/RenderErrorPanel.java
@@ -16,12 +16,16 @@
package com.android.tools.idea.rendering;
+import com.android.ide.common.rendering.api.LayoutLog;
import com.android.ide.common.resources.ResourceResolver;
import com.android.resources.Density;
+import com.android.sdklib.IAndroidTarget;
import com.android.tools.idea.configurations.RenderContext;
+import com.android.utils.HtmlBuilder;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.intellij.compiler.impl.javaCompiler.javac.JavacConfiguration;
+import com.intellij.icons.AllIcons;
import com.intellij.ide.DataManager;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.actionSystem.DataContext;
@@ -55,9 +59,13 @@
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.util.containers.HashSet;
import com.intellij.util.ui.UIUtil;
+import org.jetbrains.android.dom.attrs.AttributeDefinition;
+import org.jetbrains.android.dom.attrs.AttributeDefinitions;
+import org.jetbrains.android.dom.attrs.AttributeFormat;
import org.jetbrains.android.sdk.AndroidPlatform;
import org.jetbrains.android.sdk.AndroidSdkAdditionalData;
import org.jetbrains.android.sdk.AndroidSdkType;
+import org.jetbrains.android.sdk.AndroidTargetData;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.model.java.compiler.JpsJavaCompilerOptions;
@@ -74,6 +82,9 @@
import java.awt.*;
import java.io.IOException;
import java.io.StringReader;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
@@ -83,6 +94,7 @@
import static com.android.ide.common.rendering.api.LayoutLog.TAG_RESOURCES_PREFIX;
import static com.android.ide.common.rendering.api.LayoutLog.TAG_RESOURCES_RESOLVE_THEME_ATTR;
import static com.android.tools.idea.configurations.RenderContext.UsageType.LAYOUT_EDITOR;
+import static com.android.tools.idea.rendering.HtmlLinkManager.URL_ACTION_CLOSE;
import static com.android.tools.idea.rendering.ResourceHelper.viewNeedsPackage;
import static com.android.tools.lint.detector.api.LintUtils.editDistance;
import static com.android.tools.lint.detector.api.LintUtils.stripIdPrefix;
@@ -97,6 +109,8 @@
public class RenderErrorPanel extends JPanel {
public static final boolean SIZE_ERROR_PANEL_DYNAMICALLY = true;
private static final int ERROR_PANEL_OPACITY = UIUtil.isUnderDarcula() ? 224 : 208; // out of 255
+ /** Class of the render session implementation class; for render errors, we cut off stack dumps at this frame */
+ private static final String RENDER_SESSION_IMPL_FQCN = "com.android.layoutlib.bridge.impl.RenderSessionImpl";
private JEditorPane myHTMLViewer;
private final HyperlinkListener myHyperLinkListener;
@@ -161,13 +175,17 @@
return;
}
- String ref = e.getDescription();
+ String url = e.getDescription();
+ if (url.equals(URL_ACTION_CLOSE)) {
+ close();
+ return;
+ }
Module module = myResult.getModule();
PsiFile file = myResult.getFile();
DataContext dataContext = DataManager.getInstance().getDataContext(RenderErrorPanel.this);
assert dataContext != null;
- myLinkManager.handleUrl(ref, module, file, dataContext, myResult);
+ myLinkManager.handleUrl(url, module, file, dataContext, myResult);
}
}
};
@@ -180,6 +198,10 @@
add(myScrollPane, BorderLayout.CENTER);
}
+ private void close() {
+ this.setVisible(false);
+ }
+
private void setupStyle() {
// Make the scrollPane transparent
if (myScrollPane != null) {
@@ -223,7 +245,16 @@
assert logger.hasProblems();
HtmlBuilder builder = new HtmlBuilder(new StringBuilder(300));
- builder.addHeading("Rendering Problems").newline();
+ builder.openHtmlBody();
+
+ // Construct close button. Sadly <img align="right"> doesn't work in JEditorPanes; would
+ // have looked a lot nicer with the image flushed to the right!
+ builder.addHtml("<A HREF=\"");
+ builder.addHtml(URL_ACTION_CLOSE);
+ builder.addHtml("\">");
+ builder.addIcon(HtmlBuilderHelper.getCloseIconPath());
+ builder.addHtml("</A>");
+ builder.addHeading("Rendering Problems", HtmlBuilderHelper.getHeaderFontColor()).newline();
reportMissingStyles(logger, builder);
if (renderService != null) {
@@ -240,7 +271,9 @@
reportRenderingFidelityProblems(logger, builder, renderService);
}
- return "<HTML><BODY>" + builder.getHtml() + "</BODY></HTML>";
+ builder.closeHtmlBody();
+
+ return builder.getHtml();
}
private void reportMissingClasses(@NotNull RenderLogger logger, @NotNull HtmlBuilder builder, @NotNull RenderService renderService) {
@@ -309,7 +342,7 @@
}
builder.endList();
- builder.addTipIcon();
+ builder.addIcon(HtmlBuilderHelper.getTipIconPath());
builder.addLink("Tip: Try to ", "build", " the project", myLinkManager.createCompileModuleUrl());
builder.newline().newline();
}
@@ -508,14 +541,14 @@
}
builder.endList();
- builder.addTipIcon();
+ builder.addIcon(HtmlBuilderHelper.getTipIconPath());
builder.addLink("Tip: Use ", "View.isInEditMode()", " in your custom views to skip code or show sample data when shown in the IDE",
"http://developer.android.com/reference/android/view/View.html#isInEditMode()");
if (firstThrowable != null) {
builder.newline().newline();
- builder.addHeading("Exception Details").newline();
- reportThrowable(builder, firstThrowable);
+ builder.addHeading("Exception Details", HtmlBuilderHelper.getHeaderFontColor()).newline();
+ reportThrowable(builder, firstThrowable, false);
}
builder.newline().newline();
}
@@ -548,6 +581,7 @@
count++;
// Only display the first 3 render fidelity issues
if (count == 3) {
+ @SuppressWarnings("ConstantConditions")
int remaining = fidelityWarnings.size() - count;
if (remaining > 0) {
builder.add("(").addHtml(Integer.toString(remaining)).add(" additional render fidelity issues hidden)");
@@ -573,7 +607,7 @@
private static void reportMissingStyles(RenderLogger logger, HtmlBuilder builder) {
if (logger.seenTagPrefix(TAG_RESOURCES_RESOLVE_THEME_ATTR)) {
builder.addBold("Missing styles. Is the correct theme chosen for this layout?").newline();
- builder.addTipIcon();
+ builder.addIcon(HtmlBuilderHelper.getTipIconPath());
builder.add("Use the Theme combo box above the layout to choose a different layout, or fix the theme style references.");
builder.newline().newline();
}
@@ -689,19 +723,24 @@
HighlightSeverity severity = message.getSeverity();
if (severity == HighlightSeverity.ERROR) {
- builder.addErrorIcon();
+ builder.addIcon(HtmlBuilderHelper.getErrorIconPath());
} else if (severity == HighlightSeverity.WARNING) {
- builder.addWarningIcon();
+ builder.addIcon(HtmlBuilderHelper.getWarningIconPath());
}
- message.appendHtml(builder.getStringBuilder());
+ String html = message.getHtml();
+ builder.getStringBuilder().append(html);
Throwable throwable = message.getThrowable();
if (throwable != null) {
- reportThrowable(builder, throwable);
+ reportThrowable(builder, throwable, !html.isEmpty());
}
if (tag != null) {
+ if (LayoutLog.TAG_RESOURCES_FORMAT.equals(tag)) {
+ appendFlagValueSuggestions(builder, message);
+ }
+
int count = logger.getTagCount(tag);
if (count > 1) {
builder.add(" (").addHtml(Integer.toString(count)).add(" similar errors not shown)");
@@ -713,8 +752,61 @@
}
}
+ private void appendFlagValueSuggestions(HtmlBuilder builder, RenderProblem message) {
+ Object clientData = message.getClientData();
+ if (!(clientData instanceof String[])) {
+ return;
+ }
+ String[] strings = (String[])clientData;
+ if (strings.length != 2) {
+ return;
+ }
+
+ RenderService renderService = myResult.getRenderService();
+ if (renderService == null) {
+ return;
+ }
+ IAndroidTarget target = renderService.getConfiguration().getTarget();
+ if (target == null) {
+ return;
+ }
+ AndroidPlatform platform = renderService.getPlatform();
+ if (platform == null) {
+ return;
+ }
+ AndroidTargetData targetData = platform.getSdkData().getTargetData(target);
+ AttributeDefinitions definitionLookup = targetData.getAttrDefs(myResult.getFile().getProject());
+ final String attributeName = strings[0];
+ final String currentValue = strings[1];
+ if (definitionLookup == null) {
+ return;
+ }
+ AttributeDefinition definition = definitionLookup.getAttrDefByName(attributeName);
+ if (definition == null) {
+ return;
+ }
+ Set<AttributeFormat> formats = definition.getFormats();
+ if (formats.contains(AttributeFormat.Flag) || formats.contains(AttributeFormat.Enum)) {
+ String[] values = definition.getValues();
+ if (values.length > 0) {
+ builder.newline();
+ builder.addNbsps(4);
+ builder.add("Change ").add(currentValue).add(" to: ");
+ boolean first = true;
+ for (String value : values) {
+ if (first) {
+ first = false;
+ } else {
+ builder.add(", ");
+ }
+ builder.addLink(value, myLinkManager.createReplaceAttributeValueUrl(attributeName, currentValue, value));
+ }
+ }
+ }
+ }
+
/** Display the problem list encountered during a render */
- private void reportThrowable(@NotNull HtmlBuilder builder, @NotNull Throwable throwable) {
+ private void reportThrowable(@NotNull HtmlBuilder builder, @NotNull Throwable throwable, boolean hideIfIrrelevant) {
StackTraceElement[] frames = throwable.getStackTrace();
int end = -1;
boolean haveInterestingFrame = false;
@@ -724,7 +816,7 @@
haveInterestingFrame = true;
}
String className = frame.getClassName();
- if (className.equals("com.android.layoutlib.bridge.impl.RenderSessionImpl")) { //$NON-NLS-1$
+ if (className.equals(RENDER_SESSION_IMPL_FQCN)) {
end = i;
break;
}
@@ -732,7 +824,29 @@
if (end == -1 || !haveInterestingFrame) {
// Not a recognized stack trace range: just skip it
- return;
+ if (hideIfIrrelevant) {
+ return;
+ } else {
+ // List just the top frames
+ for (int i = 0; i < frames.length; i++) {
+ StackTraceElement frame = frames[i];
+ if (!isVisible(frame)) {
+ end = i;
+ if (end == 0) {
+ // Find end instead
+ for (int j = 0; j < frames.length; j++) {
+ frame = frames[j];
+ String className = frame.getClassName();
+ if (className.equals(RENDER_SESSION_IMPL_FQCN)) {
+ end = j;
+ break;
+ }
+ }
+ }
+ break;
+ }
+ }
+ }
}
builder.add(throwable.toString()).newline();
@@ -787,6 +901,14 @@
|| className.startsWith("sun.")); //$NON-NLS-1$
}
+ private static boolean isVisible(StackTraceElement frame) {
+ String className = frame.getClassName();
+ return !(className.startsWith("android.") //$NON-NLS-1$
+ || className.startsWith("java.") //$NON-NLS-1$
+ || className.startsWith("javax.") //$NON-NLS-1$
+ || className.startsWith("sun.")); //$NON-NLS-1$
+ }
+
private void reportMissingSize(@NotNull HtmlBuilder builder,
@NotNull RenderLogger logger,
@NotNull String fill,
@@ -858,9 +980,8 @@
// Code copied from the old RenderUtil
private static void askAndRebuild(Project project) {
- final int r = Messages.showYesNoDialog(project,
- "You have to rebuild project to see the fixed preview. Would you like to do it?", "Rebuild Project",
- Messages.getQuestionIcon());
+ final int r = Messages.showYesNoDialog(project, "You have to rebuild project to see the fixed preview. Would you like to do it?",
+ "Rebuild Project", Messages.getQuestionIcon());
if (r == Messages.YES) {
CompilerManager.getInstance(project).rebuild(null);
}
@@ -973,5 +1094,49 @@
}
}
}
+
+ private static class HtmlBuilderHelper {
+ @Nullable
+ private static String getIconPath(String relative) {
+ // TODO: Find a way to do this more efficiently; not referencing assets but the corresponding
+ // AllIcons constants, and loading them into HTML class loader contexts?
+ URL resource = AllIcons.class.getClassLoader().getResource(relative);
+ try {
+ return (resource != null) ? resource.toURI().toURL().toExternalForm() : null;
+ }
+ catch (MalformedURLException e) {
+ return null;
+ }
+ catch (URISyntaxException e) {
+ return null;
+ }
+ }
+
+ @Nullable
+ public static String getCloseIconPath() {
+ return getIconPath("/actions/closeNew.png");
+ }
+
+ @Nullable
+ public static String getTipIconPath() {
+ return getIconPath("/actions/createFromUsage.png");
+ }
+
+ @Nullable
+ public static String getWarningIconPath() {
+ return getIconPath("/actions/warning.png");
+ }
+
+ @Nullable
+ public static String getErrorIconPath() {
+ return getIconPath("/actions/error.png");
+ }
+
+ public static String getHeaderFontColor() {
+ // See om.intellij.codeInspection.HtmlComposer.appendHeading
+ // (which operates on StringBuffers)
+ return UIUtil.isUnderDarcula() ? "#A5C25C" : "#005555";
+ }
+ }
}
diff --git a/android/src/com/android/tools/idea/rendering/RenderLogger.java b/android/src/com/android/tools/idea/rendering/RenderLogger.java
index e003ad7..aef41b3 100644
--- a/android/src/com/android/tools/idea/rendering/RenderLogger.java
+++ b/android/src/com/android/tools/idea/rendering/RenderLogger.java
@@ -16,11 +16,13 @@
package com.android.tools.idea.rendering;
import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.utils.HtmlBuilder;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
+import org.jetbrains.android.util.AndroidCommonUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -166,6 +168,25 @@
if (description.equals(throwable.getLocalizedMessage()) || description.equals(throwable.getMessage())) {
description = "Exception raised during rendering: " + description;
+ } else if (message == null) {
+ StackTraceElement[] stackTrace = throwable.getStackTrace();
+ if (stackTrace.length >= 2 &&
+ stackTrace[0].getClassName().equals("android.text.format.DateUtils") &&
+ stackTrace[1].getClassName().equals("android.widget.CalendarView")) {
+ RenderProblem.Html problem = RenderProblem.create(WARNING);
+ problem.throwable(throwable);
+ HtmlBuilder builder = problem.getHtmlBuilder();
+ builder.add("<CalendarView> and <DatePicker> are broken in this version of the rendering library. " +
+ "Try updating your SDK in the SDK Manager when issue 59732 is fixed.");
+ builder.add(" (");
+ builder.addLink("Open Issue 59732", "http://b.android.com/59732");
+ builder.add(", ");
+ ShowExceptionFix detailsFix = new ShowExceptionFix(getModule().getProject(), throwable);
+ builder.addLink("Show Exception", getLinkManager().createRunnableLink(detailsFix));
+ builder.add(")");
+ addMessage(problem);
+ return;
+ }
}
recordThrowable(throwable);
@@ -214,7 +235,10 @@
addTag(tag);
RenderProblem.Html problem = RenderProblem.create(WARNING);
problem.tag(tag);
- String url = getLinkManager().createEditAttributeUrl(matcher.group(2), matcher.group(1));
+ String attribute = matcher.group(2);
+ String value = matcher.group(1);
+ problem.setClientData(new String[]{attribute, value});
+ String url = getLinkManager().createEditAttributeUrl(attribute, value);
problem.getHtmlBuilder().add(description).add(" (").addLink("Edit", url).add(")");
addMessage(problem);
return;
@@ -227,8 +251,12 @@
addTag(tag);
RenderProblem.Html problem = RenderProblem.create(WARNING);
problem.tag(tag);
- String url = getLinkManager().createEditAttributeUrl(matcher.group(2), matcher.group(1));
+ String attribute = matcher.group(2);
+ String value = matcher.group(1);
+ problem.setClientData(new String[]{attribute, value});
+ String url = getLinkManager().createEditAttributeUrl(attribute, value);
problem.getHtmlBuilder().add(description).add(" (").addLink("Edit", url).add(")");
+ problem.setClientData(url);
addMessage(problem);
return;
}
diff --git a/android/src/com/android/tools/idea/rendering/RenderProblem.java b/android/src/com/android/tools/idea/rendering/RenderProblem.java
index f46084d..7e68b9a 100644
--- a/android/src/com/android/tools/idea/rendering/RenderProblem.java
+++ b/android/src/com/android/tools/idea/rendering/RenderProblem.java
@@ -15,6 +15,7 @@
*/
package com.android.tools.idea.rendering;
+import com.android.utils.HtmlBuilder;
import com.android.utils.XmlUtils;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.project.Project;
diff --git a/android/src/com/android/tools/idea/rendering/RenderResult.java b/android/src/com/android/tools/idea/rendering/RenderResult.java
index 2d0fe1c..3befe34 100644
--- a/android/src/com/android/tools/idea/rendering/RenderResult.java
+++ b/android/src/com/android/tools/idea/rendering/RenderResult.java
@@ -17,8 +17,6 @@
import com.android.ide.common.rendering.api.RenderSession;
import com.android.ide.common.rendering.api.ViewInfo;
-import com.android.sdklib.devices.Device;
-import com.android.sdklib.devices.State;
import com.android.tools.idea.configurations.Configuration;
import com.intellij.openapi.module.Module;
import com.intellij.psi.PsiFile;
diff --git a/android/src/com/android/tools/idea/rendering/RenderService.java b/android/src/com/android/tools/idea/rendering/RenderService.java
index 4d18603..b474a60 100644
--- a/android/src/com/android/tools/idea/rendering/RenderService.java
+++ b/android/src/com/android/tools/idea/rendering/RenderService.java
@@ -118,6 +118,9 @@
@Nullable
private RenderContext myRenderContext;
+ @NotNull
+ private final Locale myLocale;
+
/**
* Creates a new {@link RenderService} associated with the given editor.
*
@@ -217,6 +220,7 @@
Pair<Integer, Integer> sdkVersions = getSdkVersions(myFacet);
myMinSdkVersion = sdkVersions.getFirst();
myTargetSdkVersion = sdkVersions.getSecond();
+ myLocale = configuration.getLocale();
}
@Nullable
@@ -279,7 +283,7 @@
}
candidate = AndroidUtils.getIntAttrValue(usesSdkTag, ATTR_TARGET_SDK_VERSION);
if (candidate >= 0) {
- minSdkVersion = candidate;
+ targetSdkVersion = candidate;
}
}
}
@@ -326,11 +330,17 @@
* @param renderingMode the rendering mode to be used
* @return this (such that chains of setters can be stringed together)
*/
- public RenderService setRenderingMode(RenderingMode renderingMode) {
+ public RenderService setRenderingMode(@NotNull RenderingMode renderingMode) {
myRenderingMode = renderingMode;
return this;
}
+ /** Returns the {@link RenderingMode} to be used */
+ @NotNull
+ public RenderingMode getRenderingMode() {
+ return myRenderingMode;
+ }
+
public RenderService setTimeout(long timeout) {
myTimeout = timeout;
return this;
@@ -479,11 +489,18 @@
// same session
params.setExtendedViewInfoMode(true);
+ params.setLocale(myLocale.toLocaleId());
+
+ ManifestInfo manifestInfo = ManifestInfo.get(myModule);
+ try {
+ params.setRtlSupport(manifestInfo.isRtlSupported());
+ } catch (Exception e) {
+ // ignore.
+ }
if (!myShowDecorations) {
params.setForceNoDecor();
}
else {
- ManifestInfo manifestInfo = ManifestInfo.get(myModule);
try {
params.setAppLabel(manifestInfo.getApplicationLabel());
params.setAppIcon(manifestInfo.getApplicationIcon());
@@ -517,7 +534,13 @@
RenderSession session = null;
while (retries < 10) {
session = myLayoutLib.createSession(params);
- if (session.getResult().getStatus() != Result.Status.ERROR_TIMEOUT) {
+ Result result = session.getResult();
+ if (result.getStatus() != Result.Status.ERROR_TIMEOUT) {
+ // Sometimes happens at startup; treat it as a timeout; typically a retry fixes it
+ if (!result.isSuccess() && "The main Looper has already been prepared.".equals(result.getErrorMessage())) {
+ retries++;
+ continue;
+ }
break;
}
retries++;
@@ -653,4 +676,52 @@
}
return false;
}
-}
\ No newline at end of file
+
+ @Nullable
+ public static LayoutLibrary getLayoutLibrary(@Nullable final Module module, @Nullable IAndroidTarget target) {
+ if (module == null || target == null) {
+ return null;
+ }
+ Project project = module.getProject();
+ AndroidPlatform platform = getPlatform(module);
+ if (platform != null) {
+ try {
+ RenderServiceFactory factory = platform.getSdkData().getTargetData(target).getRenderServiceFactory(project);
+ if (factory != null) {
+ return factory.getLibrary();
+ }
+ }
+ catch (RenderingException e) {
+ // Ignore.
+ }
+ catch (IOException e) {
+ // Ditto
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Notifies the render service that it is being used in design mode for this layout.
+ * For example, that means that when rendering a ScrollView, it should measure the necessary
+ * vertical space, and size the layout according to the needs rather than the available
+ * device size.
+ * <p>
+ * We don't want to do this when for example offering thumbnail previews of the various
+ * layouts.
+ *
+ * @param rootTag the tag, if any
+ */
+ public void useDesignMode(@Nullable XmlTag rootTag) {
+ if (rootTag != null) {
+ String tagName = rootTag.getName();
+ if (SCROLL_VIEW.equals(tagName)) {
+ setRenderingMode(RenderingMode.V_SCROLL);
+ setDecorations(false);
+ } else if (HORIZONTAL_SCROLL_VIEW.equals(tagName)) {
+ setRenderingMode(RenderingMode.H_SCROLL);
+ setDecorations(false);
+ }
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/rendering/RenderedViewHierarchy.java b/android/src/com/android/tools/idea/rendering/RenderedViewHierarchy.java
index b72d10f..016b68d 100644
--- a/android/src/com/android/tools/idea/rendering/RenderedViewHierarchy.java
+++ b/android/src/com/android/tools/idea/rendering/RenderedViewHierarchy.java
@@ -39,6 +39,10 @@
return new RenderedViewHierarchy(file, convert(null, roots, 0, 0));
}
+ public List<RenderedView> getRoots() {
+ return myRoots;
+ }
+
@NotNull
private static List<RenderedView> convert(@Nullable RenderedView parent, @NotNull List<ViewInfo> roots, int parentX, int parentY) {
List<RenderedView> views = new ArrayList<RenderedView>(roots.size());
diff --git a/android/src/com/android/tools/idea/rendering/ResourceFolderRegistry.java b/android/src/com/android/tools/idea/rendering/ResourceFolderRegistry.java
new file mode 100644
index 0000000..5a32a65
--- /dev/null
+++ b/android/src/com/android/tools/idea/rendering/ResourceFolderRegistry.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.rendering;
+
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class ResourceFolderRegistry {
+ private final static Map<VirtualFile, ResourceFolderRepository> ourDirMap = new HashMap<VirtualFile, ResourceFolderRepository>();
+
+ @NotNull
+ public static ResourceFolderRepository get(@NotNull final AndroidFacet facet, @NotNull VirtualFile dir) {
+ ResourceFolderRepository repository = ourDirMap.get(dir);
+ if (repository == null) {
+ repository = ResourceFolderRepository.create(facet, dir);
+ PsiProjectListener.addRoot(facet.getModule().getProject(), dir, repository);
+
+ ourDirMap.put(dir, repository);
+ }
+
+ return repository;
+ }
+}
diff --git a/android/src/com/android/tools/idea/rendering/ResourceFolderRepository.java b/android/src/com/android/tools/idea/rendering/ResourceFolderRepository.java
new file mode 100644
index 0000000..cfb9c5e
--- /dev/null
+++ b/android/src/com/android/tools/idea/rendering/ResourceFolderRepository.java
@@ -0,0 +1,1532 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.rendering;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.res2.ResourceItem;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.resources.FolderTypeRelationship;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.android.tools.lint.detector.api.LintUtils;
+import com.google.common.collect.*;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.fileTypes.StdFileTypes;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.*;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.psi.xml.*;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+
+import static com.android.SdkConstants.*;
+import static com.android.resources.ResourceFolderType.*;
+import static com.android.tools.idea.rendering.PsiProjectListener.isRelevantFile;
+import static com.android.tools.idea.rendering.PsiProjectListener.isRelevantFileType;
+import static com.android.tools.lint.detector.api.LintUtils.stripIdPrefix;
+
+/**
+ * Remaining work:
+ * <ul>
+ * <li>Find some way to have event updates in this resource folder directly update parent repositories
+ * (typically {@link ModuleResourceRepository}</li>
+ * <li>consider *initializing* this repository initially from IO files to not force full modelling of
+ * XML objects for all these tiny files (translations etc) ? Or find some way to persist the data in the index.</li>
+ * <li>Add defensive checks for non-read permission reads of resource values</li>
+ * <li>Idea: For {@link #rescan}; compare the removed items from the added items, and if they're the same, avoid
+ * creating a new generation.</li>
+ * <li>Register the psi project listener as a project service instead</li>
+ * </ul>
+ */
+public final class ResourceFolderRepository extends ProjectResources {
+ private static final Logger LOG = Logger.getInstance(ResourceFolderRepository.class);
+ private final Module myModule;
+ private final AndroidFacet myFacet;
+ private final PsiListener myListener;
+ private final VirtualFile myResourceDir;
+ private final Map<ResourceType, ListMultimap<String, ResourceItem>> myItems = Maps.newEnumMap(ResourceType.class);
+ private final Map<PsiFile, PsiResourceFile> myResourceFiles = Maps.newHashMap();
+ private final Object SCAN_LOCK = new Object();
+ private Set<PsiFile> myPendingScans;
+
+ @VisibleForTesting
+ static int ourFullRescans;
+
+ private ResourceFolderRepository(@NotNull AndroidFacet facet, @NotNull VirtualFile resourceDir) {
+ super(resourceDir.getName());
+ myFacet = facet;
+ myModule = facet.getModule();
+ myListener = new PsiListener();
+ myResourceDir = resourceDir;
+ scan();
+ }
+
+ VirtualFile getResourceDir() {
+ return myResourceDir;
+ }
+
+ /** NOTE: You should normally use {@link ResourceFolderRegistry#get} rather than this method. */
+ @NotNull
+ static ResourceFolderRepository create(@NotNull final AndroidFacet facet, @NotNull VirtualFile dir) {
+ return new ResourceFolderRepository(facet, dir);
+ }
+
+ private void scan() {
+ ApplicationManager.getApplication().runReadAction(new Runnable() {
+ @Override
+ public void run() {
+ PsiManager manager = PsiManager.getInstance(myFacet.getModule().getProject());
+ PsiDirectory directory = manager.findDirectory(myResourceDir);
+ if (directory != null) {
+ scanResFolder(directory);
+ }
+ }
+ });
+ }
+
+ @Nullable
+ private PsiFile ensureValid(@NotNull PsiFile psiFile) {
+ if (psiFile. isValid()) {
+ return psiFile;
+ } else {
+ VirtualFile virtualFile = psiFile.getVirtualFile();
+ if (virtualFile != null) {
+ return PsiManager.getInstance(myModule.getProject()).findFile(virtualFile);
+ }
+ }
+
+ return null;
+ }
+
+ private void scanResFolder(@NotNull PsiDirectory res) {
+ for (PsiDirectory dir : res.getSubdirectories()) {
+ String name = dir.getName();
+ ResourceFolderType folderType = ResourceFolderType.getFolderType(name);
+ if (folderType != null) {
+ String qualifiers = getQualifiers(name);
+ FolderConfiguration folderConfiguration = FolderConfiguration.getConfigForFolder(name);
+ if (folderConfiguration == null) {
+ continue;
+ }
+ if (folderType == VALUES) {
+ scanValueResFolder(dir, qualifiers, folderConfiguration);
+ } else {
+ scanFileResourceFolder(dir, folderType, qualifiers, folderConfiguration);
+ }
+ }
+ }
+ }
+
+ private static String getQualifiers(String dirName) {
+ int index = dirName.indexOf('-');
+ return index != -1 ? dirName.substring(index + 1) : "";
+ }
+
+ private void scanFileResourceFolder(@NotNull PsiDirectory directory, ResourceFolderType folderType, String qualifiers,
+ FolderConfiguration folderConfiguration) {
+ List<ResourceType> resourceTypes = FolderTypeRelationship.getRelatedResourceTypes(folderType);
+ assert resourceTypes.size() >= 1 : folderType;
+ ResourceType type = resourceTypes.get(0);
+
+ boolean idGenerating = resourceTypes.size() > 1;
+ assert !idGenerating || resourceTypes.size() == 2 && resourceTypes.get(1) == ResourceType.ID;
+
+ ListMultimap<String, ResourceItem> map = myItems.get(type);
+ if (map == null) {
+ map = ArrayListMultimap.create();
+ myItems.put(type, map);
+ }
+
+ for (PsiFile file : directory.getFiles()) {
+ FileType fileType = file.getFileType();
+ if (isRelevantFileType(fileType) || folderType == ResourceFolderType.RAW) {
+ scanFileResourceFile(qualifiers, folderType, folderConfiguration, type, idGenerating, map, file);
+
+ } // TODO: Else warn about files that aren't expected to be found here?
+ }
+ }
+
+ private void scanFileResourceFile(String qualifiers,
+ ResourceFolderType folderType,
+ FolderConfiguration folderConfiguration,
+ ResourceType type,
+ boolean idGenerating,
+ ListMultimap<String, ResourceItem> map,
+ PsiFile file) {
+ // XML or Image
+ String name = ResourceHelper.getResourceName(file);
+ ResourceItem item = new PsiResourceItem(name, type, null, file);
+
+ if (idGenerating) {
+ List<ResourceItem> items = Lists.newArrayList();
+ items.add(item);
+ map.put(name, item);
+ addIds(items, file);
+
+ PsiResourceFile resourceFile = new PsiResourceFile(file, items, qualifiers, folderType, folderConfiguration);
+ myResourceFiles.put(file, resourceFile);
+ } else {
+ PsiResourceFile resourceFile = new PsiResourceFile(file, item, qualifiers, folderType, folderConfiguration);
+ myResourceFiles.put(file, resourceFile);
+ map.put(name, item);
+ }
+ }
+
+ @NonNull
+ @Override
+ protected Map<ResourceType, ListMultimap<String, ResourceItem>> getMap() {
+ return myItems;
+ }
+
+ @Nullable
+ @Override
+ protected ListMultimap<String, ResourceItem> getMap(ResourceType type, boolean create) {
+ ListMultimap<String, ResourceItem> multimap = myItems.get(type);
+ if (multimap == null && create) {
+ multimap = ArrayListMultimap.create();
+ myItems.put(type, multimap);
+ }
+ return multimap;
+ }
+
+ @Override
+ public void clear() {
+ super.clear();
+ myResourceFiles.clear();
+ }
+
+ private void addIds(List<ResourceItem> items, PsiFile file) {
+ addIds(items, file, file);
+ }
+
+ private void addIds(List<ResourceItem> items, PsiElement element, PsiFile file) {
+ Collection<XmlTag> xmlTags = PsiTreeUtil.findChildrenOfType(element, XmlTag.class);
+ if (element instanceof XmlTag) {
+ addId(items, file, (XmlTag)element);
+ }
+ if (!xmlTags.isEmpty()) {
+ for (XmlTag tag : xmlTags) {
+ addId(items, file, tag);
+ }
+ }
+ }
+
+ private void addId(List<ResourceItem> items, PsiFile file, XmlTag tag) {
+ assert tag.isValid();
+ String id = tag.getAttributeValue(ATTR_ID, ANDROID_URI);
+ if (id != null && id.startsWith(NEW_ID_PREFIX)) {
+ String name = id.substring(NEW_ID_PREFIX.length());
+ PsiResourceItem item = new PsiResourceItem(name, ResourceType.ID, null, file);
+ items.add(item);
+
+ ListMultimap<String, ResourceItem> map = myItems.get(ResourceType.ID);
+ if (map == null) {
+ map = ArrayListMultimap.create();
+ myItems.put(ResourceType.ID, map);
+ }
+ map.put(name, item);
+ }
+ }
+
+ private void scanValueResFolder(@NotNull PsiDirectory directory, String qualifiers, FolderConfiguration folderConfiguration) {
+ //noinspection ConstantConditions
+ assert directory.getName().startsWith(FD_RES_VALUES);
+
+ for (PsiFile file : directory.getFiles()) {
+ scanValueFile(qualifiers, file, folderConfiguration);
+ }
+ }
+
+ private boolean scanValueFile(String qualifiers, PsiFile file, FolderConfiguration folderConfiguration) {
+ boolean added = false;
+ FileType fileType = file.getFileType();
+ if (fileType == StdFileTypes.XML) {
+ XmlFile xmlFile = (XmlFile)file;
+ assert xmlFile.isValid();
+ XmlDocument document = xmlFile.getDocument();
+ if (document != null) {
+ XmlTag root = document.getRootTag();
+ if (root == null) {
+ return false;
+ }
+ if (!root.getName().equals(TAG_RESOURCES)) {
+ return false;
+ }
+ XmlTag[] subTags = root.getSubTags(); // Not recursive, right?
+ List<ResourceItem> items = Lists.newArrayListWithExpectedSize(subTags.length);
+ for (XmlTag tag : subTags) {
+ String name = tag.getAttributeValue(ATTR_NAME);
+ if (name != null) {
+ ResourceType type = getType(tag);
+ if (type != null) {
+ ListMultimap<String, ResourceItem> map = myItems.get(type);
+ if (map == null) {
+ map = ArrayListMultimap.create();
+ myItems.put(type, map);
+ }
+
+ ResourceItem item = new PsiResourceItem(name, type, tag, file);
+ map.put(name, item);
+ items.add(item);
+ added = true;
+
+ if (type == ResourceType.DECLARE_STYLEABLE) {
+ // for declare styleables we also need to create attr items for its children
+ XmlTag[] attrs = tag.getSubTags();
+ if (attrs.length > 0) {
+ map = myItems.get(ResourceType.ATTR);
+ if (map == null) {
+ map = ArrayListMultimap.create();
+ myItems.put(ResourceType.ATTR, map);
+ }
+
+ for (XmlTag child : attrs) {
+ String attrName = child.getAttributeValue(ATTR_NAME);
+ if (attrName != null && !attrName.startsWith(ANDROID_NS_NAME_PREFIX)
+ // Only add attr nodes for elements that specify a format; otherwise
+ // it's just a reference to an existing attr
+ && child.getAttribute(ATTR_FORMAT) != null) {
+ ResourceItem attrItem = new PsiResourceItem(attrName, ResourceType.ATTR, child, file);
+ items.add(attrItem);
+ map.put(attrName, attrItem);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (items != null) {
+ PsiResourceFile resourceFile = new PsiResourceFile(file, items, qualifiers, ResourceFolderType.VALUES, folderConfiguration);
+ myResourceFiles.put(file, resourceFile);
+ }
+ }
+ }
+
+ return added;
+ }
+
+ /**
+ * Returns the type of the ResourceItem based on a node's attributes.
+ * @param node the node
+ * @return the ResourceType or null if it could not be inferred.
+ */
+ @Nullable
+ private static ResourceType getType(XmlTag node) {
+ String nodeName = node.getLocalName();
+ String typeString = null;
+
+ if (TAG_ITEM.equals(nodeName)) {
+ String attribute = node.getAttributeValue(ATTR_TYPE);
+ if (attribute != null) {
+ typeString = attribute;
+ }
+ } else {
+ // the type is the name of the node.
+ typeString = nodeName;
+ }
+
+ if (typeString != null) {
+ return ResourceType.getEnum(typeString);
+ }
+
+ return null;
+ }
+
+ private boolean isResourceFolder(@Nullable PsiElement parent) {
+ // Returns true if the given element represents a resource folder (e.g. res/values-en-rUS or layout-land, *not* the root res/ folder)
+ if (parent instanceof PsiDirectory) {
+ PsiDirectory directory = (PsiDirectory)parent;
+ PsiDirectory parentDirectory = directory.getParentDirectory();
+ if (parentDirectory != null) {
+ VirtualFile dir = parentDirectory.getVirtualFile();
+ return dir == myResourceDir;
+ }
+ }
+ return false;
+ }
+
+ private boolean isResourceFile(PsiFile psiFile) {
+ return isResourceFolder(psiFile.getParent());
+ }
+
+ @Override
+ boolean isScanPending(@NonNull PsiFile psiFile) {
+ synchronized (SCAN_LOCK) {
+ return myPendingScans != null && myPendingScans.contains(psiFile);
+ }
+ }
+
+ private void rescan(@NonNull final PsiFile psiFile, final @NonNull ResourceFolderType folderType) {
+ synchronized(SCAN_LOCK) {
+ if (isScanPending(psiFile)) {
+ return;
+ }
+
+ if (myPendingScans == null) {
+ myPendingScans = Sets.newHashSet();
+ }
+ myPendingScans.add(psiFile);
+ }
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ ApplicationManager.getApplication().runReadAction(new Runnable() {
+ @Override
+ public void run() {
+ rescanImmediately(psiFile, folderType);
+ synchronized (SCAN_LOCK) {
+ myPendingScans.remove(psiFile);
+ if (myPendingScans.isEmpty()) {
+ myPendingScans = null;
+ }
+ }
+ }
+ });
+ }
+ });
+ }
+
+ private void rescanImmediately(@NonNull final PsiFile psiFile, final @NonNull ResourceFolderType folderType) {
+ PsiFile file = psiFile;
+ if (folderType == VALUES) {
+ // For unit test tracking purposes only
+ //noinspection AssignmentToStaticFieldFromInstanceMethod
+ ourFullRescans++;
+
+ // First delete out the previous items
+ PsiResourceFile resourceFile = myResourceFiles.get(file);
+ boolean removed = false;
+ if (resourceFile != null) {
+ Collection<ResourceItem> items = resourceFile.getItems();
+ for (ResourceItem item : items) {
+ boolean removeFromFile = false; // Will throw away file
+ removed |= removeItems(resourceFile, item.getType(), item.getName(), removeFromFile);
+ }
+
+ myResourceFiles.remove(file);
+ }
+
+ file = ensureValid(file);
+ boolean added = false;
+ if (file != null) {
+ // Add items for this file
+ PsiDirectory parent = file.getParent();
+ assert parent != null; // since we have a folder type
+ String dirName = parent.getName();
+ PsiDirectory fileParent = psiFile.getParent();
+ if (fileParent != null) {
+ FolderConfiguration folderConfiguration = FolderConfiguration.getConfigForFolder(fileParent.getName());
+ if (folderConfiguration != null) {
+ added = scanValueFile(getQualifiers(dirName), file, folderConfiguration);
+ }
+ }
+ }
+
+ if (added || removed) {
+ // TODO: Consider doing a deeper diff of the changes to the resource items
+ // to determine if the removed and added items actually differ
+ myGeneration++;
+ invalidateItemCaches();
+ }
+ } else {
+ PsiResourceFile resourceFile = myResourceFiles.get(file);
+ if (resourceFile != null) {
+ // Already seen this file; no need to do anything unless it's a layout or
+ // menu file; in that case we may need to update the id's
+ if (folderType == LAYOUT || folderType == MENU) {
+ // For unit test tracking purposes only
+ //noinspection AssignmentToStaticFieldFromInstanceMethod
+ ourFullRescans++;
+
+ // We've already seen this resource, so no change in the ResourceItem for the
+ // file itself (e.g. @layout/foo from layout-land/foo.xml). However, we may have
+ // to update the id's:
+ Set<String> idsBefore = Sets.newHashSet();
+ Set<String> idsAfter = Sets.newHashSet();
+ ListMultimap<String, ResourceItem> map = myItems.get(ResourceType.ID);
+ if (map != null) {
+ Collection<ResourceItem> items = resourceFile.getItems();
+ List<ResourceItem> idItems = Lists.newArrayListWithExpectedSize(items.size() - 1);
+ for (ResourceItem item : items) {
+ if (item.getType() == ResourceType.ID) {
+ idsBefore.add(item.getName());
+ idItems.add(item);
+ }
+ }
+ for (String id : idsBefore) {
+ // Note that ResourceFile has a flat map (not a multimap) so it doesn't
+ // record all items (unlike the myItems map) so we need to remove the map
+ // items manually, can't just do map.remove(item.getName(), item)
+ List<ResourceItem> mapItems = map.get(id);
+ if (mapItems != null && !mapItems.isEmpty()) {
+ List<ResourceItem> toDelete = Lists.newArrayListWithExpectedSize(mapItems.size());
+ for (ResourceItem mapItem : mapItems) {
+ if (mapItem.getSource() == resourceFile) {
+ toDelete.add(mapItem);
+ }
+ }
+ for (ResourceItem delete : toDelete) {
+ map.remove(delete.getName(), delete);
+ }
+ }
+ }
+ resourceFile.removeItems(idItems);
+ }
+
+ // Add items for this file
+ List<ResourceItem> idItems = Lists.newArrayList();
+ file = ensureValid(file);
+ if (file != null) {
+ addIds(idItems, file);
+ }
+ if (!idItems.isEmpty()) {
+ resourceFile.addItems(idItems);
+ for (ResourceItem item : idItems) {
+ idsAfter.add(item.getName());
+ }
+ }
+
+ if (!idsBefore.equals(idsAfter)) {
+ myGeneration++;
+ }
+ // Identities may have changed even if the ids are the same, so update maps
+ invalidateItemCaches(ResourceType.ID);
+ }
+ } else {
+ // For unit test tracking purposes only
+ //noinspection AssignmentToStaticFieldFromInstanceMethod
+ ourFullRescans++;
+
+ PsiDirectory parent = file.getParent();
+ assert parent != null; // since we have a folder type
+ String dirName = parent.getName();
+
+ List<ResourceType> resourceTypes = FolderTypeRelationship.getRelatedResourceTypes(folderType);
+ assert resourceTypes.size() >= 1 : folderType;
+ ResourceType type = resourceTypes.get(0);
+
+ boolean idGenerating = resourceTypes.size() > 1;
+ assert !idGenerating || resourceTypes.size() == 2 && resourceTypes.get(1) == ResourceType.ID;
+
+ ListMultimap<String, ResourceItem> map = myItems.get(type);
+ if (map == null) {
+ map = ArrayListMultimap.create();
+ myItems.put(type, map);
+ }
+
+ file = ensureValid(file);
+ if (file != null) {
+ PsiDirectory fileParent = psiFile.getParent();
+ if (fileParent != null) {
+ FolderConfiguration folderConfiguration = FolderConfiguration.getConfigForFolder(fileParent.getName());
+ if (folderConfiguration != null) {
+ scanFileResourceFile(getQualifiers(dirName), folderType, folderConfiguration, type, idGenerating, map, file);
+ }
+ }
+ myGeneration++;
+ invalidateItemCaches();
+ }
+ }
+ }
+ }
+
+ private boolean removeItems(PsiResourceFile resourceFile, ResourceType type, String name, boolean removeFromFile) {
+ boolean removed = false;
+
+ // Remove the item of the given name and type from the given resource file.
+ // We CAN'T just remove items found in ResourceFile.getItems() because that map
+ // flattens everything down to a single item for a given name (it's using a flat
+ // map rather than a multimap) so instead we have to look up from the map instead
+ ListMultimap<String, ResourceItem> map = myItems.get(type);
+ if (map != null) {
+ List<ResourceItem> mapItems = map.get(name);
+ if (mapItems != null) {
+ ListIterator<ResourceItem> iterator = mapItems.listIterator();
+ while (iterator.hasNext()) {
+ ResourceItem next = iterator.next();
+ if (next.getSource() == resourceFile) {
+ iterator.remove();
+ if (removeFromFile) {
+ resourceFile.removeItem(next);
+ }
+ removed = true;
+ }
+ }
+ }
+ }
+
+ return removed;
+ }
+
+ @NotNull
+ public PsiTreeChangeListener getPsiListener() {
+ return myListener;
+ }
+
+ /** PSI listener which keeps the repository up to date */
+ private final class PsiListener extends PsiTreeChangeAdapter {
+ private boolean myIgnoreChildrenChanged;
+
+ @Override
+ public void childAdded(@NotNull PsiTreeChangeEvent event) {
+ PsiFile psiFile = event.getFile();
+ if (psiFile == null) {
+ // Called when you've added a file
+ PsiElement child = event.getChild();
+ if (child instanceof PsiFile) {
+ psiFile = (PsiFile)child;
+ if (isRelevantFile(psiFile)) {
+ addFile(psiFile);
+ }
+ } else if (child instanceof PsiDirectory) {
+ PsiDirectory directory = (PsiDirectory)child;
+ if (isResourceFolder(directory)) {
+ for (PsiFile file : directory.getFiles()) {
+ if (isRelevantFile(file)) {
+ addFile(file);
+ }
+ }
+ }
+ }
+ } else if (isRelevantFile(psiFile)) {
+ if (isScanPending(psiFile)) {
+ return;
+ }
+ // Some child was added within a file
+ ResourceFolderType folderType = getFolderType(psiFile);
+ if (folderType != null && isResourceFile(psiFile)) {
+ PsiElement child = event.getChild();
+ PsiElement parent = event.getParent();
+ if (folderType == ResourceFolderType.VALUES) {
+ if (child instanceof XmlTag) {
+ XmlTag tag = (XmlTag)child;
+
+ if (isItemElement(tag)) {
+ PsiResourceFile resourceFile = myResourceFiles.get(psiFile);
+ if (resourceFile != null) {
+ String name = tag.getAttributeValue(ATTR_NAME);
+ if (name != null) {
+ ResourceType type = getType(tag);
+ if (type == ResourceType.DECLARE_STYLEABLE) {
+ // Can't handle declare styleable additions incrementally yet; need to update paired attr items
+ rescan(psiFile, folderType);
+ return;
+ }
+ if (type != null) {
+ ListMultimap<String, ResourceItem> map = myItems.get(type);
+ if (map == null) {
+ map = ArrayListMultimap.create();
+ myItems.put(type, map);
+ }
+
+ ResourceItem item = new PsiResourceItem(name, type, tag, psiFile);
+ map.put(name, item);
+ resourceFile.addItems(Collections.singletonList(item));
+ myGeneration++;
+ invalidateItemCaches(type);
+ }
+ }
+
+ return;
+ }
+ }
+
+ // See if you just added a new item inside a <style> or <array> or <declare-styleable> etc
+ XmlTag parentTag = tag.getParentTag();
+ if (parentTag != null && ResourceType.getEnum(parentTag.getName()) != null) {
+ // Yes just invalidate the corresponding style value
+ ResourceItem style = findValueResourceItem(parentTag, psiFile);
+ if (style instanceof PsiResourceItem) {
+ if (((PsiResourceItem)style).recomputeValue()) {
+ myGeneration++;
+ }
+ return;
+ }
+ }
+
+ rescan(psiFile, folderType);
+ // Else: fall through and do full file rescan
+ } else if (parent instanceof XmlText) {
+ // If the edit is within an item tag
+ XmlText text = (XmlText)parent;
+ handleValueXmlTextEdit(text.getParentTag(), psiFile);
+ return;
+ } else if (child instanceof XmlText) {
+ // If the edit is within an item tag
+ handleValueXmlTextEdit(parent, psiFile);
+ return;
+ } else if (parent instanceof XmlComment || child instanceof XmlComment) {
+ // Can ignore comment edits or new comments
+ return;
+ }
+ rescan(psiFile, folderType);
+ } else if (folderType == LAYOUT || folderType == MENU) {
+ if (parent instanceof XmlComment || child instanceof XmlComment) {
+ return;
+ }
+ if (parent instanceof XmlText ||
+ (child instanceof XmlText && child.getText().trim().isEmpty())) {
+ return;
+ }
+
+ if (parent instanceof XmlElement && child instanceof XmlElement) {
+ if (child instanceof XmlTag) {
+ List<ResourceItem> ids = Lists.newArrayList();
+ addIds(ids, child, psiFile);
+ if (!ids.isEmpty()) {
+ PsiResourceFile resourceFile = myResourceFiles.get(psiFile);
+ if (resourceFile != null) {
+ resourceFile.addItems(ids);
+ }
+ }
+ return;
+ } else if (child instanceof XmlAttributeValue) {
+ assert parent instanceof XmlAttribute : parent;
+ @SuppressWarnings("CastConflictsWithInstanceof") // IDE bug? Cast is valid.
+ XmlAttribute attribute = (XmlAttribute)parent;
+ if (ATTR_ID.equals(attribute.getLocalName()) &&
+ ANDROID_URI.equals(attribute.getNamespace())) {
+ // TODO: Update it incrementally
+ rescan(psiFile, folderType);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ myIgnoreChildrenChanged = true;
+ }
+
+ @Override
+ public void childRemoved(@NotNull PsiTreeChangeEvent event) {
+ PsiFile psiFile = event.getFile();
+ if (psiFile == null) {
+ // Called when you've removed a file
+ PsiElement child = event.getChild();
+ if (child instanceof PsiFile) {
+ psiFile = (PsiFile)child;
+ if (isRelevantFile(psiFile)) {
+ removeFile(psiFile);
+ }
+ } else if (child instanceof PsiDirectory) {
+ // We can't iterate the children here because the dir is already empty.
+ // Instead, try to locate the files
+ String dirName = ((PsiDirectory)child).getName();
+ ResourceFolderType folderType = ResourceFolderType.getFolderType(dirName);
+
+ if (folderType != null) {
+ // Make sure it's really a resource folder. We can't look at the directory
+ // itself since the file no longer exists, but make sure the parent directory is
+ // a resource directory root
+ PsiDirectory parentDirectory = ((PsiDirectory)child).getParent();
+ if (parentDirectory != null) {
+ VirtualFile dir = parentDirectory.getVirtualFile();
+ if (!myFacet.getLocalResourceManager().isResourceDir(dir)) {
+ return;
+ }
+ } else {
+ return;
+ }
+ int index = dirName.indexOf('-');
+ String qualifiers;
+ if (index == -1) {
+ qualifiers = "";
+ } else {
+ qualifiers = dirName.substring(index + 1);
+ }
+
+ // Copy file map so we can delete while iterating
+ Collection<PsiResourceFile> resourceFiles = new ArrayList<PsiResourceFile>(myResourceFiles.values());
+ for (PsiResourceFile file : resourceFiles) {
+ if (folderType == file.getFolderType() && qualifiers.equals(file.getQualifiers())) {
+ removeFile(file);
+ }
+ }
+ }
+ }
+ } else if (isRelevantFile(psiFile)) {
+ if (isScanPending(psiFile)) {
+ return;
+ }
+ // Some child was removed within a file
+ ResourceFolderType folderType = getFolderType(psiFile);
+ if (folderType != null && isResourceFile(psiFile)) {
+ PsiElement child = event.getChild();
+ PsiElement parent = event.getParent();
+
+ if (folderType == ResourceFolderType.VALUES) {
+ if (child instanceof XmlTag) {
+ XmlTag tag = (XmlTag)child;
+
+ // See if you just removed an item inside a <style> or <array> or <declare-styleable> etc
+ if (parent instanceof XmlTag) {
+ XmlTag parentTag = (XmlTag)parent;
+ if (ResourceType.getEnum(parentTag.getName()) != null) {
+ // Yes just invalidate the corresponding style value
+ ResourceItem style = findValueResourceItem(parentTag, psiFile);
+ if (style instanceof PsiResourceItem) {
+ if (((PsiResourceItem)style).recomputeValue()) {
+ myGeneration++;
+ }
+
+ if (style.getType() == ResourceType.ATTR) {
+ parentTag = parentTag.getParentTag();
+ if (parentTag != null && parentTag.getName().equals(ResourceType.DECLARE_STYLEABLE.getName())) {
+ ResourceItem declareStyleable = findValueResourceItem(parentTag, psiFile);
+ if (declareStyleable instanceof PsiResourceItem) {
+ if (((PsiResourceItem)declareStyleable).recomputeValue()) {
+ myGeneration++;
+ }
+ }
+ }
+ }
+ return;
+ }
+ }
+ }
+
+ if (isItemElement(tag)) {
+ PsiResourceFile resourceFile = myResourceFiles.get(psiFile);
+ if (resourceFile != null) {
+ String name;
+ if (!tag.isValid()) {
+ ResourceItem item = findValueResourceItem(tag, psiFile);
+ if (item != null) {
+ name = item.getName();
+ } else {
+ // Can't find the name of the deleted tag; just do a full rescan
+ rescan(psiFile, folderType);
+ return;
+ }
+ } else {
+ name = tag.getAttributeValue(ATTR_NAME);
+ }
+ if (name != null) {
+ ResourceType type = getType(tag);
+ if (type != null) {
+ ListMultimap<String, ResourceItem> map = myItems.get(type);
+ if (map == null) {
+ return;
+ }
+ if (removeItems(resourceFile, type, name, true)) {
+ myGeneration++;
+ invalidateItemCaches(type);
+ }
+ }
+ }
+
+ return;
+ }
+ }
+
+ rescan(psiFile, folderType);
+ } else if (parent instanceof XmlText) {
+ // If the edit is within an item tag
+ XmlText text = (XmlText)parent;
+ handleValueXmlTextEdit(text.getParentTag(), psiFile);
+ } else if (child instanceof XmlText) {
+ handleValueXmlTextEdit(parent, psiFile);
+ } else if (parent instanceof XmlComment || child instanceof XmlComment) {
+ // Can ignore comment edits or removed comments
+ return;
+ } else {
+ // Some other change: do full file rescan
+ rescan(psiFile, folderType);
+ }
+ } else if (folderType == LAYOUT || folderType == MENU) {
+ // TODO: Handle removals of id's (values an attributes) incrementally
+ rescan(psiFile, folderType);
+ }
+ }
+ }
+
+ myIgnoreChildrenChanged = true;
+ }
+
+ private void removeFile(@Nullable PsiResourceFile resourceFile) {
+ if (resourceFile == null) {
+ // No resources for this file
+ return;
+ }
+ for (Map.Entry<PsiFile, PsiResourceFile> entry : myResourceFiles.entrySet()) {
+ PsiResourceFile file = entry.getValue();
+ if (resourceFile == file) {
+ PsiFile psiFile = entry.getKey();
+ myResourceFiles.remove(psiFile);
+ break;
+ }
+ }
+
+ myGeneration++;
+ invalidateItemCaches();
+
+ ResourceFolderType folderType = resourceFile.getFolderType();
+ if (folderType == VALUES || folderType == LAYOUT || folderType == MENU) {
+ removeItemsFromFile(resourceFile);
+ } else if (folderType != null) {
+ // Remove the file item
+ List<ResourceType> resourceTypes = FolderTypeRelationship.getRelatedResourceTypes(folderType);
+ for (ResourceType type : resourceTypes) {
+ if (type != ResourceType.ID) {
+ String name = LintUtils.getBaseName(resourceFile.getName());
+ boolean removeFromFile = false; // no need since we're discarding the file
+ removeItems(resourceFile, type, name, removeFromFile);
+ }
+ }
+ } // else: not a resource folder
+ }
+
+ private void removeFile(PsiFile psiFile) {
+ assert !psiFile.isValid() || isRelevantFile(psiFile);
+
+ PsiResourceFile resourceFile = myResourceFiles.get(psiFile);
+ if (resourceFile == null) {
+ // No resources for this file
+ return;
+ }
+ myResourceFiles.remove(psiFile);
+ myGeneration++;
+ invalidateItemCaches();
+
+ ResourceFolderType folderType = getFolderType(psiFile);
+ if (folderType == VALUES || folderType == LAYOUT || folderType == MENU) {
+ removeItemsFromFile(resourceFile);
+ } else if (folderType != null) {
+ // Remove the file item
+ List<ResourceType> resourceTypes = FolderTypeRelationship.getRelatedResourceTypes(folderType);
+ for (ResourceType type : resourceTypes) {
+ if (type != ResourceType.ID) {
+ String name = ResourceHelper.getResourceName(psiFile);
+ boolean removeFromFile = false; // no need since we're discarding the file
+ removeItems(resourceFile, type, name, removeFromFile);
+ }
+ }
+ } // else: not a resource folder
+ }
+
+ private void addFile(PsiFile psiFile) {
+ assert isRelevantFile(psiFile);
+
+ // Same handling as rescan, where the initial deletion is a no-op
+ ResourceFolderType folderType = getFolderType(psiFile);
+ if (folderType != null && isResourceFile(psiFile)) {
+ rescanImmediately(psiFile, folderType);
+ }
+ }
+
+ private ResourceFolderType getFolderType(PsiFile psiFile) {
+ PsiDirectory folder = psiFile.getParent();
+ assert folder != null;
+ return ResourceFolderType.getFolderType(folder.getName());
+ }
+
+ @Override
+ public void childReplaced(@NotNull PsiTreeChangeEvent event) {
+ PsiFile psiFile = event.getFile();
+ if (psiFile != null) {
+ if (isScanPending(psiFile)) {
+ return;
+ }
+ // This method is called when you edit within a file
+ if (isRelevantFile(psiFile)) {
+ // First determine if the edit is non-consequential.
+ // That's the case if the XML edited is not a resource file (e.g. the manifest file),
+ // or if it's within a file that is not a value file or an id-generating file (layouts and menus),
+ // such as editing the content of a drawable XML file.
+ ResourceFolderType folderType = getFolderType(psiFile);
+ if (folderType == LAYOUT || folderType == MENU) {
+ // The only way the edit affected the set of resources was if the user added or removed an
+ // id attribute. Since these can be added redundantly we can't automatically remove the old
+ // value if you renamed one, so we'll need a full file scan.
+ // However, we only need to do this scan if the change appears to be related to ids; this can
+ // only happen if the attribute value is changed.
+ PsiElement parent = event.getParent();
+ PsiElement child = event.getChild();
+ if (parent instanceof XmlText || child instanceof XmlText ||
+ parent instanceof XmlComment || child instanceof XmlComment) {
+ return;
+ }
+ if (parent instanceof XmlElement && child instanceof XmlElement) {
+ if (event.getOldChild() == event.getNewChild()) {
+ // We're not getting accurate PSI information: we have to do a full file scan
+ rescan(psiFile, folderType);
+ return;
+ }
+ if (child instanceof XmlAttributeValue) {
+ assert parent instanceof XmlAttribute : parent;
+ @SuppressWarnings("CastConflictsWithInstanceof") // IDE bug? Cast is valid.
+ XmlAttribute attribute = (XmlAttribute)parent;
+ if (ATTR_ID.equals(attribute.getLocalName()) &&
+ ANDROID_URI.equals(attribute.getNamespace())) {
+ // for each id attribute!
+ PsiResourceFile resourceFile = myResourceFiles.get(psiFile);
+ if (resourceFile != null) {
+ XmlTag xmlTag = attribute.getParent();
+ PsiElement oldChild = event.getOldChild();
+ PsiElement newChild = event.getNewChild();
+ if (oldChild instanceof XmlAttributeValue && newChild instanceof XmlAttributeValue) {
+ XmlAttributeValue oldValue = (XmlAttributeValue)oldChild;
+ XmlAttributeValue newValue = (XmlAttributeValue)newChild;
+ String oldName = stripIdPrefix(oldValue.getValue());
+ String newName = stripIdPrefix(newValue.getValue());
+ if (oldName.equals(newName)) {
+ // Can happen when there are error nodes (e.g. attribute value not yet closed during typing etc)
+ return;
+ }
+ ResourceItem item = findResourceItem(ResourceType.ID, psiFile, oldName);
+ if (item != null) {
+ ListMultimap<String, ResourceItem> map = myItems.get(item.getType());
+ if (map != null) {
+ // Found the relevant item: delete it and create a new one in a new location
+ map.remove(oldName, item);
+ ResourceItem newItem = new PsiResourceItem(newName, ResourceType.ID, xmlTag, psiFile);
+ map.put(newName, newItem);
+ resourceFile.replace(item, newItem);
+ myGeneration++;
+ invalidateItemCaches(ResourceType.ID);
+ return;
+ }
+ }
+ }
+ }
+
+ rescan(psiFile, folderType);
+ }
+ } else if (parent instanceof XmlAttributeValue) {
+ assert parent.getParent() instanceof XmlAttribute : parent;
+ XmlAttribute attribute = (XmlAttribute)parent.getParent();
+ if (ATTR_ID.equals(attribute.getLocalName()) &&
+ ANDROID_URI.equals(attribute.getNamespace())) {
+ // for each id attribute!
+ PsiResourceFile resourceFile = myResourceFiles.get(psiFile);
+ if (resourceFile != null) {
+ XmlTag xmlTag = attribute.getParent();
+ PsiElement oldChild = event.getOldChild();
+ PsiElement newChild = event.getNewChild();
+ String oldName = stripIdPrefix(oldChild.getText());
+ String newName = stripIdPrefix(newChild.getText());
+ if (oldName.equals(newName)) {
+ // Can happen when there are error nodes (e.g. attribute value not yet closed during typing etc)
+ return;
+ }
+ ResourceItem item = findResourceItem(ResourceType.ID, psiFile, oldName);
+ if (item != null) {
+ ListMultimap<String, ResourceItem> map = myItems.get(item.getType());
+ if (map != null) {
+ // Found the relevant item: delete it and create a new one in a new location
+ map.remove(oldName, item);
+ ResourceItem newItem = new PsiResourceItem(newName, ResourceType.ID, xmlTag, psiFile);
+ map.put(newName, newItem);
+ resourceFile.replace(item, newItem);
+ myGeneration++;
+ invalidateItemCaches(ResourceType.ID);
+ return;
+ }
+ }
+ }
+
+ rescan(psiFile, folderType);
+ }
+ }
+
+ return;
+ }
+
+ // TODO: Handle adding/removing elements in layouts incrementally
+
+ rescan(psiFile, folderType);
+ } else if (folderType == VALUES) {
+ PsiElement parent = event.getParent();
+ if (parent instanceof XmlElement) {
+ // Editing within an XML file
+ // An edit in a comment can be ignored
+ // An edit in a text inside an element can be used to invalidate the ResourceValue of an element
+ // (need to search upwards since strings can have HTML content)
+ // An edit between elements can be ignored
+ // An edit to an attribute name (not the attribute value for the attribute named "name"...) can
+ // sometimes be ignored (if you edit type or name, consider what to do)
+ // An edit of an attribute value can affect the name of type so update item
+ // An edit of other parts; for example typing in a new <string> item character by character.
+ // etc.
+
+ // See if you just removed an item inside a <style> or <array> or <declare-styleable> etc
+ if (parent instanceof XmlTag) {
+ XmlTag parentTag = (XmlTag)parent;
+ if (ResourceType.getEnum(parentTag.getName()) != null) {
+ // Yes just invalidate the corresponding style value
+ ResourceItem style = findValueResourceItem(parentTag, psiFile);
+ if (style instanceof PsiResourceItem) {
+ if (((PsiResourceItem)style).recomputeValue()) {
+ myGeneration++;
+ }
+ return;
+ }
+ }
+
+ if (parentTag.getName().equals(TAG_RESOURCES)
+ && event.getOldChild() instanceof XmlText
+ && event.getNewChild() instanceof XmlText) {
+ return;
+ }
+ }
+
+ if (parent instanceof XmlText) {
+ XmlText text = (XmlText)parent;
+ handleValueXmlTextEdit(text.getParentTag(), psiFile);
+ return;
+ } else if (parent instanceof XmlComment) {
+ // Nothing to do
+ return;
+ }
+
+ if (parent instanceof XmlAttributeValue) {
+ PsiElement attribute = parent.getParent();
+ PsiElement tag = attribute.getParent();
+ assert attribute instanceof XmlAttribute : attribute;
+ XmlAttribute xmlAttribute = (XmlAttribute)attribute;
+ assert tag instanceof XmlTag : tag;
+ XmlTag xmlTag = (XmlTag)tag;
+ String attributeName = xmlAttribute.getName();
+ // We could also special case handling of editing the type attribute, and the parent attribute,
+ // but editing these is rare enough that we can just stick with the fallback full file scan for those
+ // scenarios.
+ if (isItemElement(xmlTag) && attributeName.equals(ATTR_NAME)) {
+ // Edited the name of the item: replace it
+ ResourceType type = getType(xmlTag);
+ if (type != null) {
+ String oldName = event.getOldChild().getText();
+ String newName = event.getNewChild().getText();
+ if (oldName.equals(newName)) {
+ // Can happen when there are error nodes (e.g. attribute value not yet closed during typing etc)
+ return;
+ }
+ ResourceItem item = findResourceItem(type, psiFile, oldName);
+ if (item != null) {
+ ListMultimap<String, ResourceItem> map = myItems.get(item.getType());
+ if (map != null) {
+ // Found the relevant item: delete it and create a new one in a new location
+ map.remove(oldName, item);
+ ResourceItem newItem = new PsiResourceItem(newName, type, xmlTag, psiFile);
+ map.put(newName, newItem);
+ PsiResourceFile resourceFile = myResourceFiles.get(psiFile);
+ if (resourceFile != null) {
+ resourceFile.replace(item, newItem);
+ }
+ else {
+ assert false : item;
+ }
+ myGeneration++;
+ invalidateItemCaches(type);
+
+ // Invalidate surrounding declare styleable if any
+ if (type == ResourceType.ATTR) {
+ XmlTag parentTag = xmlTag.getParentTag();
+ if (parentTag != null && parentTag.getName().equals(ResourceType.DECLARE_STYLEABLE.getName())) {
+ ResourceItem style = findValueResourceItem(parentTag, psiFile);
+ if (style instanceof PsiResourceItem) {
+ ((PsiResourceItem)style).recomputeValue();
+ }
+ }
+ }
+
+ return;
+ }
+ }
+ } else {
+ XmlTag parentTag = xmlTag.getParentTag();
+ if (parentTag != null && ResourceType.getEnum(parentTag.getName()) != null) {
+ // <style>, or <plurals>, or <array>, or <string-array>, ...
+ // Edited the attribute value of an item that is wrapped in a <style> tag: invalidate parent cached value
+ ResourceItem style = findValueResourceItem(parentTag, psiFile);
+ if (style instanceof PsiResourceItem) {
+ if (((PsiResourceItem)style).recomputeValue()) {
+ myGeneration++;
+ }
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Fall through: We were not able to directly manipulate the repository to accommodate
+ // the edit, so re-scan the whole value file instead
+ rescan(psiFile, folderType);
+
+ } // else: can ignore this edit
+ }
+ } else {
+ PsiElement parent = event.getParent();
+ if (isResourceFolder(parent)) {
+ PsiElement oldChild = event.getOldChild();
+ PsiElement newChild = event.getNewChild();
+ if (oldChild instanceof PsiFile) {
+ PsiFile oldFile = (PsiFile)oldChild;
+ if (isRelevantFile(oldFile)) {
+ removeFile(oldFile);
+ }
+ }
+ if (newChild instanceof PsiFile) {
+ PsiFile newFile = (PsiFile)newChild;
+ if (isRelevantFile(newFile)) {
+ addFile(newFile);
+ }
+ }
+ }
+ }
+
+ myIgnoreChildrenChanged = true;
+ }
+
+ private void handleValueXmlTextEdit(@Nullable PsiElement parent, @NotNull PsiFile psiFile) {
+ if (!(parent instanceof XmlTag)) {
+ // Edited text outside the root element
+ return;
+ }
+ XmlTag parentTag = (XmlTag)parent;
+ String parentTagName = parentTag.getName();
+ if (parentTagName.equals(TAG_RESOURCES)) {
+ // Editing whitespace between top level elements; ignore
+ return;
+ }
+
+ if (parentTagName.equals(TAG_ITEM)) {
+ XmlTag style = parentTag.getParentTag();
+ if (style != null && ResourceType.getEnum(style.getName()) != null) {
+ // <style>, or <plurals>, or <array>, or <string-array>, ...
+ // Edited the text value of an item that is wrapped in a <style> tag: invalidate
+ ResourceItem item = findValueResourceItem(style, psiFile);
+ if (item instanceof PsiResourceItem) {
+ boolean cleared = ((PsiResourceItem)item).recomputeValue();
+ if (cleared) { // Only bump revision if this is a value which has already been observed!
+ myGeneration++;
+ }
+ }
+ return;
+ }
+ }
+
+ // Find surrounding item
+ while (parentTag != null) {
+ if (isItemElement(parentTag)) {
+ ResourceItem item = findValueResourceItem(parentTag, psiFile);
+ if (item instanceof PsiResourceItem) {
+ // Edited XML value
+ boolean cleared = ((PsiResourceItem)item).recomputeValue();
+ if (cleared) { // Only bump revision if this is a value which has already been observed!
+ myGeneration++;
+ }
+ }
+ break;
+ }
+ parentTag = parentTag.getParentTag();
+ }
+
+ // Fully handled; other whitespace changes do not affect resources
+ }
+
+ @Override
+ public void childMoved(@NotNull PsiTreeChangeEvent event) {
+ PsiElement child = event.getChild();
+ PsiFile psiFile = event.getFile();
+ //noinspection StatementWithEmptyBody
+ if (psiFile == null) {
+ // This is called when you move a file from one folder to another
+ if (child instanceof PsiFile) {
+ psiFile = (PsiFile)child;
+ if (!isRelevantFile(psiFile)) {
+ return;
+ }
+
+ // If you are renaming files, determine whether we can do a simple replacement
+ // (e.g. swap out ResourceFile instances), or whether it changes the type
+ // (e.g. moving foo.xml from layout/ to animator/), or whether it adds or removes
+ // the type (e.g. moving from invalid to valid resource directories), or whether
+ // it just affects the qualifiers (special case of swapping resource file instances).
+ String name = psiFile.getName();
+
+ PsiElement oldParent = event.getOldParent();
+ PsiDirectory oldParentDir;
+ if (oldParent instanceof PsiDirectory) {
+ oldParentDir = (PsiDirectory)oldParent;
+ } else {
+ // Can't find old location: treat this as a file add
+ addFile(psiFile);
+ return;
+ }
+
+ String oldDirName = oldParentDir.getName();
+ ResourceFolderType oldFolderType = ResourceFolderType.getFolderType(oldDirName);
+ ResourceFolderType newFolderType = getFolderType(psiFile);
+
+ boolean wasResourceFolder = oldFolderType != null && isResourceFolder(oldParentDir);
+ boolean isResourceFolder = newFolderType != null && isResourceFile(psiFile);
+
+ if (wasResourceFolder == isResourceFolder) {
+ if (!isResourceFolder) {
+ // Moved a non-resource file around: nothing to do
+ return;
+ }
+
+ // Moved a resource file from one resource folder to another: we need to update
+ // the ResourceFile entries for this file. We may also need to update the types.
+ PsiResourceFile resourceFile = findResourceFile(oldDirName, name);
+ if (resourceFile != null) {
+ if (oldFolderType != newFolderType) {
+ // In some cases we can do this cheaply, e.g. if you move from layout to menu
+ // we can just look up and change @layout/foo to @menu/foo, but if you move
+ // to or from values folder it gets trickier, so for now just treat this as a delete
+ // followed by an add
+ removeFile(resourceFile);
+ addFile(psiFile);
+ } else {
+ myResourceFiles.remove(resourceFile.getPsiFile());
+ myResourceFiles.put(psiFile, resourceFile);
+ PsiDirectory newParent = psiFile.getParent();
+ assert newParent != null; // Since newFolderType != null
+ String newDirName = newParent.getName();
+ resourceFile.setPsiFile(psiFile, getQualifiers(newDirName));
+ myGeneration++; // qualifiers may have changed: can affect configuration matching
+ invalidateItemCaches();
+ }
+ } else {
+ // Couldn't find previous file; just add new file
+ addFile(psiFile);
+ }
+ } else if (isResourceFolder) {
+ // Moved file into resource folder: treat it as a file add
+ addFile(psiFile);
+ } else {
+ //noinspection ConstantConditions
+ assert wasResourceFolder;
+
+ // Moved file out of resource folders: treat it as a file deletion.
+ // The only trick here is that we don't actually have the PsiFile anymore.
+ // Work around this by searching our PsiFile to ResourceFile map for a match.
+ String dirName = oldParentDir.getName();
+ PsiResourceFile resourceFile = findResourceFile(dirName, name);
+ if (resourceFile != null) {
+ removeFile(resourceFile);
+ }
+ }
+ }
+ } else {
+ // Change inside a file
+ // Ignore: moving elements around doesn't affect the resources
+ }
+
+ myIgnoreChildrenChanged = true;
+ }
+
+ @Override
+ public final void beforeChildrenChange(@NotNull PsiTreeChangeEvent event) {
+ myIgnoreChildrenChanged = false;
+ }
+
+ @Override
+ public void childrenChanged(@NotNull PsiTreeChangeEvent event) {
+ // Called after children have changed. There are typically individual childMoved, childAdded etc
+ // calls that we hook into for more specific details. However, there are some events we don't
+ // catch using those methods, and for that we have the below handling.
+ if (myIgnoreChildrenChanged) {
+ // We've already processed this change as one or more individual childMoved, childAdded, childRemoved etc calls
+ // However, we sometimes get some surprising (=bogus) events where the parent and the child
+ // are the same, and in those cases there may be other child events we need to process
+ // so fall through and process the whole file
+ if (event.getParent() != event.getChild()) {
+ return;
+ }
+ }
+ else if (event.getNewChild() == null && event.getOldChild() == null && event.getOldParent() == null && event.getNewParent() == null
+ && event.getParent() instanceof PsiFile) {
+ return;
+ }
+
+ PsiFile psiFile = event.getFile();
+ if (psiFile != null && isRelevantFile(psiFile)) {
+ VirtualFile file = psiFile.getVirtualFile();
+ if (file != null) {
+ ResourceFolderType folderType = getFolderType(psiFile);
+ if (folderType != null && isResourceFile(psiFile)) {
+ rescan(psiFile, folderType);
+ }
+ }
+ } else {
+ Throwable throwable = new Throwable();
+ throwable.fillInStackTrace();
+ LOG.debug("Received unexpected childrenChanged event for inter-file operations", throwable);
+ }
+ }
+
+ // There are cases where a file is renamed, and I don't get a pre-notification. Use this flag
+ // to detect those scenarios, and in that case, do proper cleanup.
+ // (Note: There are also cases where *only* beforePropertyChange is called, not propertyChange.
+ // One example is the unit test for the raw folder, where we're renaming a file, and we get
+ // the beforePropertyChange notification, followed by childReplaced on the PsiDirectory, and
+ // nothing else.
+ private boolean mySeenPrePropertyChange;
+
+ @Override
+ public final void beforePropertyChange(@NotNull PsiTreeChangeEvent event) {
+ if (PsiTreeChangeEvent.PROP_FILE_NAME == event.getPropertyName()) {
+ // This is called when you rename a file (before the file has been renamed)
+ PsiElement child = event.getChild();
+ if (child instanceof PsiFile) {
+ PsiFile psiFile = (PsiFile)child;
+ if (isRelevantFile(psiFile) && isResourceFolder(event.getParent())) {
+ removeFile(psiFile);
+ }
+ }
+ // The new name will be added in the post hook (propertyChanged rather than beforePropertyChange)
+ }
+
+ mySeenPrePropertyChange = true;
+ }
+
+ @Override
+ public void propertyChanged(@NotNull PsiTreeChangeEvent event) {
+ if (PsiTreeChangeEvent.PROP_FILE_NAME == event.getPropertyName() && isResourceFolder(event.getParent())) {
+ // This is called when you rename a file (after the file has been renamed)
+ PsiElement child = event.getElement();
+ if (child instanceof PsiFile) {
+ PsiFile psiFile = (PsiFile)child;
+ if (isRelevantFile(psiFile) && isResourceFolder(event.getParent())) {
+ if (!mySeenPrePropertyChange) {
+ Object oldValue = event.getOldValue();
+ if (oldValue instanceof String) {
+ PsiDirectory parent = psiFile.getParent();
+ String oldName = (String)oldValue;
+ if (parent != null && parent.findFile(oldName) == null) {
+ removeFile(findResourceFile(parent.getName(), oldName));
+ }
+ }
+ }
+
+ addFile(psiFile);
+ }
+ }
+ }
+
+ // TODO: Do we need to handle PROP_DIRECTORY_NAME for users renaming any of the resource folders?
+ // and what about PROP_FILE_TYPES -- can users change the type of an XML File to something else?
+
+ mySeenPrePropertyChange = false;
+ }
+ }
+
+ @Nullable
+ private PsiResourceFile findResourceFile(String dirName, String fileName) {
+ int index = dirName.indexOf('-');
+ String qualifiers;
+ String folderTypeName;
+ if (index == -1) {
+ qualifiers = "";
+ folderTypeName = dirName;
+ } else {
+ qualifiers = dirName.substring(index + 1);
+ folderTypeName = dirName.substring(0, index);
+ }
+ ResourceFolderType folderType = ResourceFolderType.getTypeByName(folderTypeName);
+
+ for (PsiResourceFile file : myResourceFiles.values()) {
+ String name = file.getName();
+ if (folderType == file.getFolderType() && fileName.equals(name) && qualifiers.equals(file.getQualifiers())) {
+ return file;
+ }
+ }
+
+ return null;
+ }
+
+ private void removeItemsFromFile(PsiResourceFile resourceFile) {
+ Collection<ResourceItem> items = resourceFile.getItems();
+ for (ResourceItem item : items) {
+ boolean removeFromFile = false; // no need since we're discarding the file
+ removeItems(resourceFile, item.getType(), item.getName(), removeFromFile);
+ }
+ }
+
+ private static boolean isItemElement(XmlTag xmlTag) {
+ String tag = xmlTag.getName();
+ if (tag.equals(TAG_RESOURCES)) {
+ return false;
+ }
+ return tag.equals(TAG_ITEM) || ResourceType.getEnum(tag) != null;
+ }
+
+ @Nullable
+ private ResourceItem findValueResourceItem(XmlTag tag, PsiFile file) {
+ if (!tag.isValid()) {
+ PsiResourceFile resourceFile = myResourceFiles.get(file);
+ if (resourceFile != null) {
+ for (ResourceItem item : resourceFile.getItems()) {
+ PsiResourceItem pri = (PsiResourceItem)item;
+ XmlTag xmlTag = pri.getTag();
+ if (xmlTag == tag) {
+ return item;
+ }
+ }
+ }
+ return null;
+ }
+ String name = tag.getAttributeValue(ATTR_NAME);
+ return name != null ? findValueResourceItem(tag, file, name) : null;
+ }
+
+ @Nullable
+ private ResourceItem findValueResourceItem(XmlTag tag, PsiFile file, String name) {
+ ResourceType type = getType(tag);
+ return findResourceItem(type, file, name);
+ }
+
+ @Nullable
+ private ResourceItem findResourceItem(@Nullable ResourceType type, PsiFile file, String name) {
+ if (type != null && name != null) {
+ ListMultimap<String, ResourceItem> map = myItems.get(type);
+ if (map != null) {
+ List<ResourceItem> items = map.get(name);
+ assert items != null;
+ for (ResourceItem item : items) {
+ assert item instanceof PsiResourceItem;
+ PsiResourceItem psiItem = (PsiResourceItem)item;
+ PsiFile virtualFile = psiItem.getPsiFile();
+ if (virtualFile == file) {
+ return item;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ // For debugging only
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + " for " + myResourceDir + ": @" + Integer.toHexString(System.identityHashCode(this));
+ }
+}
diff --git a/android/src/com/android/tools/idea/rendering/ResourceHelper.java b/android/src/com/android/tools/idea/rendering/ResourceHelper.java
index 9610fc1..5f1fbc7 100644
--- a/android/src/com/android/tools/idea/rendering/ResourceHelper.java
+++ b/android/src/com/android/tools/idea/rendering/ResourceHelper.java
@@ -40,6 +40,7 @@
import java.util.List;
import static com.android.SdkConstants.*;
+import static com.android.ide.common.resources.ResourceResolver.MAX_RESOURCE_INDIRECTION;
public class ResourceHelper {
private static final Logger LOG = Logger.getInstance("#com.android.tools.idea.rendering.ResourceHelper");
@@ -268,7 +269,8 @@
}
String value = color.getValue();
- while (value != null) {
+ int depth = 0;
+ while (value != null && depth < MAX_RESOURCE_INDIRECTION) {
if (value.startsWith("#")) {
return parseColor(value);
}
@@ -300,6 +302,8 @@
return null;
}
+
+ depth++;
}
return null;
diff --git a/android/src/com/android/tools/idea/rendering/ResourceNameValidator.java b/android/src/com/android/tools/idea/rendering/ResourceNameValidator.java
new file mode 100644
index 0000000..147a522
--- /dev/null
+++ b/android/src/com/android/tools/idea/rendering/ResourceNameValidator.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.rendering;
+
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.ui.InputValidatorEx;
+import org.jetbrains.android.util.AndroidUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import static com.android.SdkConstants.DOT_XML;
+
+/**
+ * Validator which ensures that new Android resource names are valid.
+ */
+public class ResourceNameValidator implements InputValidatorEx {
+ private static final Logger LOG = Logger.getInstance(ResourceNameValidator.class);
+
+ /**
+ * Set of existing names to check for conflicts with
+ */
+ private Set<String> myExisting;
+
+ /**
+ * If true, the validated name must be unique
+ */
+ private boolean myUnique = true;
+
+ /**
+ * If true, the validated name must exist
+ */
+ private boolean myExist;
+
+ /**
+ * True if the resource name being considered is a "file" based resource (where the
+ * resource name is the actual file name, rather than just a value attribute inside an
+ * XML file name of arbitrary name
+ */
+ private boolean myIsFileType;
+
+ /**
+ * True if the resource type can point to image resources
+ */
+ private boolean myIsImageType;
+
+ /**
+ * If true, allow .xml as a name suffix
+ */
+ private boolean myAllowXmlExtension;
+
+ private ResourceNameValidator(boolean allowXmlExtension, @Nullable Set<String> existing, boolean isFileType, boolean isImageType) {
+ myAllowXmlExtension = allowXmlExtension;
+ myExisting = existing;
+ myIsFileType = isFileType;
+ myIsImageType = isImageType;
+ }
+
+ /**
+ * Makes the resource name validator require that names are unique.
+ *
+ * @return this, for construction chaining
+ */
+ public ResourceNameValidator unique() {
+ myUnique = true;
+ myExist = false;
+
+ return this;
+ }
+
+ /**
+ * Makes the resource name validator require that names already exist
+ *
+ * @return this, for construction chaining
+ */
+ public ResourceNameValidator exist() {
+ myExist = true;
+ myUnique = false;
+
+ return this;
+ }
+
+ @Nullable
+ @Override
+ public String getErrorText(String inputString) {
+ try {
+ if (inputString == null || inputString.trim().length() == 0) {
+ return "Enter a new name";
+ }
+
+ if (myAllowXmlExtension && inputString.endsWith(DOT_XML)) {
+ inputString = inputString.substring(0, inputString.length() - DOT_XML.length());
+ }
+
+ if (myAllowXmlExtension && myIsImageType && AndroidUtils.hasImageExtension(inputString)) {
+ inputString = inputString.substring(0, inputString.lastIndexOf('.'));
+ }
+
+ if (!myIsFileType) {
+ inputString = inputString.replace('.', '_');
+ }
+
+ if (myAllowXmlExtension) {
+ if (inputString.indexOf('.') != -1 && !inputString.endsWith(DOT_XML)) {
+ if (myIsImageType) {
+ return "The filename must end with .xml or .png";
+ }
+ else {
+ return "The filename must end with .xml";
+ }
+ }
+ }
+
+ // Resource names must be valid Java identifiers, since they will
+ // be represented as Java identifiers in the R file:
+ if (!Character.isJavaIdentifierStart(inputString.charAt(0))) {
+ return "The resource name must begin with a character";
+ }
+ for (int i = 1, n = inputString.length(); i < n; i++) {
+ char c = inputString.charAt(i);
+ if (!Character.isJavaIdentifierPart(c)) {
+ return String.format("'%1$c' is not a valid resource name character", c);
+ }
+ }
+
+ if (myIsFileType) {
+ char first = inputString.charAt(0);
+ if (!(first >= 'a' && first <= 'z')) {
+ return String.format("File-based resource names must start with a lowercase letter.");
+ }
+
+ // AAPT only allows lowercase+digits+_:
+ // "%s: Invalid file name: must contain only [a-z0-9_.]","
+ for (int i = 0, n = inputString.length(); i < n; i++) {
+ char c = inputString.charAt(i);
+ if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_')) {
+ return String.format("File-based resource names must contain only lowercase a-z, 0-9, or _.");
+ }
+ }
+ }
+
+ if (!AndroidUtils.isIdentifier(inputString)) {
+ // It's a reserved keyword. There are other reasons isIdentifier can return false,
+ // but we've dealt with those above.
+ return String.format("%1$s is not a valid name (reserved Java keyword)", inputString);
+ }
+
+ if (myExisting != null && (myUnique || myExist)) {
+ boolean exists = myExisting.contains(inputString);
+ if (myUnique && exists) {
+ return String.format("%1$s already exists", inputString);
+ }
+ else if (myExist && !exists) {
+ return String.format("%1$s does not exist", inputString);
+ }
+ }
+
+ return null;
+ }
+ catch (Exception e) {
+ LOG.error("Validation failed: " + e.toString(), e);
+ return "";
+ }
+ }
+
+ /**
+ * Creates a new {@link ResourceNameValidator}
+ *
+ * @param allowXmlExtension if true, allow .xml to be entered as a suffix for the
+ * resource name
+ * @param type the resource type of the resource name being validated
+ * @return a new {@link ResourceNameValidator}
+ */
+ public static ResourceNameValidator create(boolean allowXmlExtension, @NotNull ResourceFolderType type) {
+ boolean isFileType = type != ResourceFolderType.VALUES;
+ return new ResourceNameValidator(allowXmlExtension, null, isFileType, type == ResourceFolderType.DRAWABLE);
+ }
+
+ /**
+ * Creates a new {@link ResourceNameValidator}
+ *
+ * @param allowXmlExtension if true, allow .xml to be entered as a suffix for the
+ * resource name
+ * @param existing An optional set of names that already exist (and therefore will not
+ * be considered valid if entered as the new name)
+ * @param type the resource type of the resource name being validated
+ * @return a new {@link ResourceNameValidator}
+ */
+ public static ResourceNameValidator create(boolean allowXmlExtension, @Nullable Set<String> existing, @NotNull ResourceType type) {
+ boolean isFileType = ResourceHelper.isFileBasedResourceType(type);
+ return new ResourceNameValidator(allowXmlExtension, existing, isFileType, type == ResourceType.DRAWABLE).unique();
+ }
+
+ /**
+ * Creates a new {@link ResourceNameValidator}. By default, the name will need to be
+ * unique in the project.
+ *
+ * @param allowXmlExtension if true, allow .xml to be entered as a suffix for the
+ * resource name
+ * @param projectResources the project resources to validate new resource names for
+ * @param type the resource type of the resource name being validated
+ * @return a new {@link ResourceNameValidator}
+ */
+ public static ResourceNameValidator create(boolean allowXmlExtension, @Nullable ProjectResources projectResources,
+ @NotNull ResourceType type) {
+ Set<String> existing = null;
+ if (projectResources != null) {
+ existing = new HashSet<String>();
+ Collection<String> items = projectResources.getItemsOfType(type);
+ existing.addAll(items);
+ }
+
+ boolean isFileType = ResourceHelper.isFileBasedResourceType(type);
+ return new ResourceNameValidator(allowXmlExtension, existing, isFileType, type == ResourceType.DRAWABLE);
+ }
+
+ @Override
+ public boolean checkInput(String inputString) {
+ return getErrorText(inputString) == null;
+ }
+
+ @Override
+ public boolean canClose(String inputString) {
+ return checkInput(inputString);
+ }
+}
diff --git a/android/src/com/android/tools/idea/rendering/SaveScreenshotAction.java b/android/src/com/android/tools/idea/rendering/SaveScreenshotAction.java
new file mode 100644
index 0000000..ba24eb7
--- /dev/null
+++ b/android/src/com/android/tools/idea/rendering/SaveScreenshotAction.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.rendering;
+
+import com.android.SdkConstants;
+import com.android.sdklib.devices.Device;
+import com.android.tools.idea.configurations.Configuration;
+import com.android.tools.idea.configurations.RenderContext;
+import com.android.tools.idea.ddms.screenshot.ScreenshotViewer;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import icons.AndroidIcons;
+import org.jetbrains.android.util.AndroidBundle;
+import org.jetbrains.annotations.Nullable;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.File;
+
+public class SaveScreenshotAction extends AnAction {
+ private final RenderContext myContext;
+
+ public SaveScreenshotAction(RenderContext context) {
+ super("Save Screenshot...", null, AndroidIcons.Ddms.ScreenCapture);
+ myContext = context;
+ }
+
+ @Override
+ public void update(AnActionEvent e) {
+ super.update(e);
+ e.getPresentation().setEnabled(myContext.getRenderedImage() != null && e.getProject() != null);
+ }
+
+ @Override
+ public void actionPerformed(AnActionEvent e) {
+ Project project = e.getProject();
+ try {
+ BufferedImage image = myContext.getRenderedImage();
+ assert image != null && project != null; // enforced by update() above
+
+ // We need to create a temp file since the image preview editor requires a real file
+ File backingFile = FileUtil.createTempFile("screenshot", SdkConstants.DOT_PNG, true);
+ ImageIO.write(image, SdkConstants.EXT_PNG, backingFile);
+
+ ScreenshotViewer viewer = new ScreenshotViewer(project, image, backingFile, null, getDeviceName());
+ if (viewer.showAndGet()) {
+ File screenshot = viewer.getScreenshot();
+ VirtualFile vf = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(screenshot);
+ if (vf != null) {
+ FileEditorManager.getInstance(project).openFile(vf, true);
+ }
+ }
+ }
+ catch (Exception ex) {
+ Messages.showErrorDialog(project, AndroidBundle.message("android.ddms.screenshot.generic.error", e),
+ AndroidBundle.message("android.ddms.actions.screenshot"));
+ }
+ }
+
+ @Nullable
+ private String getDeviceName() {
+ Configuration config = myContext.getConfiguration();
+ if (config == null) {
+ return null;
+ }
+
+ Device device = config.getDevice();
+ if (device == null) {
+ return null;
+ }
+
+ return device.getName();
+ }
+}
diff --git a/android/src/com/android/tools/idea/rendering/XmlTagPullParser.java b/android/src/com/android/tools/idea/rendering/XmlTagPullParser.java
index 89c0d3b..c09ad14 100644
--- a/android/src/com/android/tools/idea/rendering/XmlTagPullParser.java
+++ b/android/src/com/android/tools/idea/rendering/XmlTagPullParser.java
@@ -18,7 +18,9 @@
import com.android.annotations.Nullable;
import com.android.ide.common.rendering.api.ILayoutPullParser;
import com.android.ide.common.rendering.legacy.ILegacyPullParser;
+import com.android.ide.common.res2.ValueXmlHelper;
import com.android.resources.Density;
+import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
@@ -362,9 +364,13 @@
return VALUE_FILL_PARENT;
}
- // Handle unicode escapes
- if (value.indexOf('\\') != -1) {
- value = replaceUnicodeEscapes(value);
+ // Handle unicode and XML escapes
+ for (int i = 0, n = value.length(); i < n; i++) {
+ char c = value.charAt(i);
+ if (c == '&' || c == '\\') {
+ value = ValueXmlHelper.unescapeResourceString(value, true, false);
+ break;
+ }
}
}
diff --git a/android/src/com/android/tools/idea/rendering/multi/PreviewRenderContext.java b/android/src/com/android/tools/idea/rendering/multi/PreviewRenderContext.java
index f5d5fb3..c05f342 100644
--- a/android/src/com/android/tools/idea/rendering/multi/PreviewRenderContext.java
+++ b/android/src/com/android/tools/idea/rendering/multi/PreviewRenderContext.java
@@ -24,6 +24,7 @@
import org.jetbrains.annotations.Nullable;
import java.awt.*;
+import java.awt.image.BufferedImage;
/**
* {@linkplain RenderContext} used when rendering a configuration preview.
@@ -146,4 +147,10 @@
public void setDeviceFramesEnabled(boolean on) {
myRenderContext.setDeviceFramesEnabled(on);
}
+
+ @Nullable
+ @Override
+ public BufferedImage getRenderedImage() {
+ return myRenderContext.getRenderedImage();
+ }
}
diff --git a/android/src/com/android/tools/idea/rendering/multi/RenderPreview.java b/android/src/com/android/tools/idea/rendering/multi/RenderPreview.java
index 47d55aa..4ae5dc9 100644
--- a/android/src/com/android/tools/idea/rendering/multi/RenderPreview.java
+++ b/android/src/com/android/tools/idea/rendering/multi/RenderPreview.java
@@ -18,7 +18,6 @@
import com.android.ide.common.rendering.api.RenderSession;
import com.android.ide.common.rendering.api.Result;
import com.android.ide.common.rendering.api.Result.Status;
-import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.resources.Density;
import com.android.resources.ResourceType;
@@ -39,7 +38,6 @@
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Pair;
-import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
@@ -433,10 +431,9 @@
if (editedFile != null) {
if (!myConfiguration.isBestMatchFor(editedFile, config)) {
ProjectResources resources = ProjectResources.get(myConfiguration.getModule(), true);
- ResourceFile best = resources.getMatchingFile(ResourceHelper.getResourceName(editedFile), ResourceType.LAYOUT, config);
+ VirtualFile best = resources.getMatchingFile(editedFile, ResourceType.LAYOUT, config);
if (best != null) {
- File file = best.getFile();
- myAlternateInput = LocalFileSystem.getInstance().findFileByIoFile(file);
+ myAlternateInput = best;
}
if (myAlternateInput != null) {
myAlternateConfiguration = Configuration.create(myConfiguration, myAlternateInput);
diff --git a/android/src/com/android/tools/idea/sdk/SelectSdkDialog.java b/android/src/com/android/tools/idea/sdk/SelectSdkDialog.java
index 2501af2..d337a6d 100644
--- a/android/src/com/android/tools/idea/sdk/SelectSdkDialog.java
+++ b/android/src/com/android/tools/idea/sdk/SelectSdkDialog.java
@@ -20,6 +20,8 @@
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.projectRoots.JavaSdk;
import com.intellij.openapi.ui.*;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.components.JBLabel;
@@ -43,6 +45,7 @@
/**
* Displays SDK selection dialog.
+ *
* @param jdkPath path to JDK if known, null otherwise
* @param sdkPath path to Android SDK if known, null otherwise
*/
@@ -53,13 +56,29 @@
setTitle("Select SDKs");
+ if (jdkPath != null) {
+ String err = validateJdk(jdkPath);
+ if (err != null) {
+ jdkPath = null;
+ }
+ }
+
+ if (sdkPath != null) {
+ String err = validateAndroidSdk(sdkPath);
+ if (err != null) {
+ sdkPath = null;
+ }
+ }
+
if (jdkPath == null && sdkPath == null) {
myDescriptionLabel.setText(AndroidBundle.message("android.startup.missing.both"));
- } else if (jdkPath == null) {
+ }
+ else if (jdkPath == null) {
myDescriptionLabel.setText(AndroidBundle.message("android.startup.missing.jdk"));
mySdkTextFieldWithButton.setVisible(false);
mySelectSdkLabel.setVisible(false);
- } else {
+ }
+ else {
myDescriptionLabel.setText(AndroidBundle.message("android.startup.missing.sdk"));
myJdkTextFieldWithButton.setVisible(false);
mySelectJdkLabel.setVisible(false);
@@ -77,13 +96,12 @@
}
BrowseFolderListener listener =
- new BrowseFolderListener("Select JDK Home", myJdkTextFieldWithButton,
- JavaSdk.getInstance().getHomeChooserDescriptor(), jdkPath);
+ new BrowseFolderListener("Select JDK Home", myJdkTextFieldWithButton, JavaSdk.getInstance().getHomeChooserDescriptor(), jdkPath);
myJdkTextFieldWithButton.addBrowseFolderListener(null, listener);
- listener = new BrowseFolderListener("Select Android SDK Home", mySdkTextFieldWithButton,
- AndroidSdkType.getInstance().getHomeChooserDescriptor(),
- sdkPath);
+ listener =
+ new BrowseFolderListener("Select Android SDK Home", mySdkTextFieldWithButton, AndroidSdkType.getInstance().getHomeChooserDescriptor(),
+ sdkPath);
mySdkTextFieldWithButton.addBrowseFolderListener(null, listener);
}
@@ -97,18 +115,48 @@
@Override
protected ValidationInfo doValidate() {
String jdkHome = myJdkTextFieldWithButton.getText().trim();
- if (jdkHome.isEmpty() || !JavaSdk.getInstance().isValidSdkHome(jdkHome)) {
- return new ValidationInfo("Invalid JDK", myJdkTextFieldWithButton.getTextField());
+ String jdkError = validateJdk(jdkHome);
+ if (jdkError != null) {
+ return new ValidationInfo(jdkError, myJdkTextFieldWithButton.getTextField());
}
String androidHome = mySdkTextFieldWithButton.getText().trim();
- if (androidHome.isEmpty() || !AndroidSdkType.getInstance().isValidSdkHome(androidHome)) {
- return new ValidationInfo("Invalid Android SDK", mySdkTextFieldWithButton.getTextField());
+ String sdkError = validateAndroidSdk(androidHome);
+ if (sdkError != null) {
+ return new ValidationInfo(sdkError, mySdkTextFieldWithButton.getTextField());
+ }
+ VersionCheck.VersionCheckResult result = VersionCheck.checkVersion(androidHome);
+ if (!result.isCompatibleVersion()) {
+ String msg = AndroidBundle.message("android.version.check.too.old", VersionCheck.MIN_TOOLS_REV, result.getRevision());
+ return new ValidationInfo(msg, mySdkTextFieldWithButton.getTextField());
+ }
+ return null;
+ }
+
+ @Nullable
+ private static String validateJdk(String path) {
+ if (StringUtil.isEmpty(path) || !JavaSdk.getInstance().isValidSdkHome(path)) {
+ return "Invalid JDK path.";
}
return null;
}
+ @Nullable
+ private static String validateAndroidSdk(String path) {
+ if (StringUtil.isEmpty(path)) {
+ return "Android SDK path not specified.";
+ }
+
+ Pair<Boolean, String> validationResult = AndroidSdkType.validateAndroidSdk(path);
+ String error = validationResult.getSecond();
+ if (!validationResult.getFirst()) {
+ return String.format("Invalid Android SDK (%1$s): %2$s", path, error);
+ } else {
+ return null;
+ }
+ }
+
@Override
protected void doOKAction() {
myJdkHome = myJdkTextFieldWithButton.getText();
@@ -145,9 +193,9 @@
return super.getInitialFile();
}
- return myDefaultPath == null ?
- LocalFileSystem.getInstance().findFileByPath(PathManager.getHomePath()) :
- LocalFileSystem.getInstance().findFileByPath(myDefaultPath);
+ return myDefaultPath == null
+ ? LocalFileSystem.getInstance().findFileByPath(PathManager.getHomePath())
+ : LocalFileSystem.getInstance().findFileByPath(myDefaultPath);
}
}
}
diff --git a/android/src/com/android/tools/idea/sdk/VersionCheck.java b/android/src/com/android/tools/idea/sdk/VersionCheck.java
new file mode 100644
index 0000000..451192c
--- /dev/null
+++ b/android/src/com/android/tools/idea/sdk/VersionCheck.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.sdk;
+
+import com.android.SdkConstants;
+import com.android.sdklib.repository.FullRevision;
+import com.android.sdklib.repository.PkgProps;
+import com.google.common.io.Closeables;
+import com.intellij.openapi.diagnostic.Logger;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Handles the version check for the SDK.
+ */
+public final class VersionCheck {
+ private static final Logger LOG = Logger.getInstance(VersionCheck.class);
+
+ /**
+ * The minimum version of the SDK Tools that this version of ADT requires.
+ */
+ public static final FullRevision MIN_TOOLS_REV = new FullRevision(22, 0, 0, 0);
+
+ private static final Pattern mySourcePropPattern = Pattern.compile("^" + PkgProps.PKG_REVISION + "=(.*)$");
+
+ private VersionCheck() {
+ }
+
+ /**
+ * Indicates whether the Android SDK Tools revision is at least 22.0.0.
+ *
+ * @param sdkDir the root directory of the Android SDK.
+ * @return {@code true} if the Android SDK Tools revision is at least 22.0.0; {@code false} otherwise.
+ */
+ public static boolean isCompatibleVersion(@NotNull File sdkDir) {
+ if (!sdkDir.isDirectory()) {
+ return false;
+ }
+ return isCompatibleVersion(sdkDir.getAbsolutePath());
+ }
+
+ /**
+ * Indicates whether the Android SDK Tools revision is at least 22.0.0.
+ *
+ * @param sdkPath the path of the Android SDK.
+ * @return {@code true} if the Android SDK Tools revision is at least 22.0.0; {@code false} otherwise.
+ */
+ public static boolean isCompatibleVersion(@Nullable String sdkPath) {
+ if (sdkPath == null) {
+ return false;
+ }
+ return checkVersion(sdkPath).isCompatibleVersion();
+ }
+
+ /**
+ * Verifies that the Android SDK Tools revision is at least 22.0.0.
+ *
+ * @param sdkPath the path of the Android SDK.
+ * @return the result of the check.
+ */
+ @NotNull
+ public static VersionCheckResult checkVersion(@NotNull String sdkPath) {
+ File toolsDir = new File(sdkPath, SdkConstants.OS_SDK_TOOLS_FOLDER);
+ FullRevision toolsRevision = new FullRevision(Integer.MAX_VALUE);
+ BufferedReader reader = null;
+ try {
+ File sourceProperties = new File(toolsDir, SdkConstants.FN_SOURCE_PROP);
+ //noinspection IOResourceOpenedButNotSafelyClosed
+ reader = new BufferedReader(new FileReader(sourceProperties));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ Matcher m = mySourcePropPattern.matcher(line);
+ if (m.matches()) {
+ try {
+ toolsRevision = FullRevision.parseRevision(m.group(1));
+ } catch (NumberFormatException ignore) {}
+ break;
+ }
+ }
+ } catch (IOException e) {
+ String msg = String.format("Failed to read file: '%1$s' for Android SDK at '%2$s'", SdkConstants.FN_SOURCE_PROP, sdkPath);
+ LOG.error(msg, e);
+ } finally {
+ Closeables.closeQuietly(reader);
+ }
+ return new VersionCheckResult(toolsRevision);
+ }
+
+ public static class VersionCheckResult {
+ @NotNull private final FullRevision myRevision;
+ private final boolean myCompatibleVersion;
+
+ VersionCheckResult(@NotNull FullRevision revision) {
+ myRevision = revision;
+ myCompatibleVersion = revision.compareTo(MIN_TOOLS_REV, FullRevision.PreviewComparison.IGNORE) >= 0;
+ }
+
+ @NotNull
+ public FullRevision getRevision() {
+ return myRevision;
+ }
+
+ public boolean isCompatibleVersion() {
+ return myCompatibleVersion;
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/startup/AndroidCodeStyleSettingsModifier.java b/android/src/com/android/tools/idea/startup/AndroidCodeStyleSettingsModifier.java
index bf18105..db7fd3b 100644
--- a/android/src/com/android/tools/idea/startup/AndroidCodeStyleSettingsModifier.java
+++ b/android/src/com/android/tools/idea/startup/AndroidCodeStyleSettingsModifier.java
@@ -20,12 +20,19 @@
import com.intellij.psi.codeStyle.PackageEntry;
import com.intellij.psi.codeStyle.PackageEntryTable;
import org.jetbrains.android.formatter.AndroidXmlCodeStyleSettings;
+import org.jetbrains.android.formatter.AndroidXmlPredefinedCodeStyle;
public class AndroidCodeStyleSettingsModifier {
public static void modify(CodeStyleSettings settings) {
// Use Android XML formatter by default
AndroidXmlCodeStyleSettings.getInstance(settings).USE_CUSTOM_SETTINGS = true;
+ // XML:
+ // Copy Android code style
+ AndroidXmlPredefinedCodeStyle xmlStyle = new AndroidXmlPredefinedCodeStyle();
+ xmlStyle.apply(settings);
+
+ // Java:
// Set Import order
settings.IMPORT_LAYOUT_TABLE.copyFrom(getAndroidImportOrder());
diff --git a/android/src/com/android/tools/idea/startup/AndroidStudioSpecificInitializer.java b/android/src/com/android/tools/idea/startup/AndroidStudioSpecificInitializer.java
index cd3ccbd..b01044e 100644
--- a/android/src/com/android/tools/idea/startup/AndroidStudioSpecificInitializer.java
+++ b/android/src/com/android/tools/idea/startup/AndroidStudioSpecificInitializer.java
@@ -15,19 +15,25 @@
*/
package com.android.tools.idea.startup;
+import com.android.SdkConstants;
+import com.android.tools.idea.actions.AndroidImportProjectAction;
import com.android.tools.idea.actions.AndroidNewModuleAction;
import com.android.tools.idea.actions.AndroidNewModuleInGroupAction;
import com.android.tools.idea.actions.AndroidNewProjectAction;
+import com.android.tools.idea.sdk.VersionCheck;
import com.google.common.io.Closeables;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.codeStyle.CodeStyleScheme;
import com.intellij.psi.codeStyle.CodeStyleSchemes;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.util.SystemProperties;
+import org.jetbrains.android.sdk.AndroidSdkType;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.Nullable;
@@ -35,14 +41,13 @@
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
-import java.util.List;
import java.util.Properties;
/** Initialization performed only in the context of the Android IDE. */
public class AndroidStudioSpecificInitializer implements Runnable {
private static final Logger LOG = Logger.getInstance("#com.android.tools.idea.startup.AndroidStudioSpecificInitializer");
- @NonNls public static final String NEW_NEW_PROJECT_WIZARD = "android.newProjectWizard";
+ @NonNls private static final String USE_IDEA_NEW_PROJECT_WIZARDS = "use.idea.newProjectWizard";
@NonNls private static final String ANDROID_SDK_FOLDER_NAME = "sdk";
@@ -54,8 +59,8 @@
public void run() {
// Fix New Project actions
//noinspection UseOfArchaicSystemPropertyAccessors
- if (System.getProperty(NEW_NEW_PROJECT_WIZARD) == null || Boolean.getBoolean(NEW_NEW_PROJECT_WIZARD)) {
- fixNewProjectActions();
+ if (!Boolean.getBoolean(USE_IDEA_NEW_PROJECT_WIZARDS)) {
+ replaceIdeaActions();
}
try {
@@ -76,7 +81,7 @@
}
}
- private static void fixNewProjectActions() {
+ private static void replaceIdeaActions() {
// TODO: This is temporary code. We should build out our own menu set and welcome screen exactly how we want. In the meantime,
// unregister IntelliJ's version of the project actions and manually register our own.
@@ -84,6 +89,8 @@
replaceAction("WelcomeScreen.CreateNewProject", new AndroidNewProjectAction());
replaceAction("NewModule", new AndroidNewModuleAction());
replaceAction("NewModuleInGroup", new AndroidNewModuleInGroupAction());
+ replaceAction("ImportProject", new AndroidImportProjectAction());
+ replaceAction("WelcomeScreen.ImportProject", new AndroidImportProjectAction());
}
private static void replaceAction(String actionId, AnAction newAction) {
@@ -95,19 +102,29 @@
}
private static void setupSdks() {
- Sdk sdk = findFirstAndroidSdk();
+ Sdk sdk = findFirstCompatibleAndroidSdk();
if (sdk != null) {
return;
}
- String androidSdkPath = getAndroidSdkPath();
- AndroidSdkUtils.createNewAndroidPlatform(androidSdkPath);
+ // Called in a 'invokeLater' block, otherwise file chooser will hang forever.
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ String androidSdkPath = getAndroidSdkPath();
+ AndroidSdkUtils.createNewAndroidPlatform(androidSdkPath);
+ }
+ });
}
@Nullable
- private static Sdk findFirstAndroidSdk() {
- // TODO check version
- List<Sdk> allSdks = AndroidSdkUtils.getAllAndroidSdks();
- return allSdks.isEmpty() ? null : allSdks.get(0);
+ private static Sdk findFirstCompatibleAndroidSdk() {
+ for (Sdk sdk : AndroidSdkUtils.getAllAndroidSdks()) {
+ String sdkPath = sdk.getHomePath();
+ if (VersionCheck.isCompatibleVersion(sdkPath)) {
+ return sdk;
+ }
+ }
+ return null;
}
@Nullable
@@ -120,28 +137,34 @@
LOG.info(String.format("Found Studio home directory at: '1$%s'", studioHome));
for (String path : ANDROID_SDK_RELATIVE_PATHS) {
File dir = new File(studioHome, path);
- LOG.info(String.format("Looking for Android SDK at '1$%s'", dir.getAbsolutePath()));
- if (dir.isDirectory()) {
- LOG.info(String.format("Found Android SDK at '1$%s'", dir.getAbsolutePath()));
- return dir.getAbsolutePath();
+ String absolutePath = dir.getAbsolutePath();
+ LOG.info(String.format("Looking for Android SDK at '1$%s'", absolutePath));
+ if (AndroidSdkType.getInstance().isValidSdkHome(absolutePath) && VersionCheck.isCompatibleVersion(dir)) {
+ LOG.info(String.format("Found Android SDK at '1$%s'", absolutePath));
+ return absolutePath;
}
}
}
- LOG.info("Unable to locate SDK within the Android studio installation");
+ LOG.info("Unable to locate SDK within the Android studio installation.");
- String androidHomeValue = System.getenv(AndroidSdkUtils.ANDROID_HOME_ENV);
- String msg = String.format("Value of property '%1$s' is '%2$s'", AndroidSdkUtils.ANDROID_HOME_ENV, androidHomeValue);
+ String androidHomeValue = System.getenv(SdkConstants.ANDROID_HOME_ENV);
+ String msg = String.format("Checking if ANDROID_HOME is set: '%1$s' is '%2$s'", SdkConstants.ANDROID_HOME_ENV, androidHomeValue);
LOG.info(msg);
- if (androidHomeValue != null) {
+ if (!StringUtil.isEmpty(androidHomeValue) &&
+ AndroidSdkType.getInstance().isValidSdkHome(androidHomeValue) &&
+ VersionCheck.isCompatibleVersion(androidHomeValue)) {
+ LOG.info("Using Android SDK specified by the environment variable.");
return androidHomeValue;
}
String sdkPath = getLastSdkPathUsedByAndroidTools();
- if (sdkPath == null) {
- msg = "Unable to locate last SDK used by Android tools";
+ if (!StringUtil.isEmpty(sdkPath) &&
+ AndroidSdkType.getInstance().isValidSdkHome(androidHomeValue) &&
+ VersionCheck.isCompatibleVersion(sdkPath)) {
+ msg = String.format("Last SDK used by Android tools: '%1$s'", sdkPath);
} else {
- msg = String.format("Last SDK used by Android tools: '1$%s'", sdkPath);
+ msg = "Unable to locate last SDK used by Android tools";
}
LOG.info(msg);
return sdkPath;
diff --git a/android/src/com/android/tools/idea/stats/AndroidStatisticsService.java b/android/src/com/android/tools/idea/stats/AndroidStatisticsService.java
index 4051e2b..e40e8f9 100755
--- a/android/src/com/android/tools/idea/stats/AndroidStatisticsService.java
+++ b/android/src/com/android/tools/idea/stats/AndroidStatisticsService.java
@@ -43,23 +43,17 @@
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.util.*;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
/**
* Android Statistics Service.
* Based on idea's RemotelyConfigurableStatisticsService.
- *
+ * Also sends a legacy ping using ADT's LegacySdkStatsService.
*/
@SuppressWarnings("MethodMayBeStatic")
public class AndroidStatisticsService implements StatisticsService {
private static final Logger LOG = Logger.getInstance("#" + AndroidStatisticsService.class.getName());
-
- private static final String SYS_PROP_OS_ARCH = "os.arch";
- private static final String SYS_PROP_JAVA_VERSION = "java.version";
- private static final String SYS_PROP_OS_VERSION = "os.version";
- private static final String SYS_PROP_OS_NAME = "os.name";
+ private static final boolean VERBOSE = false;
private static final String CONTENT_TYPE = "Content-Type";
private static final String HTTP_POST = "POST";
@@ -68,10 +62,10 @@
private final long myNow = System.currentTimeMillis();
-
@NonNull
@Override
- public Notification createNotification(@NotNull final String groupDisplayId, @Nullable NotificationListener listener) {
+ public Notification createNotification(@NotNull final String groupDisplayId,
+ @Nullable NotificationListener listener) {
final String fullProductName = ApplicationNamesInfo.getInstance().getFullProductName();
final String companyName = ApplicationInfo.getInstance().getCompanyName();
@@ -107,9 +101,19 @@
return labels;
}
+ @SuppressWarnings("ConstantConditions")
@Override
public StatisticsResult send() {
+
+ // --- Send legacy ping ---
+
+ // Legacy ADT-compatible stats service.
+ LegacySdkStatsService sdkstats = new LegacySdkStatsService();
+ sdkstats.ping("studio", ApplicationInfo.getInstance().getFullVersion());
+
+ // --- Send new-style stats ---
+
// Get the redirected URL
final StatisticsConnectionService service = new StatisticsConnectionService();
final String serviceUrl = service.getServiceUrl();
@@ -122,7 +126,7 @@
return new StatisticsResult(StatisticsResult.ResultCode.NOT_PERMITTED_SERVER, "NOT_PERMITTED");
}
- StatsProto.LogRequest data = getData(service.getDisabledGroups());
+ StatsProto.LogRequest data = getData(sdkstats, service.getDisabledGroups());
String error = null;
try {
@@ -134,7 +138,9 @@
error = e.getClass().getSimpleName() + " " + (e.getMessage() != null ? e.getMessage() : e.toString());
}
- LOG.debug("[SendStats/AS] Error " + (error == null ? "None" : error));
+ if (VERBOSE || error != null) {
+ LOG.debug("[SendStats/AS] Error " + (error == null ? "None" : error));
+ }
if (error == null) {
return new StatisticsResult(StatisticsResult.ResultCode.SEND, "OK");
} else {
@@ -142,7 +148,8 @@
}
}
- private StatsProto.LogRequest getData(@NotNull Set<String> disabledGroups) {
+ private StatsProto.LogRequest getData(@NotNull LegacySdkStatsService sdkstats,
+ @NotNull Set<String> disabledGroups) {
Project[] openProjects = ProjectManager.getInstance().getOpenProjects();
Map<String, KeyString[]> usages = new LinkedHashMap<String, KeyString[]>();
@@ -153,11 +160,12 @@
String uuid = UpdateChecker.getInstallationUID(PropertiesComponent.getInstance());
String appVersion = ApplicationInfo.getInstance().getFullVersion();
- return createRequest(uuid, appVersion, usages);
+ return createRequest(sdkstats, uuid, appVersion, usages);
}
@NotNull
- public Map<String, KeyString[]> getAllUsages(@Nullable Project project, @NotNull Set<String> disabledGroups) {
+ public Map<String, KeyString[]> getAllUsages(@Nullable Project project,
+ @NotNull Set<String> disabledGroups) {
Map<String, KeyString[]> allUsages = new LinkedHashMap<String, KeyString[]>();
for (UsagesCollector usagesCollector : Extensions.getExtensions(UsagesCollector.EP_NAME)) {
@@ -171,7 +179,9 @@
for (UsageDescriptor usage : usages) {
Counter counter = new Counter(usage.getKey(), usage.getValue());
counters.add(counter);
- LOG.info("[" + groupId + "] " + counter); // RM--DEBUG
+ if (VERBOSE) {
+ LOG.info("[" + groupId + "] " + counter);
+ }
}
allUsages.put(groupId, counters.toArray(new Counter[counters.size()]));
@@ -185,7 +195,8 @@
}
/** Sends data. Returns an error if something occurred. */
- public String sendData(StatsProto.LogRequest request) throws IOException {
+ @Nullable
+ public String sendData(@NotNull StatsProto.LogRequest request) throws IOException {
if (request == null) {
return "[SendStats] Invalid arguments";
@@ -217,27 +228,31 @@
return "[SendStats] Error " + code;
}
- public StatsProto.LogRequest createRequest(String uuid, String appVersion, Map<String, KeyString[]> usages) {
+ public StatsProto.LogRequest createRequest(@NotNull LegacySdkStatsService sdkstats,
+ @NotNull String uuid,
+ @NotNull String appVersion,
+ @NotNull Map<String, KeyString[]> usages) {
StatsProto.LogRequest.Builder request = StatsProto.LogRequest.newBuilder();
request.setLogSource(StatsProto.LogRequest.LogSource.ANDROID_STUDIO);
request.setRequestTimeMs(myNow);
- request.setClientInfo(createClientInfo(uuid, appVersion));
+ request.setClientInfo(createClientInfo(sdkstats, uuid, appVersion));
for (Map.Entry<String, KeyString[]> entry : usages.entrySet()) {
request.addLogEvent(createEvent(entry.getKey(), entry.getValue()));
}
request.addLogEvent(createEvent("jvm", new KeyString[] {
- new KeyString("jvm-info", getJvmInfo()),
- new KeyString("jvm-vers", getJvmVersion()),
- new KeyString("jvm-arch", getJvmArch())
+ new KeyString("jvm-info", sdkstats.getJvmInfo()),
+ new KeyString("jvm-vers", sdkstats.getJvmVersion()),
+ new KeyString("jvm-arch", sdkstats.getJvmArch())
} ));
return request.build();
}
- private StatsProto.LogEvent createEvent(String groupId, KeyString[] values) {
+ private StatsProto.LogEvent createEvent(@NotNull String groupId,
+ @NotNull KeyString[] values) {
StatsProto.LogEvent.Builder evtBuilder = StatsProto.LogEvent.newBuilder();
evtBuilder.setEventTimeMs(myNow);
evtBuilder.setTag(groupId);
@@ -252,11 +267,13 @@
return evtBuilder.build();
}
- private StatsProto.ClientInfo createClientInfo(String uuid, String appVersion) {
+ private StatsProto.ClientInfo createClientInfo(@NotNull LegacySdkStatsService sdkstats,
+ @NotNull String uuid,
+ @NotNull String appVersion) {
StatsProto.DesktopClientInfo.Builder desktop = StatsProto.DesktopClientInfo.newBuilder();
desktop.setClientId(uuid);
- OsInfo info = getOsName();
+ OsInfo info = sdkstats.getOsName();
desktop.setOs(info.getOsName());
String os_vers = info.getOsVersion();
if (os_vers != null) {
@@ -270,252 +287,4 @@
cinfo.setDesktopClientInfo(desktop);
return cinfo.build();
}
-
- private static class OsInfo {
- private String myOsName;
- private String myOsVersion;
-
- public OsInfo setOsName(String osName) {
- myOsName = osName;
- return this;
- }
-
- public OsInfo setOsVersion(String osVersion) {
- myOsVersion = osVersion;
- return this;
- }
-
- public String getOsName() {
- return myOsName;
- }
-
- public String getOsVersion() {
- return myOsVersion;
- }
-
- public String getOsFull() {
- String os = myOsName;
- if (myOsVersion != null) {
- os += "-" + myOsVersion;
- }
- return os;
- }
- }
-
- /**
- * Detects and reports the host OS: "linux", "win" or "mac".
- * For Windows and Mac also append the version, so for example
- * Win XP will return win-5.1.
- * <p/>
- * Extracted from sdkstats/src/main/java/com/android/sdkstats/SdkStatsService.java
- */
- private OsInfo getOsName() { // made protected for testing
- String os = getSystemProperty(SYS_PROP_OS_NAME);
-
- OsInfo info = new OsInfo();
-
- if (os == null || os.length() == 0) {
- return info.setOsName("unknown");
- }
-
- String os2 = os.toLowerCase(Locale.US);
- String osVers = null;
-
- if (os2.startsWith("mac")) {
- os = "mac";
- osVers = getOsVersion();
-
- } else if (os2.startsWith("win")) {
- os = "win";
- osVers = getOsVersion();
-
- } else if (os2.startsWith("linux")) {
- os = "linux";
-
- } else if (os.length() > 32) {
- // Unknown -- send it verbatim so we can see it
- // but protect against arbitrarily long values
- os = os.substring(0, 32);
- }
-
- info.setOsName(os);
- info.setOsVersion(osVers);
-
- return info;
- }
-
- /**
- * Detects and returns the OS architecture: x86, x86_64, ppc.
- * This may differ or be equal to the JVM architecture in the sense that
- * a 64-bit OS can run a 32-bit JVM.
- * <p/>
- * Extracted from sdkstats/src/main/java/com/android/sdkstats/SdkStatsService.java
- */
- private String getOsArch() {
- String arch = getJvmArch();
-
- if ("x86_64".equals(arch)) {
- // This is a simple case: the JVM runs in 64-bit so the
- // OS must be a 64-bit one.
- return arch;
-
- } else if ("x86".equals(arch)) {
- // This is the misleading case: the JVM is 32-bit but the OS
- // might be either 32 or 64. We can't tell just from this
- // property.
- // Macs are always on 64-bit, so we just need to figure it
- // out for Windows and Linux.
-
- String os = getOsName().getOsName();
- if (os.startsWith("win")) {
- // When WOW64 emulates a 32-bit environment under a 64-bit OS,
- // it sets PROCESSOR_ARCHITEW6432 to AMD64 or IA64 accordingly.
- // Ref: http://msdn.microsoft.com/en-us/library/aa384274(v=vs.85).aspx
-
- String w6432 = getSystemEnv("PROCESSOR_ARCHITEW6432");
- if (w6432 != null && w6432.contains("64")) {
- return "x86_64";
- }
- } else if (os.startsWith("linux")) {
- // Let's try the obvious. This works in Ubuntu and Debian
- String s = getSystemEnv("HOSTTYPE");
-
- s = sanitizeOsArch(s);
- if (s.contains("86")) {
- arch = s;
- }
- }
- }
-
- return arch;
- }
-
- /**
- * Returns the version of the OS version if it is defined as X.Y, or null otherwise.
- * <p/>
- * Example of returned versions can be found at http://lopica.sourceforge.net/os.html
- * <p/>
- * This method removes any exiting micro versions.
- * Returns null if the version doesn't match X.Y.Z.
- * <p/>
- * Extracted from sdkstats/src/main/java/com/android/sdkstats/SdkStatsService.java
- */
- private String getOsVersion() {
- Pattern p = Pattern.compile("(\\d+)\\.(\\d+).*");
- String osVers = getSystemProperty(SYS_PROP_OS_VERSION);
- if (osVers != null && osVers.length() > 0) {
- Matcher m = p.matcher(osVers);
- if (m.matches()) {
- return m.group(1) + '.' + m.group(2);
- }
- }
- return null;
- }
-
- /**
- * Detects and returns the JVM info: version + architecture.
- * Examples: 1.4-ppc, 1.6-x86, 1.7-x86_64
- * <p/>
- * Extracted from sdkstats/src/main/java/com/android/sdkstats/SdkStatsService.java
- */
- private String getJvmInfo() {
- return getJvmVersion() + '-' + getJvmArch();
- }
-
- /**
- * Returns the major.minor Java version.
- * <p/>
- * The "java.version" property returns something like "1.6.0_20"
- * of which we want to return "1.6".
- * <p/>
- * Extracted from sdkstats/src/main/java/com/android/sdkstats/SdkStatsService.java
- */
- private String getJvmVersion() {
- String version = getSystemProperty(SYS_PROP_JAVA_VERSION);
-
- if (version == null || version.length() == 0) {
- return "unknown";
- }
-
- Pattern p = Pattern.compile("(\\d+)\\.(\\d+).*");
- Matcher m = p.matcher(version);
- if (m.matches()) {
- return m.group(1) + '.' + m.group(2);
- }
-
- // Unknown version. Send it as-is within a reasonable size limit.
- if (version.length() > 8) {
- version = version.substring(0, 8);
- }
- return version;
- }
-
- /**
- * Detects and returns the JVM architecture.
- * <p/>
- * The HotSpot JVM has a private property for this, "sun.arch.data.model",
- * which returns either "32" or "64". However it's not in any kind of spec.
- * <p/>
- * What we want is to know whether the JVM is running in 32-bit or 64-bit and
- * the best indicator is to use the "os.arch" property.
- * - On a 32-bit system, only a 32-bit JVM can run so it will be x86 or ppc.<br/>
- * - On a 64-bit system, a 32-bit JVM will also return x86 since the OS needs
- * to masquerade as a 32-bit OS for backward compatibility.<br/>
- * - On a 64-bit system, a 64-bit JVM will properly return x86_64.
- * <pre>
- * JVM: Java 32-bit Java 64-bit
- * Windows: x86 x86_64
- * Linux: x86 x86_64
- * Mac untested x86_64
- * </pre>
- * <p/>
- * Extracted from sdkstats/src/main/java/com/android/sdkstats/SdkStatsService.java
- */
- private String getJvmArch() {
- String arch = getSystemProperty(SYS_PROP_OS_ARCH);
- return sanitizeOsArch(arch);
- }
-
- private String sanitizeOsArch(String arch) {
- if (arch == null || arch.length() == 0) {
- return "unknown";
- }
-
- if (arch.equalsIgnoreCase("x86_64") ||
- arch.equalsIgnoreCase("ia64") ||
- arch.equalsIgnoreCase("amd64")) {
- return "x86_64";
- }
-
- if (arch.length() >= 4 && arch.charAt(0) == 'i' && arch.indexOf("86") == 2) {
- // Any variation of iX86 counts as x86 (i386, i486, i686).
- return "x86";
- }
-
- if (arch.equalsIgnoreCase("PowerPC")) {
- return "ppc";
- }
-
- // Unknown arch. Send it as-is but protect against arbitrarily long values.
- if (arch.length() > 32) {
- arch = arch.substring(0, 32);
- }
- return arch;
- }
-
- /**
- * Helper to call {@link System#getProperty(String)}.
- * @see System#getProperty(String)
- */
- private String getSystemProperty(String name) {
- return System.getProperty(name);
- }
-
- /**
- * Helper to call {@link System#getenv(String)}.
- * @see System#getenv(String)
- */
- private String getSystemEnv(String name) {
- return System.getenv(name);
- }
}
diff --git a/android/src/com/android/tools/idea/stats/DdmsPreferenceStore.java b/android/src/com/android/tools/idea/stats/DdmsPreferenceStore.java
new file mode 100755
index 0000000..9565413
--- /dev/null
+++ b/android/src/com/android/tools/idea/stats/DdmsPreferenceStore.java
@@ -0,0 +1,339 @@
+/*
+ * 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.tools.idea.stats;
+
+import com.android.prefs.AndroidLocation;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+import com.intellij.internal.statistic.StatisticsUploadAssistant;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Random;
+
+/**
+ * Manages persistence settings for DDMS.
+ * <p/>
+ * For convenience, this also stores persistence settings related to the "server stats" ping
+ * as well as some ADT settings that are SDK specific but not workspace specific.
+ * <p/>
+ * This is mostly a copy of the tools/swt/sdkstats counterpart, adapted for compatibility with Studio.
+ */
+public class DdmsPreferenceStore {
+
+ public final static String PING_OPT_IN = "pingOptIn"; //$NON-NLS-1$
+ private final static String PING_TIME = "pingTime"; //$NON-NLS-1$
+ private final static String PING_ID = "pingId"; //$NON-NLS-1$
+
+ private final static String ADT_USED = "adtUsed"; //$NON-NLS-1$
+ private final static String LAST_SDK_PATH = "lastSdkPath"; //$NON-NLS-1$
+
+ /**
+ * PreferenceStore for DDMS.
+ * Creation and usage must be synchronized on {@code DdmsPreferenceStore.class}.
+ * Don't use it directly, instead retrieve it via {@link #getPreferenceStore()}.
+ */
+ private static volatile PreferenceStore sPrefStore;
+
+ public DdmsPreferenceStore() {
+ }
+
+ /**
+ * Returns the DDMS {@link PreferenceStore}.
+ * This keeps a static reference on the store, so consequent calls will
+ * return always the same store.
+ */
+ public PreferenceStore getPreferenceStore() {
+ synchronized (DdmsPreferenceStore.class) {
+ if (sPrefStore == null) {
+ // get the location of the preferences
+ String homeDir = null;
+ try {
+ homeDir = AndroidLocation.getFolder();
+ } catch (AndroidLocationException e1) {
+ // pass, we'll do a dummy store since homeDir is null
+ }
+
+ if (homeDir == null) {
+ sPrefStore = new PreferenceStore();
+ return sPrefStore;
+ }
+
+ assert homeDir != null;
+
+ String rcFileName = homeDir + "ddms.cfg"; //$NON-NLS-1$
+
+ // also look for an old pref file in the previous location
+ String oldPrefPath = System.getProperty("user.home") //$NON-NLS-1$
+ + File.separator + ".ddmsrc"; //$NON-NLS-1$
+ File oldPrefFile = new File(oldPrefPath);
+ if (oldPrefFile.isFile()) {
+ FileOutputStream fileOutputStream = null;
+ try {
+ PreferenceStore oldStore = new PreferenceStore(oldPrefPath);
+ oldStore.load();
+
+ fileOutputStream = new FileOutputStream(rcFileName);
+ oldStore.save(fileOutputStream, ""); //$NON-NLS-1$
+ oldPrefFile.delete();
+
+ PreferenceStore newStore = new PreferenceStore(rcFileName);
+ newStore.load();
+ sPrefStore = newStore;
+ } catch (IOException e) {
+ // create a new empty store.
+ sPrefStore = new PreferenceStore(rcFileName);
+ } finally {
+ if (fileOutputStream != null) {
+ try {
+ fileOutputStream.close();
+ } catch (IOException e) {
+ // pass
+ }
+ }
+ }
+ }
+ else {
+ sPrefStore = new PreferenceStore(rcFileName);
+
+ try {
+ sPrefStore.load();
+ } catch (IOException e) {
+ System.err.println("Error Loading DDMS Preferences");
+ }
+ }
+ }
+
+ assert sPrefStore != null;
+ return sPrefStore;
+ }
+ }
+
+ /**
+ * Save the prefs to the config file.
+ */
+ public void save() {
+ PreferenceStore prefs = getPreferenceStore();
+ synchronized (DdmsPreferenceStore.class) {
+ try {
+ prefs.save();
+ } catch (IOException ioe) {
+ // FIXME com.android.dmmlib.Log.w("ddms", "Failed saving prefs file: " + ioe.getMessage());
+ }
+ }
+ }
+
+ // ---- Utility methods to access some specific prefs ----
+
+ /**
+ * Indicates whether the ping ID is set.
+ * This should be true when {@link #isPingOptIn()} is true.
+ *
+ * @return true if a ping ID is set, which means the user gave permission
+ * to use the ping service.
+ */
+ public boolean hasPingId() {
+ PreferenceStore prefs = getPreferenceStore();
+ synchronized (DdmsPreferenceStore.class) {
+ return prefs != null && prefs.contains(PING_ID);
+ }
+ }
+
+ /**
+ * Retrieves the current ping ID, if set.
+ * To know if the ping ID is set, use {@link #hasPingId()}.
+ * <p/>
+ * There is no magic value reserved for "missing ping id or invalid store".
+ * The only proper way to know if the ping id is missing is to use {@link #hasPingId()}.
+ */
+ public long getPingId() {
+ PreferenceStore prefs = getPreferenceStore();
+ synchronized (DdmsPreferenceStore.class) {
+ // Note: getLong() returns 0L if the ID is missing so we do that too when
+ // there's no store.
+ return prefs == null ? 0L : prefs.getLong(PING_ID);
+ }
+ }
+
+ /**
+ * Generates a new random ping ID and saves it in the preference store.
+ *
+ * @return The new ping ID.
+ */
+ public long generateNewPingId() {
+ PreferenceStore prefs = getPreferenceStore();
+
+ Random rnd = new Random();
+ long id = rnd.nextLong();
+
+ synchronized (DdmsPreferenceStore.class) {
+ prefs.setValue(PING_ID, id);
+ try {
+ prefs.save();
+ } catch (IOException e) {
+ /* ignore exceptions while saving preferences */
+ }
+ }
+
+ return id;
+ }
+
+ /**
+ * Returns the "ping opt in" value from the preference store.
+ * This would be true if there's a valid preference store and
+ * the user opted for sending ping statistics.
+ */
+ public boolean isPingOptIn() {
+ // In the context of Studio, this checks whether the user approved to send stats
+ // from the settings > stats panel. The whole stats code will only be invoked
+ // if this is true, so it's almost certain this will return true.
+ if (StatisticsUploadAssistant.isSendAllowed()) {
+ return true;
+ }
+ // Legacy code from ADT/ddms. This won't be used here. Keep for reference.
+ PreferenceStore prefs = getPreferenceStore();
+ synchronized (DdmsPreferenceStore.class) {
+ return prefs != null && prefs.contains(PING_OPT_IN);
+ }
+ }
+
+ /**
+ * Saves the "ping opt in" value in the preference store.
+ *
+ * @param optIn The new user opt-in value.
+ */
+ public void setPingOptIn(boolean optIn) {
+ PreferenceStore prefs = getPreferenceStore();
+
+ synchronized (DdmsPreferenceStore.class) {
+ prefs.setValue(PING_OPT_IN, optIn);
+ try {
+ prefs.save();
+ } catch (IOException e) {
+ /* ignore exceptions while saving preferences */
+ }
+ }
+ }
+
+ /**
+ * Retrieves the ping time for the given app from the preference store.
+ * Callers should use {@link System#currentTimeMillis()} for time stamps.
+ *
+ * @param app The app name identifier.
+ * @return 0L if we don't have a preference store or there was no time
+ * recorded in the store for the requested app. Otherwise the time stamp
+ * from the store.
+ */
+ public long getPingTime(String app) {
+ PreferenceStore prefs = getPreferenceStore();
+ String timePref = PING_TIME + "." + app; //$NON-NLS-1$
+ synchronized (DdmsPreferenceStore.class) {
+ return prefs == null ? 0 : prefs.getLong(timePref);
+ }
+ }
+
+ /**
+ * Sets the ping time for the given app from the preference store.
+ * Callers should use {@link System#currentTimeMillis()} for time stamps.
+ *
+ * @param app The app name identifier.
+ * @param timeStamp The time stamp from the store.
+ * 0L is a special value that should not be used.
+ */
+ public void setPingTime(String app, long timeStamp) {
+ PreferenceStore prefs = getPreferenceStore();
+ String timePref = PING_TIME + "." + app; //$NON-NLS-1$
+ synchronized (DdmsPreferenceStore.class) {
+ prefs.setValue(timePref, timeStamp);
+ try {
+ prefs.save();
+ } catch (IOException ioe) {
+ /* ignore exceptions while saving preferences */
+ }
+ }
+ }
+
+ /**
+ * True if this is the first time the users runs ADT, which is detected by
+ * the lack of the setting set using {@link #setAdtUsed(boolean)}
+ * or this value being set to true.
+ *
+ * @return true if ADT has been used before
+ * @see #setAdtUsed(boolean)
+ */
+ public boolean isAdtUsed() {
+ PreferenceStore prefs = getPreferenceStore();
+ synchronized (DdmsPreferenceStore.class) {
+ if (prefs == null || !prefs.contains(ADT_USED)) {
+ return false;
+ }
+ return prefs.getBoolean(ADT_USED);
+ }
+ }
+
+ /**
+ * Sets whether the ADT startup wizard has been shown.
+ * ADT sets first to false once the welcome wizard has been shown once.
+ *
+ * @param used true if ADT has been used
+ */
+ public void setAdtUsed(boolean used) {
+ PreferenceStore prefs = getPreferenceStore();
+ synchronized (DdmsPreferenceStore.class) {
+ prefs.setValue(ADT_USED, used);
+ try {
+ prefs.save();
+ } catch (IOException ioe) {
+ /* ignore exceptions while saving preferences */
+ }
+ }
+ }
+
+ /**
+ * Retrieves the last SDK OS path.
+ * <p/>
+ * This is just an information value, the path may not exist, may not
+ * even be on an existing file system and/or may not point to an SDK
+ * anymore.
+ *
+ * @return The last SDK OS path from the preference store, or null if
+ * there is no store or an empty string if it is not defined.
+ */
+ public String getLastSdkPath() {
+ PreferenceStore prefs = getPreferenceStore();
+ synchronized (DdmsPreferenceStore.class) {
+ return prefs == null ? null : prefs.getString(LAST_SDK_PATH);
+ }
+ }
+
+ /**
+ * Sets the last SDK OS path.
+ *
+ * @param osSdkPath The SDK OS Path. Can be null or empty.
+ */
+ public void setLastSdkPath(String osSdkPath) {
+ PreferenceStore prefs = getPreferenceStore();
+ synchronized (DdmsPreferenceStore.class) {
+ prefs.setValue(LAST_SDK_PATH, osSdkPath);
+ try {
+ prefs.save();
+ } catch (IOException ioe) {
+ /* ignore exceptions while saving preferences */
+ }
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/stats/LegacySdkStatsService.java b/android/src/com/android/tools/idea/stats/LegacySdkStatsService.java
new file mode 100755
index 0000000..a6603a7
--- /dev/null
+++ b/android/src/com/android/tools/idea/stats/LegacySdkStatsService.java
@@ -0,0 +1,465 @@
+/*
+ * 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.tools.idea.stats;
+
+import com.intellij.openapi.diagnostic.Logger;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility class to send "ping" usage reports to the server.
+ * Imported from the android/tools/swt/sdkstats project, and simplified for usage in Studio.
+ * */
+@SuppressWarnings("MethodMayBeStatic")
+public class LegacySdkStatsService {
+
+ private static final Logger LOG = Logger.getInstance("#" + LegacySdkStatsService.class.getName());
+
+ protected static final String SYS_PROP_OS_ARCH = "os.arch"; //$NON-NLS-1$
+ protected static final String SYS_PROP_JAVA_VERSION = "java.version"; //$NON-NLS-1$
+ protected static final String SYS_PROP_OS_VERSION = "os.version"; //$NON-NLS-1$
+ protected static final String SYS_PROP_OS_NAME = "os.name"; //$NON-NLS-1$
+
+ /** Minimum interval between ping, in milliseconds. */
+ private static final long PING_INTERVAL_MSEC = 86400 * 1000; // 1 day
+
+ private static final boolean DEBUG = System.getenv("ANDROID_DEBUG_PING") != null; //$NON-NLS-1$
+
+ private DdmsPreferenceStore mStore = new DdmsPreferenceStore();
+
+ public LegacySdkStatsService() {
+ }
+
+ /**
+ * Send a "ping" to the Google toolbar server, if enough time has
+ * elapsed since the last ping, and if the user has not opted out.
+ * <p/>
+ * This is a simplified version of {@link #ping(String[])} that only
+ * sends an "application" name and a "version" string. See the explanation
+ * there for details.
+ *
+ * @param app The application name that reports the ping (e.g. "emulator" or "ddms".)
+ * Valid characters are a-zA-Z0-9 only.
+ * @param version The version string (e.g. "12" or "1.2.3.4", 4 groups max.)
+ */
+ public void ping(@NotNull String app, @NotNull String version) {
+ doPing(app, version, null);
+ }
+
+ // -------
+
+ /**
+ * Pings the usage stats server, as long as the prefs contain the opt-in boolean
+ *
+ * @param app The application name that reports the ping (e.g. "emulator" or "ddms".)
+ * Will be normalized. Valid characters are a-zA-Z0-9 only.
+ * @param version The version string (e.g. "12" or "1.2.3.4", 4 groups max.)
+ * @param extras Extra key/value parameters to send. They are send as-is and must
+ * already be well suited and escaped using {@link java.net.URLEncoder#encode(String, String)}.
+ */
+ protected void doPing(@NotNull String app,
+ @NotNull String version,
+ @Nullable final Map<String, String> extras) {
+ // Note: if you change the implementation here, you also need to change
+ // the overloaded SdkStatsServiceTest.doPing() used for testing.
+
+ // Validate the application and version input.
+ final String nApp = normalizeAppName(app);
+ final String nVersion = normalizeVersion(version);
+
+ // If the user has not opted in, do nothing and quietly return.
+ if (!mStore.isPingOptIn()) {
+ // user opted out.
+ return;
+ }
+
+ // If the last ping *for this app* was too recent, do nothing.
+ long now = System.currentTimeMillis();
+ long then = mStore.getPingTime(app);
+ if (now - then < PING_INTERVAL_MSEC) {
+ // too soon after a ping.
+ return;
+ }
+
+ // Record the time of the attempt, whether or not it succeeds.
+ mStore.setPingTime(app, now);
+
+ // Send the ping itself in the background (don't block if the
+ // network is down or slow or confused).
+ long id = mStore.getPingId();
+ if (id == 0) {
+ id = mStore.generateNewPingId();
+ }
+ try {
+ URL url = createPingUrl(nApp, nVersion, id, extras);
+ actuallySendPing(url);
+ } catch (IOException e) {
+ LOG.error("[AndroidSdk.SendPing failed", e);
+ }
+ }
+
+
+ /**
+ * Unconditionally send a "ping" request to the server.
+ *
+ * @param url The URL to send to the server.
+ * * @throws IOException if the ping failed
+ */
+ private void actuallySendPing(URL url) throws IOException {
+ assert url != null;
+
+ if (DEBUG) {
+ LOG.debug("Ping: " + url.toString()); //$NON-NLS-1$
+ }
+
+ // Discard the actual response, but make sure it reads OK
+ HttpURLConnection conn = (HttpURLConnection)url.openConnection();
+
+ // Believe it or not, a 404 response indicates success:
+ // the ping was logged, but no update is configured.
+ if (conn.getResponseCode() != HttpURLConnection.HTTP_OK &&
+ conn.getResponseCode() != HttpURLConnection.HTTP_NOT_FOUND) {
+ throw new IOException(conn.getResponseMessage() + ": " + url); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Compute the ping URL to send the data to the server.
+ *
+ * @param app The application name that reports the ping (e.g. "emulator" or "ddms".)
+ * Valid characters are a-zA-Z0-9 only.
+ * @param version The version string already formatted as a 4 dotted group (e.g. "1.2.3.4".)
+ * @param id of the local installation
+ * @param extras Extra key/value parameters to send. They are send as-is and must
+ * already be well suited and escaped using {@link java.net.URLEncoder#encode(String, String)}.
+ */
+ protected URL createPingUrl(@NotNull String app,
+ @NotNull String version,
+ long id,
+ @Nullable Map<String, String> extras)
+ throws UnsupportedEncodingException, MalformedURLException {
+
+ String osName = URLEncoder.encode(getOsName().getOsFull(), "UTF-8"); //$NON-NLS-1$
+ String osArch = URLEncoder.encode(getOsArch(), "UTF-8"); //$NON-NLS-1$
+ String jvmArch = URLEncoder.encode(getJvmInfo(), "UTF-8"); //$NON-NLS-1$
+
+ // Include the application's name as part of the as= value.
+ // Share the user ID for all apps, to allow unified activity reports.
+
+ String extraStr = ""; //$NON-NLS-1$
+ if (extras != null && !extras.isEmpty()) {
+ StringBuilder sb = new StringBuilder();
+ for (Map.Entry<String, String> entry : extras.entrySet()) {
+ sb.append('&').append(entry.getKey()).append('=').append(entry.getValue());
+ }
+ extraStr = sb.toString();
+ }
+
+ //noinspection UnnecessaryLocalVariable
+ URL url = new URL("http", //$NON-NLS-1$
+ "tools.google.com", //$NON-NLS-1$
+ "/service/update?as=androidsdk_" + app + //$NON-NLS-1$
+ "&id=" + Long.toHexString(id) + //$NON-NLS-1$
+ "&version=" + version + //$NON-NLS-1$
+ "&os=" + osName + //$NON-NLS-1$
+ "&osa=" + osArch + //$NON-NLS-1$
+ "&vma=" + jvmArch + //$NON-NLS-1$
+ extraStr);
+ return url;
+ }
+
+ /**
+ * Detects and reports the host OS: "linux", "win" or "mac".
+ * For Windows and Mac also append the version, so for example
+ * Win XP will return win-5.1.
+ */
+ OsInfo getOsName() { // made protected for testing
+ String os = getSystemProperty(SYS_PROP_OS_NAME);
+
+ OsInfo info = new OsInfo();
+
+ if (os == null || os.length() == 0) {
+ return info.setOsName("unknown");
+ }
+
+ String os2 = os.toLowerCase(Locale.US);
+ String osVers = null;
+
+ if (os2.startsWith("mac")) {
+ os = "mac";
+ osVers = getOsVersion();
+
+ }
+ else if (os2.startsWith("win")) {
+ os = "win";
+ osVers = getOsVersion();
+
+ }
+ else if (os2.startsWith("linux")) {
+ os = "linux";
+
+ }
+ else if (os.length() > 32) {
+ // Unknown -- send it verbatim so we can see it
+ // but protect against arbitrarily long values
+ os = os.substring(0, 32);
+ }
+
+ info.setOsName(os);
+ info.setOsVersion(osVers);
+
+ return info;
+ }
+
+ /**
+ * Detects and returns the OS architecture: x86, x86_64, ppc.
+ * This may differ or be equal to the JVM architecture in the sense that
+ * a 64-bit OS can run a 32-bit JVM.
+ */
+ String getOsArch() { // made protected for testing
+ String arch = getJvmArch();
+
+ if ("x86_64".equals(arch)) { //$NON-NLS-1$
+ // This is a simple case: the JVM runs in 64-bit so the
+ // OS must be a 64-bit one.
+ return arch;
+
+ }
+ else if ("x86".equals(arch)) { //$NON-NLS-1$
+ // This is the misleading case: the JVM is 32-bit but the OS
+ // might be either 32 or 64. We can't tell just from this
+ // property.
+ // Macs are always on 64-bit, so we just need to figure it
+ // out for Windows and Linux.
+
+ String os = getOsName().getOsName();
+ if (os.startsWith("win")) { //$NON-NLS-1$
+ // When WOW64 emulates a 32-bit environment under a 64-bit OS,
+ // it sets PROCESSOR_ARCHITEW6432 to AMD64 or IA64 accordingly.
+ // Ref: http://msdn.microsoft.com/en-us/library/aa384274(v=vs.85).aspx
+
+ String w6432 = getSystemEnv("PROCESSOR_ARCHITEW6432"); //$NON-NLS-1$
+ if (w6432 != null && w6432.contains("64")) { //$NON-NLS-1$
+ return "x86_64"; //$NON-NLS-1$
+ }
+ }
+ else if (os.startsWith("linux")) { //$NON-NLS-1$
+ // Let's try the obvious. This works in Ubuntu and Debian
+ String s = getSystemEnv("HOSTTYPE"); //$NON-NLS-1$
+
+ s = sanitizeOsArch(s);
+ if (s.contains("86")) { //$NON-NLS-1$
+ arch = s;
+ }
+ }
+ }
+
+ return arch;
+ }
+
+ /**
+ * Returns the version of the OS version if it is defined as X.Y, or null otherwise.
+ * <p/>
+ * Example of returned versions can be found at http://lopica.sourceforge.net/os.html
+ * <p/>
+ * This method removes any exiting micro versions.
+ * Returns null if the version doesn't match X.Y.Z.
+ */
+ @Nullable
+ String getOsVersion() { // made protected for testing
+ Pattern p = Pattern.compile("(\\d+)\\.(\\d+).*"); //$NON-NLS-1$
+ String osVers = getSystemProperty(SYS_PROP_OS_VERSION);
+ if (osVers != null && osVers.length() > 0) {
+ Matcher m = p.matcher(osVers);
+ if (m.matches()) {
+ return m.group(1) + '.' + m.group(2);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Detects and returns the JVM info: version + architecture.
+ * Examples: 1.4-ppc, 1.6-x86, 1.7-x86_64
+ */
+ String getJvmInfo() { // made protected for testing
+ return getJvmVersion() + '-' + getJvmArch();
+ }
+
+ /**
+ * Returns the major.minor Java version.
+ * <p/>
+ * The "java.version" property returns something like "1.6.0_20"
+ * of which we want to return "1.6".
+ */
+ String getJvmVersion() { // made protected for testing
+ String version = getSystemProperty(SYS_PROP_JAVA_VERSION);
+
+ if (version == null || version.length() == 0) {
+ return "unknown"; //$NON-NLS-1$
+ }
+
+ Pattern p = Pattern.compile("(\\d+)\\.(\\d+).*"); //$NON-NLS-1$
+ Matcher m = p.matcher(version);
+ if (m.matches()) {
+ return m.group(1) + '.' + m.group(2);
+ }
+
+ // Unknown version. Send it as-is within a reasonable size limit.
+ if (version.length() > 8) {
+ version = version.substring(0, 8);
+ }
+ return version;
+ }
+
+ /**
+ * Detects and returns the JVM architecture.
+ * <p/>
+ * The HotSpot JVM has a private property for this, "sun.arch.data.model",
+ * which returns either "32" or "64". However it's not in any kind of spec.
+ * <p/>
+ * What we want is to know whether the JVM is running in 32-bit or 64-bit and
+ * the best indicator is to use the "os.arch" property.
+ * - On a 32-bit system, only a 32-bit JVM can run so it will be x86 or ppc.<br/>
+ * - On a 64-bit system, a 32-bit JVM will also return x86 since the OS needs
+ * to masquerade as a 32-bit OS for backward compatibility.<br/>
+ * - On a 64-bit system, a 64-bit JVM will properly return x86_64.
+ * <pre>
+ * JVM: Java 32-bit Java 64-bit
+ * Windows: x86 x86_64
+ * Linux: x86 x86_64
+ * Mac untested x86_64
+ * </pre>
+ */
+ String getJvmArch() { // made protected for testing
+ String arch = getSystemProperty(SYS_PROP_OS_ARCH);
+ return sanitizeOsArch(arch);
+ }
+
+ private String sanitizeOsArch(String arch) {
+ if (arch == null || arch.length() == 0) {
+ return "unknown"; //$NON-NLS-1$
+ }
+
+ if (arch.equalsIgnoreCase("x86_64") || //$NON-NLS-1$
+ arch.equalsIgnoreCase("ia64") || //$NON-NLS-1$
+ arch.equalsIgnoreCase("amd64")) { //$NON-NLS-1$
+ return "x86_64"; //$NON-NLS-1$
+ }
+
+ if (arch.length() >= 4 && arch.charAt(0) == 'i' && arch.indexOf("86") == 2) { //$NON-NLS-1$
+ // Any variation of iX86 counts as x86 (i386, i486, i686).
+ return "x86"; //$NON-NLS-1$
+ }
+
+ if (arch.equalsIgnoreCase("PowerPC")) { //$NON-NLS-1$
+ return "ppc"; //$NON-NLS-1$
+ }
+
+ // Unknown arch. Send it as-is but protect against arbitrarily long values.
+ if (arch.length() > 32) {
+ arch = arch.substring(0, 32);
+ }
+ return arch;
+ }
+
+ /**
+ * Normalize the supplied application name.
+ *
+ * @param app to report
+ */
+ protected String normalizeAppName(String app) {
+ // Filter out \W , non-word character: [^a-zA-Z_0-9]
+ String app2 = app.replaceAll("\\W", ""); //$NON-NLS-1$ //$NON-NLS-2$
+
+ if (app.length() == 0) {
+ throw new IllegalArgumentException("Bad app name: " + app); //$NON-NLS-1$
+ }
+
+ return app2;
+ }
+
+ /**
+ * Validate the supplied application version, and normalize the version.
+ *
+ * @param version supplied by caller
+ * @return normalized dotted quad version
+ */
+ protected String normalizeVersion(String version) {
+ Pattern regex = Pattern.compile(
+ //1=major 2=minor 3=micro 4=build | 5=rc
+ "^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?(?:\\.(\\d+)| +rc(\\d+))?"); //$NON-NLS-1$
+
+ Matcher m = regex.matcher(version);
+ if (m != null && m.lookingAt()) {
+ StringBuilder normal = new StringBuilder();
+ for (int i = 1; i <= 4; i++) {
+ int v = 0;
+ // If build is null but we have an rc, take that number instead as the 4th part.
+ if (i == 4 &&
+ i < m.groupCount() &&
+ m.group(i) == null &&
+ m.group(i + 1) != null) {
+ //noinspection AssignmentToForLoopParameter
+ i++;
+ }
+ if (m.group(i) != null) {
+ try {
+ v = Integer.parseInt(m.group(i));
+ } catch (Exception ignore) {
+ }
+ }
+ if (i > 1) {
+ normal.append('.');
+ }
+ normal.append(v);
+ }
+ return normal.toString();
+ }
+
+ throw new IllegalArgumentException("Bad version: " + version); //$NON-NLS-1$
+ }
+
+ /**
+ * Calls {@link System#getProperty(String)}.
+ * Allows unit-test to override the return value.
+ * @see System#getProperty(String)
+ */
+ protected String getSystemProperty(String name) {
+ return System.getProperty(name);
+ }
+
+ /**
+ * Calls {@link System#getenv(String)}.
+ * Allows unit-test to override the return value.
+ * @see System#getenv(String)
+ */
+ protected String getSystemEnv(String name) {
+ return System.getenv(name);
+ }
+}
diff --git a/android/src/com/android/tools/idea/stats/OsInfo.java b/android/src/com/android/tools/idea/stats/OsInfo.java
new file mode 100755
index 0000000..98ea615
--- /dev/null
+++ b/android/src/com/android/tools/idea/stats/OsInfo.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.idea.stats;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+class OsInfo {
+ @NotNull
+ private String myOsName;
+ @Nullable
+ private String myOsVersion;
+
+ public OsInfo setOsName(@NotNull String osName) {
+ myOsName = osName;
+ return this;
+ }
+
+ public OsInfo setOsVersion(@Nullable String osVersion) {
+ myOsVersion = osVersion;
+ return this;
+ }
+
+ @NotNull
+ public String getOsName() {
+ return myOsName;
+ }
+
+ @Nullable
+ public String getOsVersion() {
+ return myOsVersion;
+ }
+
+ public String getOsFull() {
+ String os = myOsName;
+ if (myOsVersion != null) {
+ os += "-" + myOsVersion;
+ }
+ return os;
+ }
+}
diff --git a/android/src/com/android/tools/idea/stats/PreferenceStore.java b/android/src/com/android/tools/idea/stats/PreferenceStore.java
new file mode 100755
index 0000000..7893daa
--- /dev/null
+++ b/android/src/com/android/tools/idea/stats/PreferenceStore.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.idea.stats;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Properties;
+
+/**
+ * Basic re-implementation of the jface PreferenceStore interface.
+ * This implements the bare minimum needed by DdmsPreferenceStore which
+ * is trivial since the store is merely a wrapper on top of the standard
+ * java Properties.
+ */
+public class PreferenceStore {
+
+ private String myFilename;
+ private boolean myChanged = false;
+ private final Properties myProperties = new Properties();
+
+ /** Creates an empty store not associated with any file.
+ * Trying to invoke save() on this store will throw an exception. */
+ public PreferenceStore() {
+ myFilename = null;
+ }
+
+ /** Creates an empty store. To load from the file, caller should invoke load() */
+ public PreferenceStore(String filename) {
+ myFilename = filename;
+ }
+
+ /** Load from the consturctor's registered filename, erasing the current store. */
+ public void load() throws IOException {
+ if (myFilename == null) {
+ throw new IOException("No filename specified for PreferenceStore.");
+ }
+ FileInputStream in = new FileInputStream(myFilename);
+ try {
+ myProperties.load(in);
+ myChanged = false;
+ } finally {
+ in.close();
+ }
+ }
+
+ /** Save the current store if any value has changed since the last load. */
+ public void save() throws IOException {
+ if (myFilename == null) {
+ throw new IOException("No filename specified for PreferenceStore.");
+ }
+ if (myChanged) {
+ FileOutputStream out = new FileOutputStream(myFilename);
+ try {
+ save(out, null);
+ myChanged = false;
+ } finally {
+ out.close();
+ }
+ }
+ }
+
+ /** Unconditionally save the current store to the given stream. */
+ public void save(FileOutputStream outputStream, @Nullable String header) throws IOException {
+ myProperties.store(outputStream, header);
+ }
+
+ /** Returns true if the store contains a value for the given key. */
+ public boolean contains(String key) {
+ return myProperties.containsKey(key);
+ }
+
+ /** Returns the long value for the given key or 0 if the value is missing. */
+ public long getLong(String key) {
+ try {
+ return Long.parseLong(myProperties.getProperty(key));
+ } catch (Exception ignored) {
+ return 0; // IPreferenceStore.LONG_DEFAULT_DEFAULT is 0
+ }
+ }
+
+ /** Returns the boolean value for the given key or false if the value is missing. */
+ public boolean getBoolean(String key) {
+ try {
+ return Boolean.parseBoolean(myProperties.getProperty(key));
+ } catch (Exception ignored) {
+ return false; // IPreferenceStore.BOOLEAN_DEFAULT_DEFAULT is false
+ }
+ }
+
+ /** Returns the string value for the given key or the empty string if the value is missing.
+ * Note that the store doesn't store nulls and this never returns null. */
+ @NotNull
+ public String getString(String key) {
+ String s = myProperties.getProperty(key);
+ return s == null ? "" : s; // IPreferenceStore.STRING_DEFAULT_DEFAULT is ""
+ }
+
+ /** Sets the corresponding long value for the given key and marks the store as changed. */
+ public void setValue(String key, long value) {
+ myProperties.setProperty(key, Long.toString(value));
+ myChanged = true;
+ }
+
+ /** Sets the corresponding boolean value for the given key and marks the store as changed. */
+ public void setValue(String key, boolean value) {
+ myProperties.setProperty(key, Boolean.toString(value));
+ myChanged = true;
+ }
+
+ /** Sets the corresponding string value for the given key and marks the store as changed. */
+ public void setValue(String key, String value) {
+ myProperties.setProperty(key, value);
+ myChanged = true;
+ }
+}
diff --git a/android/src/com/android/tools/idea/templates/Parameter.java b/android/src/com/android/tools/idea/templates/Parameter.java
index 9cd808c..ac8026f 100644
--- a/android/src/com/android/tools/idea/templates/Parameter.java
+++ b/android/src/com/android/tools/idea/templates/Parameter.java
@@ -16,12 +16,11 @@
package com.android.tools.idea.templates;
import com.android.SdkConstants;
+import com.android.resources.ResourceFolderType;
+import com.android.tools.idea.rendering.ResourceNameValidator;
import com.google.common.base.Splitter;
-import com.intellij.lang.java.lexer.JavaLexer;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
-import com.intellij.openapi.util.text.StringUtil;
-import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDirectory;
@@ -254,7 +253,6 @@
}
}
boolean exists = false;
- String resourceNameError = AndroidUtils.isValidResourceName(value, true);
String fqName = (packageName != null && value.indexOf('.') == -1 ? packageName + "." : "") + value;
if (constraints.contains(Constraint.ACTIVITY)) {
@@ -284,11 +282,13 @@
exists = JavaPsiFacade.getInstance(project).findPackage(value) != null;
}
} else if (constraints.contains(Constraint.LAYOUT)) {
+ String resourceNameError = ResourceNameValidator.create(false, ResourceFolderType.LAYOUT).getErrorText(value);
if (resourceNameError != null) {
return name + " is not a valid resource name. " + resourceNameError;
}
exists = existsResourceFile(project, SdkConstants.FD_RES_LAYOUT, value + SdkConstants.DOT_XML);
} else if (constraints.contains(Constraint.DRAWABLE)) {
+ String resourceNameError = ResourceNameValidator.create(false, ResourceFolderType.DRAWABLE).getErrorText(value);
if (resourceNameError != null) {
return name + " is not a valid resource name. " + resourceNameError;
}
@@ -296,6 +296,7 @@
} else if (constraints.contains(Constraint.ID)) {
// TODO: validity and existence check
} else if (constraints.contains(Constraint.STRING)) {
+ String resourceNameError = ResourceNameValidator.create(false, ResourceFolderType.VALUES).getErrorText(value);
if (resourceNameError != null) {
return name + " is not a valid resource name. " + resourceNameError;
}
diff --git a/android/src/com/android/tools/idea/templates/RepositoryUrls.java b/android/src/com/android/tools/idea/templates/RepositoryUrls.java
new file mode 100644
index 0000000..5ff474e
--- /dev/null
+++ b/android/src/com/android/tools/idea/templates/RepositoryUrls.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.templates;
+
+import com.android.sdklib.repository.FullRevision;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.externalSystem.model.ExternalSystemException;
+import com.intellij.openapi.util.io.FileUtil;
+import org.jetbrains.android.sdk.AndroidSdkUtils;
+import org.jetbrains.annotations.Nullable;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import javax.xml.parsers.SAXParserFactory;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import static com.android.tools.idea.templates.TemplateUtils.readTextFile;
+
+/**
+ * Helper class to aid in generating Maven URLs for various internal repository files (Support Library, AppCompat, etc).
+ */
+public class RepositoryUrls {
+ private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.templates.RepositoryUrls");
+
+ /** The tag used by the maven metadata file to describe versions */
+ public static final String TAG_VERSION = "version";
+
+ /** The vendor ID of the support library. */
+ public static final String VENDOR_ID = "android";
+ /** The path ID of the support library. */
+ public static final String SUPPORT_ID = "support";
+ /** The path ID of the appcompat library. */
+ public static final String APP_COMPAT_ID = "appcompat";
+ /** The path ID of the appcompat library. */
+ public static final String GRID_LAYOUT_ID = "gridlayout";
+ /** The path ID of the compatibility library (which was its id for releases 1-3). */
+ public static final String COMPATIBILITY_ID = "compatibility";
+
+ /** Internal Maven Repository settings */
+ private static final String SUPPORT_BASE_URL = "com.android.support:%s-%s:%s";
+
+ private static final String MIN_VERSION_VALUE = "0.0.0";
+
+ private static final String SUPPORT_REPOSITORY_PATH = "%s/extras/android/m2repository/com/android/support/%s-%s/maven-metadata.xml";
+
+ /**
+ * Calculate the correct version of the support library and generate the corresponding maven URL
+ * @param minApiLevel the minimum api level specified by the template (-1 if no minApiLevel specified)
+ * @param revision the version of the support library (should be v13 or v4)
+ * @return a maven url for the android support library
+ */
+ @Nullable
+ public static String getLibraryUrl(String libraryId, String revision) {
+ // Read the support repository and find the latest version available
+ String sdkLocation = AndroidSdkUtils.tryToChooseAndroidSdk().getLocation();
+ String path = String.format(SUPPORT_REPOSITORY_PATH, sdkLocation, libraryId, revision);
+ path = FileUtil.toSystemIndependentName(path);
+ File supportMetadataFile = new File(path);
+ if (!supportMetadataFile.exists()) {
+ throw new ExternalSystemException("You must install the Android Support Repository though the SDK Manager.");
+ }
+
+ String version = getLatestVersionFromMavenMetadata(supportMetadataFile);
+
+ return String.format(SUPPORT_BASE_URL, libraryId, revision, version);
+ }
+
+ /**
+ * Parses a Maven metadata file and returns a string of the highest found version
+ * @param metadataFile the files to parse
+ * @return the string representing the highest version found in the file or "0.0.0" if no versions exist in the file
+ */
+ private static String getLatestVersionFromMavenMetadata(File metadataFile) {
+ String xml = readTextFile(metadataFile);
+ final List<FullRevision> versions = new LinkedList<FullRevision>();
+ try {
+ SAXParserFactory.newInstance().newSAXParser().parse(new ByteArrayInputStream(xml.getBytes()), new DefaultHandler() {
+ boolean inVersionTag = false;
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+ if (qName.equals(TAG_VERSION)) {
+ inVersionTag = true;
+ }
+ }
+
+ @Override
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ // Get the version and compare it to the current known max version
+ if (inVersionTag) {
+ versions.add(FullRevision.parseRevision(new String(ch, start, length)));
+ inVersionTag = false;
+ }
+ }
+ });
+ } catch (Exception e) {
+ LOG.warn(e);
+ }
+
+ if (versions.isEmpty()) {
+ return MIN_VERSION_VALUE;
+ } else {
+ return Collections.max(versions).toString();
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/templates/Template.java b/android/src/com/android/tools/idea/templates/Template.java
index e2165c7..0831749 100755
--- a/android/src/com/android/tools/idea/templates/Template.java
+++ b/android/src/com/android/tools/idea/templates/Template.java
@@ -28,14 +28,12 @@
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkManager;
-import com.android.utils.NullLogger;
import com.android.utils.SdkUtils;
import com.android.utils.StdLogger;
import com.android.utils.XmlUtils;
import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
-import com.google.common.io.Closeables;
import com.google.common.io.Files;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.io.FileUtil;
@@ -52,6 +50,7 @@
import org.jetbrains.annotations.Nullable;
import org.w3c.dom.*;
import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
@@ -64,6 +63,8 @@
import static com.android.SdkConstants.*;
import static com.android.tools.idea.templates.TemplateManager.getTemplateRootFolder;
+import static com.android.tools.idea.templates.RepositoryUrls.*;
+import static com.android.tools.idea.templates.TemplateUtils.readTextFile;
/**
* Handler which manages instantiating FreeMarker templates, copying resources
@@ -71,7 +72,6 @@
*/
public class Template {
private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.templates.Template");
- static final String SUPPORT_LIBRARY_NAME = "android-support-v4";
/** Highest supported format; templates with a higher number will be skipped
* <p>
* <ul>
@@ -136,6 +136,7 @@
public static final String TAG_THUMBS = "thumbs";
public static final String TAG_DEPENDENCY = "dependency";
public static final String TAG_ICONS = "icons";
+ public static final String TAG_MKDIR = "mkdir";
public static final String ATTR_FORMAT = "format";
public static final String ATTR_VALUE = "value";
public static final String ATTR_DEFAULT = "default";
@@ -143,24 +144,18 @@
public static final String ATTR_ID = "id";
public static final String ATTR_NAME = "name";
public static final String ATTR_DESCRIPTION = "description";
+ public static final String ATTR_VERSION = "version";
public static final String ATTR_TYPE = "type";
public static final String ATTR_HELP = "help";
public static final String ATTR_FILE = "file";
public static final String ATTR_TO = "to";
public static final String ATTR_FROM = "from";
+ public static final String ATTR_AT = "at";
public static final String ATTR_CONSTRAINTS = "constraints";
public static final String CATEGORY_ACTIVITIES = "activities";
- public static final String CATEGORY_PROJECTS = "projects";
+ public static final String CATEGORY_PROJECTS = "gradle-projects";
- /** The vendor ID of the support library. */
- private static final String VENDOR_ID = "android";
- /** The path ID of the support library. */
- private static final String SUPPORT_ID = "support";
- /** The path ID of the compatibility library (which was its id for releases 1-3). */
- private static final String COMPATIBILITY_ID = "compatibility";
- private static final String FD_V4 = "v4";
- private static final String ANDROID_SUPPORT_V4_JAR = "android-support-v4.jar";
/**
* List of files to open after the wizard has been created (these are
@@ -249,6 +244,11 @@
}
@NotNull
+ public List<String> getFilesToOpen() {
+ return myFilesToOpen;
+ }
+
+ @NotNull
private Map<String, Object> createParameterMap(@NotNull Map<String, Object> args) {
// Create the data model.
final Map<String, Object> paramMap = new HashMap<String, Object>();
@@ -265,6 +265,9 @@
paramMap.put("escapeXmlString", new FmEscapeXmlStringMethod());
paramMap.put("extractLetters", new FmExtractLettersMethod());
+ // Dependency list
+ paramMap.put(TemplateMetadata.ATTR_DEPENDENCIES_LIST, new LinkedList<String>());
+
// This should be handled better: perhaps declared "required packages" as part of the
// inputs? (It would be better if we could conditionally disable template based
// on availability)
@@ -293,7 +296,12 @@
xml = processFreemarkerTemplate(freemarker, paramMap, path);
}
- SAXParserFactory.newInstance().newSAXParser().parse(new ByteArrayInputStream(xml.getBytes()), new DefaultHandler() {
+ // Handle UTF-8 since processed file may contain file paths
+ ByteArrayInputStream inputStream = new ByteArrayInputStream(xml.getBytes(Charsets.UTF_8.toString()));
+ Reader reader = new InputStreamReader(inputStream, Charsets.UTF_8.toString());
+ InputSource inputSource = new InputSource(reader);
+ inputSource.setEncoding(Charsets.UTF_8.toString());
+ SAXParserFactory.newInstance().newSAXParser().parse(inputSource, new DefaultHandler() {
@Override
public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
if (TAG_PARAMETER.equals(name)) {
@@ -328,21 +336,12 @@
}
} else if (TAG_DEPENDENCY.equals(name)) {
String dependencyName = attributes.getValue(ATTR_NAME);
- if (dependencyName.equals(SUPPORT_LIBRARY_NAME)) {
- // We assume the revision requirement has been satisfied
- // by the wizard
- File path = getSupportJarFile();
- if (path != null) {
- try {
- // The dependency library always goes in a libs directory off the module root.
- String to = FD_NATIVE_LIBS + File.separator + path.getName();
- File targetFile = new File(myModuleRoot, to.replace('/', File.separatorChar));
- copy(path, targetFile);
- } catch (IOException ioe) {
- LOG.warn(ioe);
- }
- }
- }
+ String dependencyVersion = attributes.getValue(ATTR_VERSION);
+ List<String> dependencyList = (List<String>)paramMap.get(TemplateMetadata.ATTR_DEPENDENCIES_LIST);
+
+ if (dependencyName.equals(SUPPORT_ID) || dependencyName.equals(APP_COMPAT_ID) || dependencyName.equals(GRID_LAYOUT_ID)) {
+ dependencyList.add(getLibraryUrl(dependencyName, dependencyVersion));
+ }// TODO: Add other libraries here (Cloud SDK, Play Services, YouTube, AdMob, etc).
} else if (!name.equals("template") && !name.equals("category") && !name.equals("option") && !name.equals(TAG_THUMBS) &&
!name.equals(TAG_THUMB) && !name.equals(TAG_ICONS)) {
LOG.error("WARNING: Unknown template directive " + name);
@@ -355,50 +354,6 @@
}
}
- /**
- * Returns a path to the installed jar file for the support library,
- * or null if it does not exist
- *
- * @return a path to the v4.jar or null
- */
- @Nullable
- private static File getSupportJarFile() {
- File supportDir = getSupportPackageDir();
- if (supportDir != null) {
- File path = new File(supportDir, FD_V4 + File.separator + ANDROID_SUPPORT_V4_JAR);
- if (path.exists()) {
- return path;
- }
- }
- return null;
- }
-
- /**
- * Returns the directory containing the support libraries (v4, v7, v13,
- * ...), which may or may not exist
- *
- * @return a path to the support library or null
- */
- @Nullable
- private static File getSupportPackageDir() {
- String sdkLocation = AndroidSdkUtils.tryToChooseAndroidSdk().getLocation();
- SdkManager manager = SdkManager.createManager(sdkLocation, NullLogger.getLogger());
- Map<String, Integer> versions = manager.getExtrasVersions();
- Integer version = versions.get(VENDOR_ID + '/' + SUPPORT_ID);
- if (version != null) {
- return new File(sdkLocation, SdkConstants.FD_EXTRAS + File.separator + VENDOR_ID + File.separator + SUPPORT_ID);
- }
-
- // Check the old compatibility library. When the library is updated in-place
- // the manager doesn't change its folder name (since that is a source of
- // endless issues on Windows.)
- version = versions.get(VENDOR_ID + '/' + COMPATIBILITY_ID);
- if (version != null) {
- return new File(sdkLocation, SdkConstants.FD_EXTRAS + File.separator + VENDOR_ID + File.separator + COMPATIBILITY_ID);
- }
- return null;
- }
-
/** Executes the given recipe file: copying, merging, instantiating, opening files etc */
private void executeRecipeFile(@NotNull final Configuration freemarker, @NotNull String file, @NotNull final Map<String,
Object> paramMap) {
@@ -406,8 +361,13 @@
myLoader.setTemplateFile(getTemplateFile(file));
String xml = processFreemarkerTemplate(freemarker, paramMap, file);
- // Parse and execute the resulting instruction list.
- SAXParserFactory.newInstance().newSAXParser().parse(new ByteArrayInputStream(xml.getBytes()), new DefaultHandler() {
+ // Parse and execute the resulting instruction list. We handle UTF-8 since the processed file contains paths which may
+ // have UTF-8 characters.
+ ByteArrayInputStream inputStream = new ByteArrayInputStream(xml.getBytes(Charsets.UTF_8.toString()));
+ Reader reader = new InputStreamReader(inputStream, Charsets.UTF_8.toString());
+ InputSource inputSource = new InputSource(reader);
+ inputSource.setEncoding(Charsets.UTF_8.toString());
+ SAXParserFactory.newInstance().newSAXParser().parse(inputSource, new DefaultHandler() {
@Override
public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
try {
@@ -443,6 +403,13 @@
myFilesToOpen.add(relativePath);
}
}
+ else if (name.equals(TAG_MKDIR)) {
+ // The relative path here is within the output directory:
+ String relativePath = attributes.getValue(ATTR_AT);
+ if (relativePath != null && !relativePath.isEmpty()) {
+ mkdir(freemarker, paramMap, relativePath);
+ }
+ }
else if (!name.equals("recipe")) {
System.err.println("WARNING: Unknown template directive " + name);
}
@@ -453,7 +420,6 @@
}
}
});
-
} catch (Exception e) {
ourMostRecentException = e;
LOG.warn(e);
@@ -676,7 +642,8 @@
@NotNull final Configuration freemarker,
@NotNull final Map<String, Object> paramMap) throws IOException, TemplateException {
// TODO: Right now this is implemented as a dumb text merge. It would be much better to read it into PSI using IJ's Groovy support.
- // If Gradle build files get first-class PSI support in the future, we will pick that up cheaply.
+ // If Gradle build files get first-class PSI support in the future, we will pick that up cheaply. At the moment, Our Gradle-Groovy
+ // support requires a project, which we don't necessarily have when instantiating a template.
StringBuilder contents = new StringBuilder(dest);
@@ -720,6 +687,15 @@
}
}
+ /** Creates a directory at the given path */
+ private void mkdir(
+ @NotNull final Configuration freemarker,
+ @NotNull final Map<String, Object> paramMap,
+ @NotNull String at) throws IOException, TemplateException {
+ File targetFile = getTargetFile(at);
+ VfsUtil.createDirectories(targetFile.getAbsolutePath());
+ }
+
@NotNull
private File getFullPath(@NotNull String fromPath) {
if (fromPath.startsWith(VALUE_TEMPLATE_DIR)) {
@@ -754,18 +730,6 @@
return out.toString();
}
- /** Reads the given file as text. */
- @Nullable
- private static String readTextFile(@NotNull File file) {
- assert file.isAbsolute();
- try {
- return Files.toString(file, Charsets.UTF_8);
- } catch (IOException e) {
- LOG.warn(e);
- return null;
- }
- }
-
@NotNull
private static XmlFormatPreferences createXmlFormatPreferences() {
// TODO: implement
diff --git a/android/src/com/android/tools/idea/templates/TemplateManager.java b/android/src/com/android/tools/idea/templates/TemplateManager.java
index 86dfa68..8ebe2e0 100644
--- a/android/src/com/android/tools/idea/templates/TemplateManager.java
+++ b/android/src/com/android/tools/idea/templates/TemplateManager.java
@@ -19,7 +19,11 @@
import com.google.common.base.Charsets;
import com.google.common.collect.Maps;
import com.google.common.io.Files;
+import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -37,6 +41,14 @@
*/
public class TemplateManager {
private static final Logger LOG = Logger.getInstance("#" + TemplateManager.class.getName());
+
+ /**
+ * A directory relative to application home folder where we can find an extra template folder. This lets us ship more up-to-date
+ * templates with the application instead of waiting for SDK updates.
+ */
+ private static final String BUNDLED_TEMPLATE_PATH = "/plugins/android/lib/templates";
+ private static final String DEVELOPMENT_TEMPLATE_PATH = "/../../tools/base/templates";
+
/**
* Cache for {@link #getTemplate()}
*/
@@ -99,6 +111,23 @@
}
}
+ String homePath = FileUtil.toSystemIndependentName(PathManager.getHomePath());
+ // Release build?
+ VirtualFile root = LocalFileSystem.getInstance().findFileByPath(FileUtil.toSystemIndependentName(homePath + BUNDLED_TEMPLATE_PATH));
+ if (root == null) {
+ // Development build?
+ root = LocalFileSystem.getInstance().findFileByPath(FileUtil.toSystemIndependentName(homePath + DEVELOPMENT_TEMPLATE_PATH));
+ }
+
+ if (root == null) {
+ // error message tailored for release build file layout
+ LOG.error("Templates not found in: " + homePath + BUNDLED_TEMPLATE_PATH + " or " + homePath + DEVELOPMENT_TEMPLATE_PATH);
+ } else {
+ File templateDir = new File(root.getCanonicalPath()).getAbsoluteFile();
+ if (templateDir.isDirectory()) {
+ folders.add(templateDir);
+ }
+ }
return folders;
}
diff --git a/android/src/com/android/tools/idea/templates/TemplateMetadata.java b/android/src/com/android/tools/idea/templates/TemplateMetadata.java
index 0d4eaff..cf2b4dd 100644
--- a/android/src/com/android/tools/idea/templates/TemplateMetadata.java
+++ b/android/src/com/android/tools/idea/templates/TemplateMetadata.java
@@ -15,16 +15,18 @@
*/
package com.android.tools.idea.templates;
+import com.google.common.collect.Lists;
import com.intellij.openapi.diagnostic.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.w3c.dom.*;
import java.util.Collection;
-import java.util.HashMap;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
+import static com.android.tools.idea.templates.RepositoryUrls.*;
import static com.android.tools.idea.templates.Template.*;
/** An ADT template along with metadata */
@@ -44,6 +46,7 @@
public static final String ATTR_REVISION = "revision";
public static final String ATTR_MIN_API_LEVEL = "minApiLevel";
public static final String ATTR_PACKAGE_NAME = "packageName";
+ public static final String ATTR_PACKAGE_ROOT = "packageRoot";
public static final String ATTR_APP_TITLE = "appTitle";
public static final String ATTR_BASE_THEME = "baseTheme";
public static final String ATTR_IS_NEW_PROJECT = "isNewProject";
@@ -55,6 +58,21 @@
public static final String ATTR_MANIFEST_OUT = "manifestOut";
public static final String ATTR_MAVEN_URL = "mavenUrl";
public static final String ATTR_BUILD_TOOLS_VERSION = "buildToolsVersion";
+ public static final String ATTR_GRADLE_PLUGIN_VERSION = "gradlePluginVersion";
+ public static final String ATTR_V4_SUPPORT_LIBRARY_VERSION = "v4SupportLibraryVersion";
+ public static final String ATTR_GRADLE_VERSION = "gradleVersion";
+
+ public static final String ATTR_DEPENDENCIES_LIST = "dependencyList";
+ public static final String ATTR_FRAGMENTS_EXTRA = "usesFragments";
+ public static final String ATTR_ACTION_BAR_EXTRA = "usesActionBar";
+ public static final String ATTR_GRID_LAYOUT_EXTRA = "usesGridLayout";
+ public static final String ATTR_NAVIGATION_DRAWER_EXTRA = "usesNavigationDrawer";
+
+ public static final String V4_SUPPORT_LIBRARY_VERSION = "13.0.+";
+ public static final String GRADLE_PLUGIN_VERSION = "0.5.+";
+ public static final String GRADLE_VERSION = "1.7";
+ public static final String GRADLE_DISTRIBUTION_URL = "http://services.gradle.org/distributions/gradle-" +
+ GRADLE_VERSION + "-bin.zip";
private final Document myDocument;
private final Map<String, Parameter> myParameterMap;
@@ -155,6 +173,27 @@
return null;
}
+ /**
+ * Get any dependency declarations from the template.xml file.
+ * @return a list of maven dependency URLs
+ */
+ @NotNull
+ public List<String> getDependencies() {
+ final List<String> dependencyList = Lists.newLinkedList();
+
+ NodeList dependencies = myDocument.getElementsByTagName(TAG_DEPENDENCY);
+ for (int index = 0, max = dependencies.getLength(); index < max; index++) {
+ Element element = (Element) dependencies.item(index);
+ String dependencyName = element.getAttribute(ATTR_NAME);
+ String dependencyVersion = element.getAttribute(ATTR_VERSION);
+ if (dependencyName.equals(SUPPORT_ID) || dependencyName.equals(APP_COMPAT_ID) || dependencyName.equals(GRID_LAYOUT_ID)) {
+ dependencyList.add(getLibraryUrl(dependencyName, dependencyVersion));
+ }// TODO: Add other libraries here (Cloud SDK, Play Services, YouTube, AdMob, etc).
+ }
+
+ return dependencyList;
+ }
+
public boolean isSupported() {
String versionString = myDocument.getDocumentElement().getAttribute(ATTR_FORMAT);
if (versionString != null && !versionString.isEmpty()) {
diff --git a/android/src/com/android/tools/idea/templates/TemplateUtils.java b/android/src/com/android/tools/idea/templates/TemplateUtils.java
index 027590d..e6f28c0 100644
--- a/android/src/com/android/tools/idea/templates/TemplateUtils.java
+++ b/android/src/com/android/tools/idea/templates/TemplateUtils.java
@@ -20,8 +20,20 @@
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkManager;
import com.android.sdklib.repository.PkgProps;
-import com.android.sdklib.util.SparseArray;
+import com.android.utils.SparseArray;
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+import com.intellij.ide.impl.ProjectPaneSelectInTarget;
+import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.fileEditor.OpenFileDescriptor;
+import com.intellij.openapi.fileTypes.StdFileTypes;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -30,6 +42,7 @@
import org.w3c.dom.NodeList;
import java.io.File;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -175,7 +188,6 @@
target.getVersion().getApiString());
}
-
/**
* Returns a list of known API names
*
@@ -267,4 +279,91 @@
return result;
}
+
+ /**
+ * Opens the specified files in the editor
+ *
+ * @param project The project which contains the given file.
+ * @param paths The paths to the files on disk.
+ * @param select If true, select the last (topmost) file in the project view
+ * @return true if all files were opened
+ */
+ public static boolean openEditors(@NotNull Project project, @NotNull List<String> paths, boolean select) {
+ if (paths.size() > 0) {
+ boolean result = true;
+ VirtualFile last = null;
+ for (String path : paths) {
+ File file = new File(path);
+ if (file.exists()) {
+ VirtualFile vFile = VfsUtil.findFileByIoFile(file, true /** refreshIfNeeded */);
+ if (vFile != null) {
+ result &= openEditor(project, vFile);
+ last = vFile;
+ }
+ else {
+ result = false;
+ }
+ }
+ }
+
+ if (select && last != null) {
+ selectEditor(project, last);
+ }
+
+ return result;
+ }
+
+ return false;
+ }
+
+ /**
+ * Opens the specified file in the editor
+ *
+ * @param project The project which contains the given file.
+ * @param vFile The file to open
+ * @return
+ */
+ public static boolean openEditor(@NotNull Project project, @NotNull VirtualFile vFile) {
+ OpenFileDescriptor descriptor;
+ if (vFile.getFileType() == StdFileTypes.XML) {
+ // For XML files, ensure that we open the text editor rather than the default
+ // editor for now, until the layout editor is fully done
+ descriptor = new OpenFileDescriptor(project, vFile, 0);
+ } else {
+ descriptor = new OpenFileDescriptor(project, vFile);
+ }
+ return !FileEditorManager.getInstance(project).openEditor(descriptor, true).isEmpty();
+ }
+
+ /**
+ * Selects the specified file in the project view.
+ * <b>Note:</b> Must be called with read access.
+ *
+ * @param project the project
+ * @param file the file to select
+ */
+ public static void selectEditor(Project project, VirtualFile file) {
+ ApplicationManager.getApplication().assertReadAccessAllowed();
+ PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
+ ProjectPaneSelectInTarget selectAction = new ProjectPaneSelectInTarget(project);
+ if (selectAction.canSelect(psiFile)) {
+ selectAction.select(psiFile, false);
+ }
+ }
+
+ /**
+ * Reads the given file as text.
+ * @param file The file to read. Must be an absolute reference.
+ * @return the contents of the file as text
+ */
+ @Nullable
+ public static String readTextFile(@NotNull File file) {
+ assert file.isAbsolute();
+ try {
+ return Files.toString(file, Charsets.UTF_8);
+ } catch (IOException e) {
+ LOG.warn(e);
+ return null;
+ }
+ }
}
diff --git a/android/src/com/android/tools/idea/wizard/ChooseTemplateStep.java b/android/src/com/android/tools/idea/wizard/ChooseTemplateStep.java
index 5ee033b..45aab9a 100644
--- a/android/src/com/android/tools/idea/wizard/ChooseTemplateStep.java
+++ b/android/src/com/android/tools/idea/wizard/ChooseTemplateStep.java
@@ -19,9 +19,11 @@
import com.android.tools.idea.templates.TemplateMetadata;
import com.google.common.io.Files;
import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
import com.intellij.ui.components.JBList;
import com.intellij.util.ArrayUtil;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
@@ -38,26 +40,39 @@
*/
public class ChooseTemplateStep extends TemplateWizardStep implements ListSelectionListener {
private static final Logger LOG = Logger.getInstance("#" + ChooseTemplateStep.class.getName());
+ private final TemplateChangeListener myTemplateChangeListener;
private JPanel myPanel;
private JBList myTemplateList;
private ImageComponent myTemplateImage;
private JLabel myDescription;
private JLabel myError;
+ private int myPreviousSelection = -1;
- public ChooseTemplateStep(TemplateWizard templateWizard, TemplateWizardState state, String templateCategory) {
- super(templateWizard, state);
+ public interface TemplateChangeListener {
+ void templateChanged();
+ }
+
+ public ChooseTemplateStep(TemplateWizardState state, String templateCategory, @Nullable Project project, @Nullable Icon sidePanelIcon,
+ UpdateListener updateListener, @Nullable TemplateChangeListener templateChangeListener) {
+ super(state, project, sidePanelIcon, updateListener);
+ myTemplateChangeListener = templateChangeListener;
myTemplateList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
TemplateManager manager = TemplateManager.getInstance();
List<File> templates = manager.getTemplates(templateCategory);
List<MetadataListItem> metadataList = new ArrayList<MetadataListItem>(templates.size());
- for (int i = 0, n = templates.size(); i < n; i++) {
- File template = templates.get(i);
+ for (File template : templates) {
TemplateMetadata metadata = manager.getTemplate(template);
if (metadata == null || !metadata.isSupported()) {
continue;
}
+ // If we're trying to create a launchable activity, don't include templates that
+ // lack the isLauncher parameter.
+ Boolean isLauncher = (Boolean)state.get(ATTR_IS_LAUNCHER);
+ if (isLauncher != null && isLauncher && metadata.getParameter(TemplateMetadata.ATTR_IS_LAUNCHER) == null) {
+ continue;
+ }
metadataList.add(new MetadataListItem(template, metadata));
}
@@ -97,10 +112,7 @@
if (template != null) {
myTemplateState.setTemplateLocation(template.myTemplate);
- myTemplateState.convertToInt(ATTR_MIN_API);
- myTemplateState.convertToInt(ATTR_BUILD_API);
- myTemplateState.convertToInt(ATTR_MIN_API_LEVEL);
- myTemplateState.convertToInt(ATTR_TARGET_API);
+ myTemplateState.convertApisToInt();
String thumb = template.myMetadata.getThumbnailPath();
if (thumb != null && !thumb.isEmpty()) {
File file = new File(myTemplateState.myTemplate.getRootPath(), thumb.replace('/', File.separatorChar));
@@ -126,16 +138,22 @@
setErrorHtml(String.format("The activity %s has a minimum build API level of %d.", template.myMetadata.getTitle(), minBuildApi));
return false;
}
+ if (myTemplateChangeListener != null && myPreviousSelection != index) {
+ myPreviousSelection = index;
+ myTemplateChangeListener.templateChanged();
+ }
}
}
return true;
}
+ @NotNull
@Override
protected JLabel getDescription() {
return myDescription;
}
+ @NotNull
@Override
protected JLabel getError() {
return myError;
diff --git a/android/src/com/android/tools/idea/wizard/ConfigureAndroidModuleStep.form b/android/src/com/android/tools/idea/wizard/ConfigureAndroidModuleStep.form
index 9db1d1a..7dbb3ec 100644
--- a/android/src/com/android/tools/idea/wizard/ConfigureAndroidModuleStep.form
+++ b/android/src/com/android/tools/idea/wizard/ConfigureAndroidModuleStep.form
@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.android.tools.idea.wizard.ConfigureAndroidModuleStep">
- <grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="15" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+ <grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="15" column-count="7" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
- <xy x="20" y="20" width="694" height="583"/>
+ <xy x="20" y="20" width="694" height="600"/>
</constraints>
<properties/>
<border type="none"/>
<children>
- <component id="2ea7" class="javax.swing.JLabel">
+ <component id="2ea7" class="javax.swing.JLabel" binding="myModuleNameLabel">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
@@ -16,14 +16,9 @@
<text value="Module name:"/>
</properties>
</component>
- <vspacer id="7581">
- <constraints>
- <grid row="13" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
- </constraints>
- </vspacer>
<component id="81c6d" class="javax.swing.JTextField" binding="myModuleName">
<constraints>
- <grid row="1" column="1" row-span="1" col-span="2" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+ <grid row="1" column="1" row-span="1" col-span="6" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
@@ -44,7 +39,7 @@
</vspacer>
<component id="4f4fc" class="com.intellij.openapi.ui.TextFieldWithBrowseButton" binding="myProjectLocation">
<constraints>
- <grid row="3" column="1" row-span="1" col-span="2" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+ <grid row="3" column="1" row-span="1" col-span="6" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
@@ -84,7 +79,7 @@
</component>
<component id="c7c8" class="javax.swing.JComboBox" binding="myMinSdk">
<constraints>
- <grid row="5" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="1" indent="0" use-parent-layout="false">
+ <grid row="5" column="1" row-span="1" col-span="3" vsize-policy="0" hsize-policy="0" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="241" height="27"/>
</grid>
</constraints>
@@ -92,7 +87,7 @@
</component>
<component id="d887d" class="javax.swing.JComboBox" binding="myTargetSdk">
<constraints>
- <grid row="6" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="1" indent="0" use-parent-layout="false">
+ <grid row="6" column="1" row-span="1" col-span="3" vsize-policy="0" hsize-policy="0" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="241" height="27"/>
</grid>
</constraints>
@@ -100,7 +95,7 @@
</component>
<component id="4f7a5" class="javax.swing.JComboBox" binding="myCompileWith">
<constraints>
- <grid row="7" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="1" indent="0" use-parent-layout="false">
+ <grid row="7" column="1" row-span="1" col-span="3" vsize-policy="0" hsize-policy="0" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="241" height="27"/>
</grid>
</constraints>
@@ -108,7 +103,7 @@
</component>
<component id="fa492" class="javax.swing.JComboBox" binding="myTheme">
<constraints>
- <grid row="8" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="1" indent="0" use-parent-layout="false">
+ <grid row="8" column="1" row-span="1" col-span="3" vsize-policy="0" hsize-policy="0" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="241" height="27"/>
</grid>
</constraints>
@@ -121,7 +116,7 @@
</vspacer>
<component id="6db10" class="javax.swing.JCheckBox" binding="myCreateCustomLauncherIconCheckBox" default-binding="true">
<constraints>
- <grid row="10" column="1" row-span="1" col-span="2" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ <grid row="10" column="1" row-span="1" col-span="6" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Create custom launcher icon"/>
@@ -129,7 +124,7 @@
</component>
<component id="b6571" class="javax.swing.JCheckBox" binding="myCreateActivityCheckBox" default-binding="true">
<constraints>
- <grid row="11" column="1" row-span="1" col-span="2" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ <grid row="11" column="1" row-span="1" col-span="6" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Create activity"/>
@@ -137,7 +132,7 @@
</component>
<component id="27994" class="javax.swing.JCheckBox" binding="myLibraryCheckBox">
<constraints>
- <grid row="12" column="1" row-span="1" col-span="2" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ <grid row="12" column="1" row-span="1" col-span="6" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Mark this project as a library"/>
@@ -146,7 +141,7 @@
<grid id="e39ce" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
- <grid row="14" column="0" row-span="1" col-span="3" vsize-policy="2" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false">
+ <grid row="14" column="0" row-span="1" col-span="7" vsize-policy="2" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false">
<minimum-size width="-1" height="175"/>
</grid>
</constraints>
@@ -180,12 +175,12 @@
</grid>
<hspacer id="facce">
<constraints>
- <grid row="4" column="2" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ <grid row="4" column="4" row-span="1" col-span="3" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
</hspacer>
<component id="9116" class="javax.swing.JTextField" binding="myAppName">
<constraints>
- <grid row="0" column="1" row-span="1" col-span="2" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+ <grid row="0" column="1" row-span="1" col-span="6" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
@@ -209,12 +204,52 @@
</component>
<component id="7e6ab" class="javax.swing.JTextField" binding="myPackageName">
<constraints>
- <grid row="2" column="1" row-span="1" col-span="2" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+ <grid row="2" column="1" row-span="1" col-span="6" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
+ <component id="5202c" class="javax.swing.JLabel">
+ <constraints>
+ <grid row="13" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value="Support Mode:"/>
+ </properties>
+ </component>
+ <component id="ae91a" class="javax.swing.JCheckBox" binding="myFragmentCheckBox">
+ <constraints>
+ <grid row="13" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value="Fragments"/>
+ </properties>
+ </component>
+ <component id="ef1e5" class="javax.swing.JCheckBox" binding="myGridLayoutCheckBox">
+ <constraints>
+ <grid row="13" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value="GridLayout"/>
+ </properties>
+ </component>
+ <component id="6d9a1" class="javax.swing.JCheckBox" binding="myNavigationDrawerCheckBox">
+ <constraints>
+ <grid row="13" column="3" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value="Navigation Drawer"/>
+ </properties>
+ </component>
+ <component id="4ba56" class="javax.swing.JCheckBox" binding="myActionBarCheckBox">
+ <constraints>
+ <grid row="13" column="4" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value="Action Bar"/>
+ </properties>
+ </component>
</children>
</grid>
</form>
diff --git a/android/src/com/android/tools/idea/wizard/ConfigureAndroidModuleStep.java b/android/src/com/android/tools/idea/wizard/ConfigureAndroidModuleStep.java
index 7a7ca0b..5ae10df 100644
--- a/android/src/com/android/tools/idea/wizard/ConfigureAndroidModuleStep.java
+++ b/android/src/com/android/tools/idea/wizard/ConfigureAndroidModuleStep.java
@@ -69,15 +69,21 @@
private JCheckBox myCreateCustomLauncherIconCheckBox;
private JCheckBox myCreateActivityCheckBox;
private JCheckBox myLibraryCheckBox;
+ private JCheckBox myFragmentCheckBox;
+ private JCheckBox myActionBarCheckBox;
private JPanel myPanel;
private JTextField myModuleName;
private JLabel myDescription;
private JLabel myError;
private JLabel myProjectLocationLabel;
+ private JLabel myModuleNameLabel;
+ private JCheckBox myGridLayoutCheckBox;
+ private JCheckBox myNavigationDrawerCheckBox;
boolean myInitializedPackageNameText = false;
- public ConfigureAndroidModuleStep(TemplateWizard templateWizard, TemplateWizardState state) {
- super(templateWizard, state);
+ public ConfigureAndroidModuleStep(TemplateWizardState state, @Nullable Project project, @Nullable Icon sidePanelIcon,
+ UpdateListener updateListener) {
+ super(state, project, sidePanelIcon, updateListener);
IAndroidTarget[] targets = getCompilationTargets();
@@ -100,25 +106,7 @@
}
}
- TemplateMetadata metadata = myTemplateState.getTemplateMetadata();
- if (metadata != null) {
- Parameter param = metadata.getParameter(ATTR_BASE_THEME);
- if (param != null && param.element != null) {
- populateComboBox(myTheme, param);
- register(ATTR_BASE_THEME, myTheme);
- }
- }
-
- register(ATTR_MODULE_NAME, myModuleName);
- register(ATTR_PROJECT_LOCATION, myProjectLocation);
- register(ATTR_APP_TITLE, myAppName);
- register(ATTR_PACKAGE_NAME, myPackageName);
- register(ATTR_MIN_API, myMinSdk);
- register(ATTR_TARGET_API, myTargetSdk);
- register(ATTR_BUILD_API, myCompileWith);
- register(ATTR_CREATE_ACTIVITY, myCreateActivityCheckBox);
- register(ATTR_CREATE_ICONS, myCreateCustomLauncherIconCheckBox);
- register(ATTR_LIBRARY, myLibraryCheckBox);
+ registerUiElements();
myProjectLocation.addActionListener(new ActionListener() {
@Override
@@ -147,6 +135,44 @@
if (myTemplateState.myHidden.contains(ATTR_IS_LIBRARY_MODULE)) {
myLibraryCheckBox.setVisible(false);
}
+ if (myTemplateState.myHidden.contains(ATTR_MODULE_NAME)) {
+ myModuleName.setVisible(false);
+ myModuleNameLabel.setVisible(false);
+ }
+ }
+
+ private void registerUiElements() {
+ TemplateMetadata metadata = myTemplateState.getTemplateMetadata();
+ if (metadata != null) {
+ Parameter param = metadata.getParameter(ATTR_BASE_THEME);
+ if (param != null && param.element != null) {
+ populateComboBox(myTheme, param);
+ register(ATTR_BASE_THEME, myTheme);
+ }
+ }
+
+ register(ATTR_MODULE_NAME, myModuleName);
+ register(ATTR_PROJECT_LOCATION, myProjectLocation);
+ register(ATTR_APP_TITLE, myAppName);
+ register(ATTR_PACKAGE_NAME, myPackageName);
+ register(ATTR_MIN_API, myMinSdk);
+ register(ATTR_TARGET_API, myTargetSdk);
+ register(ATTR_BUILD_API, myCompileWith);
+ register(ATTR_CREATE_ACTIVITY, myCreateActivityCheckBox);
+ register(ATTR_CREATE_ICONS, myCreateCustomLauncherIconCheckBox);
+ register(ATTR_LIBRARY, myLibraryCheckBox);
+ register(ATTR_FRAGMENTS_EXTRA, myFragmentCheckBox);
+ register(ATTR_NAVIGATION_DRAWER_EXTRA, myNavigationDrawerCheckBox);
+ register(ATTR_ACTION_BAR_EXTRA, myActionBarCheckBox);
+ register(ATTR_GRID_LAYOUT_EXTRA, myGridLayoutCheckBox);
+ }
+
+ @Override
+ public void refreshUiFromParameters() {
+ // It's easier to just re-register the UI elements instead of trying to set their values manually. Not all of the elements have
+ // parameters in the template, and the super refreshUiFromParameters won't touch those elements.
+ registerUiElements();
+ super.refreshUiFromParameters();
}
@Override
@@ -160,6 +186,13 @@
return myAppName;
}
+ public void setModuleName(String name) {
+ myModuleName.setText(name);
+ myTemplateState.put(ATTR_MODULE_NAME, name);
+ myTemplateState.myModified.add(ATTR_MODULE_NAME);
+ validate();
+ }
+
@NotNull
private IAndroidTarget[] getCompilationTargets() {
IAndroidTarget[] targets = AndroidSdkUtils.tryToChooseAndroidSdk().getTargets();
@@ -202,6 +235,14 @@
"or the first version that supports all the APIs you want to directly access without reflection.";
} else if (param.equals(ATTR_BASE_THEME)) {
return "Choose the base theme to use for the application";
+ } else if (param.equals(ATTR_FRAGMENTS_EXTRA)) {
+ return "Select this box if you plan to use Fragments and will need the Support Library.";
+ } else if (param.equals(ATTR_ACTION_BAR_EXTRA)) {
+ return "Select this box if you plan to use the Action Bar and will need the AppCompat Library.";
+ } else if (param.equals(ATTR_GRID_LAYOUT_EXTRA)) {
+ return "Select this box if you plan to use the new GridLayout and will need the GridLayout Support Library.";
+ } else if (param.equals(ATTR_NAVIGATION_DRAWER_EXTRA)) {
+ return "Select this box if you plan to use the Navigation Drawer and will need the Support Library.";
} else {
return null;
}
@@ -292,10 +333,7 @@
setErrorHtml("The application name for most apps begins with an uppercase letter");
}
String packageName = (String)myTemplateState.get(ATTR_PACKAGE_NAME);
- if (packageName == null || packageName.isEmpty()) {
- setErrorHtml("Please specify a package name.");
- return false;
- } else if (packageName.startsWith(SAMPLE_PACKAGE_PREFIX)) {
+ if (packageName.startsWith(SAMPLE_PACKAGE_PREFIX)) {
setErrorHtml(String.format("The prefix '%1$s' is meant as a placeholder and should " +
"not be used", SAMPLE_PACKAGE_PREFIX));
}
@@ -327,6 +365,11 @@
return false;
}
+ toggleVisibleOnApi(myFragmentCheckBox, 10, minLevel);
+ toggleVisibleOnApi(myNavigationDrawerCheckBox, 10, minLevel);
+ toggleVisibleOnApi(myActionBarCheckBox, 10, minLevel);
+ toggleVisibleOnApi(myGridLayoutCheckBox, 13, minLevel);
+
if (!myTemplateState.myHidden.contains(ATTR_PROJECT_LOCATION)) {
String projectLocation = (String)myTemplateState.get(ATTR_PROJECT_LOCATION);
if (projectLocation == null || projectLocation.isEmpty()) {
@@ -373,6 +416,19 @@
return updated;
}
+ /**
+ * Shows or hides a checkbox based on a given API level and the max API level for which it should be shown
+ * @param component The component to hide
+ * @param maxApiLevel the maximum API level for which the given component should be visible
+ * @param apiLevel the selected API level
+ */
+ private void toggleVisibleOnApi(JCheckBox component, int maxApiLevel, int apiLevel) {
+ component.setVisible(apiLevel <= maxApiLevel);
+ if (!component.isVisible()) {
+ component.setSelected(false);
+ }
+ }
+
@NotNull
private String computePackageName() {
String moduleName = (String)myTemplateState.get(ATTR_MODULE_NAME);
diff --git a/android/src/com/android/tools/idea/wizard/LauncherIconStep.java b/android/src/com/android/tools/idea/wizard/LauncherIconStep.java
index 17596c8..7eb9d0c 100644
--- a/android/src/com/android/tools/idea/wizard/LauncherIconStep.java
+++ b/android/src/com/android/tools/idea/wizard/LauncherIconStep.java
@@ -19,6 +19,7 @@
import com.android.assetstudiolib.GraphicGenerator;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
+import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.ui.ColorPanel;
import org.jetbrains.annotations.NotNull;
@@ -77,8 +78,9 @@
private JLabel myBackgroundColorLabel;
private JLabel myForegroundColorLabel;
- public LauncherIconStep(TemplateWizard templateWizard, LauncherIconWizardState state) {
- super(templateWizard, state);
+ public LauncherIconStep(LauncherIconWizardState state, @Nullable Project project, @Nullable Icon sidePanelIcon,
+ UpdateListener updateListener) {
+ super(state, project, sidePanelIcon, updateListener);
myWizardState = state;
register(ATTR_TEXT, myText);
diff --git a/android/src/com/android/tools/idea/wizard/NewModuleWizard.java b/android/src/com/android/tools/idea/wizard/NewModuleWizard.java
index 578073e..33f0995 100644
--- a/android/src/com/android/tools/idea/wizard/NewModuleWizard.java
+++ b/android/src/com/android/tools/idea/wizard/NewModuleWizard.java
@@ -15,37 +15,26 @@
*/
package com.android.tools.idea.wizard;
-import com.android.tools.idea.gradle.GradleProjectImporter;
import com.android.tools.idea.templates.TemplateMetadata;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.project.Project;
-import com.intellij.openapi.ui.Messages;
-import org.jetbrains.annotations.NotNull;
+import icons.AndroidIcons;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
-import java.io.File;
-import static com.android.tools.idea.templates.Template.CATEGORY_ACTIVITIES;
import static com.android.tools.idea.templates.Template.CATEGORY_PROJECTS;
/**
* {@linkplain NewModuleWizard} guides the user through adding a new module to an existing project. It has a template-based flow and as the
* first step of the wizard allows the user to choose a template which will guide the rest of the wizard flow.
*/
-public class NewModuleWizard extends TemplateWizard {
+public class NewModuleWizard extends TemplateWizard implements ChooseTemplateStep.TemplateChangeListener {
private static final Logger LOG = Logger.getInstance("#" + NewModuleWizard.class.getName());
- private NewModuleWizardState myWizardState;
private ChooseTemplateStep myChooseModuleStep;
- private ConfigureAndroidModuleStep myConfigureAndroidModuleStep;
- private TemplateParameterStep myTemplateParameterStep;
- private LauncherIconStep myLauncherIconStep;
- private ChooseTemplateStep myChooseActivityStep;
- private TemplateParameterStep myActivityTemplateParameterStep;
- private boolean myInitializationComplete = false;
+ private TemplateWizardModuleBuilder myModuleBuilder;
public NewModuleWizard(@Nullable Project project) {
super("New Module", project);
@@ -55,46 +44,34 @@
@Override
protected void init() {
- myWizardState = new NewModuleWizardState() {
+ myModuleBuilder = new TemplateWizardModuleBuilder(null, null, myProject, AndroidIcons.Wizards.NewModuleSidePanel, mySteps, false) {
@Override
- public void setTemplateLocation(@NotNull File file) {
- super.setTemplateLocation(file);
- update();
+ public void update() {
+ super.update();
+ NewModuleWizard.this.update();
}
};
- myChooseModuleStep = new ChooseTemplateStep(this, myWizardState, CATEGORY_PROJECTS);
- myConfigureAndroidModuleStep = new ConfigureAndroidModuleStep(this, myWizardState);
- myTemplateParameterStep = new TemplateParameterStep(this, myWizardState);
- myLauncherIconStep = new LauncherIconStep(this, myWizardState.getLauncherIconState());
- myChooseActivityStep = new ChooseTemplateStep(this, myWizardState.getActivityTemplateState(), CATEGORY_ACTIVITIES);
- myActivityTemplateParameterStep = new TemplateParameterStep(this, myWizardState.getActivityTemplateState());
-
- mySteps.add(myChooseModuleStep);
- mySteps.add(myConfigureAndroidModuleStep);
- mySteps.add(myTemplateParameterStep);
- mySteps.add(myLauncherIconStep);
- mySteps.add(myChooseActivityStep);
- mySteps.add(myActivityTemplateParameterStep);
-
- myWizardState.put(NewModuleWizardState.ATTR_PROJECT_LOCATION, myProject.getBasePath());
-
- myInitializationComplete = true;
+ myChooseModuleStep = new ChooseTemplateStep(myModuleBuilder.myWizardState, CATEGORY_PROJECTS, myProject,
+ AndroidIcons.Wizards.NewModuleSidePanel, myModuleBuilder, this);
+ myModuleBuilder.mySteps.add(0, myChooseModuleStep);
super.init();
}
@Override
- void update() {
- if (!myInitializationComplete) {
+ public void update() {
+ if (myModuleBuilder == null || !myModuleBuilder.myInitializationComplete) {
return;
}
- myConfigureAndroidModuleStep.setVisible(myWizardState.myIsAndroidModule);
- myTemplateParameterStep.setVisible(!myWizardState.myIsAndroidModule);
- myLauncherIconStep.setVisible(myWizardState.myIsAndroidModule && (Boolean)myWizardState.get(TemplateMetadata.ATTR_CREATE_ICONS));
- myChooseActivityStep.setVisible(
- myWizardState.myIsAndroidModule && (Boolean)myWizardState.get(NewProjectWizardState.ATTR_CREATE_ACTIVITY));
- myActivityTemplateParameterStep.setVisible(
- myWizardState.myIsAndroidModule && (Boolean)myWizardState.get(NewProjectWizardState.ATTR_CREATE_ACTIVITY));
+ NewModuleWizardState wizardState = myModuleBuilder.myWizardState;
+ myModuleBuilder.myConfigureAndroidModuleStep.setVisible(wizardState.myIsAndroidModule);
+ myModuleBuilder.myTemplateParameterStep.setVisible(!wizardState.myIsAndroidModule);
+ myModuleBuilder.myLauncherIconStep.setVisible(wizardState.myIsAndroidModule &&
+ (Boolean)wizardState.get(TemplateMetadata.ATTR_CREATE_ICONS));
+ myModuleBuilder.myChooseActivityStep.setVisible(wizardState.myIsAndroidModule &&
+ (Boolean)wizardState.get(NewProjectWizardState.ATTR_CREATE_ACTIVITY));
+ myModuleBuilder.myActivityTemplateParameterStep.setVisible(wizardState.myIsAndroidModule &&
+ (Boolean)wizardState.get(NewProjectWizardState.ATTR_CREATE_ACTIVITY));
super.update();
}
@@ -102,32 +79,13 @@
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
- try {
- populateDirectoryParameters(myWizardState);
- File projectRoot = new File(myProject.getBasePath());
- File moduleRoot = new File(projectRoot, (String)myWizardState.get(NewProjectWizardState.ATTR_MODULE_NAME));
- projectRoot.mkdirs();
- if (myLauncherIconStep.isStepVisible() && (Boolean)myWizardState.get(TemplateMetadata.ATTR_CREATE_ICONS)) {
- myWizardState.getLauncherIconState().outputImages(moduleRoot);
- }
- myWizardState.updateParameters();
- myWizardState.myTemplate.render(projectRoot, moduleRoot, myWizardState.myParameters);
- if (myActivityTemplateParameterStep.isStepVisible() && (Boolean)myWizardState.get(NewProjectWizardState.ATTR_CREATE_ACTIVITY)) {
- myWizardState.getActivityTemplateState().getTemplate()
- .render(moduleRoot, moduleRoot, myWizardState.getActivityTemplateState().myParameters);
- }
- GradleProjectImporter.getInstance().reImportProject(myProject);
- } catch (Exception e) {
- String title;
- if (e instanceof ConfigurationException) {
- title = ((ConfigurationException)e).getTitle();
- } else {
- title = "New Module Wizard";
- }
- Messages.showErrorDialog(e.getMessage(), title);
- LOG.error(e);
- }
+ myModuleBuilder.createModule();
}
});
}
+
+ @Override
+ public void templateChanged() {
+ myModuleBuilder.myConfigureAndroidModuleStep.refreshUiFromParameters();
+ }
}
diff --git a/android/src/com/android/tools/idea/wizard/NewModuleWizardState.java b/android/src/com/android/tools/idea/wizard/NewModuleWizardState.java
index 64aa03e..690d743 100644
--- a/android/src/com/android/tools/idea/wizard/NewModuleWizardState.java
+++ b/android/src/com/android/tools/idea/wizard/NewModuleWizardState.java
@@ -17,12 +17,16 @@
package com.android.tools.idea.wizard;
import com.android.sdklib.BuildToolInfo;
+import com.intellij.util.containers.HashSet;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.NotNull;
import java.io.File;
+import java.util.LinkedList;
import java.util.Map;
+import java.util.Set;
+import static com.android.tools.idea.templates.RepositoryUrls.*;
import static com.android.tools.idea.templates.TemplateMetadata.*;
/**
@@ -55,7 +59,7 @@
myHidden.add(ATTR_PROJECT_LOCATION);
myHidden.add(ATTR_IS_LIBRARY_MODULE);
- put(ATTR_IS_LAUNCHER, true);
+ put(ATTR_IS_LAUNCHER, false);
put(ATTR_CREATE_ICONS, true);
put(ATTR_IS_NEW_PROJECT, true);
put(ATTR_CREATE_ACTIVITY, true);
@@ -97,6 +101,34 @@
}
/**
+ * Call this to update the list of dependencies to be compiled into the template
+ */
+ public void updateDependencies() {
+ // Take care of dependencies selected through the wizard
+ Set<String> dependencySet = new HashSet<String>();
+
+ dependencySet.addAll(myActivityTemplateState.getTemplateMetadata().getDependencies());
+
+ // Support Library
+ if ((get(ATTR_FRAGMENTS_EXTRA) != null && Boolean.parseBoolean(get(ATTR_FRAGMENTS_EXTRA).toString())) ||
+ (get(ATTR_NAVIGATION_DRAWER_EXTRA) != null && Boolean.parseBoolean(get(ATTR_NAVIGATION_DRAWER_EXTRA).toString()))) {
+ dependencySet.add(getLibraryUrl(SUPPORT_ID, "v4"));
+ }
+
+ // AppCompat Library
+ if (get(ATTR_ACTION_BAR_EXTRA) != null && Boolean.parseBoolean(get(ATTR_ACTION_BAR_EXTRA).toString())) {
+ dependencySet.add(getLibraryUrl(APP_COMPAT_ID, "v7"));
+ }
+
+ // GridLayout Library
+ if (get(ATTR_GRID_LAYOUT_EXTRA) != null && Boolean.parseBoolean(get(ATTR_GRID_LAYOUT_EXTRA).toString())) {
+ dependencySet.add(getLibraryUrl(GRID_LAYOUT_ID, "v7"));
+ }
+
+ put(ATTR_DEPENDENCIES_LIST, new LinkedList<String>(dependencySet));
+ }
+
+ /**
* Call this to have this state object propagate common parameter values to sub-state objects
* (i.e. states for other template wizards that are part of the same dialog).
*/
diff --git a/android/src/com/android/tools/idea/wizard/NewProjectWizard.java b/android/src/com/android/tools/idea/wizard/NewProjectWizard.java
index 7b3acf5..7e22167 100644
--- a/android/src/com/android/tools/idea/wizard/NewProjectWizard.java
+++ b/android/src/com/android/tools/idea/wizard/NewProjectWizard.java
@@ -15,24 +15,36 @@
*/
package com.android.tools.idea.wizard;
-import com.android.tools.idea.gradle.GradleProjectImporter;
+import com.android.tools.idea.gradle.project.GradleProjectImporter;
+import com.android.tools.idea.gradle.util.LocalProperties;
import com.android.tools.idea.templates.TemplateManager;
import com.android.tools.idea.templates.TemplateMetadata;
+import com.android.tools.idea.templates.TemplateUtils;
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+import com.google.common.io.Closeables;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.io.FileUtil;
-import icons.AndroidIcons;
+import com.intellij.openapi.util.io.FileUtilRt;
+import org.jetbrains.annotations.NotNull;
-import javax.swing.*;
import java.awt.*;
import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Properties;
+import static com.android.SdkConstants.*;
import static com.android.tools.idea.templates.Template.CATEGORY_ACTIVITIES;
import static com.android.tools.idea.templates.TemplateMetadata.ATTR_BUILD_API;
+import static com.android.tools.idea.templates.TemplateMetadata.ATTR_PACKAGE_NAME;
+import static icons.AndroidIcons.Wizards.NewProjectSidePanel;
/**
* NewProjectWizard runs the wizard for creating entirely new Android projects. It takes the user
@@ -40,18 +52,21 @@
* the user to choose an activity to populate it. The wizard is template-driven, using templates
* that live in the ADK.
*/
-public class NewProjectWizard extends TemplateWizard {
+public class NewProjectWizard extends TemplateWizard implements TemplateParameterStep.UpdateListener {
private static final Logger LOG = Logger.getInstance("#" + NewProjectWizard.class.getName());
+ private static final String ATTR_GRADLE_DISTRIBUTION_URL = "distributionUrl";
+ private static final String JAVA_SRC_PATH = FD_SOURCES + File.separator + FD_MAIN + File.separator + FD_JAVA;
+ private static final String ERROR_MSG_TITLE = "New Project Wizard";
+ private static final String UNABLE_TO_CREATE_DIR_FORMAT = "Unable to create directory '%s1$s'.";
private NewProjectWizardState myWizardState;
- private ConfigureAndroidModuleStep myConfigureAndroidModuleStep;
private LauncherIconStep myLauncherIconStep;
private ChooseTemplateStep myChooseActivityStep;
private TemplateParameterStep myActivityParameterStep;
private boolean myInitializationComplete = false;
public NewProjectWizard() {
- super("New Project", (Project)null);
+ super("New Project", null);
getWindow().setMinimumSize(new Dimension(800, 640));
init();
}
@@ -66,13 +81,19 @@
throw new IllegalStateException(msg);
}
myWizardState = new NewProjectWizardState();
+ myWizardState.convertApisToInt();
+ myWizardState.put(TemplateMetadata.ATTR_GRADLE_VERSION, TemplateMetadata.GRADLE_VERSION);
+ myWizardState.put(TemplateMetadata.ATTR_GRADLE_PLUGIN_VERSION, TemplateMetadata.GRADLE_PLUGIN_VERSION);
+ myWizardState.put(TemplateMetadata.ATTR_V4_SUPPORT_LIBRARY_VERSION, TemplateMetadata.V4_SUPPORT_LIBRARY_VERSION);
- myConfigureAndroidModuleStep = new ConfigureAndroidModuleStep(this, myWizardState);
- myLauncherIconStep = new LauncherIconStep(this, myWizardState.getLauncherIconState());
- myChooseActivityStep = new ChooseTemplateStep(this, myWizardState.getActivityTemplateState(), CATEGORY_ACTIVITIES);
- myActivityParameterStep = new TemplateParameterStep(this, myWizardState.getActivityTemplateState());
+ ConfigureAndroidModuleStep configureAndroidModuleStep =
+ new ConfigureAndroidModuleStep(myWizardState, myProject, NewProjectSidePanel, this);
+ myLauncherIconStep = new LauncherIconStep(myWizardState.getLauncherIconState(), myProject, NewProjectSidePanel, this);
+ myChooseActivityStep = new ChooseTemplateStep(myWizardState.getActivityTemplateState(), CATEGORY_ACTIVITIES, myProject,
+ NewProjectSidePanel, this, null);
+ myActivityParameterStep = new TemplateParameterStep(myWizardState.getActivityTemplateState(), myProject, NewProjectSidePanel, this);
- mySteps.add(myConfigureAndroidModuleStep);
+ mySteps.add(configureAndroidModuleStep);
mySteps.add(myLauncherIconStep);
mySteps.add(myChooseActivityStep);
mySteps.add(myActivityParameterStep);
@@ -82,57 +103,117 @@
}
@Override
- void update() {
+ public void update() {
if (!myInitializationComplete) {
return;
}
- myLauncherIconStep.setVisible((Boolean)myWizardState.get(TemplateMetadata.ATTR_CREATE_ICONS));
- myChooseActivityStep.setVisible((Boolean)myWizardState.get(NewProjectWizardState.ATTR_CREATE_ACTIVITY));
- myActivityParameterStep.setVisible((Boolean)myWizardState.get(NewProjectWizardState.ATTR_CREATE_ACTIVITY));
+ myLauncherIconStep.setVisible(myWizardState.getBoolean(TemplateMetadata.ATTR_CREATE_ICONS));
+ myChooseActivityStep.setVisible(myWizardState.getBoolean(NewModuleWizardState.ATTR_CREATE_ACTIVITY));
+ myActivityParameterStep.setVisible(myWizardState.getBoolean(NewModuleWizardState.ATTR_CREATE_ACTIVITY));
super.update();
}
- @Override
- public Icon getSidePanelIcon() {
- return AndroidIcons.Wizards.NewProjectSidePanel;
- }
-
public void createProject() {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
+ List<String> errors = Lists.newArrayList();
try {
- populateDirectoryParameters(myWizardState);
- String projectName = (String)myWizardState.get(NewProjectWizardState.ATTR_MODULE_NAME);
- File projectRoot = new File((String)myWizardState.get(NewProjectWizardState.ATTR_PROJECT_LOCATION));
+ myWizardState.populateDirectoryParameters();
+ String projectName = myWizardState.getString(NewProjectWizardState.ATTR_MODULE_NAME);
+ File projectRoot = new File(myWizardState.getString(NewModuleWizardState.ATTR_PROJECT_LOCATION));
File moduleRoot = new File(projectRoot, projectName);
- File gradleWrapperSrc = new File(TemplateManager.getTemplateRootFolder(), GRADLE_WRAPPER_PATH);
- projectRoot.mkdirs();
- FileUtil.copyDirContent(gradleWrapperSrc, projectRoot);
- if ((Boolean)myWizardState.get(TemplateMetadata.ATTR_CREATE_ICONS)) {
+ if (!FileUtilRt.createDirectory(projectRoot)) {
+ errors.add(String.format(UNABLE_TO_CREATE_DIR_FORMAT, projectRoot.getPath()));
+ }
+ createGradleWrapper(projectRoot);
+ int apiLevel = myWizardState.getInt(ATTR_BUILD_API);
+ Sdk sdk = getSdk(apiLevel);
+ if (sdk == null) {
+ // This will NEVER happen. The SDK has been already set before this wizard runs.
+ errors.add(String.format("Unable to find an Android SDK with API level %d.", apiLevel));
+ }
+ else {
+ LocalProperties localProperties = new LocalProperties(projectRoot);
+ localProperties.setAndroidSdkPath(sdk);
+ localProperties.save();
+ }
+ if (myWizardState.getBoolean(TemplateMetadata.ATTR_CREATE_ICONS)) {
myWizardState.getLauncherIconState().outputImages(moduleRoot);
}
myWizardState.updateParameters();
+ myWizardState.updateDependencies();
myWizardState.myTemplate.render(projectRoot, moduleRoot, myWizardState.myParameters);
- if ((Boolean)myWizardState.get(NewProjectWizardState.ATTR_CREATE_ACTIVITY)) {
+ if (myWizardState.getBoolean(NewModuleWizardState.ATTR_CREATE_ACTIVITY)) {
myWizardState.getActivityTemplateState().getTemplate()
.render(moduleRoot, moduleRoot, myWizardState.getActivityTemplateState().myParameters);
+ myWizardState.myTemplate.getFilesToOpen().addAll(myWizardState.getActivityTemplateState().getTemplate().getFilesToOpen());
}
- Sdk sdk = getSdk((Integer)myWizardState.get(ATTR_BUILD_API));
+ else {
+ // Ensure that at least the Java source directory exists. We could create other directories but this is the most used.
+ // TODO: We should perhaps instantiate this from the Freemarker template, but trying to use the copy command to copy
+ // empty directories is problematic, and we don't have a primitive command to create a directory.
+ File javaSrcDir = new File(moduleRoot, JAVA_SRC_PATH);
+ File packageDir = new File(javaSrcDir, myWizardState.getString(ATTR_PACKAGE_NAME).replace('.', File.separatorChar));
+ if (!FileUtilRt.createDirectory(packageDir)) {
+ errors.add(String.format(UNABLE_TO_CREATE_DIR_FORMAT, packageDir.getPath()));
+ }
+ }
GradleProjectImporter projectImporter = GradleProjectImporter.getInstance();
- projectImporter.importProject(projectName, projectRoot, sdk, null);
+ projectImporter.importProject(projectName, projectRoot, new GradleProjectImporter.Callback() {
+ @Override
+ public void projectImported(@NotNull Project project) {
+ TemplateUtils.openEditors(project, myWizardState.myTemplate.getFilesToOpen(), true);
+ }
+
+ @Override
+ public void importFailed(@NotNull Project project, @NotNull final String errorMessage) {
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ Messages.showErrorDialog(errorMessage, ERROR_MSG_TITLE);
+ }
+ });
+ }
+ });
}
catch (Exception e) {
- String title;
- if (e instanceof ConfigurationException) {
- title = ((ConfigurationException)e).getTitle();
- } else {
- title = "New Project Wizard";
- }
- Messages.showErrorDialog(e.getMessage(), title);
+ Messages.showErrorDialog(e.getMessage(), ERROR_MSG_TITLE);
LOG.error(e);
}
+ if (!errors.isEmpty()) {
+ String msg = errors.size() == 1 ? errors.get(0) : Joiner.on('\n').join(errors);
+ Messages.showErrorDialog(msg, ERROR_MSG_TITLE);
+ LOG.error(msg);
+ }
}
});
}
+
+ private static void createGradleWrapper(File projectRoot) throws IOException {
+ File gradleWrapperSrc = new File(TemplateManager.getTemplateRootFolder(), GRADLE_WRAPPER_PATH);
+ FileUtil.copyDirContent(gradleWrapperSrc, projectRoot);
+ File gradleWrapperProperties = new File(projectRoot, GRADLE_WRAPPER_PROPERTIES_PATH);
+
+ Properties wrapperProperties = new Properties();
+
+ FileInputStream is = null;
+ try {
+ //noinspection IOResourceOpenedButNotSafelyClosed
+ is = new FileInputStream(gradleWrapperProperties);
+ wrapperProperties.load(is);
+ } finally {
+ Closeables.closeQuietly(is);
+ }
+
+ FileOutputStream os = null;
+ try {
+ wrapperProperties.setProperty(ATTR_GRADLE_DISTRIBUTION_URL, TemplateMetadata.GRADLE_DISTRIBUTION_URL);
+ //noinspection IOResourceOpenedButNotSafelyClosed
+ os = new FileOutputStream(gradleWrapperProperties);
+ wrapperProperties.store(os, "");
+ } finally {
+ Closeables.closeQuietly(os);
+ }
+ }
}
diff --git a/android/src/com/android/tools/idea/wizard/NewProjectWizardState.java b/android/src/com/android/tools/idea/wizard/NewProjectWizardState.java
index 6d7f67e..9a94b8c 100644
--- a/android/src/com/android/tools/idea/wizard/NewProjectWizardState.java
+++ b/android/src/com/android/tools/idea/wizard/NewProjectWizardState.java
@@ -25,7 +25,8 @@
import java.io.File;
import static com.android.tools.idea.templates.Template.CATEGORY_PROJECTS;
-import static com.android.tools.idea.templates.TemplateMetadata.*;
+import static com.android.tools.idea.templates.TemplateMetadata.ATTR_IS_LAUNCHER;
+import static com.android.tools.idea.templates.TemplateMetadata.ATTR_IS_LIBRARY_MODULE;
/**
* Value object which holds the current state of the wizard pages for the
@@ -48,15 +49,11 @@
myHidden.remove(ATTR_IS_LIBRARY_MODULE);
put(ATTR_LIBRARY, false);
+ put(ATTR_IS_LAUNCHER, true);
put(ATTR_PROJECT_LOCATION, getProjectFileDirectory());
updateTemplate();
setParameterDefaults();
- convertToInt(ATTR_MIN_API);
- convertToInt(ATTR_BUILD_API);
- convertToInt(ATTR_MIN_API_LEVEL);
- convertToInt(ATTR_TARGET_API);
-
updateParameters();
}
diff --git a/android/src/com/android/tools/idea/wizard/NewTemplateObjectWizard.java b/android/src/com/android/tools/idea/wizard/NewTemplateObjectWizard.java
index 5dbeafd..afb9db5 100644
--- a/android/src/com/android/tools/idea/wizard/NewTemplateObjectWizard.java
+++ b/android/src/com/android/tools/idea/wizard/NewTemplateObjectWizard.java
@@ -17,44 +17,46 @@
import com.android.tools.idea.rendering.ManifestInfo;
import com.android.tools.idea.templates.TemplateMetadata;
-import com.intellij.openapi.application.AccessToken;
+import com.android.tools.idea.templates.TemplateUtils;
import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.externalSystem.util.ExternalSystemUtil;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModuleRootManager;
-import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.openapi.vfs.VirtualFileSystem;
-import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
import org.jetbrains.android.sdk.AndroidPlatform;
import org.jetbrains.annotations.Nullable;
-import org.jetbrains.plugins.gradle.util.GradleConstants;
import java.io.File;
import static com.android.tools.idea.templates.TemplateMetadata.ATTR_BUILD_API;
+import static com.android.tools.idea.templates.TemplateMetadata.ATTR_MIN_API_LEVEL;
/**
* NewTemplateObjectWizard is a base class for templates that instantiate new Android objects based on templates. These aren't for
* complex objects like projects or modules that get customized wizards, but objects simple enough that we can show a generic template
* parameter page and run the template against the source tree.
*/
-public class NewTemplateObjectWizard extends TemplateWizard {
+public class NewTemplateObjectWizard extends TemplateWizard implements TemplateParameterStep.UpdateListener {
private static final Logger LOG = Logger.getInstance("#" + NewTemplateObjectWizard.class.getName());
private TemplateWizardState myWizardState;
private Project myProject;
private Module myModule;
private String myTemplateCategory;
+ private VirtualFile myTargetFolder;
- public NewTemplateObjectWizard(@Nullable Project project, Module module, String templateCategory) {
+ public NewTemplateObjectWizard(@Nullable Project project, Module module, VirtualFile invocationTarget, String templateCategory) {
super("New " + templateCategory, project);
myProject = project;
myModule = module;
myTemplateCategory = templateCategory;
+ if (invocationTarget.isDirectory()) {
+ myTargetFolder = invocationTarget;
+ } else {
+ myTargetFolder = invocationTarget.getParent();
+ }
+
init();
}
@@ -62,15 +64,21 @@
protected void init() {
myWizardState = new TemplateWizardState();
myWizardState.put(ATTR_BUILD_API, AndroidPlatform.getInstance(myModule).getTarget().getVersion().getApiLevel());
+ myWizardState.put(ATTR_MIN_API_LEVEL, ManifestInfo.get(myModule).getMinSdkVersion());
- mySteps.add(new ChooseTemplateStep(this, myWizardState, myTemplateCategory));
- mySteps.add(new TemplateParameterStep(this, myWizardState));
+ mySteps.add(new ChooseTemplateStep(myWizardState, myTemplateCategory, myProject, null, this, null));
+ mySteps.add(new TemplateParameterStep(myWizardState, myProject, null, this));
myWizardState.put(NewProjectWizardState.ATTR_PROJECT_LOCATION, myProject.getBasePath());
- myWizardState.put(NewProjectWizardState.ATTR_MODULE_NAME, myModule.getName());
+ // We're really interested in the directory name on disk, not the module name. These will be different if you give a module the same
+ // name as its containing project.
+ String moduleName = new File(myModule.getModuleFilePath()).getParentFile().getName();
+ myWizardState.put(NewProjectWizardState.ATTR_MODULE_NAME, moduleName);
myWizardState.myHidden.add(TemplateMetadata.ATTR_PACKAGE_NAME);
- myWizardState.put(TemplateMetadata.ATTR_PACKAGE_NAME, ManifestInfo.get(myModule).getPackage());
+ myWizardState.put(TemplateMetadata.ATTR_PACKAGE_ROOT, myTargetFolder.getPath());
+
+ myWizardState.myFinal.add(TemplateMetadata.ATTR_PACKAGE_ROOT);
super.init();
}
@@ -80,7 +88,7 @@
@Override
public void run() {
try {
- populateDirectoryParameters(myWizardState);
+ myWizardState.populateDirectoryParameters();
File projectRoot = new File(myProject.getBasePath());
ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(myModule);
@@ -89,6 +97,8 @@
VirtualFile rootDir = contentRoots[0];
File moduleRoot = new File(rootDir.getCanonicalPath());
myWizardState.myTemplate.render(projectRoot, moduleRoot, myWizardState.myParameters);
+ // Open any new files specified by the template
+ TemplateUtils.openEditors(myProject, myWizardState.myTemplate.getFilesToOpen(), true);
}
}
catch (Exception e) {
diff --git a/android/src/com/android/tools/idea/wizard/TemplateParameterStep.java b/android/src/com/android/tools/idea/wizard/TemplateParameterStep.java
index 6fc63b3..34110ad 100644
--- a/android/src/com/android/tools/idea/wizard/TemplateParameterStep.java
+++ b/android/src/com/android/tools/idea/wizard/TemplateParameterStep.java
@@ -15,10 +15,12 @@
*/
package com.android.tools.idea.wizard;
+import com.intellij.openapi.project.Project;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
import com.android.tools.idea.templates.Parameter;
import com.intellij.uiDesigner.core.Spacer;
+import org.jetbrains.annotations.Nullable;
import java.util.Collection;
@@ -35,8 +37,9 @@
private JLabel myError;
private JComponent myPreferredFocusComponent;
- public TemplateParameterStep(TemplateWizard templateWizard, TemplateWizardState state) {
- super(templateWizard, state);
+ public TemplateParameterStep(TemplateWizardState state, @Nullable Project project, @Nullable Icon sidePanelIcon,
+ UpdateListener updateListener) {
+ super(state, project, sidePanelIcon, updateListener);
}
@Override
diff --git a/android/src/com/android/tools/idea/wizard/TemplateWizard.java b/android/src/com/android/tools/idea/wizard/TemplateWizard.java
index 663462f..379c8b0 100644
--- a/android/src/com/android/tools/idea/wizard/TemplateWizard.java
+++ b/android/src/com/android/tools/idea/wizard/TemplateWizard.java
@@ -16,7 +16,6 @@
package com.android.tools.idea.wizard;
import com.android.sdklib.IAndroidTarget;
-import com.android.tools.idea.templates.TemplateMetadata;
import com.intellij.ide.util.projectWizard.ModuleWizardStep;
import com.intellij.ide.wizard.AbstractWizard;
import com.intellij.openapi.project.Project;
@@ -25,11 +24,10 @@
import icons.AndroidIcons;
import org.jetbrains.android.sdk.AndroidPlatform;
import org.jetbrains.android.sdk.AndroidSdkData;
+import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
-import java.io.File;
-import java.io.IOException;
/**
* TemplateWizard is a base class for Freemarker template-based wizards.
@@ -39,11 +37,12 @@
public static final String JAVA_SOURCE_PATH = "java";
public static final String RESOURCE_SOURCE_PATH = "res";
public static final String GRADLE_WRAPPER_PATH = "gradle/wrapper";
+ public static final String GRADLE_WRAPPER_PROPERTIES_PATH = "gradle/wrapper/gradle-wrapper.properties";
protected static final String MAVEN_URL_PROPERTY = "android.mavenRepoUrl";
protected Project myProject;
- public TemplateWizard(String title, Project project) {
+ public TemplateWizard(@NotNull String title, @Nullable Project project) {
super(title, project);
myProject = project;
}
@@ -52,14 +51,18 @@
* Subclasses and step classes can call this to update next/previous button state; this is
* generally called after parameter validation has finished.
*/
- void update() {
+ public void update() {
updateButtons();
}
@Override
protected void init() {
super.init();
- TemplateWizardStep step = (TemplateWizardStep)mySteps.get(getCurrentStep());
+ int currentStep = getCurrentStep();
+ if (currentStep >= mySteps.size()) {
+ return;
+ }
+ TemplateWizardStep step = (TemplateWizardStep)mySteps.get(currentStep);
step.getPreferredFocusedComponent();
if (step != null) {
step.update();
@@ -101,29 +104,6 @@
return AndroidIcons.Wizards.NewModuleSidePanel;
}
- /**
- * Sets a number of parameters that get picked up as globals in the Freemarker templates. These are used to specify the directories where
- * a number of files go. The templates use these globals to allow them to service both old-style Ant builds with the old directory
- * structure and new-style Gradle builds with the new structure.
- */
- protected void populateDirectoryParameters(TemplateWizardState wizardState) throws IOException {
- File projectRoot = new File((String)wizardState.get(NewModuleWizardState.ATTR_PROJECT_LOCATION));
- File moduleRoot = new File(projectRoot, (String)wizardState.get(NewProjectWizardState.ATTR_MODULE_NAME));
- File mainFlavorSourceRoot = new File(moduleRoot, MAIN_FLAVOR_SOURCE_PATH);
- File javaSourceRoot = new File(mainFlavorSourceRoot, JAVA_SOURCE_PATH);
- File javaSourcePackageRoot = new File(javaSourceRoot, ((String)wizardState.get(TemplateMetadata.ATTR_PACKAGE_NAME)).replace('.', '/'));
- File resourceSourceRoot = new File(mainFlavorSourceRoot, RESOURCE_SOURCE_PATH);
- String mavenUrl = System.getProperty(MAVEN_URL_PROPERTY);
- wizardState.put(TemplateMetadata.ATTR_TOP_OUT, projectRoot.getPath());
- wizardState.put(TemplateMetadata.ATTR_PROJECT_OUT, moduleRoot.getPath());
- wizardState.put(TemplateMetadata.ATTR_MANIFEST_OUT, mainFlavorSourceRoot.getPath());
- wizardState.put(TemplateMetadata.ATTR_SRC_OUT, javaSourcePackageRoot.getPath());
- wizardState.put(TemplateMetadata.ATTR_RES_OUT, resourceSourceRoot.getPath());
- if (mavenUrl != null) {
- wizardState.put(TemplateMetadata.ATTR_MAVEN_URL, mavenUrl);
- }
- }
-
@Nullable
protected Sdk getSdk(int apiLevel) {
for (Sdk sdk : ProjectJdkTable.getInstance().getAllJdks()) {
diff --git a/android/src/com/android/tools/idea/wizard/TemplateWizardModuleBuilder.java b/android/src/com/android/tools/idea/wizard/TemplateWizardModuleBuilder.java
new file mode 100644
index 0000000..4b897f4
--- /dev/null
+++ b/android/src/com/android/tools/idea/wizard/TemplateWizardModuleBuilder.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.wizard;
+
+import com.android.tools.idea.gradle.project.GradleProjectImporter;
+import com.android.tools.idea.templates.TemplateMetadata;
+import com.intellij.ide.util.projectWizard.ModuleBuilder;
+import com.intellij.ide.util.projectWizard.ModuleWizardStep;
+import com.intellij.ide.util.projectWizard.WizardContext;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.ModuleType;
+import com.intellij.openapi.module.StdModuleTypes;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.project.DumbAwareRunnable;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ModifiableRootModel;
+import com.intellij.openapi.roots.ui.configuration.ModulesProvider;
+import com.intellij.openapi.startup.StartupManager;
+import com.intellij.openapi.ui.Messages;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.io.File;
+import java.util.List;
+
+import static com.android.tools.idea.templates.Template.CATEGORY_ACTIVITIES;
+import static com.android.tools.idea.wizard.NewProjectWizardState.ATTR_MODULE_NAME;
+
+public class TemplateWizardModuleBuilder extends ModuleBuilder implements TemplateWizardStep.UpdateListener {
+ private static final Logger LOG = Logger.getInstance("#" + TemplateWizardModuleBuilder.class.getName());
+ private final TemplateMetadata myMetadata;
+ final List<ModuleWizardStep> mySteps;
+ private Project myProject;
+
+ NewModuleWizardState myWizardState;
+ ConfigureAndroidModuleStep myConfigureAndroidModuleStep;
+ TemplateParameterStep myTemplateParameterStep;
+ LauncherIconStep myLauncherIconStep;
+ ChooseTemplateStep myChooseActivityStep;
+ TemplateParameterStep myActivityTemplateParameterStep;
+ boolean myInitializationComplete = false;
+
+ public TemplateWizardModuleBuilder(@Nullable File templateFile,
+ @Nullable TemplateMetadata metadata,
+ @NotNull Project project,
+ @Nullable Icon sidePanelIcon,
+ List<ModuleWizardStep> steps,
+ boolean hideModuleName) {
+ myMetadata = metadata;
+ myProject = project;
+ mySteps = steps;
+
+ myWizardState = new NewModuleWizardState() {
+ @Override
+ public void setTemplateLocation(@NotNull File file) {
+ super.setTemplateLocation(file);
+ update();
+ }
+ };
+
+ if (templateFile != null) {
+ myWizardState.setTemplateLocation(templateFile);
+ }
+ if (hideModuleName) {
+ myWizardState.myHidden.add(ATTR_MODULE_NAME);
+ }
+
+ myWizardState.convertApisToInt();
+
+ myConfigureAndroidModuleStep = new ConfigureAndroidModuleStep(myWizardState, myProject, sidePanelIcon, this);
+ myTemplateParameterStep = new TemplateParameterStep(myWizardState, myProject, sidePanelIcon, this);
+ myLauncherIconStep = new LauncherIconStep(myWizardState.getLauncherIconState(), myProject, sidePanelIcon, this);
+ myChooseActivityStep = new ChooseTemplateStep(myWizardState.getActivityTemplateState(), CATEGORY_ACTIVITIES, myProject, sidePanelIcon,
+ this, null);
+ myActivityTemplateParameterStep = new TemplateParameterStep(myWizardState.getActivityTemplateState(), myProject, sidePanelIcon, this);
+
+ mySteps.add(myConfigureAndroidModuleStep);
+ mySteps.add(myTemplateParameterStep);
+ mySteps.add(myLauncherIconStep);
+ mySteps.add(myChooseActivityStep);
+ mySteps.add(myActivityTemplateParameterStep);
+
+ myWizardState.put(NewModuleWizardState.ATTR_PROJECT_LOCATION, project.getBasePath());
+ myWizardState.put(TemplateMetadata.ATTR_GRADLE_VERSION, TemplateMetadata.GRADLE_VERSION);
+ myWizardState.put(TemplateMetadata.ATTR_GRADLE_PLUGIN_VERSION, TemplateMetadata.GRADLE_PLUGIN_VERSION);
+ myWizardState.put(TemplateMetadata.ATTR_V4_SUPPORT_LIBRARY_VERSION, TemplateMetadata.V4_SUPPORT_LIBRARY_VERSION);
+
+ update();
+
+ myInitializationComplete = true;
+ }
+
+ @Nullable
+ @Override
+ public String getBuilderId() {
+ return myMetadata.getTitle();
+ }
+
+
+ @Override
+ @NotNull
+ public ModuleWizardStep[] createWizardSteps(@NotNull WizardContext wizardContext, @NotNull ModulesProvider modulesProvider) {
+ return mySteps.toArray(new ModuleWizardStep[] {});
+ }
+
+ @Override
+ public void update() {
+ if (!myInitializationComplete) {
+ return;
+ }
+ myConfigureAndroidModuleStep.setVisible(myWizardState.myIsAndroidModule);
+ myTemplateParameterStep.setVisible(!myWizardState.myIsAndroidModule);
+ myLauncherIconStep.setVisible(myWizardState.myIsAndroidModule && (Boolean)myWizardState.get(TemplateMetadata.ATTR_CREATE_ICONS));
+ myChooseActivityStep.setVisible(
+ myWizardState.myIsAndroidModule && (Boolean)myWizardState.get(NewProjectWizardState.ATTR_CREATE_ACTIVITY));
+ myActivityTemplateParameterStep.setVisible(
+ myWizardState.myIsAndroidModule && (Boolean)myWizardState.get(NewProjectWizardState.ATTR_CREATE_ACTIVITY));
+ }
+
+ @Override
+ public void setupRootModel(final @NotNull ModifiableRootModel rootModel) throws ConfigurationException {
+ StartupManager.getInstance(myProject).runWhenProjectIsInitialized(new DumbAwareRunnable() {
+ @Override
+ public void run() {
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ createModule();
+ }
+ });
+ }
+ });
+ }
+ });
+ }
+
+ @Override
+ @NotNull
+ public ModuleType getModuleType() {
+ return StdModuleTypes.JAVA;
+ }
+
+ @Override
+ public void setName(@NotNull String name) {
+ super.setName(name);
+ myConfigureAndroidModuleStep.setModuleName(name);
+ }
+
+ public void createModule() {
+ try {
+ myWizardState.populateDirectoryParameters();
+ File projectRoot = new File(myProject.getBasePath());
+ File moduleRoot = new File(projectRoot, (String)myWizardState.get(NewProjectWizardState.ATTR_MODULE_NAME));
+ projectRoot.mkdirs();
+ if (myLauncherIconStep.isStepVisible() && (Boolean)myWizardState.get(TemplateMetadata.ATTR_CREATE_ICONS)) {
+ myWizardState.getLauncherIconState().outputImages(moduleRoot);
+ }
+ myWizardState.updateParameters();
+ myWizardState.myTemplate.render(projectRoot, moduleRoot, myWizardState.myParameters);
+ if (myActivityTemplateParameterStep.isStepVisible() && (Boolean)myWizardState.get(NewProjectWizardState.ATTR_CREATE_ACTIVITY)) {
+ myWizardState.getActivityTemplateState().getTemplate()
+ .render(moduleRoot, moduleRoot, myWizardState.getActivityTemplateState().myParameters);
+ }
+ GradleProjectImporter.getInstance().reImportProject(myProject, null);
+ } catch (Exception e) {
+ Messages.showErrorDialog(e.getMessage(), "New Module");
+ LOG.error(e);
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/wizard/TemplateWizardProjectTemplateFactory.java b/android/src/com/android/tools/idea/wizard/TemplateWizardProjectTemplateFactory.java
new file mode 100644
index 0000000..9dc5ab2
--- /dev/null
+++ b/android/src/com/android/tools/idea/wizard/TemplateWizardProjectTemplateFactory.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.wizard;
+
+import com.android.tools.idea.gradle.util.Projects;
+import com.android.tools.idea.templates.TemplateManager;
+import com.android.tools.idea.templates.TemplateMetadata;
+import com.intellij.ide.util.projectWizard.ModuleWizardStep;
+import com.intellij.ide.util.projectWizard.WizardContext;
+import com.intellij.openapi.project.Project;
+import com.intellij.platform.ProjectTemplate;
+import com.intellij.platform.ProjectTemplatesFactory;
+import com.intellij.platform.templates.BuilderBasedTemplate;
+import icons.AndroidIcons;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class TemplateWizardProjectTemplateFactory extends ProjectTemplatesFactory {
+
+ public static final String ANDROID_GRADLE_GROUP = "Android Gradle";
+ public static final ProjectTemplate[] EMPTY_PROJECT_TEMPLATES = new ProjectTemplate[]{};
+
+ @NotNull
+ @Override
+ public String[] getGroups() {
+ return new String[] {ANDROID_GRADLE_GROUP};
+ }
+
+ @Override
+ public Icon getGroupIcon(String group) {
+ return AndroidIcons.Android;
+ }
+
+ @NotNull
+ @Override
+ public ProjectTemplate[] createTemplates(String group, WizardContext context) {
+ Project project = context.getProject();
+ if (project == null || !Projects.isGradleProject(project)) {
+ return EMPTY_PROJECT_TEMPLATES;
+ }
+ TemplateManager manager = TemplateManager.getInstance();
+ List<File> templates = manager.getTemplates("projects");
+ List<ProjectTemplate> tt = new ArrayList<ProjectTemplate>();
+ for (int i = 0, n = templates.size(); i < n; i++) {
+ File template = templates.get(i);
+ TemplateMetadata metadata = manager.getTemplate(template);
+ if (metadata == null || !metadata.isSupported()) {
+ continue;
+ }
+ tt.add(new AndroidProjectTemplate(template, metadata, project));
+ }
+ return tt.toArray(EMPTY_PROJECT_TEMPLATES);
+ }
+
+ private static class AndroidProjectTemplate extends BuilderBasedTemplate {
+ private final TemplateMetadata myTemplateMetadata;
+
+ private AndroidProjectTemplate(File templateFile, TemplateMetadata metadata, Project project) {
+ super(new TemplateWizardModuleBuilder(templateFile, metadata, project, null, new ArrayList<ModuleWizardStep>(), true));
+ myTemplateMetadata = metadata;
+ }
+
+ @NotNull
+ @Override
+ public String getName() {
+ return myTemplateMetadata.getTitle();
+ }
+
+ @Nullable
+ @Override
+ public String getDescription() {
+ return myTemplateMetadata.getDescription();
+ }
+ }
+
+}
diff --git a/android/src/com/android/tools/idea/wizard/TemplateWizardState.java b/android/src/com/android/tools/idea/wizard/TemplateWizardState.java
index c2800b2..ac35aca 100644
--- a/android/src/com/android/tools/idea/wizard/TemplateWizardState.java
+++ b/android/src/com/android/tools/idea/wizard/TemplateWizardState.java
@@ -23,16 +23,46 @@
import org.jetbrains.annotations.Nullable;
import java.io.File;
+import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
+import static com.android.tools.idea.templates.TemplateMetadata.*;
+
/**
* Value object which holds the current state of the wizard pages for
* {@link NewTemplateObjectWizard}-derived wizards.
*/
public class TemplateWizardState {
+ /*
+ * TODO: The parameter handling code needs to be completely rewritten. It's extremely fragile now. When it's rewritten, it needs to take
+ * the following into account:
+ *
+ * Parameters may or may not have corresponding parameters in the template. If they do, they may or may not have default values.
+ *
+ * Parameters have types, and the conversion between types ought to be fairly transparent, so that you don't have to do all the type
+ * conversion you do now -- see the int vs. string problems with API level parameters (convertApisToInt).
+ *
+ * A parameter can be linked to a UI object. When the UI object is modified by the user, the parameter needs to be updated. Beware of UI
+ * changes that happen programatically
+ *
+ * Some parameters have calculated values that can be overridden by the user.
+ *
+ * Sometimes we want to clear out parameters or reset their values from defaults.
+ *
+ * Sometimes we know at wizard step creation time how we want to populate parameters; sometimes we only find that out later. This
+ * shouldn't affect how we bind parameters to UI objects.
+ *
+ * Look at all the places that validate, update, register, and refresh UI, and rationalize. Right now too many times these methods are
+ * called as ad-hoc fixes for individual bugs.
+ *
+ * ConfigureAndroidModuleStep may or may not know its template at construction time.
+ *
+ * You ought to be able to change a value either by the user changing something in the UI, or programatically updaing something and
+ * propagating changes to the UI, without undue hackery. Right now to do the latter we have to set this global disable-changes bit.
+ */
/** Suffix added by default to activity names */
public static final String ACTIVITY_NAME_SUFFIX = "Activity";
/** Prefix added to default layout names */
@@ -48,36 +78,90 @@
* has information for these parameters) */
protected final Set<String> myHidden = new HashSet<String>();
+ /**
+ * Ids for parameters whose values should not change.
+ */
+ protected final Set<String> myFinal = new HashSet<String>();
+
/** Ids for parameters which have been modified directly by the user. */
protected final Set<String> myModified = new HashSet<String>();
- /**
- * Create a new state object for use by the {@link NewTemplatePage}
- */
public TemplateWizardState() {
put(TemplateMetadata.ATTR_IS_NEW_PROJECT, false);
put(TemplateMetadata.ATTR_IS_GRADLE, "true");
}
+ /**
+ * Sets a number of parameters that get picked up as globals in the Freemarker templates. These are used to specify the directories where
+ * a number of files go. The templates use these globals to allow them to service both old-style Ant builds with the old directory
+ * structure and new-style Gradle builds with the new structure.
+ */
+ protected void populateDirectoryParameters() throws IOException {
+ File projectRoot = new File((String)get(NewModuleWizardState.ATTR_PROJECT_LOCATION));
+ File moduleRoot = new File(projectRoot, (String)get(NewProjectWizardState.ATTR_MODULE_NAME));
+ File mainFlavorSourceRoot = new File(moduleRoot, TemplateWizard.MAIN_FLAVOR_SOURCE_PATH);
+
+ File javaSourcePackageRoot;
+ if (myParameters.containsKey(TemplateMetadata.ATTR_PACKAGE_ROOT)) {
+ javaSourcePackageRoot = new File((String)get(TemplateMetadata.ATTR_PACKAGE_ROOT));
+ } else {
+ File javaSourceRoot = new File(mainFlavorSourceRoot, TemplateWizard.JAVA_SOURCE_PATH);
+ javaSourcePackageRoot = new File(javaSourceRoot, getString(TemplateMetadata.ATTR_PACKAGE_NAME).replace('.', '/'));
+ }
+ File resourceSourceRoot = new File(mainFlavorSourceRoot, TemplateWizard.RESOURCE_SOURCE_PATH);
+ String mavenUrl = System.getProperty(TemplateWizard.MAVEN_URL_PROPERTY);
+ put(TemplateMetadata.ATTR_TOP_OUT, projectRoot.getPath());
+ put(TemplateMetadata.ATTR_PROJECT_OUT, moduleRoot.getPath());
+ put(TemplateMetadata.ATTR_MANIFEST_OUT, mainFlavorSourceRoot.getPath());
+ put(TemplateMetadata.ATTR_SRC_OUT, javaSourcePackageRoot.getPath());
+ put(TemplateMetadata.ATTR_RES_OUT, resourceSourceRoot.getPath());
+ if (mavenUrl != null) {
+ put(TemplateMetadata.ATTR_MAVEN_URL, mavenUrl);
+ }
+ }
+
public boolean hasTemplate() {
return myTemplate != null && myTemplate.getMetadata() != null;
}
- @NotNull
+ @Nullable
public Template getTemplate() {
return myTemplate;
}
- @NotNull
+ @Nullable
public TemplateMetadata getTemplateMetadata() {
+ if (myTemplate == null) {
+ return null;
+ }
return myTemplate.getMetadata();
}
@Nullable
- public Object get(String key) {
+ public Object get(@NotNull String key) {
return myParameters.get(key);
}
+ public boolean getBoolean(@NotNull String key) {
+ return get(key, Boolean.class);
+ }
+
+ public int getInt(@NotNull String key) {
+ return get(key, Integer.class);
+ }
+
+ @NotNull
+ public String getString(@NotNull String key) {
+ return get(key, String.class);
+ }
+
+ @NotNull
+ private <T> T get(@NotNull String key, @NotNull Class<T> valueType) {
+ Object val = get(key);
+ assert valueType.isInstance(val);
+ return valueType.cast(val);
+ }
+
public boolean hasAttr(String key) {
return myParameters.containsKey(key);
}
@@ -94,7 +178,9 @@
// Clear out any parameters from the old template and bring in the defaults for the new template.
if (myTemplate != null && myTemplate.getMetadata() != null) {
for (Parameter param : myTemplate.getMetadata().getParameters()) {
- myParameters.remove(param.id);
+ if (!myFinal.contains(param.id)) {
+ myParameters.remove(param.id);
+ }
}
}
myTemplate = Template.createFromPath(file);
@@ -104,7 +190,7 @@
protected void setParameterDefaults() {
for (Parameter param : myTemplate.getMetadata().getParameters()) {
- if (!myParameters.containsKey(param.id) && param.initial != null) {
+ if (!myFinal.contains(param.id) && !myParameters.containsKey(param.id) && param.initial != null) {
switch(param.type) {
case BOOLEAN:
put(param.id, Boolean.valueOf(param.initial));
@@ -119,9 +205,17 @@
}
}
- protected void convertToInt(String attr) {
- if (get(attr) != null) {
- put(attr, Integer.parseInt(get(attr).toString()));
+ public void convertApisToInt() {
+ convertToInt(ATTR_MIN_API);
+ convertToInt(ATTR_BUILD_API);
+ convertToInt(ATTR_MIN_API_LEVEL);
+ convertToInt(ATTR_TARGET_API);
+ }
+
+ private void convertToInt(@NotNull String key) {
+ Object value = get(key);
+ if (value != null && !(value instanceof Integer)) {
+ put(key, Integer.parseInt(value.toString()));
}
}
}
diff --git a/android/src/com/android/tools/idea/wizard/TemplateWizardStep.java b/android/src/com/android/tools/idea/wizard/TemplateWizardStep.java
index 963adfe..349d4bb 100644
--- a/android/src/com/android/tools/idea/wizard/TemplateWizardStep.java
+++ b/android/src/com/android/tools/idea/wizard/TemplateWizardStep.java
@@ -24,6 +24,7 @@
import com.google.common.collect.Maps;
import com.intellij.ide.util.projectWizard.ModuleWizardStep;
import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.ui.ColorPanel;
import com.intellij.ui.ColorUtil;
@@ -63,14 +64,23 @@
protected final TemplateWizardState myTemplateState;
protected final BiMap<String, JComponent> myParamFields = HashBiMap.create();
protected final Map<JRadioButton, Pair<String, Object>> myRadioButtonValues = Maps.newHashMap();
- protected final TemplateWizard myTemplateWizard;
+ private final Project myProject;
+ private final Icon mySidePanelIcon;
protected boolean myIgnoreUpdates = false;
protected boolean myIsValid = true;
protected boolean myVisible = true;
+ protected final UpdateListener myUpdateListener;
- public TemplateWizardStep(@NotNull TemplateWizard templateWizard, @NotNull TemplateWizardState state) {
+ public interface UpdateListener {
+ public void update();
+ }
+
+ public TemplateWizardStep(@NotNull TemplateWizardState state, @Nullable Project project, @Nullable Icon sidePanelIcon,
+ UpdateListener updateListener) {
myTemplateState = state;
- myTemplateWizard = templateWizard;
+ myProject = project;
+ mySidePanelIcon = sidePanelIcon;
+ myUpdateListener = updateListener;
}
@Override
@@ -163,6 +173,7 @@
@Override
public boolean validate() {
+ myTemplateState.convertApisToInt();
if (!myVisible) {
return true;
}
@@ -219,7 +230,7 @@
}
if (param != null) {
- String error = param.validate(myTemplateWizard.myProject, (String)myTemplateState.get(ATTR_PACKAGE_NAME), newValue);
+ String error = param.validate(myProject, (String)myTemplateState.get(ATTR_PACKAGE_NAME), newValue);
if (error != null) {
setErrorHtml(error);
return false;
@@ -235,6 +246,55 @@
return true;
}
+ public void refreshUiFromParameters() {
+ myTemplateState.myModified.clear();
+
+ for (Parameter param : myTemplateState.myTemplate.getMetadata().getParameters()) {
+ if (param.initial != null) {
+ myTemplateState.myParameters.remove(param.id);
+ }
+ }
+ myTemplateState.setParameterDefaults();
+ try {
+ myIgnoreUpdates = true;
+ for (String paramName : myParamFields.keySet()) {
+ if (myTemplateState.myHidden.contains(paramName)) {
+ continue;
+ }
+ JComponent component = myParamFields.get(paramName);
+ Object value = myTemplateState.get(paramName);
+ if (value == null) {
+ continue;
+ }
+ if (component instanceof JCheckBox) {
+ ((JCheckBox)component).setSelected(Boolean.parseBoolean(value.toString()));
+ }
+ else if (component instanceof JComboBox) {
+ for (int i = 0; i < ((JComboBox)component).getItemCount(); i++) {
+ if (((ComboBoxItem)((JComboBox)component).getItemAt(i)).id.equals(value)) {
+ ((JComboBox)component).setSelectedIndex(i);
+ break;
+ }
+ }
+ }
+ else if (component instanceof JTextField) {
+ ((JTextField)component).setText(value.toString());
+ } else if (component instanceof TextFieldWithBrowseButton) {
+ ((TextFieldWithBrowseButton)component).setText(value.toString());
+ } else if (component instanceof JSlider) {
+ ((JSlider)component).setValue(Integer.parseInt(value.toString()));
+ } else if (component instanceof JSpinner) {
+ ((JSpinner)component).setValue(Integer.parseInt(value.toString()));
+ } else if (component instanceof ColorPanel) {
+ ((ColorPanel)component).setSelectedColor((Color)value);
+ }
+ }
+ } finally {
+ myIgnoreUpdates = false;
+ }
+ update();
+ }
+
/**
* Takes a {@link JComboBox} instance and a {@Parameter} that represents an enumerated type and
* populates the combo box with all possible values of the enumerated type.
@@ -382,7 +442,9 @@
}
myIsValid = validate();
- myTemplateWizard.update();
+ if (myUpdateListener != null) {
+ myUpdateListener.update();
+ }
}
@Override
@@ -392,7 +454,7 @@
@Override
public Icon getIcon() {
- return myTemplateWizard.getSidePanelIcon();
+ return mySidePanelIcon;
}
@Override
diff --git a/android/src/org/jetbrains/android/AndroidColorAnnotator.java b/android/src/org/jetbrains/android/AndroidColorAnnotator.java
index 1b843f8..8a66f6e 100644
--- a/android/src/org/jetbrains/android/AndroidColorAnnotator.java
+++ b/android/src/org/jetbrains/android/AndroidColorAnnotator.java
@@ -16,6 +16,7 @@
package org.jetbrains.android;
+import com.android.SdkConstants;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.resources.ResourceItem;
@@ -46,6 +47,7 @@
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlTag;
+import com.intellij.psi.xml.XmlTagValue;
import com.intellij.ui.ColorChooser;
import com.intellij.util.ui.ColorIcon;
import com.intellij.util.ui.EmptyIcon;
@@ -53,7 +55,6 @@
import com.intellij.util.xml.DomManager;
import org.jetbrains.android.dom.resources.ResourceElement;
import org.jetbrains.android.facet.AndroidFacet;
-import org.jetbrains.android.facet.AndroidRootUtil;
import org.jetbrains.android.util.AndroidBundle;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -73,17 +74,23 @@
private static final int ICON_SIZE = 8;
private static final String COLOR_PREFIX = "@color/";
private static final String ANDROID_COLOR_PREFIX = "@android:color/";
+ private static final String DRAWABLE_PREFIX = "@drawable/";
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
if (element instanceof XmlTag) {
XmlTag tag = (XmlTag)element;
- if ((ResourceType.COLOR.getName().equals(tag.getName()) || ResourceType.DRAWABLE.getName().equals(tag.getName()))) {
+ String tagName = tag.getName();
+ if ((ResourceType.COLOR.getName().equals(tagName) || ResourceType.DRAWABLE.getName().equals(tagName))) {
DomElement domElement = DomManager.getDomManager(element.getProject()).getDomElement(tag);
if (domElement instanceof ResourceElement) {
String value = tag.getValue().getText().trim();
annotateXml(element, holder, value);
}
+ } else if (SdkConstants.TAG_ITEM.equals(tagName)) {
+ XmlTagValue value = tag.getValue();
+ String text = value.getText();
+ annotateXml(element, holder, text);
}
} else if (element instanceof XmlAttributeValue) {
XmlAttributeValue v = (XmlAttributeValue)element;
@@ -121,6 +128,10 @@
annotateResourceReference(ResourceType.COLOR, holder, element, value.substring(COLOR_PREFIX.length()), false);
} else if (value.startsWith(ANDROID_COLOR_PREFIX)) {
annotateResourceReference(ResourceType.COLOR, holder, element, value.substring(ANDROID_COLOR_PREFIX.length()), true);
+ } else if (value.startsWith(DRAWABLE_PREFIX)) {
+ annotateResourceReference(ResourceType.DRAWABLE, holder, element, value.substring(DRAWABLE_PREFIX.length()), false);
+ } else if (value.startsWith(ANDROID_DRAWABLE_PREFIX)) {
+ annotateResourceReference(ResourceType.DRAWABLE, holder, element, value.substring(ANDROID_DRAWABLE_PREFIX.length()), false);
}
}
diff --git a/android/src/org/jetbrains/android/AndroidDocumentationProvider.java b/android/src/org/jetbrains/android/AndroidDocumentationProvider.java
index fa3bd1b..dba3299 100644
--- a/android/src/org/jetbrains/android/AndroidDocumentationProvider.java
+++ b/android/src/org/jetbrains/android/AndroidDocumentationProvider.java
@@ -78,18 +78,13 @@
return null;
}
- ProjectResources projectResources = ProjectResources.get(module, true, true);
- if (projectResources == null) {
- return null;
- }
-
ResourceType type = AndroidPsiUtils.getResourceType(originalElement);
if (type == null) {
return null;
}
String name = originalElement.getText();
- return AndroidJavaDocRenderer.render(projectResources, type, name);
+ return AndroidJavaDocRenderer.render(module, type, name);
}
@Override
diff --git a/android/src/org/jetbrains/android/AndroidPropertyFilesUpdater.java b/android/src/org/jetbrains/android/AndroidPropertyFilesUpdater.java
index 1c7e59f..4445585 100644
--- a/android/src/org/jetbrains/android/AndroidPropertyFilesUpdater.java
+++ b/android/src/org/jetbrains/android/AndroidPropertyFilesUpdater.java
@@ -230,8 +230,7 @@
final List<Object> newState = Arrays.asList(
androidTargetHashString,
- facet.getProperties().
- LIBRARY_PROJECT,
+ facet.isLibraryProject(),
Arrays.asList(dependencyPaths),
facet.getProperties().ENABLE_MANIFEST_MERGING,
facet.getProperties().ENABLE_PRE_DEXING);
@@ -354,7 +353,7 @@
final IProperty property = propertiesFile.findPropertyByKey(AndroidUtils.ANDROID_LIBRARY_PROPERTY);
if (property != null) {
- final String value = Boolean.toString(facet.getProperties().LIBRARY_PROJECT);
+ final String value = Boolean.toString(facet.isLibraryProject());
if (!value.equals(property.getValue())) {
changes.add(new Runnable() {
@@ -365,7 +364,7 @@
});
}
}
- else if (facet.getProperties().LIBRARY_PROJECT) {
+ else if (facet.isLibraryProject()) {
changes.add(new Runnable() {
@Override
public void run() {
diff --git a/android/src/org/jetbrains/android/AndroidXmlSchemaProvider.java b/android/src/org/jetbrains/android/AndroidXmlSchemaProvider.java
index dd1e435..d013026 100644
--- a/android/src/org/jetbrains/android/AndroidXmlSchemaProvider.java
+++ b/android/src/org/jetbrains/android/AndroidXmlSchemaProvider.java
@@ -159,7 +159,7 @@
@Nullable
public static String getLocalXmlNamespace(@NotNull AndroidFacet facet) {
- if (facet.getProperties().LIBRARY_PROJECT) {
+ if (facet.isLibraryProject() || facet.isGradleProject()) {
return SdkConstants.AUTO_URI;
}
final Manifest manifest = facet.getManifest();
diff --git a/android/src/org/jetbrains/android/actions/AndroidCreateLayoutFileAction.java b/android/src/org/jetbrains/android/actions/AndroidCreateLayoutFileAction.java
index 40f75e1..cfb93de 100644
--- a/android/src/org/jetbrains/android/actions/AndroidCreateLayoutFileAction.java
+++ b/android/src/org/jetbrains/android/actions/AndroidCreateLayoutFileAction.java
@@ -22,9 +22,11 @@
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.InputValidator;
+import com.intellij.openapi.ui.InputValidatorEx;
import com.intellij.openapi.ui.Messages;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
+import com.intellij.ui.DocumentAdapter;
import com.intellij.ui.TextFieldWithAutoCompletion;
import com.intellij.ui.components.JBLabel;
import org.jetbrains.android.dom.layout.AndroidLayoutUtil;
@@ -34,6 +36,7 @@
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
+import javax.swing.event.DocumentEvent;
import java.awt.*;
import java.util.List;
@@ -54,7 +57,7 @@
protected PsiElement[] invokeDialog(Project project, PsiDirectory directory) {
final AndroidFacet facet = AndroidFacet.getInstance(directory);
LOG.assertTrue(facet != null);
- MyInputValidator validator = new MyInputValidator(project, directory);
+ InputValidator validator = createValidator(project, directory);
final MyDialog dialog = new MyDialog(facet, validator);
dialog.show();
return PsiElement.EMPTY_ARRAY;
@@ -93,6 +96,16 @@
myRootElementFieldWrapper.add(myRootElementField, BorderLayout.CENTER);
myRootElementLabel.setLabelFor(myRootElementField);
init();
+
+ myFileNameField.getDocument().addDocumentListener(new DocumentAdapter() {
+ @Override
+ public void textChanged(DocumentEvent event) {
+ final String text = myFileNameField.getText().trim();
+ if (myValidator instanceof InputValidatorEx) {
+ setErrorText(((InputValidatorEx) myValidator).getErrorText(text));
+ }
+ }
+ });
}
@Override
diff --git a/android/src/org/jetbrains/android/actions/CreateResourceFileDialog.java b/android/src/org/jetbrains/android/actions/CreateResourceFileDialog.java
index 60187f0..5092771 100644
--- a/android/src/org/jetbrains/android/actions/CreateResourceFileDialog.java
+++ b/android/src/org/jetbrains/android/actions/CreateResourceFileDialog.java
@@ -19,12 +19,15 @@
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
+import com.android.tools.idea.rendering.ResourceNameValidator;
import com.intellij.CommonBundle;
import com.intellij.ide.actions.TemplateKindCombo;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.InputValidator;
+import com.intellij.openapi.ui.InputValidatorEx;
import com.intellij.openapi.ui.Messages;
+import com.intellij.ui.DocumentAdapter;
import com.intellij.ui.TextFieldWithAutoCompletion;
import com.intellij.ui.components.JBLabel;
import com.intellij.util.PlatformIcons;
@@ -41,6 +44,7 @@
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
+import javax.swing.event.DocumentEvent;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
@@ -205,6 +209,37 @@
init();
setTitle(AndroidBundle.message("new.resource.dialog.title"));
+
+ myFileNameField.getDocument().addDocumentListener(new DocumentAdapter() {
+ @Override
+ public void textChanged(DocumentEvent event) {
+ validateName();
+ }
+ });
+ myResourceTypeCombo.getComboBox().addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent actionEvent) {
+ validateName();
+ }
+ });
+ }
+
+ @Nullable
+ private String getNameError(@NotNull String fileName) {
+ String typeName = myResourceTypeCombo.getSelectedName();
+ if (typeName != null) {
+ ResourceFolderType type = ResourceFolderType.getFolderType(typeName);
+ if (type != null) {
+ ResourceNameValidator validator = ResourceNameValidator.create(true, type);
+ return validator.getErrorText(fileName);
+ }
+ }
+
+ return null;
+ }
+
+ private void validateName() {
+ setErrorText(getNameError(myFileNameField.getText()));
}
private void updateRootElementTextField() {
@@ -265,7 +300,7 @@
return;
}
- final String errorMessage = AndroidResourceUtil.getInvalidResourceFileNameMessage(fileName);
+ final String errorMessage = getNameError(fileName);
if (errorMessage != null) {
Messages.showErrorDialog(myPanel, errorMessage, CommonBundle.getErrorTitle());
return;
diff --git a/android/src/org/jetbrains/android/actions/CreateTypedResourceFileAction.java b/android/src/org/jetbrains/android/actions/CreateTypedResourceFileAction.java
index 5ec97c2..7a27b80 100644
--- a/android/src/org/jetbrains/android/actions/CreateTypedResourceFileAction.java
+++ b/android/src/org/jetbrains/android/actions/CreateTypedResourceFileAction.java
@@ -17,6 +17,7 @@
package org.jetbrains.android.actions;
import com.android.resources.ResourceFolderType;
+import com.android.tools.idea.rendering.ResourceNameValidator;
import com.intellij.CommonBundle;
import com.intellij.ide.actions.CreateElementActionBase;
import com.intellij.openapi.actionSystem.DataContext;
@@ -29,6 +30,7 @@
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.InputValidator;
import com.intellij.openapi.ui.InputValidatorEx;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Computable;
@@ -79,10 +81,14 @@
return myResourceType.getName();
}
+ protected InputValidator createValidator(Project project, PsiDirectory directory) {
+ return new MyValidator(project, directory);
+ }
+
@NotNull
@Override
protected PsiElement[] invokeDialog(Project project, PsiDirectory directory) {
- MyInputValidator validator = new MyValidator(project, directory);
+ InputValidator validator = createValidator(project, directory);
Messages.showInputDialog(project, AndroidBundle.message("new.file.dialog.text"),
AndroidBundle.message("new.typed.resource.dialog.title", myResourcePresentableName),
Messages.getQuestionIcon(), "", validator);
@@ -214,8 +220,10 @@
}
private class MyValidator extends MyInputValidator implements InputValidatorEx {
+ private final ResourceNameValidator myNameValidator;
public MyValidator(Project project, PsiDirectory directory) {
super(project, directory);
+ myNameValidator = ResourceNameValidator.create(true, myResourceType);
}
@Override
@@ -225,7 +233,7 @@
@Override
public String getErrorText(String inputString) {
- return AndroidResourceUtil.getInvalidResourceFileNameMessage(inputString);
+ return myNameValidator.getErrorText(inputString);
}
}
}
diff --git a/android/src/org/jetbrains/android/augment/ResourceTypeClass.java b/android/src/org/jetbrains/android/augment/ResourceTypeClass.java
index c924303..94298d9 100644
--- a/android/src/org/jetbrains/android/augment/ResourceTypeClass.java
+++ b/android/src/org/jetbrains/android/augment/ResourceTypeClass.java
@@ -23,7 +23,7 @@
@NotNull String resClassName,
@NotNull final PsiClass context) {
final Module circularDepLibWithSamePackage = AndroidCompileUtil.findCircularDependencyOnLibraryWithSamePackage(facet);
- final boolean generateNonFinalFields = facet.getProperties().LIBRARY_PROJECT || circularDepLibWithSamePackage != null;
+ final boolean generateNonFinalFields = facet.isLibraryProject() || circularDepLibWithSamePackage != null;
return buildResourceFields(facet.getLocalResourceManager(), generateNonFinalFields, resClassName, context);
}
diff --git a/android/src/org/jetbrains/android/compiler/AndroidAptCompiler.java b/android/src/org/jetbrains/android/compiler/AndroidAptCompiler.java
index 413e879..45c3e5e 100644
--- a/android/src/org/jetbrains/android/compiler/AndroidAptCompiler.java
+++ b/android/src/org/jetbrains/android/compiler/AndroidAptCompiler.java
@@ -314,20 +314,20 @@
if (!AndroidCommonUtils.contains2Identifiers(packageName)) {
final String message = "[" + module.getName() + "] Package name must contain at least 2 segments";
- myContext.addMessage(facet.getProperties().LIBRARY_PROJECT ? CompilerMessageCategory.WARNING : CompilerMessageCategory.ERROR,
+ myContext.addMessage(facet.isLibraryProject() ? CompilerMessageCategory.WARNING : CompilerMessageCategory.ERROR,
message, manifestFile.getUrl(), -1, -1);
continue;
}
final String[] libPackages = AndroidCompileUtil.getLibPackages(module, packageName);
final Module circularDepLibWithSamePackage = AndroidCompileUtil.findCircularDependencyOnLibraryWithSamePackage(facet);
- if (circularDepLibWithSamePackage != null && !facet.getProperties().LIBRARY_PROJECT) {
+ if (circularDepLibWithSamePackage != null && !facet.isLibraryProject()) {
myContext.addMessage(CompilerMessageCategory.WARNING,
AndroidBundle.message("android.compilation.warning.circular.app.dependency",
packageName, module.getName(),
circularDepLibWithSamePackage.getName()), null, -1, -1);
}
- final boolean generateNonFinalFields = facet.getProperties().LIBRARY_PROJECT || circularDepLibWithSamePackage != null;
+ final boolean generateNonFinalFields = facet.isLibraryProject() || circularDepLibWithSamePackage != null;
final VirtualFile outputDirForDex = AndroidDexCompiler.getOutputDirectoryForDex(module);
final String proguardCfgOutputFileOsPath =
diff --git a/android/src/org/jetbrains/android/compiler/AndroidBuildTargetScopeProvider.java b/android/src/org/jetbrains/android/compiler/AndroidBuildTargetScopeProvider.java
index fbf20d5..255584e 100644
--- a/android/src/org/jetbrains/android/compiler/AndroidBuildTargetScopeProvider.java
+++ b/android/src/org/jetbrains/android/compiler/AndroidBuildTargetScopeProvider.java
@@ -1,5 +1,6 @@
package org.jetbrains.android.compiler;
+import com.android.tools.idea.gradle.util.Projects;
import com.intellij.compiler.impl.BuildTargetScopeProvider;
import com.intellij.facet.ProjectFacetManager;
import com.intellij.openapi.compiler.CompileScope;
@@ -59,8 +60,7 @@
@Override
public List<TargetTypeBuildScope> getBuildTargetScopes(@NotNull CompileScope baseScope, @NotNull CompilerFilter filter,
@NotNull Project project, boolean forceBuild) {
-
- if (!ProjectFacetManager.getInstance(project).hasFacets(AndroidFacet.ID)) {
+ if (!ProjectFacetManager.getInstance(project).hasFacets(AndroidFacet.ID) || Projects.isGradleProject(project)) {
return Collections.emptyList();
}
final List<String> appTargetIds = new ArrayList<String>();
@@ -78,7 +78,7 @@
allTargetIds.add(module.getName());
if (fullBuild) {
- if (facet.getProperties().LIBRARY_PROJECT) {
+ if (facet.isLibraryProject()) {
libTargetIds.add(module.getName());
}
else {
diff --git a/android/src/org/jetbrains/android/compiler/AndroidCompileUtil.java b/android/src/org/jetbrains/android/compiler/AndroidCompileUtil.java
index 4e11ca0..7e5ecfe 100644
--- a/android/src/org/jetbrains/android/compiler/AndroidCompileUtil.java
+++ b/android/src/org/jetbrains/android/compiler/AndroidCompileUtil.java
@@ -616,7 +616,7 @@
final GlobalSearchScope moduleScope = facet.getModule().getModuleScope();
final Ref<Boolean> modelChangedFlag = Ref.create(false);
- if (facet.getProperties().LIBRARY_PROJECT) {
+ if (facet.isLibraryProject()) {
removeGenModule(model, modelChangedFlag);
}
initializeGenSourceRoot(model, AndroidRootUtil.getBuildconfigGenSourceRootPath(facet), true, true, modelChangedFlag);
@@ -763,7 +763,7 @@
}
final AndroidFacet facet = AndroidFacet.getInstance(module);
- if (facet == null || !facet.getProperties().LIBRARY_PROJECT) {
+ if (facet == null || !facet.isLibraryProject()) {
return true;
}
@@ -858,7 +858,7 @@
// support for lib<->lib and app<->lib circular dependencies
// see IDEA-79737 for details
public static boolean isLibraryWithBadCircularDependency(@NotNull AndroidFacet facet) {
- if (!facet.getProperties().LIBRARY_PROJECT) {
+ if (!facet.isLibraryProject()) {
return false;
}
@@ -880,7 +880,7 @@
if (depDependencies.contains(facet) &&
dependencies.contains(depFacet) &&
(depFacet.getModule().getName().compareTo(facet.getModule().getName()) < 0 ||
- !depFacet.getProperties().LIBRARY_PROJECT)) {
+ !depFacet.isLibraryProject())) {
return true;
}
}
diff --git a/android/src/org/jetbrains/android/compiler/AndroidDexCompiler.java b/android/src/org/jetbrains/android/compiler/AndroidDexCompiler.java
index 4ef7a57..0a1239c 100644
--- a/android/src/org/jetbrains/android/compiler/AndroidDexCompiler.java
+++ b/android/src/org/jetbrains/android/compiler/AndroidDexCompiler.java
@@ -128,7 +128,7 @@
List<ProcessingItem> items = new ArrayList<ProcessingItem>();
for (Module module : modules) {
AndroidFacet facet = FacetManager.getInstance(module).getFacetByType(AndroidFacet.ID);
- if (facet != null && !facet.getProperties().LIBRARY_PROJECT) {
+ if (facet != null && !facet.isLibraryProject()) {
final VirtualFile dexOutputDir = getOutputDirectoryForDex(module);
diff --git a/android/src/org/jetbrains/android/compiler/AndroidExternalApklibExtractingCompiler.java b/android/src/org/jetbrains/android/compiler/AndroidExternalApklibExtractingCompiler.java
index dfd680e..c6235be 100644
--- a/android/src/org/jetbrains/android/compiler/AndroidExternalApklibExtractingCompiler.java
+++ b/android/src/org/jetbrains/android/compiler/AndroidExternalApklibExtractingCompiler.java
@@ -49,7 +49,7 @@
for (Module module : ModuleManager.getInstance(context.getProject()).getModules()) {
final AndroidFacet facet = AndroidFacet.getInstance(module);
- if (facet == null || !facet.getProperties().LIBRARY_PROJECT) {
+ if (facet == null || !facet.isLibraryProject()) {
continue;
}
diff --git a/android/src/org/jetbrains/android/compiler/AndroidIncludingCompiler.java b/android/src/org/jetbrains/android/compiler/AndroidIncludingCompiler.java
index 0fa20d8..2d41738 100644
--- a/android/src/org/jetbrains/android/compiler/AndroidIncludingCompiler.java
+++ b/android/src/org/jetbrains/android/compiler/AndroidIncludingCompiler.java
@@ -86,7 +86,7 @@
for (Module module : context.getProjectCompileScope().getAffectedModules()) {
AndroidFacet facet = AndroidFacet.getInstance(module);
- if (facet != null && facet.getProperties().LIBRARY_PROJECT) {
+ if (facet != null && facet.isLibraryProject()) {
continue;
}
diff --git a/android/src/org/jetbrains/android/compiler/AndroidLibraryPackagingCompiler.java b/android/src/org/jetbrains/android/compiler/AndroidLibraryPackagingCompiler.java
index e803c3e..85a113a 100644
--- a/android/src/org/jetbrains/android/compiler/AndroidLibraryPackagingCompiler.java
+++ b/android/src/org/jetbrains/android/compiler/AndroidLibraryPackagingCompiler.java
@@ -39,7 +39,7 @@
for (Module module : ModuleManager.getInstance(context.getProject()).getModules()) {
final AndroidFacet facet = AndroidFacet.getInstance(module);
- if (facet == null || !facet.getProperties().LIBRARY_PROJECT) {
+ if (facet == null || !facet.isLibraryProject()) {
continue;
}
diff --git a/android/src/org/jetbrains/android/compiler/AndroidPackagingCompiler.java b/android/src/org/jetbrains/android/compiler/AndroidPackagingCompiler.java
index 384cbba..ec99ec5 100644
--- a/android/src/org/jetbrains/android/compiler/AndroidPackagingCompiler.java
+++ b/android/src/org/jetbrains/android/compiler/AndroidPackagingCompiler.java
@@ -112,7 +112,7 @@
final List<ProcessingItem> items = new ArrayList<ProcessingItem>();
for (Module module : ModuleManager.getInstance(context.getProject()).getModules()) {
AndroidFacet facet = AndroidFacet.getInstance(module);
- if (facet != null && !facet.getProperties().LIBRARY_PROJECT) {
+ if (facet != null && !facet.isLibraryProject()) {
VirtualFile manifestFile = AndroidRootUtil.getManifestFileForCompiler(facet);
VirtualFile[] sourceRoots = getSourceRootsForModuleAndDependencies(module, facet.getProperties().PACK_TEST_CODE);
if (manifestFile != null) {
diff --git a/android/src/org/jetbrains/android/compiler/AndroidPrecompileTask.java b/android/src/org/jetbrains/android/compiler/AndroidPrecompileTask.java
index 91c9541..44ac08f 100644
--- a/android/src/org/jetbrains/android/compiler/AndroidPrecompileTask.java
+++ b/android/src/org/jetbrains/android/compiler/AndroidPrecompileTask.java
@@ -117,7 +117,7 @@
LOG.debug("Platform-tools revision for module " + module.getName() + " is " + platformToolsRevision);
- if (facet.getProperties().LIBRARY_PROJECT) {
+ if (facet.isLibraryProject()) {
if (platformToolsRevision >= 0 && platformToolsRevision <= 7) {
LOG.debug("Excluded sources of module " + module.getName());
excludeAllSourceRoots(module, configuration, addedEntries);
@@ -284,7 +284,7 @@
manifestMergerProp.getSecond().getUrl(), -1, -1);
}
- if (!facet.getProperties().LIBRARY_PROJECT) {
+ if (!facet.isLibraryProject()) {
for (OrderEntry entry : ModuleRootManager.getInstance(module).getOrderEntries()) {
if (entry instanceof ModuleOrderEntry) {
@@ -296,7 +296,7 @@
if (depModule != null) {
final AndroidFacet depFacet = AndroidFacet.getInstance(depModule);
- if (depFacet != null && !depFacet.getProperties().LIBRARY_PROJECT) {
+ if (depFacet != null && !depFacet.isLibraryProject()) {
String message = "Suspicious module dependency " +
module.getName() +
" -> " +
diff --git a/android/src/org/jetbrains/android/compiler/AndroidProguardCompiler.java b/android/src/org/jetbrains/android/compiler/AndroidProguardCompiler.java
index 0bead34..6688ebe 100644
--- a/android/src/org/jetbrains/android/compiler/AndroidProguardCompiler.java
+++ b/android/src/org/jetbrains/android/compiler/AndroidProguardCompiler.java
@@ -58,7 +58,7 @@
for (final Module module : modules) {
final AndroidFacet facet = AndroidFacet.getInstance(module);
- if (facet == null || facet.getProperties().LIBRARY_PROJECT) {
+ if (facet == null || facet.isLibraryProject()) {
continue;
}
diff --git a/android/src/org/jetbrains/android/compiler/AndroidResourcesPackagingCompiler.java b/android/src/org/jetbrains/android/compiler/AndroidResourcesPackagingCompiler.java
index e042a0f..8c63bcb 100644
--- a/android/src/org/jetbrains/android/compiler/AndroidResourcesPackagingCompiler.java
+++ b/android/src/org/jetbrains/android/compiler/AndroidResourcesPackagingCompiler.java
@@ -60,7 +60,7 @@
final List<ProcessingItem> items = new ArrayList<ProcessingItem>();
for (Module module : ModuleManager.getInstance(context.getProject()).getModules()) {
AndroidFacet facet = AndroidFacet.getInstance(module);
- if (facet != null && !facet.getProperties().LIBRARY_PROJECT) {
+ if (facet != null && !facet.isLibraryProject()) {
VirtualFile manifestFile = AndroidRootUtil.getManifestFileForCompiler(facet);
final ArrayList<String> assetDirPathsList = new ArrayList<String>();
diff --git a/android/src/org/jetbrains/android/compiler/artifact/AndroidApplicationArtifactType.java b/android/src/org/jetbrains/android/compiler/artifact/AndroidApplicationArtifactType.java
index 5cf4e86..327ddb2 100644
--- a/android/src/org/jetbrains/android/compiler/artifact/AndroidApplicationArtifactType.java
+++ b/android/src/org/jetbrains/android/compiler/artifact/AndroidApplicationArtifactType.java
@@ -66,7 +66,7 @@
final FacetModel facetModel = context.getModulesProvider().getFacetModel(module);
final AndroidFacet facet = facetModel.getFacetByType(AndroidFacet.ID);
- if (facet != null && !facet.getProperties().LIBRARY_PROJECT) {
+ if (facet != null && !facet.isLibraryProject()) {
facets.add(facet);
}
}
diff --git a/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackageElementType.java b/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackageElementType.java
index 90cb796..10d1d19 100644
--- a/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackageElementType.java
+++ b/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackageElementType.java
@@ -47,7 +47,7 @@
final List<AndroidFacet> result = new ArrayList<AndroidFacet>();
for (Module module : modules) {
for (AndroidFacet facet : context.getFacetsProvider().getFacetsByType(module, AndroidFacet.ID)) {
- if (!facet.getProperties().LIBRARY_PROJECT) {
+ if (!facet.isLibraryProject()) {
result.add(facet);
}
}
diff --git a/android/src/org/jetbrains/android/dom/AndroidXmlDocumentationProvider.java b/android/src/org/jetbrains/android/dom/AndroidXmlDocumentationProvider.java
index 9bddf25..a6d87b3 100644
--- a/android/src/org/jetbrains/android/dom/AndroidXmlDocumentationProvider.java
+++ b/android/src/org/jetbrains/android/dom/AndroidXmlDocumentationProvider.java
@@ -4,7 +4,6 @@
import com.android.ide.common.resources.ResourceRepository;
import com.android.resources.ResourceType;
import com.android.tools.idea.javadoc.AndroidJavaDocRenderer;
-import com.android.tools.idea.rendering.ProjectResources;
import com.android.utils.Pair;
import com.intellij.lang.documentation.DocumentationProvider;
import com.intellij.openapi.module.Module;
@@ -37,7 +36,7 @@
import java.util.*;
-import static com.android.SdkConstants.ANDROID_PREFIX;
+import static com.android.SdkConstants.*;
import static com.intellij.psi.xml.XmlTokenType.XML_ATTRIBUTE_VALUE_START_DELIMITER;
import static com.intellij.psi.xml.XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN;
@@ -81,6 +80,25 @@
ResourceType type = pair.getFirst();
String name = pair.getSecond();
return generateDoc(originalElement, type, name);
+ } else {
+ // See if it's in a resource file definition: This allows you to invoke
+ // documentation on <string name="cursor_here">...</string>
+ // and see the various translations etc of the string
+ XmlAttribute attribute = PsiTreeUtil.getParentOfType(originalElement, XmlAttribute.class, false);
+ if (attribute != null && ATTR_NAME.equals(attribute.getName())) {
+ XmlTag tag = attribute.getParent();
+ String typeName = tag.getName();
+ if (TAG_ITEM.equals(typeName)) {
+ typeName = tag.getAttributeValue(ATTR_TYPE);
+ if (typeName == null) {
+ return null;
+ }
+ }
+ ResourceType type = ResourceType.getEnum(typeName);
+ if (type != null) {
+ return generateDoc(originalElement, type, value);
+ }
+ }
}
}
}
@@ -259,8 +277,7 @@
return null;
}
- ProjectResources projectResources = ProjectResources.get(module, true);
- return AndroidJavaDocRenderer.render(projectResources, type, name);
+ return AndroidJavaDocRenderer.render(module, type, name);
}
@Override
diff --git a/android/src/org/jetbrains/android/dom/AndroidXmlExtension.java b/android/src/org/jetbrains/android/dom/AndroidXmlExtension.java
index 9047309..b52184b 100644
--- a/android/src/org/jetbrains/android/dom/AndroidXmlExtension.java
+++ b/android/src/org/jetbrains/android/dom/AndroidXmlExtension.java
@@ -15,14 +15,18 @@
*/
package org.jetbrains.android.dom;
+import com.android.SdkConstants;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.impl.DirectoryIndex;
import com.intellij.openapi.util.Computable;
+import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiFile;
+import com.intellij.psi.impl.source.xml.SchemaPrefix;
import com.intellij.psi.impl.source.xml.TagNameReference;
import com.intellij.psi.xml.XmlFile;
+import com.intellij.psi.xml.XmlTag;
import com.intellij.xml.DefaultXmlExtension;
import org.jetbrains.android.dom.manifest.ManifestDomFileDescription;
import org.jetbrains.android.facet.AndroidFacet;
@@ -62,4 +66,26 @@
}
return false;
}
+
+ @Override
+ public SchemaPrefix getPrefixDeclaration(final XmlTag context, String namespacePrefix) {
+ SchemaPrefix prefix = super.getPrefixDeclaration(context, namespacePrefix);
+ if (prefix != null) {
+ return prefix;
+ }
+
+ if (namespacePrefix.isEmpty()) {
+ // In for example XHTML documents, the root element looks like this:
+ // <html xmlns="http://www.w3.org/1999/xhtml">
+ // This means that the IDE can find the namespace for "".
+ //
+ // However, in Android XML files it's implicit, so just return a dummy SchemaPrefix so
+ // // that we don't end up with a
+ // Namespace ''{0}'' is not bound
+ // error from {@link XmlUnboundNsPrefixInspection#checkUnboundNamespacePrefix}
+ return new SchemaPrefix(null, new TextRange(0, 0), SdkConstants.ANDROID_NS_NAME);
+ }
+
+ return null;
+ }
}
diff --git a/android/src/org/jetbrains/android/dom/attrs/AttributeDefinitionsImpl.java b/android/src/org/jetbrains/android/dom/attrs/AttributeDefinitionsImpl.java
index dd4acd0..1d02582 100644
--- a/android/src/org/jetbrains/android/dom/attrs/AttributeDefinitionsImpl.java
+++ b/android/src/org/jetbrains/android/dom/attrs/AttributeDefinitionsImpl.java
@@ -28,7 +28,12 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static com.android.SdkConstants.*;
/**
* @author yole
@@ -53,13 +58,13 @@
final XmlDocument document = file.getDocument();
if (document == null) return;
final XmlTag rootTag = document.getRootTag();
- if (rootTag == null || !"resources".equals(rootTag.getName())) return;
+ if (rootTag == null || !TAG_RESOURCES.equals(rootTag.getName())) return;
for (XmlTag tag : rootTag.getSubTags()) {
String tagName = tag.getName();
- if (tagName.equals("attr")) {
+ if (tagName.equals(TAG_ATTR)) {
parseAttrTag(tag, null);
}
- else if (tagName.equals("declare-styleable")) {
+ else if (tagName.equals(TAG_DECLARE_STYLEABLE)) {
parseDeclareStyleableTag(tag, parentMap);
}
}
@@ -81,25 +86,25 @@
}
@Nullable
- private AttributeDefinition parseAttrTag(XmlTag tag, String parentStyleable) {
- String name = tag.getAttributeValue("name");
+ private AttributeDefinition parseAttrTag(XmlTag tag, @Nullable String parentStyleable) {
+ String name = tag.getAttributeValue(ATTR_NAME);
if (name == null) {
LOG.info("Found attr tag with no name: " + tag.getText());
return null;
}
List<AttributeFormat> parsedFormats;
List<AttributeFormat> formats = new ArrayList<AttributeFormat>();
- String format = tag.getAttributeValue("format");
+ String format = tag.getAttributeValue(ATTR_FORMAT);
if (format != null) {
parsedFormats = parseAttrFormat(format);
if (parsedFormats != null) formats.addAll(parsedFormats);
}
- XmlTag[] values = tag.findSubTags("enum");
+ XmlTag[] values = tag.findSubTags(TAG_ENUM);
if (values.length > 0) {
formats.add(AttributeFormat.Enum);
}
else {
- values = tag.findSubTags("flag");
+ values = tag.findSubTags(TAG_FLAG);
if (values.length > 0) {
formats.add(AttributeFormat.Flag);
}
@@ -115,7 +120,7 @@
return def;
}
- private static void parseDocComment(XmlTag tag, AttributeDefinition def, String styleable) {
+ private static void parseDocComment(XmlTag tag, AttributeDefinition def, @Nullable String styleable) {
PsiElement comment = XmlDocumentationProvider.findPreviousComment(tag);
if (comment != null) {
String docValue = XmlUtil.getCommentText((XmlComment)comment);
@@ -143,14 +148,14 @@
private void parseAndAddValues(AttributeDefinition def, XmlTag[] values) {
for (XmlTag value : values) {
- final String valueName = value.getAttributeValue("name");
+ final String valueName = value.getAttributeValue(ATTR_NAME);
if (valueName == null) {
LOG.info("Unknown value for tag: " + value.getText());
}
else {
def.addValue(valueName);
- final String strIntValue = value.getAttributeValue("value");
+ final String strIntValue = value.getAttributeValue(ATTR_VALUE);
if (strIntValue != null) {
try {
int intValue = strIntValue.startsWith("0x")
@@ -171,13 +176,13 @@
}
private void parseDeclareStyleableTag(XmlTag tag, Map<StyleableDefinitionImpl, String[]> parentMap) {
- String name = tag.getAttributeValue("name");
+ String name = tag.getAttributeValue(ATTR_NAME);
if (name == null) {
LOG.info("Found declare-styleable tag with no name: " + tag.getText());
return;
}
StyleableDefinitionImpl def = new StyleableDefinitionImpl(name);
- String parentNameAttributeValue = tag.getAttributeValue("parent");
+ String parentNameAttributeValue = tag.getAttributeValue(ATTR_PARENT);
if (parentNameAttributeValue != null) {
String[] parentNames = parentNameAttributeValue.split("\\s+");
parentMap.put(def, parentNames);
@@ -188,13 +193,13 @@
myStateStyleables.add(def);
}
- for (XmlTag subTag : tag.findSubTags("attr")) {
+ for (XmlTag subTag : tag.findSubTags(TAG_ATTR)) {
parseStyleableAttr(def, subTag);
}
}
private void parseStyleableAttr(StyleableDefinitionImpl def, XmlTag tag) {
- String name = tag.getAttributeValue("name");
+ String name = tag.getAttributeValue(ATTR_NAME);
if (name == null) {
LOG.info("Found attr tag with no name: " + tag.getText());
return;
diff --git a/android/src/org/jetbrains/android/dom/converters/AndroidResourceReferenceBase.java b/android/src/org/jetbrains/android/dom/converters/AndroidResourceReferenceBase.java
index edb342d..d33ffdd 100644
--- a/android/src/org/jetbrains/android/dom/converters/AndroidResourceReferenceBase.java
+++ b/android/src/org/jetbrains/android/dom/converters/AndroidResourceReferenceBase.java
@@ -1,11 +1,15 @@
package org.jetbrains.android.dom.converters;
+import com.android.ide.common.res2.ResourceItem;
+import com.android.resources.ResourceType;
+import com.android.tools.idea.rendering.ProjectResources;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.resolve.ResolveCache;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlElement;
+import com.intellij.psi.xml.XmlTag;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.xml.DomUtil;
import com.intellij.util.xml.GenericDomValue;
@@ -90,6 +94,24 @@
collectTargets(myFacet, myResourceValue, elements);
final List<ResolveResult> result = new ArrayList<ResolveResult>();
+ if (elements.isEmpty() && myResourceValue.getResourceName() != null) {
+ // Temporary workaround: AAR libraries may not have been picked up properly.
+ // Use project resources to find these missing references, if applicable.
+ ProjectResources resources = ProjectResources.get(myFacet.getModule(), true);
+ ResourceType resourceType = ResourceType.getEnum(myResourceValue.getResourceType());
+ if (resourceType != null) { // If not, it could be some broken source, such as @android/test
+ List<ResourceItem> items = resources.getResourceItem(resourceType, myResourceValue.getResourceName());
+ if (items != null) {
+ for (ResourceItem item : items) {
+ XmlTag tag = ProjectResources.getItemTag(myFacet, item);
+ if (tag != null) {
+ elements.add(tag);
+ }
+ }
+ }
+ }
+ }
+
for (PsiElement target : elements) {
result.add(new PsiElementResolveResult(target));
}
diff --git a/android/src/org/jetbrains/android/dom/converters/DimensionConverter.java b/android/src/org/jetbrains/android/dom/converters/DimensionConverter.java
index 3245cff..6bd18da 100644
--- a/android/src/org/jetbrains/android/dom/converters/DimensionConverter.java
+++ b/android/src/org/jetbrains/android/dom/converters/DimensionConverter.java
@@ -112,7 +112,7 @@
for (int i = 0; i < s.length(); i++) {
final char c = s.charAt(i);
- if (!Character.isDigit(c)) {
+ if (!Character.isDigit(c) && (i > 0 || c != '-')) {
break;
}
intPrefixBuilder.append(c);
diff --git a/android/src/org/jetbrains/android/dom/converters/ParentStyleConverter.java b/android/src/org/jetbrains/android/dom/converters/ParentStyleConverter.java
index d429c73..5c71d0e 100644
--- a/android/src/org/jetbrains/android/dom/converters/ParentStyleConverter.java
+++ b/android/src/org/jetbrains/android/dom/converters/ParentStyleConverter.java
@@ -19,6 +19,7 @@
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
+import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.util.ArrayUtil;
import com.intellij.util.xml.ConvertContext;
import com.intellij.util.xml.GenericDomValue;
@@ -42,6 +43,14 @@
@NotNull
@Override
public PsiReference[] createReferences(GenericDomValue<ResourceValue> value, PsiElement element, ConvertContext context) {
+ if (element instanceof XmlAttributeValue) {
+ // parent="" is allowed; it's used to deliberately allow style A.B.C to not be a child of A.B
+ XmlAttributeValue attributeValue = (XmlAttributeValue)element;
+ if (attributeValue.isValid() && attributeValue.getValue().isEmpty()) {
+ return PsiReference.EMPTY_ARRAY;
+ }
+ }
+
final PsiReference[] refsFromSuper = super.createReferences(value, element, context);
final ResourceValue resValue = value.getValue();
diff --git a/android/src/org/jetbrains/android/facet/AndroidFacet.java b/android/src/org/jetbrains/android/facet/AndroidFacet.java
index 5b175bc..59e8445 100644
--- a/android/src/org/jetbrains/android/facet/AndroidFacet.java
+++ b/android/src/org/jetbrains/android/facet/AndroidFacet.java
@@ -17,8 +17,7 @@
import com.android.SdkConstants;
import com.android.annotations.NonNull;
-import com.android.build.gradle.model.Variant;
-import com.android.builder.model.SourceProvider;
+import com.android.builder.model.*;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.IDevice;
import com.android.prefs.AndroidLocation;
@@ -60,10 +59,7 @@
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
-import com.intellij.openapi.vfs.JarFileSystem;
-import com.intellij.openapi.vfs.LocalFileSystem;
-import com.intellij.openapi.vfs.VfsUtilCore;
-import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.*;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.ProjectScope;
@@ -123,6 +119,7 @@
private ProjectResources myProjectResources;
private ProjectResources myProjectResourcesWithLibraries;
private IdeaAndroidProject myIdeaAndroidProject;
+ private final ResourceFolderManager myFolderManager = new ResourceFolderManager(this);
private final List<GradleProjectAvailableListener> myGradleProjectAvailableListeners = Lists.newArrayList();
private SourceProvider myMainSourceSet;
@@ -141,6 +138,14 @@
return !getConfiguration().getState().ALLOW_USER_CONFIGURATION;
}
+ public boolean isLibraryProject() {
+ return getConfiguration().getState().LIBRARY_PROJECT;
+ }
+
+ public void setLibraryProject(boolean library) {
+ getConfiguration().getState().LIBRARY_PROJECT = library;
+ }
+
/**
* Returns the main source set of the project. For non-Gradle projects it returns a {@link SourceProvider} wrapper
* which provides information about the old project.
@@ -204,6 +209,10 @@
}
}
+ public ResourceFolderManager getResourceFolderManager() {
+ return myFolderManager;
+ }
+
/**
* Returns all resource directories, in the overlay order
*
@@ -211,25 +220,7 @@
*/
@NotNull
public List<VirtualFile> getAllResourceDirectories() {
- if (isGradleProject()) {
- List<VirtualFile> resDirectories = new ArrayList<VirtualFile>();
- resDirectories.addAll(getMainIdeaSourceSet().getResDirectories());
- List<IdeaSourceProvider> flavorSourceSets = getIdeaFlavorSourceSets();
- if (flavorSourceSets != null) {
- for (IdeaSourceProvider provider : flavorSourceSets) {
- resDirectories.addAll(provider.getResDirectories());
- }
- }
-
- IdeaSourceProvider buildTypeSourceSet = getIdeaBuildTypeSourceSet();
- if (buildTypeSourceSet != null) {
- resDirectories.addAll(buildTypeSourceSet.getResDirectories());
- }
-
- return resDirectories;
- } else {
- return new ArrayList<VirtualFile>(getMainIdeaSourceSet().getResDirectories());
- }
+ return myFolderManager.getFolders();
}
/**
@@ -339,9 +330,9 @@
@Deprecated
@Nullable
public VirtualFile getPrimaryResourceDir() {
- Set<VirtualFile> resDirectories = getMainIdeaSourceSet().getResDirectories();
- if (!resDirectories.isEmpty()) {
- return resDirectories.iterator().next();
+ List<VirtualFile> dirs = getAllResourceDirectories();
+ if (!dirs.isEmpty()) {
+ return dirs.get(0);
}
return null;
@@ -1092,7 +1083,14 @@
if (myIdeaAndroidProject != null) {
Variant variant = myIdeaAndroidProject.getSelectedVariant();
JpsAndroidModuleProperties state = getConfiguration().getState();
- state.ASSEMBLE_TASK_NAME = variant.getAssembleTaskName();
+
+ ArtifactInfo mainArtifactInfo = variant.getMainArtifactInfo();
+ state.ASSEMBLE_TASK_NAME = mainArtifactInfo.getAssembleTaskName();
+ state.SOURCE_GEN_TASK_NAME = mainArtifactInfo.getSourceGenTaskName();
+
+ ArtifactInfo testArtifactInfo = variant.getTestArtifactInfo();
+ state.ASSEMBLE_TEST_TASK_NAME = testArtifactInfo != null ? testArtifactInfo.getAssembleTaskName() : "";
+
state.SELECTED_BUILD_VARIANT = variant.getName();
}
}
diff --git a/android/src/org/jetbrains/android/facet/AndroidFrameworkDetector.java b/android/src/org/jetbrains/android/facet/AndroidFrameworkDetector.java
index ab979f2..34e4f84 100644
--- a/android/src/org/jetbrains/android/facet/AndroidFrameworkDetector.java
+++ b/android/src/org/jetbrains/android/facet/AndroidFrameworkDetector.java
@@ -96,7 +96,7 @@
AndroidRootUtil.getProjectPropertyValue(module, AndroidUtils.ANDROID_LIBRARY_PROPERTY);
if (androidLibraryProp != null && Boolean.parseBoolean(androidLibraryProp.getFirst())) {
- facet.getProperties().LIBRARY_PROJECT = true;
+ facet.setLibraryProject(true);
return;
}
final Pair<String,VirtualFile> dexForceJumboProp =
diff --git a/android/src/org/jetbrains/android/facet/AndroidRootUtil.java b/android/src/org/jetbrains/android/facet/AndroidRootUtil.java
index e2f0998..e8d193b 100644
--- a/android/src/org/jetbrains/android/facet/AndroidRootUtil.java
+++ b/android/src/org/jetbrains/android/facet/AndroidRootUtil.java
@@ -17,7 +17,7 @@
package org.jetbrains.android.facet;
import com.android.SdkConstants;
-import com.android.build.gradle.model.Variant;
+import com.android.builder.model.Variant;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.intellij.ide.highlighter.ArchiveFileType;
import com.intellij.lang.properties.psi.PropertiesFile;
@@ -274,7 +274,7 @@
continue;
}
final AndroidFacet facet = AndroidFacet.getInstance(depModule);
- final boolean libraryProject = facet != null && facet.getProperties().LIBRARY_PROJECT;
+ final boolean libraryProject = facet != null && facet.isLibraryProject();
CompilerModuleExtension extension = CompilerModuleExtension.getInstance(depModule);
if (extension != null) {
@@ -520,7 +520,7 @@
if (ideaAndroidProject != null) {
// For Android-Gradle projects, IdeaAndroidProject is not null.
Variant selectedVariant = ideaAndroidProject.getSelectedVariant();
- File outputFile = selectedVariant.getOutputFile();
+ File outputFile = selectedVariant.getMainArtifactInfo().getOutputFile();
return outputFile.getAbsolutePath();
}
String path = facet.getProperties().APK_PATH;
diff --git a/android/src/org/jetbrains/android/facet/ResourceFolderManager.java b/android/src/org/jetbrains/android/facet/ResourceFolderManager.java
new file mode 100644
index 0000000..37c535d
--- /dev/null
+++ b/android/src/org/jetbrains/android/facet/ResourceFolderManager.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jetbrains.android.facet;
+
+import com.android.tools.idea.gradle.IdeaAndroidProject;
+import com.android.tools.idea.gradle.variant.view.BuildVariantView;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Lists;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.roots.LibraryOrSdkOrderEntry;
+import com.intellij.openapi.roots.ModuleRootManager;
+import com.intellij.openapi.roots.OrderEntry;
+import com.intellij.openapi.roots.OrderRootType;
+import com.intellij.openapi.util.ModificationTracker;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.util.containers.hash.HashSet;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.jps.android.model.impl.JpsAndroidModuleProperties;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import static com.android.SdkConstants.*;
+import static com.android.tools.idea.gradle.variant.view.BuildVariantView.BuildVariantSelectionChangeListener;
+
+/**
+ * The resource folder manager is responsible for returning the current set
+ * of resource folders used in the project. It provides hooks for getting notified
+ * when the set of folders changes (e.g. due to variant selection changes, or
+ * the folder set changing due to the user editing the gradle files or after a
+ * delayed project initialization), and it also provides some state caching between
+ * IDE sessions such that before the gradle initialization is done, it returns
+ * the folder set as it was before the IDE exited.
+ */
+public class ResourceFolderManager implements ModificationTracker {
+ private final AndroidFacet myFacet;
+ private List<VirtualFile> myResDirCache;
+ private long myGeneration;
+ private final List<ResourceFolderListener> myListeners = Lists.newArrayList();
+ private boolean myVariantListenerAdded;
+ private boolean myGradleInitListenerAdded;
+
+ /**
+ * Should only be constructed by {@link AndroidFacet}; others should obtain instance
+ * via {@link AndroidFacet#getResourceFolderManager}
+ */
+ ResourceFolderManager(AndroidFacet facet) {
+ myFacet = facet;
+ }
+
+ /** Notifies the resource folder manager that the resource folder set may have changed */
+ public void invalidate() {
+ List<VirtualFile> old = myResDirCache;
+ myResDirCache = null;
+ getFolders();
+ if (!old.equals(myResDirCache)) {
+ notifyChanged(old, myResDirCache);
+ }
+ }
+
+ /**
+ * Returns all resource directories, in the overlay order
+ * <p>
+ * TODO: This should be changed to be a {@code List<List<VirtualFile>>} in order to be
+ * able to distinguish overlays (e.g. flavor directories) versus resource folders at
+ * the same level where duplicates are NOT allowed: [[flavor1], [flavor2], [main1,main2]]
+ *
+ * @return a list of all resource directories
+ */
+ @NotNull
+ public List<VirtualFile> getFolders() {
+ if (myResDirCache == null) {
+ myResDirCache = computeFolders();
+ }
+
+ return myResDirCache;
+ }
+
+ private List<VirtualFile> computeFolders() {
+ if (myFacet.isGradleProject()) {
+ JpsAndroidModuleProperties state = myFacet.getConfiguration().getState();
+ IdeaAndroidProject ideaAndroidProject = myFacet.getIdeaAndroidProject();
+ List<VirtualFile> resDirectories = new ArrayList<VirtualFile>();
+ if (ideaAndroidProject == null) {
+ // Read string property
+ if (state != null) {
+ String path = state.RES_FOLDERS_RELATIVE_PATH;
+ if (path != null) {
+ VirtualFileManager manager = VirtualFileManager.getInstance();
+ // Deliberately using ';' instead of File.pathSeparator; see comment later in code below which
+ // writes the property
+ for (String url : Splitter.on(';').omitEmptyStrings().trimResults().split(path)) {
+ VirtualFile dir = manager.findFileByUrl(url);
+ if (dir != null) {
+ resDirectories.add(dir);
+ }
+ }
+ } else {
+ // First time; have not yet computed the res folders
+ // just try the default: src/main/res/ (from Gradle templates), res/ (from exported Eclipse projects)
+ String mainRes = '/' + FD_SOURCES + '/' + FD_MAIN + '/' + FD_RES;
+ VirtualFile dir = AndroidRootUtil.getFileByRelativeModulePath(myFacet.getModule(), mainRes, true);
+ if (dir != null) {
+ resDirectories.add(dir);
+ } else {
+ String res = '/' + FD_RES;
+ dir = AndroidRootUtil.getFileByRelativeModulePath(myFacet.getModule(), res, true);
+ if (dir != null) {
+ resDirectories.add(dir);
+ }
+ }
+ }
+ }
+
+ // Add notification listener for when the project is initialized so we can update the
+ // resource set, if necessary
+ if (!myGradleInitListenerAdded) {
+ myGradleInitListenerAdded = true; // Avoid adding multiple listeners if we invalidate and call this repeatedly around startup
+ myFacet.addListener(new AndroidFacet.GradleProjectAvailableListener() {
+ @Override
+ public void gradleProjectAvailable(@NotNull IdeaAndroidProject project) {
+ myFacet.removeListener(this);
+ invalidate();
+ }
+ });
+ }
+ } else {
+ resDirectories.addAll(myFacet.getMainIdeaSourceSet().getResDirectories());
+ List<IdeaSourceProvider> flavorSourceSets = myFacet.getIdeaFlavorSourceSets();
+ if (flavorSourceSets != null) {
+ for (IdeaSourceProvider provider : flavorSourceSets) {
+ resDirectories.addAll(provider.getResDirectories());
+ }
+ }
+
+ IdeaSourceProvider buildTypeSourceSet = myFacet.getIdeaBuildTypeSourceSet();
+ if (buildTypeSourceSet != null) {
+ resDirectories.addAll(buildTypeSourceSet.getResDirectories());
+ }
+
+ // Write string property such that subsequent restarts can look up the most recent list
+ // before the gradle model has been initialized asynchronously
+ if (state != null) {
+ StringBuilder path = new StringBuilder(400);
+ for (VirtualFile dir : resDirectories) {
+ if (path.length() != 0) {
+ // Deliberately using ';' instead of File.pathSeparator since on Unix File.pathSeparator is ":"
+ // which is also used in URLs, meaning we could end up with something like "file://foo:file://bar"
+ path.append(';');
+ }
+ path.append(dir.getUrl());
+ }
+ state.RES_FOLDERS_RELATIVE_PATH = path.toString();
+ }
+
+ // Also refresh the project resources whenever the variant changes
+ if (!myVariantListenerAdded) {
+ myVariantListenerAdded = true;
+ BuildVariantView.getInstance(myFacet.getModule().getProject()).addListener(new BuildVariantSelectionChangeListener() {
+ @Override
+ public void buildVariantSelected(@NotNull AndroidFacet facet) {
+ invalidate();
+ }
+ });
+ }
+ }
+
+ return resDirectories;
+ } else {
+ return new ArrayList<VirtualFile>(myFacet.getMainIdeaSourceSet().getResDirectories());
+ }
+ }
+
+ private void notifyChanged(@NotNull List<VirtualFile> before, @NotNull List<VirtualFile> after) {
+ myGeneration++;
+ Set<VirtualFile> added = new HashSet<VirtualFile>(after.size());
+ added.addAll(after);
+ added.removeAll(before);
+
+ Set<VirtualFile> removed = new HashSet<VirtualFile>(before.size());
+ removed.addAll(before);
+ removed.removeAll(after);
+
+ for (ResourceFolderListener listener : new ArrayList<ResourceFolderListener>(myListeners)) {
+ listener.resourceFoldersChanged(myFacet, after, added, removed);
+ }
+ }
+
+ @Override
+ public long getModificationCount() {
+ return myGeneration;
+ }
+
+ public synchronized void addListener(@NotNull ResourceFolderListener listener) {
+ myListeners.add(listener);
+ }
+
+ public synchronized void removeListener(@NotNull ResourceFolderListener listener) {
+ myListeners.remove(listener);
+ }
+
+ /** Adds in any AAR library resource directories found in the library definitions for the given facet */
+ public static void addAarsFromModuleLibraries(@NotNull AndroidFacet facet, @NotNull Set<File> dirs) {
+ Module module = facet.getModule();
+ OrderEntry[] orderEntries = ModuleRootManager.getInstance(module).getOrderEntries();
+ for (OrderEntry orderEntry : orderEntries) {
+ if (orderEntry instanceof LibraryOrSdkOrderEntry) {
+ if (orderEntry.isValid() && orderEntry.getPresentableName().endsWith(DOT_AAR)) {
+ final LibraryOrSdkOrderEntry entry = (LibraryOrSdkOrderEntry)orderEntry;
+ final VirtualFile[] libClasses = entry.getRootFiles(OrderRootType.CLASSES);
+ File res = null;
+ for (VirtualFile root : libClasses) {
+ if (root.getName().equals(FD_RES)) {
+ res = VfsUtilCore.virtualToIoFile(root);
+ break;
+ }
+ }
+
+ if (res == null) {
+ for (VirtualFile root : libClasses) {
+ // Switch to file IO: The root may be inside a jar file system, where
+ // getParent() returns null (and to get the real parent is ugly;
+ // e.g. ((PersistentFSImpl.JarRoot)root).getParentLocalFile()).
+ // Besides, we need the java.io.File at the end of this anyway.
+ File file = new File(VfsUtilCore.virtualToIoFile(root).getParentFile(), FD_RES);
+ if (file.exists()) {
+ res = file;
+ break;
+ }
+ }
+ }
+
+ if (res != null) {
+ dirs.add(res);
+ }
+ }
+ }
+ }
+ }
+
+ /** Listeners for resource folder changes */
+ public interface ResourceFolderListener {
+ /** The resource folders in this project has changed */
+ void resourceFoldersChanged(@NotNull AndroidFacet facet,
+ @NotNull List<VirtualFile> folders,
+ @NotNull Collection<VirtualFile> added,
+ @NotNull Collection<VirtualFile> removed);
+ }
+}
diff --git a/android/src/org/jetbrains/android/formatter/AndroidXmlPredefinedCodeStyle.java b/android/src/org/jetbrains/android/formatter/AndroidXmlPredefinedCodeStyle.java
index ad43a34..d7d2602 100644
--- a/android/src/org/jetbrains/android/formatter/AndroidXmlPredefinedCodeStyle.java
+++ b/android/src/org/jetbrains/android/formatter/AndroidXmlPredefinedCodeStyle.java
@@ -48,6 +48,8 @@
rules.add(attrArrangementRule("xmlns:android", "", KEEP));
rules.add(attrArrangementRule("xmlns:.*", "", BY_NAME));
rules.add(attrArrangementRule(".*:id", SdkConstants.NS_RESOURCES, KEEP));
+ rules.add(attrArrangementRule(".*:name", SdkConstants.NS_RESOURCES, KEEP));
+ rules.add(attrArrangementRule("name", "^$", KEEP));
rules.add(attrArrangementRule("style", "^$", KEEP));
rules.add(attrArrangementRule(".*", "^$", BY_NAME));
rules.add(attrArrangementRule(".*:layout_width", SdkConstants.NS_RESOURCES, KEEP));
@@ -57,6 +59,7 @@
rules.add(attrArrangementRule(".*:height", SdkConstants.NS_RESOURCES, BY_NAME));
rules.add(attrArrangementRule(".*", SdkConstants.NS_RESOURCES, BY_NAME));
rules.add(attrArrangementRule(".*", ".*", BY_NAME));
+ // TODO: Should sort name:"color",namespace:"" to the end (primarily for color state lists)
settings.getCommonSettings(XMLLanguage.INSTANCE).setArrangementSettings(new StdRulePriorityAwareSettings(rules));
}
}
diff --git a/android/src/org/jetbrains/android/inspections/AndroidNonConstantResIdsInSwitchInspection.java b/android/src/org/jetbrains/android/inspections/AndroidNonConstantResIdsInSwitchInspection.java
index 5823673..056444b 100644
--- a/android/src/org/jetbrains/android/inspections/AndroidNonConstantResIdsInSwitchInspection.java
+++ b/android/src/org/jetbrains/android/inspections/AndroidNonConstantResIdsInSwitchInspection.java
@@ -48,7 +48,7 @@
@Override
public void visitSwitchLabelStatement(PsiSwitchLabelStatement statement) {
final AndroidFacet facet = AndroidFacet.getInstance(statement);
- if (facet == null || !facet.getProperties().LIBRARY_PROJECT) {
+ if (facet == null || !facet.isLibraryProject()) {
return;
}
diff --git a/android/src/org/jetbrains/android/inspections/lint/AndroidLintExternalAnnotator.java b/android/src/org/jetbrains/android/inspections/lint/AndroidLintExternalAnnotator.java
index 3e1b3fe..d09c82a 100644
--- a/android/src/org/jetbrains/android/inspections/lint/AndroidLintExternalAnnotator.java
+++ b/android/src/org/jetbrains/android/inspections/lint/AndroidLintExternalAnnotator.java
@@ -50,6 +50,7 @@
* @author Eugene.Kudelevsky
*/
public class AndroidLintExternalAnnotator extends ExternalAnnotator<State, State> {
+ static final boolean INCLUDE_IDEA_SUPPRESS_ACTIONS = false;
@Override
public State collectionInformation(@NotNull PsiFile file) {
@@ -205,10 +206,15 @@
annotation.registerFix(new MyDisableInspectionFix(key));
annotation.registerFix(new MyEditInspectionToolsSettingsAction(key, inspection));
- final SuppressQuickFix[] suppressActions = inspection.getBatchSuppressActions(startElement);
- for (SuppressQuickFix action : suppressActions) {
- ProblemHighlightType type = annotation.getHighlightType();
- annotation.registerFix(action, null, key, InspectionManager.getInstance(project).createProblemDescriptor(startElement, endElement, message, type, true, LocalQuickFix.EMPTY_ARRAY));
+ if (INCLUDE_IDEA_SUPPRESS_ACTIONS) {
+ final SuppressQuickFix[] suppressActions = inspection.getBatchSuppressActions(startElement);
+ for (SuppressQuickFix action : suppressActions) {
+ if (action.isAvailable(project, startElement)) {
+ ProblemHighlightType type = annotation.getHighlightType();
+ annotation.registerFix(action, null, key, InspectionManager.getInstance(project).createProblemDescriptor(
+ startElement, endElement, message, type, true, LocalQuickFix.EMPTY_ARRAY));
+ }
+ }
}
}
}
diff --git a/android/src/org/jetbrains/android/inspections/lint/AndroidLintGlobalInspectionContext.java b/android/src/org/jetbrains/android/inspections/lint/AndroidLintGlobalInspectionContext.java
index e766d35..a056c5b 100644
--- a/android/src/org/jetbrains/android/inspections/lint/AndroidLintGlobalInspectionContext.java
+++ b/android/src/org/jetbrains/android/inspections/lint/AndroidLintGlobalInspectionContext.java
@@ -340,6 +340,19 @@
return result;
}
+ @NonNull
+ @Override
+ public List<File> getResourceFolders(@NonNull com.android.tools.lint.detector.api.Project project) {
+ final Module module = findModuleForLintProject(myProject, project);
+ if (module != null) {
+ AndroidFacet facet = AndroidFacet.getInstance(module);
+ if (facet != null) {
+ return IntellijLintUtils.getResourceDirectories(facet);
+ }
+ }
+ return super.getResourceFolders(project);
+ }
+
@Nullable
@Override
public File getSdkHome() {
diff --git a/android/src/org/jetbrains/android/inspections/lint/AndroidLintInspectionBase.java b/android/src/org/jetbrains/android/inspections/lint/AndroidLintInspectionBase.java
index f61c876..c3d1887 100644
--- a/android/src/org/jetbrains/android/inspections/lint/AndroidLintInspectionBase.java
+++ b/android/src/org/jetbrains/android/inspections/lint/AndroidLintInspectionBase.java
@@ -24,6 +24,8 @@
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.psi.xml.XmlTag;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.HashMap;
import org.jetbrains.android.facet.AndroidFacet;
@@ -187,11 +189,51 @@
@NotNull
@Override
public SuppressQuickFix[] getBatchSuppressActions(@Nullable PsiElement element) {
- final List<SuppressQuickFix> result = new ArrayList<SuppressQuickFix>();
- result.addAll(Arrays.asList(BatchSuppressManager.SERVICE.getInstance().createBatchSuppressActions(HighlightDisplayKey.find(getShortName()))));
- result.addAll(Arrays.asList(new XmlSuppressableInspectionTool.SuppressTagStatic(getShortName()),
- new XmlSuppressableInspectionTool.SuppressForFile(getShortName())));
- return result.toArray(new SuppressQuickFix[result.size()]);
+ SuppressLintQuickFix suppressLintQuickFix = new SuppressLintQuickFix(myIssue);
+ if (AndroidLintExternalAnnotator.INCLUDE_IDEA_SUPPRESS_ACTIONS) {
+ final List<SuppressQuickFix> result = new ArrayList<SuppressQuickFix>();
+ result.add(suppressLintQuickFix);
+ result.addAll(Arrays.asList(BatchSuppressManager.SERVICE.getInstance().createBatchSuppressActions(HighlightDisplayKey.find(getShortName()))));
+ result.addAll(Arrays.asList(new XmlSuppressableInspectionTool.SuppressTagStatic(getShortName()),
+ new XmlSuppressableInspectionTool.SuppressForFile(getShortName())));
+ return result.toArray(new SuppressQuickFix[result.size()]);
+ } else {
+ return new SuppressQuickFix[] { suppressLintQuickFix };
+ }
+ }
+
+ private static class SuppressLintQuickFix implements SuppressQuickFix {
+ private Issue myIssue;
+
+ private SuppressLintQuickFix(Issue issue) {
+ myIssue = issue;
+ }
+
+ @Override
+ public boolean isAvailable(@NotNull Project project, @NotNull PsiElement context) {
+ return true;
+ }
+
+ @NotNull
+ @Override
+ public String getName() {
+ return "Suppress with @SuppressLint (Java) or tools:ignore (XML)";
+ }
+
+ @NotNull
+ @Override
+ public String getFamilyName() {
+ return "Suppress";
+ }
+
+ @Override
+ public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
+ PsiElement myElement = descriptor.getPsiElement();
+ PsiFile file = PsiTreeUtil.getParentOfType(myElement, PsiFile.class, false);
+ if (file != null) {
+ new SuppressLintIntentionAction(myIssue.getId(), myElement).invoke(project, null, file);
+ }
+ }
}
@Override
diff --git a/android/src/org/jetbrains/android/inspections/lint/AndroidLintInspectionToolProvider.java b/android/src/org/jetbrains/android/inspections/lint/AndroidLintInspectionToolProvider.java
index 86298a9..b91ed2c 100644
--- a/android/src/org/jetbrains/android/inspections/lint/AndroidLintInspectionToolProvider.java
+++ b/android/src/org/jetbrains/android/inspections/lint/AndroidLintInspectionToolProvider.java
@@ -102,6 +102,12 @@
}
}
+ public static class AndroidLintMissingSuperCallInspection extends AndroidLintInspectionBase {
+ public AndroidLintMissingSuperCallInspection() {
+ super(AndroidBundle.message("android.lint.inspections.missing.super.call"), CallSuperDetector.ISSUE);
+ }
+ }
+
public static class AndroidLintMissingTranslationInspection extends AndroidLintInspectionBase {
public AndroidLintMissingTranslationInspection() {
super(AndroidBundle.message("android.lint.inspections.missing.translation"), TranslationDetector.MISSING);
@@ -370,6 +376,12 @@
}
}
+ public static class AndroidLintDeviceAdminInspection extends AndroidLintInspectionBase {
+ public AndroidLintDeviceAdminInspection() {
+ super(AndroidBundle.message("android.lint.inspections.device.admin"), ManifestDetector.DEVICE_ADMIN);
+ }
+ }
+
public static class AndroidLintDisableBaselineAlignmentInspection extends AndroidLintInspectionBase {
public AndroidLintDisableBaselineAlignmentInspection() {
super(AndroidBundle.message("android.lint.inspections.disable.baseline.alignment"), InefficientWeightDetector.BASELINE_WEIGHTS);
@@ -387,19 +399,19 @@
public static class AndroidLintManifestOrderInspection extends AndroidLintInspectionBase {
public AndroidLintManifestOrderInspection() {
- super(AndroidBundle.message("android.lint.inspections.manifest.order"), ManifestOrderDetector.ORDER);
+ super(AndroidBundle.message("android.lint.inspections.manifest.order"), ManifestDetector.ORDER);
}
}
public static class AndroidLintMultipleUsesSdkInspection extends AndroidLintInspectionBase {
public AndroidLintMultipleUsesSdkInspection() {
- super(AndroidBundle.message("android.lint.inspections.multiple.uses.sdk"), ManifestOrderDetector.MULTIPLE_USES_SDK);
+ super(AndroidBundle.message("android.lint.inspections.multiple.uses.sdk"), ManifestDetector.MULTIPLE_USES_SDK);
}
}
public static class AndroidLintUsesMinSdkAttributesInspection extends AndroidLintInspectionBase {
public AndroidLintUsesMinSdkAttributesInspection() {
- super(AndroidBundle.message("android.lint.inspections.uses.min.sdk.attributes"), ManifestOrderDetector.USES_SDK);
+ super(AndroidBundle.message("android.lint.inspections.uses.min.sdk.attributes"), ManifestDetector.USES_SDK);
}
}
@@ -632,12 +644,12 @@
public static class AndroidLintDuplicateUsesFeatureInspection extends AndroidLintInspectionBase {
public AndroidLintDuplicateUsesFeatureInspection() {
- super(AndroidBundle.message("android.lint.inspections.duplicate.uses.feature"), ManifestOrderDetector.DUPLICATE_USES_FEATURE);
+ super(AndroidBundle.message("android.lint.inspections.duplicate.uses.feature"), ManifestDetector.DUPLICATE_USES_FEATURE);
}
}
public static class AndroidLintMissingApplicationIconInspection extends AndroidLintInspectionBase {
public AndroidLintMissingApplicationIconInspection() {
- super(AndroidBundle.message("android.lint.inspections.missing.application.icon"), ManifestOrderDetector.APPLICATION_ICON);
+ super(AndroidBundle.message("android.lint.inspections.missing.application.icon"), ManifestDetector.APPLICATION_ICON);
}
}
public static class AndroidLintRtlCompatInspection extends AndroidLintInspectionBase {
@@ -665,7 +677,7 @@
public static class AndroidLintAllowBackupInspection extends AndroidLintInspectionBase {
public AndroidLintAllowBackupInspection() {
- super(AndroidBundle.message("android.lint.inspections.allow.backup"), ManifestOrderDetector.ALLOW_BACKUP);
+ super(AndroidBundle.message("android.lint.inspections.allow.backup"), ManifestDetector.ALLOW_BACKUP);
}
}
public static class AndroidLintButtonStyleInspection extends AndroidLintInspectionBase {
@@ -685,7 +697,7 @@
}
public static class AndroidLintDuplicateActivityInspection extends AndroidLintInspectionBase {
public AndroidLintDuplicateActivityInspection() {
- super(AndroidBundle.message("android.lint.inspections.duplicate.activity"), ManifestOrderDetector.DUPLICATE_ACTIVITY);
+ super(AndroidBundle.message("android.lint.inspections.duplicate.activity"), ManifestDetector.DUPLICATE_ACTIVITY);
}
}
public static class AndroidLintDuplicateDefinitionInspection extends AndroidLintInspectionBase {
@@ -730,7 +742,7 @@
}
public static class AndroidLintIllegalResourceRefInspection extends AndroidLintInspectionBase {
public AndroidLintIllegalResourceRefInspection() {
- super(AndroidBundle.message("android.lint.inspections.illegal.resource.ref"), ManifestOrderDetector.ILLEGAL_REFERENCE);
+ super(AndroidBundle.message("android.lint.inspections.illegal.resource.ref"), ManifestDetector.ILLEGAL_REFERENCE);
}
}
public static class AndroidLintInOrMmUsageInspection extends AndroidLintInspectionBase {
@@ -776,12 +788,12 @@
}
public static class AndroidLintMissingVersionInspection extends AndroidLintInspectionBase {
public AndroidLintMissingVersionInspection() {
- super(AndroidBundle.message("android.lint.inspections.missing.version"), ManifestOrderDetector.SET_VERSION);
+ super(AndroidBundle.message("android.lint.inspections.missing.version"), ManifestDetector.SET_VERSION);
}
}
public static class AndroidLintOldTargetApiInspection extends AndroidLintInspectionBase {
public AndroidLintOldTargetApiInspection() {
- super(AndroidBundle.message("android.lint.inspections.old.target.api"), ManifestOrderDetector.TARGET_NEWER);
+ super(AndroidBundle.message("android.lint.inspections.old.target.api"), ManifestDetector.TARGET_NEWER);
}
}
public static class AndroidLintOrientationInspection extends AndroidLintInspectionBase {
@@ -868,7 +880,7 @@
}
public static class AndroidLintUniquePermissionInspection extends AndroidLintInspectionBase {
public AndroidLintUniquePermissionInspection() {
- super(AndroidBundle.message("android.lint.inspections.unique.permission"), ManifestOrderDetector.UNIQUE_PERMISSION);
+ super(AndroidBundle.message("android.lint.inspections.unique.permission"), ManifestDetector.UNIQUE_PERMISSION);
}
}
public static class AndroidLintUnlocalizedSmsInspection extends AndroidLintInspectionBase {
diff --git a/android/src/org/jetbrains/android/inspections/lint/DomPsiConverter.java b/android/src/org/jetbrains/android/inspections/lint/DomPsiConverter.java
index 63014f3..f91d1af 100644
--- a/android/src/org/jetbrains/android/inspections/lint/DomPsiConverter.java
+++ b/android/src/org/jetbrains/android/inspections/lint/DomPsiConverter.java
@@ -73,6 +73,7 @@
if (BENCHMARK) {
timer.stop();
+ //noinspection UseOfSystemOutOrSystemErr
System.out.println("Creating PSI for " + xmlFile.getName() + " took " + timer.elapsedMillis() + "ms (" + timer.toString() + ")");
}
@@ -1129,7 +1130,11 @@
@NotNull
@Override
public String getValue() {
- return myAttribute.getValue();
+ String value = myAttribute.getValue();
+ if (value == null) {
+ value = "";
+ }
+ return value;
}
@NotNull
diff --git a/android/src/org/jetbrains/android/inspections/lint/IntellijApiDetector.java b/android/src/org/jetbrains/android/inspections/lint/IntellijApiDetector.java
index b83f796..51fad86 100644
--- a/android/src/org/jetbrains/android/inspections/lint/IntellijApiDetector.java
+++ b/android/src/org/jetbrains/android/inspections/lint/IntellijApiDetector.java
@@ -19,6 +19,7 @@
import com.android.annotations.Nullable;
import com.android.ide.common.sdk.SdkVersionInfo;
import com.android.tools.lint.checks.ApiDetector;
+import com.android.tools.lint.checks.ApiLookup;
import com.android.tools.lint.detector.api.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.psi.*;
@@ -44,7 +45,7 @@
* <p>
* TODO:
* <ul>
- * <li>Unit tests, and compare to the bytecode based results</li>
+ * <li>Compare to the bytecode based results</li>
* </ul>
*/
public class IntellijApiDetector extends ApiDetector {
@@ -157,14 +158,8 @@
if (dotIndex != -1) {
text = text.substring(dotIndex + 1);
}
- for (int api = 1; api <= SdkVersionInfo.HIGHEST_KNOWN_API; api++) {
- String code = SdkVersionInfo.getBuildCode(api);
- if (code != null && code.equalsIgnoreCase(text)) {
- return api;
- }
- }
- return -1;
+ return SdkVersionInfo.getApiByBuildCode(text, true);
}
private class ApiCheckVisitor extends JavaRecursiveElementVisitor {
@@ -405,7 +400,7 @@
@NonNull PsiElement node,
@NonNull String name,
@NonNull String owner) {
- if (IntellijApiDetector.this.isBenignConstantUsage(null, name, owner)) {
+ if (ApiDetector.isBenignConstantUsage(null, name, owner)) {
return true;
}
@@ -511,9 +506,7 @@
if (expressionOwner != null && !expressionOwner.equals(owner)) {
int specificApi = mApiDatabase.getCallVersion(expressionOwner, name, desc);
if (specificApi == -1) {
- if (expressionOwner.startsWith("android/")
- || expressionOwner.startsWith("java/")
- || expressionOwner.startsWith("javax/")) {
+ if (ApiLookup.isRelevantOwner(expressionOwner)) {
return;
}
} else if (specificApi <= minSdk) {
@@ -531,9 +524,7 @@
}
int specificApi = mApiDatabase.getCallVersion(expressionOwner, name, desc);
if (specificApi == -1) {
- if (expressionOwner.startsWith("android/")
- || expressionOwner.startsWith("java/")
- || expressionOwner.startsWith("javax/")) {
+ if (ApiLookup.isRelevantOwner(expressionOwner)) {
return;
}
} else if (specificApi <= minSdk) {
@@ -582,7 +573,7 @@
private static boolean isWithinVersionCheckConditional(PsiElement element, int api) {
PsiElement current = element.getParent();
PsiElement prev = current;
- while (true) {
+ while (current != null) {
if (current instanceof PsiIfStatement) {
PsiIfStatement ifStatement = (PsiIfStatement)current;
PsiExpression condition = ifStatement.getCondition();
@@ -600,7 +591,7 @@
if (right instanceof PsiReferenceExpression) {
PsiReferenceExpression ref2 = (PsiReferenceExpression)right;
String codeName = ref2.getReferenceName();
- level = getApiForCodenameField(codeName);
+ level = SdkVersionInfo.getApiByBuildCode(codeName, true);
} else if (right instanceof PsiLiteralExpression) {
PsiLiteralExpression lit = (PsiLiteralExpression)right;
Object value = lit.getValue();
@@ -635,7 +626,6 @@
}
}
}
- break;
} else if (current instanceof PsiMethod || current instanceof PsiFile) {
return false;
}
@@ -645,15 +635,4 @@
return false;
}
-
- private static int getApiForCodenameField(@Nullable String codeName) {
- for (int level = 1; level < SdkVersionInfo.HIGHEST_KNOWN_API; level++) {
- String s = SdkVersionInfo.getBuildCode(level);
- if (s != null && s.equals(codeName)) {
- return level;
- }
- }
-
- return -1;
- }
}
diff --git a/android/src/org/jetbrains/android/inspections/lint/IntellijLintClient.java b/android/src/org/jetbrains/android/inspections/lint/IntellijLintClient.java
index fccadca..69a55b5 100644
--- a/android/src/org/jetbrains/android/inspections/lint/IntellijLintClient.java
+++ b/android/src/org/jetbrains/android/inspections/lint/IntellijLintClient.java
@@ -128,6 +128,16 @@
return result;
}
+ @NonNull
+ @Override
+ public List<File> getResourceFolders(@NonNull com.android.tools.lint.detector.api.Project project) {
+ AndroidFacet facet = AndroidFacet.getInstance(myState.getModule());
+ if (facet != null) {
+ return IntellijLintUtils.getResourceDirectories(facet);
+ }
+ return super.getResourceFolders(project);
+ }
+
@Override
@NotNull
public String readFile(File file) {
diff --git a/android/src/org/jetbrains/android/inspections/lint/IntellijLintIssueRegistry.java b/android/src/org/jetbrains/android/inspections/lint/IntellijLintIssueRegistry.java
index 01aa8c9..e62767e 100644
--- a/android/src/org/jetbrains/android/inspections/lint/IntellijLintIssueRegistry.java
+++ b/android/src/org/jetbrains/android/inspections/lint/IntellijLintIssueRegistry.java
@@ -74,6 +74,7 @@
}
result.add(issue);
}
+ //noinspection AssignmentToStaticFieldFromInstanceMethod
ourFilteredIssues = result;
}
diff --git a/android/src/org/jetbrains/android/inspections/lint/IntellijLintUtils.java b/android/src/org/jetbrains/android/inspections/lint/IntellijLintUtils.java
index 121e569..c7b839b 100644
--- a/android/src/org/jetbrains/android/inspections/lint/IntellijLintUtils.java
+++ b/android/src/org/jetbrains/android/inspections/lint/IntellijLintUtils.java
@@ -18,11 +18,13 @@
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
+import com.android.builder.model.SourceProvider;
import com.android.tools.lint.client.api.LintRequest;
import com.android.tools.lint.detector.api.*;
import com.google.common.base.Splitter;
import com.intellij.debugger.engine.JVMNameUtil;
import com.intellij.ide.util.JavaAnonymousClassesHelper;
+import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.io.FileUtil;
@@ -33,9 +35,13 @@
import com.intellij.psi.util.ClassUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.TypeConversionUtil;
+import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
import static com.android.SdkConstants.CONSTRUCTOR_NAME;
import static com.android.SdkConstants.SUPPRESS_ALL;
@@ -337,4 +343,38 @@
}
return true;
}
+
+ /** Returns the resource directories to use for the given module */
+ @NotNull
+ public static List<File> getResourceDirectories(@NotNull AndroidFacet facet) {
+ if (facet.isGradleProject()) {
+ List<File> resDirectories = new ArrayList<File>();
+ resDirectories.addAll(facet.getMainSourceSet().getResDirectories());
+ List<SourceProvider> flavorSourceSets = facet.getFlavorSourceSets();
+ if (flavorSourceSets != null) {
+ for (SourceProvider provider : flavorSourceSets) {
+ for (File file : provider.getResDirectories()) {
+ if (file.isDirectory()) {
+ resDirectories.add(file);
+ }
+ }
+ }
+ }
+
+ SourceProvider buildTypeSourceSet = facet.getBuildTypeSourceSet();
+ if (buildTypeSourceSet != null) {
+ for (File file : buildTypeSourceSet.getResDirectories()) {
+ if (file.isDirectory()) {
+ resDirectories.add(file);
+ }
+ }
+ }
+
+ return resDirectories;
+ } else {
+ return new ArrayList<File>(facet.getMainSourceSet().getResDirectories());
+ }
+ }
+
+
}
diff --git a/android/src/org/jetbrains/android/inspections/lint/LombokPsiConverter.java b/android/src/org/jetbrains/android/inspections/lint/LombokPsiConverter.java
index 8281758..66b82ad 100644
--- a/android/src/org/jetbrains/android/inspections/lint/LombokPsiConverter.java
+++ b/android/src/org/jetbrains/android/inspections/lint/LombokPsiConverter.java
@@ -869,6 +869,11 @@
if (reference != null) {
return toVariableReference(reference);
}
+ if (p instanceof PsiSuperExpression) {
+ Super superExpression = new Super();
+ bind(superExpression, p);
+ return superExpression;
+ }
return toVariableReference(p.getText(), p);
} else if (expression instanceof PsiReferenceExpression) {
PsiReferenceExpression refExpression = (PsiReferenceExpression)expression;
@@ -1031,6 +1036,7 @@
return null;
}
+ @Nullable
private static BinaryOperator convertOperation(IElementType operation) {
BinaryOperator operator = null;
if (operation == JavaTokenType.EQEQ) {
@@ -1276,10 +1282,21 @@
if (initialization instanceof PsiDeclarationStatement) {
PsiDeclarationStatement pds = (PsiDeclarationStatement)initialization;
f.astVariableDeclaration(toVariableDefinition(pds));
+ } else if (initialization instanceof PsiExpressionStatement) {
+ PsiExpressionStatement expressionStatement = (PsiExpressionStatement)initialization;
+ f.astExpressionInits().addToEnd(toExpression(expressionStatement.getExpression()));
+ } else if (initialization instanceof PsiExpression) {
+ PsiExpression expression = (PsiExpression)initialization;
+ f.astExpressionInits().addToEnd(toExpression(expression));
+ } else if (initialization instanceof PsiExpressionListStatement) {
+ PsiExpressionList expressionList = ((PsiExpressionListStatement)initialization).getExpressionList();
+ if (expressionList != null) {
+ for (PsiExpression expression : expressionList.getExpressions()) {
+ f.astExpressionInits().addToEnd(toExpression(expression));
+ }
+ }
} else {
// Unexpected type of initializer
- // TODO: Handle initialization; Lombok only allows a variable declaration here,
- // PSI seems to imply more
assert false : initialization;
}
}
diff --git a/android/src/org/jetbrains/android/inspections/lint/SuppressLintIntentionAction.java b/android/src/org/jetbrains/android/inspections/lint/SuppressLintIntentionAction.java
index 10ddb1f..22011b7 100644
--- a/android/src/org/jetbrains/android/inspections/lint/SuppressLintIntentionAction.java
+++ b/android/src/org/jetbrains/android/inspections/lint/SuppressLintIntentionAction.java
@@ -67,9 +67,10 @@
return "";
} else if (file instanceof XmlFile) {
return AndroidBundle.message("android.lint.fix.suppress.lint.api.attr", id);
- } else {
- assert file instanceof PsiJavaFile : file;
+ } else if (file instanceof PsiJavaFile) {
return AndroidBundle.message("android.lint.fix.suppress.lint.api.annotation", id);
+ } else {
+ return "";
}
}
@@ -85,7 +86,7 @@
}
@Override
- public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
+ public void invoke(@NotNull Project project, @Nullable Editor editor, @NotNull PsiFile file) throws IncorrectOperationException {
if (file instanceof XmlFile) {
final XmlTag element = PsiTreeUtil.getParentOfType(myElement, XmlTag.class);
if (element == null) {
diff --git a/android/src/org/jetbrains/android/logcat/AndroidLogcatFormatter.java b/android/src/org/jetbrains/android/logcat/AndroidLogcatFormatter.java
index 860dbf4..e2d1b91 100644
--- a/android/src/org/jetbrains/android/logcat/AndroidLogcatFormatter.java
+++ b/android/src/org/jetbrains/android/logcat/AndroidLogcatFormatter.java
@@ -16,6 +16,7 @@
package org.jetbrains.android.logcat;
+import com.android.annotations.VisibleForTesting;
import com.android.ddmlib.Log;
import com.google.common.primitives.Ints;
import com.intellij.openapi.util.Pair;
@@ -27,25 +28,37 @@
import java.util.regex.Pattern;
public class AndroidLogcatFormatter {
+ /**
+ * The separator printed out after the tag.
+ * The output of {@link #formatMessage(String, org.jetbrains.android.logcat.AndroidLogcatReceiver.LogMessageHeader)} is displayed
+ * to users, but is also parsed back by {@link #parseMessage(String)}. In particular, the tag and message strings come directly
+ * from the user, and can contain any sequence of characters. So we the unicode colon character to distinguish between
+ * where the tag ends and where the message begins. The character could really be anything as long as it is both displayable and
+ * meaningful to the user, yet is unlikely to occur in user strings.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ static final String TAG_SEPARATOR = "\ufe55"; // unicode small colon
+
@NonNls
private static final Pattern LOGMESSAGE_PATTERN =
Pattern.compile("(\\d\\d-\\d\\d\\s\\d\\d:\\d\\d:\\d\\d.\\d+)\\s+" + // time
- "(\\d+)-(\\d+)/" + // pid-tid
- "(\\S+)\\s+" + // package
- "([A-Z])/" + // log level
- "(\\S*)\\s*: " + // tag
- "(.*)" // message
+ "(\\d+)-(\\d+)/" + // pid-tid
+ "(\\S+)\\s+" + // package
+ "([A-Z])/" + // log level
+ "(.*)" + TAG_SEPARATOR + " " + // tag
+ "(.*)" // message
);
public static String formatMessage(String message, LogMessageHeader header) {
String ids = String.format(Locale.US, "%d-%s", header.myPid, header.myTid);
return String.format(Locale.US,
- "%1$s %2$12s/%3$-30s %4$c/%5$s: %6$s",
+ "%1$s %2$12s/%3$s %4$c/%5$s%6$s %7$s",
header.myTime,
ids,
header.myAppPackage.isEmpty() ? "?" : header.myAppPackage,
header.myLogLevel.getPriorityLetter(),
header.myTag,
+ TAG_SEPARATOR,
message);
}
diff --git a/android/src/org/jetbrains/android/logcat/AndroidToolWindowFactory.java b/android/src/org/jetbrains/android/logcat/AndroidToolWindowFactory.java
index 39a9f76..7de3cbe 100644
--- a/android/src/org/jetbrains/android/logcat/AndroidToolWindowFactory.java
+++ b/android/src/org/jetbrains/android/logcat/AndroidToolWindowFactory.java
@@ -104,6 +104,8 @@
final ContentManager contentManager = toolWindow.getContentManager();
Content c = contentManager.getFactory().createContent(layoutUi.getComponent(), "DDMS", true);
+ // Store a reference to the logcat view, so that this view can be retrieved directly from
+ // the DDMS tool window. (e.g. to clear logcat before a launch)
// add possibility to access logcat view externally in the future
c.putUserData(AndroidLogcatView.ANDROID_LOGCAT_VIEW_KEY, logcatView);
diff --git a/android/src/org/jetbrains/android/maven/AndroidFacetImporterBase.java b/android/src/org/jetbrains/android/maven/AndroidFacetImporterBase.java
index dec2278..32f20ee 100644
--- a/android/src/org/jetbrains/android/maven/AndroidFacetImporterBase.java
+++ b/android/src/org/jetbrains/android/maven/AndroidFacetImporterBase.java
@@ -15,6 +15,7 @@
*/
package org.jetbrains.android.maven;
+import com.android.SdkConstants;
import com.android.sdklib.IAndroidTarget;
import com.android.utils.NullLogger;
import com.intellij.facet.FacetType;
@@ -129,7 +130,7 @@
AndroidMavenProviderImpl.configureAaptCompilation(mavenProject, facet.getModule(), facet.getConfiguration(), hasApkSources);
if (AndroidMavenUtil.APKLIB_DEPENDENCY_AND_PACKAGING_TYPE.equals(mavenProject.getPackaging())) {
- facet.getProperties().LIBRARY_PROJECT = true;
+ facet.setLibraryProject(true);
}
facet.getConfiguration().setIncludeAssetsFromLibraries(true);
@@ -788,7 +789,7 @@
sdkPath = ANDROID_SDK_PATH_TEST;
}
else {
- sdkPath = System.getenv(AndroidSdkUtils.ANDROID_HOME_ENV);
+ sdkPath = System.getenv(SdkConstants.ANDROID_HOME_ENV);
}
LOG.info("android home: " + sdkPath);
diff --git a/android/src/org/jetbrains/android/newProject/AndroidAppPropertiesEditor.java b/android/src/org/jetbrains/android/newProject/AndroidAppPropertiesEditor.java
index 37b9901..ae2d7e2 100644
--- a/android/src/org/jetbrains/android/newProject/AndroidAppPropertiesEditor.java
+++ b/android/src/org/jetbrains/android/newProject/AndroidAppPropertiesEditor.java
@@ -142,7 +142,7 @@
if (!library) {
for (Module module : modulesProvider.getModules()) {
final AndroidFacet facet = AndroidFacet.getInstance(module);
- if (facet != null && !facet.getProperties().LIBRARY_PROJECT) {
+ if (facet != null && !facet.isLibraryProject()) {
final Manifest manifest = facet.getManifest();
if (manifest != null) {
final String packageName = manifest.getPackage().getValue();
diff --git a/android/src/org/jetbrains/android/newProject/AndroidModulesComboBox.java b/android/src/org/jetbrains/android/newProject/AndroidModulesComboBox.java
index 289bf96..c89e13d 100644
--- a/android/src/org/jetbrains/android/newProject/AndroidModulesComboBox.java
+++ b/android/src/org/jetbrains/android/newProject/AndroidModulesComboBox.java
@@ -62,7 +62,7 @@
List<Module> result = new ArrayList<Module>();
for (Module module : modules) {
final AndroidFacet facet = AndroidFacet.getInstance(module);
- if (facet != null && !facet.getProperties().LIBRARY_PROJECT) {
+ if (facet != null && !facet.isLibraryProject()) {
result.add(module);
}
}
diff --git a/android/src/org/jetbrains/android/newProject/AndroidProjectTemplatesFactory.java b/android/src/org/jetbrains/android/newProject/AndroidProjectTemplatesFactory.java
index 7e7afe0..181cc98 100644
--- a/android/src/org/jetbrains/android/newProject/AndroidProjectTemplatesFactory.java
+++ b/android/src/org/jetbrains/android/newProject/AndroidProjectTemplatesFactory.java
@@ -15,8 +15,10 @@
*/
package org.jetbrains.android.newProject;
+import com.android.tools.idea.gradle.util.Projects;
import com.intellij.ide.util.projectWizard.ModuleWizardStep;
import com.intellij.ide.util.projectWizard.WizardContext;
+import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ui.configuration.ModulesProvider;
import com.intellij.platform.ProjectTemplate;
import com.intellij.platform.ProjectTemplatesFactory;
@@ -39,6 +41,8 @@
public static final String LIBRARY_MODULE = "Library Module";
public static final String TEST_MODULE = "Test Module";
+ public static final ProjectTemplate[] EMPTY_PROJECT_TEMPLATES = new ProjectTemplate[]{};
+
@NotNull
@Override
public String[] getGroups() {
@@ -53,6 +57,10 @@
@NotNull
@Override
public ProjectTemplate[] createTemplates(String group, WizardContext context) {
+ Project project = context.getProject();
+ if (project != null && Projects.isGradleProject(project)) {
+ return EMPTY_PROJECT_TEMPLATES;
+ }
ProjectTemplate[] templates = {
new BuilderBasedTemplate(new AndroidModuleBuilder()),
new AndroidProjectTemplate(EMPTY_MODULE,
@@ -76,7 +84,7 @@
"that can be referenced by other Android modules",
new AndroidModuleBuilder.Library())
};
- if (context.getProject() == null) {
+ if (project == null) {
return templates;
}
else {
diff --git a/android/src/org/jetbrains/android/refactoring/AndroidFindStyleApplicationsDialog.java b/android/src/org/jetbrains/android/refactoring/AndroidFindStyleApplicationsDialog.java
index 450e425..f4b7d65 100644
--- a/android/src/org/jetbrains/android/refactoring/AndroidFindStyleApplicationsDialog.java
+++ b/android/src/org/jetbrains/android/refactoring/AndroidFindStyleApplicationsDialog.java
@@ -47,12 +47,14 @@
myFileScopeRadio.setVisible(false);
}
final String scopeValue = PropertiesComponent.getInstance().getValue(FIND_STYLE_APPLICATIONS_SCOPE_PROPERTY);
- AndroidFindStyleApplicationsProcessor.MyScope scope;
- try {
- scope = Enum.valueOf(AndroidFindStyleApplicationsProcessor.MyScope.class, scopeValue);
- }
- catch (IllegalArgumentException e) {
- scope = null;
+ AndroidFindStyleApplicationsProcessor.MyScope scope = null;
+ if (scopeValue != null) {
+ try {
+ scope = Enum.valueOf(AndroidFindStyleApplicationsProcessor.MyScope.class, scopeValue);
+ }
+ catch (IllegalArgumentException e) {
+ scope = null;
+ }
}
if (scope == null) {
diff --git a/android/src/org/jetbrains/android/resourceManagers/LocalResourceManager.java b/android/src/org/jetbrains/android/resourceManagers/LocalResourceManager.java
index 11b0c7d..4d88b3d 100644
--- a/android/src/org/jetbrains/android/resourceManagers/LocalResourceManager.java
+++ b/android/src/org/jetbrains/android/resourceManagers/LocalResourceManager.java
@@ -17,9 +17,12 @@
package org.jetbrains.android.resourceManagers;
import com.android.resources.ResourceType;
+import com.android.tools.idea.rendering.ModuleSetResourceRepository;
+import com.google.common.collect.Sets;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
@@ -39,12 +42,14 @@
import org.jetbrains.android.dom.resources.Resources;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.facet.AndroidRootUtil;
+import org.jetbrains.android.facet.ResourceFolderManager;
import org.jetbrains.android.util.AndroidResourceUtil;
import org.jetbrains.android.util.AndroidUtils;
import org.jetbrains.android.util.ResourceEntry;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.io.File;
import java.util.*;
/**
@@ -122,6 +127,21 @@
return;
}
}
+
+ // Add in local AAR dependencies, if any
+ if (facet.isGradleProject()) {
+ Set<File> dirs = Sets.newHashSet();
+ ResourceFolderManager.addAarsFromModuleLibraries(facet, dirs);
+ if (!dirs.isEmpty()) {
+ for (File dir : dirs) {
+ VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByIoFile(dir);
+ if (virtualFile != null) {
+ result.add(virtualFile);
+ }
+ }
+ }
+ }
+
for (AndroidFacet depFacet : AndroidUtils.getAllAndroidDependencies(facet.getModule(), false)) {
collectResourceDirs(depFacet, result, visited);
}
diff --git a/android/src/org/jetbrains/android/run/AndroidDebugRunner.java b/android/src/org/jetbrains/android/run/AndroidDebugRunner.java
index bbd7e28..8329eb6 100644
--- a/android/src/org/jetbrains/android/run/AndroidDebugRunner.java
+++ b/android/src/org/jetbrains/android/run/AndroidDebugRunner.java
@@ -58,6 +58,7 @@
import icons.AndroidIcons;
import org.jetbrains.android.dom.manifest.Instrumentation;
import org.jetbrains.android.dom.manifest.Manifest;
+import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.logcat.AndroidToolWindowFactory;
import org.jetbrains.android.logcat.AndroidLogcatView;
import org.jetbrains.android.run.testing.AndroidTestRunConfiguration;
@@ -149,12 +150,16 @@
final RunProfile runProfile = environment.getRunProfile();
if (runProfile instanceof AndroidTestRunConfiguration) {
- String targetPackage = getTargetPackage((AndroidTestRunConfiguration)runProfile, state);
- if (targetPackage == null) {
- throw new ExecutionException(AndroidBundle.message("target.package.not.specified.error"));
+ // attempt to set the target package only in case on non Gradle projects
+ if (!state.getFacet().isGradleProject()) {
+ String targetPackage = getTargetPackage((AndroidTestRunConfiguration)runProfile, state);
+ if (targetPackage == null) {
+ throw new ExecutionException(AndroidBundle.message("target.package.not.specified.error"));
+ }
+ state.setTargetPackageName(targetPackage);
}
- state.setTargetPackageName(targetPackage);
}
+
state.setDebugMode(true);
RunContentDescriptor runDescriptor;
synchronized (myDebugLock) {
diff --git a/android/src/org/jetbrains/android/run/AndroidRunConfiguration.java b/android/src/org/jetbrains/android/run/AndroidRunConfiguration.java
index 0afea87..f672792 100755
--- a/android/src/org/jetbrains/android/run/AndroidRunConfiguration.java
+++ b/android/src/org/jetbrains/android/run/AndroidRunConfiguration.java
@@ -17,6 +17,7 @@
import com.android.SdkConstants;
import com.android.ddmlib.*;
+import com.google.common.base.Predicates;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.Executor;
import com.intellij.execution.JavaExecutionUtil;
@@ -78,6 +79,11 @@
}
@Override
+ protected Pair<Boolean, String> supportsRunningLibraryProjects(@NotNull AndroidFacet facet) {
+ return new Pair<Boolean, String>(Boolean.FALSE, AndroidBundle.message("android.cannot.run.library.project.error"));
+ }
+
+ @Override
protected void checkConfiguration(@NotNull AndroidFacet facet) throws RuntimeConfigurationException {
final boolean packageContainMavenProperty = doesPackageContainMavenProperty(facet);
final JavaRunConfigurationModule configurationModule = getConfigurationModule();
@@ -136,7 +142,8 @@
@Override
public SettingsEditor<? extends RunConfiguration> getConfigurationEditor() {
Project project = getProject();
- AndroidRunConfigurationEditor<AndroidRunConfiguration> editor = new AndroidRunConfigurationEditor<AndroidRunConfiguration>(project);
+ AndroidRunConfigurationEditor<AndroidRunConfiguration> editor =
+ new AndroidRunConfigurationEditor<AndroidRunConfiguration>(project, Predicates.<AndroidFacet>alwaysFalse());
editor.setConfigurationSpecificEditor(new ApplicationRunParameters(project, editor.getModuleSelector()));
return editor;
}
diff --git a/android/src/org/jetbrains/android/run/AndroidRunConfigurationBase.java b/android/src/org/jetbrains/android/run/AndroidRunConfigurationBase.java
index f5163aa..b0f0fdc 100644
--- a/android/src/org/jetbrains/android/run/AndroidRunConfigurationBase.java
+++ b/android/src/org/jetbrains/android/run/AndroidRunConfigurationBase.java
@@ -92,8 +92,11 @@
if (facet == null) {
throw new RuntimeConfigurationError(AndroidBundle.message("android.no.facet.error"));
}
- if (facet.getProperties().LIBRARY_PROJECT) {
- throw new RuntimeConfigurationError(AndroidBundle.message("android.cannot.run.library.project.error"));
+ if (facet.isLibraryProject()) {
+ Pair<Boolean, String> result = supportsRunningLibraryProjects(facet);
+ if (!result.getFirst()) {
+ throw new RuntimeConfigurationError(result.getSecond());
+ }
}
if (facet.getConfiguration().getAndroidPlatform() == null) {
throw new RuntimeConfigurationError(AndroidBundle.message("select.platform.error"));
@@ -120,6 +123,8 @@
checkConfiguration(facet);
}
+ /** Returns whether the configuration supports running library projects, and if it doesn't, then an explanation as to why it doesn't. */
+ protected abstract Pair<Boolean,String> supportsRunningLibraryProjects(@NotNull AndroidFacet facet);
protected abstract void checkConfiguration(@NotNull AndroidFacet facet) throws RuntimeConfigurationException;
@Override
diff --git a/android/src/org/jetbrains/android/run/AndroidRunConfigurationEditor.java b/android/src/org/jetbrains/android/run/AndroidRunConfigurationEditor.java
index 3d37956..f81d6ea 100644
--- a/android/src/org/jetbrains/android/run/AndroidRunConfigurationEditor.java
+++ b/android/src/org/jetbrains/android/run/AndroidRunConfigurationEditor.java
@@ -17,6 +17,7 @@
import com.android.sdklib.internal.avd.AvdInfo;
import com.android.sdklib.internal.avd.AvdManager;
+import com.google.common.base.Predicate;
import com.intellij.execution.ui.ConfigurationModuleSelector;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.application.ModalityState;
@@ -78,7 +79,7 @@
setAnchor(myConfigurationSpecificEditor.getAnchor());
}
- public AndroidRunConfigurationEditor(final Project project) {
+ public AndroidRunConfigurationEditor(final Project project, final Predicate<AndroidFacet> libraryProjectValidator) {
myCommandLineField.setDialogCaption("Emulator Additional Command Line Options");
myModuleSelector = new ConfigurationModuleSelector(project, myModulesComboBox) {
@@ -87,8 +88,13 @@
if (module == null || !super.isModuleAccepted(module)) {
return false;
}
+
final AndroidFacet facet = AndroidFacet.getInstance(module);
- return facet != null && !facet.getProperties().LIBRARY_PROJECT;
+ if (facet == null) {
+ return false;
+ }
+
+ return !facet.isLibraryProject() || libraryProjectValidator.apply(facet);
}
};
diff --git a/android/src/org/jetbrains/android/run/AndroidRunningState.java b/android/src/org/jetbrains/android/run/AndroidRunningState.java
index e273083..2c55f7d 100644
--- a/android/src/org/jetbrains/android/run/AndroidRunningState.java
+++ b/android/src/org/jetbrains/android/run/AndroidRunningState.java
@@ -16,10 +16,7 @@
package org.jetbrains.android.run;
import com.android.SdkConstants;
-import com.android.build.gradle.model.AndroidProject;
-import com.android.build.gradle.model.BuildTypeContainer;
-import com.android.build.gradle.model.Variant;
-import com.android.builder.model.ProductFlavor;
+import com.android.builder.model.*;
import com.android.ddmlib.*;
import com.android.prefs.AndroidLocation;
import com.android.sdklib.IAndroidTarget;
@@ -77,6 +74,7 @@
import org.jetbrains.android.logcat.AndroidLogcatUtil;
import org.jetbrains.android.logcat.AndroidLogcatView;
import org.jetbrains.android.logcat.AndroidToolWindowFactory;
+import org.jetbrains.android.run.testing.AndroidTestRunConfiguration;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.android.sdk.AvdManagerLog;
import org.jetbrains.android.util.AndroidBundle;
@@ -113,7 +111,17 @@
static final int NO_ERROR = -2;
private static final int UNTYPED_ERROR = -1;
+ /** Default suffix for test packages (as added by Android Gradle plugin) */
+ private static final String DEFAULT_TEST_PACKAGE_SUFFIX = ".test";
+
private String myPackageName;
+
+ // In non gradle projects, test packages belong to a separate module, so their name is equal to
+ // the package name of the module. i.e. myPackageName = myTestPackageName.
+ // In gradle projects, tests are part of the same module, and their package name is either specified
+ // in build.gradle or generated automatically by Android Gradle plugin
+ private String myTestPackageName;
+
private String myTargetPackageName;
private final AndroidFacet myFacet;
private final String myCommandLine;
@@ -299,7 +307,7 @@
AndroidFacet depFacet = AndroidFacet.getInstance(depModule);
if (depFacet != null &&
!module2PackageName.containsKey(depFacet) &&
- !depFacet.getProperties().LIBRARY_PROJECT) {
+ !depFacet.isLibraryProject()) {
String packageName = computePackageName(depFacet);
if (packageName == null) {
return false;
@@ -337,6 +345,10 @@
return myPackageName;
}
+ public String getTestPackageName() {
+ return myTestPackageName;
+ }
+
public Module getModule() {
return myFacet.getModule();
}
@@ -431,7 +443,7 @@
}
@Nullable
- private IDevice[] chooseDevicesAutomaticaly() {
+ private IDevice[] chooseDevicesAutomatically() {
final List<IDevice> compatibleDevices = getAllCompatibleDevices();
if (compatibleDevices.size() == 0) {
@@ -530,12 +542,16 @@
void start(boolean chooseTargetDevice) {
LocalFileSystem.getInstance().refresh(false);
- myPackageName = computePackageName(myFacet);
+ myPackageName = computePackageName(myFacet);
if (myPackageName == null) {
getProcessHandler().destroyProcess();
return;
}
+
+ myPackageName = getPackageNameFromGradle(myPackageName, myFacet);
+ myTestPackageName = computeTestPackageName(myFacet, myPackageName);
+
myTargetPackageName = myPackageName;
final HashMap<AndroidFacet, String> depFacet2PackageName = new HashMap<AndroidFacet, String>();
@@ -582,7 +598,7 @@
}
private boolean chooseOrLaunchDevice() {
- IDevice[] targetDevices = chooseDevicesAutomaticaly();
+ IDevice[] targetDevices = chooseDevicesAutomatically();
if (targetDevices == null) {
message("Canceled", STDERR);
return false;
@@ -776,13 +792,36 @@
clearLogcatAndConsole(getModule().getProject(), device);
}
- myPackageName = getCorrectPackageName(myPackageName, myFacet);
message("Target device: " + getDevicePresentableName(device), STDOUT);
try {
if (myDeploy) {
if (!checkPackageNames()) return false;
- if (!uploadAndInstall(device, myPackageName, myFacet)) return false;
- if (!uploadAndInstallDependentModules(device)) return false;
+ IdeaAndroidProject ideaAndroidProject = myFacet.getIdeaAndroidProject();
+ if (ideaAndroidProject == null) {
+ if (!uploadAndInstall(device, myPackageName, myFacet)) return false;
+ if (!uploadAndInstallDependentModules(device)) return false;
+ } else {
+ Variant selectedVariant = ideaAndroidProject.getSelectedVariant();
+
+ // install apk (note that variant.getOutputFile() will point to a .aar in the case of a library)
+ if (!ideaAndroidProject.getDelegate().isLibrary()) {
+ File apk = selectedVariant.getMainArtifactInfo().getOutputFile();
+ if (!uploadAndInstallApk(device, myPackageName, apk.getAbsolutePath())) {
+ return false;
+ }
+ }
+
+ // install test apk
+ if (getConfiguration() instanceof AndroidTestRunConfiguration) {
+ ArtifactInfo testArtifactInfo = selectedVariant.getTestArtifactInfo();
+ if (testArtifactInfo != null) {
+ File testApk = testArtifactInfo.getOutputFile();
+ if (!uploadAndInstallApk(device, myTestPackageName, testApk.getAbsolutePath())) {
+ return false;
+ }
+ }
+ }
+ }
myApplicationDeployed = true;
}
final AndroidApplicationLauncher.LaunchResult launchResult =
@@ -942,7 +981,7 @@
throws IOException, AdbCommandRejectedException, TimeoutException {
for (AndroidFacet depFacet : myAdditionalFacet2PackageName.keySet()) {
String packageName = myAdditionalFacet2PackageName.get(depFacet);
- packageName = getCorrectPackageName(packageName, depFacet);
+ packageName = getPackageNameFromGradle(packageName, depFacet);
if (!uploadAndInstall(device, packageName, depFacet)) {
return false;
}
@@ -950,36 +989,60 @@
return true;
}
- @NotNull
- private static String getCorrectPackageName(@NotNull String packageName, @NotNull AndroidFacet facet) {
+ private static String computeTestPackageName(AndroidFacet facet, String packageName) {
IdeaAndroidProject ideaAndroidProject = facet.getIdeaAndroidProject();
- if (ideaAndroidProject != null) {
- Variant selectedVariant = ideaAndroidProject.getSelectedVariant();
- ProductFlavor flavor = selectedVariant.getMergedFlavor();
- String correctPackageName = flavor.getPackageName();
- if (correctPackageName == null) {
- correctPackageName = packageName;
- }
- String buildTypeName = selectedVariant.getBuildType();
- AndroidProject delegate = ideaAndroidProject.getDelegate();
- BuildTypeContainer buildTypeContainer = delegate.getBuildTypes().get(buildTypeName);
- String packageNameSuffix = buildTypeContainer.getBuildType().getPackageNameSuffix();
- if (packageNameSuffix != null) {
- correctPackageName = correctPackageName + packageNameSuffix;
- }
- return correctPackageName;
+ if (ideaAndroidProject == null) {
+ return packageName;
+ }
+
+ // In the case of Gradle projects, either the merged flavor provides a test package name,
+ // or we just append ".test" to the source package name
+ Variant selectedVariant = ideaAndroidProject.getSelectedVariant();
+ String testPackageName = selectedVariant.getMergedFlavor().getTestPackageName();
+ return (testPackageName != null) ? testPackageName : packageName + DEFAULT_TEST_PACKAGE_SUFFIX;
+ }
+
+ @NotNull
+ private static String getPackageNameFromGradle(@NotNull String packageNameInManifest, @NotNull AndroidFacet facet) {
+ IdeaAndroidProject ideaAndroidProject = facet.getIdeaAndroidProject();
+ if (ideaAndroidProject == null) {
+ return packageNameInManifest;
+ }
+
+ Variant selectedVariant = ideaAndroidProject.getSelectedVariant();
+ ProductFlavor flavor = selectedVariant.getMergedFlavor();
+
+ // if the current variant specifies a package name, use that
+ String packageName = flavor.getPackageName();
+ if (packageName == null) {
+ // otherwise default to whatever was in the manifest
+ packageName = packageNameInManifest;
+ }
+
+ String buildTypeName = selectedVariant.getBuildType();
+ AndroidProject delegate = ideaAndroidProject.getDelegate();
+ BuildTypeContainer buildTypeContainer = delegate.getBuildTypes().get(buildTypeName);
+ String packageNameSuffix = buildTypeContainer.getBuildType().getPackageNameSuffix();
+ // append the build type suffix to package name if necessary
+ if (packageNameSuffix != null) {
+ packageName += packageNameSuffix;
}
return packageName;
}
private boolean uploadAndInstall(@NotNull IDevice device, @NotNull String packageName, AndroidFacet facet)
throws IOException, AdbCommandRejectedException, TimeoutException {
- String remotePath = "/data/local/tmp/" + packageName;
String localPath = AndroidRootUtil.getApkPath(facet);
if (localPath == null) {
message("ERROR: APK path is not specified for module \"" + facet.getModule().getName() + '"', STDERR);
return false;
}
+ return uploadAndInstallApk(device, packageName, localPath);
+ }
+
+ private boolean uploadAndInstallApk(@NotNull IDevice device, @NotNull String packageName, @NotNull String localPath)
+ throws IOException, AdbCommandRejectedException, TimeoutException {
+ String remotePath = "/data/local/tmp/" + packageName;
if (!uploadApp(device, remotePath, localPath)) return false;
if (!installApp(device, remotePath, packageName)) return false;
return true;
diff --git a/android/src/org/jetbrains/android/run/testing/AndroidTestRunConfiguration.java b/android/src/org/jetbrains/android/run/testing/AndroidTestRunConfiguration.java
index 0a6bd35..b45258b 100644
--- a/android/src/org/jetbrains/android/run/testing/AndroidTestRunConfiguration.java
+++ b/android/src/org/jetbrains/android/run/testing/AndroidTestRunConfiguration.java
@@ -16,11 +16,14 @@
package org.jetbrains.android.run.testing;
+import com.android.builder.model.ArtifactInfo;
import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.TimeoutException;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.tools.idea.gradle.IdeaAndroidProject;
+import com.google.common.base.Predicate;
import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.execution.*;
import com.intellij.execution.configurations.*;
@@ -36,7 +39,9 @@
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.util.Pair;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiMethod;
@@ -56,11 +61,9 @@
import java.io.IOException;
/**
- * Created by IntelliJ IDEA.
* User: Eugene.Kudelevsky
* Date: Aug 27, 2009
* Time: 2:23:56 PM
- * To change this template use File | Settings | File Templates.
*/
public class AndroidTestRunConfiguration extends AndroidRunConfigurationBase {
private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.run.testing.AndroidTestRunConfiguration");
@@ -82,6 +85,65 @@
}
@Override
+ protected Pair<Boolean, String> supportsRunningLibraryProjects(@NotNull AndroidFacet facet) {
+ if (!facet.isGradleProject()) {
+ // Non Gradle projects always require an application
+ return new Pair<Boolean, String>(Boolean.FALSE, AndroidBundle.message("android.cannot.run.library.project.error"));
+ }
+
+ final IdeaAndroidProject project = facet.getIdeaAndroidProject();
+ if (project == null) {
+ return new Pair<Boolean, String>(Boolean.FALSE, AndroidBundle.message("android.cannot.run.library.project.error"));
+ }
+
+ // Gradle only supports testing against a single build type (which could be anything, but is "debug" build type by default)
+ // Currently, the only information the model exports that we can use to detect whether the current build type
+ // is testable is by looking at the test task name and checking whether it is null.
+ ArtifactInfo testArtifactInfo = project.getSelectedVariant().getTestArtifactInfo();
+ String testTask = testArtifactInfo != null ? testArtifactInfo.getAssembleTaskName() : null;
+ return new Pair<Boolean, String>(testTask != null, AndroidBundle.message("android.cannot.run.library.project.in.this.buildtype"));
+ }
+
+ @Override
+ public boolean isGeneratedName() {
+ final String name = getName();
+
+ if ((TESTING_TYPE == TEST_CLASS || TESTING_TYPE == TEST_METHOD) &&
+ (CLASS_NAME == null || CLASS_NAME.length() == 0)) {
+ return JavaExecutionUtil.isNewName(name);
+ }
+ if (TESTING_TYPE == TEST_METHOD &&
+ (METHOD_NAME == null || METHOD_NAME.length() == 0)) {
+ return JavaExecutionUtil.isNewName(name);
+ }
+ return Comparing.equal(name, getGeneratedName());
+ }
+
+ @Nullable
+ @Override
+ public String getGeneratedName() {
+ final JavaRunConfigurationModule confModule = getConfigurationModule();
+ final String moduleName = confModule.getModuleName();
+
+ if (TESTING_TYPE == TEST_ALL_IN_PACKAGE) {
+ if (PACKAGE_NAME.length() == 0) {
+ return ExecutionBundle.message("default.junit.config.name.all.in.module", moduleName);
+ }
+ if (moduleName.length() > 0) {
+ return ExecutionBundle.message("default.junit.config.name.all.in.package.in.module", PACKAGE_NAME, moduleName);
+ }
+ return PACKAGE_NAME + " in " + moduleName;
+ }
+ else if (TESTING_TYPE == TEST_CLASS) {
+ return JavaExecutionUtil.getPresentableClassName(CLASS_NAME, confModule);
+ }
+ else if (TESTING_TYPE == TEST_METHOD) {
+ return JavaExecutionUtil.getPresentableClassName(CLASS_NAME, confModule) + "." + METHOD_NAME;
+ }
+ return moduleName;
+ }
+
+ @Override
public String suggestedName() {
if (TESTING_TYPE == TEST_ALL_IN_PACKAGE) {
return ExecutionBundle.message("test.in.scope.presentable.text", PACKAGE_NAME);
@@ -134,8 +196,8 @@
final AndroidFacet facet = state.getFacet();
final AndroidFacetConfiguration configuration = facet.getConfiguration();
-
- if (!configuration.getState().PACK_TEST_CODE) {
+
+ if (!facet.isGradleProject() && !configuration.getState().PACK_TEST_CODE) {
final Module module = facet.getModule();
final int count = getTestSourceRootCount(module);
@@ -201,7 +263,12 @@
public SettingsEditor<? extends RunConfiguration> getConfigurationEditor() {
Project project = getProject();
AndroidRunConfigurationEditor<AndroidTestRunConfiguration> editor =
- new AndroidRunConfigurationEditor<AndroidTestRunConfiguration>(project);
+ new AndroidRunConfigurationEditor<AndroidTestRunConfiguration>(project, new Predicate<AndroidFacet>() {
+ @Override
+ public boolean apply(@Nullable AndroidFacet facet) {
+ return facet != null && supportsRunningLibraryProjects(facet).getFirst();
+ }
+ });
editor.setConfigurationSpecificEditor(new TestRunParameters(project, editor.getModuleSelector()));
return editor;
}
@@ -259,7 +326,7 @@
public LaunchResult launch(@NotNull AndroidRunningState state, @NotNull IDevice device)
throws IOException, AdbCommandRejectedException, TimeoutException {
state.getProcessHandler().notifyTextAvailable("Running tests\n", ProcessOutputTypes.STDOUT);
- RemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(state.getPackageName(), myInstrumentationTestRunner, device);
+ RemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(state.getTestPackageName(), myInstrumentationTestRunner, device);
switch (TESTING_TYPE) {
case TEST_ALL_IN_PACKAGE:
runner.setTestPackageName(PACKAGE_NAME);
diff --git a/android/src/org/jetbrains/android/run/testing/AndroidTestRunConfigurationType.java b/android/src/org/jetbrains/android/run/testing/AndroidTestRunConfigurationType.java
index 94c9030..eb5b680 100644
--- a/android/src/org/jetbrains/android/run/testing/AndroidTestRunConfigurationType.java
+++ b/android/src/org/jetbrains/android/run/testing/AndroidTestRunConfigurationType.java
@@ -25,6 +25,7 @@
import com.intellij.ui.LayeredIcon;
import icons.AndroidIcons;
import org.jetbrains.android.util.AndroidBundle;
+import org.jetbrains.android.util.AndroidCommonUtils;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
@@ -80,7 +81,7 @@
@Override
@NotNull
public String getId() {
- return "AndroidTestRunConfigurationType";
+ return AndroidCommonUtils.ANDROID_TEST_RUN_CONFIGURATION_TYPE;
}
@Override
diff --git a/android/src/org/jetbrains/android/sdk/AndroidSdkType.java b/android/src/org/jetbrains/android/sdk/AndroidSdkType.java
index affdcd4..68c8e4b 100644
--- a/android/src/org/jetbrains/android/sdk/AndroidSdkType.java
+++ b/android/src/org/jetbrains/android/sdk/AndroidSdkType.java
@@ -18,11 +18,11 @@
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.tools.idea.sdk.Jdks;
-import com.android.utils.NullLogger;
import com.intellij.CommonBundle;
import com.intellij.openapi.projectRoots.*;
import com.intellij.openapi.projectRoots.impl.JavaDependentSdkType;
import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.Pair;
import icons.AndroidIcons;
import org.jdom.Element;
import org.jetbrains.android.util.AndroidBundle;
@@ -79,7 +79,21 @@
@Override
public boolean isValidSdkHome(String path) {
- return AndroidCommonUtils.createSdkManager(path, NullLogger.getLogger()) != null;
+ return validateAndroidSdk(path).getFirst();
+ }
+
+ public static Pair<Boolean,String> validateAndroidSdk(@Nullable String path) {
+ if (path == null) {
+ return Pair.create(Boolean.FALSE, "");
+ }
+
+ MessageBuildingSdkLog logger = new MessageBuildingSdkLog();
+ if (AndroidCommonUtils.createSdkManager(path, logger) != null) {
+ //noinspection ConstantConditions
+ return Pair.create(Boolean.TRUE, null);
+ } else {
+ return Pair.create(Boolean.FALSE, logger.getErrorMessage());
+ }
}
@Override
diff --git a/android/src/org/jetbrains/android/sdk/AndroidSdkUtils.java b/android/src/org/jetbrains/android/sdk/AndroidSdkUtils.java
index 94fa280..2523f85 100644
--- a/android/src/org/jetbrains/android/sdk/AndroidSdkUtils.java
+++ b/android/src/org/jetbrains/android/sdk/AndroidSdkUtils.java
@@ -26,6 +26,7 @@
import com.android.sdklib.SdkManager;
import com.android.tools.idea.sdk.Jdks;
import com.android.tools.idea.sdk.SelectSdkDialog;
+import com.android.tools.idea.sdk.VersionCheck;
import com.android.tools.idea.startup.ExternalAnnotationsSupport;
import com.android.utils.ILogger;
import com.android.utils.NullLogger;
@@ -75,7 +76,6 @@
import org.jetbrains.android.util.AndroidBundle;
import org.jetbrains.android.util.AndroidCommonUtils;
import org.jetbrains.android.util.AndroidUtils;
-import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -88,7 +88,6 @@
private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.sdk.AndroidSdkUtils");
public static final String DEFAULT_PLATFORM_NAME_PROPERTY = "AndroidPlatformName";
- @NonNls public static final String ANDROID_HOME_ENV = "ANDROID_HOME";
private static SdkManager ourSdkManager;
@@ -248,7 +247,6 @@
if (target != null) {
Sdk sdk = createNewAndroidPlatform(target, sdkPath, chooseNameForNewLibrary(target), jdk, true);
if (sdk != null) {
- // TODO check version
return sdk;
}
}
@@ -300,7 +298,15 @@
return createNewAndroidPlatform(target, sdkPath, sdkName, jdk, addRoots);
}
- private static Sdk createNewAndroidPlatform(IAndroidTarget target, String sdkPath, String sdkName, Sdk javaSdk, boolean addRoots) {
+ @Nullable
+ private static Sdk createNewAndroidPlatform(@NotNull IAndroidTarget target,
+ @NotNull String sdkPath,
+ @NotNull String sdkName,
+ @Nullable Sdk jdk,
+ boolean addRoots) {
+ if (!VersionCheck.isCompatibleVersion(sdkPath)) {
+ return null;
+ }
ProjectJdkTable table = ProjectJdkTable.getInstance();
String tmpName = SdkConfigurationUtil.createUniqueSdkName(AndroidSdkType.SDK_NAME, Arrays.asList(table.getAllJdks()));
@@ -310,7 +316,7 @@
sdkModificator.setHomePath(sdkPath);
sdkModificator.commitChanges();
- setUpSdk(sdk, sdkName, table.getAllJdks(), target, javaSdk, addRoots);
+ setUpSdk(sdk, sdkName, table.getAllJdks(), target, jdk, addRoots);
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
@@ -444,18 +450,25 @@
AndroidPlatform androidPlatform = data.getAndroidPlatform();
if (androidPlatform != null) {
String baseDir = FileUtil.toSystemIndependentName(androidPlatform.getSdkData().getLocation());
+ boolean compatibleVersion = VersionCheck.isCompatibleVersion(baseDir);
boolean matchingHashString = targetHashString.equals(androidPlatform.getTarget().hashString());
- boolean suitable = matchingHashString && checkSdkRoots(sdk, androidPlatform.getTarget(), false);
+ boolean suitable = compatibleVersion && matchingHashString && checkSdkRoots(sdk, androidPlatform.getTarget(), false);
if (sdkPath != null && FileUtil.pathsEqual(baseDir, sdkPath)) {
if (suitable) {
return sdk;
}
if (promptUserIfNecessary) {
+ if (!compatibleVersion) {
+ // Old SDK, needs to be replaced.
+ Sdk jdk = Jdks.chooseOrCreateJavaSdk();
+ String jdkPath = jdk == null ? null : jdk.getHomePath();
+ return promptUserForSdkCreation(androidPlatform.getTarget(), null, jdkPath);
+ }
+
if (!matchingHashString) {
// This is the specified SDK (usually in local.properties file.) We try our best to fix it.
// TODO download platform;
}
- // TODO: check if it is an old SDK. If so, it needs to be replaced.
}
}
else if (suitable) {
@@ -496,23 +509,27 @@
sdkPath = FileUtil.toSystemIndependentName(sdkPath);
}
+ //noinspection TestOnlyProblems
Sdk sdk = findSuitableAndroidSdk(targetHashString, sdkPath, promptUserIfNecessary);
if (sdk != null) {
ModuleRootModificationUtil.setModuleSdk(module, sdk);
return true;
}
+ //noinspection TestOnlyProblems
if (sdkPath != null && tryToCreateAndSetAndroidSdk(module, sdkPath, targetHashString, promptUserIfNecessary)) {
return true;
}
- String androidHomeValue = System.getenv(ANDROID_HOME_ENV);
+ String androidHomeValue = System.getenv(SdkConstants.ANDROID_HOME_ENV);
+ //noinspection TestOnlyProblems
if (androidHomeValue != null &&
tryToCreateAndSetAndroidSdk(module, FileUtil.toSystemIndependentName(androidHomeValue), targetHashString, false)) {
return true;
}
for (String dir : getAndroidSdkPathsFromExistingPlatforms()) {
+ //noinspection TestOnlyProblems
if (tryToCreateAndSetAndroidSdk(module, dir, targetHashString, false)) {
return true;
}
@@ -544,8 +561,16 @@
}
}
else if (promptUserIfNecessary) {
- // There is not a matching target hashString.
- // TODO download platform and try again.
+ // We got here because we couldn't get target for given SDK path. Most likely it is an old SDK.
+ String pathToShow = VersionCheck.isCompatibleVersion(sdkPath) ? sdkPath : null;
+ Sdk jdk = Jdks.chooseOrCreateJavaSdk();
+ String jdkPath = jdk == null ? null : jdk.getHomePath();
+ Sdk androidSdk = promptUserForSdkCreation(null, pathToShow, jdkPath);
+ // TODO check platform
+ if (androidSdk != null) {
+ ModuleRootModificationUtil.setModuleSdk(module, androidSdk);
+ return true;
+ }
}
}
return false;
diff --git a/android/src/org/jetbrains/android/spellchecker/android.dic b/android/src/org/jetbrains/android/spellchecker/android.dic
index fcdfb87..5080f17 100644
--- a/android/src/org/jetbrains/android/spellchecker/android.dic
+++ b/android/src/org/jetbrains/android/spellchecker/android.dic
@@ -1,14 +1,56 @@
actionbar
anim
+antialias
+appcompat
+appwidget
attrs
+bluetooth
+borderless
+checkable
+clickable
+codec
+datagram
dimen
drawables
+dropdown
+ellipsize
+focusability
focusable
+framebuffer
+fullscreen
+geolocation
+gradle
+gridlayout
+haptic
holo
+iconified
inflater
+inputmethod
+interpolator
+intra
+keycode
+keyguard
+mipmap
+monospace
+multiline
+nanos
+nullable
+oneshot
+overscan
parcelable
parcelables
+prefs
+pressable
proguard
+renderscript
+ringtone
+scrollbar
+scrollbars
+spannable
styleable
styleables
-viewpager
\ No newline at end of file
+syncable
+unregister
+viewpager
+viewport
+xlarge
diff --git a/android/src/org/jetbrains/android/uipreview/AndroidLayoutPreviewPanel.java b/android/src/org/jetbrains/android/uipreview/AndroidLayoutPreviewPanel.java
index 1abac7d..4b060bf 100644
--- a/android/src/org/jetbrains/android/uipreview/AndroidLayoutPreviewPanel.java
+++ b/android/src/org/jetbrains/android/uipreview/AndroidLayoutPreviewPanel.java
@@ -15,6 +15,7 @@
*/
package org.jetbrains.android.uipreview;
+import com.android.ide.common.rendering.api.SessionParams;
import com.android.tools.idea.configurations.RenderContext;
import com.android.tools.idea.rendering.*;
import com.android.tools.idea.rendering.multi.RenderPreviewManager;
@@ -341,7 +342,8 @@
myRenderResult = renderResult;
ScalableImage image = myRenderResult.getImage();
if (image != null) {
- image.setDeviceFrameEnabled(myShowDeviceFrames);
+ image.setDeviceFrameEnabled(myShowDeviceFrames && myRenderResult.getRenderService() != null &&
+ myRenderResult.getRenderService().getRenderingMode() == SessionParams.RenderingMode.NORMAL);
if (myPreviewManager != null && RenderPreviewMode.getCurrent() != RenderPreviewMode.NONE) {
Dimension fixedRenderSize = myPreviewManager.getFixedRenderSize();
if (fixedRenderSize != null) {
@@ -378,6 +380,10 @@
}
}
+ RenderResult getRenderResult() {
+ return myRenderResult;
+ }
+
private void setEditor(@Nullable TextEditor editor) {
if (editor != myEditor) {
myEditor = editor;
diff --git a/android/src/org/jetbrains/android/uipreview/AndroidLayoutPreviewToolWindowForm.java b/android/src/org/jetbrains/android/uipreview/AndroidLayoutPreviewToolWindowForm.java
index fb1c4cc..83b4b22 100644
--- a/android/src/org/jetbrains/android/uipreview/AndroidLayoutPreviewToolWindowForm.java
+++ b/android/src/org/jetbrains/android/uipreview/AndroidLayoutPreviewToolWindowForm.java
@@ -17,11 +17,10 @@
import com.android.tools.idea.configurations.*;
-import com.android.tools.idea.gradle.IdeaAndroidProject;
-import com.android.tools.idea.gradle.variant.view.BuildVariantView;
-import com.android.tools.idea.rendering.ProjectResources;
-import com.android.tools.idea.rendering.multi.RenderPreviewManager;
import com.android.tools.idea.rendering.RenderResult;
+import com.android.tools.idea.rendering.SaveScreenshotAction;
+import com.android.tools.idea.rendering.ScalableImage;
+import com.android.tools.idea.rendering.multi.RenderPreviewManager;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
@@ -37,7 +36,7 @@
import com.intellij.ui.components.JBScrollPane;
import icons.AndroidIcons;
import org.jetbrains.android.facet.AndroidFacet;
-import org.jetbrains.android.sdk.AndroidPlatform;
+import org.jetbrains.android.facet.ResourceFolderManager;
import org.jetbrains.android.util.AndroidBundle;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -46,21 +45,22 @@
import java.awt.*;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
-
-import static com.android.tools.idea.gradle.variant.view.BuildVariantView.BuildVariantSelectionChangeListener;
+import java.awt.image.BufferedImage;
+import java.util.Collection;
+import java.util.List;
/**
* @author Eugene.Kudelevsky
*/
public class AndroidLayoutPreviewToolWindowForm implements Disposable, ConfigurationListener, RenderContext,
- BuildVariantSelectionChangeListener {
+ ResourceFolderManager.ResourceFolderListener {
private JPanel myContentPanel;
private AndroidLayoutPreviewPanel myPreviewPanel;
private JBScrollPane myScrollPane;
private JPanel myComboPanel;
private PsiFile myFile;
private Configuration myConfiguration;
- private BuildVariantView myVariantView;
+ private AndroidFacet myFacet;
private final AndroidLayoutPreviewToolWindowManager myToolWindowManager;
private final ActionToolbar myActionToolBar;
private final AndroidLayoutPreviewToolWindowSettings mySettings;
@@ -79,6 +79,7 @@
actionGroup.add(new ZoomOutAction());
actionGroup.addSeparator();
actionGroup.add(new RefreshAction());
+ actionGroup.add(new SaveScreenshotAction(this));
myActionToolBar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, actionGroup, true);
myActionToolBar.setReservePlaceAutoPopupIcon(false);
@@ -193,57 +194,21 @@
myConfiguration = null;
}
- if (myVariantView != null) {
- myVariantView.removeListener(this);
- myVariantView = null;
+ if (myFacet != null) {
+ myFacet.getResourceFolderManager().removeListener(this);
}
if (file != null) {
final VirtualFile virtualFile = file.getVirtualFile();
if (virtualFile != null) {
- final AndroidFacet facet = AndroidFacet.getInstance(file);
- if (facet != null) {
- if (facet.isGradleProject() && facet.getIdeaAndroidProject() == null) {
- facet.addListener(new AndroidFacet.GradleProjectAvailableListener() {
- @Override
- public void gradleProjectAvailable(@NotNull IdeaAndroidProject project) {
- facet.removeListener(this);
- ConfigurationManager manager = facet.getConfigurationManager();
- myConfiguration = manager.getConfiguration(virtualFile);
- myConfiguration.addListener(AndroidLayoutPreviewToolWindowForm.this);
- myToolWindowManager.finishSetFile();
- if (facet.isGradleProject()) {
- myVariantView = BuildVariantView.getInstance(facet.getModule().getProject());
- if (myVariantView != null) {
- // Ensure that the project resources have been initialized first, since
- // we want it to add its own variant listeners before ours (such that
- // when the variant changes, the project resources get notified and updated
- // before our own update listener attempts a re-render)
- facet.getProjectResources(false /*libraries*/, true /*createIfNecessary*/);
-
- myVariantView.removeListener(AndroidLayoutPreviewToolWindowForm.this);
- myVariantView.addListener(AndroidLayoutPreviewToolWindowForm.this);
- }
- }
- }
- });
- // Couldn't initialize: finish later (in project listener will call finishSetFile
- return false;
- } else {
- ConfigurationManager manager = facet.getConfigurationManager();
- myConfiguration = manager.getConfiguration(virtualFile);
- myConfiguration.removeListener(this);
- myConfiguration.addListener(this);
- }
-
- if (facet.isGradleProject()) {
- myVariantView = BuildVariantView.getInstance(facet.getModule().getProject());
- if (myVariantView != null) {
- facet.getProjectResources(false /*libraries*/, true /*createIfNecessary*/);
- myVariantView.removeListener(this);
- myVariantView.addListener(this);
- }
- }
+ myFacet = AndroidFacet.getInstance(file);
+ if (myFacet != null) {
+ myFacet.getResourceFolderManager().removeListener(this);
+ myFacet.getResourceFolderManager().addListener(this);
+ ConfigurationManager manager = myFacet.getConfigurationManager();
+ myConfiguration = manager.getConfiguration(virtualFile);
+ myConfiguration.removeListener(this);
+ myConfiguration.addListener(this);
}
}
}
@@ -275,10 +240,6 @@
myPreviewPanel.update();
}
- public void updateDevicesAndTargets(@Nullable AndroidPlatform platform) {
- // TODO: When is this called? How do I update my configuration?
- }
-
// ---- Implements RenderContext ----
@Override
@@ -360,6 +321,19 @@
myPreviewPanel.setDeviceFramesEnabled(on);
}
+ @Nullable
+ @Override
+ public BufferedImage getRenderedImage() {
+ RenderResult result = myPreviewPanel.getRenderResult();
+ if (result != null) {
+ ScalableImage scalableImage = result.getImage();
+ if (scalableImage != null) {
+ return scalableImage.getOriginalImage();
+ }
+ }
+ return null;
+ }
+
@Override
@NotNull
public Dimension getFullImageSize() {
@@ -416,10 +390,13 @@
return true;
}
- // ---- Implements BuildVariantSelectionChangeListener ----
+ // ---- Implements ResourceFolderManager.ResourceFolderListener ----
@Override
- public void buildVariantSelected(@NotNull AndroidFacet facet) {
+ public void resourceFoldersChanged(@NotNull AndroidFacet facet,
+ @NotNull List<VirtualFile> folders,
+ @NotNull Collection<VirtualFile> added,
+ @NotNull Collection<VirtualFile> removed) {
// The project resources should already have been refreshed by their own variant listener
myToolWindowManager.render();
}
@@ -484,6 +461,10 @@
@Override
public void actionPerformed(AnActionEvent e) {
+ Configuration configuration = getConfiguration();
+ if (configuration != null) {
+ configuration.updated(ConfigurationListener.MASK_RENDERING);
+ }
myToolWindowManager.render();
}
}
diff --git a/android/src/org/jetbrains/android/uipreview/AndroidLayoutPreviewToolWindowManager.java b/android/src/org/jetbrains/android/uipreview/AndroidLayoutPreviewToolWindowManager.java
index c671c15..3625754 100644
--- a/android/src/org/jetbrains/android/uipreview/AndroidLayoutPreviewToolWindowManager.java
+++ b/android/src/org/jetbrains/android/uipreview/AndroidLayoutPreviewToolWindowManager.java
@@ -84,7 +84,7 @@
private final Object myRenderingQueueLock = new Object();
private MergingUpdateQueue myRenderingQueue;
- private final MergingUpdateQueue mySaveAndRenderQueue;
+ private final MergingUpdateQueue myRenderQueue;
private final Project myProject;
private final FileEditorManager myFileEditorManager;
@@ -102,7 +102,7 @@
myFileEditorManager = fileEditorManager;
myToolWindowUpdateQueue = new MergingUpdateQueue("android.layout.preview", 300, true, null, project);
- mySaveAndRenderQueue = new MergingUpdateQueue("android.layout.preview.save.and.render", 1000, true, null, project, null, true);
+ myRenderQueue = new MergingUpdateQueue("android.layout.preview.save.and.render", 1000, true, null, project, null, true);
final MessageBusConnection connection = project.getMessageBus().connect(project);
connection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, new MyFileEditorManagerListener());
@@ -193,13 +193,12 @@
if (vFile != null) {
final VirtualFile finalVFile = vFile;
- mySaveAndRenderQueue.queue(new Update("saveAndRender") {
+ myRenderQueue.queue(new Update("saveAndRender") {
@Override
public void run() {
final VirtualFile[] resDirs = facet.getLocalResourceManager().getAllResourceDirs();
if (ArrayUtil.find(resDirs, finalVFile) >= 0) {
- ApplicationManager.getApplication().saveAll();
render();
}
}
@@ -318,7 +317,6 @@
final boolean toRender = myToolWindowForm.getFile() != psiFile;
if (toRender) {
- ApplicationManager.getApplication().saveAll();
if (!myToolWindowForm.setFile(psiFile)) {
return;
}
@@ -411,8 +409,12 @@
service.getResourceResolver();
result = ApplicationManager.getApplication().runReadAction(new Computable<RenderResult>() {
+ @Nullable
@Override
public RenderResult compute() {
+ if (psiFile instanceof XmlFile) {
+ service.useDesignMode(((XmlFile)psiFile).getRootTag());
+ }
return service.render();
}
});
diff --git a/android/src/org/jetbrains/android/uipreview/ProjectClassLoader.java b/android/src/org/jetbrains/android/uipreview/ProjectClassLoader.java
index 413069e..9e0a405 100644
--- a/android/src/org/jetbrains/android/uipreview/ProjectClassLoader.java
+++ b/android/src/org/jetbrains/android/uipreview/ProjectClassLoader.java
@@ -1,6 +1,7 @@
package org.jetbrains.android.uipreview;
-import com.android.build.gradle.model.Variant;
+import com.android.builder.model.ArtifactInfo;
+import com.android.builder.model.Variant;
import com.android.sdklib.IAndroidTarget;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.intellij.openapi.diagnostic.Logger;
@@ -107,13 +108,14 @@
if (gradleProject != null) {
Variant variant = gradleProject.getSelectedVariant();
String variantName = variant.getName();
- File classesFolder = variant.getClassesFolder();
+ ArtifactInfo mainArtifactInfo = variant.getMainArtifactInfo();
+ File classesFolder = mainArtifactInfo.getClassesFolder();
// Older models may not supply it; in that case, we rely on looking relative
// to the .APK file location:
//noinspection ConstantConditions
if (classesFolder == null) {
- File file = variant.getOutputFile();
+ File file = mainArtifactInfo.getOutputFile();
File buildFolder = file.getParentFile().getParentFile();
classesFolder = new File(buildFolder, "classes"); // See AndroidContentRoot
}
diff --git a/android/src/org/jetbrains/android/uipreview/ViewLoader.java b/android/src/org/jetbrains/android/uipreview/ViewLoader.java
index 3257cfd..8fb023b 100644
--- a/android/src/org/jetbrains/android/uipreview/ViewLoader.java
+++ b/android/src/org/jetbrains/android/uipreview/ViewLoader.java
@@ -429,8 +429,8 @@
final Map<IntArrayWrapper, String> styleableId2res = new HashMap<IntArrayWrapper, String>();
if (parseClass(aClass, id2res, styleableId2res, res2id)) {
- ProjectResources mainProjectResources = ProjectResources.get(myModule, false);
- mainProjectResources.setCompiledResources(id2res, styleableId2res, res2id);
+ ProjectResources projectResources = ProjectResources.get(myModule, true);
+ projectResources.setCompiledResources(id2res, styleableId2res, res2id);
}
}
}
@@ -462,7 +462,7 @@
for (Field field : resClass.getDeclaredFields()) {
final int modifiers = field.getModifiers();
- if (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)) {
+ if (Modifier.isStatic(modifiers)) { // May not be final in library projects
final Class<?> type = field.getType();
if (type.isArray() && type.getComponentType() == int.class) {
styleableId2Res.put(new IntArrayWrapper((int[])field.get(null)), field.getName());
diff --git a/android/src/org/jetbrains/android/util/AndroidResourceUtil.java b/android/src/org/jetbrains/android/util/AndroidResourceUtil.java
index ff8cb2c..39dc306 100644
--- a/android/src/org/jetbrains/android/util/AndroidResourceUtil.java
+++ b/android/src/org/jetbrains/android/util/AndroidResourceUtil.java
@@ -902,18 +902,4 @@
public static String getFieldNameByResourceName(@NotNull String fieldName) {
return fieldName.replace('.', '_').replace('-', '_').replace(':', '_');
}
-
- @Nullable
- public static String getInvalidResourceFileNameMessage(@NotNull String fileName) {
- for (int i = 0, n = fileName.length(); i < n; i++) {
- char c = fileName.charAt(i);
- if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '.')) {
- return AndroidBundle.message("invalid.file.resource.name.error");
- }
- }
- if (!StringUtil.isJavaIdentifier(getFieldNameByResourceName(fileName))) {
- return AndroidBundle.message("invalid.file.resource.name.error1");
- }
- return null;
- }
}
diff --git a/android/src/org/jetbrains/android/util/AndroidUtils.java b/android/src/org/jetbrains/android/util/AndroidUtils.java
index 221a088..ff823b2 100644
--- a/android/src/org/jetbrains/android/util/AndroidUtils.java
+++ b/android/src/org/jetbrains/android/util/AndroidUtils.java
@@ -108,6 +108,9 @@
import java.util.*;
import java.util.List;
+import static com.android.SdkConstants.*;
+import static com.android.utils.SdkUtils.endsWithIgnoreCase;
+
/**
* @author yole, coyote
*/
@@ -607,7 +610,7 @@
final List<AndroidFacet> result = new ArrayList<AndroidFacet>();
for (AndroidFacet facet : ProjectFacetManager.getInstance(project).getFacets(AndroidFacet.ID)) {
- if (!facet.getProperties().LIBRARY_PROJECT) {
+ if (!facet.isLibraryProject()) {
result.add(facet);
}
}
@@ -628,7 +631,7 @@
if (depModule != null) {
final AndroidFacet depFacet = AndroidFacet.getInstance(depModule);
- if (depFacet != null && depFacet.getProperties().LIBRARY_PROJECT) {
+ if (depFacet != null && depFacet.isLibraryProject()) {
depFacets.add(depFacet);
}
}
@@ -680,7 +683,7 @@
final AndroidFacet depFacet = AndroidFacet.getInstance(depModule);
if (depFacet != null &&
- (!androidLibrariesOnly || depFacet.getProperties().LIBRARY_PROJECT) &&
+ (!androidLibrariesOnly || depFacet.isLibraryProject()) &&
visited.add(depFacet)) {
collectAllAndroidDependencies(depModule, androidLibrariesOnly, result, visited);
result.add(0, depFacet);
@@ -807,49 +810,13 @@
public static boolean isIdentifier(@NotNull String candidate) {
ApplicationManager.getApplication().assertReadAccessAllowed();
- Lexer lexer = JavaParserDefinition.createLexer(LanguageLevel.JDK_1_3);
+ Lexer lexer = JavaParserDefinition.createLexer(LanguageLevel.JDK_1_5);
lexer.start(candidate);
if (lexer.getTokenType() != JavaTokenType.IDENTIFIER) return false;
lexer.advance();
return lexer.getTokenType() == null;
}
- @Nullable
- public static String isValidResourceName(@NotNull String name, boolean isFileType) {
- // Resource names must be valid Java identifiers, since they will
- // be represented as Java identifiers in the R file:
- if (!Character.isJavaIdentifierStart(name.charAt(0))) {
- return "The resource name must begin with a character";
- }
- for (int i = 1, n = name.length(); i < n; i++) {
- char c = name.charAt(i);
- if (!Character.isJavaIdentifierPart(c)) {
- return String.format("'%1$c' is not a valid resource name character", c);
- }
- }
-
- if (isFileType) {
- char first = name.charAt(0);
- if (!(first >= 'a' && first <= 'z')) {
- return String.format(
- "File-based resource names must start with a lowercase letter.");
- }
-
- // AAPT only allows lowercase+digits+_:
- // "%s: Invalid file name: must contain only [a-z0-9_.]","
- for (int i = 0, n = name.length(); i < n; i++) {
- char c = name.charAt(i);
- if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_')) {
- return String.format(
- "File-based resource names must contain only lowercase a-z, 0-9, or _.");
-
-
- }
- }
- }
- return null;
- }
-
public static void reportImportErrorToEventLog(String message, String modName, Project project) {
Notifications.Bus.notify(new Notification(AndroidBundle.message("android.facet.importing.notification.group"),
AndroidBundle.message("android.facet.importing.title", modName),
@@ -857,6 +824,23 @@
LOG.debug(message);
}
+ /**
+ * Returns true if the given file path points to an image file recognized by
+ * Android. See http://developer.android.com/guide/appendix/media-formats.html
+ * for details.
+ *
+ * @param path the filename to be tested
+ * @return true if the file represents an image file
+ */
+ public static boolean hasImageExtension(String path) {
+ return endsWithIgnoreCase(path, DOT_PNG) ||
+ endsWithIgnoreCase(path, DOT_9PNG) ||
+ endsWithIgnoreCase(path, DOT_GIF) ||
+ endsWithIgnoreCase(path, DOT_JPG) ||
+ endsWithIgnoreCase(path, DOT_JPEG) ||
+ endsWithIgnoreCase(path, DOT_BMP);
+ }
+
public static boolean isPackagePrefix(@NotNull String prefix, @NotNull String name) {
return name.equals(prefix) || name.startsWith(prefix + ".");
}
diff --git a/android/testData/apiCheck/Basic.java b/android/testData/apiCheck/Basic.java
index 48d710b..a7b94b7 100644
--- a/android/testData/apiCheck/Basic.java
+++ b/android/testData/apiCheck/Basic.java
@@ -125,22 +125,24 @@
public static class ApiCallTest6 {
public void test(Throwable throwable) {
// IOException(Throwable) requires API 9
- IOException ioException = new IOException(throwable);
+ IOException ioException = <error descr="Call requires API level 9 (current min is 1): java.io.IOException#IOException">new IOException(throwable)</error>;
}
}
@SuppressWarnings("serial")
public static class ApiCallTest7 extends IOException {
public ApiCallTest7(String message, Throwable cause) {
- super(message, cause); // API 9
+ <error descr="Call requires API level 9 (current min is 1): java.io.IOException#IOException">super(message, cause)</error>; // API 9
}
public void fun() throws IOException {
- super.toString(); throw new IOException((Throwable) null); // API 9
+ super.toString(); throw <error descr="Call requires API level 9 (current min is 1): java.io.IOException#IOException">new IOException((Throwable) null)</error>; // API 9
}
}
+ /* Temporarily hidden: We need to have a more recent build target for our unit test platform
public void closeTest(android.database.sqlite.SQLiteDatabase db) throws Exception {
db.close();
}
+ */
}
diff --git a/android/testData/apiCheck/VersionConditional.java b/android/testData/apiCheck/VersionConditional.java
index bd6d564..032f2d4 100644
--- a/android/testData/apiCheck/VersionConditional.java
+++ b/android/testData/apiCheck/VersionConditional.java
@@ -3,46 +3,65 @@
import android.os.Build;
import android.widget.GridLayout;
+import static android.os.Build.VERSION;
import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES;
import static android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH;
@SuppressWarnings("UnusedDeclaration")
public class Class {
- public void test() {
- if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
- new GridLayout(null).getOrientation(); // Not flagged
- } else {
- new GridLayout(null).getOrientation(); // Flagged
- }
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
- new GridLayout(null).getOrientation(); // Not flagged
- } else {
- new GridLayout(null).getOrientation(); // Flagged
- }
-
- if (SDK_INT >= ICE_CREAM_SANDWICH) {
- new GridLayout(null).getOrientation(); // Not flagged
- } else {
- new GridLayout(null).getOrientation(); // Flagged
- }
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
- new GridLayout(null).getOrientation(); // Not flagged
- } else {
- new GridLayout(null).getOrientation(); // Flagged
- }
-
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
- new GridLayout(null).getOrientation(); // Flagged
- } else {
- new GridLayout(null).getOrientation(); // Not flagged
- }
-
- if (Build.VERSION.SDK_INT >= 14) {
- new GridLayout(null).getOrientation(); // Not flagged
- } else {
- new GridLayout(null).getOrientation(); // Flagged
- }
+ public void test(boolean priority) {
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ new GridLayout(null).getOrientation(); // Not flagged
+ } else {
+ <error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#GridLayout">new GridLayout(null)</error>.<error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#getOrientation">getOrientation</error>(); // Flagged
}
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ new GridLayout(null).getOrientation(); // Not flagged
+ } else {
+ <error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#GridLayout">new GridLayout(null)</error>.<error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#getOrientation">getOrientation</error>(); // Flagged
+ }
+
+ if (SDK_INT >= ICE_CREAM_SANDWICH) {
+ new GridLayout(null).getOrientation(); // Not flagged
+ } else {
+ <error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#GridLayout">new GridLayout(null)</error>.<error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#getOrientation">getOrientation</error>(); // Flagged
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ new GridLayout(null).getOrientation(); // Not flagged
+ } else {
+ <error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#GridLayout">new GridLayout(null)</error>.<error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#getOrientation">getOrientation</error>(); // Flagged
+ }
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ <error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#GridLayout">new GridLayout(null)</error>.<error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#getOrientation">getOrientation</error>(); // Flagged
+ } else {
+ new GridLayout(null).getOrientation(); // Not flagged
+ }
+
+ if (Build.VERSION.SDK_INT >= 14) {
+ new GridLayout(null).getOrientation(); // Not flagged
+ } else {
+ <error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#GridLayout">new GridLayout(null)</error>.<error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#getOrientation">getOrientation</error>(); // Flagged
+ }
+
+ if (VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH) {
+ new GridLayout(null).getOrientation(); // Not flagged
+ } else {
+ <error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#GridLayout">new GridLayout(null)</error>.<error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#getOrientation">getOrientation</error>(); // Flagged
+ }
+
+ // Nested conditionals
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ if (priority) {
+ new GridLayout(null).getOrientation(); // Not flagged
+ } else {
+ new GridLayout(null).getOrientation(); // Not flagged
+ }
+ } else {
+ <error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#GridLayout">new GridLayout(null)</error>.<error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#getOrientation">getOrientation</error>(); // Flagged
+ }
+ }
}
diff --git a/android/testData/folding/javaStrings.java b/android/testData/folding/javaStrings.java
index acdd904..ba7d217 100644
--- a/android/testData/folding/javaStrings.java
+++ b/android/testData/folding/javaStrings.java
@@ -26,7 +26,7 @@
String label = <fold text='"Application Name"' expand='false'>c.getString(R.string.app_name)</fold>;
String label2 = <fold text='"This is a really really really long string, and the fol..."' expand='false'>getString(R.string.foobar)</fold>;
String label3 = <fold text='"Vibration level is {10}."' expand='false'>getString(R.string.string_width_formatting, 10)</fold>;
- String label2 = <fold text='getString(R.string.empty)' expand='false'>getString(R.string.empty)</fold>;
+ String label2 = <fold text='""' expand='false'>getString(R.string.empty)</fold>;
String label2 = <fold text='getString(R.string.unknown)' expand='false'>getString(R.string.unknown)</fold>;
}</fold>
}
diff --git a/android/testData/render/layout2.xml b/android/testData/render/layout2.xml
new file mode 100644
index 0000000..77d0e59
--- /dev/null
+++ b/android/testData/render/layout2.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+</RelativeLayout>
diff --git a/android/testData/resNavigation/res/layout/main.nav b/android/testData/resNavigation/res/layout/main.nav
deleted file mode 100644
index b204600..0000000
--- a/android/testData/resNavigation/res/layout/main.nav
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<NavigationModel
- ns = "http://schemas.android.com?import=com.android.navigation.*"
->
- <Transition
- type = ""
- >
- <State
- outer.property = "destination"
- controllerClassName = "account_activity.xml"
- xmlResourceName = "account_activity.xml"
- />
- <State
- outer.property = "source"
- id = "0"
- controllerClassName = "account_activity.xml"
- xmlResourceName = "account_activity.xml"
- />
- </Transition>
- <Transition
- type = ""
- >
- <State
- outer.property = "destination"
- controllerClassName = "conversation_fragment.xml"
- xmlResourceName = "conversation_fragment.xml"
- />
- <State
- outer.property = "source"
- idref = "0"
- />
- </Transition>
-</NavigationModel>
diff --git a/android/testData/resNavigation/res/xml/test.xml b/android/testData/resNavigation/res/xml/test.xml
deleted file mode 100644
index 198537d..0000000
--- a/android/testData/resNavigation/res/xml/test.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<ArrayList
- xmlns="http://schemas.android.com?import=java.util.*;import=com.android.navigation.*">
- <TestClassA
- name="I'm A1"
- value="1"/>
- <TestClassA
- name="I'm A2"
- value="2">
- <TestClassB
- outer.property = "child"
- name="I'm B1"
- value="3"/>
- </TestClassA>
-</ArrayList>
\ No newline at end of file
diff --git a/android/testData/resourceRepository/empty.xml b/android/testData/resourceRepository/empty.xml
new file mode 100644
index 0000000..bf74fed
--- /dev/null
+++ b/android/testData/resourceRepository/empty.xml
@@ -0,0 +1,2 @@
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+</resources>
diff --git a/android/testData/resourceRepository/layout.xml b/android/testData/resourceRepository/layout.xml
new file mode 100644
index 0000000..6a80c03
--- /dev/null
+++ b/android/testData/resourceRepository/layout.xml
@@ -0,0 +1,78 @@
+<?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">
+ <include layout="@layout/colorstrip" android:layout_height="@dimen/colorstrip_height" android:layout_width="match_parent"/>
+
+ <LinearLayout style="@style/TitleBar" android:id="@+id/header">
+ <ImageView style="@style/TitleBarLogo"
+ android:contentDescription="@string/description_logo"
+ android:src="@drawable/title_logo" />
+
+ <View style="@style/TitleBarSpring" />
+
+ <ImageView style="@style/TitleBarSeparator" />
+ <ImageButton style="@style/TitleBarAction"
+ android:id="@+id/btn_title_refresh"
+ android:contentDescription="@string/description_refresh"
+ android:src="@drawable/ic_title_refresh"
+ android:layout_width="wrap_content"
+ android:layout_height="42dp"
+ android:onClick="onRefreshClick" />
+ <ProgressBar style="@style/TitleBarProgressIndicator"
+ android:id="@+id/title_refresh_progress"
+ android:layout_width="wrap_content"
+ android:visibility="visible"/>
+
+ <ImageView style="@style/TitleBarSeparator" />
+ <ImageButton style="@style/TitleBarAction"
+ android:contentDescription="@string/description_search"
+ android:src="@drawable/ic_title_search"
+ android:layout_width="wrap_content"
+ android:layout_height="42dp"
+ android:onClick="onSearchClick" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/noteArea"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_margin="5dip">
+ <EditText
+ android:id="@android:id/text1"
+ android:layout_height="fill_parent"
+ android:hint="@string/note_hint"
+ android:freezesText="true"
+ android:gravity="top" android:layout_width="wrap_content" android:layout_weight="1">
+ </EditText>
+ <EditText
+ android:id="@+id/text2"
+ android:layout_height="fill_parent"
+ android:freezesText="true"
+ android:gravity="top" android:layout_width="wrap_content" android:layout_weight="1">
+ <requestFocus />
+ </EditText>
+ </LinearLayout>
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ style="@android:style/ButtonBar">
+ <Button
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:onClick="onSaveClick"
+ android:text="@string/note_save" />
+ <Button
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:onClick="onDiscardClick"
+ android:text="@string/note_discard" />
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/android/testData/resourceRepository/layout2.xml b/android/testData/resourceRepository/layout2.xml
new file mode 100644
index 0000000..f318eb9
--- /dev/null
+++ b/android/testData/resourceRepository/layout2.xml
@@ -0,0 +1,78 @@
+<?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">
+ <include layout="@layout/colorstrip" android:layout_height="@dimen/colorstrip_height" android:layout_width="match_parent"/>
+
+ <LinearLayout style="@style/TitleBar" android:id="@+id/header">
+ <ImageView style="@style/TitleBarLogo"
+ android:contentDescription="@string/description_logo"
+ android:src="@drawable/title_logo" />
+
+ <View style="@style/TitleBarSpring" />
+
+ <ImageView style="@style/TitleBarSeparator" />
+ <ImageButton style="@style/TitleBarAction"
+ android:id="@+id/btn_title_refresh2"
+ android:contentDescription="@string/description_refresh"
+ android:src="@drawable/ic_title_refresh"
+ android:layout_width="wrap_content"
+ android:layout_height="42dp"
+ android:onClick="onRefreshClick" />
+ <ProgressBar style="@style/TitleBarProgressIndicator"
+ android:id="@+id/title_refresh_progress"
+ android:layout_width="wrap_content"
+ android:visibility="visible"/>
+
+ <ImageView style="@style/TitleBarSeparator" />
+ <ImageButton style="@style/TitleBarAction"
+ android:contentDescription="@string/description_search"
+ android:src="@drawable/ic_title_search"
+ android:layout_width="wrap_content"
+ android:layout_height="42dp"
+ android:onClick="onSearchClick" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/noteArea"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_margin="5dip">
+ <EditText
+ android:id="@android:id/text1"
+ android:layout_height="fill_parent"
+ android:hint="@string/note_hint"
+ android:freezesText="true"
+ android:gravity="top" android:layout_width="wrap_content" android:layout_weight="1">
+ </EditText>
+ <EditText
+ android:id="@android:id/text2"
+ android:layout_height="fill_parent"
+ android:freezesText="true"
+ android:gravity="top" android:layout_width="wrap_content" android:layout_weight="1">
+ <requestFocus />
+ </EditText>
+ </LinearLayout>
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ style="@android:style/ButtonBar">
+ <Button
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:onClick="onSaveClick"
+ android:text="@string/note_save" />
+ <Button
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:onClick="onDiscardClick"
+ android:text="@string/note_discard" />
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/android/testData/resourceRepository/layoutOverlay.xml b/android/testData/resourceRepository/layoutOverlay.xml
new file mode 100644
index 0000000..9bcf226
--- /dev/null
+++ b/android/testData/resourceRepository/layoutOverlay.xml
@@ -0,0 +1,36 @@
+<?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">
+
+ <LinearLayout
+ android:id="@+id/noteArea"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_margin="5dip">
+ <EditText
+ android:id="@android:id/text1"
+ android:layout_height="fill_parent"
+ android:hint="@string/note_hint"
+ android:freezesText="true"
+ android:gravity="top" android:layout_width="wrap_content" android:layout_weight="1">
+ </EditText>
+ <EditText
+ android:id="@+id/text2"
+ android:layout_height="fill_parent"
+ android:freezesText="true"
+ android:gravity="top" android:layout_width="wrap_content" android:layout_weight="1">
+ <requestFocus />
+ </EditText>
+ </LinearLayout>
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/bottomBar"
+ style="@android:style/ButtonBar"/>
+
+</LinearLayout>
diff --git a/android/testData/resourceRepository/strings.xml b/android/testData/resourceRepository/strings.xml
new file mode 100644
index 0000000..72eb461
--- /dev/null
+++ b/android/testData/resourceRepository/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">My Application 574</string>
+ <string name="action_settings">Settings</string>
+ <string name="hello_world">Hello world!</string>
+
+</resources>
diff --git a/android/testData/resourceRepository/values.xml b/android/testData/resourceRepository/values.xml
new file mode 100644
index 0000000..68e33a5
--- /dev/null
+++ b/android/testData/resourceRepository/values.xml
@@ -0,0 +1,77 @@
+<!--
+ Copyright 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Titles -->
+ <string name="app_name">Animations Demo</string>
+ <string name="title_crossfade">Simple Crossfade</string>
+ <string name="title_card_flip">Card Flip</string>
+ <string name="title_screen_slide">Screen Slide</string>
+ <string name="title_zoom">Zoom</string>
+ <string name="title_layout_changes">Layout Changes</string>
+ <string name="title_template_step">Step <xliff:g id="step_number">%1$d</xliff:g>: Lorem
+ Ipsum</string>
+ <string name="ellipsis">Here it is: \u2026!</string>
+
+ <item type="id" name="action_next" />
+ <item type="id" name="action_flip" />
+
+ <style name="DarkTheme" parent="android:Theme.Holo">
+ <item name="android:actionBarStyle">@style/DarkActionBar</item>
+ </style>
+
+ <style name="DarkActionBar" parent="android:Widget.Holo.ActionBar">
+ <item name="android:background">@android:color/transparent</item>
+ </style>
+
+ <integer name="card_flip_time_full">300</integer>
+ <integer name="card_flip_time_half">150</integer>
+
+ <declare-styleable name="MyCustomView">
+ <attr name="watchType" format="enum">
+ <enum name="type_stopwatch" value="1"/>
+ <enum name="type_countdown" value="0"/>
+ </attr>
+ <attr name="flagType" format="enum">
+ <flag name="flag1" value="0x10"/>
+ <flag name="flag2" value="0x20"/>
+ </attr>
+ <attr name="crash" format="boolean" />
+ <attr name="android:minWidth" />
+ <attr name="ignore_no_format" />
+ </declare-styleable>
+
+ <plurals name="my_plural" tools:quantity="two">
+ <item quantity="one">@string/hello</item>
+ <item quantity="two">@string/hello_two</item>
+ <item quantity="other">@string/hello_many</item>
+ </plurals>
+
+ <string-array name="security_questions" tools:index="3">
+ <item>Question 1</item>
+ <item>Question 2</item>
+ <item>Question 3</item>
+ <item>Question 4</item>
+ <item>Question 5</item>
+ </string-array>
+
+ <integer-array name="integers">
+ <item>10</item>
+ <item>20</item>
+ </integer-array>
+
+</resources>
diff --git a/android/testData/resourceRepository/valuesOverlay1.xml b/android/testData/resourceRepository/valuesOverlay1.xml
new file mode 100644
index 0000000..e5057d9
--- /dev/null
+++ b/android/testData/resourceRepository/valuesOverlay1.xml
@@ -0,0 +1,16 @@
+<resources>
+
+ <string name="app_name">Different App Name</string>
+ <string name="title_crossfade">Complex Crossfade</string>
+ <string name="unique_string">Unique</string>
+
+ <style name="DarkActionBar" parent="android:Widget.Holo.ActionBar">
+ <item name="android:background">@android:color/red</item>
+ </style>
+
+ <string-array name="security_questions" tools:index="1">
+ <item>Question 1</item>
+ <item>Question 2</item>
+ </string-array>
+
+</resources>
diff --git a/android/testData/resourceRepository/valuesOverlay2.xml b/android/testData/resourceRepository/valuesOverlay2.xml
new file mode 100644
index 0000000..4fa7b72
--- /dev/null
+++ b/android/testData/resourceRepository/valuesOverlay2.xml
@@ -0,0 +1,7 @@
+<resources>
+
+ <string name="app_name">Very Different App Name</string>
+ <string name="title_zoom">Zoom!</string>
+ <string name="another_unique_string">Another Unique</string>
+
+</resources>
diff --git a/android/testData/resourceRepository/valuesOverlay2No.xml b/android/testData/resourceRepository/valuesOverlay2No.xml
new file mode 100644
index 0000000..9a88d07
--- /dev/null
+++ b/android/testData/resourceRepository/valuesOverlay2No.xml
@@ -0,0 +1,6 @@
+<resources>
+
+ <string name="app_name">Forskjellig Navn</string>
+ <string name="another_unique_string">En Annen</string>
+
+</resources>
diff --git a/android/testData/sdk1.5/source.properties b/android/testData/sdk1.5/source.properties
new file mode 100644
index 0000000..c08a272
--- /dev/null
+++ b/android/testData/sdk1.5/source.properties
@@ -0,0 +1,13 @@
+### Dummy file
+#Fri Jun 28 12:21:41 PDT 2013
+Layoutlib.Api=4
+Layoutlib.Revision=0
+Pkg.Desc=Partial Install for Unit Tests
+Archive.Arch=ANY
+Platform.Version=1.5
+Pkg.DescUrl=http\://developer.android.com/sdk/
+Platform.MinToolsRev=12
+Archive.Os=ANY
+Pkg.SourceUrl=https\://dl-ssl.google.com/android/repository/repository-3.xml
+Pkg.Revision=1
+AndroidVersion.ApiLevel=3
diff --git a/android/testSrc/com/android/navigation/NavigationReaderTest.java b/android/testSrc/com/android/navigation/NavigationReaderTest.java
index a010c8a..91645b2 100644
--- a/android/testSrc/com/android/navigation/NavigationReaderTest.java
+++ b/android/testSrc/com/android/navigation/NavigationReaderTest.java
@@ -19,10 +19,18 @@
import junit.framework.TestCase;
import org.jetbrains.android.AndroidTestCase;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class NavigationReaderTest extends TestCase {
+ private static State createState(String className, String xmlFileName) {
+ State s0 = new State(className);
+ s0.setXmlResourceName(xmlFileName);
+ return s0;
+ }
+
public void test1() throws FileNotFoundException {
FileInputStream stream = new FileInputStream(AndroidTestCase.getTestDataPath() + "/resNavigation/res/layout/main.nav");
@@ -34,15 +42,36 @@
writer.write(result);
}
- /*
- public void test2() throws FileNotFoundException {
- FileInputStream stream = new FileInputStream("../adt/idea/android/testData/resNavigation/res/xml/test.xml");
- XMLReader reader = new XMLReader(stream);
- Object result = reader.read();
- System.out.println("result = " + result);
- assertTrue(result != null);
+ private static Object fromString(String output) {
+ return new XMLReader(new ByteArrayInputStream(output.getBytes())).read();
}
- */
+
+ private static String toString(Object model) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ XMLWriter writer = new XMLWriter(out);
+ writer.write(model);
+ return out.toString();
+ }
+
+ public void test0() throws FileNotFoundException {
+ NavigationModel model = new NavigationModel();
+ State s0 = createState("com.acme.MasterController", "master_controller");
+ State s1 = createState("com.acme.SlaveController", "slave_controller");
+ Transition t1 = Transition.of("click", s0, s1);
+ Transition t2 = Transition.of("swipe", s1, s0);
+ t2.getSource().setViewName("ere");
+ model.add(t1);
+ model.add(t2);
+
+ String output = toString(model);
+ System.out.println(output);
+
+ Object model2 = fromString(output);
+ String output2 = toString(model2);
+ //System.out.println(output2);
+
+ assertEquals(output, output2);
+ }
//public static void main(String[] args) throws FileNotFoundException {
// new NavigationReaderTest().test1();
diff --git a/android/testSrc/com/android/tools/idea/configurations/ConfigurationManagerTest.java b/android/testSrc/com/android/tools/idea/configurations/ConfigurationManagerTest.java
index 3aa856e..5c64a40 100644
--- a/android/testSrc/com/android/tools/idea/configurations/ConfigurationManagerTest.java
+++ b/android/testSrc/com/android/tools/idea/configurations/ConfigurationManagerTest.java
@@ -15,6 +15,7 @@
*/
package com.android.tools.idea.configurations;
+import com.android.tools.idea.rendering.FileProjectResourceRepositoryTest;
import com.android.tools.idea.rendering.Locale;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.android.AndroidTestCase;
@@ -23,6 +24,8 @@
import java.util.Arrays;
import java.util.List;
+import static com.android.tools.idea.rendering.FileProjectResourceRepositoryTest.runOutOfMemory;
+
public class ConfigurationManagerTest extends AndroidTestCase {
public void testGetLocales() {
myFixture.copyFileToProject("xmlpull/layout.xml", "res/layout/layout1.xml");
@@ -40,6 +43,7 @@
assertEquals(Arrays.asList(Locale.create("no"), Locale.create("no-rNO"), Locale.create("se")), locales);
}
+ @SuppressWarnings("UnusedAssignment") // need to null out local vars before GC
public void testCaching() {
VirtualFile file1 = myFixture.copyFileToProject("xmlpull/layout.xml", "res/layout/layout1.xml");
VirtualFile file2 = myFixture.copyFileToProject("xmlpull/layout.xml", "res/layout-no-rNO/layout1.xml");
@@ -58,5 +62,22 @@
assertSame(configuration2, manager.getConfiguration(file2));
assertSame(file1, configuration1.getFile());
assertSame(file2, configuration2.getFile());
+
+ // GC test: Ensure that we keep a cache through the first GC, but not if
+ // we nearly run out of memory:
+
+ assertTrue(manager.hasCachedConfiguration(file1));
+ assertTrue(manager.hasCachedConfiguration(file2));
+
+ configuration1 = null;
+ configuration2 = null;
+ System.gc();
+ assertTrue(manager.hasCachedConfiguration(file1));
+ assertTrue(manager.hasCachedConfiguration(file2));
+
+ runOutOfMemory();
+ System.gc();
+ assertFalse(manager.hasCachedConfiguration(file1));
+ assertFalse(manager.hasCachedConfiguration(file2));
}
}
diff --git a/android/testSrc/com/android/tools/idea/configurations/ConfigurationStateManagerTest.java b/android/testSrc/com/android/tools/idea/configurations/ConfigurationStateManagerTest.java
index b5314de..0979166 100644
--- a/android/testSrc/com/android/tools/idea/configurations/ConfigurationStateManagerTest.java
+++ b/android/testSrc/com/android/tools/idea/configurations/ConfigurationStateManagerTest.java
@@ -42,6 +42,10 @@
assertEquals("en-rUS", manager.getProjectState().getLocale());
manager.loadState(secondState);
manager.getProjectState().setLocale("de");
+
+ assertTrue(manager.getProjectState().isPickTarget());
+ manager.getProjectState().setPickTarget(false);
+ assertFalse(manager.getProjectState().isPickTarget());
}
public void test2() {
diff --git a/android/testSrc/com/android/tools/idea/configurations/ConfigurationTest.java b/android/testSrc/com/android/tools/idea/configurations/ConfigurationTest.java
index c68449a..ed30841 100644
--- a/android/testSrc/com/android/tools/idea/configurations/ConfigurationTest.java
+++ b/android/testSrc/com/android/tools/idea/configurations/ConfigurationTest.java
@@ -210,7 +210,7 @@
assertSame(manager, facet.getConfigurationManager());
Configuration configuration1 = manager.getConfiguration(file1);
- configuration1.setLocale(Locale.create("en"));
+ configuration1.getConfigurationManager().setLocale(Locale.create("en"));
configuration1.setTheme("Theme.Dialog");
Device device = manager.getDevices().get(manager.getDevices().size() / 2);
State state = device.getAllStates().get(device.getAllStates().size() - 1);
@@ -234,6 +234,10 @@
assertEquals("Landscape", configuration3.getDeviceState().getName());
assertEquals(ScreenSize.XLARGE, configuration3.getDevice().getDefaultHardware().getScreen().getSize());
assertEquals(configuration1.getLocale(), configuration3.getLocale());
+ // Ensure project-wide location switching works: both locales should update
+ configuration1.getConfigurationManager().setLocale(Locale.create("no"));
+ assertEquals(Locale.create("no"), configuration1.getLocale());
+ assertEquals(configuration1.getLocale(), configuration3.getLocale());
}
public void testTargetSpecificFolder() throws Exception {
diff --git a/android/testSrc/com/android/tools/idea/editors/AndroidImportFilterTest.java b/android/testSrc/com/android/tools/idea/editors/AndroidImportFilterTest.java
new file mode 100644
index 0000000..eeb9012
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/editors/AndroidImportFilterTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.editors;
+
+import junit.framework.TestCase;
+
+public class AndroidImportFilterTest extends TestCase {
+ public void test() {
+ AndroidImportFilter filter = new AndroidImportFilter();
+ assertTrue(filter.shouldUseFullyQualifiedName("android.R"));
+ assertTrue(filter.shouldUseFullyQualifiedName("android.R.anim"));
+ assertTrue(filter.shouldUseFullyQualifiedName("android.R.anything"));
+ assertFalse(filter.shouldUseFullyQualifiedName("com.android.tools.R"));
+ assertTrue(filter.shouldUseFullyQualifiedName("com.android.tools.R.anim"));
+ assertTrue(filter.shouldUseFullyQualifiedName("com.android.tools.R.layout"));
+ assertTrue(filter.shouldUseFullyQualifiedName("a.R.string"));
+ assertFalse(filter.shouldUseFullyQualifiedName("my.weird.clz.R"));
+ assertFalse(filter.shouldUseFullyQualifiedName("my.weird.clz.R.bogus"));
+ assertFalse(filter.shouldUseFullyQualifiedName(""));
+ assertFalse(filter.shouldUseFullyQualifiedName("."));
+ assertFalse(filter.shouldUseFullyQualifiedName("a.R"));
+ assertFalse(filter.shouldUseFullyQualifiedName("android"));
+ assertFalse(filter.shouldUseFullyQualifiedName("android."));
+ assertFalse(filter.shouldUseFullyQualifiedName("android.r"));
+ assertFalse(filter.shouldUseFullyQualifiedName("android.Random"));
+ assertFalse(filter.shouldUseFullyQualifiedName("my.R.unrelated"));
+ assertFalse(filter.shouldUseFullyQualifiedName("my.R.unrelated.to"));
+ assertFalse(filter.shouldUseFullyQualifiedName("R.string")); // R is never in the default package
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/ContentRootSourcePaths.java b/android/testSrc/com/android/tools/idea/gradle/ContentRootSourcePaths.java
index 8fa7734..2226322 100644
--- a/android/testSrc/com/android/tools/idea/gradle/ContentRootSourcePaths.java
+++ b/android/testSrc/com/android/tools/idea/gradle/ContentRootSourcePaths.java
@@ -15,11 +15,11 @@
*/
package com.android.tools.idea.gradle;
-import com.android.build.gradle.model.BuildTypeContainer;
-import com.android.build.gradle.model.ProductFlavorContainer;
-import com.android.build.gradle.model.Variant;
+import com.android.builder.model.BuildTypeContainer;
+import com.android.builder.model.ProductFlavorContainer;
import com.android.builder.model.SourceProvider;
import com.android.tools.idea.gradle.stubs.android.AndroidProjectStub;
+import com.android.tools.idea.gradle.stubs.android.ArtifactInfoStub;
import com.android.tools.idea.gradle.stubs.android.VariantStub;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@@ -69,11 +69,17 @@
}
}
- private void addSourceDirectoryPaths(@NotNull Variant variant) {
- addSourceDirectoryPaths(ExternalSystemSourceType.SOURCE, variant.getGeneratedResourceFolders());
- addSourceDirectoryPaths(ExternalSystemSourceType.SOURCE, variant.getGeneratedSourceFolders());
- addSourceDirectoryPaths(ExternalSystemSourceType.TEST, variant.getGeneratedTestResourceFolders());
- addSourceDirectoryPaths(ExternalSystemSourceType.TEST, variant.getGeneratedTestSourceFolders());
+ private void addSourceDirectoryPaths(@NotNull VariantStub variant) {
+ ArtifactInfoStub mainArtifactInfo = variant.getMainArtifactInfo();
+ addSourceDirectoryPaths(ExternalSystemSourceType.SOURCE, mainArtifactInfo);
+
+ ArtifactInfoStub testArtifactInfo = variant.getTestArtifactInfo();
+ addSourceDirectoryPaths(ExternalSystemSourceType.TEST, testArtifactInfo);
+ }
+
+ private void addSourceDirectoryPaths(@NotNull ExternalSystemSourceType sourceType, @NotNull ArtifactInfoStub artifactInfo) {
+ addSourceDirectoryPaths(sourceType, artifactInfo.getGeneratedResourceFolders());
+ addSourceDirectoryPaths(sourceType, artifactInfo.getGeneratedSourceFolders());
}
private void addSourceDirectoryPaths(@NotNull ProductFlavorContainer productFlavor) {
diff --git a/android/testSrc/com/android/tools/idea/gradle/TestProjects.java b/android/testSrc/com/android/tools/idea/gradle/TestProjects.java
index b9c15b8..61c3e7d 100644
--- a/android/testSrc/com/android/tools/idea/gradle/TestProjects.java
+++ b/android/testSrc/com/android/tools/idea/gradle/TestProjects.java
@@ -16,13 +16,14 @@
package com.android.tools.idea.gradle;
import com.android.tools.idea.gradle.stubs.android.AndroidProjectStub;
+import com.android.tools.idea.gradle.stubs.android.ArtifactInfoStub;
import com.android.tools.idea.gradle.stubs.android.VariantStub;
import org.jetbrains.annotations.NotNull;
import java.io.File;
/**
- * Factory of {@link com.android.build.gradle.model.AndroidProject}s for testing purposes. The created projects mimic the structure of the
+ * Factory of {@link com.android.builder.model.AndroidProject}s for testing purposes. The created projects mimic the structure of the
* sample projects distributed with the Android Gradle plug-in.
*/
public final class TestProjects {
@@ -54,19 +55,19 @@
androidProject.addBuildType("debug");
VariantStub debugVariant = androidProject.addVariant("debug");
- debugVariant.addGeneratedSourceFolder("build/source/aidl/debug");
- debugVariant.addGeneratedSourceFolder("build/source/buildConfig/debug");
- debugVariant.addGeneratedSourceFolder("build/source/r/debug");
- debugVariant.addGeneratedSourceFolder("build/source/rs/debug");
+ ArtifactInfoStub mainArtifactInfo = debugVariant.getMainArtifactInfo();
+ mainArtifactInfo.addGeneratedSourceFolder("build/source/aidl/debug");
+ mainArtifactInfo.addGeneratedSourceFolder("build/source/buildConfig/debug");
+ mainArtifactInfo.addGeneratedSourceFolder("build/source/r/debug");
+ mainArtifactInfo.addGeneratedSourceFolder("build/source/rs/debug");
+ mainArtifactInfo.addGeneratedResourceFolder("build/res/rs/debug");
- debugVariant.addGeneratedResourceFolder("build/res/rs/debug");
-
- debugVariant.addGeneratedTestSourceFolder("build/source/aidl/test");
- debugVariant.addGeneratedTestSourceFolder("build/source/buildConfig/test");
- debugVariant.addGeneratedTestSourceFolder("build/source/r/test");
- debugVariant.addGeneratedTestSourceFolder("build/source/rs/test");
-
- debugVariant.addGeneratedTestResourceFolder("build/res/rs/test");
+ ArtifactInfoStub testArtifactInfo = debugVariant.getTestArtifactInfo();
+ testArtifactInfo.addGeneratedSourceFolder("build/source/aidl/test");
+ testArtifactInfo.addGeneratedSourceFolder("build/source/buildConfig/test");
+ testArtifactInfo.addGeneratedSourceFolder("build/source/r/test");
+ testArtifactInfo.addGeneratedSourceFolder("build/source/rs/test");
+ testArtifactInfo.addGeneratedResourceFolder("build/res/rs/test");
}
@NotNull
@@ -76,19 +77,19 @@
project.addBuildType("debug");
VariantStub f1faDebugVariant = project.addVariant("f1fa-debug", "debug");
- f1faDebugVariant.addGeneratedSourceFolder("build/source/aidl/f1fa/debug");
- f1faDebugVariant.addGeneratedSourceFolder("build/source/buildConfig/f1fa/debug");
- f1faDebugVariant.addGeneratedSourceFolder("build/source/r/f1fa/debug");
- f1faDebugVariant.addGeneratedSourceFolder("build/source/rs/f1fa/debug");
+ ArtifactInfoStub mainArtifactInfo = f1faDebugVariant.getMainArtifactInfo();
+ mainArtifactInfo.addGeneratedSourceFolder("build/source/aidl/f1fa/debug");
+ mainArtifactInfo.addGeneratedSourceFolder("build/source/buildConfig/f1fa/debug");
+ mainArtifactInfo.addGeneratedSourceFolder("build/source/r/f1fa/debug");
+ mainArtifactInfo.addGeneratedSourceFolder("build/source/rs/f1fa/debug");
+ mainArtifactInfo.addGeneratedResourceFolder("build/res/rs/f1fa/debug");
- f1faDebugVariant.addGeneratedResourceFolder("build/res/rs/f1fa/debug");
-
- f1faDebugVariant.addGeneratedTestSourceFolder("build/source/aidl/f1fa/test");
- f1faDebugVariant.addGeneratedTestSourceFolder("build/source/buildConfig/f1fa/test");
- f1faDebugVariant.addGeneratedTestSourceFolder("build/source/r/f1fa/test");
- f1faDebugVariant.addGeneratedTestSourceFolder("build/source/rs/f1fa/test");
-
- f1faDebugVariant.addGeneratedTestResourceFolder("build/res/rs/f1fa/test");
+ ArtifactInfoStub testArtifactInfo = f1faDebugVariant.getTestArtifactInfo();
+ testArtifactInfo.addGeneratedSourceFolder("build/source/aidl/f1fa/test");
+ testArtifactInfo.addGeneratedSourceFolder("build/source/buildConfig/f1fa/test");
+ testArtifactInfo.addGeneratedSourceFolder("build/source/r/f1fa/test");
+ testArtifactInfo.addGeneratedSourceFolder("build/source/rs/f1fa/test");
+ testArtifactInfo.addGeneratedResourceFolder("build/res/rs/f1fa/test");
f1faDebugVariant.addProductFlavors("f1", "fa");
diff --git a/android/testSrc/com/android/tools/idea/gradle/compiler/AndroidGradleBuildProcessParametersProviderTest.java b/android/testSrc/com/android/tools/idea/gradle/compiler/AndroidGradleBuildProcessParametersProviderTest.java
index 142e148..6579326 100644
--- a/android/testSrc/com/android/tools/idea/gradle/compiler/AndroidGradleBuildProcessParametersProviderTest.java
+++ b/android/testSrc/com/android/tools/idea/gradle/compiler/AndroidGradleBuildProcessParametersProviderTest.java
@@ -15,6 +15,7 @@
*/
package com.android.tools.idea.gradle.compiler;
+import com.google.common.collect.Lists;
import com.intellij.openapi.project.Project;
import junit.framework.TestCase;
import org.jetbrains.plugins.gradle.settings.GradleExecutionSettings;
@@ -28,33 +29,39 @@
*/
public class AndroidGradleBuildProcessParametersProviderTest extends TestCase {
private Project myProject;
- private GradleExecutionSettings myGradleExecutionSettings;
-
private AndroidGradleBuildProcessParametersProvider myParametersProvider;
@Override
public void setUp() throws Exception {
super.setUp();
myProject = createMock(Project.class);
- myGradleExecutionSettings = createMock(GradleExecutionSettings.class);
myParametersProvider = new AndroidGradleBuildProcessParametersProvider(myProject);
}
- public void testGetGradleExecutionSettingsAsVmArgs() {
+ public void testPopulateJvmArgsWithGradleExecutionSettings() {
+ GradleExecutionSettings settings = createMock(GradleExecutionSettings.class);
+
expect(myProject.getBasePath()).andReturn("~/projects/project1");
- expect(myGradleExecutionSettings.getRemoteProcessIdleTtlInMs()).andReturn(55L);
- expect(myGradleExecutionSettings.getGradleHome()).andReturn("~/gradle-1.6");
- expect(myGradleExecutionSettings.isVerboseProcessing()).andReturn(true);
- expect(myGradleExecutionSettings.getServiceDirectory()).andReturn("~./gradle");
+ expect(settings.getRemoteProcessIdleTtlInMs()).andReturn(55L);
+ expect(settings.getGradleHome()).andReturn("~/gradle-1.6");
+ expect(settings.isVerboseProcessing()).andReturn(true);
+ expect(settings.getServiceDirectory()).andReturn("~./gradle");
+ expect(settings.getDaemonVmOptions()).andReturn("-Xmx2048m -XX:MaxPermSize=512m");
+ expect(settings.getJavaHome()).andReturn("~/Libraries/Java Home");
- replay(myProject, myGradleExecutionSettings);
+ replay(myProject, settings);
- List<String> vmArgs = myParametersProvider.getGradleExecutionSettingsAsVmArgs(myGradleExecutionSettings);
- assertEquals(5, vmArgs.size());
- assertTrue(vmArgs.contains("-Dcom.android.studio.gradle.project.path=~/projects/project1"));
- assertTrue(vmArgs.contains("-Dcom.android.studio.gradle.daemon.max.idle.time=55"));
- assertTrue(vmArgs.contains("-Dcom.android.studio.gradle.home.path=~/gradle-1.6"));
- assertTrue(vmArgs.contains("-Dcom.android.studio.gradle.use.verbose.logging=true"));
- assertTrue(vmArgs.contains("-Dcom.android.studio.gradle.service.dir.path=~./gradle"));
+ List<String> jvmArgs = Lists.newArrayList();
+ myParametersProvider.populateJvmArgs(settings, jvmArgs);
+ assertEquals(9, jvmArgs.size());
+ assertTrue(jvmArgs.contains("-Dcom.android.studio.gradle.project.path=~/projects/project1"));
+ assertTrue(jvmArgs.contains("-Dcom.android.studio.gradle.daemon.max.idle.time=55"));
+ assertTrue(jvmArgs.contains("-Dcom.android.studio.gradle.home.path=~/gradle-1.6"));
+ assertTrue(jvmArgs.contains("-Dcom.android.studio.gradle.use.verbose.logging=true"));
+ assertTrue(jvmArgs.contains("-Dcom.android.studio.gradle.service.dir.path=~./gradle"));
+ assertTrue(jvmArgs.contains("-Dcom.android.studio.daemon.gradle.vm.option.count=2"));
+ assertTrue(jvmArgs.contains("-Dcom.android.studio.daemon.gradle.vm.option.0=-Xmx2048m"));
+ assertTrue(jvmArgs.contains("-Dcom.android.studio.daemon.gradle.vm.option.1=-XX:MaxPermSize=512m"));
+ assertTrue(jvmArgs.contains("-Dcom.android.studio.gradle.java.home.path=~/Libraries/Java Home"));
}
}
diff --git a/android/testSrc/com/android/tools/idea/gradle/customizer/AndroidFacetModuleCustomizerTest.java b/android/testSrc/com/android/tools/idea/gradle/customizer/AndroidFacetModuleCustomizerTest.java
index 6890908..0668fc5 100644
--- a/android/testSrc/com/android/tools/idea/gradle/customizer/AndroidFacetModuleCustomizerTest.java
+++ b/android/testSrc/com/android/tools/idea/gradle/customizer/AndroidFacetModuleCustomizerTest.java
@@ -55,12 +55,11 @@
VariantStub selectedVariant = myAndroidProject.getFirstVariant();
assertNotNull(selectedVariant);
String selectedVariantName = selectedVariant.getName();
- IdeaAndroidProject project =
- new IdeaAndroidProject(myAndroidProject.getName(), rootDirPath, rootDirPath, myAndroidProject, selectedVariantName);
+ IdeaAndroidProject project = new IdeaAndroidProject(myAndroidProject.getName(), rootDirPath, myAndroidProject, selectedVariantName);
myCustomizer.customizeModule(myModule, myProject, project);
// Verify that AndroidFacet was added and configured.
- AndroidFacet facet = Facets.getFirstFacet(myModule, AndroidFacet.ID);
+ AndroidFacet facet = Facets.getFirstFacetOfType(myModule, AndroidFacet.ID);
assertNotNull(facet);
assertSame(project, facet.getIdeaAndroidProject());
diff --git a/android/testSrc/com/android/tools/idea/gradle/customizer/CompilerOutputPathModuleCustomizerTest.java b/android/testSrc/com/android/tools/idea/gradle/customizer/CompilerOutputPathModuleCustomizerTest.java
index ac0ab96..d6e6f95 100644
--- a/android/testSrc/com/android/tools/idea/gradle/customizer/CompilerOutputPathModuleCustomizerTest.java
+++ b/android/testSrc/com/android/tools/idea/gradle/customizer/CompilerOutputPathModuleCustomizerTest.java
@@ -51,7 +51,7 @@
public void testCustomizeModule() {
String rootDirPath = androidProject.getRootDir().getAbsolutePath();
- IdeaAndroidProject ideaAndroidProject = new IdeaAndroidProject(myModule.getName(), rootDirPath, rootDirPath, androidProject, "debug");
+ IdeaAndroidProject ideaAndroidProject = new IdeaAndroidProject(myModule.getName(), rootDirPath, androidProject, "debug");
customizer.customizeModule(myModule, myProject, ideaAndroidProject);
ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(myModule);
@@ -60,7 +60,7 @@
String compilerOutputPath = compilerSettings.getCompilerOutputUrl();
moduleSettings.commit();
- File classesFolder = ideaAndroidProject.getSelectedVariant().getClassesFolder();
+ File classesFolder = ideaAndroidProject.getSelectedVariant().getMainArtifactInfo().getClassesFolder();
String expected = VfsUtilCore.pathToUrl(ExternalSystemApiUtil.toCanonicalPath(classesFolder.getAbsolutePath()));
assertEquals(expected, compilerOutputPath);
}
diff --git a/android/testSrc/com/android/tools/idea/gradle/customizer/ContentRootModuleCustomizerTest.java b/android/testSrc/com/android/tools/idea/gradle/customizer/ContentRootModuleCustomizerTest.java
index cb01aae..f7a122d 100644
--- a/android/testSrc/com/android/tools/idea/gradle/customizer/ContentRootModuleCustomizerTest.java
+++ b/android/testSrc/com/android/tools/idea/gradle/customizer/ContentRootModuleCustomizerTest.java
@@ -15,7 +15,7 @@
*/
package com.android.tools.idea.gradle.customizer;
-import com.android.build.gradle.model.Variant;
+import com.android.builder.model.Variant;
import com.android.tools.idea.gradle.ContentRootSourcePaths;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.android.tools.idea.gradle.TestProjects;
@@ -55,8 +55,7 @@
Collection<Variant> variants = myAndroidProject.getVariants().values();
Variant selectedVariant = ContainerUtil.getFirstItem(variants);
assertNotNull(selectedVariant);
- myIdeaAndroidProject =
- new IdeaAndroidProject(myAndroidProject.getName(), basePath, basePath, myAndroidProject, selectedVariant.getName());
+ myIdeaAndroidProject = new IdeaAndroidProject(myAndroidProject.getName(), basePath, myAndroidProject, selectedVariant.getName());
addContentEntry();
myCustomizer = new ContentRootModuleCustomizer();
diff --git a/android/testSrc/com/android/tools/idea/gradle/dependency/DependencySetTest.java b/android/testSrc/com/android/tools/idea/gradle/dependency/DependencySetTest.java
new file mode 100644
index 0000000..050f741
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/gradle/dependency/DependencySetTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.dependency;
+
+import com.intellij.openapi.roots.DependencyScope;
+import com.intellij.util.containers.ContainerUtil;
+import junit.framework.TestCase;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collection;
+
+/**
+ * Tests for {@link DependencySet}.
+ */
+public class DependencySetTest extends TestCase {
+ private DependencySet myDependencies;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ myDependencies = new DependencySet();
+ }
+
+ public void testAddWithNewDependency() {
+ TestDependency dependency = new TestDependency("asm-4.0.jar", DependencyScope.COMPILE);
+ myDependencies.add(dependency);
+ Collection<Dependency> all = myDependencies.getValues();
+ assertEquals(1, all.size());
+ assertSame(dependency, ContainerUtil.getFirstItem(all));
+ }
+
+ public void testAddWithExistingDependencyWithNarrowerScope() {
+ TestDependency dependency = new TestDependency("asm-4.0.jar", DependencyScope.COMPILE);
+ myDependencies.add(dependency);
+ myDependencies.add(new TestDependency("asm-4.0.jar", DependencyScope.TEST));
+ Collection<Dependency> all = myDependencies.getValues();
+ assertEquals(1, all.size());
+ assertSame(dependency, ContainerUtil.getFirstItem(all));
+ }
+
+ public void testAddWithExistingDependencyWithWiderScope() {
+ myDependencies.add(new TestDependency("asm-4.0.jar", DependencyScope.TEST));
+ TestDependency dependency = new TestDependency("asm-4.0.jar", DependencyScope.COMPILE);
+ myDependencies.add(dependency);
+ Collection<Dependency> all = myDependencies.getValues();
+ assertEquals(1, all.size());
+ assertSame(dependency, ContainerUtil.getFirstItem(all));
+ }
+
+ private static class TestDependency extends Dependency {
+ TestDependency(@NotNull String name, @NotNull DependencyScope scope) {
+ super(name, scope);
+ }
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/dependency/DependencyTest.java b/android/testSrc/com/android/tools/idea/gradle/dependency/DependencyTest.java
new file mode 100644
index 0000000..92afc49
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/gradle/dependency/DependencyTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.dependency;
+
+import com.intellij.openapi.roots.DependencyScope;
+import junit.framework.TestCase;
+
+/**
+ * Tests for {@link Dependency}.
+ */
+public class DependencyTest extends TestCase {
+ public void testConstructorWithScope() {
+ try {
+ new Dependency("test", DependencyScope.RUNTIME) {
+ };
+ fail("Expecting an " + IllegalArgumentException.class.getSimpleName());
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("'Runtime' is not a supported scope. Supported scopes are [Compile, Test].", e.getMessage());
+ }
+ }
+
+ public void testSetScope() {
+ Dependency dependency = new Dependency("test") {
+ };
+ try {
+ dependency.setScope(DependencyScope.PROVIDED);
+ fail("Expecting an " + IllegalArgumentException.class.getSimpleName());
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("'Provided' is not a supported scope. Supported scopes are [Compile, Test].", e.getMessage());
+ }
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/dependency/ExtractAndroidDependenciesTest.java b/android/testSrc/com/android/tools/idea/gradle/dependency/ExtractAndroidDependenciesTest.java
new file mode 100644
index 0000000..96cde8b
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/gradle/dependency/ExtractAndroidDependenciesTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.dependency;
+
+import com.android.tools.idea.gradle.IdeaAndroidProject;
+import com.android.tools.idea.gradle.TestProjects;
+import com.android.tools.idea.gradle.stubs.android.AndroidLibraryStub;
+import com.android.tools.idea.gradle.stubs.android.AndroidProjectStub;
+import com.android.tools.idea.gradle.stubs.android.VariantStub;
+import com.google.common.collect.Lists;
+import com.intellij.openapi.roots.DependencyScope;
+import com.intellij.util.containers.ContainerUtil;
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Tests for {@link Dependency#extractFrom(com.android.tools.idea.gradle.IdeaAndroidProject)}.
+ */
+public class ExtractAndroidDependenciesTest extends TestCase {
+ private IdeaAndroidProject myIdeaAndroidProject;
+ private AndroidProjectStub myAndroidProject;
+ private VariantStub myVariant;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ myAndroidProject = TestProjects.createBasicProject();
+ myVariant = myAndroidProject.getFirstVariant();
+ assertNotNull(myVariant);
+
+ String rootDirPath = myAndroidProject.getRootDir().getAbsolutePath();
+ myIdeaAndroidProject = new IdeaAndroidProject(myAndroidProject.getName(), rootDirPath, myAndroidProject, myVariant.getName());
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ if (myAndroidProject != null) {
+ myAndroidProject.dispose();
+ }
+ super.tearDown();
+ }
+
+ public void testExtractFromWithJar() {
+ File jarFile = new File("~/repo/guava/guava-11.0.2.jar");
+
+ myVariant.getMainArtifactInfo().getDependencies().addJar(jarFile);
+ myVariant.getTestArtifactInfo().getDependencies().addJar(jarFile);
+
+ Collection<Dependency> dependencies = Dependency.extractFrom(myIdeaAndroidProject);
+ assertEquals(1, dependencies.size());
+
+ LibraryDependency dependency = (LibraryDependency)ContainerUtil.getFirstItem(dependencies);
+ assertNotNull(dependency);
+ assertEquals("guava-11.0.2", dependency.getName());
+ // Make sure that is a "compile" dependency, even if specified as "test".
+ assertEquals(DependencyScope.COMPILE, dependency.getScope());
+
+ Collection<String> binaryPaths = dependency.getPaths(LibraryDependency.PathType.BINARY);
+ assertEquals(1, binaryPaths.size());
+ assertEquals(jarFile.getPath(), ContainerUtil.getFirstItem(binaryPaths));
+ }
+
+ public void testExtractFromWithLibraryProject() {
+ String rootDirPath = myAndroidProject.getRootDir().getAbsolutePath();
+ File libJar = new File(rootDirPath, "library.aar/library.jar");
+ String gradlePath = "abc:xyz:library";
+ AndroidLibraryStub library = new AndroidLibraryStub(libJar, gradlePath);
+
+ myVariant.getMainArtifactInfo().getDependencies().addLibrary(library);
+ myVariant.getTestArtifactInfo().getDependencies().addLibrary(library);
+
+ Collection<Dependency> dependencies = Dependency.extractFrom(myIdeaAndroidProject);
+ assertEquals(1, dependencies.size());
+
+ ModuleDependency dependency = (ModuleDependency)ContainerUtil.getFirstItem(dependencies);
+ assertNotNull(dependency);
+ assertEquals("library", dependency.getName());
+ assertEquals(gradlePath, dependency.getGradlePath());
+ // Make sure that is a "compile" dependency, even if specified as "test".
+ assertEquals(DependencyScope.COMPILE, dependency.getScope());
+
+ LibraryDependency backup = dependency.getBackupDependency();
+ assertNotNull(backup);
+ assertEquals("library.aar", backup.getName());
+ assertEquals(DependencyScope.COMPILE, backup.getScope());
+
+ Collection<String> binaryPaths = backup.getPaths(LibraryDependency.PathType.BINARY);
+ assertEquals(1, binaryPaths.size());
+ assertEquals(libJar.getPath(), ContainerUtil.getFirstItem(binaryPaths));
+ }
+
+ public void testExtractFromWithLibraryAar() {
+ String rootDirPath = myAndroidProject.getRootDir().getAbsolutePath();
+ File libJar = new File(rootDirPath, "library.aar/library.jar");
+ AndroidLibraryStub library = new AndroidLibraryStub(libJar);
+
+ myVariant.getMainArtifactInfo().getDependencies().addLibrary(library);
+ myVariant.getTestArtifactInfo().getDependencies().addLibrary(library);
+
+ Collection<Dependency> dependencies = Dependency.extractFrom(myIdeaAndroidProject);
+ assertEquals(1, dependencies.size());
+
+ LibraryDependency dependency = (LibraryDependency)ContainerUtil.getFirstItem(dependencies);
+ assertNotNull(dependency);
+ assertEquals("library.aar", dependency.getName());
+ // Make sure that is a "compile" dependency, even if specified as "test".
+ assertEquals(DependencyScope.COMPILE, dependency.getScope());
+
+ Collection<String> binaryPaths = dependency.getPaths(LibraryDependency.PathType.BINARY);
+ assertEquals(1, binaryPaths.size());
+ assertEquals(libJar.getPath(), ContainerUtil.getFirstItem(binaryPaths));
+ }
+
+ public void testExtractFromWithLibraryLocalJar() {
+ String rootDirPath = myAndroidProject.getRootDir().getAbsolutePath();
+ File libJar = new File(rootDirPath, "library.aar/library.jar");
+ AndroidLibraryStub library = new AndroidLibraryStub(libJar);
+
+ File localJar = new File(rootDirPath, "local.jar");
+ library.addLocalJar(localJar);
+
+ myVariant.getMainArtifactInfo().getDependencies().addLibrary(library);
+ myVariant.getTestArtifactInfo().getDependencies().addLibrary(library);
+
+ List<Dependency> dependencies = Lists.newArrayList(Dependency.extractFrom(myIdeaAndroidProject));
+ assertEquals(2, dependencies.size());
+
+ LibraryDependency dependency = (LibraryDependency)dependencies.get(1);
+ assertNotNull(dependency);
+ assertEquals("local", dependency.getName());
+ // Make sure that is a "compile" dependency, even if specified as "test".
+ assertEquals(DependencyScope.COMPILE, dependency.getScope());
+
+ Collection<String> binaryPaths = dependency.getPaths(LibraryDependency.PathType.BINARY);
+ assertEquals(1, binaryPaths.size());
+ assertEquals(localJar.getPath(), ContainerUtil.getFirstItem(binaryPaths));
+ }
+
+ public void testExtractFromWithProject() {
+ String gradlePath = "abc:xyz:library";
+ myVariant.getMainArtifactInfo().getDependencies().addProject(gradlePath);
+ myVariant.getTestArtifactInfo().getDependencies().addProject(gradlePath);
+ Collection<Dependency> dependencies = Dependency.extractFrom(myIdeaAndroidProject);
+ assertEquals(1, dependencies.size());
+
+ ModuleDependency dependency = (ModuleDependency)ContainerUtil.getFirstItem(dependencies);
+ assertNotNull(dependency);
+ assertEquals("library", dependency.getName());
+ assertEquals(gradlePath, dependency.getGradlePath());
+ // Make sure that is a "compile" dependency, even if specified as "test".
+ assertEquals(DependencyScope.COMPILE, dependency.getScope());
+
+ LibraryDependency backup = dependency.getBackupDependency();
+ assertNull(backup);
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/dependency/ExtractJavaDependenciesTest.java b/android/testSrc/com/android/tools/idea/gradle/dependency/ExtractJavaDependenciesTest.java
new file mode 100644
index 0000000..7efb0e2
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/gradle/dependency/ExtractJavaDependenciesTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.dependency;
+
+import com.android.tools.idea.gradle.stubs.gradle.IdeaModuleDependencyStub;
+import com.android.tools.idea.gradle.stubs.gradle.IdeaModuleStub;
+import com.android.tools.idea.gradle.stubs.gradle.IdeaProjectStub;
+import com.android.tools.idea.gradle.stubs.gradle.IdeaSingleEntryLibraryDependencyStub;
+import com.intellij.openapi.roots.DependencyScope;
+import com.intellij.util.containers.ContainerUtil;
+import junit.framework.TestCase;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.Collection;
+
+/**
+ * Tests for {@link Dependency#extractFrom(org.gradle.tooling.model.idea.IdeaModule)}.
+ */
+public class ExtractJavaDependenciesTest extends TestCase {
+ private IdeaProjectStub myIdeaProject;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ myIdeaProject = new IdeaProjectStub("test");
+ }
+
+ public void testExtractFromUsingModuleDependency() {
+ // module2 depends on module1
+ IdeaModuleStub module1 = myIdeaProject.addModule("module1");
+ IdeaModuleStub module2 = myIdeaProject.addModule("module2");
+ module2.addDependency(new IdeaModuleDependencyStub(module1));
+
+ Collection<Dependency> dependencies = Dependency.extractFrom(module2);
+ assertEquals(1, dependencies.size());
+
+ ModuleDependency dependency = (ModuleDependency)ContainerUtil.getFirstItem(dependencies);
+ assertNotNull(dependency);
+ assertEquals(module1.getName(), dependency.getName());
+ assertEquals(module1.getGradleProject().getPath(), dependency.getGradlePath());
+ assertEquals(DependencyScope.COMPILE, dependency.getScope());
+ }
+
+ public void testExtractFromUsingLibraryDependency() {
+ File javadocFile = new File("~/repo/guava/guava-11.0.2-javadoc.jar");
+ File jarFile = new File("~/repo/guava/guava-11.0.2.jar");
+ File sourceFile = new File("~/repo/guava/guava-11.0.2-src.jar");
+
+ IdeaSingleEntryLibraryDependencyStub ideaDependency = new IdeaSingleEntryLibraryDependencyStub(jarFile);
+ ideaDependency.setJavadoc(javadocFile);
+ ideaDependency.setSource(sourceFile);
+
+ IdeaModuleStub module1 = myIdeaProject.addModule("module1");
+ module1.addDependency(ideaDependency);
+
+ Collection<Dependency> dependencies = Dependency.extractFrom(module1);
+ assertEquals(1, dependencies.size());
+
+ LibraryDependency dependency = (LibraryDependency)ContainerUtil.getFirstItem(dependencies);
+ assertNotNull(dependency);
+ assertEquals("guava-11.0.2", dependency.getName());
+ assertEquals(DependencyScope.COMPILE, dependency.getScope());
+ assertHasEqualPath(javadocFile, dependency.getPaths(LibraryDependency.PathType.DOC));
+ assertHasEqualPath(jarFile, dependency.getPaths(LibraryDependency.PathType.BINARY));
+ assertHasEqualPath(sourceFile, dependency.getPaths(LibraryDependency.PathType.SOURCE));
+ }
+
+ private static void assertHasEqualPath(@NotNull File expected, @NotNull Collection<String> actualPaths) {
+ assertEquals(1, actualPaths.size());
+ assertEquals(expected.getPath(), ContainerUtil.getFirstItem(actualPaths));
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/dependency/LibraryDependencyTest.java b/android/testSrc/com/android/tools/idea/gradle/dependency/LibraryDependencyTest.java
new file mode 100644
index 0000000..cf33108
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/gradle/dependency/LibraryDependencyTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.dependency;
+
+import com.intellij.openapi.roots.DependencyScope;
+import com.intellij.util.containers.ContainerUtil;
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.util.Collection;
+
+/**
+ * Tests for {@link LibraryDependency}.
+ */
+public class LibraryDependencyTest extends TestCase {
+ public void testConstructorWithJar() {
+ File jarFile = new File("~/repo/guava/guava-11.0.2.jar");
+ LibraryDependency dependency = new LibraryDependency(jarFile, DependencyScope.TEST);
+ assertEquals("guava-11.0.2", dependency.getName());
+ Collection<String> binaryPaths = dependency.getPaths(LibraryDependency.PathType.BINARY);
+ assertEquals(1, binaryPaths.size());
+ assertEquals(jarFile.getPath(), ContainerUtil.getFirstItem(binaryPaths));
+ assertEquals(DependencyScope.TEST, dependency.getScope());
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/dependency/ModuleDependencyTest.java b/android/testSrc/com/android/tools/idea/gradle/dependency/ModuleDependencyTest.java
new file mode 100644
index 0000000..44e967d
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/gradle/dependency/ModuleDependencyTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.dependency;
+
+import com.intellij.openapi.roots.DependencyScope;
+import junit.framework.TestCase;
+
+/**
+ * Tests for {@link ModuleDependency}.
+ */
+public class ModuleDependencyTest extends TestCase {
+ public void testConstructorWithGradlePath() {
+ String projectName = "module1";
+ String gradlePath = "abc:xyz:" + projectName;
+ ModuleDependency dependency = new ModuleDependency(gradlePath, DependencyScope.TEST);
+ assertEquals(projectName, dependency.getName());
+ assertEquals(gradlePath, dependency.getGradlePath());
+ assertEquals(DependencyScope.TEST, dependency.getScope());
+ }
+
+ public void testSetBackupDependency() {
+ ModuleDependency dependency = new ModuleDependency("abc:module1", DependencyScope.COMPILE);
+ LibraryDependency backup = new LibraryDependency("guava-11.0.2", DependencyScope.TEST);
+ dependency.setBackupDependency(backup);
+ assertSame(backup, dependency.getBackupDependency());
+ assertEquals(DependencyScope.COMPILE, backup.getScope());
+ }
+
+ public void testSetScope() {
+ ModuleDependency dependency = new ModuleDependency("abc:module1", DependencyScope.COMPILE);
+ LibraryDependency backup = new LibraryDependency("guava-11.0.2", DependencyScope.COMPILE);
+ dependency.setBackupDependency(backup);
+ dependency.setScope(DependencyScope.TEST);
+ assertEquals(DependencyScope.TEST, dependency.getScope());
+ assertEquals(DependencyScope.TEST, backup.getScope());
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/model/AndroidDependenciesTest.java b/android/testSrc/com/android/tools/idea/gradle/model/AndroidDependenciesTest.java
deleted file mode 100644
index c6f8add..0000000
--- a/android/testSrc/com/android/tools/idea/gradle/model/AndroidDependenciesTest.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tools.idea.gradle.model;
-
-import com.android.tools.idea.gradle.IdeaAndroidProject;
-import com.android.tools.idea.gradle.TestProjects;
-import com.android.tools.idea.gradle.model.AndroidDependencies.DependencyFactory;
-import com.android.tools.idea.gradle.stubs.android.AndroidLibraryStub;
-import com.android.tools.idea.gradle.stubs.android.AndroidProjectStub;
-import com.android.tools.idea.gradle.stubs.android.ProductFlavorContainerStub;
-import com.android.tools.idea.gradle.stubs.android.VariantStub;
-import com.intellij.openapi.roots.DependencyScope;
-import junit.framework.TestCase;
-
-import java.io.File;
-
-import static org.easymock.EasyMock.*;
-
-/**
- * Tests for {@link AndroidDependencies}.
- */
-public class AndroidDependenciesTest extends TestCase {
- private AndroidProjectStub myAndroidProject;
- private VariantStub myVariant;
- private IdeaAndroidProject myIdeaAndroidProject;
- private DependencyFactory myDependencyFactory;
-
- @Override
- public void setUp() throws Exception {
- super.setUp();
- myAndroidProject = TestProjects.createFlavorsProject();
- myVariant = myAndroidProject.getFirstVariant();
- assertNotNull(myVariant);
-
- String rootDirPath = myAndroidProject.getRootDir().getAbsolutePath();
- myIdeaAndroidProject =
- new IdeaAndroidProject(myAndroidProject.getName(), rootDirPath, rootDirPath, myAndroidProject, myVariant.getName());
-
- myDependencyFactory = createMock(DependencyFactory.class);
- }
-
- @Override
- protected void tearDown() throws Exception {
- if (myAndroidProject != null) {
- myAndroidProject.dispose();
- }
- super.tearDown();
- }
-
- public void testAddToWithJarDependency() {
- // Set up a jar dependency in one of the flavors of the selected variant.
- File jarFile = new File("~/repo/guava/guava-11.0.2.jar");
- String flavorName = myVariant.getProductFlavors().get(0);
- ProductFlavorContainerStub productFlavor = (ProductFlavorContainerStub)myAndroidProject.getProductFlavors().get(flavorName);
- productFlavor.getDependencies().addJar(jarFile);
-
- myDependencyFactory.addDependency(DependencyScope.COMPILE, "guava-11.0.2", jarFile);
- expectLastCall();
-
- replay(myDependencyFactory);
-
- AndroidDependencies.populate(myIdeaAndroidProject, myDependencyFactory);
-
- verify(myDependencyFactory);
- }
-
- public void testAddToWithLibraryDependency() {
- // Set up a library dependency to the default configuration.
- ProductFlavorContainerStub defaultConfig = myAndroidProject.getDefaultConfig();
-
- String rootDirPath = myAndroidProject.getRootDir().getAbsolutePath();
- File libJar = new File(rootDirPath, "library.aar/library.jar");
- AndroidLibraryStub library = new AndroidLibraryStub(libJar);
- defaultConfig.getDependencies().addLibrary(library);
-
- myDependencyFactory.addDependency(DependencyScope.COMPILE, "library.aar", libJar);
- expectLastCall();
-
- replay(myDependencyFactory);
-
- AndroidDependencies.populate(myIdeaAndroidProject, myDependencyFactory);
-
- verify(myDependencyFactory);
- }
-}
diff --git a/android/testSrc/com/android/tools/idea/gradle/parser/GradleBuildFileTest.java b/android/testSrc/com/android/tools/idea/gradle/parser/GradleBuildFileTest.java
new file mode 100644
index 0000000..432c639
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/gradle/parser/GradleBuildFileTest.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.parser;
+
+import com.android.SdkConstants;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.testFramework.IdeaTestCase;
+import com.intellij.util.ActionRunner;
+import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory;
+import org.jetbrains.plugins.groovy.lang.psi.api.util.GrStatementOwner;
+
+import java.io.IOException;
+
+public class GradleBuildFileTest extends IdeaTestCase {
+ private Document myDocument;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ myDocument = null;
+ }
+
+ public void testGetTopLevelValue() throws Exception {
+ final GradleBuildFile file = getTestFile(getSimpleTestFile());
+ assertEquals("17.0.0", file.getValue(GradleBuildFile.BuildSettingKey.BUILD_TOOLS_VERSION));
+ }
+
+ public void testNestedValue() throws Exception {
+ final GradleBuildFile file = getTestFile(getSimpleTestFile());
+ GrStatementOwner closure = file.getClosure("android/defaultConfig");
+ assertEquals(1, file.getValue(closure, GradleBuildFile.BuildSettingKey.TARGET_SDK_VERSION));
+ }
+
+ public void testCanParseSimpleValue() throws Exception {
+ final GradleBuildFile file = getTestFile(getSimpleTestFile());
+ assertTrue(file.canParseValue(GradleBuildFile.BuildSettingKey.BUILD_TOOLS_VERSION));
+ }
+
+ public void testCantParseComplexValue() throws Exception {
+ final GradleBuildFile file = getTestFile(getSimpleTestFile());
+ GrStatementOwner closure = file.getClosure("android/defaultConfig");
+ assertFalse(file.canParseValue(closure, GradleBuildFile.BuildSettingKey.MIN_SDK_VERSION));
+ }
+
+ public void testSetTopLevelValue() throws Exception {
+ final GradleBuildFile file = getTestFile(getSimpleTestFile());
+ ActionRunner.runInsideWriteAction(new ActionRunner.InterruptibleRunnable() {
+ @Override
+ public void run() throws Exception {
+ file.setValue(GradleBuildFile.BuildSettingKey.BUILD_TOOLS_VERSION, "18.0.0");
+ }
+ });
+ String expected = getSimpleTestFile().replaceAll("17.0.0", "18.0.0");
+ assertContents(file, expected);
+ }
+
+ public void testSetNestedValue() throws Exception {
+ final GradleBuildFile file = getTestFile(getSimpleTestFile());
+ ActionRunner.runInsideWriteAction(new ActionRunner.InterruptibleRunnable() {
+ @Override
+ public void run() throws Exception {
+ GrStatementOwner closure = file.getClosure("android/defaultConfig");
+ file.setValue(closure, GradleBuildFile.BuildSettingKey.TARGET_SDK_VERSION, 2);
+ }
+ });
+ String expected = getSimpleTestFile().replaceAll("targetSdkVersion 1", "targetSdkVersion 2");
+ assertContents(file, expected);
+ }
+
+ public void testCanParseValueChecksInitialization() {
+ GradleBuildFile file = getBadGradleBuildFile();
+ try {
+ file.canParseValue(GradleBuildFile.BuildSettingKey.TARGET_SDK_VERSION);
+ } catch (IllegalStateException e) {
+ // expected
+ return;
+ }
+ fail("Failed to get expected IllegalStateException");
+ }
+
+ public void testCanParseNestedValueChecksInitialization() {
+ GradleBuildFile file = getBadGradleBuildFile();
+ try {
+ file.canParseValue(getDummyClosure(), GradleBuildFile.BuildSettingKey.TARGET_SDK_VERSION);
+ } catch (IllegalStateException e) {
+ // expected
+ return;
+ }
+ fail("Failed to get expected IllegalStateException");
+ }
+
+ public void testGetClosureChecksInitialization() {
+ GradleBuildFile file = getBadGradleBuildFile();
+ try {
+ file.getClosure("/");
+ } catch (IllegalStateException e) {
+ // expected
+ return;
+ }
+ fail("Failed to get expected IllegalStateException");
+ }
+
+ public void testGetValueChecksInitialization() {
+ GradleBuildFile file = getBadGradleBuildFile();
+ try {
+ file.getValue(GradleBuildFile.BuildSettingKey.TARGET_SDK_VERSION);
+ } catch (IllegalStateException e) {
+ // expected
+ return;
+ }
+ fail("Failed to get expected IllegalStateException");
+ }
+
+ public void testGetNestedValueChecksInitialization() {
+ GradleBuildFile file = getBadGradleBuildFile();
+ try {
+ file.getValue(getDummyClosure(), GradleBuildFile.BuildSettingKey.TARGET_SDK_VERSION);
+ } catch (IllegalStateException e) {
+ // expected
+ return;
+ }
+ fail("Failed to get expected IllegalStateException");
+ }
+
+ public void testSetValueChecksInitialization() {
+ GradleBuildFile file = getBadGradleBuildFile();
+ try {
+ file.setValue(GradleBuildFile.BuildSettingKey.TARGET_SDK_VERSION, 2);
+ } catch (IllegalStateException e) {
+ // expected
+ return;
+ }
+ fail("Failed to get expected IllegalStateException");
+ }
+
+ public void testSetNestedValueChecksInitialization() {
+ GradleBuildFile file = getBadGradleBuildFile();
+ try {
+ file.setValue(getDummyClosure(), GradleBuildFile.BuildSettingKey.TARGET_SDK_VERSION, 2);
+ } catch (IllegalStateException e) {
+ // expected
+ return;
+ }
+ fail("Failed to get expected IllegalStateException");
+ }
+
+ public void testGetsPropertyFromRedundantBlock() throws Exception {
+ GradleBuildFile file = getTestFile(
+ "android {\n" +
+ " buildToolsVersion '17.0.0'\n" +
+ "}\n" +
+ "android {\n" +
+ " compileSdkVersion 17\n" +
+ "}\n"
+ );
+ assertEquals(17, file.getValue(GradleBuildFile.BuildSettingKey.COMPILE_SDK_VERSION));
+ assertEquals("17.0.0", file.getValue(GradleBuildFile.BuildSettingKey.BUILD_TOOLS_VERSION));
+ }
+
+ private static String getSimpleTestFile() throws IOException {
+ return
+ "buildscript {\n" +
+ " repositories {\n" +
+ " mavenCentral()\n" +
+ " }\n" +
+ " dependencies {\n" +
+ " classpath 'com.android.tools.build:gradle:0.5.+'\n" +
+ " }\n" +
+ "}\n" +
+ "apply plugin: 'android'\n" +
+ "\n" +
+ "repositories {\n" +
+ " mavenCentral()\n" +
+ "}\n" +
+ "\n" +
+ "dependencies {\n" +
+ " compile 'com.android.support:support-v4:13.0.+'\n" +
+ "}\n" +
+ "\n" +
+ "android {\n" +
+ " compileSdkVersion 17\n" +
+ " buildToolsVersion '17.0.0'\n" +
+ "\n" +
+ " defaultConfig {\n" +
+ " minSdkVersion someCrazyMethodCall()\n" +
+ " targetSdkVersion 1\n" +
+ " }\n" +
+ "}";
+ }
+
+ private GradleBuildFile getTestFile(String contents) throws IOException {
+ VirtualFile vf = getVirtualFile(createTempFile(SdkConstants.FN_BUILD_GRADLE, contents));
+ myDocument = FileDocumentManager.getInstance().getDocument(vf);
+ return new GradleBuildFile(vf, getProject());
+ }
+
+ private GradleBuildFile getBadGradleBuildFile() {
+ // Use an intentionally invalid file path so that GradleBuildFile will remain uninitialized. This simulates the condition of
+ // the PSI file not being parsed yet. GradleBuildFile will warn about the PSI file; this is expected.
+ VirtualFile vf = LocalFileSystem.getInstance().getRoot();
+ return new GradleBuildFile(vf, getProject());
+ }
+
+ private GrStatementOwner getDummyClosure() {
+ return GroovyPsiElementFactory.getInstance(myProject).createClosureFromText("{}");
+ }
+
+ private void assertContents(GradleBuildFile file, String expected) throws IOException {
+ PsiDocumentManager.getInstance(getProject()).commitDocument(myDocument);
+ String actual = myDocument.getText();
+ assertEquals(expected, actual);
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/parser/GradleSettingsFileTest.java b/android/testSrc/com/android/tools/idea/gradle/parser/GradleSettingsFileTest.java
new file mode 100644
index 0000000..e9a295b
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/gradle/parser/GradleSettingsFileTest.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.parser;
+
+import com.android.SdkConstants;
+import com.google.common.collect.Iterables;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.testFramework.IdeaTestCase;
+import com.intellij.util.ActionRunner;
+import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory;
+import org.jetbrains.plugins.groovy.lang.psi.api.util.GrStatementOwner;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+public class GradleSettingsFileTest extends IdeaTestCase {
+ private Document myDocument;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ myDocument = null;
+ }
+
+ public void testGetModules() throws Exception {
+ GradleSettingsFile file = getSimpleTestFile();
+ String[] expected = new String[] {
+ ":one", ":two", ":three"
+ };
+ assert Arrays.equals(expected, Iterables.toArray(file.getModules(), String.class));
+ }
+
+ public void testAddModuleToEmptyFile() throws Exception {
+ final GradleSettingsFile file = getEmptyTestFile();
+ ActionRunner.runInsideWriteAction(new ActionRunner.InterruptibleRunnable() {
+ @Override
+ public void run() throws Exception {
+ file.addModule(":one");
+ }
+ });
+ String expected = "include ':one'";
+ assertContents(file, expected);
+ }
+
+ public void testAddModuleToExistingFile() throws Exception {
+ final GradleSettingsFile file = getSimpleTestFile();
+ ActionRunner.runInsideWriteAction(new ActionRunner.InterruptibleRunnable() {
+ @Override
+ public void run() throws Exception {
+ file.addModule(":four");
+ }
+ });
+ String expected =
+ "include ':one', ':two', ':four'\n" +
+ "include ':three'\n" +
+ "include callSomeMethod()";
+
+ assertContents(file, expected);
+ }
+
+ public void testAddModuleToLineContainingMethodCall() throws Exception {
+ final GradleSettingsFile file = getMethodCallTestFile();
+ ActionRunner.runInsideWriteAction(new ActionRunner.InterruptibleRunnable() {
+ @Override
+ public void run() throws Exception {
+ file.addModule(":one");
+ }
+ });
+ String expected =
+ "include callSomeMethod(), ':one'";
+
+ assertContents(file, expected);
+ }
+
+ public void testRemovesFromLineWithMultipleModules() throws Exception {
+ final GradleSettingsFile file = getSimpleTestFile();
+ ActionRunner.runInsideWriteAction(new ActionRunner.InterruptibleRunnable() {
+ @Override
+ public void run() throws Exception {
+ file.removeModule(":two");
+ }
+ });
+ String expected =
+ "include ':one'\n" +
+ "include ':three'\n" +
+ "include callSomeMethod()";
+
+ assertContents(file, expected);
+ }
+
+ public void testRemovesEntireLine() throws Exception {
+ final GradleSettingsFile file = getSimpleTestFile();
+ ActionRunner.runInsideWriteAction(new ActionRunner.InterruptibleRunnable() {
+ @Override
+ public void run() throws Exception {
+ file.removeModule(":three");
+ }
+ });
+ String expected =
+ "include ':one', ':two'\n" +
+ "include callSomeMethod()";
+
+ assertContents(file, expected);
+ }
+
+ public void testRemovesMultipleEntries() throws Exception {
+ GradleSettingsFile file = getTestFile(
+ "include ':one'\n" +
+ "include ':one', ':two'"
+ );
+ file.removeModule(":one");
+ assertContents(file, "include ':two'");
+ }
+
+ public void testAddModuleStringChecksInitialization() {
+ GradleSettingsFile file = getBadGradleSettingsFile();
+ try {
+ file.addModule("asdf");
+ } catch (IllegalStateException e) {
+ // expected
+ return;
+ }
+ fail("Failed to get expected IllegalStateException");
+ }
+
+ public void testAddModuleChecksInitialization() {
+ GradleSettingsFile file = getBadGradleSettingsFile();
+ try {
+ file.addModule(myModule);
+ } catch (IllegalStateException e) {
+ // expected
+ return;
+ }
+ fail("Failed to get expected IllegalStateException");
+ }
+
+ public void testGetModulesChecksInitialization() {
+ GradleSettingsFile file = getBadGradleSettingsFile();
+ try {
+ file.getModules();
+ } catch (IllegalStateException e) {
+ // expected
+ return;
+ }
+ fail("Failed to get expected IllegalStateException");
+ }
+
+ public void testRemoveModuleStringChecksInitialization() {
+ GradleSettingsFile file = getBadGradleSettingsFile();
+ try {
+ file.removeModule("asdf");
+ } catch (IllegalStateException e) {
+ // expected
+ return;
+ }
+ fail("Failed to get expected IllegalStateException");
+ }
+
+ public void testRemoveModuleChecksInitialization() {
+ GradleSettingsFile file = getBadGradleSettingsFile();
+ try {
+ file.removeModule(myModule);
+ } catch (IllegalStateException e) {
+ // expected
+ return;
+ }
+ fail("Failed to get expected IllegalStateException");
+ }
+
+ private GradleSettingsFile getSimpleTestFile() throws IOException {
+ String contents =
+ "include ':one', ':two'\n" +
+ "include ':three'\n" +
+ "include callSomeMethod()";
+ return getTestFile(contents);
+ }
+
+ private GradleSettingsFile getMethodCallTestFile() throws IOException {
+ String contents =
+ "include callSomeMethod()";
+ return getTestFile(contents);
+ }
+
+ private GradleSettingsFile getEmptyTestFile() throws IOException {
+ return getTestFile("");
+ }
+
+ private GradleSettingsFile getTestFile(String contents) throws IOException {
+ VirtualFile vf = getVirtualFile(createTempFile(SdkConstants.FN_SETTINGS_GRADLE, contents));
+ myDocument = FileDocumentManager.getInstance().getDocument(vf);
+ return new GradleSettingsFile(vf, getProject());
+ }
+
+ private GradleSettingsFile getBadGradleSettingsFile() {
+ // Use an intentionally invalid file path so that GradleSettingsFile will remain uninitialized. This simulates the condition of
+ // the PSI file not being parsed yet. GradleSettingsFile will warn about the PSI file; this is expected.
+ VirtualFile vf = LocalFileSystem.getInstance().getRoot();
+ return new GradleSettingsFile(vf, getProject());
+ }
+
+ private void assertContents(GradleSettingsFile file, String expected) throws IOException {
+ PsiDocumentManager.getInstance(getProject()).commitDocument(myDocument);
+ String actual = myDocument.getText();
+ assertEquals(expected, actual);
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/model/AndroidContentRootTest.java b/android/testSrc/com/android/tools/idea/gradle/project/AndroidContentRootTest.java
similarity index 91%
rename from android/testSrc/com/android/tools/idea/gradle/model/AndroidContentRootTest.java
rename to android/testSrc/com/android/tools/idea/gradle/project/AndroidContentRootTest.java
index 60da854..c15c6ca 100644
--- a/android/testSrc/com/android/tools/idea/gradle/model/AndroidContentRootTest.java
+++ b/android/testSrc/com/android/tools/idea/gradle/project/AndroidContentRootTest.java
@@ -13,12 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.tools.idea.gradle.model;
+package com.android.tools.idea.gradle.project;
import com.android.tools.idea.gradle.ContentRootSourcePaths;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.android.tools.idea.gradle.TestProjects;
-import com.android.tools.idea.gradle.model.AndroidContentRoot.ContentRootStorage;
+import com.android.tools.idea.gradle.project.AndroidContentRoot;
+import com.android.tools.idea.gradle.project.AndroidContentRoot.ContentRootStorage;
import com.android.tools.idea.gradle.stubs.android.AndroidProjectStub;
import com.android.tools.idea.gradle.stubs.android.VariantStub;
import com.google.common.collect.Maps;
@@ -33,7 +34,7 @@
import java.util.Map;
/**
- * Tests for {@link com.android.tools.idea.gradle.model.AndroidContentRoot}.
+ * Tests for {@link com.android.tools.idea.gradle.project.AndroidContentRoot}.
*/
public class AndroidContentRootTest extends TestCase {
private ContentRootSourcePaths myExpectedSourcePaths;
@@ -94,7 +95,7 @@
String rootDirPath = myAndroidProject.getRootDir().getAbsolutePath();
IdeaAndroidProject project =
- new IdeaAndroidProject(myAndroidProject.getName(), rootDirPath, rootDirPath, myAndroidProject, selectedVariant.getName());
+ new IdeaAndroidProject(myAndroidProject.getName(), rootDirPath, myAndroidProject, selectedVariant.getName());
AndroidContentRoot.storePaths(project, myStorage);
myExpectedSourcePaths.storeExpectedSourcePaths(myAndroidProject);
diff --git a/android/testSrc/com/android/tools/idea/gradle/project/AndroidGradleProjectResolverTest.java b/android/testSrc/com/android/tools/idea/gradle/project/AndroidGradleProjectResolverTest.java
index 56b76a4..3518ec5 100644
--- a/android/testSrc/com/android/tools/idea/gradle/project/AndroidGradleProjectResolverTest.java
+++ b/android/testSrc/com/android/tools/idea/gradle/project/AndroidGradleProjectResolverTest.java
@@ -15,17 +15,32 @@
*/
package com.android.tools.idea.gradle.project;
-import com.android.tools.idea.gradle.project.AndroidGradleProjectResolver.ProjectResolverFunctionFactory;
+import com.android.tools.idea.gradle.ContentRootSourcePaths;
+import com.android.tools.idea.gradle.TestProjects;
+import com.android.tools.idea.gradle.stubs.android.AndroidProjectStub;
+import com.android.tools.idea.gradle.stubs.gradle.IdeaModuleStub;
+import com.android.tools.idea.gradle.stubs.gradle.IdeaProjectStub;
+import com.google.common.collect.Lists;
import com.intellij.openapi.externalSystem.model.DataNode;
+import com.intellij.openapi.externalSystem.model.ProjectKeys;
+import com.intellij.openapi.externalSystem.model.project.ContentRootData;
+import com.intellij.openapi.externalSystem.model.project.ExternalSystemSourceType;
+import com.intellij.openapi.externalSystem.model.project.ModuleData;
import com.intellij.openapi.externalSystem.model.project.ProjectData;
-import com.intellij.openapi.externalSystem.model.task.*;
-import com.intellij.util.Function;
+import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId;
+import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType;
+import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
+import com.intellij.openapi.util.io.FileUtil;
import junit.framework.TestCase;
+import org.gradle.tooling.ModelBuilder;
import org.gradle.tooling.ProjectConnection;
+import org.gradle.tooling.model.idea.IdeaProject;
import org.jetbrains.annotations.NotNull;
-import org.jetbrains.plugins.gradle.service.project.GradleExecutionHelper;
import org.jetbrains.plugins.gradle.settings.GradleExecutionSettings;
+import java.util.Collections;
+import java.util.List;
+
import static com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListenerAdapter.NULL_OBJECT;
import static org.easymock.classextension.EasyMock.*;
@@ -33,36 +48,111 @@
* Tests for {@link AndroidGradleProjectResolver}.
*/
public class AndroidGradleProjectResolverTest extends TestCase {
- private GradleExecutionHelper myHelper;
- private ProjectResolverFunctionFactory myFunctionFactory;
- private Function<ProjectConnection, DataNode<ProjectData>> myProjectResolverFunction;
+ private ContentRootSourcePaths myExpectedSourcePaths;
+ private IdeaProjectStub myIdeaProject;
+ private AndroidProjectStub myAndroidProject;
+ private ExternalSystemTaskId myId;
+ private ProjectConnection myConnection;
+ private GradleExecutionHelperDouble myHelper;
+ private GradleExecutionSettings mySettings;
private AndroidGradleProjectResolver myProjectResolver;
+ private IdeaModuleStub myUtilModule;
- @SuppressWarnings("unchecked")
@Override
public void setUp() throws Exception {
super.setUp();
- myHelper = createMock(GradleExecutionHelper.class);
- myFunctionFactory = createMock(ProjectResolverFunctionFactory.class);
- myProjectResolverFunction = createMock(Function.class);
- myProjectResolver = new AndroidGradleProjectResolver(myHelper, myFunctionFactory);
+ myExpectedSourcePaths = new ContentRootSourcePaths();
+ myIdeaProject = new IdeaProjectStub("multiProject");
+ myAndroidProject = TestProjects.createBasicProject(myIdeaProject.getRootDir());
+ myIdeaProject.addModule(myAndroidProject.getName(), "androidTask");
+ myUtilModule = myIdeaProject.addModule("util", "compileJava", "jar", "classes");
+ myIdeaProject.addModule("notReallyAGradleProject");
+ myId = ExternalSystemTaskId.create(ExternalSystemTaskType.RESOLVE_PROJECT, "dummy");
+ myConnection = createMock(ProjectConnection.class);
+ myHelper = GradleExecutionHelperDouble.newMock();
+ mySettings = createMock(GradleExecutionSettings.class);
+ myProjectResolver = new AndroidGradleProjectResolver(myHelper, createMock(ProjectImportErrorHandler.class));
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ if (myIdeaProject != null) {
+ myIdeaProject.dispose();
+ }
+ super.tearDown();
}
@SuppressWarnings("unchecked")
- public void testResolveProjectInfo() throws Exception {
- ExternalSystemTaskId id = ExternalSystemTaskId.create(ExternalSystemTaskType.RESOLVE_PROJECT, "1");
- String projectPath = "~/basic/build.gradle";
- GradleExecutionSettings settings = createMock(GradleExecutionSettings.class);
+ public void testResolveProjectInfo() {
+ // Quick test of isIdeaTask
+ assertTrue(AndroidGradleProjectResolver.isIdeaTask("idea"));
+ assertTrue(AndroidGradleProjectResolver.isIdeaTask("ideaFoo"));
+ assertFalse(AndroidGradleProjectResolver.isIdeaTask("ideal"));
- expect(myFunctionFactory.createFunction(id, projectPath, settings, NULL_OBJECT)).andReturn(myProjectResolverFunction);
- DataNode<ProjectData> projectInfo = createMock(DataNode.class);
- expect(myHelper.execute(projectPath, settings, myProjectResolverFunction)).andReturn(projectInfo);
+ // Record mock expectations.
+ ModelBuilder<IdeaProject> ideaProjectModelBuilder = createMock(ModelBuilder.class);
+ myHelper.getModelBuilder(IdeaProject.class, myId, mySettings, myConnection, NULL_OBJECT, Collections.<String>emptyList());
+ expectLastCall().andReturn(ideaProjectModelBuilder);
- replay(myFunctionFactory, myHelper);
+ // Simulate retrieval of the top-level IdeaProject.
+ expect(ideaProjectModelBuilder.get()).andReturn(myIdeaProject);
- DataNode<ProjectData> resolved = myProjectResolver.resolveProjectInfo(id, projectPath, true, settings, NULL_OBJECT);
- assertSame(projectInfo, resolved);
+ // Simulate retrieval of AndroidProject from IdeaModule 'basic'
+ myHelper.setExecutionResult(myAndroidProject);
- verify(myFunctionFactory, myHelper);
+ replay(myConnection, myHelper, ideaProjectModelBuilder);
+
+ // Code under test.
+ String projectPath = myIdeaProject.getBuildFile().getParentFile().getPath();
+ List<String> extraJvmArgs = Collections.emptyList();
+ DataNode<ProjectData> projectInfo =
+ myProjectResolver.resolveProjectInfo(myId, projectPath, myConnection, NULL_OBJECT, extraJvmArgs, mySettings);
+
+ // Verify mock expectations.
+ verify(myConnection, myHelper, ideaProjectModelBuilder);
+
+ // Verify project.
+ assertNotNull(projectInfo);
+ ProjectData projectData = projectInfo.getData();
+ assertEquals(myIdeaProject.getName(), projectData.getName());
+ assertEquals(FileUtil.toSystemIndependentName(myIdeaProject.getRootDir().getAbsolutePath()),
+ projectData.getIdeProjectFileDirectoryPath());
+
+ // Verify modules.
+ List<DataNode<ModuleData>> modules = Lists.newArrayList(ExternalSystemApiUtil.getChildren(projectInfo, ProjectKeys.MODULE));
+ assertEquals("Module count", 2, modules.size());
+
+ // Verify 'basic' module.
+ DataNode<ModuleData> moduleInfo = modules.get(0);
+ ModuleData moduleData = moduleInfo.getData();
+ assertEquals(myAndroidProject.getName(), moduleData.getName());
+
+ // Verify content root in 'basic' module.
+ List<DataNode<ContentRootData>> contentRoots = Lists.newArrayList(ExternalSystemApiUtil.getChildren(moduleInfo, ProjectKeys.CONTENT_ROOT));
+ assertEquals(1, contentRoots.size());
+
+ String projectRootDirPath = FileUtil.toSystemIndependentName(myAndroidProject.getRootDir().getAbsolutePath());
+ ContentRootData contentRootData = contentRoots.get(0).getData();
+ assertEquals(projectRootDirPath, contentRootData.getRootPath());
+ myExpectedSourcePaths.storeExpectedSourcePaths(myAndroidProject);
+ assertCorrectStoredDirPaths(contentRootData, ExternalSystemSourceType.SOURCE);
+ assertCorrectStoredDirPaths(contentRootData, ExternalSystemSourceType.TEST);
+
+ // Verify 'util' module.
+ moduleInfo = modules.get(1);
+ moduleData = moduleInfo.getData();
+ assertEquals(myUtilModule.getName(), moduleData.getName());
+
+ // Verify content root in 'util' module.
+ contentRoots = Lists.newArrayList(ExternalSystemApiUtil.getChildren(moduleInfo, ProjectKeys.CONTENT_ROOT));
+ assertEquals(1, contentRoots.size());
+
+ String moduleRootDirPath = FileUtil.toSystemIndependentName(myUtilModule.getRootDir().getPath());
+ contentRootData = contentRoots.get(0).getData();
+ assertEquals(moduleRootDirPath, contentRootData.getRootPath());
+ }
+
+ private void assertCorrectStoredDirPaths(@NotNull ContentRootData contentRootData, @NotNull ExternalSystemSourceType sourceType) {
+ myExpectedSourcePaths.assertCorrectStoredDirPaths(contentRootData.getPaths(sourceType), sourceType);
}
}
diff --git a/android/testSrc/com/android/tools/idea/gradle/project/GradleDependenciesTest.java b/android/testSrc/com/android/tools/idea/gradle/project/GradleDependenciesTest.java
deleted file mode 100644
index c178ce4..0000000
--- a/android/testSrc/com/android/tools/idea/gradle/project/GradleDependenciesTest.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tools.idea.gradle.project;
-
-import com.android.tools.idea.gradle.stubs.gradle.IdeaModuleDependencyStub;
-import com.android.tools.idea.gradle.stubs.gradle.IdeaModuleStub;
-import com.android.tools.idea.gradle.stubs.gradle.IdeaProjectStub;
-import com.android.tools.idea.gradle.stubs.gradle.IdeaSingleEntryLibraryDependencyStub;
-import com.intellij.openapi.externalSystem.model.DataNode;
-import com.intellij.openapi.externalSystem.model.ProjectKeys;
-import com.intellij.openapi.externalSystem.model.project.*;
-import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
-import com.intellij.openapi.module.StdModuleTypes;
-import com.intellij.openapi.roots.DependencyScope;
-import com.intellij.openapi.util.io.FileUtil;
-import com.intellij.util.containers.ContainerUtil;
-import junit.framework.TestCase;
-import org.jetbrains.plugins.gradle.util.GradleConstants;
-
-import java.io.File;
-import java.util.Collection;
-import java.util.Set;
-
-/**
- * Tests for {@link GradleDependencies}.
- */
-public class GradleDependenciesTest extends TestCase {
- private IdeaProjectStub myProject;
- private IdeaModuleStub myModule;
- private DataNode<ProjectData> myProjectInfo;
- private DataNode<ModuleData> myModuleInfo;
-
- @Override
- public void setUp() throws Exception {
- super.setUp();
- myProject = new IdeaProjectStub("basic");
-
- String projectName = myProject.getName();
- myModule = myProject.addModule(projectName);
-
- String rootDirPath = myProject.getRootDir().getAbsolutePath();
- String buildFilePath = myProject.getBuildFile().getAbsolutePath();
-
- ProjectData projectData = new ProjectData(GradleConstants.SYSTEM_ID, rootDirPath, buildFilePath);
- projectData.setName(projectName);
- myProjectInfo = new DataNode<ProjectData>(ProjectKeys.PROJECT, projectData, null);
-
- String configPath = rootDirPath + "/build.gradle";
- ModuleData moduleData = new ModuleData(GradleConstants.SYSTEM_ID, StdModuleTypes.JAVA.getId(), projectName, rootDirPath, configPath);
- myModuleInfo = myProjectInfo.createChild(ProjectKeys.MODULE, moduleData);
- }
-
- public void testAddToWithJarDependency() {
- File jarFile = new File("~/repo/guava/guava-11.0.2.jar");
- IdeaSingleEntryLibraryDependencyStub dependency = new IdeaSingleEntryLibraryDependencyStub(jarFile);
- myModule.addDependency(dependency);
-
- File sourceFile = new File("~/repo/guava/guava-11.0.2-src.jar");
- dependency.setSource(sourceFile);
-
- File javadocFile = new File("~/repo/guava/guava-11.0.2-javadoc.jar");
- dependency.setJavadoc(javadocFile);
-
- GradleDependencies.populate(myModuleInfo, myProjectInfo, myModule);
-
- Collection<DataNode<LibraryDependencyData>> deps = ExternalSystemApiUtil.getChildren(myModuleInfo, ProjectKeys.LIBRARY_DEPENDENCY);
- assertEquals(1, deps.size());
-
- DataNode<LibraryDependencyData> dependencyInfo = ContainerUtil.getFirstItem(deps);
- assertNotNull(dependencyInfo);
-
- LibraryDependencyData dependencyData = dependencyInfo.getData();
- assertEquals("guava-11.0.2", dependencyData.getName());
- assertEquals(DependencyScope.COMPILE, dependencyData.getScope());
-
- LibraryData libraryData = dependencyData.getTarget();
-
- Set<String> binaryPaths = libraryData.getPaths(LibraryPathType.BINARY);
- assertEquals(1, binaryPaths.size());
- String binaryPath = ContainerUtil.getFirstItem(binaryPaths);
- assertEquals(FileUtil.toSystemIndependentName(jarFile.getAbsolutePath()), binaryPath);
-
- Set<String> sourcePaths = libraryData.getPaths(LibraryPathType.SOURCE);
- assertEquals(1, sourcePaths.size());
- String sourcePath = ContainerUtil.getFirstItem(sourcePaths);
- assertEquals(FileUtil.toSystemIndependentName(sourceFile.getAbsolutePath()), sourcePath);
-
- Set<String> javadocPaths = libraryData.getPaths(LibraryPathType.DOC);
- assertEquals(1, javadocPaths.size());
- String javadocPath = ContainerUtil.getFirstItem(javadocPaths);
- assertEquals(FileUtil.toSystemIndependentName(javadocFile.getAbsolutePath()), javadocPath);
- }
-
- public void testAddToWithModuleDependency() {
- String dependencyModuleName = "util";
-
- String rootDirPath = myProject.getRootDir().getAbsolutePath();
- String configPath = rootDirPath + "/build.gradle";
- ModuleData moduleData
- = new ModuleData(GradleConstants.SYSTEM_ID, StdModuleTypes.JAVA.getId(), dependencyModuleName, rootDirPath, configPath);
- myProjectInfo.createChild(ProjectKeys.MODULE, moduleData);
-
- IdeaModuleStub dependencyModule = myProject.addModule(dependencyModuleName);
- IdeaModuleDependencyStub dependency = new IdeaModuleDependencyStub(dependencyModule);
- myModule.addDependency(dependency);
-
- GradleDependencies.populate(myModuleInfo, myProjectInfo, myModule);
-
- Collection<DataNode<ModuleDependencyData>> deps = ExternalSystemApiUtil.getChildren(myModuleInfo, ProjectKeys.MODULE_DEPENDENCY);
- assertEquals(1, deps.size());
-
- DataNode<ModuleDependencyData> dependencyInfo = ContainerUtil.getFirstItem(deps);
- assertNotNull(dependencyInfo);
-
- ModuleDependencyData dependencyData = dependencyInfo.getData();
- assertEquals(dependencyModuleName, dependencyData.getName());
- assertEquals(DependencyScope.COMPILE, dependencyData.getScope());
- }
-}
diff --git a/android/testSrc/com/android/tools/idea/gradle/project/GradleModelVersionCheckTest.java b/android/testSrc/com/android/tools/idea/gradle/project/GradleModelVersionCheckTest.java
new file mode 100644
index 0000000..c629acd
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/gradle/project/GradleModelVersionCheckTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.project;
+
+import com.android.builder.model.AndroidProject;
+import junit.framework.TestCase;
+
+import static org.easymock.EasyMock.*;
+
+/**
+ * Tests for {@link GradleModelVersionCheck}.
+ */
+public class GradleModelVersionCheckTest extends TestCase {
+ private AndroidProject myProject;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ myProject = createMock(AndroidProject.class);
+ }
+
+ public void testIsSupportedVersionWithNullVersion() {
+ expect(myProject.getModelVersion()).andReturn(null);
+ replay(myProject);
+
+ assertFalse(GradleModelVersionCheck.isSupportedVersion(myProject));
+
+ verify(myProject);
+ }
+
+ public void testIsSupportedVersionWithEmptyVersion() {
+ expect(myProject.getModelVersion()).andReturn("");
+ replay(myProject);
+
+ assertFalse(GradleModelVersionCheck.isSupportedVersion(myProject));
+
+ verify(myProject);
+ }
+
+ public void testIsSupportedVersionWithOldVersion() {
+ expect(myProject.getModelVersion()).andReturn("0.4.3");
+ replay(myProject);
+
+ assertFalse(GradleModelVersionCheck.isSupportedVersion(myProject));
+
+ verify(myProject);
+ }
+
+ public void testIsSupportedVersionWithMinimumSupportedVersion() {
+ expect(myProject.getModelVersion()).andReturn("0.5.0");
+ replay(myProject);
+
+ assertTrue(GradleModelVersionCheck.isSupportedVersion(myProject));
+
+ verify(myProject);
+ }
+
+ public void testIsSupportedVersionWithSupportedVersionWithMacroGreaterThanZero() {
+ expect(myProject.getModelVersion()).andReturn("0.5.1");
+ replay(myProject);
+
+ assertTrue(GradleModelVersionCheck.isSupportedVersion(myProject));
+
+ verify(myProject);
+ }
+
+ public void testIsSupportedVersionWithSupportedVersionWithMinorGreaterThanFive() {
+ expect(myProject.getModelVersion()).andReturn("0.6.0");
+ replay(myProject);
+
+ assertTrue(GradleModelVersionCheck.isSupportedVersion(myProject));
+
+ verify(myProject);
+ }
+
+ public void testIsSupportedVersionWithSupportedVersionWithMajorGreaterThanZero() {
+ expect(myProject.getModelVersion()).andReturn("1.0.0");
+ replay(myProject);
+
+ assertTrue(GradleModelVersionCheck.isSupportedVersion(myProject));
+
+ verify(myProject);
+ }
+
+ public void testIsSupportedVersionWithSupportedSnapshotVersion() {
+ expect(myProject.getModelVersion()).andReturn("0.5.0-SNAPSHOT");
+ replay(myProject);
+
+ assertTrue(GradleModelVersionCheck.isSupportedVersion(myProject));
+
+ verify(myProject);
+ }
+
+ public void testIsSupportedVersionWithUnparseableVersion() {
+ expect(myProject.getModelVersion()).andReturn("Hello");
+ replay(myProject);
+
+ assertFalse(GradleModelVersionCheck.isSupportedVersion(myProject));
+
+ verify(myProject);
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/GradleProjectImporterTest.java b/android/testSrc/com/android/tools/idea/gradle/project/GradleProjectImporterTest.java
similarity index 86%
rename from android/testSrc/com/android/tools/idea/gradle/GradleProjectImporterTest.java
rename to android/testSrc/com/android/tools/idea/gradle/project/GradleProjectImporterTest.java
index a734884..8039134 100644
--- a/android/testSrc/com/android/tools/idea/gradle/GradleProjectImporterTest.java
+++ b/android/testSrc/com/android/tools/idea/gradle/project/GradleProjectImporterTest.java
@@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.tools.idea.gradle;
+package com.android.tools.idea.gradle.project;
+import com.android.SdkConstants;
import com.intellij.openapi.externalSystem.model.DataNode;
import com.intellij.openapi.externalSystem.model.ProjectKeys;
import com.intellij.openapi.externalSystem.model.project.ModuleData;
@@ -25,7 +26,6 @@
import com.intellij.openapi.module.StdModuleTypes;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.project.Project;
-import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.testFramework.IdeaTestCase;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.plugins.gradle.util.GradleConstants;
@@ -49,7 +49,7 @@
myProjectRootDir = createTempDir(myProjectName);
String projectRootDirPath = myProjectRootDir.getAbsolutePath();
- final File projectFile = new File(myProjectRootDir, "build.gradle");
+ final File projectFile = new File(myProjectRootDir, SdkConstants.FN_BUILD_GRADLE);
final String configPath = projectFile.getAbsolutePath();
ProjectData projectData = new ProjectData(GradleConstants.SYSTEM_ID, projectRootDirPath, configPath);
projectData.setName(myProjectName);
@@ -61,11 +61,10 @@
GradleProjectImporter.ImporterDelegate delegate = new GradleProjectImporter.ImporterDelegate() {
@Override
- void importProject(@NotNull Project newProject, @NotNull String projectFilePath, @NotNull ExternalProjectRefreshCallback callback)
+ void importProject(@NotNull Project project, @NotNull ExternalProjectRefreshCallback callback, boolean modal)
throws ConfigurationException {
- assertNotNull(newProject);
- assertEquals(myProjectName, newProject.getName());
- assertEquals(configPath, projectFilePath);
+ assertNotNull(project);
+ assertEquals(myProjectName, project.getName());
callback.onSuccess(myProjectInfo);
}
@@ -86,11 +85,8 @@
}
public void testImportProject() throws Exception {
- Sdk sdk = getTestProjectJdk();
- assertNotNull(sdk);
-
MyCallback callback = new MyCallback();
- myImporter.importProject(myProjectName, myProjectRootDir, sdk, callback);
+ myImporter.importProject(myProjectName, myProjectRootDir, callback);
}
private class MyCallback implements GradleProjectImporter.Callback {
@@ -111,5 +107,10 @@
assertEquals(1, modules.length);
assertEquals(myProjectName, modules[0].getName());
}
+
+ @Override
+ public void importFailed(@NotNull Project project, @NotNull String errorMessage) {
+ fail(errorMessage);
+ }
}
}
diff --git a/android/testSrc/com/android/tools/idea/gradle/project/ImportedDependencyUpdaterTest.java b/android/testSrc/com/android/tools/idea/gradle/project/ImportedDependencyUpdaterTest.java
new file mode 100644
index 0000000..0804a64
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/gradle/project/ImportedDependencyUpdaterTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.project;
+
+import com.android.SdkConstants;
+import com.android.tools.idea.gradle.AndroidProjectKeys;
+import com.android.tools.idea.gradle.ProjectImportEventMessage;
+import com.android.tools.idea.gradle.dependency.LibraryDependency;
+import com.android.tools.idea.gradle.dependency.ModuleDependency;
+import com.google.common.collect.Lists;
+import com.google.common.io.Files;
+import com.intellij.openapi.externalSystem.model.DataNode;
+import com.intellij.openapi.externalSystem.model.ProjectKeys;
+import com.intellij.openapi.externalSystem.model.project.*;
+import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
+import com.intellij.openapi.module.StdModuleTypes;
+import com.intellij.openapi.roots.DependencyScope;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.util.BooleanFunction;
+import com.intellij.util.containers.ContainerUtil;
+import junit.framework.TestCase;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.gradle.util.GradleConstants;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Tests for {@link ImportedDependencyUpdater}.
+ */
+public class ImportedDependencyUpdaterTest extends TestCase {
+ private File myTempDir;
+ private DataNode<ProjectData> myProjectInfo;
+ private DataNode<ModuleData> myModuleInfo;
+ private ImportedDependencyUpdater importer;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ myTempDir = Files.createTempDir();
+
+ File projectDir = new File(myTempDir, "project1");
+ FileUtil.createDirectory(projectDir);
+ File buildFile = new File(projectDir, SdkConstants.FN_BUILD_GRADLE);
+ FileUtil.createIfDoesntExist(buildFile);
+ ProjectData projectData = new ProjectData(GradleConstants.SYSTEM_ID, projectDir.getPath(), buildFile.getPath());
+
+ myProjectInfo = new DataNode<ProjectData>(ProjectKeys.PROJECT, projectData, null);
+ myModuleInfo = addModule("module1");
+ addModule("module2");
+
+ importer = new ImportedDependencyUpdater(myProjectInfo);
+ }
+
+ @NotNull
+ private DataNode<ModuleData> addModule(@NotNull String moduleName) {
+ ModuleData moduleData = new ModuleData(GradleConstants.SYSTEM_ID, StdModuleTypes.JAVA.getId(), moduleName,
+ myProjectInfo.getData().getIdeProjectFileDirectoryPath(), "");
+ return myProjectInfo.createChild(ProjectKeys.MODULE, moduleData);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ if (myTempDir != null) {
+ FileUtil.delete(myTempDir);
+ }
+ super.tearDown();
+ }
+
+ public void testImportDependenciesWithLibraryDependency() {
+ File jarFile = new File("~/repo/guava/guava-11.0.2.jar");
+ final LibraryDependency dependency = new LibraryDependency(jarFile, DependencyScope.TEST);
+ List<LibraryDependency> dependencies = Lists.newArrayList(dependency);
+
+ importer.updateDependencies(myModuleInfo, dependencies);
+
+ // verify that a library was added.
+ DataNode<LibraryData> libraryInfo =
+ ExternalSystemApiUtil.find(myProjectInfo, ProjectKeys.LIBRARY, new BooleanFunction<DataNode<LibraryData>>() {
+ @Override
+ public boolean fun(DataNode<LibraryData> node) {
+ LibraryData other = node.getData();
+ return dependency.getName().equals(other.getName());
+ }
+ });
+ assertNotNull(libraryInfo);
+ LibraryData libraryData = libraryInfo.getData();
+ Set<String> paths = libraryData.getPaths(LibraryPathType.BINARY);
+ assertEquals(1, paths.size());
+ String actualPath = ContainerUtil.getFirstItem(paths);
+ assertNotNull(actualPath);
+ assertTrue(actualPath.endsWith(jarFile.getPath()));
+
+ // verify that the library dependency was added.
+ DataNode<LibraryDependencyData> dependencyInfo = findLibraryDependency(dependency);
+ assertNotNull(dependencyInfo);
+ LibraryDependencyData dependencyData = dependencyInfo.getData();
+ assertEquals(DependencyScope.TEST, dependencyData.getScope());
+ assertSame(libraryData, dependencyData.getTarget());
+ assertTrue(dependencyData.isExported());
+ }
+
+ public void testImportDependenciesWithModuleDependency() {
+ final ModuleDependency dependency = new ModuleDependency("abc:module2", DependencyScope.TEST);
+ List<ModuleDependency> dependencies = Lists.newArrayList(dependency);
+
+ importer.updateDependencies(myModuleInfo, dependencies);
+
+ // verify that the module dependency was added.
+ DataNode<ModuleDependencyData> dependencyInfo =
+ ExternalSystemApiUtil.find(myModuleInfo, ProjectKeys.MODULE_DEPENDENCY, new BooleanFunction<DataNode<ModuleDependencyData>>() {
+ @Override
+ public boolean fun(DataNode<ModuleDependencyData> node) {
+ ModuleDependencyData data = node.getData();
+ return dependency.getName().equals(data.getName());
+ }
+ });
+ assertNotNull(dependencyInfo);
+ ModuleDependencyData dependencyData = dependencyInfo.getData();
+ assertEquals(DependencyScope.TEST, dependencyData.getScope());
+ assertTrue(dependencyData.isExported());
+ }
+
+ public void testImportDependenciesWithNonExistingModuleAndBackupJar() {
+ ModuleDependency dependency = new ModuleDependency("abc:module3", DependencyScope.TEST);
+ File jarFile = new File("~/repo/guava/guava-11.0.2.jar");
+ final LibraryDependency backup = new LibraryDependency(jarFile, DependencyScope.TEST);
+ dependency.setBackupDependency(backup);
+
+ List<ModuleDependency> dependencies = Lists.newArrayList(dependency);
+
+ importer.updateDependencies(myModuleInfo, dependencies);
+
+ verifyZeroModuleDependencies();
+
+ // verify that the library dependency was added.
+ DataNode<LibraryDependencyData> dependencyInfo = findLibraryDependency(backup);
+ assertNotNull(dependencyInfo);
+
+ assertMessageLogged();
+ }
+
+ @Nullable
+ private DataNode<LibraryDependencyData> findLibraryDependency(@NotNull final com.android.tools.idea.gradle.dependency.LibraryDependency source) {
+ return ExternalSystemApiUtil.find(myModuleInfo, ProjectKeys.LIBRARY_DEPENDENCY, new BooleanFunction<DataNode<LibraryDependencyData>>() {
+ @Override
+ public boolean fun(DataNode<LibraryDependencyData> node) {
+ LibraryDependencyData data = node.getData();
+ LibraryData other = data.getTarget();
+ return source.getName().equals(other.getName());
+ }
+ });
+ }
+
+ public void testImportDependenciesWithNonExistingModuleAndWithoutBackupJar() {
+ ModuleDependency dependency = new ModuleDependency("abc:module3", DependencyScope.TEST);
+ List<ModuleDependency> dependencies = Lists.newArrayList(dependency);
+
+ importer.updateDependencies(myModuleInfo, dependencies);
+
+ // verify there are no dependencies.
+ verifyZeroModuleDependencies();
+ assertEquals(0, ExternalSystemApiUtil.findAll(myModuleInfo, ProjectKeys.LIBRARY_DEPENDENCY).size());
+
+ assertMessageLogged();
+ }
+
+ private void verifyZeroModuleDependencies() {
+ assertEquals(0, ExternalSystemApiUtil.findAll(myModuleInfo, ProjectKeys.MODULE_DEPENDENCY).size());
+ }
+
+ private void assertMessageLogged() {
+ Collection<DataNode<ProjectImportEventMessage>> messages =
+ ExternalSystemApiUtil.findAll(myModuleInfo, AndroidProjectKeys.IMPORT_EVENT_MSG);
+ assertEquals(1, messages.size());
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/project/ProjectImportErrorHandlerTest.java b/android/testSrc/com/android/tools/idea/gradle/project/ProjectImportErrorHandlerTest.java
new file mode 100644
index 0000000..0a607d9
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/gradle/project/ProjectImportErrorHandlerTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.project;
+
+import junit.framework.TestCase;
+import org.gradle.api.internal.LocationAwareException;
+import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry;
+
+/**
+ * Tests for {@link ProjectImportErrorHandler}.
+ */
+@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
+public class ProjectImportErrorHandlerTest extends TestCase {
+ private ProjectImportErrorHandler myErrorHandler;
+ private String myProjectPath;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ myErrorHandler = new ProjectImportErrorHandler();
+ myProjectPath = "basic";
+ }
+
+ public void testGetUserFriendlyErrorWithOldGradleVersion() {
+ ClassNotFoundException rootCause = new ClassNotFoundException(ToolingModelBuilderRegistry.class.getName());
+ Throwable error = new Throwable(rootCause);
+ RuntimeException realCause = myErrorHandler.getUserFriendlyError(error, myProjectPath, null);
+ assertTrue(realCause.getMessage().contains("old, unsupported version of Gradle"));
+ }
+
+ public void testGetUserFriendlyErrorWithMissingAndroidSupportRepository() {
+ RuntimeException rootCause = new RuntimeException("Could not find any version that matches com.android.support:support-v4:13.0.+");
+ Throwable error = new Throwable(rootCause);
+ RuntimeException realCause = myErrorHandler.getUserFriendlyError(error, myProjectPath, null);
+ assertTrue(realCause.getMessage().contains("Please install the Android Support Repository"));
+ }
+
+ public void testGetUserFriendlyErrorWithMissingAndroidSupportRepository2() {
+ RuntimeException rootCause = new RuntimeException("Could not find com.android.support:support-v4:13.0.0");
+ Throwable error = new Throwable(rootCause);
+ RuntimeException realCause = myErrorHandler.getUserFriendlyError(error, myProjectPath, null);
+ assertTrue(realCause.getMessage().contains("Please install the Android Support Repository"));
+ }
+
+ public void testGetUserFriendlyErrorWithPlatformVersionNotFound() {
+ String causeMsg = "failed to find target current";
+ RuntimeException rootCause = new IllegalStateException(causeMsg);
+ String locationMsg = "Build file '~/project/build.gradle' line: 86";
+
+ RuntimeException locationError = new RuntimeException(locationMsg, rootCause) {
+ @Override
+ public String toString() {
+ return LocationAwareException.class.getName() + ": " + super.toString();
+ }
+ };
+
+ Throwable error = new Throwable(locationError);
+
+ RuntimeException realCause = myErrorHandler.getUserFriendlyError(error, myProjectPath, null);
+ String actualMsg = realCause.getMessage();
+ assertTrue(actualMsg.contains(locationMsg));
+ assertTrue(actualMsg.contains("Cause: " + causeMsg));
+ }
+
+ public void testGetUserFriendlyErrorWithClassNotFoundException() {
+ String causeMsg = "com.mypackage.MyImaginaryClass";
+ ClassNotFoundException rootCause = new ClassNotFoundException(causeMsg);
+ Throwable error = new Throwable(rootCause);
+ RuntimeException realCause = myErrorHandler.getUserFriendlyError(error, myProjectPath, null);
+ assertTrue(realCause.getMessage().contains("Unable to load class 'com.mypackage.MyImaginaryClass'."));
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/project/ProjectResolverFunctionFactoryTest.java b/android/testSrc/com/android/tools/idea/gradle/project/ProjectResolverFunctionFactoryTest.java
deleted file mode 100644
index ec6cbce..0000000
--- a/android/testSrc/com/android/tools/idea/gradle/project/ProjectResolverFunctionFactoryTest.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tools.idea.gradle.project;
-
-import com.android.tools.idea.gradle.project.AndroidGradleProjectResolver.ProjectResolverFunctionFactory;
-import com.intellij.openapi.externalSystem.model.DataNode;
-import com.intellij.openapi.externalSystem.model.project.ProjectData;
-import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId;
-import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType;
-import com.intellij.util.Function;
-import junit.framework.TestCase;
-import org.gradle.tooling.ProjectConnection;
-import org.jetbrains.plugins.gradle.settings.GradleExecutionSettings;
-
-import static com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListenerAdapter.NULL_OBJECT;
-import static org.easymock.classextension.EasyMock.*;
-
-/**
- * Tests for {@link ProjectResolverFunctionFactory}.
- */
-public class ProjectResolverFunctionFactoryTest extends TestCase {
- private ProjectResolver myStrategy;
- private ProjectResolverFunctionFactory myFunctionFactory;
-
- @Override
- protected void setUp() throws Exception {
- super.setUp();
- myStrategy = createMock(ProjectResolver.class);
- myFunctionFactory = new ProjectResolverFunctionFactory(myStrategy);
- }
-
- public void testCreateFunction() {
- ExternalSystemTaskId id = ExternalSystemTaskId.create(ExternalSystemTaskType.RESOLVE_PROJECT, "dummy");
- String projectPath = "~/basic/build.gradle";
- GradleExecutionSettings settings = createMock(GradleExecutionSettings.class);
- ProjectConnection connection = createMock(ProjectConnection.class);
-
- Function<ProjectConnection,DataNode<ProjectData>> function = myFunctionFactory.createFunction(id, projectPath, settings, NULL_OBJECT);
- assertNotNull(function);
-
- DataNode<ProjectData> projectInfo = createMock(DataNode.class);
-
- // Verify that function execution delegates to ProjectResolverDelegates.
- expect(myStrategy.resolveProjectInfo(id, projectPath, settings, connection, NULL_OBJECT)).andReturn(projectInfo);
- replay(myStrategy);
-
- DataNode<ProjectData> resolved = function.fun(connection);
-
- verify(myStrategy);
-
- assertSame(projectInfo, resolved);
- }
-}
diff --git a/android/testSrc/com/android/tools/idea/gradle/project/ProjectResolverTest.java b/android/testSrc/com/android/tools/idea/gradle/project/ProjectResolverTest.java
deleted file mode 100644
index 2929c57..0000000
--- a/android/testSrc/com/android/tools/idea/gradle/project/ProjectResolverTest.java
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tools.idea.gradle.project;
-
-import com.android.tools.idea.gradle.ContentRootSourcePaths;
-import com.android.tools.idea.gradle.TestProjects;
-import com.android.tools.idea.gradle.stubs.android.AndroidProjectStub;
-import com.android.tools.idea.gradle.stubs.gradle.IdeaModuleStub;
-import com.android.tools.idea.gradle.stubs.gradle.IdeaProjectStub;
-import com.google.common.collect.Lists;
-import com.intellij.openapi.externalSystem.model.DataNode;
-import com.intellij.openapi.externalSystem.model.ProjectKeys;
-import com.intellij.openapi.externalSystem.model.project.ContentRootData;
-import com.intellij.openapi.externalSystem.model.project.ExternalSystemSourceType;
-import com.intellij.openapi.externalSystem.model.project.ModuleData;
-import com.intellij.openapi.externalSystem.model.project.ProjectData;
-import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId;
-import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType;
-import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
-import com.intellij.openapi.util.io.FileUtil;
-import junit.framework.TestCase;
-import org.gradle.tooling.ModelBuilder;
-import org.gradle.tooling.ProjectConnection;
-import org.gradle.tooling.model.idea.IdeaProject;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.plugins.gradle.settings.GradleExecutionSettings;
-
-import java.util.Collections;
-import java.util.List;
-
-import static com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListenerAdapter.NULL_OBJECT;
-import static org.easymock.classextension.EasyMock.*;
-
-/**
- * Tests for {@link ProjectResolver}.
- */
-public class ProjectResolverTest extends TestCase {
- private ContentRootSourcePaths myExpectedSourcePaths;
- private IdeaProjectStub myIdeaProject;
- private AndroidProjectStub myAndroidProject;
- private ExternalSystemTaskId myId;
- private ProjectConnection myConnection;
- private GradleExecutionHelperDouble myHelper;
- private GradleExecutionSettings mySettings;
- private ProjectResolver myStrategy;
- private IdeaModuleStub myUtilModule;
-
- @Override
- public void setUp() throws Exception {
- super.setUp();
- myExpectedSourcePaths = new ContentRootSourcePaths();
- myIdeaProject = new IdeaProjectStub("multiProject");
- myAndroidProject = TestProjects.createBasicProject(myIdeaProject.getRootDir());
- myIdeaProject.addModule(myAndroidProject.getName());
- myUtilModule = myIdeaProject.addModule("util");
- myId = ExternalSystemTaskId.create(ExternalSystemTaskType.RESOLVE_PROJECT, "dummy");
- myConnection = createMock(ProjectConnection.class);
- myHelper = GradleExecutionHelperDouble.newMock();
- mySettings = createMock(GradleExecutionSettings.class);
- myStrategy = new ProjectResolver(myHelper);
- }
-
- @Override
- protected void tearDown() throws Exception {
- if (myIdeaProject != null) {
- myIdeaProject.dispose();
- }
- super.tearDown();
- }
-
- @SuppressWarnings("unchecked")
- public void testResolveProjectInfo() {
- // Record mock expectations.
- ModelBuilder<IdeaProject> ideaProjectModelBuilder = createMock(ModelBuilder.class);
- myHelper.getModelBuilder(IdeaProject.class, myId, mySettings, myConnection, NULL_OBJECT, Collections.<String>emptyList());
- expectLastCall().andReturn(ideaProjectModelBuilder);
-
- // Simulate retrieval of the top-level IdeaProject.
- expect(ideaProjectModelBuilder.get()).andReturn(myIdeaProject);
-
- // Simulate retrieval of AndroidProject from IdeaModule 'basic'
- myHelper.setExecutionResult(myAndroidProject);
-
- replay(myConnection, myHelper, ideaProjectModelBuilder);
-
- // Code under test.
- String projectPath = myIdeaProject.getBuildFile().getAbsolutePath();
- DataNode<ProjectData> projectInfo = myStrategy.resolveProjectInfo(myId, projectPath, mySettings, myConnection, NULL_OBJECT);
-
- // Verify mock expectations.
- verify(myConnection, myHelper, ideaProjectModelBuilder);
-
- // Verify project.
- assertNotNull(projectInfo);
- ProjectData projectData = projectInfo.getData();
- assertEquals(myIdeaProject.getName(), projectData.getName());
- assertEquals(FileUtil.toSystemIndependentName(myIdeaProject.getRootDir().getAbsolutePath()),
- projectData.getIdeProjectFileDirectoryPath());
-
- // Verify modules.
- List<DataNode<ModuleData>> modules = Lists.newArrayList(ExternalSystemApiUtil.getChildren(projectInfo, ProjectKeys.MODULE));
- assertEquals("Module count", 2, modules.size());
-
- // Verify 'basic' module.
- DataNode<ModuleData> moduleInfo = modules.get(0);
- ModuleData moduleData = moduleInfo.getData();
- assertEquals(myAndroidProject.getName(), moduleData.getName());
-
- // Verify content root in 'basic' module.
- List<DataNode<ContentRootData>> contentRoots = Lists.newArrayList(ExternalSystemApiUtil.getChildren(moduleInfo, ProjectKeys.CONTENT_ROOT));
- assertEquals(1, contentRoots.size());
-
- String projectRootDirPath = FileUtil.toSystemIndependentName(myAndroidProject.getRootDir().getAbsolutePath());
- ContentRootData contentRootData = contentRoots.get(0).getData();
- assertEquals(projectRootDirPath, contentRootData.getRootPath());
- myExpectedSourcePaths.storeExpectedSourcePaths(myAndroidProject);
- assertCorrectStoredDirPaths(contentRootData, ExternalSystemSourceType.SOURCE);
- assertCorrectStoredDirPaths(contentRootData, ExternalSystemSourceType.TEST);
-
- // Verify 'util' module.
- moduleInfo = modules.get(1);
- moduleData = moduleInfo.getData();
- assertEquals(myUtilModule.getName(), moduleData.getName());
-
- // Verify content root in 'util' module.
- contentRoots = Lists.newArrayList(ExternalSystemApiUtil.getChildren(moduleInfo, ProjectKeys.CONTENT_ROOT));
- assertEquals(1, contentRoots.size());
-
- String moduleRootDirPath = FileUtil.toSystemIndependentName(myUtilModule.getRootDir().getPath());
- contentRootData = contentRoots.get(0).getData();
- assertEquals(moduleRootDirPath, contentRootData.getRootPath());
- }
-
- private void assertCorrectStoredDirPaths(@NotNull ContentRootData contentRootData, @NotNull ExternalSystemSourceType sourceType) {
- myExpectedSourcePaths.assertCorrectStoredDirPaths(contentRootData.getPaths(sourceType), sourceType);
- }
-}
diff --git a/android/testSrc/com/android/tools/idea/gradle/service/AndroidProjectDataServiceTest.java b/android/testSrc/com/android/tools/idea/gradle/service/AndroidProjectDataServiceTest.java
index d4d6b28..4d11fbe 100644
--- a/android/testSrc/com/android/tools/idea/gradle/service/AndroidProjectDataServiceTest.java
+++ b/android/testSrc/com/android/tools/idea/gradle/service/AndroidProjectDataServiceTest.java
@@ -48,7 +48,7 @@
myAndroidProject.addVariant(DEBUG);
myAndroidProject.addBuildType(DEBUG);
String rootDirPath = myAndroidProject.getRootDir().getAbsolutePath();
- myIdeaAndroidProject = new IdeaAndroidProject(myAndroidProject.getName(), rootDirPath, rootDirPath, myAndroidProject, DEBUG);
+ myIdeaAndroidProject = new IdeaAndroidProject(myAndroidProject.getName(), rootDirPath, myAndroidProject, DEBUG);
myCustomizer1 = createMock(ModuleCustomizer.class);
myCustomizer2 = createMock(ModuleCustomizer.class);
service = new AndroidProjectDataService(myCustomizer1, myCustomizer2);
diff --git a/android/testSrc/com/android/tools/idea/gradle/service/notification/CustomNotificationListenerTest.java b/android/testSrc/com/android/tools/idea/gradle/service/notification/CustomNotificationListenerTest.java
new file mode 100644
index 0000000..42dc3b0
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/gradle/service/notification/CustomNotificationListenerTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.service.notification;
+
+import com.android.tools.idea.gradle.service.notification.GradleNotificationExtension.CustomNotificationListener;
+import com.intellij.notification.Notification;
+import junit.framework.TestCase;
+
+import javax.swing.event.HyperlinkEvent;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.classextension.EasyMock.replay;
+import static org.easymock.classextension.EasyMock.verify;
+import static org.easymock.classextension.EasyMock.createMock;
+
+/**
+ * Tests for {@link CustomNotificationListener}.
+ */
+public class CustomNotificationListenerTest extends TestCase {
+ private NotificationHyperlink myHyperlink1;
+ private NotificationHyperlink myHyperlink2;
+ private NotificationHyperlink myHyperlink3;
+ private Notification myNotification;
+ private HyperlinkEvent myHyperlinkEvent;
+ private CustomNotificationListener myListener;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ myHyperlink1 = createMock(NotificationHyperlink.class);
+ myHyperlink2 = createMock(NotificationHyperlink.class);
+ myHyperlink3 = createMock(NotificationHyperlink.class);
+ myNotification = createMock(Notification.class);
+ myHyperlinkEvent = createMock(HyperlinkEvent.class);
+ }
+
+ public void testHyperlinkActivatedWithOneHyperlink() {
+ myListener = new CustomNotificationListener(myHyperlink1);
+
+ // if there is only one hyperlink, just execute it.
+ expect(myHyperlink1.executeIfClicked(myHyperlinkEvent)).andReturn(true);
+ replay(myHyperlink1, myHyperlink2, myHyperlink3);
+
+ myListener.hyperlinkActivated(myNotification, myHyperlinkEvent);
+
+ verify(myHyperlink1, myHyperlink2, myHyperlink3);
+ }
+
+ public void testHyperlinkActivatedWithMoreThanOneHyperlink() {
+ myListener = new CustomNotificationListener(myHyperlink1, myHyperlink2, myHyperlink3);
+
+ // should not try to execute myHyperlink3, because execution of myHyperlink2 was successful.
+ expect(myHyperlink1.executeIfClicked(myHyperlinkEvent)).andReturn(false);
+ expect(myHyperlink2.executeIfClicked(myHyperlinkEvent)).andReturn(true);
+ replay(myHyperlink1, myHyperlink2, myHyperlink3);
+
+ myListener.hyperlinkActivated(myNotification, myHyperlinkEvent);
+
+ verify(myHyperlink1, myHyperlink2, myHyperlink3);
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/service/notification/GradleNotificationExtensionTest.java b/android/testSrc/com/android/tools/idea/gradle/service/notification/GradleNotificationExtensionTest.java
new file mode 100644
index 0000000..f275478
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/gradle/service/notification/GradleNotificationExtensionTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.service.notification;
+
+import com.android.tools.idea.gradle.service.notification.GradleNotificationExtension.CustomNotificationListener;
+import com.intellij.openapi.externalSystem.service.notification.ExternalSystemNotificationExtension.CustomizationResult;
+import com.intellij.openapi.project.Project;
+import junit.framework.TestCase;
+import org.jetbrains.annotations.NotNull;
+
+import static org.easymock.EasyMock.*;
+
+/**
+ * Tests for {@link GradleNotificationExtension}.
+ */
+public class GradleNotificationExtensionTest extends TestCase {
+ private Project myProject;
+ private NotificationHyperlink myHyperlink1;
+ private NotificationHyperlink myHyperlink2;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ myProject = createMock(Project.class);
+ myHyperlink1 = new TestingHyperlink("1", "Hyperlink 1");
+ myHyperlink2 = new TestingHyperlink("2", "Hyperlink 2");
+ }
+
+ public void testCreateNotification() {
+ String projectName = "project1";
+ String errorMsg = "Hello";
+
+ expect(myProject.getName()).andReturn(projectName);
+ replay(myProject);
+
+ CustomizationResult notification = GradleNotificationExtension.createNotification(myProject, errorMsg, myHyperlink1, myHyperlink2);
+
+ verify(myProject);
+
+ String title = notification.getTitle();
+ assertNotNull(title);
+ assertTrue(title.contains("'" + projectName + "'"));
+ assertTrue(title.contains(errorMsg));
+
+ assertEquals("<a href=\"1\">Hyperlink 1</a> <a href=\"2\">Hyperlink 2</a>", notification.getMessage());
+
+ CustomNotificationListener notificationListener = (CustomNotificationListener)notification.getListener();
+ assertNotNull(notificationListener);
+ NotificationHyperlink[] hyperlinks = notificationListener.getHyperlinks();
+ assertEquals(2, hyperlinks.length);
+ assertSame(myHyperlink1, hyperlinks[0]);
+ assertSame(myHyperlink2, hyperlinks[1]);
+ }
+
+ private static class TestingHyperlink extends NotificationHyperlink {
+ TestingHyperlink(@NotNull String url, @NotNull String text) {
+ super(url, text);
+ }
+
+ @Override
+ void execute() {
+ }
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/service/notification/NotificationHyperlinkTest.java b/android/testSrc/com/android/tools/idea/gradle/service/notification/NotificationHyperlinkTest.java
new file mode 100644
index 0000000..2a9876d
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/gradle/service/notification/NotificationHyperlinkTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.service.notification;
+
+import junit.framework.TestCase;
+
+import javax.swing.event.HyperlinkEvent;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.classextension.EasyMock.*;
+
+/**
+ * Tests for {@link NotificationHyperlink}.
+ */
+public class NotificationHyperlinkTest extends TestCase {
+ private boolean myExecuted;
+ private NotificationHyperlink myHyperlink;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ myHyperlink = new NotificationHyperlink("openFile", "Open File") {
+ @Override
+ void execute() {
+ myExecuted = true;
+ }
+ };
+ }
+
+ public void testExecuteIfClickedWhenDescriptionMatchesUrl() {
+ HyperlinkEvent event = createMock(HyperlinkEvent.class);
+ expect(event.getDescription()).andReturn("openFile");
+ replay(event);
+
+ assertTrue(myHyperlink.executeIfClicked(event));
+
+ verify(event);
+
+ assertTrue(myExecuted);
+ }
+
+ public void testExecuteIfClickedWhenDescriptionDoesNotMatchUrl() {
+ HyperlinkEvent event = createMock(HyperlinkEvent.class);
+ expect(event.getDescription()).andReturn("browse");
+ replay(event);
+
+ assertFalse(myHyperlink.executeIfClicked(event));
+
+ verify(event);
+
+ assertFalse(myExecuted);
+ }
+
+ public void testToString() {
+ assertEquals("<a href=\"openFile\">Open File</a>", myHyperlink.toString());
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/stubs/android/AndroidLibraryStub.java b/android/testSrc/com/android/tools/idea/gradle/stubs/android/AndroidLibraryStub.java
index 9a9aa40..0d88b20 100644
--- a/android/testSrc/com/android/tools/idea/gradle/stubs/android/AndroidLibraryStub.java
+++ b/android/testSrc/com/android/tools/idea/gradle/stubs/android/AndroidLibraryStub.java
@@ -15,36 +15,55 @@
*/
package com.android.tools.idea.gradle.stubs.android;
-import com.android.annotations.NonNull;
+import com.android.SdkConstants;
import com.android.builder.model.AndroidLibrary;
import com.google.common.collect.Lists;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.List;
public class AndroidLibraryStub implements AndroidLibrary {
@NotNull private final File myJarFile;
+ @NotNull private final String myProject;
@NotNull private final List<File> myLocalJars = Lists.newArrayList();
public AndroidLibraryStub(@NotNull File jarFile) {
- myJarFile = jarFile;
+ this(jarFile, null);
}
- @NonNull
+ public AndroidLibraryStub(@NotNull File jarFile, @Nullable String project) {
+ myJarFile = jarFile;
+ myProject = project;
+ }
+
+ @Nullable
@Override
- public File getFolder() {
+ public String getProject() {
+ return myProject;
+ }
+
+ @Override
+ @NotNull
+ public File getBundle() {
throw new UnsupportedOperationException();
}
- @NonNull
@Override
+ @NotNull
+ public File getFolder() {
+ return myJarFile.getParentFile();
+ }
+
+ @Override
+ @NotNull
public List<? extends AndroidLibrary> getLibraryDependencies() {
throw new UnsupportedOperationException();
}
- @NonNull
@Override
+ @NotNull
public File getJarFile() {
return myJarFile;
}
@@ -53,50 +72,50 @@
myLocalJars.add(localJar);
}
- @NonNull
@Override
+ @NotNull
public List<File> getLocalJars() {
return myLocalJars;
}
- @NonNull
@Override
+ @NotNull
public File getResFolder() {
- throw new UnsupportedOperationException();
+ return new File(getFolder(), SdkConstants.FD_RES);
}
- @NonNull
@Override
+ @NotNull
public File getAssetsFolder() {
throw new UnsupportedOperationException();
}
- @NonNull
@Override
+ @NotNull
public File getJniFolder() {
throw new UnsupportedOperationException();
}
- @NonNull
@Override
+ @NotNull
public File getAidlFolder() {
throw new UnsupportedOperationException();
}
- @NonNull
@Override
+ @NotNull
public File getRenderscriptFolder() {
throw new UnsupportedOperationException();
}
- @NonNull
@Override
+ @NotNull
public File getProguardRules() {
throw new UnsupportedOperationException();
}
- @NonNull
@Override
+ @NotNull
public File getLintJar() {
throw new UnsupportedOperationException();
}
diff --git a/android/testSrc/com/android/tools/idea/gradle/stubs/android/AndroidProjectStub.java b/android/testSrc/com/android/tools/idea/gradle/stubs/android/AndroidProjectStub.java
index 8eada35..38b8f93 100644
--- a/android/testSrc/com/android/tools/idea/gradle/stubs/android/AndroidProjectStub.java
+++ b/android/testSrc/com/android/tools/idea/gradle/stubs/android/AndroidProjectStub.java
@@ -15,10 +15,8 @@
*/
package com.android.tools.idea.gradle.stubs.android;
-import com.android.build.gradle.model.AndroidProject;
-import com.android.build.gradle.model.BuildTypeContainer;
-import com.android.build.gradle.model.ProductFlavorContainer;
-import com.android.build.gradle.model.Variant;
+import com.android.SdkConstants;
+import com.android.builder.model.*;
import com.android.tools.idea.gradle.stubs.FileStructure;
import com.google.common.collect.Maps;
import org.jetbrains.annotations.NotNull;
@@ -53,17 +51,17 @@
this.myName = name;
myFileStructure = fileStructure;
myDefaultConfig = new ProductFlavorContainerStub("main", myFileStructure);
- myBuildFile = myFileStructure.createProjectFile("build.gradle");
+ myBuildFile = myFileStructure.createProjectFile(SdkConstants.FN_BUILD_GRADLE);
}
- @NotNull
@Override
+ @NotNull
public String getModelVersion() {
- return "0.4-SNAPSHOT";
+ return "0.5.0-SNAPSHOT";
}
- @NotNull
@Override
+ @NotNull
public String getName() {
return myName;
}
@@ -77,8 +75,8 @@
return myLibrary;
}
- @NotNull
@Override
+ @NotNull
public ProductFlavorContainerStub getDefaultConfig() {
return myDefaultConfig;
}
@@ -89,8 +87,8 @@
return buildType;
}
- @NotNull
@Override
+ @NotNull
public Map<String, BuildTypeContainer> getBuildTypes() {
return myBuildTypes;
}
@@ -102,8 +100,8 @@
return flavor;
}
- @NotNull
@Override
+ @NotNull
public Map<String, ProductFlavorContainer> getProductFlavors() {
return myProductFlavors;
}
@@ -114,7 +112,7 @@
return addVariant(variantName, variantName);
}
- @NotNull
+ @NotNull
public VariantStub addVariant(@NotNull String variantName, @NotNull String buildTypeName) {
VariantStub variant = new VariantStub(variantName, buildTypeName, myFileStructure);
addVariant(variant);
@@ -128,8 +126,8 @@
myVariants.put(variant.getName(), variant);
}
- @NotNull
@Override
+ @NotNull
public Map<String, Variant> getVariants() {
return myVariants;
}
@@ -139,18 +137,30 @@
return myFirstVariant;
}
- @NotNull
@Override
+ @NotNull
public String getCompileTarget() {
throw new UnsupportedOperationException();
}
- @NotNull
@Override
+ @NotNull
public List<String> getBootClasspath() {
throw new UnsupportedOperationException();
}
+ @Override
+ @NotNull
+ public Map<String, SigningConfig> getSigningConfigs() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ @NotNull
+ public AaptOptions getAaptOptions() {
+ throw new UnsupportedOperationException();
+ }
+
/**
* Deletes this project's directory structure.
*/
diff --git a/android/testSrc/com/android/tools/idea/gradle/stubs/android/ArtifactInfoStub.java b/android/testSrc/com/android/tools/idea/gradle/stubs/android/ArtifactInfoStub.java
new file mode 100644
index 0000000..90bf3f2
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/gradle/stubs/android/ArtifactInfoStub.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.stubs.android;
+
+import com.android.builder.model.ArtifactInfo;
+import com.android.tools.idea.gradle.stubs.FileStructure;
+import com.google.common.collect.Lists;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.List;
+
+public class ArtifactInfoStub implements ArtifactInfo {
+ @NotNull private final List<File> myGeneratedResourceFolders = Lists.newArrayList();
+ @NotNull private final List<File> myGeneratedSourceFolders = Lists.newArrayList();
+
+ @NotNull private final DependenciesStub myDependencies;
+ @NotNull private final String myAssembleTaskName;
+ @NotNull private final String myBuildType;
+ @NotNull private final FileStructure myFileStructure;
+
+ ArtifactInfoStub(@NotNull String assembleTaskName, @NotNull String buildType, @NotNull FileStructure fileStructure) {
+ myDependencies = new DependenciesStub();
+ myAssembleTaskName = assembleTaskName;
+ myBuildType = buildType;
+ myFileStructure = fileStructure;
+ }
+
+ @Override
+ @NotNull
+ public File getOutputFile() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isSigned() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ @Nullable
+ public String getSigningConfigName() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ @NotNull
+ public String getPackageName() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ @NotNull
+ public String getSourceGenTaskName() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ @NotNull
+ public String getAssembleTaskName() {
+ return myAssembleTaskName;
+ }
+
+ @Override
+ @NotNull
+ public List<File> getGeneratedSourceFolders() {
+ return myGeneratedSourceFolders;
+ }
+
+ @Override
+ @NotNull
+ public List<File> getGeneratedResourceFolders() {
+ return myGeneratedResourceFolders;
+ }
+
+ @Override
+ @NotNull
+ public File getClassesFolder() {
+ String path = "build/classes/" + myBuildType;
+ return new File(myFileStructure.getRootDir(), path);
+ }
+
+ @Override
+ @NotNull
+ public DependenciesStub getDependencies() {
+ return myDependencies;
+ }
+
+ /**
+ * Adds the given path to the list of generated source directories. It also creates the directory in the file system.
+ *
+ * @param path path of the generated source directory to add, relative to the root directory of the Android project.
+ */
+ public void addGeneratedSourceFolder(@NotNull String path) {
+ File directory = myFileStructure.createProjectDir(path);
+ myGeneratedSourceFolders.add(directory);
+ }
+
+ /**
+ * Adds the given path to the list of generated resource directories. It also creates the directory in the file system.
+ *
+ * @param path path of the generated resource directory to add, relative to the root directory of the Android project.
+ */
+ public void addGeneratedResourceFolder(@NotNull String path) {
+ File directory = myFileStructure.createProjectDir(path);
+ myGeneratedResourceFolders.add(directory);
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/stubs/android/BuildTypeContainerStub.java b/android/testSrc/com/android/tools/idea/gradle/stubs/android/BuildTypeContainerStub.java
index ef29468..775a35c 100644
--- a/android/testSrc/com/android/tools/idea/gradle/stubs/android/BuildTypeContainerStub.java
+++ b/android/testSrc/com/android/tools/idea/gradle/stubs/android/BuildTypeContainerStub.java
@@ -15,20 +15,18 @@
*/
package com.android.tools.idea.gradle.stubs.android;
-import com.android.build.gradle.model.BuildTypeContainer;
import com.android.builder.model.BuildType;
+import com.android.builder.model.BuildTypeContainer;
import com.android.tools.idea.gradle.stubs.FileStructure;
import org.jetbrains.annotations.NotNull;
public class BuildTypeContainerStub implements BuildTypeContainer {
@NotNull private final String myName;
@NotNull private final SourceProviderStub mySourceProvider;
- @NotNull private final DependenciesStub myDependencies;
BuildTypeContainerStub(@NotNull String name, @NotNull FileStructure fileStructure) {
myName = name;
mySourceProvider = new SourceProviderStub(fileStructure);
- myDependencies = new DependenciesStub();
setUpPaths();
}
@@ -59,10 +57,4 @@
public SourceProviderStub getSourceProvider() {
return mySourceProvider;
}
-
- @Override
- @NotNull
- public DependenciesStub getDependency() {
- return myDependencies;
- }
}
diff --git a/android/testSrc/com/android/tools/idea/gradle/stubs/android/DependenciesStub.java b/android/testSrc/com/android/tools/idea/gradle/stubs/android/DependenciesStub.java
index ad36254..7d708c7 100644
--- a/android/testSrc/com/android/tools/idea/gradle/stubs/android/DependenciesStub.java
+++ b/android/testSrc/com/android/tools/idea/gradle/stubs/android/DependenciesStub.java
@@ -15,8 +15,9 @@
*/
package com.android.tools.idea.gradle.stubs.android;
-import com.android.build.gradle.model.Dependencies;
+import com.android.annotations.NonNull;
import com.android.builder.model.AndroidLibrary;
+import com.android.builder.model.Dependencies;
import com.google.common.collect.Lists;
import org.jetbrains.annotations.NotNull;
@@ -26,13 +27,14 @@
public class DependenciesStub implements Dependencies {
@NotNull private final List<AndroidLibrary> myLibraries = Lists.newArrayList();
@NotNull private final List<File> myJars = Lists.newArrayList();
+ @NotNull private final List<String> myProjects = Lists.newArrayList();
public void addLibrary(@NotNull AndroidLibraryStub library) {
myLibraries.add(library);
}
- @NotNull
@Override
+ @NotNull
public List<AndroidLibrary> getLibraries() {
return myLibraries;
}
@@ -41,15 +43,19 @@
myJars.add(jar);
}
- @NotNull
@Override
+ @NotNull
public List<File> getJars() {
return myJars;
}
- @NotNull
+ public void addProject(@NotNull String project) {
+ myProjects.add(project);
+ }
+
+ @NonNull
@Override
- public List<String> getProjectDependenciesPath() {
- throw new UnsupportedOperationException();
+ public List<String> getProjects() {
+ return myProjects;
}
}
diff --git a/android/testSrc/com/android/tools/idea/gradle/stubs/android/ProductFlavorContainerStub.java b/android/testSrc/com/android/tools/idea/gradle/stubs/android/ProductFlavorContainerStub.java
index c70fefb..5222a00 100644
--- a/android/testSrc/com/android/tools/idea/gradle/stubs/android/ProductFlavorContainerStub.java
+++ b/android/testSrc/com/android/tools/idea/gradle/stubs/android/ProductFlavorContainerStub.java
@@ -15,7 +15,7 @@
*/
package com.android.tools.idea.gradle.stubs.android;
-import com.android.build.gradle.model.ProductFlavorContainer;
+import com.android.builder.model.ProductFlavorContainer;
import com.android.tools.idea.gradle.stubs.FileStructure;
import com.intellij.openapi.util.text.StringUtil;
import org.jetbrains.annotations.NotNull;
@@ -24,8 +24,6 @@
@NotNull private final ProductFlavorStub myFlavor;
@NotNull private final SourceProviderStub mySourceProvider;
@NotNull private final SourceProviderStub myTestSourceProvider;
- @NotNull private final DependenciesStub myDependencies;
- @NotNull private final DependenciesStub myTestDependencies;
/**
* Creates a new {@clink ProductFlavorContainerStub}.
@@ -38,8 +36,6 @@
myFlavor = new ProductFlavorStub(flavorName);
mySourceProvider = new SourceProviderStub(fileStructure);
myTestSourceProvider = new SourceProviderStub(fileStructure);
- myDependencies = new DependenciesStub();
- myTestDependencies = new DependenciesStub();
setUpPaths(flavorName);
}
@@ -78,19 +74,7 @@
@NotNull
@Override
- public DependenciesStub getDependencies() {
- return myDependencies;
- }
-
- @NotNull
- @Override
public SourceProviderStub getTestSourceProvider() {
return myTestSourceProvider;
}
-
- @NotNull
- @Override
- public DependenciesStub getTestDependencies() {
- return myTestDependencies;
- }
}
diff --git a/android/testSrc/com/android/tools/idea/gradle/stubs/android/ProductFlavorStub.java b/android/testSrc/com/android/tools/idea/gradle/stubs/android/ProductFlavorStub.java
index 70378f0..e17de6b 100644
--- a/android/testSrc/com/android/tools/idea/gradle/stubs/android/ProductFlavorStub.java
+++ b/android/testSrc/com/android/tools/idea/gradle/stubs/android/ProductFlavorStub.java
@@ -19,6 +19,7 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.io.File;
import java.util.List;
public class ProductFlavorStub implements ProductFlavor {
@@ -91,7 +92,7 @@
@NotNull
@Override
- public List<Object> getProguardFiles() {
+ public List<File> getProguardFiles() {
throw new UnsupportedOperationException();
}
}
diff --git a/android/testSrc/com/android/tools/idea/gradle/stubs/android/VariantStub.java b/android/testSrc/com/android/tools/idea/gradle/stubs/android/VariantStub.java
index 9fb286c..d8d5eb7 100644
--- a/android/testSrc/com/android/tools/idea/gradle/stubs/android/VariantStub.java
+++ b/android/testSrc/com/android/tools/idea/gradle/stubs/android/VariantStub.java
@@ -15,29 +15,23 @@
*/
package com.android.tools.idea.gradle.stubs.android;
-import com.android.build.gradle.model.Variant;
import com.android.builder.model.ProductFlavor;
+import com.android.builder.model.Variant;
import com.android.tools.idea.gradle.stubs.FileStructure;
import com.google.common.collect.Lists;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
-import java.io.File;
import java.util.Arrays;
import java.util.List;
public class VariantStub implements Variant {
- private static final String DEFAULT_ASSEMBLE_TASK_NAME = "assemble";
-
- @NotNull private final List<File> myGeneratedResourceFolders = Lists.newArrayList();
- @NotNull private final List<File> myGeneratedSourceFolders = Lists.newArrayList();
- @NotNull private final List<File> myGeneratedTestResourceFolders = Lists.newArrayList();
- @NotNull private final List<File> myGeneratedTestSourceFolders = Lists.newArrayList();
-
@NotNull private final List<String> myProductFlavors = Lists.newArrayList();
@NotNull private final String myName;
@NotNull private final String myBuildType;
- @NotNull private final FileStructure myFileStructure;
+ @NotNull private final ArtifactInfoStub myMainArtifactInfo;
+ @NotNull private final ArtifactInfoStub myTestArtifactInfo;
/**
* Creates a new {@link VariantStub}.
@@ -49,131 +43,56 @@
VariantStub(@NotNull String name, @NotNull String buildType, @NotNull FileStructure fileStructure) {
myName = name;
myBuildType = buildType;
- myFileStructure = fileStructure;
+ myMainArtifactInfo = new ArtifactInfoStub("assemble", buildType, fileStructure);
+ myTestArtifactInfo = new ArtifactInfoStub("assembleTest", buildType, fileStructure);
}
- @NotNull
@Override
+ @NotNull
public String getName() {
return myName;
}
- @NotNull
@Override
- public File getOutputFile() {
- throw new UnsupportedOperationException();
+ @NotNull
+ public String getDisplayName() {
+ return myName;
}
@Override
- public boolean isSigned() {
- throw new UnsupportedOperationException();
+ @NotNull
+ public ArtifactInfoStub getMainArtifactInfo() {
+ return myMainArtifactInfo;
}
- @NotNull
@Override
- public File getOutputTestFile() {
- throw new UnsupportedOperationException();
+ @Nullable
+ public ArtifactInfoStub getTestArtifactInfo() {
+ return myTestArtifactInfo;
}
- @NotNull
@Override
- public String getAssembleTaskName() {
- return DEFAULT_ASSEMBLE_TASK_NAME;
- }
-
@NotNull
- @Override
- public String getAssembleTestTaskName() {
- throw new UnsupportedOperationException();
- }
-
- @NotNull
- @Override
public String getBuildType() {
return myBuildType;
}
- @NotNull
@Override
+ @NotNull
public List<String> getProductFlavors() {
return myProductFlavors;
}
- @NotNull
@Override
+ @NotNull
public ProductFlavor getMergedFlavor() {
throw new UnsupportedOperationException();
}
- /**
- * Adds the given path to the list of generated source directories. It also creates the directory in the file system.
- *
- * @param path path of the generated source directory to add, relative to the root directory of the Android project.
- */
- public void addGeneratedSourceFolder(@NotNull String path) {
- File directory = myFileStructure.createProjectDir(path);
- myGeneratedSourceFolders.add(directory);
- }
-
- @NotNull
@Override
- public List<File> getGeneratedSourceFolders() {
- return myGeneratedSourceFolders;
- }
-
- /**
- * Adds the given path to the list of generated resource directories. It also creates the directory in the file system.
- *
- * @param path path of the generated resource directory to add, relative to the root directory of the Android project.
- */
- public void addGeneratedResourceFolder(@NotNull String path) {
- File directory = myFileStructure.createProjectDir(path);
- myGeneratedResourceFolders.add(directory);
- }
-
@NotNull
- @Override
- public List<File> getGeneratedResourceFolders() {
- return myGeneratedResourceFolders;
- }
-
- /**
- * Adds the given path to the list of generated test source directories. It also creates the directory in the file system.
- *
- * @param path path of the generated test source directory to add, relative to the root directory of the Android project.
- */
- public void addGeneratedTestSourceFolder(@NotNull String path) {
- File directory = myFileStructure.createProjectDir(path);
- myGeneratedTestSourceFolders.add(directory);
- }
-
- @NotNull
- @Override
- public List<File> getGeneratedTestSourceFolders() {
- return myGeneratedTestSourceFolders;
- }
-
- /**
- * Adds the given path to the list of generated test resource directories. It also creates the directory in the file system.
- *
- * @param path path of the generated test resource directory to add, relative to the root directory of the Android project.
- */
- public void addGeneratedTestResourceFolder(@NotNull String path) {
- File directory = myFileStructure.createProjectDir(path);
- myGeneratedTestResourceFolders.add(directory);
- }
-
- @NotNull
- @Override
- public List<File> getGeneratedTestResourceFolders() {
- return myGeneratedTestResourceFolders;
- }
-
- @NotNull
- @Override
- public File getClassesFolder() {
- String path = "build/classes/" + getBuildType();
- return new File(myFileStructure.getRootDir(), path);
+ public List<String> getResourceConfigurations() {
+ throw new UnsupportedOperationException();
}
public void addProductFlavors(@NotNull String... flavorNames) {
diff --git a/android/testSrc/com/android/tools/idea/gradle/stubs/gradle/GradleProjectStub.java b/android/testSrc/com/android/tools/idea/gradle/stubs/gradle/GradleProjectStub.java
index 7e22657..fcae8de 100644
--- a/android/testSrc/com/android/tools/idea/gradle/stubs/gradle/GradleProjectStub.java
+++ b/android/testSrc/com/android/tools/idea/gradle/stubs/gradle/GradleProjectStub.java
@@ -15,25 +15,36 @@
*/
package com.android.tools.idea.gradle.stubs.gradle;
+import com.google.common.collect.Lists;
import org.gradle.tooling.model.DomainObjectSet;
import org.gradle.tooling.model.GradleProject;
import org.gradle.tooling.model.GradleScript;
import org.gradle.tooling.model.GradleTask;
+import org.gradle.tooling.model.internal.ImmutableDomainObjectSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.util.Collections;
+import java.util.List;
+
public class GradleProjectStub implements GradleProject {
@NotNull private final String myName;
@NotNull private final String myPath;
+ @NotNull private final List<GradleTaskStub> myTasks;
- public GradleProjectStub(@NotNull String name, @NotNull String path) {
+ public GradleProjectStub(@NotNull String name, @NotNull String path, @NotNull String...tasks) {
myName = name;
myPath = path;
+ myTasks = Lists.newArrayList();
+ for (String taskName : tasks) {
+ GradleTaskStub task = new GradleTaskStub(taskName, this);
+ myTasks.add(task);
+ }
}
@Override
public DomainObjectSet<? extends GradleTask> getTasks() {
- throw new UnsupportedOperationException();
+ return ImmutableDomainObjectSet.of(myTasks);
}
@Override
diff --git a/android/testSrc/com/android/tools/idea/gradle/stubs/gradle/GradleTaskStub.java b/android/testSrc/com/android/tools/idea/gradle/stubs/gradle/GradleTaskStub.java
new file mode 100644
index 0000000..c81ea76
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/gradle/stubs/gradle/GradleTaskStub.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.stubs.gradle;
+
+import org.gradle.tooling.model.GradleTask;
+import org.jetbrains.annotations.NotNull;
+
+public class GradleTaskStub implements GradleTask {
+ @NotNull private final String myName;
+ @NotNull private final GradleProjectStub myProject;
+
+ GradleTaskStub(@NotNull String name, @NotNull GradleProjectStub project) {
+ myName = name;
+ myProject = project;
+ }
+
+ @Override
+ public String getPath() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getName() {
+ return myName;
+ }
+
+ @Override
+ public String getDescription() {
+ return null;
+ }
+
+ @Override
+ public GradleProjectStub getProject() {
+ return myProject;
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/stubs/gradle/IdeaModuleStub.java b/android/testSrc/com/android/tools/idea/gradle/stubs/gradle/IdeaModuleStub.java
index e09bf36..6578adb 100644
--- a/android/testSrc/com/android/tools/idea/gradle/stubs/gradle/IdeaModuleStub.java
+++ b/android/testSrc/com/android/tools/idea/gradle/stubs/gradle/IdeaModuleStub.java
@@ -39,13 +39,13 @@
@NotNull private final FileStructure myFileStructure;
@NotNull private final GradleProjectStub myGradleProject;
- IdeaModuleStub(@NotNull String name, @NotNull IdeaProjectStub parent) {
+ IdeaModuleStub(@NotNull String name, @NotNull IdeaProjectStub parent, @NotNull String...tasks) {
myName = name;
myParent = parent;
myFileStructure = new FileStructure(parent.getRootDir(), name);
myFileStructure.createProjectFile(GradleConstants.DEFAULT_SCRIPT_NAME);
myContentRoots.add(new IdeaContentRootStub(getRootDir()));
- myGradleProject = new GradleProjectStub(name, ":" + name);
+ myGradleProject = new GradleProjectStub(name, ":" + name, tasks);
}
/**
@@ -62,6 +62,7 @@
return ImmutableDomainObjectSet.of(myContentRoots);
}
+ @NotNull
@Override
public GradleProjectStub getGradleProject() {
return myGradleProject;
diff --git a/android/testSrc/com/android/tools/idea/gradle/stubs/gradle/IdeaProjectStub.java b/android/testSrc/com/android/tools/idea/gradle/stubs/gradle/IdeaProjectStub.java
index cfe249d..c0e6fc9 100644
--- a/android/testSrc/com/android/tools/idea/gradle/stubs/gradle/IdeaProjectStub.java
+++ b/android/testSrc/com/android/tools/idea/gradle/stubs/gradle/IdeaProjectStub.java
@@ -15,6 +15,7 @@
*/
package com.android.tools.idea.gradle.stubs.gradle;
+import com.android.SdkConstants;
import com.android.tools.idea.gradle.stubs.FileStructure;
import com.google.common.collect.Lists;
import org.gradle.tooling.model.DomainObjectSet;
@@ -38,7 +39,7 @@
public IdeaProjectStub(@NotNull String name) {
myName = name;
myFileStructure = new FileStructure(name);
- myBuildFile = myFileStructure.createProjectFile("build.gradle");
+ myBuildFile = myFileStructure.createProjectFile(SdkConstants.FN_BUILD_GRADLE);
}
@Override
@@ -78,8 +79,8 @@
}
@NotNull
- public IdeaModuleStub addModule(@NotNull String name) {
- IdeaModuleStub module = new IdeaModuleStub(name, this);
+ public IdeaModuleStub addModule(@NotNull String name, @NotNull String...tasks) {
+ IdeaModuleStub module = new IdeaModuleStub(name, this, tasks);
modules.add(module);
return module;
}
diff --git a/android/testSrc/com/android/tools/idea/gradle/util/FacetsTest.java b/android/testSrc/com/android/tools/idea/gradle/util/FacetsTest.java
new file mode 100644
index 0000000..381118c
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/gradle/util/FacetsTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.gradle.util;
+
+import com.android.tools.idea.gradle.facet.AndroidGradleFacet;
+import com.intellij.facet.FacetManager;
+import com.intellij.facet.ModifiableFacetModel;
+import com.intellij.testFramework.IdeaTestCase;
+import org.jetbrains.android.facet.AndroidFacet;
+
+/**
+ * Tests for {@link Facets}.
+ */
+public class FacetsTest extends IdeaTestCase {
+ public void testRemoveAllFacetsWithAndroidFacets() throws Exception {
+ FacetManager facetManager = FacetManager.getInstance(myModule);
+ ModifiableFacetModel model = facetManager.createModifiableModel();
+ try {
+ AndroidFacet facet = facetManager.createFacet(AndroidFacet.getFacetType(), AndroidFacet.NAME, null);
+ model.addFacet(facet);
+ }
+ finally {
+ model.commit();
+ }
+
+ assertEquals(1, facetManager.getFacetsByType(AndroidFacet.ID).size());
+
+ Facets.removeAllFacetsOfType(myModule, AndroidFacet.ID);
+
+ assertEquals(0, facetManager.getFacetsByType(AndroidFacet.ID).size());
+ }
+
+ public void testRemoveAllFacetsWithAndroidGradleFacets() throws Exception {
+ FacetManager facetManager = FacetManager.getInstance(myModule);
+ ModifiableFacetModel model = facetManager.createModifiableModel();
+ try {
+ AndroidGradleFacet facet = facetManager.createFacet(AndroidGradleFacet.getFacetType(), AndroidGradleFacet.NAME, null);
+ model.addFacet(facet);
+ }
+ finally {
+ model.commit();
+ }
+
+ assertEquals(1, facetManager.getFacetsByType(AndroidGradleFacet.TYPE_ID).size());
+
+ Facets.removeAllFacetsOfType(myModule, AndroidGradleFacet.TYPE_ID);
+
+ assertEquals(0, facetManager.getFacetsByType(AndroidGradleFacet.TYPE_ID).size());
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/util/LocalPropertiesTest.java b/android/testSrc/com/android/tools/idea/gradle/util/LocalPropertiesTest.java
index 5aa63e7..221c46f 100644
--- a/android/testSrc/com/android/tools/idea/gradle/util/LocalPropertiesTest.java
+++ b/android/testSrc/com/android/tools/idea/gradle/util/LocalPropertiesTest.java
@@ -15,11 +15,11 @@
*/
package com.android.tools.idea.gradle.util;
+import com.android.SdkConstants;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.testFramework.IdeaTestCase;
import java.io.File;
-import java.util.Properties;
import static org.easymock.EasyMock.*;
@@ -27,31 +27,42 @@
* Tests for {@link LocalProperties}.
*/
public class LocalPropertiesTest extends IdeaTestCase {
- private Sdk androidSdk;
+ private LocalProperties myLocalProperties;
@Override
protected void setUp() throws Exception {
super.setUp();
- androidSdk = createMock(Sdk.class);
+ myLocalProperties = new LocalProperties(myProject);
}
- public void testCreateAndReadFile() throws Exception {
- String androidSdkPath = "/home/sdk";
-
- expect(androidSdk.getHomePath()).andReturn(androidSdkPath);
- replay(androidSdk);
-
- LocalProperties.createFile(getProject(), androidSdk);
-
- verify(androidSdk);
-
- File localPropertiesFile = new File(myProject.getBasePath(), "local.properties");
+ public void testCreateFileOnSave() throws Exception {
+ myLocalProperties.save();
+ File localPropertiesFile = new File(myProject.getBasePath(), SdkConstants.FN_LOCAL_PROPERTIES);
assertTrue(localPropertiesFile.isFile());
+ }
- Properties properties = LocalProperties.readFile(myProject);
- assertNotNull(properties);
+ public void testSetAndroidSdkPathWithString() throws Exception {
+ String androidSdkPath = "/home/sdk2";
+ myLocalProperties.setAndroidSdkPath(androidSdkPath);
+ myLocalProperties.save();
- assertEquals(androidSdkPath, properties.getProperty("sdk.dir"));
- assertEquals(androidSdkPath, LocalProperties.getAndroidSdkPath(myProject));
+ assertEquals(androidSdkPath, myLocalProperties.getAndroidSdkPath());
+ }
+
+ public void testSetAndroidSdkPathWithSdk() throws Exception {
+ String androidSdkPath = "/home/sdk2";
+
+ Sdk sdk = createMock(Sdk.class);
+ expect(sdk.getHomePath()).andReturn(androidSdkPath);
+
+ replay(sdk);
+
+ myLocalProperties.setAndroidSdkPath(sdk);
+
+ verify(sdk);
+
+ myLocalProperties.save();
+
+ assertEquals(androidSdkPath, myLocalProperties.getAndroidSdkPath());
}
}
diff --git a/android/testSrc/com/android/tools/idea/rendering/FileProjectResourceRepositoryTest.java b/android/testSrc/com/android/tools/idea/rendering/FileProjectResourceRepositoryTest.java
new file mode 100644
index 0000000..38bdde3
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/rendering/FileProjectResourceRepositoryTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.rendering;
+
+import com.google.common.collect.Lists;
+import com.google.common.io.Files;
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+public class FileProjectResourceRepositoryTest extends TestCase {
+ public void test() throws IOException {
+
+ File dir = Files.createTempDir();
+ assertNotNull(FileProjectResourceRepository.get(dir));
+ // We shouldn't clear it out immediately on GC *eligibility*:
+ System.gc();
+ assertNotNull(FileProjectResourceRepository.getCached(dir));
+ // However, in low memory conditions we should:
+ runOutOfMemory();
+ System.gc();
+ assertNull(FileProjectResourceRepository.getCached(dir));
+ }
+
+ public static void runOutOfMemory() {
+ List<Object> objects = Lists.newArrayList();
+ while (true) {
+ try {
+ objects.add(new String[1024*1024]);
+ } catch (OutOfMemoryError error) {
+ return;
+ }
+ }
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/rendering/HtmlBuilderTest.java b/android/testSrc/com/android/tools/idea/rendering/HtmlBuilderTest.java
deleted file mode 100644
index 2a0c331..0000000
--- a/android/testSrc/com/android/tools/idea/rendering/HtmlBuilderTest.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tools.idea.rendering;
-
-import com.intellij.openapi.util.io.FileUtil;
-import junit.framework.TestCase;
-
-import java.io.File;
-import java.io.IOException;
-
-public class HtmlBuilderTest extends TestCase {
- public void test1() {
- HtmlBuilder builder = new HtmlBuilder();
- builder.add("Plain.");
- builder.addLink(" (link) ", "runnable:0");
- builder.add("Plain.");
- // Check that the spaces surrounding the link text are not included in the link range
- assertEquals("Plain. <A HREF=\"runnable:0\">(link)</A>Plain.", builder.getHtml());
- }
-
- public void test2() {
- HtmlBuilder builder = new HtmlBuilder();
- builder.add("Plain").newline().addLink("mylink", "runnable:0").newline();
- builder.beginList().listItem().add("item 1").listItem().add("item 2").endList();
-
- assertEquals("Plain<BR/>\n" +
- "<A HREF=\"runnable:0\">mylink</A><BR/>\n" +
- "<DL>\n" +
- "<DD>-&NBSP;item 1\n" +
- "<DD>-&NBSP;item 2\n" +
- "</DL>", builder.getHtml());
- }
-
- public void test3() {
- HtmlBuilder builder1 = new HtmlBuilder();
- builder1.addBold("This is bold");
- assertEquals("<B>This is bold</B>", builder1.getHtml());
-
- HtmlBuilder builder2 = new HtmlBuilder();
- builder2.add("Plain. ");
- builder2.beginBold();
- builder2.add("Bold. ");
- builder2.addLink("mylink", "runnable:0");
- builder2.endBold();
- assertEquals("Plain. <B>Bold. <A HREF=\"runnable:0\">mylink</A></B>", builder2.getHtml());
- }
-
- public void test4() {
- HtmlBuilder builder = new HtmlBuilder();
- builder.add("Plain. ");
- builder.beginBold();
- builder.add("Bold. ");
- builder.addLink("mylink", "foo://bar:123");
- builder.endBold();
- assertEquals("Plain. <B>Bold. <A HREF=\"foo://bar:123\">mylink</A></B>", builder.getHtml());
- }
-
- public void test5() {
- HtmlBuilder builder = new HtmlBuilder();
- builder.addLink("This is the ", "linked text", "!", "foo://bar");
- assertEquals("This is the <A HREF=\"foo://bar\">linked text</A>!", builder.getHtml());
- }
-
- public void testTable1() {
- HtmlBuilder builder = new HtmlBuilder();
- builder.beginTable().addTableRow(true, "Header1", "Header2").addTableRow("Data1", "Data2").endTable();
- assertEquals("<table><tr><th>Header1</th><th>Header2</th></tr><tr><td>Data1</td><td>Data2</td></tr></table>",
- builder.getHtml());
- }
-
- public void testTable2() {
- HtmlBuilder builder = new HtmlBuilder();
- builder.beginTable("valign=\"top\"").addTableRow("Data1", "Data2").endTable();
- assertEquals("<table><tr><td valign=\"top\">Data1</td><td valign=\"top\">Data2</td></tr></table>",
- builder.getHtml());
- }
-
- public void testDiv1() {
- HtmlBuilder builder = new HtmlBuilder();
- assertEquals("<div>Hello</div>", builder.beginDiv().add("Hello").endDiv().getHtml());
- }
-
- public void testDiv2() {
- HtmlBuilder builder = new HtmlBuilder();
- assertEquals("<div style=\"padding: 10px; text-color: gray\">Hello</div>",
- builder.beginDiv("padding: 10px; text-color: gray").add("Hello").endDiv().getHtml());
- }
-
- public void testImage() throws IOException {
- File f = File.createTempFile("img", "png");
- f.deleteOnExit();
-
- String actual = new HtmlBuilder().addImage(f.toURI().toURL(), "preview").getHtml();
- String path = FileUtil.toSystemIndependentName(f.getAbsolutePath());
-
- if (!path.startsWith("/")) {
- path = '/' + path;
- }
- String expected = String.format("<img src='file:%1$s' alt=\"preview\" />", path);
- assertEquals(expected, actual);
- }
-}
diff --git a/android/testSrc/com/android/tools/idea/rendering/LocaleTest.java b/android/testSrc/com/android/tools/idea/rendering/LocaleTest.java
index 2d0f48d..b7c9ceb 100644
--- a/android/testSrc/com/android/tools/idea/rendering/LocaleTest.java
+++ b/android/testSrc/com/android/tools/idea/rendering/LocaleTest.java
@@ -15,6 +15,7 @@
*/
package com.android.tools.idea.rendering;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.ide.common.resources.configuration.LanguageQualifier;
import com.android.ide.common.resources.configuration.RegionQualifier;
import junit.framework.TestCase;
@@ -75,4 +76,13 @@
assertEquals("Locale{nb, __}", Locale.create(language1).toString());
assertEquals("Locale{nb, NO}", Locale.create(language1, region1).toString());
}
+
+ public void testFolderConfig() {
+ FolderConfiguration config = new FolderConfiguration();
+ assertEquals(Locale.ANY, Locale.create(config));
+ config.setLanguageQualifier(new LanguageQualifier("en"));
+ assertEquals(Locale.create("en"), Locale.create(config));
+ config.setRegionQualifier(new RegionQualifier("US"));
+ assertEquals(Locale.create("en-rUS"), Locale.create(config));
+ }
}
diff --git a/android/testSrc/com/android/tools/idea/rendering/ModuleResourceRepositoryTest.java b/android/testSrc/com/android/tools/idea/rendering/ModuleResourceRepositoryTest.java
new file mode 100644
index 0000000..95f5fae
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/rendering/ModuleResourceRepositoryTest.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.rendering;
+
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.res2.ResourceItem;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.resources.ResourceType;
+import com.android.tools.lint.detector.api.LintUtils;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import org.jetbrains.android.AndroidTestCase;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+@SuppressWarnings("SpellCheckingInspection")
+public class ModuleResourceRepositoryTest extends AndroidTestCase {
+ private static final String LAYOUT = "resourceRepository/layout.xml";
+ private static final String LAYOUT_OVERLAY = "resourceRepository/layoutOverlay.xml";
+ private static final String VALUES = "resourceRepository/values.xml";
+ private static final String VALUES_OVERLAY1 = "resourceRepository/valuesOverlay1.xml";
+ private static final String VALUES_OVERLAY2 = "resourceRepository/valuesOverlay2.xml";
+ private static final String VALUES_OVERLAY2_NO = "resourceRepository/valuesOverlay2No.xml";
+
+ public void testSingleResourceFolder() {
+ ProjectResources repository = ModuleResourceRepository.create(myFacet);
+ assertTrue(repository instanceof ResourceFolderRepository);
+ }
+
+ public void testOverlays() {
+ myFixture.copyFileToProject(LAYOUT, "res/layout/layout1.xml");
+ myFixture.copyFileToProject(LAYOUT_OVERLAY, "res2/layout/layout1.xml");
+ VirtualFile res1 = myFixture.copyFileToProject(VALUES, "res/values/values.xml").getParent().getParent();
+ VirtualFile res2 = myFixture.copyFileToProject(VALUES_OVERLAY1, "res2/values/values.xml").getParent().getParent();
+ VirtualFile res3 = myFixture.copyFileToProject(VALUES_OVERLAY2, "res3/values/nameDoesNotMatter.xml").getParent().getParent();
+ myFixture.copyFileToProject(VALUES_OVERLAY2_NO, "res3/values-no/values.xml");
+
+ assertNotSame(res1, res2);
+ assertNotSame(res1, res3);
+ assertNotSame(res2, res3);
+
+ ModuleResourceRepository resources = ModuleResourceRepository.createForTest(myFacet, Arrays.asList(res1, res2, res3));
+
+ // Check that values are handled correctly. First a plain value (not overridden anywhere).
+ assertStringIs(resources, "title_layout_changes", "Layout Changes");
+
+ // Check that an overridden key (overridden in just one flavor) is picked up
+ assertStringIs(resources, "title_crossfade", "Complex Crossfade"); // Overridden in res2
+ assertStringIs(resources, "title_zoom", "Zoom!"); // Overridden in res3
+
+ // Make sure that new/unique strings from flavors are available
+ assertStringIs(resources, "unique_string", "Unique"); // Overridden in res2
+ assertStringIs(resources, "another_unique_string", "Another Unique", false); // Overridden in res3
+
+ // Check that an overridden key (overridden in multiple flavors) picks the last one
+ assertStringIs(resources, "app_name", "Very Different App Name", false); // res3 (not unique because we have a values-no item too)
+
+ // Layouts: Should only be offered id's from the overriding layout (plus those defined in values.xml)
+ assertTrue(resources.hasResourceItem(ResourceType.ID, "action_next")); // from values.xml
+ assertTrue(resources.hasResourceItem(ResourceType.ID, "noteArea")); // from res2 layout1.xml
+
+ // Layout masking does not currently work. I'm not 100% certain what the intended behavior is
+ // here (e.g. res1's layout1 contains @+id/button1, res2's layout1 does not; should @+id/button1 be visible?)
+ //assertFalse(resources.hasResourceItem(ResourceType.ID, "btn_title_refresh")); // masked in res1 by res2's layout replacement
+
+ // Check that localized lookup (qualifier matching works)
+ List<ResourceItem> stringList = resources.getResourceItem(ResourceType.STRING, "another_unique_string");
+ assertNotNull(stringList);
+ assertSize(2, stringList);
+ FolderConfiguration valueConfig = FolderConfiguration.getConfigForFolder("values-no");
+ assertNotNull(valueConfig);
+ ResourceValue stringValue = resources.getConfiguredResources(ResourceType.STRING, valueConfig).get("another_unique_string");
+ assertNotNull(stringValue);
+ assertEquals("En Annen", stringValue.getValue());
+
+ // Change flavor order and make sure things are updated and work correctly
+ resources.updateRoots(Arrays.asList(res1, res3, res2));
+
+ // Should now be picking app_name from res2 rather than res3 since it's now last
+ assertStringIs(resources, "app_name", "Different App Name", false); // res2
+
+ // Sanity check other merging
+ assertStringIs(resources, "title_layout_changes", "Layout Changes");
+ assertStringIs(resources, "title_crossfade", "Complex Crossfade"); // Overridden in res2
+ assertStringIs(resources, "title_zoom", "Zoom!"); // Overridden in res3
+ assertStringIs(resources, "unique_string", "Unique"); // Overridden in res2
+ assertStringIs(resources, "another_unique_string", "Another Unique", false); // Overridden in res3
+
+ // Hide a resource root (res2)
+ resources.updateRoots(Arrays.asList(res1, res3));
+
+ // No longer aliasing the main layout
+ assertTrue(resources.hasResourceItem(ResourceType.ID, "btn_title_refresh")); // res1 layout1.xml
+ assertTrue(resources.hasResourceItem(ResourceType.ID, "noteArea")); // from res1 layout1.xml
+ assertTrue(resources.hasResourceItem(ResourceType.ID, "action_next")); // from values.xml
+
+ assertStringIs(resources, "title_crossfade", "Simple Crossfade"); // No longer overridden in res2
+
+ // Finally ensure that we can switch roots repeatedly (had some earlier bugs related to root unregistration)
+ resources.updateRoots(Arrays.asList(res1, res3, res2));
+ resources.updateRoots(Arrays.asList(res1));
+ resources.updateRoots(Arrays.asList(res1, res3, res2));
+ resources.updateRoots(Arrays.asList(res1));
+ resources.updateRoots(Arrays.asList(res1, res3, res2));
+ resources.updateRoots(Arrays.asList(res2));
+ resources.updateRoots(Arrays.asList(res1));
+ resources.updateRoots(Arrays.asList(res1, res2, res3));
+ assertStringIs(resources, "title_layout_changes", "Layout Changes");
+ }
+
+ public void testOverlayUpdates1() {
+ final VirtualFile layout = myFixture.copyFileToProject(LAYOUT, "res/layout/layout1.xml");
+ final VirtualFile layoutOverlay = myFixture.copyFileToProject(LAYOUT_OVERLAY, "res2/layout/layout1.xml");
+ VirtualFile res1 = myFixture.copyFileToProject(VALUES, "res/values/values.xml").getParent().getParent();
+ VirtualFile res2 = myFixture.copyFileToProject(VALUES_OVERLAY1, "res2/values/values.xml").getParent().getParent();
+ VirtualFile res3 = myFixture.copyFileToProject(VALUES_OVERLAY2, "res3/values/nameDoesNotMatter.xml").getParent().getParent();
+ myFixture.copyFileToProject(VALUES_OVERLAY2_NO, "res3/values-no/values.xml");
+ ModuleResourceRepository resources = ModuleResourceRepository.createForTest(myFacet, Arrays.asList(res1, res2, res3));
+ assertStringIs(resources, "title_layout_changes", "Layout Changes"); // sanity check
+
+ // Layout resource check:
+ // Check that our @/layout/layout1 resource currently refers to res2 override,
+ // then rename it to @layout/layout2, and verify that we have both, and then
+ // rename base to @layout/layout2 and verify that we are back to overriding.
+
+ assertTrue(resources.hasResourceItem(ResourceType.LAYOUT, "layout1"));
+ assertFalse(resources.hasResourceItem(ResourceType.LAYOUT, "layout2"));
+ PsiResourceItem layout1 = getSingleItem(resources, ResourceType.LAYOUT, "layout1");
+ assertItemIsInDir(res2, layout1);
+
+ long generation = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ layoutOverlay.rename(this, "layout2.xml");
+ }
+ catch (IOException e) {
+ fail(e.toString());
+ }
+ }
+ });
+ assertTrue(resources.getModificationCount() > generation);
+
+ assertTrue(resources.hasResourceItem(ResourceType.LAYOUT, "layout2"));
+ assertTrue(resources.hasResourceItem(ResourceType.LAYOUT, "layout1"));
+
+ // Layout should now be coming through from res1 since res2 is no longer overriding it
+ layout1 = getSingleItem(resources, ResourceType.LAYOUT, "layout1");
+ assertItemIsInDir(res1, layout1);
+
+ PsiResourceItem layout2 = getSingleItem(resources, ResourceType.LAYOUT, "layout2");
+ assertItemIsInDir(res2, layout2);
+
+ // Now rename layout1 to layout2 to hide it again
+ generation = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ layout.rename(this, "layout2.xml");
+ }
+ catch (IOException e) {
+ fail(e.toString());
+ }
+ }
+ });
+ assertTrue(resources.getModificationCount() > generation);
+
+ assertTrue(resources.hasResourceItem(ResourceType.LAYOUT, "layout2"));
+ assertFalse(resources.hasResourceItem(ResourceType.LAYOUT, "layout1"));
+
+ layout2 = getSingleItem(resources, ResourceType.LAYOUT, "layout2");
+ assertItemIsInDir(res2, layout2);
+ }
+
+ public void testOverlayUpdates2() {
+ // Like testOverlayUpdates1, but rather than testing changes to layout resources (file-based resource)
+ // perform document edits in value-documents
+
+ myFixture.copyFileToProject(LAYOUT, "res/layout/layout1.xml");
+ myFixture.copyFileToProject(LAYOUT_OVERLAY, "res2/layout/layout1.xml");
+ VirtualFile values1 = myFixture.copyFileToProject(VALUES, "res/values/values.xml");
+ VirtualFile values2 = myFixture.copyFileToProject(VALUES_OVERLAY1, "res2/values/values.xml");
+ VirtualFile values3 = myFixture.copyFileToProject(VALUES_OVERLAY2, "res3/values/nameDoesNotMatter.xml");
+ final VirtualFile values3No = myFixture.copyFileToProject(VALUES_OVERLAY2_NO, "res3/values-no/values.xml");
+ VirtualFile res1 = values1.getParent().getParent();
+ VirtualFile res2 = values2.getParent().getParent();
+ VirtualFile res3 = values3.getParent().getParent();
+ ModuleResourceRepository resources = ModuleResourceRepository.createForTest(myFacet, Arrays.asList(res1, res2, res3));
+ PsiFile psiValues1 = PsiManager.getInstance(getProject()).findFile(values1);
+ assertNotNull(psiValues1);
+ PsiFile psiValues2 = PsiManager.getInstance(getProject()).findFile(values2);
+ assertNotNull(psiValues2);
+ PsiFile psiValues3 = PsiManager.getInstance(getProject()).findFile(values3);
+ assertNotNull(psiValues3);
+ PsiFile psiValues3No = PsiManager.getInstance(getProject()).findFile(values3No);
+ assertNotNull(psiValues3No);
+
+ // Initial state; sanity check from #testOverlays()
+ assertStringIs(resources, "title_layout_changes", "Layout Changes");
+ assertStringIs(resources, "title_crossfade", "Complex Crossfade"); // Overridden in res2
+ assertStringIs(resources, "title_zoom", "Zoom!"); // Overridden in res3
+ assertStringIs(resources, "unique_string", "Unique"); // Overridden in res2
+ assertStringIs(resources, "another_unique_string", "Another Unique", false); // Overridden in res3
+ assertStringIs(resources, "app_name", "Very Different App Name", false); // res3 (not unique because we have a values-no item too)
+
+ // Value resource check:
+ // Verify that an edit in a value file, both in a non-overridden and an overridden
+ // value, is observed; and that an override in an overridden value is not observed.
+
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "app_name"));
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "title_layout_changes"));
+ PsiResourceItem appName = getFirstItem(resources, ResourceType.STRING, "app_name");
+ assertItemIsInDir(res3, appName);
+ assertStringIs(resources, "app_name", "Very Different App Name", false); // res3 (not unique because we have a values-no item too)
+
+ long generation = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiValues3);
+ assertNotNull(document);
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ int offset = document.getText().indexOf("Very Different App Name");
+ document.insertString(offset, "Not ");
+ documentManager.commitDocument(document);
+ }
+ });
+ assertTrue(resources.getModificationCount() > generation);
+
+ // Should still be defined in res3 but have new value
+ appName = getFirstItem(resources, ResourceType.STRING, "app_name");
+ assertItemIsInDir(res3, appName);
+ assertStringIs(resources, "app_name", "Not Very Different App Name", false);
+
+ generation = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ int offset = document.getText().indexOf("app_name");
+ document.insertString(offset, "r");
+ documentManager.commitDocument(document);
+ }
+ });
+ assertTrue(resources.getModificationCount() > generation);
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "rapp_name"));
+
+ appName = getFirstItem(resources, ResourceType.STRING, "app_name");
+ // The item is still under res3, but now it's in the Norwegian translation
+ assertEquals("no", appName.getSource().getQualifiers());
+ assertStringIs(resources, "app_name", "Forskjellig Navn", false);
+
+ // Delete that file:
+ generation = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ values3No.delete(this);
+ }
+ catch (IOException e) {
+ fail(e.toString());
+ }
+ }
+ });
+ assertTrue(resources.getModificationCount() > generation);
+
+ // Now the item is no longer available in res3; should fallback to res 2
+ appName = getFirstItem(resources, ResourceType.STRING, "app_name");
+ assertItemIsInDir(res2, appName);
+ assertStringIs(resources, "app_name", "Different App Name", false);
+
+ // Check that editing an overridden attribute does not count as a change
+ final Document document2 = documentManager.getDocument(psiValues1);
+ assertNotNull(document2);
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ int offset = document2.getText().indexOf("Animations Demo");
+ document2.insertString(offset, "Cool ");
+ documentManager.commitDocument(document2);
+ }
+ });
+ // Unaffected by above change
+ assertStringIs(resources, "app_name", "Different App Name", false);
+
+ // Finally check that editing an non-overridden attribute also gets picked up as a change
+ generation = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ int offset = document2.getText().indexOf("Layout Changes");
+ document2.insertString(offset, "New ");
+ documentManager.commitDocument(document2);
+ }
+ });
+ assertTrue(resources.getModificationCount() > generation);
+ assertStringIs(resources, "title_layout_changes", "New Layout Changes", false);
+ }
+
+ // Unit test support methods
+
+ static void assertItemIsInDir(VirtualFile dir, PsiResourceItem item) {
+ PsiFile psiFile = item.getPsiFile();
+ assertNotNull(psiFile);
+ VirtualFile parent = psiFile.getVirtualFile();
+ assertNotNull(parent);
+ assertSame(dir, parent.getParent().getParent());
+ }
+
+ static void assertStringIs(ProjectResources repository, String key, String expected) {
+ assertStringIs(repository, key, expected, true);
+ }
+
+ @NotNull
+ private static PsiResourceItem getSingleItem(ProjectResources repository, ResourceType type, String key) {
+ List<ResourceItem> list = repository.getResourceItem(type, key);
+ assertNotNull(list);
+ assertSize(1, list);
+ ResourceItem item = list.get(0);
+ assertNotNull(item);
+ assertTrue(item instanceof PsiResourceItem);
+ return (PsiResourceItem)item;
+ }
+
+ @NotNull
+ static PsiResourceItem getFirstItem(ProjectResources repository, ResourceType type, String key) {
+ List<ResourceItem> list = repository.getResourceItem(type, key);
+ assertNotNull(list);
+ ResourceItem item = list.get(0);
+ assertNotNull(item);
+ assertTrue(item instanceof PsiResourceItem);
+ return (PsiResourceItem)item;
+ }
+
+ static void assertStringIs(ProjectResources repository, String key, String expected, boolean mustBeUnique) {
+ assertTrue(repository.hasResourceItem(ResourceType.STRING, key));
+ List<ResourceItem> list = repository.getResourceItem(ResourceType.STRING, key);
+ assertNotNull(list);
+
+ // generally we expect just one item (e.g. overlays should not visible, which is why we assert a single item, but for items
+ // that for example have translations there could be multiple items, and we test this, so allow assertion to specify whether it's
+ // expected)
+ if (mustBeUnique) {
+ assertSize(1, list);
+ }
+
+ ResourceItem item = list.get(0);
+ ResourceValue resourceValue = item.getResourceValue(false);
+ assertNotNull(resourceValue);
+ assertEquals(expected, resourceValue.getValue());
+ }
+
+ public void testAllowEmpty() {
+ assertTrue(LintUtils.assertionsEnabled()); // this test should be run with assertions enabled!
+ ProjectResources repository = ModuleResourceRepository.createForTest(myFacet, Collections.<VirtualFile>emptyList());
+ assertNotNull(repository);
+ repository.getModificationCount();
+ assertEmpty(repository.getItemsOfType(ResourceType.ID));
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/rendering/ModuleSetResourceRepositoryTest.java b/android/testSrc/com/android/tools/idea/rendering/ModuleSetResourceRepositoryTest.java
new file mode 100644
index 0000000..e46346c
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/rendering/ModuleSetResourceRepositoryTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.rendering;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.res2.ResourceItem;
+import com.android.resources.ResourceType;
+import com.android.tools.idea.gradle.IdeaAndroidProject;
+import com.android.tools.idea.gradle.TestProjects;
+import com.android.tools.idea.gradle.stubs.android.AndroidLibraryStub;
+import com.android.tools.idea.gradle.stubs.android.AndroidProjectStub;
+import com.android.tools.idea.gradle.stubs.android.VariantStub;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.roots.ModuleRootModificationUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.testFramework.fixtures.IdeaProjectTestFixture;
+import com.intellij.testFramework.fixtures.TestFixtureBuilder;
+import org.jetbrains.android.AndroidTestCase;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.android.util.AndroidUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.*;
+
+import static com.android.tools.idea.rendering.ModuleResourceRepositoryTest.getFirstItem;
+
+public class ModuleSetResourceRepositoryTest extends AndroidTestCase {
+ private static final String LAYOUT = "resourceRepository/layout.xml";
+ private static final String VALUES = "resourceRepository/values.xml";
+ private static final String VALUES_OVERLAY1 = "resourceRepository/valuesOverlay1.xml";
+ private static final String VALUES_OVERLAY2 = "resourceRepository/valuesOverlay2.xml";
+ private static final String VALUES_OVERLAY2_NO = "resourceRepository/valuesOverlay2No.xml";
+
+ // Ensure that we invalidate the id cache when the file is rescanned but ids don't change
+ // (this was broken)
+ public void testInvalidateIds() {
+ // Like testOverlayUpdates1, but rather than testing changes to layout resources (file-based resource)
+ // perform document edits in value-documents
+
+ VirtualFile layoutFile = myFixture.copyFileToProject(LAYOUT, "res/layout/layout1.xml");
+
+ VirtualFile res1 = myFixture.copyFileToProject(VALUES, "res/values/values.xml").getParent().getParent();
+ VirtualFile res2 = myFixture.copyFileToProject(VALUES_OVERLAY1, "res2/values/values.xml").getParent().getParent();
+ VirtualFile res3 = myFixture.copyFileToProject(VALUES_OVERLAY2, "res3/values/nameDoesNotMatter.xml").getParent().getParent();
+ myFixture.copyFileToProject(VALUES_OVERLAY2_NO, "res3/values-no/values.xml");
+
+ assertNotSame(res1, res2);
+ assertNotSame(res1, res3);
+ assertNotSame(res2, res3);
+
+ // Just need an empty repository to make it a real module -set-; otherwise with a single
+ // module we just get a module repository, not a module set repository
+ ProjectResources other = new ProjectResources("unit test") {
+ @NonNull
+ @Override
+ protected Map<ResourceType, ListMultimap<String, ResourceItem>> getMap() {
+ return Collections.emptyMap();
+ }
+
+ @Nullable
+ @Override
+ protected ListMultimap<String, ResourceItem> getMap(ResourceType type, boolean create) {
+ return ArrayListMultimap.create();
+ }
+ };
+
+ ModuleResourceRepository module = ModuleResourceRepository.createForTest(myFacet, Arrays.asList(res1, res2, res3));
+ final ProjectResources r = ModuleSetResourceRepository.create(myFacet, Arrays.asList(module, other));
+ assertTrue(r instanceof ModuleSetResourceRepository);
+ final ModuleSetResourceRepository resources = (ModuleSetResourceRepository)r;
+
+ PsiFile layoutPsiFile = PsiManager.getInstance(getProject()).findFile(layoutFile);
+ assertNotNull(layoutPsiFile);
+ assertTrue(resources.hasResourceItem(ResourceType.ID, "btn_title_refresh"));
+ final PsiResourceItem item = getFirstItem(resources, ResourceType.ID, "btn_title_refresh");
+
+ final long generation = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(layoutPsiFile);
+ assertNotNull(document);
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ String string = "<ImageView style=\"@style/TitleBarSeparator\" />";
+ int offset = document.getText().indexOf(string);
+ document.deleteString(offset, offset + string.length());
+ documentManager.commitDocument(document);
+ }
+ });
+
+ assertTrue(resources.isScanPending(layoutPsiFile));
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ assertTrue(generation < resources.getModificationCount());
+ // Should still be defined:
+ assertTrue(resources.hasResourceItem(ResourceType.ID, "btn_title_refresh"));
+ PsiResourceItem newItem = getFirstItem(resources, ResourceType.ID, "btn_title_refresh");
+ assertNotNull(newItem.getSource());
+ // However, should be a different item
+ assertNotSame(item, newItem);
+ }
+ });
+ }
+
+ // Regression test for https://code.google.com/p/android/issues/detail?id=57090
+ public void testParents() {
+ myFixture.copyFileToProject(LAYOUT, "res/layout/layout1.xml");
+ List<AndroidFacet> libraries = AndroidUtils.getAllAndroidDependencies(myModule, true);
+ assertEquals(2, libraries.size());
+ ModuleRootModificationUtil.addDependency(libraries.get(0).getModule(), libraries.get(1).getModule());
+
+
+ addArchiveLibraries();
+
+ ProjectResources r = ModuleSetResourceRepository.create(myFacet);
+ assertTrue(r instanceof ModuleSetResourceRepository);
+ ModuleSetResourceRepository repository = (ModuleSetResourceRepository)r;
+ assertEquals(3, repository.getChildCount());
+ Collection<String> items = repository.getItemsOfType(ResourceType.STRING);
+ assertTrue(items.isEmpty());
+
+ for (AndroidFacet facet : libraries) {
+ ProjectResources moduleRepository = facet.getProjectResources(false, true);
+ assertNotNull(moduleRepository);
+ ProjectResources moduleSetRepository = facet.getProjectResources(true, true);
+ assertNotNull(moduleSetRepository);
+ }
+ myFacet.getProjectResources(false, true);
+ myFacet.getProjectResources(true, true);
+ }
+
+ private void addArchiveLibraries() {
+ // Add in some Android projects too
+ myFacet.getConfiguration().getState().ALLOW_USER_CONFIGURATION = false; // make it a Gradle project
+ AndroidProjectStub androidProject = TestProjects.createFlavorsProject();
+ VariantStub variant = androidProject.getFirstVariant();
+ assertNotNull(variant);
+ String rootDirPath = androidProject.getRootDir().getAbsolutePath();
+ IdeaAndroidProject ideaAndroidProject =
+ new IdeaAndroidProject(androidProject.getName(), rootDirPath, androidProject, variant.getName());
+ myFacet.setIdeaAndroidProject(ideaAndroidProject);
+
+ File libJar = new File(rootDirPath, "library.aar/library.jar");
+ AndroidLibraryStub library = new AndroidLibraryStub(libJar);
+ variant.getMainArtifactInfo().getDependencies().addLibrary(library);
+ }
+
+ @Override
+ protected void configureAdditionalModules(@NotNull TestFixtureBuilder<IdeaProjectTestFixture> projectBuilder,
+ @NotNull List<MyAdditionalModuleData> modules) {
+ final String testName = getTestName(true);
+ if (testName.equals("parents")) { // for unit test testParents
+ addModuleWithAndroidFacet(projectBuilder, modules, "lib1", true);
+ addModuleWithAndroidFacet(projectBuilder, modules, "lib2", true);
+ }
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/rendering/RenderErrorPanelTest.java b/android/testSrc/com/android/tools/idea/rendering/RenderErrorPanelTest.java
index f3e05b3..c489d85 100644
--- a/android/testSrc/com/android/tools/idea/rendering/RenderErrorPanelTest.java
+++ b/android/testSrc/com/android/tools/idea/rendering/RenderErrorPanelTest.java
@@ -18,12 +18,23 @@
import com.android.tools.idea.configurations.Configuration;
import com.android.tools.idea.configurations.ConfigurationManager;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Lists;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import org.jetbrains.android.AndroidTestCase;
import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.android.util.AndroidCommonUtils;
+import org.jetbrains.annotations.NotNull;
+import java.lang.reflect.Constructor;
+import java.util.Iterator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@SuppressWarnings("SpellCheckingInspection")
public class RenderErrorPanelTest extends AndroidTestCase {
public static final String BASE_PATH = "render/";
@@ -39,7 +50,7 @@
AndroidFacet facet = AndroidFacet.getInstance(myModule);
PsiFile psiFile = PsiManager.getInstance(getProject()).findFile(file);
assertNotNull(psiFile);
-
+ assertNotNull(facet);
ConfigurationManager configurationManager = facet.getConfigurationManager();
assertNotNull(configurationManager);
Configuration configuration = configurationManager.getConfiguration(file);
@@ -51,8 +62,11 @@
assertTrue(logger.hasProblems());
RenderErrorPanel panel = new RenderErrorPanel();
String html = panel.showErrors(render);
+ assert html != null;
+ html = stripImages(html);
+
assertEquals(
- "<HTML><BODY><font style=\"font-weight:bold; color:#005555;\">Rendering Problems</font><BR/>\n" +
+ "<html><body><A HREF=\"action:close\"></A><font style=\"font-weight:bold; color:#005555;\">Rendering Problems</font><BR/>\n" +
"<B>NOTE: One or more layouts are missing the layout_width or layout_height attributes. These are required in most layouts.</B><BR/>\n" +
"<LinearLayout> does not set the required layout_width attribute: <BR/>\n" +
" <A HREF=\"command:0\">Set to wrap_content</A>, <A HREF=\"command:1\">Set to match_parent</A><BR/>\n" +
@@ -62,11 +76,287 @@
"Or: <A HREF=\"command:4\">Automatically add all missing attributes</A><BR/>\n" +
"<BR/>\n" +
"<BR/>\n" +
- "The following classes could not be found:<DL>\n" +
- "<DD>-&NBSP;LinerLayout (<A HREF=\"action:classpath\">Fix Build Path</A>)\n" +
- "</DL>Tip: Try to <A HREF=\"action:build\">build</A> the project<BR/>\n" +
- "<BR/>\n" +
- "</BODY></HTML>",
+ "</body></html>",
html);
}
+
+ public void testBrokenLayoutLib() {
+ VirtualFile file = myFixture.copyFileToProject(BASE_PATH + "layout2.xml", "res/layout/layout.xml");
+ assertNotNull(file);
+ AndroidFacet facet = AndroidFacet.getInstance(myModule);
+ PsiFile psiFile = PsiManager.getInstance(getProject()).findFile(file);
+ assertNotNull(psiFile);
+ assertNotNull(facet);
+ ConfigurationManager configurationManager = facet.getConfigurationManager();
+ assertNotNull(configurationManager);
+ Configuration configuration = configurationManager.getConfiguration(file);
+ RenderLogger logger = new RenderLogger("mylogger", myModule);
+ RenderService service = RenderService.create(facet, myModule, psiFile, configuration, logger, null);
+ assertNotNull(service);
+ RenderResult render = service.render();
+ assertNotNull(render);
+
+ // MANUALLY register errors
+ logger.error(null, "This is an error with entities: & < \"", null);
+
+ Throwable throwable = createExceptionFromDesc(
+ "java.lang.NullPointerException\n" +
+ "\tat android.text.format.DateUtils.getDayOfWeekString(DateUtils.java:248)\n" +
+ "\tat android.widget.CalendarView.setUpHeader(CalendarView.java:1034)\n" +
+ "\tat android.widget.CalendarView.<init>(CalendarView.java:403)\n" +
+ "\tat android.widget.CalendarView.<init>(CalendarView.java:333)\n" +
+ "\tat sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)\n" +
+ "\tat sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)\n" +
+ "\tat sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)\n" +
+ "\tat java.lang.reflect.Constructor.newInstance(Constructor.java:513)\n" +
+ "\tat android.view.LayoutInflater.createView(LayoutInflater.java:594)\n" +
+ "\tat android.view.BridgeInflater.onCreateView(BridgeInflater.java:86)\n" +
+ "\tat android.view.LayoutInflater.onCreateView(LayoutInflater.java:669)\n" +
+ "\tat android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:694)\n" +
+ "\tat android.view.BridgeInflater.createViewFromTag(BridgeInflater.java:131)\n" +
+ "\tat android.view.LayoutInflater.rInflate_Original(LayoutInflater.java:755)\n" +
+ "\tat android.view.LayoutInflater_Delegate.rInflate(LayoutInflater_Delegate.java:64)\n" +
+ "\tat android.view.LayoutInflater.rInflate(LayoutInflater.java:727)\n" +
+ "\tat android.view.LayoutInflater.inflate(LayoutInflater.java:492)\n" +
+ "\tat android.view.LayoutInflater.inflate(LayoutInflater.java:397)\n" +
+ "\tat android.widget.DatePicker.<init>(DatePicker.java:171)\n" +
+ "\tat android.widget.DatePicker.<init>(DatePicker.java:145)\n" +
+ "\tat sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)\n" +
+ "\tat sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)\n" +
+ "\tat sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)\n" +
+ "\tat java.lang.reflect.Constructor.newInstance(Constructor.java:513)\n" +
+ "\tat android.view.LayoutInflater.createView(LayoutInflater.java:594)\n" +
+ "\tat android.view.BridgeInflater.onCreateView(BridgeInflater.java:86)\n" +
+ "\tat android.view.LayoutInflater.onCreateView(LayoutInflater.java:669)\n" +
+ "\tat android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:694)\n" +
+ "\tat android.view.BridgeInflater.createViewFromTag(BridgeInflater.java:131)\n" +
+ "\tat android.view.LayoutInflater.rInflate_Original(LayoutInflater.java:755)\n" +
+ "\tat android.view.LayoutInflater_Delegate.rInflate(LayoutInflater_Delegate.java:64)\n" +
+ "\tat android.view.LayoutInflater.rInflate(LayoutInflater.java:727)\n" +
+ "\tat android.view.LayoutInflater.inflate(LayoutInflater.java:492)\n" +
+ "\tat android.view.LayoutInflater.inflate(LayoutInflater.java:373)\n" +
+ "\tat com.android.layoutlib.bridge.impl.RenderSessionImpl.inflate(RenderSessionImpl.java:385)\n" +
+ "\tat com.android.layoutlib.bridge.Bridge.createSession(Bridge.java:332)\n" +
+ "\tat com.android.ide.common.rendering.LayoutLibrary.createSession(LayoutLibrary.java:325)\n" +
+ "\tat com.android.tools.idea.rendering.RenderService$3.compute(RenderService.java:525)\n" +
+ "\tat com.android.tools.idea.rendering.RenderService$3.compute(RenderService.java:518)\n" +
+ "\tat com.intellij.openapi.application.impl.ApplicationImpl.runReadAction(ApplicationImpl.java:958)\n" +
+ "\tat com.android.tools.idea.rendering.RenderService.createRenderSession(RenderService.java:518)\n" +
+ "\tat com.android.tools.idea.rendering.RenderService.render(RenderService.java:555)\n" +
+ "\tat com.intellij.android.designer.designSurface.AndroidDesignerEditorPanel$7$2.compute(AndroidDesignerEditorPanel.java:498)\n" +
+ "\tat com.intellij.android.designer.designSurface.AndroidDesignerEditorPanel$7$2.compute(AndroidDesignerEditorPanel.java:491)\n" +
+ "\tat com.intellij.openapi.application.impl.ApplicationImpl.runReadAction(ApplicationImpl.java:969)\n" +
+ "\tat com.intellij.android.designer.designSurface.AndroidDesignerEditorPanel$7.run(AndroidDesignerEditorPanel.java:491)\n" +
+ "\tat com.intellij.util.ui.update.MergingUpdateQueue.execute(MergingUpdateQueue.java:320)\n" +
+ "\tat com.intellij.util.ui.update.MergingUpdateQueue.execute(MergingUpdateQueue.java:310)\n" +
+ "\tat com.intellij.util.ui.update.MergingUpdateQueue$2.run(MergingUpdateQueue.java:254)\n" +
+ "\tat com.intellij.util.ui.update.MergingUpdateQueue.flush(MergingUpdateQueue.java:269)\n" +
+ "\tat com.intellij.util.ui.update.MergingUpdateQueue.flush(MergingUpdateQueue.java:227)\n" +
+ "\tat com.intellij.util.ui.update.MergingUpdateQueue.run(MergingUpdateQueue.java:217)\n" +
+ "\tat com.intellij.util.concurrency.QueueProcessor.runSafely(QueueProcessor.java:237)\n" +
+ "\tat com.intellij.util.Alarm$Request$1.run(Alarm.java:297)\n" +
+ "\tat java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:439)\n" +
+ "\tat java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:303)\n" +
+ "\tat java.util.concurrent.FutureTask.run(FutureTask.java:138)\n" +
+ "\tat java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895)\n" +
+ "\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)\n" +
+ "\tat java.lang.Thread.run(Thread.java:680)\n");
+ logger.error(null, null, throwable, null);
+
+ assertTrue(logger.hasProblems());
+ RenderErrorPanel panel = new RenderErrorPanel();
+ String html = panel.showErrors(render);
+ assert html != null;
+ html = stripImages(html);
+
+ assertEquals(
+ "<html><body><A HREF=\"action:close\"></A><font style=\"font-weight:bold; color:#005555;\">Rendering Problems</font><BR/>\n" +
+ "This is an error with entities: & < \"<BR/>\n" +
+ "<CalendarView> and <DatePicker> are broken in this version of the rendering library. " +
+ "Try updating your SDK in the SDK Manager when issue 59732 is fixed. " +
+ "(<A HREF=\"http://b.android.com/59732\">Open Issue 59732</A>, <A HREF=\"runnable:0\">Show Exception</A>)<BR/>\n" +
+ "</body></html>",
+ html);
+ }
+
+ public void testBrokenCustomView() {
+ VirtualFile file = myFixture.copyFileToProject(BASE_PATH + "layout2.xml", "res/layout/layout.xml");
+ assertNotNull(file);
+ AndroidFacet facet = AndroidFacet.getInstance(myModule);
+ PsiFile psiFile = PsiManager.getInstance(getProject()).findFile(file);
+ assertNotNull(psiFile);
+ assertNotNull(facet);
+ ConfigurationManager configurationManager = facet.getConfigurationManager();
+ assertNotNull(configurationManager);
+ Configuration configuration = configurationManager.getConfiguration(file);
+ RenderLogger logger = new RenderLogger("mylogger", myModule);
+ RenderService service = RenderService.create(facet, myModule, psiFile, configuration, logger, null);
+ assertNotNull(service);
+ RenderResult render = service.render();
+ assertNotNull(render);
+
+ Throwable throwable = createExceptionFromDesc(
+ "java.lang.ArithmeticException: / by zero\n" +
+ "\tat com.example.myapplication574.MyCustomView.<init>(MyCustomView.java:13)\n" +
+ "\tat sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)\n" +
+ "\tat sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)\n" +
+ "\tat sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)\n" +
+ "\tat java.lang.reflect.Constructor.newInstance(Constructor.java:513)\n" +
+ "\tat org.jetbrains.android.uipreview.ViewLoader.createNewInstance(ViewLoader.java:365)\n" +
+ "\tat org.jetbrains.android.uipreview.ViewLoader.loadView(ViewLoader.java:97)\n" +
+ "\tat com.android.tools.idea.rendering.ProjectCallback.loadView(ProjectCallback.java:121)\n" +
+ "\tat android.view.BridgeInflater.loadCustomView(BridgeInflater.java:207)\n" +
+ "\tat android.view.BridgeInflater.createViewFromTag(BridgeInflater.java:135)\n" +
+ "\tat android.view.LayoutInflater.rInflate_Original(LayoutInflater.java:755)\n" +
+ "\tat android.view.LayoutInflater_Delegate.rInflate(LayoutInflater_Delegate.java:64)\n" +
+ "\tat android.view.LayoutInflater.rInflate(LayoutInflater.java:727)\n" +
+ "\tat android.view.LayoutInflater.inflate(LayoutInflater.java:492)\n" +
+ "\tat android.view.LayoutInflater.inflate(LayoutInflater.java:373)\n" +
+ "\tat com.android.layoutlib.bridge.impl.RenderSessionImpl.inflate(RenderSessionImpl.java:385)\n" +
+ "\tat com.android.layoutlib.bridge.Bridge.createSession(Bridge.java:332)\n" +
+ "\tat com.android.ide.common.rendering.LayoutLibrary.createSession(LayoutLibrary.java:325)\n" +
+ "\tat com.android.tools.idea.rendering.RenderService$3.compute(RenderService.java:525)\n" +
+ "\tat com.android.tools.idea.rendering.RenderService$3.compute(RenderService.java:518)\n" +
+ "\tat com.intellij.openapi.application.impl.ApplicationImpl.runReadAction(ApplicationImpl.java:958)\n" +
+ "\tat com.android.tools.idea.rendering.RenderService.createRenderSession(RenderService.java:518)\n" +
+ "\tat com.android.tools.idea.rendering.RenderService.render(RenderService.java:555)\n" +
+ "\tat org.jetbrains.android.uipreview.AndroidLayoutPreviewToolWindowManager$9.compute(AndroidLayoutPreviewToolWindowManager.java:418)\n" +
+ "\tat org.jetbrains.android.uipreview.AndroidLayoutPreviewToolWindowManager$9.compute(AndroidLayoutPreviewToolWindowManager.java:411)\n" +
+ "\tat com.intellij.openapi.application.impl.ApplicationImpl.runReadAction(ApplicationImpl.java:969)\n" +
+ "\tat org.jetbrains.android.uipreview.AndroidLayoutPreviewToolWindowManager.doRender(AndroidLayoutPreviewToolWindowManager.java:411)\n" +
+ "\tat org.jetbrains.android.uipreview.AndroidLayoutPreviewToolWindowManager.access$1100(AndroidLayoutPreviewToolWindowManager.java:79)\n" +
+ "\tat org.jetbrains.android.uipreview.AndroidLayoutPreviewToolWindowManager$8$1.run(AndroidLayoutPreviewToolWindowManager.java:373)\n" +
+ "\tat com.intellij.openapi.progress.impl.ProgressManagerImpl$2.run(ProgressManagerImpl.java:178)\n" +
+ "\tat com.intellij.openapi.progress.ProgressManager.executeProcessUnderProgress(ProgressManager.java:207)\n" +
+ "\tat com.intellij.openapi.progress.impl.ProgressManagerImpl.executeProcessUnderProgress(ProgressManagerImpl.java:212)\n" +
+ "\tat com.intellij.openapi.progress.impl.ProgressManagerImpl.runProcess(ProgressManagerImpl.java:171)\n" +
+ "\tat org.jetbrains.android.uipreview.AndroidLayoutPreviewToolWindowManager$8.run(AndroidLayoutPreviewToolWindowManager.java:368)\n" +
+ "\tat com.intellij.util.ui.update.MergingUpdateQueue.execute(MergingUpdateQueue.java:320)\n" +
+ "\tat com.intellij.util.ui.update.MergingUpdateQueue.execute(MergingUpdateQueue.java:310)\n" +
+ "\tat com.intellij.util.ui.update.MergingUpdateQueue$2.run(MergingUpdateQueue.java:254)\n" +
+ "\tat com.intellij.util.ui.update.MergingUpdateQueue.flush(MergingUpdateQueue.java:269)\n" +
+ "\tat com.intellij.util.ui.update.MergingUpdateQueue.flush(MergingUpdateQueue.java:227)\n" +
+ "\tat com.intellij.util.ui.update.MergingUpdateQueue.run(MergingUpdateQueue.java:217)\n" +
+ "\tat com.intellij.util.concurrency.QueueProcessor.runSafely(QueueProcessor.java:237)\n" +
+ "\tat com.intellij.util.Alarm$Request$1.run(Alarm.java:297)\n" +
+ "\tat java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:439)\n" +
+ "\tat java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:303)\n" +
+ "\tat java.util.concurrent.FutureTask.run(FutureTask.java:138)\n" +
+ "\tat java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895)\n" +
+ "\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)\n" +
+ "\tat java.lang.Thread.run(Thread.java:680)\n");
+ logger.error(null, null, throwable, null);
+
+ assertTrue(logger.hasProblems());
+ RenderErrorPanel panel = new RenderErrorPanel();
+ String html = panel.showErrors(render);
+ assert html != null;
+ html = stripImages(html);
+
+ assertEquals(
+ "<html><body><A HREF=\"action:close\"></A><font style=\"font-weight:bold; color:#005555;\">Rendering Problems</font><BR/>\n" +
+ "java.lang.ArithmeticException: / by zero<BR/>\n" +
+ " at com.example.myapplication574.MyCustomView.<init>" +
+ "(<A HREF=\"open:com.example.myapplication574.MyCustomView#<init>;MyCustomView.java:13\">MyCustomView.java:13</A>)<BR/>\n" +
+ " at java.lang.reflect.Constructor.newInstance<BR/>\n" +
+ " at android.view.LayoutInflater.rInflate_Original<BR/>\n" +
+ " at android.view.LayoutInflater_Delegate.rInflate<BR/>\n" +
+ " at android.view.LayoutInflater.rInflate<BR/>\n" +
+ " at android.view.LayoutInflater.inflate<BR/>\n" +
+ " at android.view.LayoutInflater.inflate<BR/>\n" +
+ "<BR/>\n" +
+ "</body></html>",
+ html);
+ }
+
+ // Image paths will include full resource urls which depends on the test environment
+ private static String stripImages(@NotNull String html) {
+ while (true) {
+ int index = html.indexOf("<img");
+ if (index == -1) {
+ return html;
+ }
+ int end = html.indexOf('>', index);
+ if (end == -1) {
+ return html;
+ } else {
+ html = html.substring(0, index) + html.substring(end + 1);
+ }
+ }
+ }
+
+ /** Attempts to create an exception object that matches the given description, which
+ * is in the form of the output of an exception stack dump ({@link Throwable#printStackTrace()})
+ *
+ * @param desc the description of an exception
+ * @return a corresponding exception if possible
+ */
+ @SuppressWarnings("ThrowableInstanceNeverThrown")
+ private static Throwable createExceptionFromDesc(String desc) {
+ // First line: description and type
+ Iterator<String> iterator = Splitter.on('\n').split(desc).iterator();
+ assertTrue(iterator.hasNext());
+ String first = iterator.next();
+ assertTrue(iterator.hasNext());
+ String message = null;
+ String exceptionClass;
+ int index = first.indexOf(':');
+ if (index != -1) {
+ exceptionClass = first.substring(0, index).trim();
+ message = first.substring(index + 1).trim();
+ } else {
+ exceptionClass = first.trim();
+ }
+
+ Throwable throwable;
+ try {
+ @SuppressWarnings("unchecked")
+ Class<Throwable> clz = (Class<Throwable>)Class.forName(exceptionClass);
+ if (message == null) {
+ throwable = clz.newInstance();
+ } else {
+ Constructor<Throwable> constructor = clz.getConstructor(String.class);
+ throwable = constructor.newInstance(message);
+ }
+ } catch (Throwable t) {
+ throwable = message != null ? new Throwable(message) : new Throwable();
+ }
+
+ List<StackTraceElement> frames = Lists.newArrayList();
+ Pattern outerPattern = Pattern.compile("\tat (.*)\\.([^.]*)\\((.*)\\)");
+ Pattern innerPattern = Pattern.compile("(.*):(\\d*)");
+ while (iterator.hasNext()) {
+ String line = iterator.next();
+ if (line.isEmpty()) {
+ break;
+ }
+ Matcher outerMatcher = outerPattern.matcher(line);
+ if (!outerMatcher.matches()) {
+ fail("Line " + line + " does not match expected stactrace pattern");
+ } else {
+ String clz = outerMatcher.group(1);
+ String method = outerMatcher.group(2);
+ String inner = outerMatcher.group(3);
+ if (inner.equals("Native Method")) {
+ frames.add(new StackTraceElement(clz, method, null, -2));
+ } else {
+ Matcher innerMatcher = innerPattern.matcher(inner);
+ if (!innerMatcher.matches()) {
+ fail("Trace parameter list " + inner + " does not match expected pattern");
+ } else {
+ String file = innerMatcher.group(1);
+ int lineNum = Integer.parseInt(innerMatcher.group(2));
+ frames.add(new StackTraceElement(clz, method, file, lineNum));
+ }
+ }
+ }
+ }
+
+ throwable.setStackTrace(frames.toArray(new StackTraceElement[frames.size()]));
+
+ // Dump stack back to string to make sure we have the same exception
+ assertEquals(desc, AndroidCommonUtils.getStackTrace(throwable));
+
+ return throwable;
+ }
}
diff --git a/android/testSrc/com/android/tools/idea/rendering/ResourceFolderRepositoryTest.java b/android/testSrc/com/android/tools/idea/rendering/ResourceFolderRepositoryTest.java
new file mode 100644
index 0000000..0618902
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/rendering/ResourceFolderRepositoryTest.java
@@ -0,0 +1,1917 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.rendering;
+
+import com.android.ide.common.rendering.api.*;
+import com.android.ide.common.res2.ResourceItem;
+import com.android.resources.Density;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.psi.xml.XmlTag;
+import org.jetbrains.android.AndroidTestCase;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+
+import static com.android.SdkConstants.*;
+import static com.android.tools.idea.rendering.ResourceFolderRepository.ourFullRescans;
+
+/**
+ * TODO: Add XmlTags with Psi events to check childAdded etc working correctly! Currently they mostly seem to generate big rescans.
+ * TODO: Test moving from one resource folder to another; should be simulated as an add in one folder and a remove in another;
+ * check that in the ModuleResourceRepository test!
+ * TODO: Test that adding and removing characters inside a {@code <string>} element does not cause a full rescan
+ */
+@SuppressWarnings({"UnusedDeclaration", "SpellCheckingInspection"})
+public class ResourceFolderRepositoryTest extends AndroidTestCase {
+ private static final String LAYOUT1 = "resourceRepository/layout.xml";
+ private static final String LAYOUT2 = "resourceRepository/layout2.xml";
+ private static final String VALUES1 = "resourceRepository/values.xml";
+ private static final String VALUES_EMPTY = "resourceRepository/empty.xml";
+ private static final String STRINGS = "resourceRepository/strings.xml";
+
+ private static void resetScanCounter() {
+ ourFullRescans = 0;
+ }
+
+ private static void ensureIncremental() {
+ assertEquals(0, ourFullRescans);
+ }
+
+ private static void ensureSingleScan() {
+ assertEquals(1, ourFullRescans);
+ }
+
+ private ResourceFolderRepository createRepository() {
+ List<VirtualFile> resourceDirectories = myFacet.getAllResourceDirectories();
+ assertNotNull(resourceDirectories);
+ assertSize(1, resourceDirectories);
+ VirtualFile dir = resourceDirectories.get(0);
+ return ResourceFolderRegistry.get(myFacet, dir);
+ }
+
+ public void testComputeResourceStrings() throws Exception {
+ // Tests the handling of markup to raw strings
+ // For example, for this strings definition
+ // <string name="title_template_step">Step <xliff:g id="step_number">%1$d</xliff:g>: Lorem Ipsum</string>
+ // the resource value should be
+ // Step %1$d: Lorem Ipsum
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ List<ResourceItem> labelList = resources.getResourceItem(ResourceType.STRING, "title_template_step");
+ assertNotNull(labelList);
+ assertEquals(1, labelList.size());
+ ResourceItem label = labelList.get(0);
+ ResourceValue resourceValue = label.getResourceValue(false);
+ assertNotNull(resourceValue);
+ assertEquals("Step %1$d: Lorem Ipsum", resourceValue.getValue()); // In the file, there's whitespace unlike example above
+
+ // Test unicode escape handling: <string name="ellipsis">Here it is: \u2026!</string>
+ labelList = resources.getResourceItem(ResourceType.STRING, "ellipsis");
+ assertNotNull(labelList);
+ assertEquals(1, labelList.size());
+ label = labelList.get(0);
+ resourceValue = label.getResourceValue(false);
+ assertNotNull(resourceValue);
+ assertEquals("Here it is: \u2026!", resourceValue.getValue());
+
+ // Make sure we pick up id's defined using types
+ assertTrue(resources.hasResourceItem(ResourceType.ID, "action_next"));
+ assertFalse(resources.hasResourceItem(ResourceType.ID, "action_next2"));
+ }
+
+ public void testInitialCreate() throws Exception {
+ myFixture.copyFileToProject(LAYOUT1, "res/layout/layout1.xml");
+ myFixture.copyFileToProject(LAYOUT1, "res/layout/layout2.xml");
+
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+
+ Collection<String> layouts = resources.getItemsOfType(ResourceType.LAYOUT);
+ assertEquals(2, layouts.size());
+
+ assertNotNull(resources.getResourceItem(ResourceType.LAYOUT, "layout1"));
+ assertNotNull(resources.getResourceItem(ResourceType.LAYOUT, "layout2"));
+ }
+
+ public void testAddFile() throws Exception {
+ myFixture.copyFileToProject(LAYOUT1, "res/layout/layout1.xml");
+ myFixture.copyFileToProject(LAYOUT1, "res/layout/layout2.xml");
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ Collection<String> layouts = resources.getItemsOfType(ResourceType.LAYOUT);
+ assertEquals(2, layouts.size());
+ assertNotNull(resources.getResourceItem(ResourceType.LAYOUT, "layout1"));
+ assertNotNull(resources.getResourceItem(ResourceType.LAYOUT, "layout2"));
+
+ long generation = resources.getModificationCount();
+ myFixture.copyFileToProject(LAYOUT1, "res/layout/layout2.xml");
+ assertEquals(generation, resources.getModificationCount()); // no changes in file: no new generation
+
+ generation = resources.getModificationCount();
+ VirtualFile file3 = myFixture.copyFileToProject(LAYOUT1, "res/layout-xlarge-land/layout3.xml");
+ PsiFile psiFile3 = PsiManager.getInstance(getProject()).findFile(file3);
+ assertNotNull(psiFile3);
+ assertTrue(resources.getModificationCount() > generation);
+
+ layouts = resources.getItemsOfType(ResourceType.LAYOUT);
+ assertEquals(3, layouts.size());
+ }
+
+ public void testAddUnrelatedFile() throws Exception {
+ myFixture.copyFileToProject(LAYOUT1, "res/layout/layout1.xml");
+ myFixture.copyFileToProject(LAYOUT1, "res/layout/layout2.xml");
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ Collection<String> layouts = resources.getItemsOfType(ResourceType.LAYOUT);
+ assertEquals(2, layouts.size());
+ assertNotNull(resources.getResourceItem(ResourceType.LAYOUT, "layout1"));
+ assertNotNull(resources.getResourceItem(ResourceType.LAYOUT, "layout2"));
+
+ long generation = resources.getModificationCount();
+ myFixture.copyFileToProject(LAYOUT1, "res/layout/layout2.unrelated");
+ assertEquals(generation, resources.getModificationCount()); // no changes in file: no new generation
+ assertEquals(2, layouts.size());
+
+ myFixture.copyFileToProject(LAYOUT1, "src/layout/layout2.xml"); // not a resource folder
+ assertEquals(generation, resources.getModificationCount()); // no changes in file: no new generation
+ assertEquals(2, layouts.size());
+ }
+
+ public void testDeleteResourceFile() throws Exception {
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+
+ Collection<String> drawables = resources.getItemsOfType(ResourceType.DRAWABLE);
+ assertEquals(drawables.toString(), 0, drawables.size());
+ long generation = resources.getModificationCount();
+ VirtualFile file4 = myFixture.copyFileToProject(LAYOUT1, "res/drawable-mdpi/foo.png");
+ final PsiFile psiFile4 = PsiManager.getInstance(getProject()).findFile(file4);
+ assertNotNull(psiFile4);
+ assertTrue(resources.getModificationCount() > generation);
+
+ // Delete a file and make sure the item is removed from the repository (and modification count bumped)
+ drawables = resources.getItemsOfType(ResourceType.DRAWABLE);
+ assertEquals(1, drawables.size());
+ generation = resources.getModificationCount();
+ assertEquals("foo", drawables.iterator().next());
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ psiFile4.delete();
+ }
+ });
+ drawables = resources.getItemsOfType(ResourceType.DRAWABLE);
+ assertEquals(0, drawables.size());
+ assertTrue(resources.getModificationCount() > generation);
+ }
+
+ public void testDeleteResourceDirectory() throws Exception {
+ final VirtualFile file1 = myFixture.copyFileToProject(LAYOUT1, "res/layout/layout1.xml");
+ final VirtualFile file2 = myFixture.copyFileToProject(LAYOUT1, "res/layout/layout2.xml");
+ final VirtualFile file3 = myFixture.copyFileToProject(LAYOUT1, "res/layout-xlarge-land/layout3.xml");
+ PsiFile psiFile3 = PsiManager.getInstance(getProject()).findFile(file3);
+ assertNotNull(psiFile3);
+
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+
+ // Try deleting a whole resource directory and ensure we remove the files within
+ long generation = resources.getModificationCount();
+ Collection<String> layouts = resources.getItemsOfType(ResourceType.LAYOUT);
+ assertEquals(3, layouts.size());
+ final PsiDirectory directory = psiFile3.getContainingDirectory();
+ assertNotNull(directory);
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ directory.delete();
+ }
+ });
+ layouts = resources.getItemsOfType(ResourceType.LAYOUT);
+ assertEquals(2, layouts.size());
+ assertTrue(resources.getModificationCount() > generation);
+ }
+
+ public void testRenameLayoutFile() throws Exception {
+ final VirtualFile file2 = myFixture.copyFileToProject(LAYOUT1, "res/layout/layout2.xml");
+
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+
+ // Check renames
+ // rename layout file
+ long generation = resources.getModificationCount();
+ assertTrue(resources.hasResourceItem(ResourceType.LAYOUT, "layout2"));
+ assertFalse(resources.hasResourceItem(ResourceType.LAYOUT, "layout2b"));
+
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ file2.rename(this, "layout2b.xml");
+ }
+ catch (IOException e) {
+ fail(e.toString());
+ }
+ }
+ });
+
+ assertTrue(resources.hasResourceItem(ResourceType.LAYOUT, "layout2b"));
+ assertFalse(resources.hasResourceItem(ResourceType.LAYOUT, "layout2"));
+ assertTrue(resources.getModificationCount() > generation);
+ }
+
+ public void testRenameDrawableFile() throws Exception {
+ // rename drawable file
+ final VirtualFile file5 = myFixture.copyFileToProject(LAYOUT1, "res/drawable-xhdpi/foo2.png");
+ ResourceFolderRepository resources = createRepository();
+
+ assertTrue(resources.hasResourceItem(ResourceType.DRAWABLE, "foo2"));
+ assertFalse(resources.hasResourceItem(ResourceType.DRAWABLE, "foo3"));
+ ResourceItem item = getOnlyItem(resources, ResourceType.DRAWABLE, "foo2");
+ assertTrue(item.getResourceValue(false) instanceof DensityBasedResourceValue);
+ DensityBasedResourceValue rv = (DensityBasedResourceValue)item.getResourceValue(false);
+ assertNotNull(rv);
+ assertSame(Density.XHIGH, rv.getResourceDensity());
+
+ long generation = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ file5.rename(this, "foo3.png");
+ }
+ catch (IOException e) {
+ fail(e.toString());
+ }
+ }
+ });
+ assertTrue(resources.hasResourceItem(ResourceType.DRAWABLE, "foo3"));
+ assertFalse(resources.hasResourceItem(ResourceType.DRAWABLE, "foo2"));
+ assertTrue(resources.getModificationCount() > generation);
+ }
+
+ public void testRenameValueFile() throws Exception {
+ final VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "title_template_step"));
+
+ List<ResourceItem> items = resources.getResourceItem(ResourceType.STRING, "title_template_step");
+ assertNotNull(items);
+ assertEquals(1, items.size());
+ ResourceItem item = items.get(0);
+ assertEquals("myvalues.xml", item.getSource().getFile().getName());
+
+ // Renaming a value file should have no visible effect
+
+ long generation = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ file1.rename(this, "renamedvalues.xml");
+ }
+ catch (IOException e) {
+ fail(e.toString());
+ }
+ }
+ });
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "title_template_step"));
+ items = resources.getResourceItem(ResourceType.STRING, "title_template_step");
+ assertNotNull(items);
+ assertEquals(1, items.size());
+ item = items.get(0);
+ assertEquals("renamedvalues.xml", item.getSource().getFile().getName());
+
+ // TODO: Optimize this such that there's no modification change for this. It's tricky because
+ // for file names we get separate notification from the old file deletion (beforePropertyChanged)
+ // and the new file name (propertyChanged). (Note that I tried performing the rename via a
+ // setName operation on the PsiFile instead of at the raw VirtualFile level, but the resulting
+ // events were the same.)
+ //assertEquals(generation, resources.getModificationCount());
+ }
+
+ public void testRenameValueFileToInvalid() throws Exception {
+ final VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "title_template_step"));
+
+ long generation = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // After this rename, the values are no longer considered values since they're in an unrecognized file
+ file1.rename(this, "renamedvalues.badextension");
+ }
+ catch (IOException e) {
+ fail(e.toString());
+ }
+ }
+ });
+ assertTrue(generation < resources.getModificationCount());
+ assertFalse(resources.hasResourceItem(ResourceType.STRING, "title_template_step"));
+ }
+
+ private static ResourceItem getOnlyItem(ResourceFolderRepository repository, ResourceType type, String name) {
+ List<ResourceItem> item = repository.getResourceItem(type, name);
+ assertNotNull(item);
+ assertEquals(1, item.size());
+ return item.get(0);
+ }
+
+ public void testMoveFileResourceFileToNewConfiguration() throws Exception {
+ // Move a file-based resource file from one configuration to another; verify that
+ // items are preserved, generation changed (since it can affect config matching),
+ // and resource files updated.
+ final VirtualFile file1 = myFixture.copyFileToProject(LAYOUT1, "res/layout-land/layout1.xml");
+ final VirtualFile file2 = myFixture.copyFileToProject(LAYOUT1, "res/layout-port/dummy.ignore");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ ResourceItem item = getOnlyItem(resources, ResourceType.LAYOUT, "layout1");
+ assertEquals("land", item.getSource().getQualifiers());
+ ResourceItem idItem = getOnlyItem(resources, ResourceType.ID, "btn_title_refresh");
+ assertEquals("layout-land", idItem.getSource().getFile().getParentFile().getName());
+
+ long generation = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // Move file from one location to another
+ file1.move(this, file2.getParent());
+ }
+ catch (IOException e) {
+ fail(e.toString());
+ }
+ }
+ });
+ assertTrue(generation < resources.getModificationCount());
+ assertTrue(resources.hasResourceItem(ResourceType.LAYOUT, "layout1"));
+ item = getOnlyItem(resources, ResourceType.LAYOUT, "layout1");
+ assertEquals("port", item.getSource().getQualifiers());
+ idItem = getOnlyItem(resources, ResourceType.ID, "btn_title_refresh");
+ assertEquals("layout-port", idItem.getSource().getFile().getParentFile().getName());
+ }
+
+ public void testMoveValueResourceFileToNewConfiguration() throws Exception {
+ // Move a value file from one configuration to another; verify that
+ // items are preserved, generation changed (since it can affect config matching),
+ // and resource files updated.
+ final VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values-en/layout1.xml");
+ final VirtualFile file2 = myFixture.copyFileToProject(VALUES1, "res/values-no/dummy.ignore");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ ResourceItem item = getOnlyItem(resources, ResourceType.STRING, "app_name");
+ assertEquals("en", item.getSource().getQualifiers());
+ assertEquals("en", item.getConfiguration().getLanguageQualifier().getValue());
+ //noinspection ConstantConditions
+ assertEquals("Animations Demo", item.getResourceValue(false).getValue());
+
+ long generation = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // Move file from one location to another
+ file1.move(this, file2.getParent());
+ }
+ catch (IOException e) {
+ fail(e.toString());
+ }
+ }
+ });
+ assertTrue(generation < resources.getModificationCount());
+ item = getOnlyItem(resources, ResourceType.STRING, "app_name");
+ assertEquals("no", item.getSource().getQualifiers());
+ assertEquals("no", item.getConfiguration().getLanguageQualifier().getValue());
+ //noinspection ConstantConditions
+ assertEquals("Animations Demo", item.getResourceValue(false).getValue());
+ }
+
+ public void testMoveFileResourceFileToNewType() throws Exception {
+ // Move a file resource file file from one folder to another, changing the type
+ // (e.g. anim to animator), verify that resource types are updated
+ final VirtualFile file1 = myFixture.copyFileToProject(LAYOUT1, "res/layout/layout1.xml");
+ final VirtualFile file2 = myFixture.copyFileToProject(LAYOUT1, "res/menu/dummy.ignore");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ assertTrue(resources.hasResourceItem(ResourceType.LAYOUT, "layout1"));
+
+ long generation = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ file1.move(this, file2.getParent());
+ }
+ catch (IOException e) {
+ fail(e.toString());
+ }
+ }
+ });
+ assertTrue(generation < resources.getModificationCount());
+ assertTrue(resources.hasResourceItem(ResourceType.MENU, "layout1"));
+ assertFalse(resources.hasResourceItem(ResourceType.LAYOUT, "layout1"));
+ ResourceItem item = getOnlyItem(resources, ResourceType.MENU, "layout1");
+ assertSame(ResourceFolderType.MENU, ((PsiResourceFile)item.getSource()).getFolderType());
+ }
+
+ public void testMoveOutOfResourceFolder() throws Exception {
+ // Move value files out of its resource folder; items should disappear
+ final VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ final VirtualFile javaFile = myFixture.copyFileToProject(VALUES1, "src/my/pkg/Dummy.java");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "title_template_step"));
+
+ long generation = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ file1.move(this, javaFile.getParent());
+ }
+ catch (IOException e) {
+ fail(e.toString());
+ }
+ }
+ });
+ assertTrue(generation < resources.getModificationCount());
+ assertFalse(resources.hasResourceItem(ResourceType.STRING, "title_template_step"));
+ }
+
+ public void testMoveIntoResourceFolder() throws Exception {
+ // Move value files out of its resource folder; items should disappear
+ final VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/dummy.ignore");
+ final VirtualFile xmlFile = myFixture.copyFileToProject(VALUES1, "src/my/pkg/values.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ final ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ assertFalse(resources.hasResourceItem(ResourceType.STRING, "title_template_step"));
+
+ final long generation = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ xmlFile.move(this, file1.getParent());
+ }
+ catch (IOException e) {
+ fail(e.toString());
+ }
+ }
+ });
+
+ assertTrue(generation < resources.getModificationCount());
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "title_template_step"));
+ }
+
+ public void testReplaceResourceFile() throws Exception {
+ final VirtualFile file1 = myFixture.copyFileToProject(LAYOUT1, "res/layout/layout1.xml");
+ final PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ assertTrue(resources.hasResourceItem(ResourceType.LAYOUT, "layout1"));
+ assertTrue(resources.hasResourceItem(ResourceType.ID, "btn_title_refresh"));
+ assertFalse(resources.hasResourceItem(ResourceType.ID, "btn_title_refresh2"));
+
+ long generation = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ myFixture.copyFileToProject(LAYOUT2, "res/layout/layout1.xml");
+ }
+ catch (Exception e) {
+ fail(e.toString());
+ }
+ }
+ });
+
+ // TODO: Find out how I can work around this!
+ // This doesn't work because copyFileToProject does not trigger PSI file notifications!
+ //assertTrue(resources.hasResourceItem(ResourceType.ID, "btn_title_refresh2"));
+ //assertFalse(resources.hasResourceItem(ResourceType.ID, "btn_title_refresh"));
+ //assertTrue(generation < resources.getModificationCount());
+ }
+
+ public void testAddEmptyValueFile() throws Exception {
+ myFixture.copyFileToProject(LAYOUT1, "res/layout/layout1.xml");
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ long generation = resources.getModificationCount();
+ final VirtualFile file2 = myFixture.copyFileToProject(VALUES_EMPTY, "res/values/empty.xml");
+ assertEquals(generation, resources.getModificationCount());
+ }
+
+ public void testRawFolder() throws Exception {
+ // In this folder, any file extension is allowed
+ myFixture.copyFileToProject(LAYOUT1, "res/raw/raw1.xml");
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ Collection<String> raw = resources.getItemsOfType(ResourceType.RAW);
+ assertEquals(1, raw.size());
+ long generation = resources.getModificationCount();
+ final VirtualFile file2 = myFixture.copyFileToProject(LAYOUT1, "res/raw/numbers.random");
+ assertTrue(generation < resources.getModificationCount());
+ assertTrue(resources.hasResourceItem(ResourceType.RAW, "numbers"));
+ raw = resources.getItemsOfType(ResourceType.RAW);
+ assertEquals(2, raw.size());
+
+ generation = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ file2.rename(this, "numbers2.whatever");
+ }
+ catch (IOException e) {
+ fail(e.toString());
+ }
+ }
+ });
+ assertTrue(resources.getModificationCount() > generation);
+ assertTrue(resources.hasResourceItem(ResourceType.RAW, "numbers2"));
+ assertFalse(resources.hasResourceItem(ResourceType.RAW, "numbers"));
+ }
+
+ public void testEditLayoutNoOp() throws Exception {
+ resetScanCounter();
+
+ // Make some miscellaneous edits in the file that have no bearing on the
+ // project resources and therefore end up doing no work
+ VirtualFile file1 = myFixture.copyFileToProject(LAYOUT1, "res/layout/layout1.xml");
+ final PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ assert(psiFile1 instanceof XmlFile);
+ final XmlFile xmlFile = (XmlFile)psiFile1;
+ final ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ Collection<String> layouts = resources.getItemsOfType(ResourceType.LAYOUT);
+ assertEquals(1, layouts.size());
+ assertNotNull(resources.getResourceItem(ResourceType.LAYOUT, "layout1"));
+
+ final long initial = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ // Insert a comment at the beginning
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ XmlTag rootTag = xmlFile.getRootTag();
+ assertNotNull(rootTag);
+ int rootTagOffset = rootTag.getTextOffset();
+ document.insertString(rootTagOffset, "<!-- This is a\nmultiline comment -->");
+ documentManager.commitDocument(document);
+ // Edit the comment some more
+ document.deleteString(rootTagOffset + 8, rootTagOffset + 8 + 5);
+ documentManager.commitDocument(document);
+ document.insertString(rootTagOffset + 8, "Replacement");
+ documentManager.commitDocument(document);
+ }
+ });
+ // Inserting the comment and editing it shouldn't have had any observable results on the resource repository
+ assertEquals(initial, resources.getModificationCount());
+
+ assertTrue(resources.hasResourceItem(ResourceType.ID, "noteArea"));
+ final XmlTag tag = findTagById(psiFile1, "noteArea");
+ assertNotNull(tag);
+
+ // Now insert some whitespace before a tag
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ int indentAreaBeforeTag = tag.getTextOffset() - 1;
+ document.insertString(indentAreaBeforeTag, " ");
+ documentManager.commitDocument(document);
+ document.deleteString(indentAreaBeforeTag, indentAreaBeforeTag + 2);
+ documentManager.commitDocument(document);
+ }
+ });
+ // Space edits outside the tag shouldn't be observable
+ assertEquals(initial, resources.getModificationCount());
+
+ // Edit text inside an element tag. No effect in value files!
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ final XmlTag header = findTagById(xmlFile, "header");
+ assertNotNull(header);
+ int indentAreaBeforeTag = header.getSubTags()[0].getTextOffset();
+ document.insertString(indentAreaBeforeTag, " ");
+ documentManager.commitDocument(document);
+ }
+ });
+ // Space edits inside the tag shouldn't be observable
+ assertEquals(initial, resources.getModificationCount());
+
+ // Insert tag (without id) in layout file: ignored (only ids and file item matters)
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ final XmlTag header = findTagById(xmlFile, "text2");
+ assertNotNull(header);
+ int indentAreaBeforeTag = header.getTextOffset() - 1;
+ document.insertString(indentAreaBeforeTag, "<Button />");
+ documentManager.commitDocument(document);
+ }
+ });
+ // Non-id new tags shouldn't be observable
+ assertEquals(initial, resources.getModificationCount());
+
+ // Shouldn't have done any full file rescans during the above edits
+ ensureIncremental();
+
+ // Finally make an edit which *does* affect the project resources to ensure
+ // that document edits actually *do* fire PSI events that are digested by
+ // this repository
+ final String elementDeclaration = "<Button android:id=\"@+id/newid\" />\n";
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ final XmlTag tag = findTagById(psiFile1, "noteArea");
+ assertNotNull(tag);
+ document.insertString(tag.getTextOffset() - 1, elementDeclaration);
+ documentManager.commitDocument(document);
+ }
+ });
+ assertTrue(resources.isScanPending(psiFile1));
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ ensureSingleScan();
+ assertTrue(resources.hasResourceItem(ResourceType.ID, "newid"));
+ assertTrue(resources.getModificationCount() > initial);
+
+ final long generation = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ int startOffset = document.getText().indexOf(elementDeclaration);
+ document.deleteString(startOffset, startOffset + elementDeclaration.length());
+ documentManager.commitDocument(document);
+ }
+ });
+
+ assertTrue(resources.isScanPending(psiFile1));
+ resetScanCounter();
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ ensureSingleScan();
+ assertFalse(resources.hasResourceItem(ResourceType.ID, "newid"));
+ assertTrue(resources.getModificationCount() > generation);
+ }
+ });
+ }
+ });
+ }
+
+ public void testEditValueFileNoOp() throws Exception {
+ resetScanCounter();
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ Collection<String> strings = resources.getItemsOfType(ResourceType.STRING);
+ assertEquals(8, strings.size());
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "app_name"));
+ assertTrue(resources.hasResourceItem(ResourceType.INTEGER, "card_flip_time_full"));
+
+ long generation = resources.getModificationCount();
+
+ long initial = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ // Edit comment header; should be a no-op
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ int offset = document.getText().indexOf("Licensed under the");
+ document.insertString(offset, "This code is ");
+ documentManager.commitDocument(document);
+ }
+ });
+ assertEquals(initial, resources.getModificationCount());
+
+ // Test edit text NOT under an item: no-op
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ int offset = document.getText().indexOf(" <item type=\"id\""); // insert BEFORE this
+ document.insertString(offset, "Ignored text");
+ documentManager.commitDocument(document);
+ }
+ });
+ assertEquals(initial, resources.getModificationCount());
+
+ // Shouldn't have done any full file rescans during the above edits
+ ensureIncremental();
+ }
+
+ public void testInsertNewElementWithId() throws Exception {
+ resetScanCounter();
+
+ // Make some miscellaneous edits in the file that have no bearing on the
+ // project resources and therefore end up doing no work
+ VirtualFile file1 = myFixture.copyFileToProject(LAYOUT1, "res/layout/layout1.xml");
+ final PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ assert(psiFile1 instanceof XmlFile);
+ final XmlFile xmlFile = (XmlFile)psiFile1;
+ final ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ Collection<String> layouts = resources.getItemsOfType(ResourceType.LAYOUT);
+ assertEquals(1, layouts.size());
+ assertNotNull(resources.getResourceItem(ResourceType.LAYOUT, "layout1"));
+
+ final long initial = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ // Insert tag (with an id) in layout file: should incrementally update set of ids
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ final XmlTag header = findTagById(xmlFile, "text2");
+ assertNotNull(header);
+ int indentAreaBeforeTag = header.getTextOffset() - 1;
+ document.insertString(indentAreaBeforeTag,
+ "<LinearLayout android:id=\"@+id/newid1\"><Child android:id=\"@+id/newid2\"/></LinearLayout>");
+ documentManager.commitDocument(document);
+ }
+ });
+ // Currently, the PSI events delivered for the above edits results in PSI events without enough
+ // info for incremental analysis
+ assertTrue(resources.isScanPending(psiFile1));
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ ensureSingleScan();
+ assertTrue(initial < resources.getModificationCount());
+ assertTrue(resources.hasResourceItem(ResourceType.ID, "newid1"));
+ assertTrue(resources.hasResourceItem(ResourceType.ID, "newid2"));
+ }
+ });
+ }
+
+ public void testEditIdAttributeValue() throws Exception {
+ resetScanCounter();
+ // Edit the id attribute value of a layout item to change the set of available ids
+ VirtualFile file1 = myFixture.copyFileToProject(LAYOUT1, "res/layout/layout1.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ Collection<String> layouts = resources.getItemsOfType(ResourceType.LAYOUT);
+ assertEquals(1, layouts.size());
+ assertNotNull(resources.getResourceItem(ResourceType.LAYOUT, "layout1"));
+
+ assertTrue(resources.hasResourceItem(ResourceType.ID, "noteArea"));
+ assertFalse(resources.hasResourceItem(ResourceType.ID, "note2Area"));
+
+ long generation = resources.getModificationCount();
+ final XmlTag tag = findTagById(psiFile1, "noteArea");
+ assertNotNull(tag);
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ tag.setAttribute(ATTR_ID, ANDROID_URI, "@+id/note2Area");
+ }
+ });
+ assertTrue(resources.hasResourceItem(ResourceType.ID, "note2Area"));
+ assertFalse(resources.hasResourceItem(ResourceType.ID, "noteArea"));
+ assertTrue(resources.getModificationCount() > generation);
+
+ // Shouldn't have done any full file rescans during the above edits
+ ensureIncremental();
+ }
+
+ public void testEditIdAttributeValue2() throws Exception {
+ // Edit the id attribute value: rather than by making a full value replacement,
+ // perform a tiny edit on the character content; this takes a different code
+ // path in the incremental updater
+
+ resetScanCounter();
+ // Edit the id attribute value of a layout item to change the set of available ids
+ VirtualFile file1 = myFixture.copyFileToProject(LAYOUT1, "res/layout/layout1.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ Collection<String> layouts = resources.getItemsOfType(ResourceType.LAYOUT);
+ assertEquals(1, layouts.size());
+ assertNotNull(resources.getResourceItem(ResourceType.LAYOUT, "layout1"));
+
+ assertTrue(resources.hasResourceItem(ResourceType.ID, "noteArea"));
+ assertFalse(resources.hasResourceItem(ResourceType.ID, "note2Area"));
+
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ // Edit value should cause update
+ long generation = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ final int offset = document.getText().indexOf("noteArea");
+ document.insertString(offset + 4, "2");
+ documentManager.commitDocument(document);
+ }
+ });
+ assertTrue(resources.hasResourceItem(ResourceType.ID, "note2Area"));
+ assertFalse(resources.hasResourceItem(ResourceType.ID, "noteArea"));
+ assertTrue(resources.getModificationCount() > generation);
+
+ // Shouldn't have done any full file rescans during the above edits
+ ensureIncremental();
+ }
+
+ public void testEditValueText() throws Exception {
+ resetScanCounter();
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ Collection<String> strings = resources.getItemsOfType(ResourceType.STRING);
+ assertEquals(8, strings.size());
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "app_name"));
+ assertTrue(resources.hasResourceItem(ResourceType.INTEGER, "card_flip_time_full"));
+
+ long generation = resources.getModificationCount();
+
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ // Edit value should cause update
+ final int screenSlideOffset = document.getText().indexOf("Screen Slide");
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ document.replaceString(screenSlideOffset + 3, screenSlideOffset + 3, "e");
+ documentManager.commitDocument(document);
+ }
+ });
+ // NO revision bump yet, because the resource value hasn't been observed!
+ assertEquals(generation, resources.getModificationCount());
+
+ // Now observe it, do another edit, and see what happens
+ List<ResourceItem> labelList = resources.getResourceItem(ResourceType.STRING, "title_screen_slide");
+ assertNotNull(labelList);
+ assertEquals(1, labelList.size());
+ ResourceItem slideLabel = labelList.get(0);
+ ResourceValue resourceValue = slideLabel.getResourceValue(false);
+ assertNotNull(resourceValue);
+ assertEquals("Screeen Slide", resourceValue.getValue());
+
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ document.deleteString(screenSlideOffset + 3, screenSlideOffset + 6);
+ documentManager.commitDocument(document);
+ }
+ });
+ assertTrue(generation < resources.getModificationCount());
+ resourceValue = slideLabel.getResourceValue(false);
+ assertNotNull(resourceValue);
+ assertEquals("Scrn Slide", resourceValue.getValue());
+
+ // Shouldn't have done any full file rescans during the above edits
+ ensureIncremental();
+ }
+
+ public void testNestedEditValueText() throws Exception {
+ resetScanCounter();
+
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+
+ List<ResourceItem> labelList = resources.getResourceItem(ResourceType.STRING, "title_template_step");
+ assertNotNull(labelList);
+ assertEquals(1, labelList.size());
+ ResourceItem label = labelList.get(0);
+ ResourceValue resourceValue = label.getResourceValue(false);
+ assertNotNull(resourceValue);
+ assertEquals("Step %1$d: Lorem Ipsum", resourceValue.getValue());
+
+ long generation = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ // Edit value should cause update
+ final int textOffset = document.getText().indexOf("Lorem");
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ document.insertString(textOffset + 1, "l");
+ documentManager.commitDocument(document);
+ }
+ });
+ assertTrue(generation < resources.getModificationCount());
+ resourceValue = label.getResourceValue(false);
+ assertNotNull(resourceValue);
+ assertEquals("Step %1$d: Llorem Ipsum", resourceValue.getValue());
+
+ // Shouldn't have done any full file rescans during the above edits
+ ensureIncremental();
+ }
+
+ public void testEditValueName() throws Exception {
+ resetScanCounter();
+
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ Collection<String> strings = resources.getItemsOfType(ResourceType.STRING);
+ assertEquals(8, strings.size());
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "app_name"));
+
+ long generation = resources.getModificationCount();
+
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ final int offset = document.getText().indexOf("app_name");
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ document.replaceString(offset, offset + 3, "rap");
+ documentManager.commitDocument(document);
+ }
+ });
+ assertTrue(generation < resources.getModificationCount());
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "rap_name"));
+ assertFalse(resources.hasResourceItem(ResourceType.STRING, "app_name"));
+
+ // Shouldn't have done any full file rescans during the above edits
+ ensureIncremental();
+ }
+
+ public void testAddValue() throws Exception {
+ resetScanCounter();
+
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+
+ final ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ Collection<String> strings = resources.getItemsOfType(ResourceType.STRING);
+ assertEquals(8, strings.size());
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "app_name"));
+
+ // Incrementally add in a new item
+ final long generation = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ final int offset = document.getText().indexOf(" <item type");
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ String firstHalf = "<string name=\"new_s";
+ String secondHalf = "tring\">New String</string>";
+ document.insertString(offset, firstHalf);
+ documentManager.commitDocument(document);
+ document.insertString(offset + firstHalf.length(), secondHalf);
+ documentManager.commitDocument(document);
+ }
+ });
+
+ // This currently doesn't work incrementally because we get psi events that do not contain
+ // enough info to be handled incrementally, so instead we do an asynchronous update (such that
+ // we can do a single update rather than rescanning the file 20 times)
+ assertTrue(resources.isScanPending(psiFile1));
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ ensureSingleScan();
+ assertTrue(generation < resources.getModificationCount());
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "new_string"));
+ //noinspection ConstantConditions
+ assertEquals("New String", resources.getResourceItem(ResourceType.STRING, "new_string").get(0).getResourceValue(false).getValue());
+ }
+ });
+ }
+
+ public void testRemoveValue() throws Exception {
+ resetScanCounter();
+
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ Collection<String> strings = resources.getItemsOfType(ResourceType.STRING);
+ assertEquals(8, strings.size());
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "app_name"));
+
+ long generation = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ final String textToRemove = "<string name=\"app_name\">Animations Demo</string>";
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ int offset = document.getText().indexOf(textToRemove);
+ document.deleteString(offset, offset + textToRemove.length());
+ documentManager.commitDocument(document);
+ }
+ });
+ assertTrue(generation < resources.getModificationCount());
+ assertFalse(resources.hasResourceItem(ResourceType.STRING, "app_name"));
+
+ // Shouldn't have done any full file rescans during the above edits
+ ensureIncremental();
+ }
+
+ public void testChangeType() throws Exception {
+ resetScanCounter();
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ final ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ assertTrue(resources.hasResourceItem(ResourceType.ID, "action_next"));
+ assertFalse(resources.hasResourceItem(ResourceType.DIMEN, "action_next"));
+
+ final long generation = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ final int offset = document.getText().indexOf("\"id\" name=\"action_next\" />") + 1;
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ document.replaceString(offset, offset + 2, "dimen");
+ documentManager.commitDocument(document);
+ }
+ });
+ assertTrue(resources.isScanPending(psiFile1));
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ ensureSingleScan();
+ assertTrue(generation < resources.getModificationCount());
+ assertFalse(resources.hasResourceItem(ResourceType.ID, "action_next"));
+ assertTrue(resources.hasResourceItem(ResourceType.DIMEN, "action_next"));
+ }
+ });
+ }
+
+ public void testBreakNameAttribute() throws Exception {
+ resetScanCounter();
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ final ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "app_name"));
+
+ final long generation = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ final int offset = document.getText().indexOf("name=\"app_name\">");
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ document.replaceString(offset + 2, offset + 3, "o"); // name => nome
+ documentManager.commitDocument(document);
+ }
+ });
+
+ assertTrue(resources.isScanPending(psiFile1));
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ ensureSingleScan();
+ assertTrue(generation < resources.getModificationCount());
+ assertFalse(resources.hasResourceItem(ResourceType.STRING, "app_name"));
+ }
+ });
+ }
+
+ public void testChangeValueTypeByTagNameEdit() throws Exception {
+ resetScanCounter();
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ final ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ assertTrue(resources.hasResourceItem(ResourceType.INTEGER, "card_flip_time_half"));
+
+ final long generation = resources.getModificationCount();
+ final XmlTag tag = findTagByName(psiFile1, "card_flip_time_half");
+ assertNotNull(tag);
+
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ tag.setName("dimen"); // Change <integer> to <dimen>
+ }
+ });
+ assertTrue(resources.isScanPending(psiFile1));
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ ensureSingleScan();
+
+ assertTrue(generation < resources.getModificationCount());
+ assertTrue(resources.hasResourceItem(ResourceType.DIMEN, "card_flip_time_half"));
+ assertFalse(resources.hasResourceItem(ResourceType.INTEGER, "card_flip_time_half"));
+ }
+ });
+ }
+
+ public void testEditStyleName() throws Exception {
+ resetScanCounter();
+
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ final ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+
+ assertTrue(resources.hasResourceItem(ResourceType.STYLE, "DarkTheme"));
+
+ // Change style name
+ long generation = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ final int offset = document.getText().indexOf("DarkTheme");
+ document.replaceString(offset, offset + 4, "Light");
+ documentManager.commitDocument(document);
+ }
+ });
+ assertTrue(generation < resources.getModificationCount());
+ assertFalse(resources.hasResourceItem(ResourceType.STYLE, "DarkTheme"));
+ assertTrue(resources.hasResourceItem(ResourceType.STYLE, "LightTheme"));
+
+ // Shouldn't have done any full file rescans during the above edits
+ ensureIncremental();
+
+ // Change style parent
+ generation = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ final int offset = document.getText().indexOf("android:Theme.Holo");
+ document.replaceString(offset, offset + "android:Theme.Holo".length(), "android:Theme.Light");
+ documentManager.commitDocument(document);
+ }
+ });
+
+ assertTrue(resources.isScanPending(psiFile1));
+ final long finalGeneration = generation;
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ ensureSingleScan();
+ assertTrue(finalGeneration < resources.getModificationCount());
+ ResourceItem style = getOnlyItem(resources, ResourceType.STYLE, "LightTheme");
+ ResourceValue resourceValue = style.getResourceValue(false);
+ assertNotNull(resourceValue);
+ assertTrue(resourceValue instanceof StyleResourceValue);
+ StyleResourceValue srv = (StyleResourceValue)resourceValue;
+ assertEquals("android:Theme.Light", srv.getParentStyle());
+ ResourceValue actionBarStyle = srv.findValue("actionBarStyle", true);
+ assertNotNull(actionBarStyle);
+ assertEquals("@style/DarkActionBar", actionBarStyle.getValue());
+
+ // (We don't expect editing the style parent to be incremental)
+ }
+ });
+ }
+
+ public void testEditStyleItemText() throws Exception {
+ resetScanCounter();
+
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+
+ assertTrue(resources.hasResourceItem(ResourceType.STYLE, "DarkTheme"));
+ ResourceItem style = getOnlyItem(resources, ResourceType.STYLE, "DarkTheme");
+ StyleResourceValue srv = (StyleResourceValue)style.getResourceValue(false);
+ assertNotNull(srv);
+ ResourceValue actionBarStyle = srv.findValue("actionBarStyle", true);
+ assertNotNull(actionBarStyle);
+ assertEquals("@style/DarkActionBar", actionBarStyle.getValue());
+
+ long generation = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ final int offset = document.getText().indexOf("@style/DarkActionBar");
+ document.replaceString(offset + 7, offset + 11, "Light");
+ documentManager.commitDocument(document);
+ }
+ });
+ assertTrue(generation < resources.getModificationCount());
+ assertTrue(resources.hasResourceItem(ResourceType.STYLE, "DarkTheme"));
+
+ style = getOnlyItem(resources, ResourceType.STYLE, "DarkTheme");
+ srv = (StyleResourceValue)style.getResourceValue(false);
+ assertNotNull(srv);
+ actionBarStyle = srv.findValue("actionBarStyle", true);
+ assertNotNull(actionBarStyle);
+ assertEquals("@style/LightActionBar", actionBarStyle.getValue());
+
+ // Shouldn't have done any full file rescans during the above edits
+ ensureIncremental();
+ }
+
+ public void testEditStyleItemName() throws Exception {
+ resetScanCounter();
+
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+
+ assertTrue(resources.hasResourceItem(ResourceType.STYLE, "DarkTheme"));
+ ResourceItem style = getOnlyItem(resources, ResourceType.STYLE, "DarkTheme");
+ StyleResourceValue srv = (StyleResourceValue)style.getResourceValue(false);
+ assertNotNull(srv);
+ ResourceValue actionBarStyle = srv.findValue("actionBarStyle", true);
+ assertNotNull(actionBarStyle);
+ assertEquals("@style/DarkActionBar", actionBarStyle.getValue());
+
+ long generation = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ final int offset = document.getText().indexOf("android:actionBarStyle");
+ document.insertString(offset + 8, "in");
+ documentManager.commitDocument(document);
+ }
+ });
+ assertTrue(generation < resources.getModificationCount());
+ assertTrue(resources.hasResourceItem(ResourceType.STYLE, "DarkTheme"));
+
+ style = getOnlyItem(resources, ResourceType.STYLE, "DarkTheme");
+ srv = (StyleResourceValue)style.getResourceValue(false);
+ assertNotNull(srv);
+ actionBarStyle = srv.findValue("inactionBarStyle", true);
+ assertNotNull(actionBarStyle);
+ assertEquals("@style/DarkActionBar", actionBarStyle.getValue());
+
+ // Shouldn't have done any full file rescans during the above edits
+ ensureIncremental();
+ }
+
+ @SuppressWarnings("deprecation")
+ public void testEditDeclareStyleableAttr() throws Exception {
+ // Check edits of the name in a <declare-styleable> element.
+ resetScanCounter();
+
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+
+ assertTrue(resources.hasResourceItem(ResourceType.DECLARE_STYLEABLE, "MyCustomView"));
+ assertTrue(resources.hasResourceItem(ResourceType.ATTR, "watchType"));
+ ResourceItem style = getOnlyItem(resources, ResourceType.DECLARE_STYLEABLE, "MyCustomView");
+ DeclareStyleableResourceValue srv = (DeclareStyleableResourceValue)style.getResourceValue(false);
+ assertNotNull(srv);
+ assertEquals(5, srv.getAllAttributes().size());
+ AttrResourceValue watchType = srv.getAllAttributes().get("watchType");
+ assertNotNull(watchType);
+ assertEquals(2, watchType.getAttributeValues().size());
+ assertEquals(Integer.valueOf(1), watchType.getAttributeValues().get("type_stopwatch"));
+ assertEquals(Integer.valueOf(0), watchType.getAttributeValues().get("type_countdown"));
+ AttrResourceValue crash = srv.getAllAttributes().get("crash");
+ assertNotNull(crash);
+ assertNull(crash.getAttributeValues());
+
+ AttrResourceValue minWidth = srv.getAllAttributes().get("minWidth");
+ assertNotNull(minWidth);
+ assertFalse(resources.hasResourceItem(ResourceType.ATTR, "minWidth"));
+
+ long generation = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ final int offset = document.getText().indexOf("MyCustomView");
+ document.insertString(offset + 8, "er");
+ documentManager.commitDocument(document);
+ }
+ });
+ assertTrue(generation < resources.getModificationCount());
+ assertTrue(resources.hasResourceItem(ResourceType.DECLARE_STYLEABLE, "MyCustomerView"));
+ assertTrue(resources.hasResourceItem(ResourceType.ATTR, "watchType"));
+ assertFalse(resources.hasResourceItem(ResourceType.DECLARE_STYLEABLE, "MyCustomView"));
+ style = getOnlyItem(resources, ResourceType.DECLARE_STYLEABLE, "MyCustomerView");
+ srv = (DeclareStyleableResourceValue)style.getResourceValue(false);
+ assertNotNull(srv);
+ assertEquals(5, srv.getAllAttributes().size());
+ watchType = srv.getAllAttributes().get("watchType");
+ assertNotNull(watchType);
+ assertEquals(2, watchType.getAttributeValues().size());
+ assertEquals(Integer.valueOf(1), watchType.getAttributeValues().get("type_stopwatch"));
+
+ // Shouldn't have done any full file rescans during the above edits
+ ensureIncremental();
+ }
+
+ @SuppressWarnings("deprecation")
+ public void testEditAttr() throws Exception {
+ // Insert, remove and change <attr> attributes inside a <declare-styleable> and ensure that
+ resetScanCounter();
+
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ final ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+
+ assertTrue(resources.hasResourceItem(ResourceType.DECLARE_STYLEABLE, "MyCustomView"));
+ // Fetch resource value to ensure it gets replaced after update
+ assertTrue(resources.hasResourceItem(ResourceType.ATTR, "watchType"));
+ ResourceItem style = getOnlyItem(resources, ResourceType.DECLARE_STYLEABLE, "MyCustomView");
+ DeclareStyleableResourceValue srv = (DeclareStyleableResourceValue)style.getResourceValue(false);
+ assertNotNull(srv);
+ assertEquals(5, srv.getAllAttributes().size());
+ AttrResourceValue watchType = srv.getAllAttributes().get("watchType");
+ assertNotNull(watchType);
+ assertEquals(2, watchType.getAttributeValues().size());
+ assertEquals(Integer.valueOf(1), watchType.getAttributeValues().get("type_stopwatch"));
+ assertEquals(Integer.valueOf(0), watchType.getAttributeValues().get("type_countdown"));
+ AttrResourceValue crash = srv.getAllAttributes().get("crash");
+ assertNotNull(crash);
+ assertNull(crash.getAttributeValues());
+
+ final long generation = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ final int offset = document.getText().indexOf("watchType");
+ document.insertString(offset, "w");
+ documentManager.commitDocument(document);
+ }
+ });
+ assertFalse(resources.isScanPending(psiFile1));
+ assertTrue(generation < resources.getModificationCount());
+ assertTrue(resources.hasResourceItem(ResourceType.DECLARE_STYLEABLE, "MyCustomView"));
+ assertFalse(resources.hasResourceItem(ResourceType.ATTR, "watchType"));
+ assertTrue(resources.hasResourceItem(ResourceType.ATTR, "wwatchType"));
+ style = getOnlyItem(resources, ResourceType.DECLARE_STYLEABLE, "MyCustomView");
+ srv = (DeclareStyleableResourceValue)style.getResourceValue(false);
+ assertNotNull(srv);
+ assertEquals(5, srv.getAllAttributes().size());
+ watchType = srv.getAllAttributes().get("wwatchType");
+ assertNotNull(watchType);
+ assertEquals(2, watchType.getAttributeValues().size());
+ assertEquals(Integer.valueOf(1), watchType.getAttributeValues().get("type_stopwatch"));
+
+ // Shouldn't have done any full file rescans during the above edits
+ ensureIncremental();
+
+ // Now insert a new item and delete one and make sure we're still okay
+ resetScanCounter();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ String crashAttr = "<attr name=\"crash\" format=\"boolean\" />";
+ final int offset = document.getText().indexOf(crashAttr);
+ document.deleteString(offset, offset + crashAttr.length());
+ document.insertString(offset, "<attr name=\"newcrash\" format=\"integer\" />");
+ documentManager.commitDocument(document);
+ }
+ });
+ assertTrue(resources.isScanPending(psiFile1));
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ ensureSingleScan();
+ assertTrue(generation < resources.getModificationCount());
+ assertTrue(resources.hasResourceItem(ResourceType.DECLARE_STYLEABLE, "MyCustomView"));
+ assertFalse(resources.hasResourceItem(ResourceType.ATTR, "watchType"));
+ assertTrue(resources.hasResourceItem(ResourceType.ATTR, "wwatchType"));
+ ResourceItem style = getOnlyItem(resources, ResourceType.DECLARE_STYLEABLE, "MyCustomView");
+ DeclareStyleableResourceValue srv = (DeclareStyleableResourceValue)style.getResourceValue(false);
+ assertNotNull(srv);
+ assertEquals(5, srv.getAllAttributes().size());
+ AttrResourceValue watchType = srv.getAllAttributes().get("wwatchType");
+ assertNotNull(watchType);
+ assertEquals(2, watchType.getAttributeValues().size());
+ assertEquals(Integer.valueOf(1), watchType.getAttributeValues().get("type_stopwatch"));
+ assertEquals(Integer.valueOf(0), watchType.getAttributeValues().get("type_countdown"));
+ AttrResourceValue crash = srv.getAllAttributes().get("crash");
+ assertNull(crash);
+ AttrResourceValue newcrash = srv.getAllAttributes().get("newcrash");
+ assertNotNull(newcrash);
+ assertNull(newcrash.getAttributeValues());
+ }
+ });
+ }
+
+ @SuppressWarnings("deprecation")
+ public void testEditDeclareStyleableFlag() throws Exception {
+ // Rename, add and remove <flag> and <enum> nodes under a declare styleable and assert
+ // that the declare styleable parent is updated
+ resetScanCounter();
+
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ final PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ final ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+
+ assertTrue(resources.hasResourceItem(ResourceType.DECLARE_STYLEABLE, "MyCustomView"));
+ // Fetch resource value to ensure it gets replaced after update
+ assertTrue(resources.hasResourceItem(ResourceType.ATTR, "watchType"));
+ assertFalse(resources.hasResourceItem(ResourceType.ATTR, "ignore_no_format"));
+ final ResourceItem style = getOnlyItem(resources, ResourceType.DECLARE_STYLEABLE, "MyCustomView");
+ final DeclareStyleableResourceValue srv = (DeclareStyleableResourceValue)style.getResourceValue(false);
+ assertNotNull(srv);
+ assertEquals(5, srv.getAllAttributes().size());
+ final AttrResourceValue flagType = srv.getAllAttributes().get("flagType");
+ assertNotNull(flagType);
+ assertEquals(2, flagType.getAttributeValues().size());
+ assertEquals(Integer.valueOf(16), flagType.getAttributeValues().get("flag1"));
+ assertEquals(Integer.valueOf(32), flagType.getAttributeValues().get("flag2"));
+
+ final long generation = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ final int offset = document.getText().indexOf("flag1");
+ document.insertString(offset + 1, "l");
+ documentManager.commitDocument(document);
+ }
+ });
+ assertTrue(resources.isScanPending(psiFile1));
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ ensureSingleScan();
+ assertTrue(generation < resources.getModificationCount());
+ assertTrue(resources.hasResourceItem(ResourceType.DECLARE_STYLEABLE, "MyCustomView"));
+ assertTrue(resources.hasResourceItem(ResourceType.ATTR, "flagType"));
+ ResourceItem style = getOnlyItem(resources, ResourceType.DECLARE_STYLEABLE, "MyCustomView");
+ DeclareStyleableResourceValue srv = (DeclareStyleableResourceValue)style.getResourceValue(false);
+ assertNotNull(srv);
+ assertEquals(5, srv.getAllAttributes().size());
+ AttrResourceValue flagType = srv.getAllAttributes().get("flagType");
+ assertNotNull(flagType);
+ assertEquals(2, flagType.getAttributeValues().size());
+ assertNull(flagType.getAttributeValues().get("flag1"));
+ assertEquals(Integer.valueOf(16), flagType.getAttributeValues().get("fllag1"));
+
+ // Now insert a new enum and delete one and make sure we're still okay
+ resetScanCounter();
+ long nextGeneration = resources.getModificationCount();
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ String enumAttr = "<enum name=\"type_stopwatch\" value=\"1\"/>";
+ int offset = document.getText().indexOf(enumAttr);
+ document.deleteString(offset, offset + enumAttr.length());
+ String flagAttr = "<flag name=\"flag2\" value=\"0x20\"/>";
+ offset = document.getText().indexOf(flagAttr);
+ document.insertString(offset, "<flag name=\"flag3\" value=\"0x40\"/>");
+ documentManager.commitDocument(document);
+ }
+ });
+ assertTrue(nextGeneration < resources.getModificationCount());
+ assertTrue(resources.hasResourceItem(ResourceType.DECLARE_STYLEABLE, "MyCustomView"));
+ assertTrue(resources.hasResourceItem(ResourceType.ATTR, "watchType"));
+ assertTrue(resources.hasResourceItem(ResourceType.ATTR, "flagType"));
+ style = getOnlyItem(resources, ResourceType.DECLARE_STYLEABLE, "MyCustomView");
+ srv = (DeclareStyleableResourceValue)style.getResourceValue(false);
+ assertNotNull(srv);
+ assertEquals(5, srv.getAllAttributes().size());
+ flagType = srv.getAllAttributes().get("flagType");
+ assertNotNull(flagType);
+ assertEquals(3, flagType.getAttributeValues().size());
+ assertEquals(Integer.valueOf(16), flagType.getAttributeValues().get("fllag1"));
+ assertEquals(Integer.valueOf(32), flagType.getAttributeValues().get("flag2"));
+ assertEquals(Integer.valueOf(64), flagType.getAttributeValues().get("flag3"));
+
+ AttrResourceValue watchType = srv.getAllAttributes().get("watchType");
+ assertNotNull(watchType);
+ assertEquals(1, watchType.getAttributeValues().size());
+ assertEquals(Integer.valueOf(0), watchType.getAttributeValues().get("type_countdown"));
+ }
+ });
+ }
+
+ public void testEditPluralItems() throws Exception {
+ resetScanCounter();
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+
+ // Test that our tools:quantity works correctly for getResourceValue()
+ assertTrue(resources.hasResourceItem(ResourceType.PLURALS, "my_plural"));
+ ResourceItem plural = getOnlyItem(resources, ResourceType.PLURALS, "my_plural");
+ ResourceValue resourceValue = plural.getResourceValue(false);
+ assertNotNull(resourceValue);
+ assertEquals("@string/hello_two", resourceValue.getValue());
+
+ // TODO: It would be nice to avoid updating the generation if you
+ // edit a different item than the one being picked (default or via
+ // tools:quantity) but for now we're not worrying about that optimization
+
+ long generation = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ final int offset = document.getText().indexOf("@string/hello_two");
+ document.replaceString(offset + 9, offset + 10, "a");
+ documentManager.commitDocument(document);
+ }
+ });
+ plural = getOnlyItem(resources, ResourceType.PLURALS, "my_plural");
+ resourceValue = plural.getResourceValue(false);
+ assertNotNull(resourceValue);
+ assertEquals("@string/hallo_two", resourceValue.getValue());
+ assertTrue(generation < resources.getModificationCount());
+
+ // Shouldn't have done any full file rescans during the above edits
+ ensureIncremental();
+ }
+
+ public void testEditArrayItemText() throws Exception {
+ resetScanCounter();
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+
+ // Test that our tools:index and fallback handling for arrays works correctly
+ // for getResourceValue()
+ assertTrue(resources.hasResourceItem(ResourceType.ARRAY, "security_questions"));
+ ResourceItem array = getOnlyItem(resources, ResourceType.ARRAY, "security_questions");
+ ResourceValue resourceValue = array.getResourceValue(false);
+ assertNotNull(resourceValue);
+ assertEquals("Question 4", resourceValue.getValue());
+
+ assertTrue(resources.hasResourceItem(ResourceType.ARRAY, "integers"));
+ array = getOnlyItem(resources, ResourceType.ARRAY, "integers");
+ resourceValue = array.getResourceValue(false);
+ assertNotNull(resourceValue);
+ assertEquals("10", resourceValue.getValue());
+
+ long generation = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ final int offset = document.getText().indexOf("Question 4");
+ document.insertString(offset, "Q");
+ documentManager.commitDocument(document);
+ }
+ });
+
+ assertTrue(resources.hasResourceItem(ResourceType.ARRAY, "security_questions"));
+ array = getOnlyItem(resources, ResourceType.ARRAY, "security_questions");
+ resourceValue = array.getResourceValue(false);
+ assertNotNull(resourceValue);
+ assertEquals("QQuestion 4", resourceValue.getValue());
+
+ // Shouldn't have done any full file rescans during the above edits
+ ensureIncremental();
+ }
+
+ public void testAddArrayItemElements() throws Exception {
+ resetScanCounter();
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ final int offset = document.getText().indexOf("<item>Question 3</item>");
+ document.insertString(offset, "<item>Question 2.5</item>");
+ documentManager.commitDocument(document);
+ }
+ });
+
+ assertTrue(resources.hasResourceItem(ResourceType.ARRAY, "security_questions"));
+ ResourceItem array = getOnlyItem(resources, ResourceType.ARRAY, "security_questions");
+ ResourceValue resourceValue = array.getResourceValue(false);
+ assertNotNull(resourceValue);
+ assertEquals("Question 3", resourceValue.getValue());
+ assertTrue(resourceValue instanceof ArrayResourceValue);
+ ArrayResourceValue arv = (ArrayResourceValue)resourceValue;
+ assertEquals(6, arv.getElementCount());
+ assertEquals("Question 2", arv.getElement(1));
+ assertEquals("Question 2.5", arv.getElement(2));
+ assertEquals("Question 3", arv.getElement(3));
+
+ // Shouldn't have done any full file rescans during the above edits
+ ensureIncremental();
+ }
+
+ public void testRemoveArrayItemElements() throws Exception {
+ resetScanCounter();
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES1, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ String elementString = "<item>Question 3</item>";
+ final int offset = document.getText().indexOf(elementString);
+ document.deleteString(offset, offset + elementString.length());
+ documentManager.commitDocument(document);
+ }
+ });
+
+ assertTrue(resources.hasResourceItem(ResourceType.ARRAY, "security_questions"));
+ ResourceItem array = getOnlyItem(resources, ResourceType.ARRAY, "security_questions");
+ ResourceValue resourceValue = array.getResourceValue(false);
+ assertNotNull(resourceValue);
+ assertEquals("Question 5", resourceValue.getValue());
+ assertTrue(resourceValue instanceof ArrayResourceValue);
+ ArrayResourceValue arv = (ArrayResourceValue)resourceValue;
+ assertEquals(4, arv.getElementCount());
+
+ // Shouldn't have done any full file rescans during the above edits
+ ensureIncremental();
+ }
+
+ public void testGradualEdits() throws Exception {
+ resetScanCounter();
+
+ // Gradually type in the contents of a value file and make sure we end up with a valid view of the world
+ VirtualFile file1 = myFixture.copyFileToProject(VALUES_EMPTY, "res/values/myvalues.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ final ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+
+ final long generation = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ document.deleteString(0, document.getTextLength());
+ documentManager.commitDocument(document);
+ }
+ });
+
+ final String contents =
+ "<!--\n" +
+ " -->\n" +
+ "\n" +
+ "<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n" +
+ "\n" +
+ " <!-- Titles -->\n" +
+ " <string name=\"app_name\">Animations Demo</string>\n" +
+ " <string name=\"title_zoom\">Zoom</string>\n" +
+ " <string name=\"title_layout_changes\">Layout Changes</string>\n" +
+ " <string name=\"title_template_step\">Step <xliff:g id=\"step_number\">%1$d</xliff:g>: Lorem\n" +
+ " Ipsum</string>\n" +
+ " <string name=\"ellipsis\">Here it is: \\u2026!</string>\n" +
+ "\n" +
+ " <item type=\"id\" name=\"action_next\" />\n" +
+ "\n" +
+ " <style name=\"DarkActionBar\" parent=\"android:Widget.Holo.ActionBar\">\n" +
+ " <item name=\"android:background\">@android:color/transparent</item>\n" +
+ " </style>\n" +
+ "\n" +
+ " <integer name=\"card_flip_time_half\">150</integer>\n" +
+ "\n" +
+ " <declare-styleable name=\"MyCustomView\">\n" +
+ " <attr name=\"watchType\" format=\"enum\">\n" +
+ " <enum name=\"type_countdown\" value=\"0\"/>\n" +
+ " </attr>\n" +
+ " <attr name=\"crash\" format=\"boolean\" />\n" +
+ " </declare-styleable>\n" +
+ "</resources>\n";
+ for (int i = 0; i < contents.length(); i++) {
+ final int offset = i;
+ final char character = contents.charAt(i);
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ document.insertString(offset, String.valueOf(character));
+ documentManager.commitDocument(document);
+ }
+ });
+ }
+
+ assertTrue(resources.isScanPending(psiFile1));
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ ensureSingleScan();
+ assertTrue(generation < resources.getModificationCount());
+ assertTrue(resources.hasResourceItem(ResourceType.STYLE, "DarkActionBar"));
+
+ assertTrue(resources.hasResourceItem(ResourceType.STYLE, "DarkActionBar"));
+ ResourceItem style = getOnlyItem(resources, ResourceType.STYLE, "DarkActionBar");
+ StyleResourceValue srv = (StyleResourceValue)style.getResourceValue(false);
+ assertNotNull(srv);
+ ResourceValue actionBarStyle = srv.findValue("background", true);
+ assertNotNull(actionBarStyle);
+ assertEquals("@android:color/transparent", actionBarStyle.getValue());
+ //noinspection ConstantConditions
+ assertEquals("Zoom", getOnlyItem(resources, ResourceType.STRING, "title_zoom").getResourceValue(false).getValue());
+ }
+ });
+ }
+
+ public void testIssue56799() throws Exception {
+ // Test deleting a string; ensure that the whole repository is updated correctly.
+ // Regression test for
+ // https://code.google.com/p/android/issues/detail?id=56799
+ VirtualFile file1 = myFixture.copyFileToProject(STRINGS, "res/values/strings.xml");
+ PsiFile psiFile1 = PsiManager.getInstance(getProject()).findFile(file1);
+ assertNotNull(psiFile1);
+ final ResourceFolderRepository resources = createRepository();
+ assertNotNull(resources);
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "app_name"));
+ assertFalse(resources.hasResourceItem(ResourceType.STRING, "app_name2"));
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "hello_world"));
+
+
+ final long generation = resources.getModificationCount();
+ final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+ final Document document = documentManager.getDocument(psiFile1);
+ assertNotNull(document);
+
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ String string = " <string name=\"hello_world\">Hello world!</string>";
+ final int offset = document.getText().indexOf(string);
+ assertTrue(offset != -1);
+ document.deleteString(offset, offset + string.length());
+ documentManager.commitDocument(document);
+ }
+ });
+
+ assertTrue(generation < resources.getModificationCount());
+
+ assertTrue(resources.isScanPending(psiFile1));
+ resetScanCounter();
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ ensureSingleScan();
+ assertTrue(resources.hasResourceItem(ResourceType.STRING, "app_name"));
+ assertFalse(resources.hasResourceItem(ResourceType.STRING, "hello_world"));
+ }
+ });
+ }
+
+ @Nullable
+ private static XmlTag findTagById(@NotNull PsiFile file, @NotNull String id) {
+ assertFalse(id.startsWith(PREFIX_RESOURCE_REF)); // just the id
+ String newId = NEW_ID_PREFIX + id;
+ String oldId = ID_PREFIX + id;
+ for (XmlTag tag : PsiTreeUtil.findChildrenOfType(file, XmlTag.class)) {
+ String tagId = tag.getAttributeValue(ATTR_ID, ANDROID_URI);
+ if (newId.equals(tagId) || oldId.equals(tagId)) {
+ return tag;
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ private static XmlTag findTagByName(@NotNull PsiFile file, @NotNull String name) {
+ for (XmlTag tag : PsiTreeUtil.findChildrenOfType(file, XmlTag.class)) {
+ String tagName = tag.getAttributeValue(ATTR_NAME);
+ if (name.equals(tagName)) {
+ return tag;
+ }
+ }
+ return null;
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/rendering/ResourceNameValidatorTest.java b/android/testSrc/com/android/tools/idea/rendering/ResourceNameValidatorTest.java
new file mode 100644
index 0000000..e159185
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/rendering/ResourceNameValidatorTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.rendering;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.res2.ResourceItem;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Maps;
+import junit.framework.TestCase;
+import org.jetbrains.android.AndroidTestCase;
+
+@SuppressWarnings("javadoc")
+public class ResourceNameValidatorTest extends AndroidTestCase {
+ public void testValidator() throws Exception {
+ // Valid
+ ResourceNameValidator validator = ResourceNameValidator.create(true, ResourceFolderType.VALUES);
+ assertTrue(validator.getErrorText("foo") == null);
+ assertTrue(validator.checkInput("foo"));
+ assertTrue(validator.canClose("foo"));
+ assertTrue(validator.getErrorText("foo.xml") == null);
+ assertTrue(validator.getErrorText("Foo123_$") == null);
+ assertTrue(validator.getErrorText("foo.xm") == null); // For non-file types, . => _
+
+ // Invalid
+ assertEquals("Enter a new name", validator.getErrorText(""));
+ assertFalse(validator.checkInput(""));
+ assertFalse(validator.canClose(""));
+ assertEquals("Enter a new name", validator.getErrorText(" "));
+ assertEquals("' ' is not a valid resource name character", validator.getErrorText("foo bar"));
+ assertEquals("The resource name must begin with a character", validator.getErrorText("1foo"));
+ assertEquals("'%' is not a valid resource name character", validator.getErrorText("foo%bar"));
+ assertEquals("foo already exists",
+ ResourceNameValidator.create(true, Collections.singleton("foo"),ResourceType.STRING).getErrorText("foo"));
+ assertEquals("The filename must end with .xml",
+ ResourceNameValidator.create(true, ResourceFolderType.LAYOUT).getErrorText("foo.xm"));
+ assertEquals("The filename must end with .xml or .png",
+ ResourceNameValidator.create(true, ResourceFolderType.DRAWABLE).getErrorText("foo.xm"));
+ assertEquals("'.' is not a valid resource name character",
+ ResourceNameValidator.create(false, ResourceFolderType.DRAWABLE).getErrorText("foo.xm"));
+
+ // Only lowercase chars allowed in file-based resource names
+ assertEquals("File-based resource names must start with a lowercase letter.",
+ ResourceNameValidator.create(true, ResourceFolderType.LAYOUT).getErrorText("Foo123_$"));
+ assertEquals(null, ResourceNameValidator.create(true, ResourceFolderType.LAYOUT).getErrorText("foo123_"));
+
+ // Can't start with _ in file-based resource names, is okay for value based resources
+ assertEquals(null, ResourceNameValidator.create(true, ResourceFolderType.VALUES).getErrorText("_foo"));
+ assertEquals("File-based resource names must start with a lowercase letter.",
+ ResourceNameValidator.create(true, ResourceFolderType.LAYOUT).getErrorText("_foo"));
+ assertEquals("File-based resource names must start with a lowercase letter.",
+ ResourceNameValidator.create(true, ResourceFolderType.DRAWABLE).getErrorText("_foo"));
+
+ assertEquals(null, ResourceNameValidator.create(true, ResourceFolderType.DRAWABLE).getErrorText("foo.xml"));
+ assertEquals("'.' is not a valid resource name character",
+ ResourceNameValidator.create(false, ResourceFolderType.DRAWABLE).getErrorText("foo.xml"));
+ assertEquals("'.' is not a valid resource name character",
+ ResourceNameValidator.create(false, ResourceFolderType.DRAWABLE).getErrorText("foo.1.2"));
+ }
+
+ public void testIds() throws Exception {
+ ResourceNameValidator validator = ResourceNameValidator.create(false, (ProjectResources)null, ResourceType.ID);
+ assertEquals(null, validator.getErrorText("foo"));
+ assertEquals("The resource name must begin with a character", validator.getErrorText(" foo"));
+ assertEquals("' ' is not a valid resource name character", validator.getErrorText("foo "));
+ assertEquals("'@' is not a valid resource name character", validator.getErrorText("foo@"));
+ }
+
+ public void testIds2() throws Exception {
+ final Map<ResourceType, ListMultimap<String, ResourceItem>> map = Maps.newHashMap();
+ ListMultimap<String, ResourceItem> multimap = ArrayListMultimap.create();
+ map.put(ResourceType.ID, multimap);
+ multimap.put("foo1", new ResourceItem("foo1", ResourceType.ID, null));
+ multimap.put("foo3", new ResourceItem("foo3", ResourceType.ID, null));
+ ProjectResources resources = new ProjectResources("unit test") {
+ @NonNull
+ @Override
+ protected Map<ResourceType, ListMultimap<String, ResourceItem>> getMap() {
+ return map;
+ }
+
+ @Nullable
+ @Override
+ protected ListMultimap<String, ResourceItem> getMap(ResourceType type, boolean create) {
+ return map.get(type);
+ }
+ };
+ ResourceNameValidator validator = ResourceNameValidator.create(false, resources, ResourceType.ID);
+ assertEquals("foo1 already exists", validator.getErrorText("foo1"));
+ assertEquals(null, validator.getErrorText("foo2"));
+ assertEquals("foo3 already exists", validator.getErrorText("foo3"));
+ }
+
+ public void testUniqueOrExists() throws Exception {
+ Set<String> existing = new HashSet<String>();
+ existing.add("foo1");
+ existing.add("foo2");
+ existing.add("foo3");
+
+ ResourceNameValidator validator = ResourceNameValidator.create(true, existing, ResourceType.ID);
+ validator.unique();
+
+ assertNull(validator.getErrorText("foo")); // null: ok (no error message)
+ assertNull(validator.getErrorText("foo4"));
+ assertNotNull(validator.getErrorText("foo1"));
+ assertNotNull(validator.getErrorText("foo2"));
+ assertNotNull(validator.getErrorText("foo3"));
+
+ validator.exist();
+ assertNotNull(validator.getErrorText("foo"));
+ assertNotNull(validator.getErrorText("foo4"));
+ assertNull(validator.getErrorText("foo1"));
+ assertNull(validator.getErrorText("foo2"));
+ assertNull(validator.getErrorText("foo3"));
+ }
+}
\ No newline at end of file
diff --git a/android/testSrc/com/android/tools/idea/sdk/VersionCheckTest.java b/android/testSrc/com/android/tools/idea/sdk/VersionCheckTest.java
new file mode 100644
index 0000000..ff064a8
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/sdk/VersionCheckTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.sdk;
+
+import junit.framework.TestCase;
+import org.jetbrains.android.AndroidTestCase;
+
+/**
+ * Tests for {@link VersionCheck}.
+ */
+public class VersionCheckTest extends TestCase {
+ private static final String PRE_V22_SDK_PATH = "PRE_V22_SDK_PATH";
+
+ private String mySdkPath;
+ private String myPreV22SdkPath;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mySdkPath = System.getProperty(AndroidTestCase.SDK_PATH_PROPERTY);
+ if (mySdkPath == null) {
+ mySdkPath = System.getenv(AndroidTestCase.SDK_PATH_PROPERTY);
+ }
+ String format = "Please specify the path of an Android SDK (v22.0.0) in the system property '%1$s'";
+ String msg = String.format(format, AndroidTestCase.SDK_PATH_PROPERTY);
+ assertNotNull(msg, mySdkPath);
+
+ myPreV22SdkPath = System.getProperty(PRE_V22_SDK_PATH);
+ if (myPreV22SdkPath == null) {
+ myPreV22SdkPath = System.getenv(PRE_V22_SDK_PATH);
+ }
+ msg = String.format("Please specify the path of an old Android SDK (pre-22.0.0) with the system property '%1$s'", PRE_V22_SDK_PATH);
+ assertNotNull(msg, myPreV22SdkPath);
+ }
+
+ public void testCheckVersion() {
+ VersionCheck.VersionCheckResult result = VersionCheck.checkVersion(mySdkPath);
+ assertTrue(result.isCompatibleVersion());
+ }
+
+ public void testCheckVersionWithOldSdk() {
+ VersionCheck.VersionCheckResult result = VersionCheck.checkVersion(myPreV22SdkPath);
+ assertFalse(result.isCompatibleVersion());
+ assertNotNull(result.getRevision());
+ }
+}
diff --git a/android/testSrc/org/jetbrains/android/AndroidFacetImporterTest.java b/android/testSrc/org/jetbrains/android/AndroidFacetImporterTest.java
index f471814..ac6acc6 100644
--- a/android/testSrc/org/jetbrains/android/AndroidFacetImporterTest.java
+++ b/android/testSrc/org/jetbrains/android/AndroidFacetImporterTest.java
@@ -487,12 +487,12 @@
final Module module = getModule("module");
final AndroidFacet facet = AndroidFacet.getInstance(module);
assertNotNull(facet);
- assertFalse(facet.getProperties().LIBRARY_PROJECT);
+ assertFalse(facet.isLibraryProject());
final Module apklibModule = getModule("~apklib-com_myapklib_1.0");
final AndroidFacet apklibFacet = AndroidFacet.getInstance(apklibModule);
assertNotNull(apklibFacet);
- assertTrue(apklibFacet.getProperties().LIBRARY_PROJECT);
+ assertTrue(apklibFacet.isLibraryProject());
checkSdk(ModuleRootManager.getInstance(apklibModule).getSdk(), "Maven Android 1.1 Platform", "android-2");
final Library jarLib = checkAppModuleDeps(module, apklibModule);
@@ -561,17 +561,17 @@
final Module module1 = getModule("module1");
final AndroidFacet facet1 = AndroidFacet.getInstance(module1);
assertNotNull(facet1);
- assertFalse(facet1.getProperties().LIBRARY_PROJECT);
+ assertFalse(facet1.isLibraryProject());
final Module module2 = getModule("module1");
final AndroidFacet facet2 = AndroidFacet.getInstance(module2);
assertNotNull(facet2);
- assertFalse(facet2.getProperties().LIBRARY_PROJECT);
+ assertFalse(facet2.isLibraryProject());
final Module apklibModule = getModule("~apklib-com_myapklib_1.0");
final AndroidFacet apklibFacet = AndroidFacet.getInstance(apklibModule);
assertNotNull(apklibFacet);
- assertTrue(apklibFacet.getProperties().LIBRARY_PROJECT);
+ assertTrue(apklibFacet.isLibraryProject());
final Library jarLib1 = checkAppModuleDeps(module1, apklibModule);
final Library jarLib2 = checkAppModuleDeps(module2, apklibModule);
diff --git a/android/testSrc/org/jetbrains/android/AndroidTestCase.java b/android/testSrc/org/jetbrains/android/AndroidTestCase.java
index 6e06485..373a847 100644
--- a/android/testSrc/org/jetbrains/android/AndroidTestCase.java
+++ b/android/testSrc/org/jetbrains/android/AndroidTestCase.java
@@ -53,7 +53,8 @@
@SuppressWarnings({"JUnitTestCaseWithNonTrivialConstructors"})
public abstract class AndroidTestCase extends UsefulTestCase {
/** Environment variable or system property containing the full path to an SDK install */
- private static final String SDK_PATH_PROPERTY = "ADT_TEST_SDK_PATH";
+ public static final String SDK_PATH_PROPERTY = "ADT_TEST_SDK_PATH";
+
/** Environment variable or system property pointing to the directory name of the platform inside $sdk/platforms, e.g. "android-17" */
private static final String PLATFORM_DIR_PROPERTY = "ADT_TEST_PLATFORM";
@@ -189,7 +190,7 @@
final Module additionalModule = data.myModuleFixtureBuilder.getFixture().getModule();
myAdditionalModules.add(additionalModule);
final AndroidFacet facet = addAndroidFacet(additionalModule, sdkPath, getPlatformDir());
- facet.getConfiguration().getState().LIBRARY_PROJECT = data.myLibrary;
+ facet.setLibraryProject(data.myLibrary);
final String rootPath = getContentRootPath(data.myDirName);
myFixture.copyDirectoryToProject("res", rootPath + "/res");
myFixture.copyFileToProject(SdkConstants.FN_ANDROID_MANIFEST_XML,
diff --git a/android/testSrc/org/jetbrains/android/dom/AndroidLibraryProjectTest.java b/android/testSrc/org/jetbrains/android/dom/AndroidLibraryProjectTest.java
index 6cfadd8..ec8a3fc 100644
--- a/android/testSrc/org/jetbrains/android/dom/AndroidLibraryProjectTest.java
+++ b/android/testSrc/org/jetbrains/android/dom/AndroidLibraryProjectTest.java
@@ -93,7 +93,7 @@
myAppFacet = AndroidTestCase.addAndroidFacet(myAppModule, getDefaultTestSdkPath(), getDefaultPlatformDir());
myLibFacet = AndroidTestCase.addAndroidFacet(myLibModule, getDefaultTestSdkPath(), getDefaultPlatformDir());
- myLibFacet.getConfiguration().getState().LIBRARY_PROJECT = true;
+ myLibFacet.setLibraryProject(true);
ModuleRootModificationUtil.addDependency(myAppModule, myLibModule);
ModuleRootModificationUtil.addDependency(myLibModule, myLibGenModule);
@@ -243,13 +243,13 @@
}
public void testFileResourceFindUsagesFromJava1() throws Throwable {
- boolean temp = myLibFacet.getConfiguration().getState().LIBRARY_PROJECT;
+ boolean temp = myLibFacet.isLibraryProject();
try {
- myLibFacet.getConfiguration().getState().LIBRARY_PROJECT = true;
+ myLibFacet.setLibraryProject(true);
doFindUsagesTest("java", "app/src/p1/p2/lib/");
}
finally {
- myLibFacet.getConfiguration().getState().LIBRARY_PROJECT = temp;
+ myLibFacet.setLibraryProject(temp);
}
}
diff --git a/android/testSrc/org/jetbrains/android/inspections/lint/AndroidLintInspectionToolProviderTest.java b/android/testSrc/org/jetbrains/android/inspections/lint/AndroidLintInspectionToolProviderTest.java
index 3555756..9167527 100644
--- a/android/testSrc/org/jetbrains/android/inspections/lint/AndroidLintInspectionToolProviderTest.java
+++ b/android/testSrc/org/jetbrains/android/inspections/lint/AndroidLintInspectionToolProviderTest.java
@@ -213,7 +213,7 @@
if (issue == NamespaceDetector.TYPO || // IDEA has its own spelling check
issue == NamespaceDetector.UNUSED || // IDEA already does full validation
issue == ManifestTypoDetector.ISSUE || // IDEA already does full validation
- issue == ManifestOrderDetector.WRONG_PARENT || // IDEA already does full validation
+ issue == ManifestDetector.WRONG_PARENT || // IDEA already does full validation
issue == DeprecationDetector.ISSUE ||
issue == LocaleDetector.STRING_LOCALE) { // IDEA checks for this in Java code
return false;
diff --git a/android/testSrc/org/jetbrains/android/inspections/lint/IntellijApiDetectorTest.java b/android/testSrc/org/jetbrains/android/inspections/lint/IntellijApiDetectorTest.java
index 660ad40..7e249ed 100644
--- a/android/testSrc/org/jetbrains/android/inspections/lint/IntellijApiDetectorTest.java
+++ b/android/testSrc/org/jetbrains/android/inspections/lint/IntellijApiDetectorTest.java
@@ -45,7 +45,7 @@
public void testInterfaces2() throws Exception {
AndroidLintNewApiInspection inspection = new AndroidLintNewApiInspection();
- doTest(inspection, "Add @SuppressLint(\"NewApi\") annotation");
+ doTest(inspection, "Suppress: Add @SuppressLint(\"NewApi\") annotation");
}
public void testListView() throws Exception {
@@ -53,15 +53,10 @@
doTest(inspection, null);
}
- /**
- * For unknown reasons this test doesn't work; it cannot resolve the symbol android.os.Build. This
- * is probably related to the fact that the unit tests work with an incomplete snapshot of an ancient
- * SDK (1.5 or something like that.)
public void testVersionConditional() throws Exception {
AndroidLintNewApiInspection inspection = new AndroidLintNewApiInspection();
doTest(inspection, null);
}
- */
private void doTest(@NotNull final AndroidLintInspectionBase inspection, @Nullable String quickFixName) throws Exception {
createManifest();
diff --git a/android/testSrc/org/jetbrains/android/inspections/lint/LombokPsiConverterTest.java b/android/testSrc/org/jetbrains/android/inspections/lint/LombokPsiConverterTest.java
index c431e18..5ed7bf8 100644
--- a/android/testSrc/org/jetbrains/android/inspections/lint/LombokPsiConverterTest.java
+++ b/android/testSrc/org/jetbrains/android/inspections/lint/LombokPsiConverterTest.java
@@ -438,6 +438,37 @@
check(file, testClass);
}
+ public void test57783() {
+ String testClass =
+ "package test.pkg;\n" +
+ "\n" +
+ "public final class R7 {\n" +
+ " public void foo() {\n" +
+ " int i = 0;\n" +
+ " int j = 0;\n" +
+ " for (i = 0; i < 10; i++)\n" +
+ " i++;" +
+ " for (i = 0, j = 0; i < 10; i++)\n" +
+ " i++;" +
+ " }\n" +
+ "}";
+ PsiFile file = myFixture.addFileToProject("src/test/pkg/R7.java", testClass);
+ check(file, testClass);
+ }
+
+ public void testSuper() {
+ String testClass =
+ "package test.pkg;\n" +
+ "\n" +
+ "public final class R8 {\n" +
+ " public String toString() {\n" +
+ " return super.toString();\n" +
+ " }\n" +
+ "}";
+ PsiFile file = myFixture.addFileToProject("src/test/pkg/R8.java", testClass);
+ check(file, testClass);
+ }
+
private void check(VirtualFile file) {
assertNotNull(file);
assertTrue(file.exists());
@@ -445,7 +476,7 @@
assertNotNull(project);
PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
assertNotNull(psiFile);
- check(psiFile, psiFile.getText());;
+ check(psiFile, psiFile.getText());
}
private void check(String source, String relativePath) {
diff --git a/android/testSrc/org/jetbrains/android/intentions/AndroidIntentionsTest.java b/android/testSrc/org/jetbrains/android/intentions/AndroidIntentionsTest.java
index 59c17c2..d63817c 100644
--- a/android/testSrc/org/jetbrains/android/intentions/AndroidIntentionsTest.java
+++ b/android/testSrc/org/jetbrains/android/intentions/AndroidIntentionsTest.java
@@ -13,21 +13,21 @@
private static final String BASE_PATH = "intentions/";
public void testSwitchOnResourceId() {
- myFacet.getConfiguration().getState().LIBRARY_PROJECT = true;
+ myFacet.setLibraryProject(true);
myFixture.copyFileToProject(BASE_PATH + "R.java", "src/p1/p2/R.java");
final AndroidNonConstantResIdsInSwitchInspection inspection = new AndroidNonConstantResIdsInSwitchInspection();
doTest(inspection, true, inspection.getQuickFixName());
}
public void testSwitchOnResourceId1() {
- myFacet.getConfiguration().getState().LIBRARY_PROJECT = false;
+ myFacet.setLibraryProject(false);
myFixture.copyFileToProject(BASE_PATH + "R.java", "src/p1/p2/R.java");
final AndroidNonConstantResIdsInSwitchInspection inspection = new AndroidNonConstantResIdsInSwitchInspection();
doTest(inspection, false, inspection.getQuickFixName());
}
public void testSwitchOnResourceId2() {
- myFacet.getConfiguration().getState().LIBRARY_PROJECT = true;
+ myFacet.setLibraryProject(true);
myFixture.copyFileToProject(BASE_PATH + "R.java", "src/p1/p2/R.java");
final AndroidNonConstantResIdsInSwitchInspection inspection = new AndroidNonConstantResIdsInSwitchInspection();
doTest(inspection, false, inspection.getQuickFixName());
diff --git a/android/testSrc/org/jetbrains/android/logcat/AndroidLogcatFormatterTest.java b/android/testSrc/org/jetbrains/android/logcat/AndroidLogcatFormatterTest.java
index 600fa8c..109ffc4 100644
--- a/android/testSrc/org/jetbrains/android/logcat/AndroidLogcatFormatterTest.java
+++ b/android/testSrc/org/jetbrains/android/logcat/AndroidLogcatFormatterTest.java
@@ -65,7 +65,7 @@
public void test1() {
String message = "02-12 17:04:44.005 1282-12/com.google.android.apps" +
- ".maps:GoogleLocationService D/dalvikvm: Debugger has detached; object " +
+ ".maps:GoogleLocationService D/dalvikvm" + AndroidLogcatFormatter.TAG_SEPARATOR + " Debugger has detached; object " +
"registry had 1 entries";
LogMessageHeader header = AndroidLogcatFormatter.parseMessage(message).getFirst();
@@ -74,4 +74,12 @@
assertEquals(Log.LogLevel.DEBUG, header.myLogLevel);
assertEquals("dalvikvm", header.myTag);
}
+
+ public void testSpaces() {
+ String msg = String.format("08-23 14:30:59.370 32664-32664/com.timios.gfe I/Web Console%1$s pds: can you see this? at " +
+ "file:///android_asset/www/scripts/loan_officer.js:429", AndroidLogcatFormatter.TAG_SEPARATOR);
+
+ LogMessageHeader header = AndroidLogcatFormatter.parseMessage(msg).getFirst();
+ assertEquals(Log.LogLevel.INFO, header.myLogLevel);
+ }
}
diff --git a/android/testSrc/org/jetbrains/android/logcat/AndroidLogcatReceiverTest.java b/android/testSrc/org/jetbrains/android/logcat/AndroidLogcatReceiverTest.java
index 52e9024..8a67da9 100644
--- a/android/testSrc/org/jetbrains/android/logcat/AndroidLogcatReceiverTest.java
+++ b/android/testSrc/org/jetbrains/android/logcat/AndroidLogcatReceiverTest.java
@@ -38,7 +38,7 @@
myReceiver.processNewLine("Thread[Service Reconnect,5,main]: Connection to service failed 1");
assertEquals(
- "02-11 16:41:10.621 17945-17995/? W/GAV2: Thread[Service Reconnect,5,main]: Connection to service failed 1\n",
+ insertTagSeparator("02-11 16:41:10.621 17945-17995/? W/GAV2", "Thread[Service Reconnect,5,main]: Connection to service failed 1\n"),
myWriter.getBuffer().toString());
}
@@ -52,7 +52,8 @@
myReceiver.processNewLine(line2);
myReceiver.processNewLine(line3);
- assertEquals("02-11 18:03:35.037 19796-19796/? E/AndroidRuntime: "
+ assertEquals("02-11 18:03:35.037 19796-19796/? E/AndroidRuntime"
+ + AndroidLogcatFormatter.TAG_SEPARATOR + " "
+ line1 + "\n" +
SHIFT + line2 + "\n" +
SHIFT + line3 + "\n",
@@ -73,7 +74,7 @@
"warning message",
"[ 08-11 19:11:07.132 495:0x1ef F/wtftag ]",
"wtf message",
- "[ 08-11 21:15:35.7524 540:0x21c D/dtag ]",
+ "[ 08-11 21:15:35.7524 540:0x21c D/debug tag ]",
"debug message",
};
@@ -82,13 +83,17 @@
}
assertEquals(
- "08-11 19:11:07.132 495-495/? D/dtag: debug message\n" +
- "08-11 19:11:07.132 495-234/? E/etag: error message\n" +
- "08-11 19:11:07.132 495-495/? I/itag: info message\n" +
- "08-11 19:11:07.132 495-495/? V/vtag: verbose message\n" +
- "08-11 19:11:07.132 495-495/? W/wtag: warning message\n" +
- "08-11 19:11:07.132 495-495/? A/wtftag: wtf message\n" +
- "08-11 21:15:35.7524 540-540/? D/dtag: debug message\n",
+ insertTagSeparator("08-11 19:11:07.132 495-495/? D/dtag", "debug message\n") +
+ insertTagSeparator("08-11 19:11:07.132 495-234/? E/etag", "error message\n") +
+ insertTagSeparator("08-11 19:11:07.132 495-495/? I/itag", "info message\n") +
+ insertTagSeparator("08-11 19:11:07.132 495-495/? V/vtag", "verbose message\n") +
+ insertTagSeparator("08-11 19:11:07.132 495-495/? W/wtag", "warning message\n") +
+ insertTagSeparator("08-11 19:11:07.132 495-495/? A/wtftag", "wtf message\n") +
+ insertTagSeparator("08-11 21:15:35.7524 540-540/? D/debug tag", "debug message\n"),
myWriter.getBuffer().toString());
}
+
+ private String insertTagSeparator(String header, String msg) {
+ return String.format("%1$s%2$s %3$s", header, AndroidLogcatFormatter.TAG_SEPARATOR, msg);
+ }
}
diff --git a/android/testSrc/org/jetbrains/android/sdk/AndroidSdkUtilsTest.java b/android/testSrc/org/jetbrains/android/sdk/AndroidSdkUtilsTest.java
index 7e00587..d20cc5c 100644
--- a/android/testSrc/org/jetbrains/android/sdk/AndroidSdkUtilsTest.java
+++ b/android/testSrc/org/jetbrains/android/sdk/AndroidSdkUtilsTest.java
@@ -21,27 +21,26 @@
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.testFramework.IdeaTestCase;
+import org.jetbrains.android.AndroidTestCase;
import org.jetbrains.annotations.NotNull;
/**
* Tests for {@link AndroidSdkUtils}.
*/
public class AndroidSdkUtilsTest extends IdeaTestCase {
- public static final String MODERN_ANDROID_SDK_PATH = "android.modern.sdk.path";
-
- private String myModernAndroidSdkPath;
+ private String mySdkPath;
@Override
protected void setUp() throws Exception {
super.setUp();
- myModernAndroidSdkPath = System.getProperty(MODERN_ANDROID_SDK_PATH);
- String msg =
- String.format("Please specify the path of a modern Android SDK (v22.0.1) in the system property '%1$s'", MODERN_ANDROID_SDK_PATH);
- assertNotNull(msg, myModernAndroidSdkPath);
+ mySdkPath = System.getProperty(AndroidTestCase.SDK_PATH_PROPERTY);
+ String format = "Please specify the path of an Android SDK (v22.0.0) in the system property '%1$s'";
+ String msg = String.format(format, AndroidTestCase.SDK_PATH_PROPERTY);
+ assertNotNull(msg, mySdkPath);
}
public void testFindSuitableAndroidSdkWhenNoSdkSet() {
- Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk("android-17", myModernAndroidSdkPath, false);
+ Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk("android-17", mySdkPath, false);
assertNull(sdk);
}
@@ -49,25 +48,25 @@
String targetHashString = "android-17";
Sdk jdk = getTestProjectJdk();
assertNotNull(jdk);
- createAndroidSdk(myModernAndroidSdkPath, targetHashString, jdk);
+ createAndroidSdk(mySdkPath, targetHashString, jdk);
- Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk(targetHashString, myModernAndroidSdkPath, false);
+ Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk(targetHashString, mySdkPath, false);
assertNotNull(sdk);
- assertEquals(myModernAndroidSdkPath, sdk.getHomePath());
+ assertEquals(mySdkPath, sdk.getHomePath());
}
public void testTryToCreateAndSetAndroidSdkWithPathOfModernSdk() {
- boolean sdkSet = AndroidSdkUtils.tryToCreateAndSetAndroidSdk(myModule, myModernAndroidSdkPath, "android-17", false);
+ boolean sdkSet = AndroidSdkUtils.tryToCreateAndSetAndroidSdk(myModule, mySdkPath, "android-17", false);
assertTrue(sdkSet);
Sdk sdk = ModuleRootManager.getInstance(myModule).getSdk();
assertNotNull(sdk);
- assertEquals(myModernAndroidSdkPath, sdk.getHomePath());
+ assertEquals(mySdkPath, sdk.getHomePath());
}
public void testCreateNewAndroidPlatformWithPathOfModernSdkOnly() {
- Sdk sdk = AndroidSdkUtils.createNewAndroidPlatform(myModernAndroidSdkPath);
+ Sdk sdk = AndroidSdkUtils.createNewAndroidPlatform(mySdkPath);
assertNotNull(sdk);
- assertEquals(myModernAndroidSdkPath, sdk.getHomePath());
+ assertEquals(mySdkPath, sdk.getHomePath());
}
private static void createAndroidSdk(@NotNull String androidHomePath, @NotNull String targetHashString, @NotNull Sdk javaSdk) {