merge in studio-1.0-release history after reset to 8c142f811a8d2dfce7ecd6ced5c529b8737b9599
diff --git a/adt-branding/src/idea/AndroidStudioApplicationInfo.xml b/adt-branding/src/idea/AndroidStudioApplicationInfo.xml
index e3157a2..5e351c0 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="Beta" major="0" minor="9.1" eap="false"/>
+ <version codename="Beta" major="0" minor="9.2" eap="false"/>
<company name="Google Inc." url="http://developer.android.com"/>
<build number="__BUILD_NUMBER__" date="__BUILD_DATE__" apiVersion="135.1286"/>
<install-over minbuild="0.1" maxbuild="999.999999" version="0"/>
diff --git a/android/guiTestSrc/com/android/tools/idea/tests/gui/framework/GuiTests.java b/android/guiTestSrc/com/android/tools/idea/tests/gui/framework/GuiTests.java
index c691f72..d1a51bc 100644
--- a/android/guiTestSrc/com/android/tools/idea/tests/gui/framework/GuiTests.java
+++ b/android/guiTestSrc/com/android/tools/idea/tests/gui/framework/GuiTests.java
@@ -99,7 +99,7 @@
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
- DefaultSdks.setDefaultAndroidHome(androidSdkPath);
+ DefaultSdks.setDefaultAndroidHome(androidSdkPath, null);
}
});
}
diff --git a/android/guiTestSrc/com/android/tools/idea/tests/gui/framework/fixture/layout/RenderErrorPanelFixture.java b/android/guiTestSrc/com/android/tools/idea/tests/gui/framework/fixture/layout/RenderErrorPanelFixture.java
index 56d4fd4..9e6fc0b 100644
--- a/android/guiTestSrc/com/android/tools/idea/tests/gui/framework/fixture/layout/RenderErrorPanelFixture.java
+++ b/android/guiTestSrc/com/android/tools/idea/tests/gui/framework/fixture/layout/RenderErrorPanelFixture.java
@@ -21,6 +21,7 @@
import com.android.tools.idea.rendering.RenderResult;
import org.fest.swing.core.Robot;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import static org.junit.Assert.*;
@@ -83,4 +84,29 @@
}
}
}
+
+ public boolean haveErrors(boolean includeWarnings) {
+ RenderResult lastResult = myRenderContext.getLastResult();
+ assertNotNull("No render result available", lastResult);
+ RenderLogger logger = lastResult.getLogger();
+
+ return includeWarnings ? logger.hasProblems() : logger.hasErrors();
+ }
+
+ @NotNull
+ public String getErrorHtml() {
+ RenderResult lastResult = myRenderContext.getLastResult();
+ assertNotNull("No render result available", lastResult);
+ RenderLogger logger = lastResult.getLogger();
+
+ if (logger.hasProblems()) {
+ RenderErrorPanel panel = new RenderErrorPanel();
+ String html = panel.showErrors(lastResult);
+ if (html != null) {
+ return html;
+ }
+ }
+
+ return "";
+ }
}
diff --git a/android/guiTestSrc/com/android/tools/idea/tests/gui/layout/NewProjectTest.java b/android/guiTestSrc/com/android/tools/idea/tests/gui/layout/NewProjectTest.java
index 8d9cb41..b744006 100644
--- a/android/guiTestSrc/com/android/tools/idea/tests/gui/layout/NewProjectTest.java
+++ b/android/guiTestSrc/com/android/tools/idea/tests/gui/layout/NewProjectTest.java
@@ -25,6 +25,7 @@
import com.android.tools.idea.tests.gui.framework.fixture.IdeFrameFixture;
import com.android.tools.idea.tests.gui.framework.fixture.InspectionsFixture;
import com.android.tools.idea.tests.gui.framework.fixture.layout.LayoutEditorFixture;
+import com.android.tools.idea.tests.gui.framework.fixture.layout.RenderErrorPanelFixture;
import com.android.tools.idea.tests.gui.framework.fixture.newProjectWizard.ConfigureAndroidProjectStepFixture;
import com.android.tools.idea.tests.gui.framework.fixture.newProjectWizard.NewProjectWizardFixture;
import com.intellij.openapi.module.Module;
@@ -39,6 +40,7 @@
import java.io.IOException;
import static com.android.tools.idea.wizard.FormFactorUtils.FormFactor.MOBILE;
+import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static org.fest.assertions.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
@@ -154,6 +156,26 @@
}
}
+ @Test
+ @IdeGuiTest(closeProjectBeforeExecution = true)
+ public void testStillBuildingMessage() throws Exception {
+ // Creates a new project with minSdk 15, which should use appcompat.
+ // Check that if there are render-error messages on first render,
+ // they don't include "Missing Styles" (should now talk about project building instead)
+ IdeFrameFixture projectFrame = newProject("Test Application").withBriefNames().withMinSdk("15").withoutSync().create();
+ EditorFixture editor = projectFrame.getEditor();
+ editor.open("app/src/main/res/layout/activity_a.xml", EditorFixture.Tab.DESIGN);
+ LayoutEditorFixture layoutEditor = editor.getLayoutEditor(true);
+ assertNotNull(layoutEditor);
+ layoutEditor.waitForNextRenderToFinish();
+
+ RenderErrorPanelFixture renderErrors = layoutEditor.getRenderErrors();
+ String html = renderErrors.getErrorHtml();
+ // We could be showing an error message, but if we do, it should *not* say missing styles
+ // (should only be showing project render errors)
+ assertFalse(html, html.contains("Missing styles"));
+ }
+
@NotNull
private NewProjectDescriptor newProject(@NotNull String name) {
return new NewProjectDescriptor(name);
@@ -168,6 +190,7 @@
private String myMinSdk = "19";
private String myName = "TestProject";
private String myDomain = "com.android";
+ private boolean myWaitForSync = true;
private NewProjectDescriptor(@NotNull String name) {
withName(name);
@@ -221,6 +244,12 @@
return this;
}
+ /** Turns off the automatic wait-for-sync that normally happens on {@link #create} */
+ NewProjectDescriptor withoutSync() {
+ myWaitForSync = false;
+ return this;
+ }
+
/**
* Creates a project fixture for this description
*/
@@ -245,7 +274,9 @@
newProjectWizard.clickFinish();
IdeFrameFixture projectFrame = findIdeFrame(myName, projectPath);
- projectFrame.waitForGradleProjectSyncToFinish();
+ if (myWaitForSync) {
+ projectFrame.waitForGradleProjectSyncToFinish();
+ }
return projectFrame;
}
diff --git a/android/resources/icons/AndroidIcons.java b/android/resources/icons/AndroidIcons.java
index 0c8bd78..8d1ba0f 100755
--- a/android/resources/icons/AndroidIcons.java
+++ b/android/resources/icons/AndroidIcons.java
@@ -67,17 +67,17 @@
// Form factors
public static class FormFactors {
- public static final Icon Wear_16 = load("/icons/wear.png"); // 16x16
- public static final Icon Car_16 = load("/icons/car.png"); // 16x16
- public static final Icon Glass_16 = load("/icons/glass.png"); // 16x16
- public static final Icon Mobile_16 = load("/icons/mobile.png"); // 16x16
- public static final Icon Tv_16 = load("/icons/tv.png"); // 16x16
+ public static final Icon Wear_16 = load("/icons/wear.png"); // 16x16
+ public static final Icon Car_16 = load("/icons/car.png"); // 16x16
+ public static final Icon Glass_16 = load("/icons/glass.png"); // 16x16
+ public static final Icon Mobile_16 = load("/icons/mobile.png"); // 16x16
+ public static final Icon Tv_16 = load("/icons/tv.png"); // 16x16
- public static final Icon Wear_32 = load("/icons/formfactors/wear_32.png"); // 32x32
- public static final Icon Car_32 = load("/icons/formfactors/car_32.png"); // 32x32
- public static final Icon Glass_32 = load("/icons/formfactors/glass_32.png"); // 32x32
+ public static final Icon Wear_32 = load("/icons/formfactors/wear_32.png"); // 32x32
+ public static final Icon Car_32 = load("/icons/formfactors/car_32.png"); // 32x32
+ public static final Icon Glass_32 = load("/icons/formfactors/glass_32.png"); // 32x32
public static final Icon Mobile_32 = load("/icons/formfactors/phone_tablet_32.png"); // 32x32
- public static final Icon Tv_32 = load("/icons/formfactors/tv_32.png"); // 32x32
+ public static final Icon Tv_32 = load("/icons/formfactors/tv_32.png"); // 32x32
public static final Icon Wear_64 = load("/icons/formfactors/64/wear.png"); // 64x64
public static final Icon Car_64 = load("/icons/formfactors/64/car.png"); // 64x64
@@ -85,11 +85,11 @@
public static final Icon Mobile_64 = load("/icons/formfactors/64/phone_tablet.png"); // 64x64
public static final Icon Tv_64 = load("/icons/formfactors/64/tv.png"); // 64x64
- public static final Icon Wear_128 = load("/icons/wear_128.png"); // 128x128
- public static final Icon Car_128 = load("/icons/car_128.png"); // 128x128
- public static final Icon Glass_128 = load("/icons/glass_128.png"); // 128x128
- public static final Icon Mobile_128 = load("/icons/mobile_128.png"); // 128x128
- public static final Icon Tv_128 = load("/icons/tv_128.png"); // 128x128
+ public static final Icon Wear_128 = load("/icons/formfactors/128/wear.png"); // 128x128
+ public static final Icon Car_128 = load("/icons/formfactors/128/car.png"); // 128x128
+ public static final Icon Glass_128 = load("/icons/formfactors/128/glass.png"); // 128x128
+ public static final Icon Mobile_128 = load("/icons/formfactors/128/mobile.png"); // 128x128
+ public static final Icon Tv_128 = load("/icons/formfactors/128/tv.png"); // 128x128
}
public static class Configs {
diff --git a/android/resources/icons/car_128.png b/android/resources/icons/formfactors/128/car.png
similarity index 100%
rename from android/resources/icons/car_128.png
rename to android/resources/icons/formfactors/128/car.png
Binary files differ
diff --git a/android/resources/icons/formfactors/128/car@2x.png b/android/resources/icons/formfactors/128/car@2x.png
new file mode 100755
index 0000000..728ce0a
--- /dev/null
+++ b/android/resources/icons/formfactors/128/car@2x.png
Binary files differ
diff --git a/android/resources/icons/glass_128.png b/android/resources/icons/formfactors/128/glass.png
similarity index 100%
rename from android/resources/icons/glass_128.png
rename to android/resources/icons/formfactors/128/glass.png
Binary files differ
diff --git a/android/resources/icons/formfactors/128/glass@2x.png b/android/resources/icons/formfactors/128/glass@2x.png
new file mode 100755
index 0000000..3ee9ded
--- /dev/null
+++ b/android/resources/icons/formfactors/128/glass@2x.png
Binary files differ
diff --git a/android/resources/icons/mobile_128.png b/android/resources/icons/formfactors/128/mobile.png
similarity index 100%
rename from android/resources/icons/mobile_128.png
rename to android/resources/icons/formfactors/128/mobile.png
Binary files differ
diff --git a/android/resources/icons/formfactors/128/mobile@2x.png b/android/resources/icons/formfactors/128/mobile@2x.png
new file mode 100755
index 0000000..2469efe
--- /dev/null
+++ b/android/resources/icons/formfactors/128/mobile@2x.png
Binary files differ
diff --git a/android/resources/icons/tv_128.png b/android/resources/icons/formfactors/128/tv.png
similarity index 100%
rename from android/resources/icons/tv_128.png
rename to android/resources/icons/formfactors/128/tv.png
Binary files differ
diff --git a/android/resources/icons/formfactors/128/tv@2x.png b/android/resources/icons/formfactors/128/tv@2x.png
new file mode 100755
index 0000000..496d2e9
--- /dev/null
+++ b/android/resources/icons/formfactors/128/tv@2x.png
Binary files differ
diff --git a/android/resources/icons/wear_128.png b/android/resources/icons/formfactors/128/wear.png
similarity index 100%
rename from android/resources/icons/wear_128.png
rename to android/resources/icons/formfactors/128/wear.png
Binary files differ
diff --git a/android/resources/icons/formfactors/128/wear@2x.png b/android/resources/icons/formfactors/128/wear@2x.png
new file mode 100755
index 0000000..4ac3a4d
--- /dev/null
+++ b/android/resources/icons/formfactors/128/wear@2x.png
Binary files differ
diff --git a/android/src/META-INF/plugin.xml b/android/src/META-INF/plugin.xml
index 88db329..21ece2d 100755
--- a/android/src/META-INF/plugin.xml
+++ b/android/src/META-INF/plugin.xml
@@ -4,7 +4,7 @@
<description>
Supports development of Open Handset Alliance Android applications with IntelliJ IDEA.
</description>
- <version>10.0.9.1</version>
+ <version>10.0.9.2</version>
<vendor>JetBrains</vendor>
<application-components>
<component>
@@ -644,6 +644,7 @@
<spellchecker.support language="Groovy" implementationClass="org.jetbrains.android.spellchecker.AndroidGradleSpellcheckingStrategy" order="first"/>
<deadCode implementation="org.jetbrains.android.inspections.AndroidComponentEntryPoint"/>
<testSrcLocator implementation="org.jetbrains.android.run.testing.AndroidTestLocationProvider"/>
+ <methodConflictResolver implementation="com.android.tools.idea.psi.resolve.AndroidMethodConflictResolver"/>
</extensions>
<extensions defaultExtensionNs="com.intellij.properties">
diff --git a/android/src/com/android/tools/idea/avdmanager/AvdActionPanel.java b/android/src/com/android/tools/idea/avdmanager/AvdActionPanel.java
index ce6aa97..e9d5179 100644
--- a/android/src/com/android/tools/idea/avdmanager/AvdActionPanel.java
+++ b/android/src/com/android/tools/idea/avdmanager/AvdActionPanel.java
@@ -109,7 +109,7 @@
myOverflowMenuButton.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
- myOverflowMenu.show(myOverflowMenuButton, e.getX(), e.getY());
+ myOverflowMenu.show(myOverflowMenuButton, e.getX() - myOverflowMenu.getPreferredSize().width, e.getY());
}
});
}
diff --git a/android/src/com/android/tools/idea/avdmanager/AvdConfigurationOptionHelpPanel.java b/android/src/com/android/tools/idea/avdmanager/AvdConfigurationOptionHelpPanel.java
index f208915..19d6bde 100644
--- a/android/src/com/android/tools/idea/avdmanager/AvdConfigurationOptionHelpPanel.java
+++ b/android/src/com/android/tools/idea/avdmanager/AvdConfigurationOptionHelpPanel.java
@@ -153,6 +153,7 @@
put(USE_SNAPSHOT_KEY, "Enable Snapshot").
put(CUSTOM_SKIN_FILE_KEY, "Custom Hardware Skin").
put(DISPLAY_NAME_KEY, "AVD Name").
+ put(HAS_HARDWARE_KEYBOARD_KEY, "Enable keyboard input").
build();
private static Map<Key<?>, String> DESCRIPTIONS = ImmutableMap.<Key<?>, String>builder().
@@ -191,6 +192,8 @@
"several \"layouts\" (e.g. \"landscape\" and \"portrait\") corresponding to different orientation " +
"/ physical configurations of the emulated device.\n").
put(DISPLAY_NAME_KEY, "The name of this AVD.").
+ put(HAS_HARDWARE_KEYBOARD_KEY, "Enables you to enter text input and interact with the AVD with your hardware computer keyboard " +
+ "instead of a of the on on-screen software keyboard.\n").
build();
/**
diff --git a/android/src/com/android/tools/idea/avdmanager/AvdDisplayList.java b/android/src/com/android/tools/idea/avdmanager/AvdDisplayList.java
index f4c4b89..fe90b55 100644
--- a/android/src/com/android/tools/idea/avdmanager/AvdDisplayList.java
+++ b/android/src/com/android/tools/idea/avdmanager/AvdDisplayList.java
@@ -107,6 +107,7 @@
newButton.setText(createAvdAction.getText());
southPanel.add(newButton, BorderLayout.WEST);
nonemptyPanel.add(southPanel, BorderLayout.SOUTH);
+ myTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
myTable.getSelectionModel().addListSelectionListener(this);
myTable.addMouseListener(myEditingListener);
myTable.addMouseMotionListener(myEditingListener);
diff --git a/android/src/com/android/tools/idea/avdmanager/AvdEditWizard.java b/android/src/com/android/tools/idea/avdmanager/AvdEditWizard.java
index 13cd6c0..18f2bf4 100644
--- a/android/src/com/android/tools/idea/avdmanager/AvdEditWizard.java
+++ b/android/src/com/android/tools/idea/avdmanager/AvdEditWizard.java
@@ -139,6 +139,7 @@
state.put(BACK_CAMERA_KEY, properties.get(BACK_CAMERA_KEY.name));
state.put(NETWORK_LATENCY_KEY, properties.get(NETWORK_LATENCY_KEY.name));
state.put(NETWORK_SPEED_KEY, properties.get(NETWORK_SPEED_KEY.name));
+ state.put(HAS_HARDWARE_KEYBOARD_KEY, fromIniString(properties.get(HAS_HARDWARE_KEYBOARD_KEY.name)));
state.put(DISPLAY_NAME_KEY, AvdManagerConnection.getAvdDisplayName(avdInfo));
String skinPath = properties.get(CUSTOM_SKIN_FILE_KEY.name);
diff --git a/android/src/com/android/tools/idea/avdmanager/AvdManagerConnection.java b/android/src/com/android/tools/idea/avdmanager/AvdManagerConnection.java
index 8b2d29d..9d1a870 100644
--- a/android/src/com/android/tools/idea/avdmanager/AvdManagerConnection.java
+++ b/android/src/com/android/tools/idea/avdmanager/AvdManagerConnection.java
@@ -367,7 +367,7 @@
if (currentInfo != null) {
avdFolder = new File(currentInfo.getDataFolderPath());
} else {
- avdFolder = AvdInfo.getDefaultAvdFolder(ourAvdManager, avdName);
+ avdFolder = AvdInfo.getDefaultAvdFolder(ourAvdManager, avdName, true);
}
}
catch (AndroidLocation.AndroidLocationException e) {
diff --git a/android/src/com/android/tools/idea/avdmanager/AvdWizardConstants.java b/android/src/com/android/tools/idea/avdmanager/AvdWizardConstants.java
index 1e6519f..37c9db1 100644
--- a/android/src/com/android/tools/idea/avdmanager/AvdWizardConstants.java
+++ b/android/src/com/android/tools/idea/avdmanager/AvdWizardConstants.java
@@ -25,6 +25,7 @@
import com.android.sdklib.devices.Screen;
import com.android.sdklib.devices.Storage;
import com.android.sdklib.internal.avd.AvdManager;
+import com.android.sdklib.internal.avd.HardwareProperties;
import com.android.sdklib.internal.repository.packages.*;
import com.android.sdklib.repository.descriptors.IdDisplay;
import com.android.sdklib.repository.descriptors.PkgDesc;
@@ -93,7 +94,7 @@
public static final Key<Integer> RESOLUTION_HEIGHT_KEY = createKey("ResolutionHeight", STEP, Integer.class);
public static final Key<Boolean> HAS_HARDWARE_BUTTONS_KEY = createKey("HasHardwareButtons", STEP, Boolean.class);
- public static final Key<Boolean> HAS_HARDWARE_KEYBOARD_KEY = createKey("HasHardwareKeyboard", STEP, Boolean.class);
+ public static final Key<Boolean> HAS_HARDWARE_KEYBOARD_KEY = createKey(HardwareProperties.HW_KEYBOARD, WIZARD, Boolean.class);
public static final Key<Navigation> NAVIGATION_KEY = createKey("Navigation", STEP, Navigation.class);
public static final Key<Boolean> SUPPORTS_LANDSCAPE_KEY = createKey("SupportsLandscape", STEP, Boolean.class);
diff --git a/android/src/com/android/tools/idea/avdmanager/ConfigureAvdOptionsStep.form b/android/src/com/android/tools/idea/avdmanager/ConfigureAvdOptionsStep.form
index 51584d0..7c35a75 100644
--- a/android/src/com/android/tools/idea/avdmanager/ConfigureAvdOptionsStep.form
+++ b/android/src/com/android/tools/idea/avdmanager/ConfigureAvdOptionsStep.form
@@ -19,7 +19,7 @@
</properties>
<border type="none"/>
<children>
- <grid id="27dc6" layout-manager="GridLayoutManager" row-count="13" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="5">
+ <grid id="27dc6" layout-manager="GridLayoutManager" row-count="14" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="5">
<margin top="3" left="3" bottom="3" right="10"/>
<constraints/>
<properties/>
@@ -84,7 +84,7 @@
<grid id="6207e" binding="myStartupOptionsPanel" layout-manager="GridLayoutManager" row-count="3" 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>
- <grid row="7" column="0" row-span="1" col-span="3" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+ <grid row="7" column="0" row-span="1" col-span="3" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="true"/>
</constraints>
<properties/>
<border type="none"/>
@@ -143,7 +143,7 @@
<grid id="f7319" binding="myCameraPanel" layout-manager="GridLayoutManager" row-count="4" 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>
- <grid row="11" column="0" row-span="1" col-span="3" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+ <grid row="11" column="0" row-span="1" col-span="3" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="true"/>
</constraints>
<properties/>
<border type="none"/>
@@ -223,7 +223,7 @@
<grid id="62a06" binding="myNetworkPanel" layout-manager="GridLayoutManager" row-count="2" 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>
- <grid row="12" column="0" row-span="1" col-span="3" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+ <grid row="13" column="0" row-span="1" col-span="3" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="true"/>
</constraints>
<properties/>
<border type="none"/>
@@ -296,14 +296,14 @@
<grid id="38c9" binding="myPerformancePanel" layout-manager="GridLayoutManager" row-count="3" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
- <grid row="8" column="0" row-span="1" col-span="3" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+ <grid row="8" column="0" row-span="1" col-span="3" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="true"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="937ef" class="com.intellij.ui.components.JBLabel">
<constraints>
- <grid row="0" column="0" row-span="3" col-span="1" vsize-policy="0" hsize-policy="2" anchor="9" fill="0" indent="0" use-parent-layout="false">
+ <grid row="0" column="0" row-span="3" col-span="1" vsize-policy="0" hsize-policy="3" anchor="9" fill="0" indent="0" use-parent-layout="false">
<preferred-size width="100" height="-1"/>
</grid>
</constraints>
@@ -331,7 +331,7 @@
</component>
<component id="9397d" class="com.intellij.ui.components.JBLabel">
<constraints>
- <grid row="2" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ <grid row="2" 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>
<enabled value="false"/>
@@ -368,7 +368,7 @@
<grid id="70d60" binding="myMemoryAndStoragePanel" layout-manager="GridLayoutManager" row-count="6" 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>
- <grid row="10" column="0" row-span="1" col-span="3" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+ <grid row="10" column="0" row-span="1" col-span="3" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="true"/>
</constraints>
<properties/>
<border type="none"/>
@@ -387,7 +387,7 @@
</component>
<component id="57934" class="com.intellij.ui.components.JBLabel" binding="myRamLabel">
<constraints>
- <grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ <grid row="1" 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="RAM:"/>
@@ -395,7 +395,7 @@
</component>
<component id="6b1e9" class="com.intellij.ui.components.JBLabel" binding="myVmHeapLabel">
<constraints>
- <grid row="2" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ <grid row="2" 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="VM heap:"/>
@@ -403,7 +403,7 @@
</component>
<component id="89fc4" class="com.intellij.ui.components.JBLabel" binding="myInternalStorageLabel">
<constraints>
- <grid row="3" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ <grid row="3" 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="Internal Storage:"/>
@@ -434,7 +434,7 @@
</grid>
<component id="95158" class="com.intellij.ui.components.JBLabel" binding="mySdCardLabel">
<constraints>
- <grid row="4" column="1" row-span="2" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ <grid row="4" column="1" row-span="2" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<anchor value="e383a"/>
@@ -482,14 +482,14 @@
<grid id="92d7e" binding="mySkinPanel" layout-manager="GridLayoutManager" row-count="4" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
- <grid row="9" column="0" row-span="1" col-span="3" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+ <grid row="9" column="0" row-span="1" col-span="3" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="true"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="14ec1" class="com.intellij.ui.components.JBLabel" binding="mySkinDefinitionLabel">
<constraints>
- <grid row="1" column="0" row-span="3" col-span="1" vsize-policy="0" hsize-policy="0" anchor="9" fill="0" indent="0" use-parent-layout="false">
+ <grid row="1" column="0" row-span="3" col-span="1" vsize-policy="0" hsize-policy="3" anchor="9" fill="0" indent="0" use-parent-layout="false">
<preferred-size width="100" height="-1"/>
</grid>
</constraints>
@@ -505,13 +505,45 @@
</component>
<component id="23213" class="com.android.tools.idea.avdmanager.SkinChooser" binding="mySkinComboBox" custom-create="true">
<constraints>
- <grid row="1" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
+ <grid row="1" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
</component>
<component id="61e9" class="com.intellij.ui.HyperlinkLabel" binding="myHardwareSkinHelpLabel" custom-create="true">
<constraints>
- <grid row="2" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ <grid row="2" 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/>
+ </component>
+ </children>
+ </grid>
+ <grid id="f9eac" binding="myKeyboardPanel" layout-manager="GridLayoutManager" row-count="2" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+ <margin top="0" left="0" bottom="0" right="0"/>
+ <constraints>
+ <grid row="12" column="0" row-span="1" col-span="3" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="true"/>
+ </constraints>
+ <properties/>
+ <border type="none"/>
+ <children>
+ <component id="3bf7b" class="com.intellij.ui.components.JBLabel">
+ <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 value="Keyboard"/>
+ </properties>
+ </component>
+ <component id="29def" class="javax.swing.JCheckBox" binding="myEnableComputerKeyboard">
+ <constraints>
+ <grid row="0" 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="Enable keyboard input"/>
+ </properties>
+ </component>
+ <component id="6abc5" class="javax.swing.JSeparator">
+ <constraints>
+ <grid row="1" column="0" row-span="1" col-span="2" vsize-policy="1" hsize-policy="3" anchor="1" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
</component>
diff --git a/android/src/com/android/tools/idea/avdmanager/ConfigureAvdOptionsStep.java b/android/src/com/android/tools/idea/avdmanager/ConfigureAvdOptionsStep.java
index 5d04cf8..843af6b 100644
--- a/android/src/com/android/tools/idea/avdmanager/ConfigureAvdOptionsStep.java
+++ b/android/src/com/android/tools/idea/avdmanager/ConfigureAvdOptionsStep.java
@@ -17,6 +17,7 @@
import com.android.SdkConstants;
import com.android.resources.Density;
+import com.android.resources.Keyboard;
import com.android.resources.ScreenOrientation;
import com.android.resources.ScreenSize;
import com.android.sdklib.devices.CameraLocation;
@@ -59,6 +60,7 @@
import static com.android.sdklib.devices.Storage.Unit;
import static com.android.tools.idea.avdmanager.AvdWizardConstants.*;
import static com.android.tools.idea.wizard.ScopedStateStore.Key;
+import static com.android.tools.idea.wizard.ScopedStateStore.Scope.WIZARD;
import static com.android.tools.idea.wizard.ScopedStateStore.createKey;
/**
@@ -112,6 +114,8 @@
private SkinChooser mySkinComboBox;
private JPanel myAvdDisplayNamePanel;
private JBLabel myAvdNameLabel;
+ private JCheckBox myEnableComputerKeyboard;
+ private JPanel myKeyboardPanel;
private Set<JComponent> myAdvancedOptionsComponents;
private String myOriginalName;
@@ -198,8 +202,8 @@
registerComponents();
deregister(getDescriptionText());
getDescriptionText().setVisible(false);
+ Device device = myState.get(DEVICE_DEFINITION_KEY);
if (myState.get(DISPLAY_NAME_KEY) == null || myState.get(DISPLAY_NAME_KEY).isEmpty()) {
- Device device = myState.get(DEVICE_DEFINITION_KEY);
SystemImageDescription systemImage = myState.get(SYSTEM_IMAGE_KEY);
assert device != null && systemImage != null;
String avdName = String.format(Locale.getDefault(), "%1$s API %2$d", device.getDisplayName(), systemImage.getVersion().getApiLevel());
@@ -212,13 +216,14 @@
myOriginalName = editMode ? myState.get(AvdWizardConstants.DISPLAY_NAME_KEY) : "";
File skinPath = myState.get(CUSTOM_SKIN_FILE_KEY);
- if (skinPath == null && !editMode) {
- Device device = myState.get(DEVICE_DEFINITION_KEY);
- if (device != null) {
- skinPath = AvdEditWizard.getHardwareSkinPath(device.getDefaultHardware());
- }
+ if (skinPath == null && !editMode && device != null) {
+ skinPath = AvdEditWizard.getHardwareSkinPath(device.getDefaultHardware());
}
myState.put(CUSTOM_SKIN_FILE_KEY, skinPath != null ? skinPath : NO_SKIN);
+
+ if (device.getDefaultHardware().getKeyboard().equals(Keyboard.QWERTY)) {
+ myEnableComputerKeyboard.setEnabled(false);
+ }
}
private static String uniquifyDisplayName(String name) {
@@ -573,6 +578,12 @@
myState.put(CUSTOM_SKIN_FILE_KEY, skinFile);
setControlDescription(mySkinComboBox, myAvdConfigurationOptionHelpPanel.getDescription(CUSTOM_SKIN_FILE_KEY));
+ if (!myState.containsKey(HAS_HARDWARE_KEYBOARD_KEY)) {
+ myState.put(HAS_HARDWARE_KEYBOARD_KEY, true);
+ }
+ register(HAS_HARDWARE_KEYBOARD_KEY, myEnableComputerKeyboard);
+ setControlDescription(myEnableComputerKeyboard, myAvdConfigurationOptionHelpPanel.getDescription(HAS_HARDWARE_KEYBOARD_KEY));
+
invokeUpdate(null);
}
@@ -718,8 +729,8 @@
};
private void registerAdvancedOptionsVisibility() {
- myAdvancedOptionsComponents =
- ImmutableSet.<JComponent>of(myMemoryAndStoragePanel, myCameraPanel, myNetworkPanel, mySkinPanel, myAvdIdLabel, myAvdId);
+ myAdvancedOptionsComponents = ImmutableSet.<JComponent>of(myMemoryAndStoragePanel, myCameraPanel, myNetworkPanel,
+ mySkinPanel, myAvdIdLabel, myAvdId, myKeyboardPanel);
}
/**
diff --git a/android/src/com/android/tools/idea/avdmanager/SystemImageList.java b/android/src/com/android/tools/idea/avdmanager/SystemImageList.java
index 097370a..0c35e96 100644
--- a/android/src/com/android/tools/idea/avdmanager/SystemImageList.java
+++ b/android/src/com/android/tools/idea/avdmanager/SystemImageList.java
@@ -26,19 +26,20 @@
import com.android.sdklib.repository.descriptors.PkgDesc;
import com.android.sdklib.repository.descriptors.PkgType;
import com.android.sdklib.repository.local.LocalSdk;
+import com.android.sdklib.repository.remote.RemotePkgInfo;
import com.android.sdklib.repository.remote.RemoteSdk;
import com.android.tools.idea.sdk.wizard.SdkQuickfixWizard;
import com.android.utils.ILogger;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Comparing;
-import com.intellij.ui.IdeBorderFactory;
-import com.intellij.ui.JBColor;
-import com.intellij.ui.ScrollPaneFactory;
+import com.intellij.ui.*;
import com.intellij.ui.SingleSelectionModel;
import com.intellij.ui.components.JBCheckBox;
import com.intellij.ui.components.JBLabel;
@@ -60,10 +61,8 @@
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableRowSorter;
import java.awt.*;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
+import java.awt.event.*;
+import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.util.*;
import java.util.List;
@@ -114,11 +113,13 @@
myTable.setSelectionModel(new SingleSelectionModel() {
@Override
public void setSelectionInterval(int index0, int index1) {
- AvdWizardConstants.SystemImageDescription description = myTable.getRow(index0);
- if (description == null || description.isRemote()) {
- return;
- }
super.setSelectionInterval(index0, index1);
+ TableCellEditor editor = myTable.getCellEditor();
+ if (editor != null) {
+ editor.cancelCellEditing();
+ }
+ myTable.repaint();
+ possiblySwitchEditors(index0, 0);
}
});
myTable.setRowSelectionAllowed(true);
@@ -234,17 +235,13 @@
Point p = e.getPoint();
int row = myTable.rowAtPoint(p);
int col = myTable.columnAtPoint(p);
+ possiblySwitchEditors(row, col);
+ }
+
+ private void possiblySwitchEditors(int row, int col) {
if (row != myTable.getEditingRow() || col != myTable.getEditingColumn()) {
- if (myTable.isEditing()) {
- TableCellEditor editor = myTable.getCellEditor();
- if (!editor.stopCellEditing()) {
- editor.cancelCellEditing();
- }
- }
- if (!myTable.isEditing()) {
- if (row != -1 && col != -1 && myTable.isCellEditable(row, col)) {
- myTable.editCellAt(row, col);
- }
+ if (row != -1 && col != -1 && myTable.isCellEditable(row, col)) {
+ myTable.editCellAt(row, col);
}
}
}
@@ -296,24 +293,30 @@
}
SdkSources sources = myRemoteSdk.fetchSources(RemoteSdk.DEFAULT_EXPIRATION_PERIOD_MS, ILOG);
- myRemoteSdk.fetch(sources, ILOG);
- for (SdkSource source : sources.getAllSources()) {
- for (com.android.sdklib.internal.repository.packages.Package pack : source.getPackages()) {
- if (!(pack instanceof SystemImagePackage)) {
- continue;
- }
- AvdWizardConstants.SystemImageDescription desc = new AvdWizardConstants.SystemImageDescription(pack);
- ImageFingerprint probe = new ImageFingerprint();
- probe.version = desc.getVersion();
- probe.tag = desc.getTag();
- probe.abiType = desc.getAbiType();
- // If we don't have a filter or this image passes the filter
- if (!seen.contains(probe) && (myFilter == null || myFilter.apply(desc))) {
- items.add(desc);
+ Multimap<PkgType, RemotePkgInfo> packages = myRemoteSdk.fetch(sources, ILOG);
+ if (packages.isEmpty()) {
+ // Couldn't get packages. Disable display.
+ myShowRemoteCheckbox.setEnabled(false);
+ myShowRemoteCheckbox.setSelected(false);
+ }
+ else {
+ for (SdkSource source : sources.getAllSources()) {
+ for (com.android.sdklib.internal.repository.packages.Package pack : source.getPackages()) {
+ if (!(pack instanceof SystemImagePackage)) {
+ continue;
+ }
+ AvdWizardConstants.SystemImageDescription desc = new AvdWizardConstants.SystemImageDescription(pack);
+ ImageFingerprint probe = new ImageFingerprint();
+ probe.version = desc.getVersion();
+ probe.tag = desc.getTag();
+ probe.abiType = desc.getAbiType();
+ // If we don't have a filter or this image passes the filter
+ if (!seen.contains(probe) && (myFilter == null || myFilter.apply(desc))) {
+ items.add(desc);
+ }
}
}
}
-
myModel.setItems(items);
}
@@ -361,6 +364,9 @@
@Override
public void valueChanged(ListSelectionEvent e) {
AvdWizardConstants.SystemImageDescription selected = myTable.getSelectedObject();
+ if (selected != null && selected.isRemote()) {
+ selected = null;
+ }
for (SystemImageSelectionListener listener : myListeners) {
listener.onSystemImageSelected(selected);
}
@@ -465,8 +471,12 @@
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
- if (table.getSelectedRow() == row) {
- panel.setBackground(table.getSelectionBackground());
+ if (isSelected) {
+ if (image.isRemote()) {
+ panel.setBackground(UIUtil.getListUnfocusedSelectionBackground());
+ } else {
+ panel.setBackground(table.getSelectionBackground());
+ }
panel.setForeground(table.getSelectionForeground());
panel.setOpaque(true);
}
@@ -481,7 +491,8 @@
label.setFont(labelFont.deriveFont(Font.BOLD));
}
if (image.isRemote()) {
- label.setFont(labelFont.deriveFont(label.getFont().getStyle() | Font.ITALIC));
+ Font font = labelFont.deriveFont(label.getFont().getStyle() | Font.ITALIC);
+ label.setFont(font);
label.setForeground(UIUtil.getLabelDisabledForeground());
// on OS X the actual text width isn't computed correctly. Compensating for that..
if (!label.getText().isEmpty()) {
@@ -492,32 +503,32 @@
label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, offset));
}
}
+ panel.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyTyped(KeyEvent e) {
+ if (e.getKeyChar() == KeyEvent.VK_ENTER || e.getKeyChar() == KeyEvent.VK_SPACE) {
+ downloadImage(image);
+ }
+ }
+ });
}
panel.add(label);
if (image.isRemote() && column == 0) {
- JBLabel link = new JBLabel("Download");
+ final JBLabel link = new JBLabel("Download");
link.setBackground(table.getBackground());
link.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
link.setForeground(JBColor.BLUE);
+ Font font = link.getFont();
+ if (isSelected) {
+ Map<TextAttribute, Integer> attrs = Maps.newHashMap();
+ attrs.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
+ font = font.deriveFont(attrs);
+ }
+ link.setFont(font);
link.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
- IPkgDesc remote = image.getRemotePackage().getPkgDesc();
- IPkgDesc request = null;
- if (remote.getType().equals(PkgType.PKG_SYS_IMAGE)) {
- request =
- PkgDesc.Builder.newSysImg(remote.getAndroidVersion(), remote.getTag(), remote.getPath(), remote.getMajorRevision())
- .create();
- }
- else if (remote.getType().equals(PkgType.PKG_ADDON_SYS_IMAGE)) {
- request = PkgDesc.Builder.newAddonSysImg(image.getVersion(), remote.getVendor(), image.getTag(), image.getAbiType(),
- (MajorRevision)image.getRemotePackage().getRevision()).create();
- }
- List<IPkgDesc> requestedPackages = Lists.newArrayList(request);
- SdkQuickfixWizard sdkQuickfixWizard = new SdkQuickfixWizard(null, null, requestedPackages);
- sdkQuickfixWizard.init();
- sdkQuickfixWizard.show();
- refreshImages(true);
+ downloadImage(image);
}
});
panel.add(link);
@@ -542,6 +553,25 @@
}
+ private void downloadImage(AvdWizardConstants.SystemImageDescription image) {
+ IPkgDesc remote = image.getRemotePackage().getPkgDesc();
+ IPkgDesc request = null;
+ if (remote.getType().equals(PkgType.PKG_SYS_IMAGE)) {
+ request =
+ PkgDesc.Builder.newSysImg(remote.getAndroidVersion(), remote.getTag(), remote.getPath(), remote.getMajorRevision())
+ .create();
+ }
+ else if (remote.getType().equals(PkgType.PKG_ADDON_SYS_IMAGE)) {
+ request = PkgDesc.Builder.newAddonSysImg(image.getVersion(), remote.getVendor(), image.getTag(), image.getAbiType(),
+ (MajorRevision)image.getRemotePackage().getRevision()).create();
+ }
+ List<IPkgDesc> requestedPackages = Lists.newArrayList(request);
+ SdkQuickfixWizard sdkQuickfixWizard = new SdkQuickfixWizard(null, null, requestedPackages);
+ sdkQuickfixWizard.init();
+ sdkQuickfixWizard.show();
+ refreshImages(true);
+ }
+
@Nullable
@Override
public Comparator<AvdWizardConstants.SystemImageDescription> getComparator() {
diff --git a/android/src/com/android/tools/idea/avdmanager/SystemImagePreview.java b/android/src/com/android/tools/idea/avdmanager/SystemImagePreview.java
index 2e6cbc8..c244470 100644
--- a/android/src/com/android/tools/idea/avdmanager/SystemImagePreview.java
+++ b/android/src/com/android/tools/idea/avdmanager/SystemImagePreview.java
@@ -217,7 +217,7 @@
// Paint the help link
g2d.setFont(AvdWizardConstants.STANDARD_FONT);
g2d.setColor(JBColor.BLUE);
- g2d.drawString("? See documentation for Android " + myDistribution.getVersion().toShortString() + " APIs", PADDING,
+ g2d.drawString("? - See documentation for Android " + myDistribution.getVersion().toShortString() + " APIs", PADDING,
getHeight() - PADDING);
}
}
diff --git a/android/src/com/android/tools/idea/designer/SegmentType.java b/android/src/com/android/tools/idea/designer/SegmentType.java
index 82e9ab9..7e08af9 100644
--- a/android/src/com/android/tools/idea/designer/SegmentType.java
+++ b/android/src/com/android/tools/idea/designer/SegmentType.java
@@ -16,6 +16,7 @@
package com.android.tools.idea.designer;
import com.intellij.android.designer.model.RadViewComponent;
+import com.intellij.android.designer.model.layout.TextDirection;
import com.intellij.designer.utils.Position;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -27,6 +28,14 @@
*/
public enum SegmentType {
/**
+ * Segment is on start side (left if LTR, right if RTL).
+ */
+ @NotNull START,
+ /**
+ * Segment is on end side (right if LTR, left if RTL).
+ */
+ @NotNull END,
+ /**
* Segment is on the left edge
*/
@NotNull LEFT,
@@ -70,11 +79,23 @@
* @param bounds the bounds of the node
* @return the X coordinate for an edge of this type given its bounds
*/
- public int getX(@SuppressWarnings("UnusedParameters") @Nullable RadViewComponent node, @NotNull Rectangle bounds) {
+ public int getX(@NotNull TextDirection textDirection,
+ @SuppressWarnings("UnusedParameters") @Nullable RadViewComponent node,
+ @NotNull Rectangle bounds) {
+ SegmentType me = this;
+ switch(this) {
+ case START:
+ me = textDirection == TextDirection.RIGHT_TO_LEFT ? RIGHT : LEFT;
+ break;
+ case END:
+ me = textDirection == TextDirection.RIGHT_TO_LEFT ? LEFT : RIGHT;
+ break;
+ }
+
// We pass in the bounds rather than look it up via node.getBounds() because
// during a resize or move operation, we call this method to look up proposed
// bounds rather than actual bounds
- switch (this) {
+ switch (me) {
case RIGHT:
return bounds.x + bounds.width;
case TOP:
diff --git a/android/src/com/android/tools/idea/gradle/GradleSyncState.java b/android/src/com/android/tools/idea/gradle/GradleSyncState.java
index 07f821f..c38285f 100644
--- a/android/src/com/android/tools/idea/gradle/GradleSyncState.java
+++ b/android/src/com/android/tools/idea/gradle/GradleSyncState.java
@@ -163,7 +163,6 @@
}
}
- notifications.updateAllNotifications();
BuildVariantView.getInstance(myProject).updateContents();
}
});
diff --git a/android/src/com/android/tools/idea/gradle/eclipse/AdtImportSdkStep.java b/android/src/com/android/tools/idea/gradle/eclipse/AdtImportSdkStep.java
index cf41d87..c4785f2b 100644
--- a/android/src/com/android/tools/idea/gradle/eclipse/AdtImportSdkStep.java
+++ b/android/src/com/android/tools/idea/gradle/eclipse/AdtImportSdkStep.java
@@ -32,7 +32,7 @@
super(context);
// TODO: Pass in a context here which allows the configurable to request
// validation when the text field is edited
- myConfigurable = new DefaultSdksConfigurable(null);
+ myConfigurable = new DefaultSdksConfigurable(null, null);
}
@Override
diff --git a/android/src/com/android/tools/idea/gradle/eclipse/GradleImport.java b/android/src/com/android/tools/idea/gradle/eclipse/GradleImport.java
index 2295ee3..ac88b66 100755
--- a/android/src/com/android/tools/idea/gradle/eclipse/GradleImport.java
+++ b/android/src/com/android/tools/idea/gradle/eclipse/GradleImport.java
@@ -343,7 +343,7 @@
* @return true if it is known to be a source file
*/
@VisibleForTesting
- static boolean isTextFile(@NonNull File file) {
+ public static boolean isTextFile(@NonNull File file) {
String name = file.getName();
return name.endsWith(DOT_JAVA) ||
name.endsWith(DOT_XML) ||
@@ -1491,7 +1491,7 @@
@Nullable CopyHandler handler,
boolean updateEncoding,
@Nullable ImportModule sourceModule) throws IOException {
- if (handler != null && handler.handle(source, dest)) {
+ if (handler != null && handler.handle(source, dest, updateEncoding, sourceModule)) {
return;
}
if (source.isDirectory()) {
@@ -1617,8 +1617,14 @@
/**
* Optionally handle the given file; returns true if the file has been
* handled
+ *
+ * @param source the source file/directory to copy
+ * @param dest the destination for that file
+ * @param updateEncoding if false, do not try to rewrite encodings to UTF-8
+ * @param sourceModule if non null, a corresponding module this source file belongs to
*/
- boolean handle(@NonNull File source, @NonNull File dest) throws IOException;
+ boolean handle(@NonNull File source, @NonNull File dest, boolean updateEncoding, @Nullable ImportModule sourceModule)
+ throws IOException;
}
private static class ImportException extends RuntimeException {
diff --git a/android/src/com/android/tools/idea/gradle/eclipse/ImportModule.java b/android/src/com/android/tools/idea/gradle/eclipse/ImportModule.java
index fa7f202..d99dfc3 100644
--- a/android/src/com/android/tools/idea/gradle/eclipse/ImportModule.java
+++ b/android/src/com/android/tools/idea/gradle/eclipse/ImportModule.java
@@ -20,6 +20,7 @@
import com.android.annotations.Nullable;
import com.android.ide.common.repository.GradleCoordinate;
import com.android.ide.common.repository.SdkMavenRepository;
+import com.android.resources.ResourceFolderType;
import com.android.sdklib.AndroidVersion;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
@@ -31,9 +32,9 @@
import java.util.*;
import static com.android.SdkConstants.*;
-import static com.android.tools.idea.gradle.eclipse.GradleImport.ECLIPSE_DOT_CLASSPATH;
-import static com.android.tools.idea.gradle.eclipse.GradleImport.ECLIPSE_DOT_PROJECT;
+import static com.android.tools.idea.gradle.eclipse.GradleImport.*;
import static java.io.File.separator;
+import static java.io.File.separatorChar;
abstract class ImportModule implements Comparable<ImportModule> {
@SuppressWarnings("SpellCheckingInspection")
@@ -460,7 +461,44 @@
if (srcRes != null && srcRes.exists()) {
File destRes = new File(main, FD_RES);
myImporter.mkdirs(destRes);
- myImporter.copyDir(srcRes, destRes, null, true, this);
+ myImporter.copyDir(srcRes, destRes, new GradleImport.CopyHandler() {
+ @Override
+ public boolean handle(@NonNull File source, @NonNull File dest, boolean updateEncoding, @Nullable ImportModule sourceModule)
+ throws IOException {
+ // Resource files in non-value folders should use only lower case characters
+ if (hasUpperCaseExtension(dest) && !isIgnoredFile(source)) {
+ File parentFile = source.getParentFile();
+ if (parentFile != null) {
+ ResourceFolderType folderType = ResourceFolderType.getFolderType(parentFile.getName());
+ if (folderType != ResourceFolderType.VALUES) {
+ String name = dest.getName();
+ int dot = name.indexOf('.');
+ if (dot != -1) {
+ name = name.substring(0, dot) + name.substring(dot).toLowerCase(Locale.US);
+ File destParent = dest.getParentFile();
+ dest = destParent != null ? new File(destParent, name) : new File(name);
+ if (updateEncoding && isTextFile(source)) {
+ myImporter.copyTextFile(sourceModule, source, dest);
+ } else {
+ Files.copy(source, dest);
+ }
+ if (sourceModule != null) {
+ // Just use the names rather than the full paths to make it clear that this was just
+ // a file renaming (even though there is also a move happening for all resources including
+ // these. In other words, instead of displaying
+ // * res/drawable-hdpi/other_icon.PNG => app/src/main/res/drawable-hdpi/other_icon.png
+ // we display
+ // * other_icon.PNG => app/src/main/res/drawable-hdpi/other_icon.png
+ myImporter.getSummary().reportMoved(sourceModule, new File(source.getName()), new File(name));
+ }
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+ }, true, this);
summary.reportMoved(this, srcRes, destRes);
recordCopiedFile(copied, srcRes);
}
@@ -499,7 +537,8 @@
// Handle moving .rs/.rsh/.fs files to main/rs/ and .aidl files to the
// corresponding aidl package under main/aidl
@Override
- public boolean handle(@NonNull File source, @NonNull File dest) throws IOException {
+ public boolean handle(@NonNull File source, @NonNull File dest, boolean updateEncoding, @Nullable ImportModule sourceModule)
+ throws IOException {
String sourcePath = source.getPath();
if (sourcePath.endsWith(DOT_AIDL)) {
File aidlDir = new File(main, FD_AIDL);
@@ -627,6 +666,25 @@
reportIgnored(copied);
}
+ private static boolean hasUpperCaseExtension(@NonNull File file) {
+ String path = file.getPath();
+ int index = path.lastIndexOf(separatorChar);
+ // Can be -1 (if this is just a file name, with no path); the below still works since we start from the next char (0)
+ index = path.indexOf('.', index + 1);
+ if (index == -1) {
+ return false; // no extension in the file name
+ }
+ index++;
+
+ for (; index < path.length(); index++) {
+ if (Character.isUpperCase(path.charAt(index))) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
private void reportIgnored(Set<File> copied) throws IOException {
File canonicalDir = getCanonicalModuleDir();
diff --git a/android/src/com/android/tools/idea/gradle/project/AndroidGradleProjectData.java b/android/src/com/android/tools/idea/gradle/project/AndroidGradleProjectData.java
index 7622bd8..4b425ab 100644
--- a/android/src/com/android/tools/idea/gradle/project/AndroidGradleProjectData.java
+++ b/android/src/com/android/tools/idea/gradle/project/AndroidGradleProjectData.java
@@ -247,7 +247,7 @@
return false;
}
- private static boolean needsAndroidSdkSync(@NotNull Project project) {
+ private static boolean needsAndroidSdkSync(@NotNull final Project project) {
if (AndroidStudioSpecificInitializer.isAndroidStudio()) {
final File ideSdkPath = DefaultSdks.getDefaultAndroidHome();
if (ideSdkPath != null) {
@@ -256,7 +256,7 @@
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
- DefaultSdks.setDefaultAndroidHome(ideSdkPath, DefaultSdks.getDefaultJdk());
+ DefaultSdks.setDefaultAndroidHome(ideSdkPath, DefaultSdks.getDefaultJdk(), project);
}
});
return true;
diff --git a/android/src/com/android/tools/idea/gradle/project/PostProjectSetupTasksExecutor.java b/android/src/com/android/tools/idea/gradle/project/PostProjectSetupTasksExecutor.java
index 0622adc..8d4df03 100755
--- a/android/src/com/android/tools/idea/gradle/project/PostProjectSetupTasksExecutor.java
+++ b/android/src/com/android/tools/idea/gradle/project/PostProjectSetupTasksExecutor.java
@@ -19,6 +19,7 @@
import com.android.builder.model.AndroidProject;
import com.android.sdklib.AndroidTargetHash;
import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.repository.FullRevision;
import com.android.sdklib.repository.descriptors.IPkgDesc;
import com.android.sdklib.repository.descriptors.PkgDesc;
@@ -48,6 +49,7 @@
import com.android.tools.idea.sdk.VersionCheck;
import com.android.tools.idea.sdk.wizard.SdkQuickfixWizard;
import com.android.tools.idea.startup.AndroidStudioSpecificInitializer;
+import com.android.tools.idea.startup.ExternalAnnotationsSupport;
import com.android.tools.idea.templates.TemplateManager;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
@@ -59,6 +61,7 @@
import com.intellij.openapi.externalSystem.util.ExternalSystemConstants;
import com.intellij.openapi.module.*;
import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.ProjectJdkTable;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.SdkAdditionalData;
import com.intellij.openapi.projectRoots.SdkModificator;
@@ -66,6 +69,7 @@
import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTable;
+import com.intellij.openapi.roots.libraries.ui.OrderRoot;
import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.StandardFileSystems;
@@ -76,6 +80,8 @@
import com.intellij.util.io.URLUtil;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.sdk.AndroidSdkAdditionalData;
+import org.jetbrains.android.sdk.AndroidSdkData;
+import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.gradle.util.GradleConstants;
@@ -85,6 +91,7 @@
import java.util.List;
import java.util.Set;
+import static com.android.SdkConstants.FN_FRAMEWORK_LIBRARY;
import static com.android.tools.idea.gradle.messages.CommonMessageGroupNames.FAILED_TO_SET_UP_SDK;
import static com.android.tools.idea.gradle.service.notification.errors.AbstractSyncErrorHandler.FAILED_TO_SYNC_GRADLE_PROJECT_ERROR_GROUP_FORMAT;
import static com.android.tools.idea.gradle.util.Projects.hasErrors;
@@ -110,8 +117,23 @@
myProject = project;
}
-
public void onProjectRestoreFromDisk() {
+ ensureValidSdks();
+
+ if (hasErrors(myProject)) {
+ addSdkLinkIfNecessary();
+ checkSdkVersion(myProject);
+ return;
+ }
+
+ findAndShowVariantConflicts();
+ addSdkLinkIfNecessary();
+ checkSdkVersion(myProject);
+
+ TemplateManager.getInstance().refreshDynamicTemplateMenu(myProject);
+ }
+
+ public void ensureValidSdks() {
ProjectSyncMessages messages = ProjectSyncMessages.getInstance(myProject);
boolean checkJdkVersion = true;
@@ -124,7 +146,28 @@
if (androidFacet != null && androidFacet.getIdeaAndroidProject() != null) {
Sdk sdk = ModuleRootManager.getInstance(module).getSdk();
if (sdk != null && !invalidAndroidSdks.contains(sdk) && isMissingAndroidLibrary(sdk)) {
- invalidAndroidSdks.add(sdk);
+ // First try to recreate SDK; workaround for issue 78072
+ AndroidSdkAdditionalData additionalData = (AndroidSdkAdditionalData)sdk.getSdkAdditionalData();
+ AndroidSdkData sdkData = AndroidSdkData.getSdkData(sdk);
+ if (additionalData != null && sdkData != null) {
+ IAndroidTarget target = additionalData.getBuildTarget(sdkData);
+ if (target != null) {
+ SdkModificator sdkModificator = sdk.getSdkModificator();
+ sdkModificator.removeAllRoots();
+ for (OrderRoot orderRoot : AndroidSdkUtils.getLibraryRootsForTarget(target, sdk.getHomePath(), true)) {
+ sdkModificator.addRoot(orderRoot.getFile(), orderRoot.getType());
+ }
+ ExternalAnnotationsSupport.attachJdkAnnotations(sdkModificator);
+ sdkModificator.commitChanges();
+ }
+ }
+
+ // If attempting to fix up the roots in the SDK fails, install the target over again
+ // (this is a truly corrupt install, as opposed to an incorrectly synced SDK which the
+ // above workaround deals with)
+ if (isMissingAndroidLibrary(sdk)) {
+ invalidAndroidSdks.add(sdk);
+ }
}
IdeaAndroidProject androidProject = androidFacet.getIdeaAndroidProject();
@@ -143,21 +186,9 @@
}
}
- if (hasErrors(myProject)) {
- addSdkLinkIfNecessary();
- checkSdkVersion(myProject);
- return;
- }
-
if (!invalidAndroidSdks.isEmpty()) {
reinstallMissingPlatforms(invalidAndroidSdks);
}
-
- findAndShowVariantConflicts();
- addSdkLinkIfNecessary();
- checkSdkVersion(myProject);
-
- TemplateManager.getInstance().refreshDynamicTemplateMenu(myProject);
}
private static boolean isMissingAndroidLibrary(@NotNull Sdk sdk) {
@@ -165,12 +196,12 @@
for (VirtualFile library : sdk.getRootProvider().getFiles(OrderRootType.CLASSES)) {
// This code does not through the classes in the Android SDK. It iterates through a list of 3 files in the IDEA SDK: android.jar,
// annotations.jar and res folder.
- if (!library.exists()) {
- return true;
+ if (library.getName().equals(FN_FRAMEWORK_LIBRARY) && library.exists()) {
+ return false;
}
}
}
- return false;
+ return true;
}
private void reinstallMissingPlatforms(@NotNull Collection<Sdk> invalidAndroidSdks) {
@@ -245,6 +276,8 @@
myGenerateSourcesAfterSync = DEFAULT_GENERATE_SOURCES_AFTER_SYNC;
}
+ ensureValidSdks();
+
TemplateManager.getInstance().refreshDynamicTemplateMenu(myProject);
}
diff --git a/android/src/com/android/tools/idea/gradle/project/SdkSync.java b/android/src/com/android/tools/idea/gradle/project/SdkSync.java
index 2d455ad..20986fa 100644
--- a/android/src/com/android/tools/idea/gradle/project/SdkSync.java
+++ b/android/src/com/android/tools/idea/gradle/project/SdkSync.java
@@ -133,7 +133,7 @@
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
- DefaultSdks.setDefaultAndroidHome(projectAndroidHomePath);
+ DefaultSdks.setDefaultAndroidHome(projectAndroidHomePath, null);
}
});
}
diff --git a/android/src/com/android/tools/idea/gradle/service/notification/errors/MissingPlatformErrorHandler.java b/android/src/com/android/tools/idea/gradle/service/notification/errors/MissingPlatformErrorHandler.java
index 6b9c43b..8235ec1 100644
--- a/android/src/com/android/tools/idea/gradle/service/notification/errors/MissingPlatformErrorHandler.java
+++ b/android/src/com/android/tools/idea/gradle/service/notification/errors/MissingPlatformErrorHandler.java
@@ -20,8 +20,8 @@
import com.android.sdklib.repository.descriptors.PkgType;
import com.android.sdklib.repository.local.LocalPkgInfo;
import com.android.sdklib.repository.local.LocalSdk;
-import com.android.tools.idea.gradle.service.notification.hyperlink.NotificationHyperlink;
import com.android.tools.idea.gradle.service.notification.hyperlink.InstallPlatformHyperlink;
+import com.android.tools.idea.gradle.service.notification.hyperlink.NotificationHyperlink;
import com.android.tools.idea.gradle.service.notification.hyperlink.OpenAndroidSdkManagerHyperlink;
import com.google.common.collect.Lists;
import com.intellij.facet.ProjectFacetManager;
@@ -75,9 +75,7 @@
if (pkgInfo != null) {
loadError = pkgInfo.getLoadError();
}
- if (pkgInfo == null || StringUtil.isNotEmpty(loadError)) {
- hyperlinks.add(new InstallPlatformHyperlink(version));
- }
+ hyperlinks.add(new InstallPlatformHyperlink(version));
}
}
if (hyperlinks.isEmpty()) {
diff --git a/android/src/com/android/tools/idea/gradle/structure/AndroidProjectStructureConfigurable.java b/android/src/com/android/tools/idea/gradle/structure/AndroidProjectStructureConfigurable.java
index d11deab..e93a431 100644
--- a/android/src/com/android/tools/idea/gradle/structure/AndroidProjectStructureConfigurable.java
+++ b/android/src/com/android/tools/idea/gradle/structure/AndroidProjectStructureConfigurable.java
@@ -100,7 +100,7 @@
@NotNull private final Wrapper myDetails = new Wrapper();
@NotNull private final UiState myUiState;
- @NotNull private final DefaultSdksConfigurable mySdksConfigurable = new DefaultSdksConfigurable(this);
+ @NotNull private final DefaultSdksConfigurable mySdksConfigurable;
@NotNull private final List<Configurable> myConfigurables = Lists.newLinkedList();
private final GradleSettingsFile mySettingsFile;
@@ -153,6 +153,7 @@
public AndroidProjectStructureConfigurable(@NotNull Project project) {
myProject = project;
myUiState = new UiState(project);
+ mySdksConfigurable = new DefaultSdksConfigurable(this, project);
myConfigurables.add(mySdksConfigurable);
if (!project.isDefault()) {
diff --git a/android/src/com/android/tools/idea/gradle/structure/DefaultSdksConfigurable.java b/android/src/com/android/tools/idea/gradle/structure/DefaultSdksConfigurable.java
index 931e696..1283db2 100644
--- a/android/src/com/android/tools/idea/gradle/structure/DefaultSdksConfigurable.java
+++ b/android/src/com/android/tools/idea/gradle/structure/DefaultSdksConfigurable.java
@@ -24,6 +24,7 @@
import com.intellij.openapi.fileChooser.PathChooserDialog;
import com.intellij.openapi.options.BaseConfigurable;
import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.JavaSdk;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.ui.DetailsComponent;
@@ -57,7 +58,9 @@
public class DefaultSdksConfigurable extends BaseConfigurable implements ValidationAwareConfigurable {
private static final String CHOOSE_VALID_JDK_DIRECTORY_ERR = "Please choose a valid JDK directory.";
private static final String CHOOSE_VALID_SDK_DIRECTORY_ERR = "Please choose a valid Android SDK directory.";
- private final ConfigurableHost myHost;
+
+ @Nullable private final ConfigurableHost myHost;
+ @Nullable private final Project myProject;
// These paths are system-dependent.
@NotNull private String myOriginalJdkHomePath;
@@ -69,8 +72,9 @@
private DetailsComponent myDetailsComponent;
- public DefaultSdksConfigurable(@Nullable ConfigurableHost host) {
+ public DefaultSdksConfigurable(@Nullable ConfigurableHost host, @Nullable Project project) {
myHost = host;
+ myProject = project;
myWholePanel.setPreferredSize(new Dimension(700, 500));
myDetailsComponent = new DetailsComponent();
@@ -96,7 +100,7 @@
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
- DefaultSdks.setDefaultAndroidHome(getSdkLocation());
+ DefaultSdks.setDefaultAndroidHome(getSdkLocation(), myProject);
DefaultSdks.setDefaultJavaHome(getJdkLocation());
if (!ApplicationManager.getApplication().isUnitTestMode()) {
diff --git a/android/src/com/android/tools/idea/gradle/util/GradleUtil.java b/android/src/com/android/tools/idea/gradle/util/GradleUtil.java
index aa0662b..891e81c 100644
--- a/android/src/com/android/tools/idea/gradle/util/GradleUtil.java
+++ b/android/src/com/android/tools/idea/gradle/util/GradleUtil.java
@@ -16,6 +16,7 @@
package com.android.tools.idea.gradle.util;
import com.android.SdkConstants;
+import com.android.annotations.NonNull;
import com.android.builder.model.*;
import com.android.ide.common.repository.GradleCoordinate;
import com.android.sdklib.repository.FullRevision;
@@ -869,4 +870,64 @@
}
return null;
}
+
+ /**
+ * Returns true if the given project depends on the given artifact, which consists of
+ * a group id and an artifact id, such as {@link com.android.SdkConstants#APPCOMPAT_LIB_ARTIFACT}
+ *
+ * @param project the Gradle project to check
+ * @param artifact the artifact
+ * @return true if the project depends on the given artifact (including transitively)
+ */
+ public static boolean dependsOn(@NonNull IdeaAndroidProject project, @NonNull String artifact) {
+ Dependencies dependencies = project.getSelectedVariant().getMainArtifact().getDependencies();
+ return dependsOn(dependencies, artifact);
+
+ }
+
+ /**
+ * Returns true if the given dependencies include the given artifact, which consists of
+ * a group id and an artifact id, such as {@link com.android.SdkConstants#APPCOMPAT_LIB_ARTIFACT}
+ *
+ * @param dependencies the Gradle dependencies object to check
+ * @param artifact the artifact
+ * @return true if the dependencies include the given artifact (including transitively)
+ */
+ public static boolean dependsOn(@NonNull Dependencies dependencies, @NonNull String artifact) {
+ for (AndroidLibrary library : dependencies.getLibraries()) {
+ if (dependsOn(library, artifact, true)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the given library depends on the given artifact, which consists of
+ * a group id and an artifact id, such as {@link com.android.SdkConstants#APPCOMPAT_LIB_ARTIFACT}
+ *
+ * @param library the Gradle library to check
+ * @param artifact the artifact
+ * @param transitively if false, checks only direct dependencies, otherwise checks transitively
+ * @return true if the project depends on the given artifact
+ */
+ public static boolean dependsOn(@NonNull AndroidLibrary library, @NonNull String artifact, boolean transitively) {
+ MavenCoordinates resolvedCoordinates = library.getResolvedCoordinates();
+ if (resolvedCoordinates != null) {
+ String s = resolvedCoordinates.getGroupId() + ':' + resolvedCoordinates.getArtifactId();
+ if (artifact.equals(s)) {
+ return true;
+ }
+ }
+
+ if (transitively) {
+ for (AndroidLibrary dependency : library.getLibraryDependencies()) {
+ if (dependsOn(dependency, artifact, true)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
}
\ No newline at end of file
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 a9c7d2e..13dcfc9 100644
--- a/android/src/com/android/tools/idea/gradle/util/LocalProperties.java
+++ b/android/src/com/android/tools/idea/gradle/util/LocalProperties.java
@@ -36,6 +36,7 @@
private static final String HEADER_COMMENT = getHeaderComment();
@NotNull private final File myFilePath;
+ @NotNull private final File myProjectDirPath;
@NotNull private final Properties myProperties;
@NotNull
@@ -75,6 +76,7 @@
* @throws IllegalArgumentException if there is already a directory called "local.properties" at the given path.
*/
public LocalProperties(@NotNull File projectDirPath) throws IOException {
+ myProjectDirPath = projectDirPath;
myFilePath = new File(projectDirPath, SdkConstants.FN_LOCAL_PROPERTIES);
myProperties = PropertiesUtil.getProperties(myFilePath);
}
@@ -86,7 +88,10 @@
public File getAndroidSdkPath() {
String path = myProperties.getProperty(SdkConstants.SDK_DIR_PROPERTY);
if (path != null && !path.isEmpty()) {
- return new File(FileUtil.toSystemDependentName(path));
+ if (!FileUtil.isAbsolute(path)) {
+ path = new File(myProjectDirPath, path).getPath();
+ }
+ return new File(FileUtil.toSystemDependentName(path));
}
return null;
}
diff --git a/android/src/com/android/tools/idea/psi/resolve/AndroidMethodConflictResolver.java b/android/src/com/android/tools/idea/psi/resolve/AndroidMethodConflictResolver.java
new file mode 100644
index 0000000..4944940
--- /dev/null
+++ b/android/src/com/android/tools/idea/psi/resolve/AndroidMethodConflictResolver.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.psi.resolve;
+
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.roots.*;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.infos.CandidateInfo;
+import com.intellij.psi.scope.PsiConflictResolver;
+import org.jetbrains.android.AndroidSdkResolveScopeProvider;
+import org.jetbrains.android.sdk.AndroidSdkUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * There is an android specifics that either <code>*.class</code> or <code>*.java</code> files from android sdk
+ * are included to the resolve scope. That might produce errors like
+ * <a href="https://code.google.com/p/android/issues/detail?id=70135">this</a> or
+ * <a href="https://youtrack.jetbrains.com/issue/IDEA-131368">this one</a>.
+ * <p/>
+ * Android integration tries to prevent that via {@link AndroidSdkResolveScopeProvider.MyJdkScope#compare(VirtualFile, VirtualFile)}
+ * by excluding <code>*.class</code> files from the resolve scope. However, it doesn't work for situation when a super-class' method
+ * is called inside an object of an android-specific class - resolve process goes up the hierarchy and reaches <code>*.class</code>
+ * file then. <code>*.java</code> file is also in resolve scope and we have a conflict.
+ * <p/>
+ * Current conflict resolver encapsulates logic which prefers <code>*.java</code> android sdk info to <code>*.class</code>
+ * android sdk info.
+ */
+public class AndroidMethodConflictResolver implements PsiConflictResolver {
+
+ @Nullable
+ @Override
+ public CandidateInfo resolveConflict(@NotNull List<CandidateInfo> conflicts) {
+ if (conflicts.isEmpty()) {
+ return null;
+ }
+
+ ProjectFileIndex index = null;
+ for (CandidateInfo conflict : conflicts) {
+ PsiElement element = conflict.getElement();
+ if (element != null) {
+ index = ProjectRootManager.getInstance(element.getProject()).getFileIndex();
+ break;
+ }
+ }
+
+ if (index == null) {
+ return null;
+ }
+
+ CandidateInfo sourceInfo = null;
+ Sdk sdk = null;
+ for (CandidateInfo conflict : conflicts) {
+ PsiElement element = conflict.getElement();
+ if (element == null) {
+ continue;
+ }
+ PsiFile psiFile = element.getContainingFile();
+ if (psiFile == null) {
+ continue;
+ }
+ VirtualFile virtualFile = psiFile.getVirtualFile();
+ if (virtualFile == null) {
+ continue;
+ }
+
+ // We want to process a situation only with android sdk classes (either binary or source) which participate in method resolving.
+ List<OrderEntry> orderEntries = index.getOrderEntriesForFile(virtualFile);
+ if (orderEntries.size() != 1) {
+ return null;
+ }
+ OrderEntry orderEntry = orderEntries.get(0);
+ if (!(orderEntry instanceof JdkOrderEntry)) {
+ return null;
+ }
+ Sdk currentSdk = ((JdkOrderEntry)orderEntry).getJdk();
+ if (!AndroidSdkUtils.isAndroidSdk(currentSdk) || (sdk != null && !sdk.equals(currentSdk))) {
+ return null;
+ }
+ sdk = currentSdk;
+
+ if (index.isInLibrarySource(virtualFile)) {
+ if (sourceInfo == null) {
+ sourceInfo = conflict;
+ }
+ else {
+ // Adjust only 'a *.class VS single *.java' case.
+ return null;
+ }
+ }
+ }
+ return sourceInfo;
+ }
+}
diff --git a/android/src/com/android/tools/idea/rendering/AarResourceClassGenerator.java b/android/src/com/android/tools/idea/rendering/AarResourceClassGenerator.java
index 053f63d..3e03c76 100644
--- a/android/src/com/android/tools/idea/rendering/AarResourceClassGenerator.java
+++ b/android/src/com/android/tools/idea/rendering/AarResourceClassGenerator.java
@@ -21,6 +21,7 @@
import com.android.ide.common.res2.ResourceItem;
import com.android.resources.ResourceType;
import com.android.tools.lint.detector.api.ClassContext;
+import org.jetbrains.android.util.AndroidResourceUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.asm4.ClassWriter;
@@ -189,7 +190,7 @@
Collection<String> keys = myAarResources.getItemsOfType(type);
for (String key : keys) {
Integer initialValue = myAppResources.getResourceId(type, key);
- cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, key, "I",
+ cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, AndroidResourceUtil.getFieldNameByResourceName(key), "I",
null, initialValue).visitEnd();
}
}
diff --git a/android/src/com/android/tools/idea/rendering/RenderErrorPanel.java b/android/src/com/android/tools/idea/rendering/RenderErrorPanel.java
index 36cc8e6..e85a063 100644
--- a/android/src/com/android/tools/idea/rendering/RenderErrorPanel.java
+++ b/android/src/com/android/tools/idea/rendering/RenderErrorPanel.java
@@ -100,6 +100,7 @@
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.RenderLogger.TAG_STILL_BUILDING;
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;
@@ -682,7 +683,10 @@
}
private static void reportMissingStyles(RenderLogger logger, HtmlBuilder builder) {
- if (logger.seenTagPrefix(TAG_RESOURCES_RESOLVE_THEME_ATTR)) {
+ if (logger.seenTagPrefix(TAG_STILL_BUILDING)) {
+ builder.addBold("Project Still Building: May cause rendering errors until the build is done.").newline();
+ builder.newline().newline();
+ } else if (logger.seenTagPrefix(TAG_RESOURCES_RESOLVE_THEME_ATTR)) {
builder.addBold("Missing styles. Is the correct theme chosen for this layout?").newline();
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.");
diff --git a/android/src/com/android/tools/idea/rendering/RenderLogger.java b/android/src/com/android/tools/idea/rendering/RenderLogger.java
index 5c0c5ca..844720c 100644
--- a/android/src/com/android/tools/idea/rendering/RenderLogger.java
+++ b/android/src/com/android/tools/idea/rendering/RenderLogger.java
@@ -72,6 +72,7 @@
public static final String TAG_MISSING_DIMENSION = "missing.dimension";
public static final String TAG_MISSING_FRAGMENT = "missing.fragment";
+ public static final String TAG_STILL_BUILDING = "project.building";
private static Set<String> ourIgnoredFidelityWarnings;
private static boolean ourIgnoreAllFidelityWarnings;
private static boolean ourIgnoreFragments;
@@ -189,6 +190,8 @@
if (facet != null && facet.isGradleProject()) {
description = "Still building project; theme resources from libraries may be missing. Layout should refresh when the " +
"build is complete.\n\n" + description;
+ tag = TAG_STILL_BUILDING;
+ addTag(tag);
}
}
diff --git a/android/src/com/android/tools/idea/sdk/DefaultSdks.java b/android/src/com/android/tools/idea/sdk/DefaultSdks.java
index 2aa7cbd..a22dfd1 100644
--- a/android/src/com/android/tools/idea/sdk/DefaultSdks.java
+++ b/android/src/com/android/tools/idea/sdk/DefaultSdks.java
@@ -109,7 +109,7 @@
/**
* @return the first SDK it finds that matches our default naming convention. There will be several SDKs so named, one for each build
- * target installed in the SDK; which of those this method returns is not defined.
+ * target installed in the SDK; which of those this method returns is not defined.
*/
@Nullable
private static Sdk getFirstAndroidSdk() {
@@ -162,8 +162,9 @@
}
}
- public static List<Sdk> setDefaultAndroidHome(@NotNull File path) {
- return setDefaultAndroidHome(path, null);
+ @NotNull
+ public static List<Sdk> setDefaultAndroidHome(@NotNull File path, @Nullable Project currentProject) {
+ return setDefaultAndroidHome(path, null, currentProject);
}
/**
@@ -198,11 +199,11 @@
* Sets the path of Android Studio's default Android SDK. This method should be called in a write action. It is assumed that the given
* path has been validated by {@link #isValidAndroidSdkPath(File)}. This method will fail silently if the given path is not valid.
*
- *
* @param path the path of the Android SDK.
* @see com.intellij.openapi.application.Application#runWriteAction(Runnable)
*/
- public static List<Sdk> setDefaultAndroidHome(@NotNull File path, @Nullable Sdk javaSdk) {
+ @NotNull
+ public static List<Sdk> setDefaultAndroidHome(@NotNull File path, @Nullable Sdk javaSdk, @Nullable Project currentProject) {
if (isValidAndroidSdkPath(path)) {
assert ApplicationManager.getApplication().isWriteAccessAllowed();
@@ -234,7 +235,7 @@
List<Sdk> sdks = createAndroidSdksForAllTargets(resolved, javaSdk);
// Update the local.properties files for any open projects.
- updateLocalPropertiesAndSync(resolved);
+ updateLocalPropertiesAndSync(resolved, currentProject);
return sdks;
}
@@ -305,7 +306,7 @@
return androidPlatform.getTarget();
}
- private static void updateLocalPropertiesAndSync(@NotNull final File sdkHomePath) {
+ private static void updateLocalPropertiesAndSync(@NotNull final File sdkHomePath, @Nullable Project currentProject) {
ProjectManager projectManager = ApplicationManager.getApplication().getComponent(ProjectManager.class);
Project[] openProjects = projectManager.getOpenProjects();
if (openProjects.length == 0) {
@@ -322,22 +323,24 @@
LocalProperties localProperties = new LocalProperties(project);
if (!FileUtil.filesEqual(sdkHomePath, localProperties.getAndroidSdkPath())) {
localPropertiesToUpdate.add(Pair.create(project, localProperties));
- projectsToUpdateNames.add("'" + project.getName() + "'");
+ if (!project.equals(currentProject)) {
+ projectsToUpdateNames.add("'" + project.getName() + "'");
+ }
}
}
catch (IOException e) {
// Exception thrown when local.properties file exists but cannot be read (e.g. no writing permissions.)
logAndShowErrorWhenUpdatingLocalProperties(project, e, "read", sdkHomePath);
}
- } if (!localPropertiesToUpdate.isEmpty()) {
- if (!ApplicationManager.getApplication().isUnitTestMode()) {
+ }
+ if (!localPropertiesToUpdate.isEmpty()) {
+ if (!projectsToUpdateNames.isEmpty() && !ApplicationManager.getApplication().isUnitTestMode()) {
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
- String format =
- "The local.properties files in projects %1$s will be modified with the path of Android Studio's default Android SDK:\n" +
- "'%2$s'";
- Messages.showErrorDialog(String.format(format, projectsToUpdateNames, sdkHomePath), "Sync Android SDKs");
+ String msg = "The local.properties file(s) in the project(s)\n " + projectsToUpdateNames +
+ "\nwill be modified with the path of Android Studio's default Android Studio:\n'" + sdkHomePath + "'";
+ Messages.showErrorDialog(String.format(msg, projectsToUpdateNames, sdkHomePath), "Sync Android SDKs");
}
});
}
diff --git a/android/src/com/android/tools/idea/sdk/SdkLoggerIntegration.java b/android/src/com/android/tools/idea/sdk/SdkLoggerIntegration.java
new file mode 100644
index 0000000..108508c
--- /dev/null
+++ b/android/src/com/android/tools/idea/sdk/SdkLoggerIntegration.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.sdk;
+
+import com.android.utils.IReaderLogger;
+import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator;
+import com.intellij.util.ui.UIUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility class for integrating stream-based SDK manager UI with the Android Studio UI.
+ */
+public abstract class SdkLoggerIntegration implements IReaderLogger {
+ // Groups: 1=license-id
+ private static Pattern sLicenceText = Pattern.compile("^\\s*License id:\\s*([a-z0-9-]+).*\\s*");
+ // Groups: 1=progress values (%, ETA), 2=% int, 3=progress text
+ private static Pattern sProgress1Text = Pattern.compile("^\\s+\\((([0-9]+)%,\\s*[^)]*)\\)(.*)\\s*");
+ // Groups: 1=progress text, 2=progress values, 3=% int
+ private static Pattern sProgress2Text = Pattern.compile("^\\s+([^(]+)\\s+\\((([0-9]+)%)\\)\\s*");
+ // Groups: 1=task title
+ private static Pattern sDownloadingComponentText = Pattern.compile("^\\s+(Downloading .*)\\s*$");
+
+ private BackgroundableProcessIndicator myIndicator;
+ private String myCurrLicense;
+ private String myLastLine;
+
+ public void setIndicator(BackgroundableProcessIndicator indicator) {
+ myIndicator = indicator;
+ }
+
+ /**
+ * Used by UpdaterData.acceptLicense() to prompt for license acceptance
+ * when updating the SDK from the command-line.
+ * <p/>
+ * {@inheritDoc}
+ */
+ @Override
+ public int readLine(@NotNull byte[] inputBuffer) throws IOException {
+ if (myLastLine != null && myLastLine.contains("Do you accept the license")) {
+ // Let's take a simple shortcut and simply reply 'y' for yes.
+ inputBuffer[0] = 'y';
+ inputBuffer[1] = 0;
+ return 1;
+ }
+ inputBuffer[0] = 'n';
+ inputBuffer[1] = 0;
+ return 1;
+ }
+
+ @Override
+ public void error(@Nullable Throwable t, @Nullable String msgFormat, Object... args) {
+ if (msgFormat == null && t != null) {
+ if (myIndicator != null) myIndicator.setText2(t.toString());
+ outputLine(t.toString());
+ }
+ else if (msgFormat != null) {
+ if (myIndicator != null) myIndicator.setText2(String.format(msgFormat, args));
+ outputLine(String.format(msgFormat, args));
+ }
+ }
+
+ @Override
+ public void warning(@NotNull String msgFormat, Object... args) {
+ if (myIndicator != null) myIndicator.setText2(String.format(msgFormat, args));
+ outputLine(String.format(msgFormat, args));
+ }
+
+ @Override
+ public void info(@NotNull String msgFormat, Object... args) {
+ if (myIndicator != null) myIndicator.setText2(String.format(msgFormat, args));
+ outputLine(String.format(msgFormat, args));
+ }
+
+ @Override
+ public void verbose(@NotNull String msgFormat, Object... args) {
+ // Don't log verbose stuff in the background indicator.
+ outputLine(String.format(msgFormat, args));
+ }
+
+ /**
+ * This method takes the console output from the command-line updater.
+ * It filters it to remove some verbose output that is not desirable here.
+ * It also detects progress-bar like text and updates the dialog's progress
+ * bar accordingly.
+ */
+ private void outputLine(@NotNull final String line) {
+ myLastLine = line;
+ try {
+ // skip some of the verbose output such as license text & refreshing http sources
+ Matcher m = sLicenceText.matcher(line);
+ if (m.matches()) {
+ myCurrLicense = m.group(1);
+ return;
+ }
+ else if (myCurrLicense != null) {
+ if (line.contains("Do you accept the license") && line.contains(myCurrLicense)) {
+ myCurrLicense = null;
+ }
+ return;
+ }
+ else if (line.contains("Fetching http") ||
+ line.contains("Fetching URL:") ||
+ line.contains("Validate XML") ||
+ line.contains("Parse XML") ||
+ line.contains("---------")) {
+ return;
+ }
+
+ if (parseWithPattern(sProgress1Text, line, 3, 1, 2) ||
+ parseWithPattern(sProgress2Text, line, 1, 2, 3) ||
+ parseWithPattern(sDownloadingComponentText, line, 1, -1, -1)) {
+ return;
+ }
+
+ // This is invoked from a backgroundable task, only update text on the ui thread.
+ UIUtil.invokeAndWaitIfNeeded(new Runnable() {
+ @Override
+ public void run() {
+ lineAdded(line);
+ }
+ });
+ }
+ catch (Exception ignore) {
+ }
+ }
+
+ private boolean parseWithPattern(@NotNull Pattern pattern, @NotNull String line, int titleGroup, int progressTextGroup, int progressGroup) {
+ int progInt = -1;
+
+ Matcher m = pattern.matcher(line);
+ if (m.matches()) {
+ if (progressGroup >= 0) {
+ // Groups: 1=progress values (%, ETA), 2=% int, 3=progress text
+ try {
+ progInt = Integer.parseInt(m.group(progressGroup));
+ }
+ catch (NumberFormatException ignore) {
+ progInt = 0;
+ }
+ }
+ final int fProgInt = progInt;
+ final String fProgText2 = getGroupText(m, progressTextGroup);
+ final String fProgText1 = getGroupText(m, titleGroup);
+
+ // This is invoked from a backgroundable task, only update text on the ui thread.
+ UIUtil.invokeAndWaitIfNeeded(new Runnable() {
+ @Override
+ public void run() {
+ if (fProgText1 != null) {
+ setTitle(fProgText1);
+ }
+
+ if (fProgText2 != null) {
+ setDescription(fProgText2);
+ }
+
+ if (fProgInt >= 0) {
+ setProgress(fProgInt);
+ }
+ }
+ });
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
+ @Nullable
+ private static String getGroupText(Matcher m, int group) {
+ return group >= 0 ? m.group(group) : null;
+ }
+
+ protected abstract void setProgress(int progress);
+
+ protected abstract void setDescription(String description);
+
+ protected abstract void setTitle(String title);
+
+ protected abstract void lineAdded(String string);
+}
diff --git a/android/src/com/android/tools/idea/sdk/wizard/LicenseAgreementStep.form b/android/src/com/android/tools/idea/sdk/wizard/LicenseAgreementStep.form
deleted file mode 100644
index fc679f1..0000000
--- a/android/src/com/android/tools/idea/sdk/wizard/LicenseAgreementStep.form
+++ /dev/null
@@ -1,63 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.android.tools.idea.sdk.wizard.LicenseAgreementStep">
- <grid id="27dc6" binding="myComponent" layout-manager="GridLayoutManager" row-count="2" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="1" vgap="1">
- <margin top="2" left="2" bottom="2" right="2"/>
- <constraints>
- <xy x="20" y="20" width="942" height="587"/>
- </constraints>
- <properties/>
- <border type="none"/>
- <children>
- <scrollpane id="4ad9b" class="com.intellij.ui.components.JBScrollPane">
- <constraints>
- <grid row="0" column="0" row-span="2" col-span="1" vsize-policy="3" hsize-policy="7" anchor="0" fill="3" indent="0" use-parent-layout="false">
- <preferred-size width="300" height="440"/>
- </grid>
- </constraints>
- <properties/>
- <border type="none"/>
- <children>
- <component id="4646" class="com.intellij.ui.treeStructure.Tree" binding="myChangeTree">
- <constraints/>
- <properties/>
- </component>
- </children>
- </scrollpane>
- <scrollpane id="9e526" class="com.intellij.ui.components.JBScrollPane">
- <constraints>
- <grid row="0" column="1" row-span="1" col-span="2" vsize-policy="3" hsize-policy="7" anchor="0" fill="3" indent="0" use-parent-layout="false">
- <preferred-size width="350" height="-1"/>
- </grid>
- </constraints>
- <properties>
- <opaque value="false"/>
- <verticalScrollBarPolicy value="22"/>
- </properties>
- <border type="none"/>
- <children>
- <component id="ad692" class="javax.swing.JTextPane" binding="myLicenseTextField">
- <constraints/>
- <properties/>
- </component>
- </children>
- </scrollpane>
- <component id="8beac" class="javax.swing.JRadioButton" binding="myDeclineRadioButton" default-binding="true">
- <constraints>
- <grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="1" fill="0" indent="0" use-parent-layout="false"/>
- </constraints>
- <properties>
- <text value="Decline"/>
- </properties>
- </component>
- <component id="a3a11" class="javax.swing.JRadioButton" binding="myAcceptRadioButton" default-binding="true">
- <constraints>
- <grid row="1" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="1" fill="0" indent="0" use-parent-layout="false"/>
- </constraints>
- <properties>
- <hideActionText value="true"/>
- <text value="Accept"/>
- </properties>
- </component>
- </children>
- </grid>
-</form>
diff --git a/android/src/com/android/tools/idea/sdk/wizard/LicenseAgreementStep.java b/android/src/com/android/tools/idea/sdk/wizard/LicenseAgreementStep.java
index c8fb724..ad47b87 100644
--- a/android/src/com/android/tools/idea/sdk/wizard/LicenseAgreementStep.java
+++ b/android/src/com/android/tools/idea/sdk/wizard/LicenseAgreementStep.java
@@ -22,8 +22,11 @@
import com.google.common.collect.Maps;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.Disposable;
+import com.intellij.openapi.ui.Splitter;
import com.intellij.ui.ColoredTreeCellRenderer;
+import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.SimpleTextAttributes;
+import com.intellij.ui.components.JBRadioButton;
import com.intellij.ui.treeStructure.Tree;
import org.jetbrains.annotations.NotNull;
@@ -33,6 +36,7 @@
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
+import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;
@@ -46,7 +50,6 @@
public class LicenseAgreementStep extends DynamicWizardStepWithHeaderAndDescription {
private JTextPane myLicenseTextField;
private Tree myChangeTree;
- private JPanel myComponent;
private JRadioButton myDeclineRadioButton;
private JRadioButton myAcceptRadioButton;
@@ -58,7 +61,31 @@
public LicenseAgreementStep(@NotNull Disposable disposable) {
super("License Agreement", "Read and agree to the licenses for the components which will be installed", null, disposable);
- setBodyComponent(myComponent);
+ Splitter splitter = new Splitter(false, .30f);
+ splitter.setHonorComponentsMinimumSize(true);
+
+ myChangeTree = new Tree();
+ splitter.setFirstComponent(ScrollPaneFactory.createScrollPane(myChangeTree));
+
+ myLicenseTextField = new JTextPane();
+ splitter.setSecondComponent(ScrollPaneFactory.createScrollPane(myLicenseTextField));
+
+ myDeclineRadioButton = new JBRadioButton("Decline");
+ myAcceptRadioButton = new JBRadioButton("Accept");
+
+ ButtonGroup optionsGroup = new ButtonGroup();
+ optionsGroup.add(myDeclineRadioButton);
+ optionsGroup.add(myAcceptRadioButton);
+
+ JPanel optionsPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING));
+ optionsPanel.add(myDeclineRadioButton);
+ optionsPanel.add(myAcceptRadioButton);
+
+ JPanel mainPanel = new JPanel(new BorderLayout());
+ mainPanel.add(splitter, BorderLayout.CENTER);
+ mainPanel.add(optionsPanel, BorderLayout.SOUTH);
+
+ setBodyComponent(mainPanel);
}
@Override
diff --git a/android/src/com/android/tools/idea/sdk/wizard/SmwOldApiDirectInstall.form b/android/src/com/android/tools/idea/sdk/wizard/SmwOldApiDirectInstall.form
index 963be58..f8bad73 100755
--- a/android/src/com/android/tools/idea/sdk/wizard/SmwOldApiDirectInstall.form
+++ b/android/src/com/android/tools/idea/sdk/wizard/SmwOldApiDirectInstall.form
@@ -66,7 +66,7 @@
</properties>
<border type="none"/>
<children>
- <component id="9f832" class="javax.swing.JTextArea" binding="myTextArea1" default-binding="true">
+ <component id="9f832" class="javax.swing.JTextArea" binding="mySdkManagerOutput" default-binding="true">
<constraints/>
<properties>
<text value=""/>
diff --git a/android/src/com/android/tools/idea/sdk/wizard/SmwOldApiDirectInstall.java b/android/src/com/android/tools/idea/sdk/wizard/SmwOldApiDirectInstall.java
index 8b0c0a6..ffbf8bf 100755
--- a/android/src/com/android/tools/idea/sdk/wizard/SmwOldApiDirectInstall.java
+++ b/android/src/com/android/tools/idea/sdk/wizard/SmwOldApiDirectInstall.java
@@ -20,10 +20,10 @@
import com.android.sdklib.internal.repository.updater.SdkUpdaterNoWindow;
import com.android.sdklib.repository.descriptors.IPkgDesc;
import com.android.sdklib.repository.descriptors.PkgType;
+import com.android.tools.idea.sdk.SdkLoggerIntegration;
import com.android.tools.idea.sdk.SdkState;
import com.android.tools.idea.wizard.DynamicWizardStepWithHeaderAndDescription;
import com.android.utils.ILogger;
-import com.android.utils.IReaderLogger;
import com.google.common.collect.Lists;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
@@ -43,11 +43,8 @@
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import static com.android.tools.idea.wizard.WizardConstants.INSTALL_REQUESTS_KEY;
import static com.android.tools.idea.wizard.WizardConstants.NEWLY_INSTALLED_API_KEY;
@@ -56,7 +53,7 @@
public class SmwOldApiDirectInstall extends DynamicWizardStepWithHeaderAndDescription {
private Logger LOG = Logger.getInstance(SmwOldApiDirectInstall.class);
private JBLabel myLabelSdkPath;
- private JTextArea myTextArea1;
+ private JTextArea mySdkManagerOutput;
private JBLabel myLabelProgress1;
private JProgressBar myProgressBar;
private JBLabel myLabelProgress2;
@@ -64,7 +61,6 @@
private JPanel myContentPanel;
private boolean myInstallFinished;
private Boolean myBackgroundSuccess = null;
- private boolean myBeforeInstall = true;
public SmwOldApiDirectInstall(@NotNull Disposable disposable) {
super("Installing Requested Components", "", null, disposable);
@@ -74,7 +70,7 @@
@Override
public void onEnterStep() {
super.onEnterStep();
- myTextArea1.setText("");
+ mySdkManagerOutput.setText("");
startSdkInstallUsingNonSwtOldApi();
}
@@ -198,7 +194,6 @@
// The command-line API is a bit archaic and has some drastic limitations, one of them being that
// it blindly re-install stuff even if already present IIRC.
- myBeforeInstall = false;
String osSdkFolder = mySdkData.getLocation().getPath();
SdkManager sdkManager = SdkManager.createManager(osSdkFolder, myLogger);
@@ -281,176 +276,36 @@
}
}
- // Groups: 1=license-id
- private static Pattern sLicenceText = Pattern.compile("^\\s*License id:\\s*([a-z0-9-]+).*\\s*");
- // Groups: 1=progress values (%, ETA), 2=% int, 3=progress text
- private static Pattern sProgress1Text = Pattern.compile("^\\s+\\((([0-9]+)%,\\s*[^)]*)\\)(.*)\\s*");
- // Groups: 1=progress text, 2=progress values, 3=% int
- private static Pattern sProgress2Text = Pattern.compile("^\\s+([^(]+)\\s+\\((([0-9]+)%)\\)\\s*");
-
- private class CustomLogger implements IReaderLogger {
-
- private BackgroundableProcessIndicator myIndicator;
- private String myCurrLicense;
- private String myLastLine;
-
- public CustomLogger() {
- }
-
- public void setIndicator(BackgroundableProcessIndicator indicator) {
- myIndicator = indicator;
- }
-
- /**
- * Used by UpdaterData.acceptLicense() to prompt for license acceptance
- * when updating the SDK from the command-line.
- * <p/>
- * {@inheritDoc}
- */
+ private final class CustomLogger extends SdkLoggerIntegration {
@Override
- public int readLine(@NotNull byte[] inputBuffer) throws IOException {
- if (myLastLine != null && myLastLine.contains("Do you accept the license")) {
- // Let's take a simple shortcut and simply reply 'y' for yes.
- inputBuffer[0] = 'y';
- inputBuffer[1] = 0;
- return 1;
+ protected void setProgress(int progress) {
+ myProgressBar.setValue(progress);
+ }
+
+ @Override
+ protected void setDescription(String description) {
+ myLabelProgress2.setText(description);
+ }
+
+ @Override
+ protected void setTitle(String title) {
+ myLabelProgress1.setText(title);
+ }
+
+ @Override
+ protected void lineAdded(String string) {
+ String current = mySdkManagerOutput.getText();
+ if (current == null) {
+ current = "";
}
- inputBuffer[0] = 'n';
- inputBuffer[1] = 0;
- return 1;
- }
-
- @Override
- public void error(@Nullable Throwable t,
- @Nullable String msgFormat,
- Object... args) {
- if (msgFormat == null && t != null) {
- if (myIndicator != null) myIndicator.setText2(t.toString());
- outputLine(t.toString());
- } else if (msgFormat != null) {
- if (myIndicator != null) myIndicator.setText2(String.format(msgFormat, args));
- outputLine(String.format(msgFormat, args));
+ mySdkManagerOutput.setText(current + string);
+ if (string.contains("Nothing was installed") ||
+ string.contains("Failed") ||
+ string.contains("The package filter removed all packages")) {
+ myBackgroundSuccess = false;
+ } else if (string.contains("Done") && !string.contains("othing")) {
+ myBackgroundSuccess = true;
}
}
-
- @Override
- public void warning(@NotNull String msgFormat, Object... args) {
- if (myIndicator != null) myIndicator.setText2(String.format(msgFormat, args));
- outputLine(String.format(msgFormat, args));
- }
-
- @Override
- public void info(@NotNull String msgFormat, Object... args) {
- if (myIndicator != null) myIndicator.setText2(String.format(msgFormat, args));
- outputLine(String.format(msgFormat, args));
- }
-
- @Override
- public void verbose(@NotNull String msgFormat, Object... args) {
- // Don't log verbose stuff in the background indicator.
- outputLine(String.format(msgFormat, args));
- }
-
- /**
- * This method takes the console output from the command-line updater.
- * It filters it to remove some verbose output that is not desirable here.
- * It also detects progress-bar like text and updates the dialog's progress
- * bar accordingly.
- */
- private void outputLine(@NotNull String line) {
- myLastLine = line;
- try {
- // skip some of the verbose output such as license text & refreshing http sources
- Matcher m = sLicenceText.matcher(line);
- if (m.matches()) {
- myCurrLicense = m.group(1);
- return;
- }
- else if (myCurrLicense != null) {
- if (line.contains("Do you accept the license") && line.contains(myCurrLicense)) {
- myCurrLicense = null;
- }
- return;
- }
- else if (line.contains("Fetching http") ||
- line.contains("Fetching URL:") ||
- line.contains("Validate XML") ||
- line.contains("Parse XML") ||
- line.contains("---------")) {
- return;
- }
-
- int progInt = -1;
- String progText2 = null;
- String progText1 = null;
-
- m = sProgress1Text.matcher(line);
- if (m.matches()) {
- // Groups: 1=progress values (%, ETA), 2=% int, 3=progress text
- try {
- progInt = Integer.parseInt(m.group(2));
- }
- catch (NumberFormatException ignore) {
- progInt = 0;
- }
- progText1 = m.group(3);
- progText2 = m.group(1);
- line = null;
- } else {
- m = sProgress2Text.matcher(line);
- if (m.matches()) {
- // Groups: 1=progress text, 2=progress values, 3=% int
- try {
- progInt = Integer.parseInt(m.group(3));
- }
- catch (NumberFormatException ignore) {
- progInt = 0;
- }
- progText1 = m.group(1);
- progText2 = m.group(2);
- line = null;
- }
- }
-
- final int fProgInt = progInt;
- final String fProgText2 = progText2;
- final String fProgText1 = progText1;
- final String fAddLine = line;
-
- // This is invoked from a backgroundable task, only update text on the ui thread.
- UIUtil.invokeAndWaitIfNeeded(new Runnable() {
- @Override
- public void run() {
- if (fAddLine != null) {
- String current = myTextArea1.getText();
- if (current == null) {
- current = "";
- }
- myTextArea1.setText(current + fAddLine);
- if (fAddLine.contains("Nothing was installed")) {
- myBackgroundSuccess = false;
- } else if (fAddLine.contains("Failed") || fAddLine.contains("The package filter removed all packages")) {
- myBackgroundSuccess = false;
- } else if (fAddLine.contains("Done") && !fAddLine.contains("othing")) {
- myBackgroundSuccess = Boolean.TRUE;
- }
- }
-
- if (fProgText1 != null) {
- myLabelProgress1.setText(fProgText1);
- }
-
- if (fProgText2 != null) {
- myLabelProgress2.setText(fProgText2);
- }
-
- if (fProgInt >= 0) {
- myProgressBar.setValue(fProgInt);
- }
- }
- });
- } catch (Exception ignore) {}
- }
}
-
}
diff --git a/android/src/com/android/tools/idea/templates/FmHasDependencyMethod.java b/android/src/com/android/tools/idea/templates/FmHasDependencyMethod.java
new file mode 100644
index 0000000..7095dcf
--- /dev/null
+++ b/android/src/com/android/tools/idea/templates/FmHasDependencyMethod.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.templates;
+
+import com.android.SdkConstants;
+import com.android.builder.model.Dependencies;
+import com.android.tools.idea.gradle.IdeaAndroidProject;
+import com.android.tools.idea.gradle.util.GradleUtil;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleUtilCore;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ProjectLocator;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import freemarker.ext.beans.BooleanModel;
+import freemarker.template.*;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.android.inspections.lint.IntellijLintClient;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+
+import static com.android.tools.idea.templates.FmUtil.stripSuffix;
+import static com.android.tools.idea.wizard.TemplateWizardState.ACTIVITY_NAME_SUFFIX;
+import static com.android.tools.idea.wizard.TemplateWizardState.LAYOUT_NAME_PREFIX;
+
+/**
+ * Method invoked by FreeMarker to check whether a given dependency
+ * is available in this module
+ */
+public class FmHasDependencyMethod implements TemplateMethodModelEx {
+ private final Map<String, Object> myParamMap;
+
+ public FmHasDependencyMethod(Map<String, Object> paramMap) {
+ myParamMap = paramMap;
+ }
+
+ @Override
+ public TemplateModel exec(List args) throws TemplateModelException {
+ if (args.size() != 1) {
+ throw new TemplateModelException("Wrong arguments");
+ }
+ String artifact = args.get(0).toString();
+ if (artifact.isEmpty()) {
+ return TemplateBooleanModel.FALSE;
+ }
+
+ if (myParamMap.containsKey(TemplateMetadata.ATTR_DEPENDENCIES_LIST)) {
+ Object listObject = myParamMap.get(TemplateMetadata.ATTR_DEPENDENCIES_LIST);
+ if (listObject instanceof List) {
+ @SuppressWarnings("unchecked")
+ List<String> dependencyList = (List<String>)listObject;
+ for (String dependency : dependencyList) {
+ if (dependency.contains(artifact)) {
+ return TemplateBooleanModel.TRUE;
+ }
+ }
+ }
+ }
+
+ // Find the corresponding module, if any
+ String modulePath = (String)myParamMap.get(TemplateMetadata.ATTR_PROJECT_OUT);
+ if (modulePath != null) {
+ VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(new File(modulePath.replace('/', File.separatorChar)));
+ if (file != null) {
+ Project project = ProjectLocator.getInstance().guessProjectForFile(file);
+ if (project != null) {
+ Module module = ModuleUtilCore.findModuleForFile(file, project);
+ if (module != null) {
+ AndroidFacet facet = AndroidFacet.getInstance(module);
+ if (facet != null) {
+ IdeaAndroidProject gradleProject = facet.getIdeaAndroidProject();
+ if (gradleProject != null) {
+ return GradleUtil.dependsOn(gradleProject, artifact) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Creating a new module, so no existing dependencies: provide some defaults. This is really intended for appcompat-v7,
+ // but since it depends on support-v4, we include it here (such that a query to see if support-v4 is installed in a newly
+ // created project will return true since it will be by virtue of appcompat also being installed.)
+ if (artifact.contains(SdkConstants.APPCOMPAT_LIB_ARTIFACT) || artifact.contains(SdkConstants.SUPPORT_LIB_ARTIFACT)) {
+ // No dependencies: Base it off of the minApi and buildApi versions:
+ // If building with Lollipop, and targeting anything earlier than Lollipop, use appcompat.
+ // (Also use it if minApi is less than ICS.)
+ Object buildApiObject = myParamMap.get(TemplateMetadata.ATTR_BUILD_API);
+ Object minApiObject = myParamMap.get(TemplateMetadata.ATTR_MIN_API_LEVEL);
+ if (buildApiObject instanceof Integer && minApiObject instanceof Integer) {
+ int buildApi = (Integer)buildApiObject;
+ int minApi = (Integer)minApiObject;
+ return minApi >= 8 && ((buildApi >= 21 && minApi < 21) || minApi < 14) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+ }
+ }
+
+ return TemplateBooleanModel.FALSE;
+ }
+}
\ No newline at end of file
diff --git a/android/src/com/android/tools/idea/templates/Template.java b/android/src/com/android/tools/idea/templates/Template.java
index 8241671..6d078b6 100755
--- a/android/src/com/android/tools/idea/templates/Template.java
+++ b/android/src/com/android/tools/idea/templates/Template.java
@@ -314,6 +314,7 @@
paramMap.put("escapeXmlString", new FmEscapeXmlStringMethod());
paramMap.put("escapePropertyValue", new FmEscapePropertyValueMethod());
paramMap.put("extractLetters", new FmExtractLettersMethod());
+ paramMap.put("hasDependency", new FmHasDependencyMethod(paramMap));
// Dependency list
paramMap.put(TemplateMetadata.ATTR_DEPENDENCIES_LIST, new LinkedList<String>());
diff --git a/android/src/com/android/tools/idea/welcome/AndroidSdk.java b/android/src/com/android/tools/idea/welcome/AndroidSdk.java
index c30e6ca..deb4c72 100644
--- a/android/src/com/android/tools/idea/welcome/AndroidSdk.java
+++ b/android/src/com/android/tools/idea/welcome/AndroidSdk.java
@@ -15,27 +15,19 @@
*/
package com.android.tools.idea.welcome;
+import com.android.annotations.VisibleForTesting;
+import com.android.sdklib.AndroidVersion;
import com.android.sdklib.devices.Storage;
-import com.android.tools.idea.sdk.DefaultSdks;
-import com.android.tools.idea.sdk.SdkMerger;
+import com.android.sdklib.repository.FullRevision;
+import com.android.sdklib.repository.MajorRevision;
+import com.android.sdklib.repository.descriptors.IdDisplay;
+import com.android.sdklib.repository.descriptors.PkgDesc;
import com.android.tools.idea.wizard.DynamicWizardStep;
import com.android.tools.idea.wizard.ScopedStateStore;
-import com.google.common.collect.ImmutableSet;
-import com.intellij.execution.ui.ConsoleViewContentType;
-import com.intellij.openapi.application.Application;
-import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.progress.ProgressIndicator;
-import com.intellij.openapi.progress.ProgressManager;
-import com.intellij.openapi.util.io.FileUtil;
-import com.intellij.openapi.util.text.StringUtil;
-import com.intellij.util.download.DownloadableFileDescription;
-import com.intellij.util.download.DownloadableFileService;
+import com.intellij.openapi.util.SystemInfo;
import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
import java.io.File;
-import java.io.IOException;
-import java.util.Set;
/**
* Android SDK installable component.
@@ -44,117 +36,54 @@
public static final long SIZE = 2300 * Storage.Unit.MiB.getNumberOfBytes();
private static final ScopedStateStore.Key<Boolean> KEY_INSTALL_SDK =
ScopedStateStore.createKey("download.sdk", ScopedStateStore.Scope.PATH, Boolean.class);
- private static final ScopedStateStore.Key<String> KEY_SDK_INSTALL_LOCATION =
- ScopedStateStore.createKey("download.sdk.location", ScopedStateStore.Scope.PATH, String.class);
- private final DownloadableFileDescription myAndroidSdkDescription = DownloadableFileService.getInstance()
- .createFileDescription(FirstRunWizardDefaults.getSdkDownloadUrl(), FirstRunWizardDefaults.ANDROID_SDK_ARCHIVE_FILE_NAME);
- private final ScopedStateStore.Key<Boolean> myKeyCustomInstall;
- private ScopedStateStore myState;
- private SdkComponentsStep myStep;
- public AndroidSdk(ScopedStateStore.Key<Boolean> keyCustomInstall) {
+ public AndroidSdk() {
super("Android SDK", SIZE, KEY_INSTALL_SDK);
- myKeyCustomInstall = keyCustomInstall;
+ }
+
+ @VisibleForTesting
+ static PkgDesc.Builder[] getPackages() {
+ AndroidVersion lVersion = new AndroidVersion(21, null);
+ MajorRevision unspecifiedRevision = new MajorRevision(FullRevision.NOT_SPECIFIED);
+
+ PkgDesc.Builder androidSdkTools = PkgDesc.Builder.newTool(FullRevision.NOT_SPECIFIED, FullRevision.NOT_SPECIFIED);
+ PkgDesc.Builder androidSdkPlatformTools = PkgDesc.Builder.newPlatformTool(FullRevision.NOT_SPECIFIED);
+ PkgDesc.Builder androidSdkBuildTools = PkgDesc.Builder.newBuildTool(new FullRevision(21, 0, 2));
+ PkgDesc.Builder supportRepository = InstallComponentsPath.createExtra(true, "android", "m2repository");
+ PkgDesc.Builder googleRepository = InstallComponentsPath.createExtra(true, "google", "m2repository");
+ PkgDesc.Builder atomImage = PkgDesc.Builder.newSysImg(lVersion, new IdDisplay("default", ""), "x86", unspecifiedRevision);
+ PkgDesc.Builder platform = PkgDesc.Builder.newPlatform(lVersion, unspecifiedRevision, FullRevision.NOT_SPECIFIED);
+ PkgDesc.Builder sample = PkgDesc.Builder.newSample(lVersion, unspecifiedRevision, FullRevision.NOT_SPECIFIED);
+ PkgDesc.Builder platformSources = PkgDesc.Builder.newSource(lVersion, unspecifiedRevision);
+ PkgDesc.Builder usb = InstallComponentsPath.createExtra(SystemInfo.isLinux || SystemInfo.isWindows, "google", "usb_driver");
+
+ return new PkgDesc.Builder[]{androidSdkTools, androidSdkPlatformTools, androidSdkBuildTools, supportRepository, googleRepository,
+ atomImage, platform, sample, platformSources, usb};
}
@NotNull
@Override
- public Set<DownloadableFileDescription> getFilesToDownloadAndExpand() {
- if (getHandoffAndroidSdkSource() == null) {
- return ImmutableSet.of(myAndroidSdkDescription);
- }
- else {
- return ImmutableSet.of();
- }
+ public PkgDesc.Builder[] getRequiredSdkPackages() {
+ return getPackages();
}
@Override
- public void init(ScopedStateStore state) {
- myState = state;
- InstallerData data = InstallerData.get(state);
- String location;
- if (data.exists()) {
- location = data.getAndroidDest();
- }
- else {
- location = FirstRunWizardDefaults.getDefaultSdkLocation();
- }
+ public void init(@NotNull ScopedStateStore state, @NotNull ProgressStep progressStep) {
state.put(KEY_INSTALL_SDK, true);
- state.put(KEY_SDK_INSTALL_LOCATION, location);
}
@Override
public DynamicWizardStep[] createSteps() {
- myStep = new SdkComponentsStep(InstallComponentsPath.COMPONENTS, myKeyCustomInstall, KEY_SDK_INSTALL_LOCATION);
- return new DynamicWizardStep[]{myStep};
+ return new DynamicWizardStep[]{};
}
@Override
- public boolean hasVisibleStep() {
- return myStep.isStepVisible();
- }
-
- @Override
- public void perform(@NotNull InstallContext downloaded) throws WizardException {
- String destinationPath = myState.get(KEY_SDK_INSTALL_LOCATION);
- assert destinationPath != null;
- final File destination = new File(destinationPath);
- File source = getHandoffAndroidSdkSource();
- if (FileUtil.filesEqual(source, destination)) {
- return;
- }
- ProgressStep progressStep = downloaded.getProgressStep();
- ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
- indicator.setText("Installing Android SDK");
- indicator.setIndeterminate(true);
- if (source == null) {
- source = downloaded.getExpandedLocation(myAndroidSdkDescription);
- assert source != null && source.isDirectory();
- }
- try {
- FileUtil.ensureExists(destination);
- if (!FileUtil.filesEqual(destination.getCanonicalFile(), source.getCanonicalFile())) {
- if (SdkMerger.hasMergeableContent(source, destination)) {
- SdkMerger.mergeSdks(source, destination, indicator);
- }
- }
- progressStep.print(String.format("Android SDK was installed to %s", destination), ConsoleViewContentType.SYSTEM_OUTPUT);
- final Application application = ApplicationManager.getApplication();
- // SDK can only be set from write action, write action can only be started from UI thread
- ApplicationManager.getApplication().invokeAndWait(new Runnable() {
- @Override
- public void run() {
- application.runWriteAction(new Runnable() {
- @Override
- public void run() {
- DefaultSdks.setDefaultAndroidHome(destination);
- AndroidFirstRunPersistentData.getInstance().markSdkUpToDate();
- }
- });
- }
- }, application.getAnyModalityState());
- }
- catch (IOException e) {
- throw new WizardException(WelcomeUIUtils.getMessageWithDetails("Unable to install Android SDK", e.getMessage()), e);
- }
+ public void configure(@NotNull InstallContext installContext, @NotNull File sdk) {
+ // Nothing to do, having components installed is enough
}
@Override
public boolean isOptional() {
return false;
}
-
- @Nullable
- private File getHandoffAndroidSdkSource() {
- InstallerData data = InstallerData.get(myState);
- String androidSrc = data.getAndroidSrc();
- if (!StringUtil.isEmpty(androidSrc)) {
- File srcFolder = new File(androidSrc);
- File[] files = srcFolder.listFiles();
- if (srcFolder.isDirectory() && files != null && files.length > 0) {
- return srcFolder;
- }
- }
- return null;
- }
}
diff --git a/android/src/com/android/tools/idea/welcome/DownloadOperation.java b/android/src/com/android/tools/idea/welcome/DownloadOperation.java
index 36ebcfa..f7da0bd5 100644
--- a/android/src/com/android/tools/idea/welcome/DownloadOperation.java
+++ b/android/src/com/android/tools/idea/welcome/DownloadOperation.java
@@ -15,11 +15,13 @@
*/
package com.android.tools.idea.welcome;
+import com.google.common.collect.ImmutableList;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.download.DownloadableFileDescription;
import com.intellij.util.download.DownloadableFileService;
import com.intellij.util.download.FileDownloader;
+import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
@@ -28,23 +30,24 @@
/**
* Downloads files needed to setup Android Studio.
*/
-public final class DownloadOperation extends PreinstallOperation {
+public final class DownloadOperation extends PreinstallOperation<File> {
+ private final String myUrl;
- public DownloadOperation(InstallContext context) {
- super(context, 0.7);
+ public DownloadOperation(InstallContext context, String url, double progressShare) {
+ super(context, progressShare);
+ myUrl = url;
}
@Override
- protected void perform() throws WizardException {
+ @Nullable
+ protected File perform() throws WizardException {
DownloadableFileService fileService = DownloadableFileService.getInstance();
- FileDownloader downloader = fileService.createDownloader(myContext.getFilesToDownload(), "Android Studio components");
- do {
+ DownloadableFileDescription myDescription = fileService.createFileDescription(myUrl, "components.zip");
+ FileDownloader downloader = fileService.createDownloader(ImmutableList.of(myDescription), "Android Studio components");
+ while (true) {
try {
List<Pair<File, DownloadableFileDescription>> result = downloader.download(myContext.getTempDirectory());
- for (Pair<File, DownloadableFileDescription> fileDescriptionPair : result) {
- myContext.setDownloadedLocation(fileDescriptionPair.getSecond(), fileDescriptionPair.getFirst());
- }
- break;
+ return result.size() == 1 ? result.get(0).getFirst() : null;
}
catch (IOException e) {
String details = StringUtil.isEmpty(e.getMessage()) ? "." : (": " + e.getMessage());
@@ -52,6 +55,5 @@
promptToRetry(message + " Please check your Internet connection and retry.", message, e);
}
}
- while (true);
}
}
diff --git a/android/src/com/android/tools/idea/welcome/FirstRunWizard.java b/android/src/com/android/tools/idea/welcome/FirstRunWizard.java
index fbbb7d4..412d85c 100644
--- a/android/src/com/android/tools/idea/welcome/FirstRunWizard.java
+++ b/android/src/com/android/tools/idea/welcome/FirstRunWizard.java
@@ -38,7 +38,7 @@
*/
private final AtomicInteger myFinishClicks = new AtomicInteger(0);
private final SetupJdkPath myJdkPath = new SetupJdkPath();
- private final InstallComponentsPath myComponentsPath = new InstallComponentsPath();
+ private InstallComponentsPath myComponentsPath;
public FirstRunWizard(DynamicWizardHost host) {
super(null, null, WIZARD_TITLE, host);
@@ -47,6 +47,8 @@
@Override
public void init() {
+ SetupProgressStep progressStep = new SetupProgressStep();
+ myComponentsPath = new InstallComponentsPath(progressStep);
addPath(new SingleStepPath(new FirstRunWelcomeStep()));
addPath(myJdkPath);
addPath(myComponentsPath);
@@ -56,7 +58,7 @@
return super.isStepVisible() && !InstallerData.get(myState).exists();
}
}));
- addPath(new SingleStepPath(new SetupProgressStep()));
+ addPath(new SingleStepPath(progressStep));
super.init();
}
@@ -79,7 +81,7 @@
break;
}
if (path instanceof LongRunningOperationPath) {
- ((LongRunningOperationPath)path).runLongOperation(progressStep);
+ ((LongRunningOperationPath)path).runLongOperation();
}
}
}
diff --git a/android/src/com/android/tools/idea/welcome/FirstRunWizardDefaults.java b/android/src/com/android/tools/idea/welcome/FirstRunWizardDefaults.java
index 1e1b0b1..4ee0d27 100644
--- a/android/src/com/android/tools/idea/welcome/FirstRunWizardDefaults.java
+++ b/android/src/com/android/tools/idea/welcome/FirstRunWizardDefaults.java
@@ -16,31 +16,34 @@
package com.android.tools.idea.welcome;
import com.android.sdklib.devices.Storage;
+import com.google.common.collect.Iterables;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.util.SystemInfo;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.util.PathUtil;
+import com.intellij.util.Urls;
+import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.NotNull;
+import java.io.File;
+import java.net.MalformedURLException;
+import java.util.List;
+
/**
* The goal is to keep all defaults in one place so it is easier to update
* them as needed.
*/
public class FirstRunWizardDefaults {
- public static final String HAXM_INSTALLER_ARCHIVE_FILE_NAME = "haxm.zip";
public static final String HAXM_DOCUMENTATION_URL = "http://www.intel.com/software/android/";
- public static final String ANDROID_SDK_ARCHIVE_FILE_NAME = "androidsdk.zip";
private FirstRunWizardDefaults() {
// Do nothing
}
/**
- * @return Intel® HAXM download URL
- */
- @NotNull
- public static String getHaxmDownloadUrl() {
- return "https://github.com/vladikoff/chromeos-apk/archive/master.zip";
- }
-
- /**
* @return Recommended memory allocation given the computer RAM size
*/
public static int getRecommendedHaxmMemory(long memorySize) {
@@ -65,7 +68,23 @@
*/
@NotNull
public static String getSdkDownloadUrl() {
- return "https://github.com/FezVrasta/bootstrap-material-design/archive/master.zip";
+ String url = System.getProperty("android.sdkurl");
+ if (!StringUtil.isEmptyOrSpaces(url)) {
+ File file = new File(url);
+ if (file.isFile()) {
+ // Can't use any path => URL utilities as they don't add two slashes
+ // after the protocol as required by IJ downloader
+ return LocalFileSystem.PROTOCOL_PREFIX + PathUtil.toSystemIndependentName(file.getAbsolutePath());
+ }
+ else {
+ System.err.println("File " + file.getAbsolutePath() + " does not exist.");
+ }
+ }
+ String downloadUrl = AndroidSdkUtils.getSdkDownloadUrl();
+ if (downloadUrl == null) {
+ throw new IllegalStateException("Unsupported OS");
+ }
+ return downloadUrl;
}
/**
@@ -73,15 +92,18 @@
*/
@NotNull
public static String getDefaultSdkLocation() {
+ List<Sdk> sdks = AndroidSdkUtils.getAllAndroidSdks();
+ Sdk sdk = Iterables.getFirst(sdks, null);
+ if (sdk != null && !StringUtil.isEmptyOrSpaces(sdk.getHomePath())) {
+ return sdk.getHomePath();
+ }
// TODO Need exact paths
+ String userHome = System.getProperty("user.home");
if (SystemInfo.isWindows) {
- return "C:\\Android SDK";
+ return FileUtil.join(userHome, "AppData", "Local", "Android", "Sdk");
}
- else if (SystemInfo.isLinux) {
- return "/usr/local/androidsdk";
- }
- else if (SystemInfo.isMac) {
- return String.format("%s/Android SDK", System.getProperty("user.home"));
+ else if (SystemInfo.isLinux || SystemInfo.isMac) {
+ return FileUtil.join(userHome, "Android", "Sdk");
}
else {
throw new IllegalStateException("Unsupported OS");
diff --git a/android/src/com/android/tools/idea/welcome/Haxm.java b/android/src/com/android/tools/idea/welcome/Haxm.java
index 080cd68..99ce200 100644
--- a/android/src/com/android/tools/idea/welcome/Haxm.java
+++ b/android/src/com/android/tools/idea/welcome/Haxm.java
@@ -15,12 +15,14 @@
*/
package com.android.tools.idea.welcome;
+import com.android.SdkConstants;
import com.android.sdklib.devices.Storage;
+import com.android.sdklib.repository.descriptors.PkgDesc;
import com.android.tools.idea.wizard.DynamicWizardStep;
import com.android.tools.idea.wizard.ScopedStateStore;
import com.google.common.base.Joiner;
-import com.google.common.collect.ImmutableSet;
import com.intellij.execution.ExecutionException;
+import com.intellij.execution.Platform;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.process.CapturingAnsiEscapesAwareProcessHandler;
import com.intellij.execution.ui.ConsoleViewContentType;
@@ -28,8 +30,7 @@
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.SystemInfo;
-import com.intellij.util.download.DownloadableFileDescription;
-import com.intellij.util.download.DownloadableFileService;
+import com.intellij.openapi.util.io.FileUtil;
import org.jetbrains.annotations.NotNull;
import java.io.File;
@@ -37,7 +38,6 @@
import java.lang.management.OperatingSystemMXBean;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
-import java.util.Set;
/**
* Intel® HAXM installable component
@@ -45,28 +45,39 @@
public final class Haxm extends InstallableComponent {
// In UI we cannot use longs, so we need to pick a unit other then byte
public static final Storage.Unit UI_UNITS = Storage.Unit.MiB;
+ public static final Logger LOG = Logger.getInstance(Haxm.class);
+ public static final String COMPONENT_VENDOR = "intel";
+ public static final String COMPONENT_PATH = "Hardware_Accelerated_Execution_Manager";
private static final ScopedStateStore.Key<Boolean> KEY_INSTALL_HAXM =
ScopedStateStore.createKey("install.haxm", ScopedStateStore.Scope.PATH, Boolean.class);
private static final ScopedStateStore.Key<Integer> KEY_EMULATOR_MEMORY_MB =
ScopedStateStore.createKey("emulator.memory", ScopedStateStore.Scope.PATH, Integer.class);
-
- private static final Logger LOG = Logger.getInstance(Haxm.class);
-
- private static final DownloadableFileDescription myHaxmInstaller = DownloadableFileService.getInstance()
- .createFileDescription(FirstRunWizardDefaults.getHaxmDownloadUrl(), FirstRunWizardDefaults.HAXM_INSTALLER_ARCHIVE_FILE_NAME);
-
+ private static long memorySize = -1;
private final ScopedStateStore.Key<Boolean> myIsCustomInstall;
private ScopedStateStore myState;
+ private ProgressStep myProgressStep;
+
public Haxm(ScopedStateStore.Key<Boolean> isCustomInstall) {
super("SDK Emulator Extra - Intel® HAXM", 2306867, KEY_INSTALL_HAXM);
myIsCustomInstall = isCustomInstall;
}
+ public static boolean canRun() {
+ // TODO HAXM is disabled as headless install is not in the repositories yet
+ if (Boolean.getBoolean("install.haxm") && (SystemInfo.isWindows || SystemInfo.isMac)) {
+ return getMemorySize() >= Storage.Unit.GiB.getNumberOfBytes();
+ }
+ else {
+ return false;
+ }
+ }
+
+ @NotNull
private static GeneralCommandLine getMacHaxmInstallCommandLine(File source, int memorySize) {
String shellScript = getAbsolutePathString(source, "silent_install.sh");
String diskImage = getAbsolutePathString(source, "IntelHAXM_1.1.0_below_10.10.dmg");
- // Explicitely calling bash so we don't have to deal with permissions on the shell script file.
+ // Explicitly calling bash so we don't have to deal with permissions on the shell script file.
// Note that bash is preinstalled on Mac OS X so we can rely on it.
String[] installerInvocation = {"/bin/bash", shellScript, "-f", diskImage, "-m", String.valueOf(memorySize)};
@@ -83,6 +94,7 @@
return "'" + new File(source, filename).getAbsolutePath() + "'";
}
+ @NotNull
private static GeneralCommandLine getWindowsHaxmInstallCommandLine(File source, int memorySize) {
// TODO
GeneralCommandLine commandLine = new GeneralCommandLine();
@@ -97,6 +109,13 @@
}
public static long getMemorySize() {
+ if (memorySize < 0) {
+ memorySize = checkMemorySize();
+ }
+ return memorySize;
+ }
+
+ private static long checkMemorySize() {
OperatingSystemMXBean osMXBean = ManagementFactory.getOperatingSystemMXBean();
// This is specific to JDKs derived from Oracle JDK (including OpenJDK and Apple JDK among others).
// Other then this, there's no standard way of getting memory size
@@ -125,15 +144,10 @@
return 32L * Storage.Unit.GiB.getNumberOfBytes();
}
- @NotNull
@Override
- public Set<DownloadableFileDescription> getFilesToDownloadAndExpand() {
- return ImmutableSet.of(myHaxmInstaller);
- }
-
- @Override
- public void init(ScopedStateStore state) {
+ public void init(@NotNull ScopedStateStore state, @NotNull ProgressStep progressStep) {
myState = state;
+ myProgressStep = progressStep;
state.put(KEY_EMULATOR_MEMORY_MB, getRecommendedMemoryAllocation());
state.put(KEY_INSTALL_HAXM, true);
}
@@ -144,44 +158,59 @@
}
@Override
- public boolean hasVisibleStep() {
- return true;
+ public void configure(@NotNull InstallContext installContext, @NotNull File sdk) {
+ if (!canRun()) {
+ Logger.getInstance(getClass()).error("Tried to install HAXM on %s OS with %s memory size", Platform.current().name(),
+ String.valueOf(getMemorySize()));
+ installContext.print("Unable to install Intel HAXM", ConsoleViewContentType.ERROR_OUTPUT);
+ return;
+ }
+ GeneralCommandLine commandLine = getCommandLine(sdk);
+ try {
+ CapturingAnsiEscapesAwareProcessHandler process = new CapturingAnsiEscapesAwareProcessHandler(commandLine);
+ myProgressStep.attachToProcess(process);
+ int exitCode = process.runProcess().getExitCode();
+ if (exitCode != 0) {
+ // HAXM is not required so we do not stop setup process if this install failed.
+ myProgressStep.print("HAXM installation failed. To install HAXM follow the instructions found at " +
+ FirstRunWizardDefaults.HAXM_DOCUMENTATION_URL + ".", ConsoleViewContentType.ERROR_OUTPUT);
+ }
+ else {
+ myProgressStep.print("Completed HAXM installation\n", ConsoleViewContentType.SYSTEM_OUTPUT);
+ }
+ }
+ catch (ExecutionException e) {
+ installContext.print("Unable to run Intel HAXM installer: " + e.getMessage(), ConsoleViewContentType.ERROR_OUTPUT);
+ LOG.error(e);
+ }
}
- @Override
- public void perform(@NotNull InstallContext context) throws WizardException {
+ /**
+ * @return command line object or <code>null</code> if OS is not supported
+ */
+ @NotNull
+ private GeneralCommandLine getCommandLine(File sdk) {
ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator();
progressIndicator.setIndeterminate(true);
progressIndicator.setText("Running Intel® HAXM installer");
int memorySize = myState.getNotNull(KEY_EMULATOR_MEMORY_MB, getRecommendedMemoryAllocation());
- File sourceLocation = context.getExpandedLocation(myHaxmInstaller);
- GeneralCommandLine commandLine;
+ String path = FileUtil.join(SdkConstants.FD_EXTRAS, COMPONENT_VENDOR, COMPONENT_PATH);
+ File sourceLocation = new File(sdk, path);
if (SystemInfo.isMac) {
- commandLine = getMacHaxmInstallCommandLine(sourceLocation, memorySize);
+ return getMacHaxmInstallCommandLine(sourceLocation, memorySize);
}
else if (SystemInfo.isWindows) {
- commandLine = getWindowsHaxmInstallCommandLine(sourceLocation, memorySize);
+ return getWindowsHaxmInstallCommandLine(sourceLocation, memorySize);
}
else {
- throw new WizardException("Unsupported OS");
+ assert !canRun();
+ throw new IllegalStateException("Usupported OS");
}
- try {
- CapturingAnsiEscapesAwareProcessHandler process = new CapturingAnsiEscapesAwareProcessHandler(commandLine);
- ProgressStep progressStep = context.getProgressStep();
- progressStep.attachToProcess(process);
- int exitCode = process.runProcess().getExitCode();
- if (exitCode != 0) {
- // HAXM is not required so we do not stop setup process if this install failed.
- progressStep.print("HAXM installation failed. To install HAXM follow the instructions found at " +
- FirstRunWizardDefaults.HAXM_DOCUMENTATION_URL + ".",
- ConsoleViewContentType.ERROR_OUTPUT);
- }
- else {
- progressStep.print("Completed HAXM installation\n", ConsoleViewContentType.SYSTEM_OUTPUT);
- }
- }
- catch (ExecutionException e) {
- throw new WizardException("Unable to run HAXM installer", e);
- }
+ }
+
+ @NotNull
+ @Override
+ public PkgDesc.Builder[] getRequiredSdkPackages() {
+ return new PkgDesc.Builder[]{InstallComponentsPath.createExtra(true, COMPONENT_VENDOR, COMPONENT_PATH)};
}
}
diff --git a/android/src/com/android/tools/idea/welcome/InstallComponentsPath.java b/android/src/com/android/tools/idea/welcome/InstallComponentsPath.java
index 5b0cbc0..7267b21 100644
--- a/android/src/com/android/tools/idea/welcome/InstallComponentsPath.java
+++ b/android/src/com/android/tools/idea/welcome/InstallComponentsPath.java
@@ -15,20 +15,39 @@
*/
package com.android.tools.idea.welcome;
+import com.android.SdkConstants;
+import com.android.sdklib.SdkManager;
+import com.android.sdklib.internal.repository.updater.SdkUpdaterNoWindow;
+import com.android.sdklib.repository.FullRevision;
+import com.android.sdklib.repository.NoPreviewRevision;
+import com.android.sdklib.repository.descriptors.IdDisplay;
+import com.android.sdklib.repository.descriptors.PkgDesc;
+import com.android.tools.idea.sdk.DefaultSdks;
+import com.android.tools.idea.sdk.SdkMerger;
import com.android.tools.idea.wizard.DynamicWizardPath;
import com.android.tools.idea.wizard.DynamicWizardStep;
import com.android.tools.idea.wizard.ScopedStateStore;
-import com.google.common.collect.ImmutableList;
+import com.android.utils.ILogger;
+import com.android.utils.NullLogger;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
-import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.execution.ui.ConsoleViewContentType;
+import com.intellij.openapi.application.Application;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.SystemInfo;
+import com.intellij.openapi.util.ThrowableComputable;
import com.intellij.openapi.util.io.FileUtil;
-import com.intellij.util.download.DownloadableFileDescription;
+import com.intellij.openapi.util.text.StringUtil;
+import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
+import java.util.Collection;
import java.util.List;
import java.util.Set;
@@ -41,13 +60,20 @@
public class InstallComponentsPath extends DynamicWizardPath implements LongRunningOperationPath {
public static final ScopedStateStore.Key<Boolean> KEY_CUSTOM_INSTALL =
ScopedStateStore.createKey("custom.install", ScopedStateStore.Scope.PATH, Boolean.class);
- public static final InstallableComponent[] COMPONENTS = createComponents();
- private static final Logger LOG = Logger.getInstance(InstallComponentsPath.class);
+ private static final InstallableComponent[] COMPONENTS = createComponents();
+ private static final ScopedStateStore.Key<String> KEY_SDK_INSTALL_LOCATION =
+ ScopedStateStore.createKey("download.sdk.location", ScopedStateStore.Scope.PATH, String.class);
+ private final ProgressStep myProgressStep;
private InstallationTypeWizardStep myInstallationTypeWizardStep;
+ private SdkComponentsStep mySdkComponentsStep;
+
+ public InstallComponentsPath(@NotNull ProgressStep progressStep) {
+ myProgressStep = progressStep;
+ }
private static InstallableComponent[] createComponents() {
- AndroidSdk androidSdk = new AndroidSdk(KEY_CUSTOM_INSTALL);
- if (SystemInfo.isWindows || SystemInfo.isMac) {
+ AndroidSdk androidSdk = new AndroidSdk();
+ if (Haxm.canRun()) {
return new InstallableComponent[]{androidSdk, new Haxm(KEY_CUSTOM_INSTALL)};
}
else {
@@ -66,20 +92,172 @@
return tempDirectory;
}
+ private static boolean hasPlatformsDir(@Nullable File[] files) {
+ if (files == null) {
+ return false;
+ }
+ for (File file : files) {
+ if (isPlatformsDir(file)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isPlatformsDir(File file) {
+ return file.isDirectory() && file.getName().equalsIgnoreCase(SdkConstants.FD_PLATFORMS);
+ }
+
+ /**
+ * This is an attempt to isolate from SDK packaging peculiarities.
+ */
+ @NotNull
+ private static File getSdkRoot(@NotNull File expandedLocation) {
+ File[] files = expandedLocation.listFiles();
+ // Breadth-first scan - to lower chance of false positive
+ if (hasPlatformsDir(files)) {
+ return expandedLocation;
+ }
+ // Only scan one level down (no recursion) - avoid false positives
+ if (files != null) {
+ for (File file : files) {
+ if (hasPlatformsDir(file.listFiles())) {
+ return file;
+ }
+ }
+ }
+ return expandedLocation;
+ }
+
+ @VisibleForTesting
+ static boolean downloadAndUnzipSdkSeed(@NotNull InstallContext context, @NotNull File destination, double progressShare)
+ throws WizardException {
+ final double DOWNLOAD_OPERATION_PROGRESS_SHARE = progressShare * 0.8;
+ final double UNZIP_OPERATION_PROGRESS_SHARE = progressShare - DOWNLOAD_OPERATION_PROGRESS_SHARE;
+
+ File file = new DownloadOperation(context, FirstRunWizardDefaults.getSdkDownloadUrl(), DOWNLOAD_OPERATION_PROGRESS_SHARE).execute();
+ try {
+ return unzip(context, file, destination, UNZIP_OPERATION_PROGRESS_SHARE);
+ }
+ finally {
+ if (file != null && file.isFile() && file.getAbsolutePath().startsWith(context.getTempDirectory().getAbsolutePath())) {
+ FileUtil.delete(file);
+ }
+ }
+ }
+
+ private static boolean unzip(@NotNull InstallContext context, @Nullable File archive, @NotNull File destination, double progressShare)
+ throws WizardException {
+ if (archive == null) {
+ return false;
+ }
+ try {
+ FileUtil.ensureExists(destination.getParentFile());
+ File unpacked = new UnzipOperation(context, archive, progressShare).execute();
+ if (unpacked != null) {
+ try {
+ FileUtil.rename(getSdkRoot(unpacked), destination);
+ }
+ finally {
+ if (unpacked.isDirectory()) {
+ FileUtil.delete(unpacked);
+ }
+ }
+ return true;
+ }
+ }
+ catch (IOException e) {
+ throw new WizardException("Unable to prepare Android SDK", e);
+ }
+ return false;
+ }
+
+ @Nullable
+ @Contract("false, _, _ -> null;true, _, _ -> !null")
+ public static PkgDesc.Builder createExtra(boolean shouldInstallFlag, String vendor, String path) {
+ if (!shouldInstallFlag) {
+ return null;
+ }
+ return PkgDesc.Builder.newExtra(new IdDisplay(vendor, ""), path, "", null, new NoPreviewRevision(FullRevision.MISSING_MAJOR_REV));
+ }
+
+ private static File mergeRepoIntoDestination(final InstallContext context,
+ @NotNull final File repo,
+ @NotNull final File destination,
+ double progressRatio) throws WizardException {
+ try {
+ return context.run(new MergeOperation(destination, repo, context), progressRatio);
+ }
+ catch (IOException e) {
+ throw new WizardException(e.getMessage(), e);
+ }
+ }
+
+ private static boolean existsAndIsVisible(DynamicWizardStep step) {
+ return step != null && step.isStepVisible();
+ }
+
+ @VisibleForTesting
+ static void setupSdkComponents(@NotNull InstallContext installContext,
+ @NotNull File sdk,
+ @NotNull Collection<? extends InstallableComponent> selectedComponents,
+ double progressRatio) throws WizardException {
+ // TODO: Prompt about connection in handoff case?
+ Set<String> packages = Sets.newHashSet();
+ for (InstallableComponent component1 : selectedComponents) {
+ for (PkgDesc.Builder pkg : component1.getRequiredSdkPackages()) {
+ if (pkg != null) {
+ packages.add(pkg.create().getInstallId());
+ }
+ }
+ }
+ installContext.run(new InstallComponentsOperation(installContext, sdk, packages), progressRatio);
+ for (InstallableComponent component : selectedComponents) {
+ component.configure(installContext, sdk);
+ }
+ }
+
+ private static void setSdkInPreferences(final File sdk) {
+ final Application application = ApplicationManager.getApplication();
+ // SDK can only be set from write action, write action can only be started from UI thread
+ ApplicationManager.getApplication().invokeAndWait(new Runnable() {
+ @Override
+ public void run() {
+ application.runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ DefaultSdks.setDefaultAndroidHome(sdk, null);
+ AndroidFirstRunPersistentData.getInstance().markSdkUpToDate();
+ }
+ });
+ }
+ }, application.getAnyModalityState());
+ }
+
@Override
protected void init() {
- boolean handoff = InstallerData.get(myState).exists();
- if (!handoff) {
+ InstallerData data = InstallerData.get(myState);
+ String location = null;
+ if (!data.exists()) {
myInstallationTypeWizardStep = new InstallationTypeWizardStep(KEY_CUSTOM_INSTALL);
+ location = data.getAndroidDest();
addStep(myInstallationTypeWizardStep);
}
+ if (StringUtil.isEmptyOrSpaces(location)) {
+ location = FirstRunWizardDefaults.getDefaultSdkLocation();
+ }
+ myState.put(KEY_SDK_INSTALL_LOCATION, location);
+
+ mySdkComponentsStep = new SdkComponentsStep(COMPONENTS, KEY_CUSTOM_INSTALL, KEY_SDK_INSTALL_LOCATION);
+ addStep(mySdkComponentsStep);
+
for (InstallableComponent component : COMPONENTS) {
- component.init(myState);
+ component.init(myState, myProgressStep);
for (DynamicWizardStep step : component.createSteps()) {
addStep(step);
}
}
- if (SystemInfo.isLinux && !handoff) {
+ if (SystemInfo.isLinux && !data.exists()) {
addStep(new LinuxHaxmInfoStep());
}
}
@@ -90,6 +268,19 @@
return "Setup Android Studio Components";
}
+ @Override
+ public void runLongOperation() throws WizardException {
+ final double INIT_SDK_OPERATION_PROGRESS_SHARE = 0.3;
+ final double INSTALL_COMPONENTS_OPERATION_PROGRESS_SHARE = 1.0 - INIT_SDK_OPERATION_PROGRESS_SHARE;
+
+ InstallContext installContext = new InstallContext(createTempDir(), myProgressStep);
+ final File sdk = initializeSdk(installContext, INIT_SDK_OPERATION_PROGRESS_SHARE);
+ if (sdk != null) {
+ setSdkInPreferences(sdk);
+ setupSdkComponents(installContext, sdk, getSelectedComponents(), INSTALL_COMPONENTS_OPERATION_PROGRESS_SHARE);
+ }
+ }
+
private List<InstallableComponent> getSelectedComponents() throws WizardException {
boolean customInstall = myState.getNotNull(KEY_CUSTOM_INSTALL, true);
List<InstallableComponent> selectedOperations = Lists.newArrayListWithCapacity(COMPONENTS.length);
@@ -102,32 +293,46 @@
return selectedOperations;
}
- @Override
- public void runLongOperation(@NotNull ProgressStep progressStep) throws WizardException {
- List<InstallableComponent> selectedComponents = getSelectedComponents();
- File tempDirectory = createTempDir();
+ /**
+ * @return SDK location or <code>null</code> if the operation was cancelled
+ */
+ @Nullable
+ private File initializeSdk(InstallContext installContext, double progressRatio) throws WizardException {
+ String destinationPath = myState.get(KEY_SDK_INSTALL_LOCATION);
+ assert destinationPath != null;
+ final File destination = new File(destinationPath);
+ if (destination.isFile()) {
+ throw new WizardException(String.format("Path %s does not point to a directory", destination));
+ }
+ else if (destination.isDirectory()) {
+ SdkManager manager = SdkManager.createManager(destination.getAbsolutePath(), new NullLogger());
+ if (manager != null) {
+ installContext.advance(progressRatio);
+ // We got ourselves an SDK
+ return destination;
+ }
+ }
+ File handoffSource = getHandoffAndroidSdkSource();
+ if (handoffSource == null) {
+ return downloadAndUnzipSdkSeed(installContext, destination, progressRatio) ? destination : null;
+ }
+ else {
+ return mergeRepoIntoDestination(installContext, handoffSource, destination, progressRatio);
+ }
+ }
- Set<DownloadableFileDescription> descriptions = Sets.newHashSet();
- for (InstallableComponent component : selectedComponents) {
- descriptions.addAll(component.getFilesToDownloadAndExpand());
- }
- InstallContext installContext = new InstallContext(tempDirectory, descriptions, progressStep);
- List<PreinstallOperation> preinstallOperations =
- ImmutableList.of(new DownloadOperation(installContext), new UnzipOperation(installContext));
- try {
- for (PreinstallOperation operation : preinstallOperations) {
- if (!operation.execute()) {
- return;
- }
- }
- progressStep.getProgressIndicator().setIndeterminate(true);
- for (InstallableComponent component : selectedComponents) {
- component.perform(installContext);
+ @Nullable
+ private File getHandoffAndroidSdkSource() {
+ InstallerData data = InstallerData.get(myState);
+ String androidSrc = data.getAndroidSrc();
+ if (!StringUtil.isEmpty(androidSrc)) {
+ File srcFolder = new File(androidSrc);
+ File[] files = srcFolder.listFiles();
+ if (srcFolder.isDirectory() && files != null && files.length > 0) {
+ return srcFolder;
}
}
- finally {
- installContext.cleanup(progressStep);
- }
+ return null;
}
@Override
@@ -142,21 +347,58 @@
}
public boolean showsStep() {
- if (isPathVisible()) {
- if (myInstallationTypeWizardStep != null && myInstallationTypeWizardStep.isStepVisible()) {
- return true;
+ return isPathVisible() && (existsAndIsVisible(myInstallationTypeWizardStep) || existsAndIsVisible(mySdkComponentsStep));
+ }
+
+ private static class MergeOperation implements ThrowableComputable<File, IOException> {
+ private final File myDestination;
+ private final File myRepo;
+ private final InstallContext myContext;
+
+ public MergeOperation(File destination, File repo, InstallContext context) {
+ myDestination = destination;
+ myRepo = repo;
+ myContext = context;
+ }
+
+ @Override
+ public File compute() throws IOException {
+ ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
+ indicator.setText("Installing Android SDK");
+ indicator.setIndeterminate(true);
+ FileUtil.ensureExists(myDestination);
+ if (!FileUtil.filesEqual(myDestination.getCanonicalFile(), myRepo.getCanonicalFile())) {
+ SdkMerger.mergeSdks(myRepo, myDestination, indicator);
}
- try {
- for (InstallableComponent component : getSelectedComponents()) {
- if (component.hasVisibleStep()) {
- return true;
- }
- }
+ myContext.print(String.format("Android SDK was installed to %s", myDestination), ConsoleViewContentType.SYSTEM_OUTPUT);
+ return myDestination;
+ }
+ }
+
+ private static class InstallComponentsOperation implements ThrowableComputable<Void, WizardException> {
+ private final InstallContext myContext;
+ private final File mySdkLocation;
+ private final Set<String> myComponents;
+
+ public InstallComponentsOperation(InstallContext context, File sdkLocation, Set<String> components) {
+ myContext = context;
+ mySdkLocation = sdkLocation;
+ myComponents = components;
+ }
+
+ @Override
+ public Void compute() throws WizardException {
+ final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
+ ILogger log = new SdkManagerProgressIndicatorIntegration(indicator, myContext, myComponents.size());
+ SdkManager manager = SdkManager.createManager(mySdkLocation.getAbsolutePath(), log);
+ if (manager != null) {
+ SdkUpdaterNoWindow updater = new SdkUpdaterNoWindow(manager.getLocation(), manager, log, false, true, null, null);
+ updater.updateAll(Lists.newArrayList(myComponents), true, false, null);
+ return null;
}
- catch (WizardException e) {
- LOG.error(e);
+ else {
+ throw new WizardException("Corrupted SDK installation");
}
}
- return false;
}
}
diff --git a/android/src/com/android/tools/idea/welcome/InstallContext.java b/android/src/com/android/tools/idea/welcome/InstallContext.java
index 29020abb..4d82267 100644
--- a/android/src/com/android/tools/idea/welcome/InstallContext.java
+++ b/android/src/com/android/tools/idea/welcome/InstallContext.java
@@ -15,76 +15,133 @@
*/
package com.android.tools.idea.welcome;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
+import com.android.annotations.VisibleForTesting;
import com.intellij.execution.ui.ConsoleViewContentType;
-import com.intellij.openapi.util.io.FileUtil;
-import com.intellij.util.download.DownloadableFileDescription;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.openapi.progress.util.ProgressIndicatorBase;
+import com.intellij.openapi.util.ThrowableComputable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
/**
* Keeps installation process state.
*/
public class InstallContext {
private final File myTempDirectory;
- @NotNull private final List<DownloadableFileDescription> myDescriptions;
- @NotNull private final ProgressStep myProgressStep;
- private final Map<DownloadableFileDescription, File> myDownloadLocations = Maps.newHashMap();
- private Map<DownloadableFileDescription, File> myExpandedLocations = Maps.newHashMap();
+ @Nullable private final ProgressStep myProgressStep;
- public InstallContext(@NotNull File tempDirectory, @NotNull Iterable<DownloadableFileDescription> descriptions,
- @NotNull ProgressStep progressStep) {
+ @VisibleForTesting
+ InstallContext(@NotNull File tempDirectory) {
+ assert ApplicationManager.getApplication().isUnitTestMode();
myTempDirectory = tempDirectory;
- myDescriptions = ImmutableList.copyOf(descriptions);
+ myProgressStep = null;
+ }
+
+ @SuppressWarnings("NullableProblems")
+ public InstallContext(@NotNull File tempDirectory, @NotNull ProgressStep progressStep) {
+ myTempDirectory = tempDirectory;
myProgressStep = progressStep;
}
- public File getExpandedLocation(DownloadableFileDescription downloadableFile) {
- return myExpandedLocations.get(downloadableFile);
- }
-
- @NotNull
- public ProgressStep getProgressStep() {
- return myProgressStep;
- }
-
- public List<DownloadableFileDescription> getFilesToDownload() {
- return myDescriptions;
- }
-
- public void setDownloadedLocation(DownloadableFileDescription downloadableFile, File destination) {
- myDownloadLocations.put(downloadableFile, destination);
- }
-
public File getTempDirectory() {
return myTempDirectory;
}
- public void cleanup(ProgressStep step) {
- for (File file : Iterables.concat(myDownloadLocations.values(), myExpandedLocations.values())) {
- if (!FileUtil.delete(file)) {
- step.print(String.format("Can't delete %s\n", file.getAbsolutePath()), ConsoleViewContentType.ERROR_OUTPUT);
+ public boolean isCanceled() {
+ return myProgressStep != null && myProgressStep.isCanceled();
+ }
+
+ @SuppressWarnings("UseOfSystemOutOrSystemErr")
+ public void print(String message, ConsoleViewContentType contentType) {
+ if (myProgressStep != null) {
+ myProgressStep.print(message, contentType);
+ }
+ else {
+ if (contentType == ConsoleViewContentType.ERROR_OUTPUT) {
+ System.err.println(message);
+ }
+ else {
+ System.out.println(message);
}
}
}
- public Collection<File> getDownloadedFiles() {
- return myDownloadLocations.values();
+ public <R, E extends Exception> R run(ThrowableComputable<R, E> operation, double progressRatio) throws E {
+ Wrapper<R, E> wrapper = new Wrapper<R, E>(operation);
+ if (myProgressStep != null) {
+ myProgressStep.run(wrapper, progressRatio);
+ }
+ else {
+ ProgressManager.getInstance().executeProcessUnderProgress(wrapper, new TestingProgressIndicator());
+ }
+ return wrapper.getResult();
}
- @Nullable
- public File getDownloadLocation(DownloadableFileDescription description) {
- return myDownloadLocations.get(description);
+ public void advance(double progress) {
+ if (myProgressStep != null) {
+ myProgressStep.advance(progress);
+ }
}
- public void setExpandedLocation(DownloadableFileDescription description, File dir) {
- myExpandedLocations.put(description, dir);
+ @SuppressWarnings("UseOfSystemOutOrSystemErr")
+ private static class TestingProgressIndicator extends ProgressIndicatorBase {
+ private int previous = 0;
+
+ @Override
+ public void setText(String text) {
+ System.out.println(text);
+ }
+
+ @Override
+ public void setText2(String text) {
+ System.out.println("Text2: " + text);
+ }
+
+ @Override
+ public void setFraction(double fraction) {
+ int p = (int)Math.floor(fraction * 20);
+ if (p > previous) {
+ previous = p;
+ System.out.print(".");
+ }
+ }
+ }
+
+ private static class Wrapper<R, E extends Exception> implements Runnable {
+ private final ThrowableComputable<R, E> myRunnable;
+ private volatile R myResult;
+ private volatile E myException;
+
+ public Wrapper(ThrowableComputable<R, E> runnable) {
+ myRunnable = runnable;
+ }
+
+ @Override
+ public void run() {
+ try {
+ myResult = myRunnable.compute();
+ }
+ catch (RuntimeException e) {
+ throw e;
+ }
+ catch (Exception e) {
+ // We know this is not a runtime exception, hence it is the exception from the callable prototype
+ //noinspection unchecked
+ myException = (E)e;
+ }
+ }
+
+ private R getResult() throws E {
+ if (myException != null) {
+ throw myException;
+ }
+ else {
+ return myResult;
+ }
+ }
+
}
}
diff --git a/android/src/com/android/tools/idea/welcome/InstallableComponent.java b/android/src/com/android/tools/idea/welcome/InstallableComponent.java
index f87a165..1accc3e 100644
--- a/android/src/com/android/tools/idea/welcome/InstallableComponent.java
+++ b/android/src/com/android/tools/idea/welcome/InstallableComponent.java
@@ -15,12 +15,14 @@
*/
package com.android.tools.idea.welcome;
+import com.android.sdklib.repository.descriptors.PkgDesc;
import com.android.tools.idea.wizard.DynamicWizardStep;
import com.android.tools.idea.wizard.ScopedStateStore;
import com.intellij.util.download.DownloadableFileDescription;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.io.File;
import java.util.Set;
/**
@@ -85,14 +87,12 @@
}
}
- public abstract void perform(@NotNull InstallContext downloaded) throws WizardException;
-
@NotNull
- public abstract Set<DownloadableFileDescription> getFilesToDownloadAndExpand();
+ public abstract PkgDesc.Builder[] getRequiredSdkPackages();
- public abstract void init(ScopedStateStore state);
+ public abstract void init(@NotNull ScopedStateStore state, @NotNull ProgressStep progressStep);
public abstract DynamicWizardStep[] createSteps();
- public abstract boolean hasVisibleStep();
+ public abstract void configure(@NotNull InstallContext installContext, @NotNull File sdk);
}
diff --git a/android/src/com/android/tools/idea/welcome/InstallerData.java b/android/src/com/android/tools/idea/welcome/InstallerData.java
index bef55e6..ab3d272 100644
--- a/android/src/com/android/tools/idea/welcome/InstallerData.java
+++ b/android/src/com/android/tools/idea/welcome/InstallerData.java
@@ -16,11 +16,13 @@
package com.android.tools.idea.welcome;
import com.android.annotations.VisibleForTesting;
+import com.android.prefs.AndroidLocation;
import com.android.tools.idea.wizard.ScopedStateStore;
+import com.android.tools.idea.wizard.WizardUtils;
import com.google.common.base.Objects;
import com.google.common.io.Closeables;
-import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -36,6 +38,8 @@
public class InstallerData {
@VisibleForTesting static final ScopedStateStore.Key<InstallerData> CONTEXT_KEY =
ScopedStateStore.createKey("installer.handoff.data", ScopedStateStore.Scope.WIZARD, InstallerData.class);
+ public static final String PATH_FIRST_RUN_PROPERTIES = FileUtil.join("studio", "installer", "firstrun.properties");
+
@Nullable private final String myJavaDir;
@Nullable private final String myAndroidSrc;
@Nullable private final String myAndroidDest;
@@ -55,19 +59,24 @@
private static Properties readProperties() {
Properties properties = new Properties();
- File file = new File(System.getProperty("user.home"), ".android/firstlaunch/firstlaunch.properties");
- if (file.isFile()) {
- FileInputStream stream = null;
- try {
- stream = new FileInputStream(file);
- properties.load(stream);
+ try {
+ File file = new File(AndroidLocation.getFolder(), PATH_FIRST_RUN_PROPERTIES);
+ if (file.isFile()) {
+ FileInputStream stream = null;
+ try {
+ stream = new FileInputStream(file);
+ properties.load(stream);
+ }
+ catch (IOException e) {
+ Logger.getInstance(InstallerData.class).error(e);
+ }
+ finally {
+ Closeables.closeQuietly(stream);
+ }
}
- catch (IOException e) {
- Logger.getInstance(InstallerData.class).error(e);
- }
- finally {
- Closeables.closeQuietly(stream);
- }
+ }
+ catch (AndroidLocation.AndroidLocationException e) {
+ Logger.getInstance(InstallerData.class).error(e);
}
return properties;
}
@@ -118,7 +127,7 @@
}
public boolean hasValidSdkLocation() {
- return exists() && SdkComponentsStep.validateDestinationPath(getAndroidDest(), AndroidSdk.SIZE) == null;
+ return exists() && !WizardUtils.validateLocation(getAndroidDest(), SdkComponentsStep.FIELD_SDK_LOCATION, false).isError();
}
public boolean hasValidJdkLocation() {
diff --git a/android/src/com/android/tools/idea/welcome/LongRunningOperationPath.java b/android/src/com/android/tools/idea/welcome/LongRunningOperationPath.java
index a45ce63..f28d58f 100644
--- a/android/src/com/android/tools/idea/welcome/LongRunningOperationPath.java
+++ b/android/src/com/android/tools/idea/welcome/LongRunningOperationPath.java
@@ -15,11 +15,9 @@
*/
package com.android.tools.idea.welcome;
-import org.jetbrains.annotations.NotNull;
-
/**
* Interface for paths that have a long-running operation running after the wizard completes.
*/
public interface LongRunningOperationPath {
- void runLongOperation(@NotNull ProgressStep progressStep) throws WizardException;
+ void runLongOperation() throws WizardException;
}
diff --git a/android/src/com/android/tools/idea/welcome/PreinstallOperation.java b/android/src/com/android/tools/idea/welcome/PreinstallOperation.java
index 46f7b1d..257b5a6 100644
--- a/android/src/com/android/tools/idea/welcome/PreinstallOperation.java
+++ b/android/src/com/android/tools/idea/welcome/PreinstallOperation.java
@@ -15,19 +15,21 @@
*/
package com.android.tools.idea.welcome;
-import com.google.common.util.concurrent.Atomics;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.ThrowableComputable;
+import org.jetbrains.annotations.Nullable;
import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
/**
- * Operation that is executed prior to installing the application.
+ * <p>Operation that is executed prior to installing the application.</p>
+ *
+ * <p>Type argument specifies the type of the operation return value</p>
*/
-public abstract class PreinstallOperation {
+public abstract class PreinstallOperation<T> {
protected final InstallContext myContext;
private final double myProgressRatio;
@@ -39,21 +41,22 @@
/**
* Performs the actual logic
*/
- protected abstract void perform() throws WizardException;
+ protected abstract T perform() throws WizardException;
/**
* Runs the operation under progress indicator that only gives access to progress portion.
*/
- public final boolean execute() throws WizardException {
- ProgressStep myProgressStep = myContext.getProgressStep();
- if (myProgressStep.isCanceled()) {
- return false;
+ @Nullable
+ public final T execute() throws WizardException {
+ if (myContext.isCanceled()) {
+ return null;
}
- Wrapper wrapper = new Wrapper();
- myProgressStep.run(wrapper, myProgressRatio);
- wrapper.rethrowIfExceptionRecorded();
- return !myProgressStep.isCanceled();
-
+ return myContext.run(new ThrowableComputable<T, WizardException>() {
+ @Override
+ public T compute() throws WizardException {
+ return perform();
+ }
+ }, myProgressRatio);
}
/**
@@ -73,28 +76,7 @@
throw new WizardException(failureDescription, e);
}
else {
- myContext.getProgressStep().print(failureDescription + "\n", ConsoleViewContentType.ERROR_OUTPUT);
- }
- }
-
- private final class Wrapper implements Runnable {
- private final AtomicReference<WizardException> myException = Atomics.newReference(null);
-
- @Override
- public void run() {
- try {
- perform();
- }
- catch (WizardException e) {
- myException.set(e);
- }
- }
-
- public void rethrowIfExceptionRecorded() throws WizardException {
- WizardException exception = myException.get();
- if (exception != null) {
- throw exception;
- }
+ myContext.print(failureDescription + "\n", ConsoleViewContentType.ERROR_OUTPUT);
}
}
}
diff --git a/android/src/com/android/tools/idea/welcome/ProgressStep.form b/android/src/com/android/tools/idea/welcome/ProgressStep.form
index b4ecd1d7..9dc633f 100644
--- a/android/src/com/android/tools/idea/welcome/ProgressStep.form
+++ b/android/src/com/android/tools/idea/welcome/ProgressStep.form
@@ -1,55 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.android.tools.idea.welcome.ProgressStep">
- <grid id="27dc6" binding="myRoot" layout-manager="GridLayoutManager" row-count="4" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+ <grid id="27dc6" binding="myRoot" layout-manager="GridLayoutManager" row-count="3" 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="544" height="400"/>
+ <xy x="20" y="20" width="648" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
- <component id="8eaa0" class="javax.swing.JLabel" binding="myLabel">
- <constraints>
- <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
- </constraints>
- <properties/>
- </component>
- <hspacer id="fefa0">
- <constraints>
- <grid row="0" column="2" 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="986d6" class="javax.swing.JProgressBar" binding="myProgressBar">
- <constraints>
- <grid row="1" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false">
- <minimum-size width="500" height="-1"/>
- </grid>
- </constraints>
- <properties>
- <indeterminate value="true"/>
- </properties>
- </component>
- <hspacer id="e0562">
- <constraints>
- <grid row="0" column="0" 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="686b1" class="javax.swing.JButton" binding="myShowDetailsButton" default-binding="true">
- <constraints>
- <grid row="2" 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="Show Details"/>
- </properties>
- </component>
<grid id="fc7ff" binding="myConsole" layout-manager="BorderLayout" hgap="0" vgap="0">
<constraints>
- <grid row="3" column="0" row-span="1" col-span="3" vsize-policy="7" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+ <grid row="2" column="0" row-span="1" col-span="3" vsize-policy="7" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</grid>
+ <grid id="14145" 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="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties/>
+ <border type="none">
+ <size top="0" left="20" bottom="0" right="20"/>
+ </border>
+ <children>
+ <component id="8eaa0" class="javax.swing.JLabel" binding="myLabel">
+ <constraints>
+ <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false">
+ <minimum-size width="650" height="-1"/>
+ <preferred-size width="650" height="-1"/>
+ <maximum-size width="650" height="-1"/>
+ </grid>
+ </constraints>
+ <properties/>
+ </component>
+ <component id="986d6" class="javax.swing.JProgressBar" binding="myProgressBar">
+ <constraints>
+ <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false">
+ <minimum-size width="650" height="-1"/>
+ <preferred-size width="650" height="-1"/>
+ <maximum-size width="650" height="-1"/>
+ </grid>
+ </constraints>
+ <properties>
+ <indeterminate value="true"/>
+ </properties>
+ </component>
+ </children>
+ </grid>
+ <hspacer id="78ad1">
+ <constraints>
+ <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ </hspacer>
+ <hspacer id="9470d">
+ <constraints>
+ <grid row="0" column="2" 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="686b1" class="javax.swing.JButton" binding="myShowDetailsButton" default-binding="true">
+ <constraints>
+ <grid row="1" 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="Show Details"/>
+ </properties>
+ </component>
</children>
</grid>
</form>
diff --git a/android/src/com/android/tools/idea/welcome/ProgressStep.java b/android/src/com/android/tools/idea/welcome/ProgressStep.java
index 847caa4..8c09ee5 100644
--- a/android/src/com/android/tools/idea/welcome/ProgressStep.java
+++ b/android/src/com/android/tools/idea/welcome/ProgressStep.java
@@ -28,6 +28,7 @@
import com.intellij.openapi.progress.util.ProgressIndicatorBase;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.util.ThrowableComputable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -172,6 +173,10 @@
myProgressBar.setValue((int)(1000 * fraction));
}
+ public void advance(double progress) {
+ getProgressIndicator().setFraction(myFraction + progress);
+ }
+
/**
* Progress indicator that scales task to only use a portion of the parent indicator.
*/
diff --git a/android/src/com/android/tools/idea/welcome/SdkComponentsStep.java b/android/src/com/android/tools/idea/welcome/SdkComponentsStep.java
index 39d79fc..f832814 100644
--- a/android/src/com/android/tools/idea/welcome/SdkComponentsStep.java
+++ b/android/src/com/android/tools/idea/welcome/SdkComponentsStep.java
@@ -15,8 +15,10 @@
*/
package com.android.tools.idea.welcome;
+import com.android.tools.idea.templates.TemplateUtils;
import com.android.tools.idea.wizard.ScopedStateStore;
import com.android.tools.idea.wizard.WizardConstants;
+import com.android.tools.idea.wizard.WizardUtils;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
@@ -27,8 +29,8 @@
import com.intellij.ui.table.JBTable;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
-import com.intellij.util.PathUtil;
import com.intellij.util.ui.UIUtil;
+import org.jetbrains.android.sdk.AndroidSdkData;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -52,6 +54,8 @@
* Wizard page for selecting SDK components to download.
*/
public class SdkComponentsStep extends FirstRunWizardStep {
+ public static final String FIELD_SDK_LOCATION = "SDK location";
+
private final InstallableComponent[] myInstallableComponents;
private final ScopedStateStore.Key<Boolean> myKeyInstallSdk;
private JPanel myContents;
@@ -65,9 +69,9 @@
private TextFieldWithBrowseButton myPath;
private boolean myUserEditedPath = false;
- public SdkComponentsStep(InstallableComponent[] components,
- ScopedStateStore.Key<Boolean> keyInstallSdk,
- ScopedStateStore.Key<String> sdkDownloadPathKey) {
+ public SdkComponentsStep(@NotNull InstallableComponent[] components,
+ @NotNull ScopedStateStore.Key<Boolean> keyInstallSdk,
+ @NotNull ScopedStateStore.Key<String> sdkDownloadPathKey) {
super("SDK Settings");
myPath.addBrowseFolderListener("Android SDK", "Select Android SDK install directory", null,
@@ -165,44 +169,39 @@
return file;
}
- @Nullable
- public static String validateDestinationPath(@Nullable String path, long componentsSize) {
- if (StringUtil.isEmpty(path)) {
- return "Path is empty";
- }
- else {
- File file = new File(path);
- while (file != null && !file.exists()) {
- if (!PathUtil.isValidFileName(file.getName())) {
- return "Specified path is not valid";
- }
- file = file.getParentFile();
- }
- }
- File filesystem = getTargetFilesystem(path);
- if (filesystem != null) {
- long diskSpace = filesystem.getUsableSpace();
- if (diskSpace == 0) {
- return "Selected location is not writeable";
- }
- else {
- if (componentsSize >= diskSpace) {
- return "Not enough disk space";
- }
- }
- }
- return null;
- }
-
@Override
public boolean validate() {
String path = myState.get(mySdkDownloadPathKey);
if (!StringUtil.isEmpty(path)) {
myUserEditedPath = true;
}
- String error = validateDestinationPath(path, getComponentsSize());
- setErrorHtml(myUserEditedPath ? error : null);
- return error == null;
+ WizardUtils.ValidationResult error = WizardUtils.validateLocation(path, FIELD_SDK_LOCATION, false);
+ String message = error.isOk() ? null : error.getFormattedMessage();
+ boolean isOk = !error.isError();
+ if (isOk) {
+ File filesystem = getTargetFilesystem(path);
+ if (!(filesystem == null || filesystem.getFreeSpace() > getComponentsSize())) {
+ isOk = false;
+ message = "Target drive does not have enough free space";
+ }
+ else if (isNonEmptyNonSdk(path)) {
+ isOk = true;
+ message = "Target folder is neither empty nor does it point to an existing SDK installation.";
+ }
+ }
+ setErrorHtml(myUserEditedPath ? message : null);
+ return isOk;
+ }
+
+ private static boolean isNonEmptyNonSdk(@Nullable String path) {
+ if (path == null) {
+ return false;
+ }
+ File file = new File(path);
+ if (file.exists() && TemplateUtils.listFiles(file).length > 0) {
+ return AndroidSdkData.getSdkData(file) == null;
+ }
+ return false;
}
@Override
diff --git a/android/src/com/android/tools/idea/welcome/SdkManagerProgressIndicatorIntegration.java b/android/src/com/android/tools/idea/welcome/SdkManagerProgressIndicatorIntegration.java
new file mode 100644
index 0000000..a948e18
--- /dev/null
+++ b/android/src/com/android/tools/idea/welcome/SdkManagerProgressIndicatorIntegration.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.welcome;
+
+import com.android.tools.idea.sdk.SdkLoggerIntegration;
+import com.google.common.base.Objects;
+import com.intellij.execution.ui.ConsoleViewContentType;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.util.text.StringUtil;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Integrates SDK manager progress reporting with the wizard UI.
+ */
+public class SdkManagerProgressIndicatorIntegration extends SdkLoggerIntegration {
+ private final ProgressIndicator myIndicator;
+ private final InstallContext myContext;
+ private final int myComponentCount;
+ private int myCompletedOperations = -1;
+ private String previousTitle;
+
+ public SdkManagerProgressIndicatorIntegration(@NotNull ProgressIndicator indicator,
+ @NotNull InstallContext context,
+ int componentCount) {
+ assert componentCount > 0;
+ myIndicator = indicator;
+ myContext = context;
+ myComponentCount = componentCount;
+ }
+
+ @Override
+ protected void setProgress(int progress) {
+ double completedOperations = progress / 100.0 + myCompletedOperations;
+ double progressBar = completedOperations / (myComponentCount * 2); // Installing a component is 2 operations - download + unzip
+ myIndicator.setFraction(progressBar);
+ }
+
+ @Override
+ protected void setDescription(String description) {
+ // Nothing
+ }
+
+ @Override
+ protected void setTitle(String title) {
+ if (!StringUtil.isEmptyOrSpaces(title) && !Objects.equal(title, previousTitle)) {
+ previousTitle = title;
+ myCompletedOperations++;
+ myIndicator.setText(previousTitle);
+ setProgress(0);
+ }
+ }
+
+ @Override
+ protected void lineAdded(String string) {
+ myContext.print(string, ConsoleViewContentType.SYSTEM_OUTPUT);
+ }
+}
diff --git a/android/src/com/android/tools/idea/welcome/UnzipOperation.java b/android/src/com/android/tools/idea/welcome/UnzipOperation.java
index df7a0b9..aab1997 100644
--- a/android/src/com/android/tools/idea/welcome/UnzipOperation.java
+++ b/android/src/com/android/tools/idea/welcome/UnzipOperation.java
@@ -19,8 +19,8 @@
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.io.FileUtil;
-import com.intellij.util.download.DownloadableFileDescription;
-import com.intellij.util.io.ZipUtil;
+import com.intellij.platform.templates.github.ZipUtil;
+import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
@@ -28,17 +28,22 @@
/**
* Unzips the archives required for Android Studio setup.
*/
-public final class UnzipOperation extends PreinstallOperation {
- public UnzipOperation(InstallContext context) {
- super(context, 0.3);
+public final class UnzipOperation extends PreinstallOperation<File> {
+ private final InstallContext myContext;
+ private final File myArchive;
+
+ public UnzipOperation(InstallContext context, File archive, double progressShare) {
+ super(context, progressShare);
+ myContext = context;
+ myArchive = archive;
}
- private File unzip(File archive, File tempDirectory) throws WizardException {
- myContext.getProgressStep().print(String.format("Unpacking %s\n", archive.getName()), ConsoleViewContentType.SYSTEM_OUTPUT);
+ private File unzip(ProgressIndicator progressIndicator, File archive, File tempDirectory) throws WizardException {
+ myContext.print(String.format("Unpacking %s\n", archive.getName()), ConsoleViewContentType.SYSTEM_OUTPUT);
File dir = new File(tempDirectory, archive.getName() + "-unpacked");
do {
try {
- ZipUtil.extract(archive, dir, null);
+ ZipUtil.unzip(progressIndicator, dir, archive, null, null, true);
if (archive.getCanonicalPath().startsWith(myContext.getTempDirectory().getCanonicalPath())) {
FileUtil.delete(archive); // Even if this fails, there's nothing we can do, and the folder should be deleted on exit anyways
}
@@ -53,29 +58,13 @@
while (true);
}
+ @Nullable
@Override
- protected void perform() throws WizardException {
- long allFiles = 0, done = 0;
+ protected File perform() throws WizardException {
ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator();
- progressIndicator.start();
- progressIndicator.setText("Unpacking archives");
- for (File file : myContext.getDownloadedFiles()) {
- allFiles += file.length();
+ if (progressIndicator.isCanceled()) {
+ return null;
}
- try {
- for (DownloadableFileDescription description : myContext.getFilesToDownload()) {
- if (progressIndicator.isCanceled()) {
- break;
- }
- File first = myContext.getDownloadLocation(description);
- assert first != null;
- myContext.setExpandedLocation(description, unzip(first, myContext.getTempDirectory()));
- done += first.length();
- progressIndicator.setFraction(1.0 * done / allFiles);
- }
- }
- finally {
- progressIndicator.stop();
- }
+ return unzip(progressIndicator, myArchive, myContext.getTempDirectory());
}
}
diff --git a/android/src/com/android/tools/idea/wizard/ChooseAndroidAndJavaSdkStep.java b/android/src/com/android/tools/idea/wizard/ChooseAndroidAndJavaSdkStep.java
index 60dc541..02022a9 100644
--- a/android/src/com/android/tools/idea/wizard/ChooseAndroidAndJavaSdkStep.java
+++ b/android/src/com/android/tools/idea/wizard/ChooseAndroidAndJavaSdkStep.java
@@ -59,7 +59,7 @@
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
- DefaultSdks.setDefaultAndroidHome(new File(location), javaSdk);
+ DefaultSdks.setDefaultAndroidHome(new File(location), javaSdk, null);
}
});
}
diff --git a/android/src/com/android/tools/idea/wizard/GetSdkStep.java b/android/src/com/android/tools/idea/wizard/GetSdkStep.java
index 3861702..fb5699b 100644
--- a/android/src/com/android/tools/idea/wizard/GetSdkStep.java
+++ b/android/src/com/android/tools/idea/wizard/GetSdkStep.java
@@ -30,7 +30,6 @@
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.util.Pair;
-import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtil;
@@ -40,6 +39,7 @@
import com.intellij.platform.templates.github.Outcome;
import com.intellij.platform.templates.github.ZipUtil;
import org.jetbrains.android.sdk.AndroidSdkType;
+import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -58,10 +58,6 @@
*/
public class GetSdkStep extends DynamicWizardStepWithHeaderAndDescription {
private static final Logger LOG = Logger.getInstance(GetSdkStep.class);
- // TODO: Update these to a stable link
- private static final String MAC_SDK_URL = "http://dl.google.com/android/android-sdk_r22.6.2-macosx.zip";
- private static final String LINUX_SDK_URL = "http://dl.google.com/android/android-sdk_r22.6.2-linux.tgz";
- private static final String WINDOWS_SDK_URL = "http://dl.google.com/android/android-sdk_r22.6.2-windows.zip";
private TextFieldWithBrowseButton mySdkLocationField;
private JPanel myPanel;
private JButton myDownloadANewSDKButton;
@@ -126,7 +122,7 @@
@Override
public File call() throws Exception {
ProgressIndicator progress = ProgressManager.getInstance().getProgressIndicator();
- String downloadUrl = getDownloadUrl();
+ String downloadUrl = AndroidSdkUtils.getSdkDownloadUrl();
if (downloadUrl == null) {
setErrorHtml("We cannot recognize your OS. Please visit http://developer.android.com/sdk/index.html and select" +
"the appropriate SDK bundle.");
@@ -167,7 +163,7 @@
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
- DefaultSdks.setDefaultAndroidHome(sdkFile);
+ DefaultSdks.setDefaultAndroidHome(sdkFile, null);
}
});
return true;
@@ -240,17 +236,4 @@
return null;
}
}
-
- @Nullable
- private String getDownloadUrl() {
- if (SystemInfo.isLinux) {
- return LINUX_SDK_URL;
- } else if (SystemInfo.isWindows) {
- return WINDOWS_SDK_URL;
- } else if (SystemInfo.isMac) {
- return MAC_SDK_URL;
- } else {
- return null;
- }
- }
}
diff --git a/android/src/com/android/tools/idea/wizard/NewFormFactorModulePath.java b/android/src/com/android/tools/idea/wizard/NewFormFactorModulePath.java
index 8351c03..e485e360 100644
--- a/android/src/com/android/tools/idea/wizard/NewFormFactorModulePath.java
+++ b/android/src/com/android/tools/idea/wizard/NewFormFactorModulePath.java
@@ -203,6 +203,7 @@
}
if (myState.containsKey(NEWLY_INSTALLED_API_KEY)) {
Integer newApiLevel = myState.get(NEWLY_INSTALLED_API_KEY);
+ assert newApiLevel != null;
Key<Integer> targetApiLevelKey = FormFactorUtils.getTargetApiLevelKey(myFormFactor);
Integer currentTargetLevel = myState.get(targetApiLevelKey);
if (currentTargetLevel == null || newApiLevel > currentTargetLevel) {
diff --git a/android/src/com/android/tools/idea/wizard/WizardUtils.java b/android/src/com/android/tools/idea/wizard/WizardUtils.java
index 07c1d33..62dcbb1 100644
--- a/android/src/com/android/tools/idea/wizard/WizardUtils.java
+++ b/android/src/com/android/tools/idea/wizard/WizardUtils.java
@@ -18,9 +18,14 @@
import com.android.annotations.VisibleForTesting;
import com.android.tools.idea.templates.TemplateUtils;
import com.google.common.base.CharMatcher;
+import com.intellij.openapi.application.Application;
+import com.intellij.openapi.application.ApplicationNamesInfo;
+import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.util.ArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -78,18 +83,20 @@
}
public enum Message {
- NO_LOCATION_SPECIFIED("Please specify a project location"),
- BAD_SLASHES("Your project location contains incorrect slashes ('\\' vs '/')"),
- ILLEGAL_CHARACTER("Illegal character in project location path: '%1c' in filename %2s"),
- ILLEGAL_FILENAME("Illegal filename in project location path: %s"),
- WHITESPACE("Your project location contains whitespace. This can cause problems on some platforms and is not recommended."),
- NON_ASCII_CHARS("Your project location contains non-ASCII characters. This can cause problems on Windows. Proceed with caution."),
- PATH_NOT_WRITEABLE("The path '%s' is not writeable. Please choose a new location."),
- PROJECT_LOC_IS_FILE("There must not already be a file at the project location."),
+ NO_LOCATION_SPECIFIED("Please specify a %1s"),
+ BAD_SLASHES("Your %1s contains incorrect slashes ('\\' vs '/')"),
+ ILLEGAL_CHARACTER("Illegal character in %1s path: '%2c' in filename %3s"),
+ ILLEGAL_FILENAME("Illegal filename in %1s path: %2s"),
+ WHITESPACE("Your %1s contains whitespace. This can cause problems on some platforms and is not recommended."),
+ NON_ASCII_CHARS("Your %1s contains non-ASCII characters. This can cause problems on Windows. Proceed with caution."),
+ PATH_NOT_WRITEABLE("The path '%2s' is not writeable. Please choose a new location."),
+ PROJECT_LOC_IS_FILE("There must not already be a file at the %1s."),
NON_EMPTY_DIR(
- "A non-empty directory already exists at the specified project location. Existing files may be overwritten. Proceed with caution."),
- PROJECT_IS_FILE_SYSTEM_ROOT("The project location can not be at the filesystem root"),
- PARENT_NOT_DIR("The project location's parent directory must be a directory, not a plain file");
+ "A non-empty directory already exists at the specified %1s. Existing files may be overwritten. Proceed with caution."),
+ PROJECT_IS_FILE_SYSTEM_ROOT("The %1s can not be at the filesystem root"),
+ IS_UNDER_ANDROID_STUDIO_ROOT("Path points to a location within Android Studio installation directory"),
+ PARENT_NOT_DIR("The %1s's parent directory must be a directory, not a plain file"),
+ INSIDE_ANDROID_STUDIO("The %1s is inside %2s install location");
private final String myText;
@@ -103,24 +110,24 @@
}
}
- public static final ValidationResult OK = new ValidationResult(Status.OK, null);
+ public static final ValidationResult OK = new ValidationResult(Status.OK, null, "any");
private final Status myStatus;
private final Message myMessage;
private final Object[] myMessageParams;
- private ValidationResult(@NotNull Status status, @Nullable Message message, Object... messageParams) {
+ private ValidationResult(@NotNull Status status, @Nullable Message message, @NotNull String field, Object... messageParams) {
myStatus = status;
myMessage = message;
- myMessageParams = messageParams;
+ myMessageParams = ArrayUtil.prepend(field, messageParams);
}
- public static ValidationResult warn(@NotNull Message message, Object... params) {
- return new ValidationResult(Status.WARN, message, params);
+ public static ValidationResult warn(@NotNull Message message, String field, Object... params) {
+ return new ValidationResult(Status.WARN, message, field, params);
}
- public static ValidationResult error(@NotNull Message message, Object... params) {
- return new ValidationResult(Status.ERROR, message, params);
+ public static ValidationResult error(@NotNull Message message, String field, Object... params) {
+ return new ValidationResult(Status.ERROR, message, field, params);
}
@Nullable
@@ -156,19 +163,24 @@
}
/**
- * Will return {@link com.android.tools.idea.wizard.WizardUtils.ValidationResult.OK} if projectLocation is valid
+ * Will return {@link com.android.tools.idea.wizard.WizardUtils.ValidationResult#OK} if projectLocation is valid
* or {@link com.android.tools.idea.wizard.WizardUtils.ValidationResult} with error if not.
*/
@NotNull
public static ValidationResult validateLocation(@Nullable String projectLocation) {
+ return validateLocation(projectLocation, "project location", true);
+ }
+
+ @NotNull
+ public static ValidationResult validateLocation(@Nullable String projectLocation, @NotNull String fieldName, boolean checkEmpty) {
ValidationResult warningResult = null;
if (projectLocation == null || projectLocation.isEmpty()) {
- return ValidationResult.error(ValidationResult.Message.NO_LOCATION_SPECIFIED);
+ return ValidationResult.error(ValidationResult.Message.NO_LOCATION_SPECIFIED, fieldName);
}
// Check the separators
if ((File.separatorChar == '/' && projectLocation.contains("\\")) ||
(File.separatorChar == '\\' && projectLocation.contains("/"))) {
- return ValidationResult.error(ValidationResult.Message.BAD_SLASHES);
+ return ValidationResult.error(ValidationResult.Message.BAD_SLASHES, fieldName);
}
// Check the individual components for not allowed characters.
File testFile = new File(projectLocation);
@@ -176,21 +188,21 @@
String filename = testFile.getName();
if (ILLEGAL_CHARACTER_MATCHER.matchesAnyOf(filename)) {
char illegalChar = filename.charAt(ILLEGAL_CHARACTER_MATCHER.indexIn(filename));
- return ValidationResult.error(ValidationResult.Message.ILLEGAL_CHARACTER, illegalChar, filename);
+ return ValidationResult.error(ValidationResult.Message.ILLEGAL_CHARACTER, fieldName, illegalChar, filename);
}
if (WizardConstants.INVALID_WINDOWS_FILENAMES.contains(filename.toLowerCase())) {
- return ValidationResult.error(ValidationResult.Message.ILLEGAL_FILENAME, filename);
+ return ValidationResult.error(ValidationResult.Message.ILLEGAL_FILENAME, fieldName, filename);
}
if (CharMatcher.WHITESPACE.matchesAnyOf(filename)) {
- warningResult = ValidationResult.warn(ValidationResult.Message.WHITESPACE);
+ warningResult = ValidationResult.warn(ValidationResult.Message.WHITESPACE, fieldName);
}
if (!CharMatcher.ASCII.matchesAllOf(filename)) {
- warningResult = ValidationResult.warn(ValidationResult.Message.NON_ASCII_CHARS);
+ warningResult = ValidationResult.warn(ValidationResult.Message.NON_ASCII_CHARS, fieldName);
}
// Check that we can write to that location: make sure we can write into the first extant directory in the path.
if (!testFile.exists() && testFile.getParentFile() != null && testFile.getParentFile().exists()) {
if (!testFile.getParentFile().canWrite()) {
- return ValidationResult.error(ValidationResult.Message.PATH_NOT_WRITEABLE, testFile.getParentFile().getPath());
+ return ValidationResult.error(ValidationResult.Message.PATH_NOT_WRITEABLE, fieldName, testFile.getParentFile().getPath());
}
}
testFile = testFile.getParentFile();
@@ -198,18 +210,25 @@
File file = new File(projectLocation);
if (file.isFile()) {
- return ValidationResult.error(ValidationResult.Message.PROJECT_LOC_IS_FILE);
- } else if (file.isDirectory() && TemplateUtils.listFiles(file).length > 0) {
- return ValidationResult.warn(ValidationResult.Message.NON_EMPTY_DIR);
+ return ValidationResult.error(ValidationResult.Message.PROJECT_LOC_IS_FILE, fieldName);
}
if (file.getParent() == null) {
- return ValidationResult.error(ValidationResult.Message.PROJECT_IS_FILE_SYSTEM_ROOT);
+ return ValidationResult.error(ValidationResult.Message.PROJECT_IS_FILE_SYSTEM_ROOT, fieldName);
}
if (file.getParentFile().exists() && !file.getParentFile().isDirectory()) {
- return ValidationResult.error(ValidationResult.Message.PARENT_NOT_DIR);
+ return ValidationResult.error(ValidationResult.Message.PARENT_NOT_DIR, fieldName);
+ }
+
+ String installLocation = PathManager.getHomePathFor(Application.class);
+ if (installLocation != null && FileUtil.isAncestor(new File(installLocation), file, false)) {
+ String applicationName = ApplicationNamesInfo.getInstance().getProductName();
+ return ValidationResult.error(ValidationResult.Message.INSIDE_ANDROID_STUDIO, fieldName, applicationName);
+ }
+
+ if (checkEmpty && file.exists() && TemplateUtils.listFiles(file).length > 0) {
+ return ValidationResult.warn(ValidationResult.Message.NON_EMPTY_DIR, fieldName);
}
return (warningResult == null) ? ValidationResult.OK : warningResult;
}
-
}
diff --git a/android/src/com/intellij/android/designer/model/layout/TextDirection.java b/android/src/com/intellij/android/designer/model/layout/TextDirection.java
new file mode 100644
index 0000000..133874b
--- /dev/null
+++ b/android/src/com/intellij/android/designer/model/layout/TextDirection.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.android.designer.model.layout;
+
+import com.android.ide.common.resources.configuration.LayoutDirectionQualifier;
+import com.android.resources.LayoutDirection;
+import com.android.tools.idea.configurations.Configuration;
+import com.android.tools.idea.designer.SegmentType;
+import com.intellij.android.designer.designSurface.AndroidDesignerEditorPanel;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import static com.android.SdkConstants.*;
+
+/**
+ * RTL state for the designer editor. It provides helper methods to manage the RTL attributes.
+ */
+public enum TextDirection {
+ LEFT_TO_RIGHT,
+ RIGHT_TO_LEFT;
+
+ /**
+ * Returns the {@linkplain TextDirection} used in the passed {@link AndroidDesignerEditorPanel}. If the passed panel is null, LTR is used.
+ */
+ public static TextDirection fromAndroidDesignerEditorPanel(@Nullable AndroidDesignerEditorPanel panel) {
+ Configuration configuration = panel != null ? panel.getConfiguration() : null;
+
+ if (configuration == null) {
+ return LEFT_TO_RIGHT;
+ }
+ LayoutDirectionQualifier qualifier = configuration.getFullConfig().getLayoutDirectionQualifier();
+ return qualifier == null || qualifier.getValue() != LayoutDirection.RTL ? LEFT_TO_RIGHT : RIGHT_TO_LEFT;
+ }
+
+ /**
+ * Returns the RTL SegmentType that corresponds to the LEFT side of the screen. This will be different depending if
+ * the user has RTL preview enabled or not.
+ */
+ @NotNull
+ public SegmentType getLeftSegment() {
+ return this == LEFT_TO_RIGHT ? SegmentType.START : SegmentType.END;
+ }
+
+ /**
+ * Returns the RTL SegmentType that corresponds to the RIGHT side of the screen. This will be different depending if
+ * the user has RTL preview enabled or not.
+ */
+ @NotNull
+ public SegmentType getRightSegment() {
+ return this == LEFT_TO_RIGHT ? SegmentType.END : SegmentType.START;
+ }
+
+ /**
+ * Returns whether the segment type is being seen on the left side of the screen. This is always the
+ * SegmentType.LEFT but it could also be SegmentType.START (if LTR is on) or SegmentType.END (if RTL is on)
+ *
+ * @see #getLeftSegment
+ */
+ public boolean isLeftSegment(@NotNull SegmentType type) {
+ return type == SegmentType.LEFT || type == getLeftSegment();
+ }
+
+ /**
+ * Returns whether the segment type is being seen on the right side of the screen. This is always the
+ * SegmentType.RIGHT but it could also be SegmentType.END (if LTR is on) or SegmentType.START (if RTL is on)
+ *
+ * @see #getRightSegment
+ */
+ public boolean isRightSegment(@NotNull SegmentType type) {
+ return type == SegmentType.RIGHT || type == getRightSegment();
+ }
+
+ /**
+ * Returns the RTL attribute name for aligning components left on screen.
+ */
+ @NotNull
+ public String getAttrLeft() {
+ return this == LEFT_TO_RIGHT ? ATTR_LAYOUT_ALIGN_START : ATTR_LAYOUT_ALIGN_END;
+ }
+
+ /**
+ * Returns the RTL attribute name for aligning components left on screen.
+ */
+ @NotNull
+ public String getAttrLeftOf() {
+ return this == LEFT_TO_RIGHT ? ATTR_LAYOUT_TO_START_OF : ATTR_LAYOUT_TO_END_OF;
+ }
+
+ /**
+ * Returns the RTL attribute name for aligning components right on screen.
+ */
+ @NotNull
+ public String getAttrRight() {
+ return this == LEFT_TO_RIGHT ? ATTR_LAYOUT_ALIGN_END : ATTR_LAYOUT_ALIGN_START;
+ }
+
+ /**
+ * Returns the RTL attribute name for aligning components right-of on screen.
+ */
+ @NotNull
+ public String getAttrRightOf() {
+ return this == LEFT_TO_RIGHT ? ATTR_LAYOUT_TO_END_OF : ATTR_LAYOUT_TO_START_OF;
+ }
+
+ /**
+ * Returns the RTL attribute name for aligning components parent-left on screen.
+ */
+ @NotNull
+ public String getAttrAlignParentLeft() {
+ return this == LEFT_TO_RIGHT ? ATTR_LAYOUT_ALIGN_PARENT_START : ATTR_LAYOUT_ALIGN_PARENT_END;
+ }
+
+ /**
+ * Returns the RTL attribute name for aligning components parent-right on screen.
+ */
+ @NotNull
+ public String getAttrAlignParentRight() {
+ return this == LEFT_TO_RIGHT ? ATTR_LAYOUT_ALIGN_PARENT_END : ATTR_LAYOUT_ALIGN_PARENT_START;
+ }
+
+ /**
+ * Returns the RTL attribute name to specify the margin on the left side of the component.
+ */
+ @NotNull
+ public String getAttrMarginLeft() {
+ return this == LEFT_TO_RIGHT ? ATTR_LAYOUT_MARGIN_START : ATTR_LAYOUT_MARGIN_END;
+ }
+
+ /**
+ * Returns the RTL attribute name to specify the margin on the right side of the component.
+ */
+ @NotNull
+ public String getAttrMarginRight() {
+ return this == LEFT_TO_RIGHT ? ATTR_LAYOUT_MARGIN_END : ATTR_LAYOUT_MARGIN_START;
+ }
+}
diff --git a/android/src/com/intellij/android/designer/model/layout/actions/OrientationAction.java b/android/src/com/intellij/android/designer/model/layout/actions/OrientationAction.java
index 243e322..e1bcec7 100644
--- a/android/src/com/intellij/android/designer/model/layout/actions/OrientationAction.java
+++ b/android/src/com/intellij/android/designer/model/layout/actions/OrientationAction.java
@@ -43,9 +43,8 @@
super(designer, "Change attribute 'orientation'", null, AndroidDesignerIcons.SwitchHorizontalLinear);
myLayout = layout;
myDefaultHorizontal = defaultHorizontal;
- update(getTemplatePresentation());
-
myHorizontal = !VALUE_VERTICAL.equals(layout.getTag().getAttributeValue(ATTR_ORIENTATION, ANDROID_URI));
+ update(getTemplatePresentation());
}
@Override
diff --git a/android/src/com/intellij/android/designer/model/layout/relative/ConstraintPainter.java b/android/src/com/intellij/android/designer/model/layout/relative/ConstraintPainter.java
index fe7ca35..46b469a 100644
--- a/android/src/com/intellij/android/designer/model/layout/relative/ConstraintPainter.java
+++ b/android/src/com/intellij/android/designer/model/layout/relative/ConstraintPainter.java
@@ -19,6 +19,7 @@
import com.android.tools.idea.designer.SegmentType;
import com.intellij.android.designer.designSurface.graphics.DesignerGraphics;
import com.intellij.android.designer.model.RadViewComponent;
+import com.intellij.android.designer.model.layout.TextDirection;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
@@ -52,7 +53,8 @@
* @param state the handler state
* @param match the match
*/
- static void paintConstraint(DesignerGraphics graphics, GuidelineHandler state, Match match) {
+ static void paintConstraint(
+ DesignerGraphics graphics, GuidelineHandler state, Match match) {
RadViewComponent node = match.edge.node;
if (node == null) {
return;
@@ -63,7 +65,8 @@
assert type != null;
Rectangle sourceBounds = state.myBounds;
paintConstraint(graphics, type, node, sourceBounds, node, targetBounds, null /* allConstraints */,
- true /* highlightTargetEdge */);
+ true /* highlightTargetEdge */,
+ state.myTextDirection);
}
/**
@@ -75,7 +78,10 @@
* @param graphics the graphics context to draw into
* @param constraint The constraint to be drawn
*/
- private static void paintConstraint(DesignerGraphics graphics, DependencyGraph.Constraint constraint, Set<DependencyGraph.Constraint> allConstraints) {
+ private static void paintConstraint(DesignerGraphics graphics,
+ DependencyGraph.Constraint constraint,
+ Set<DependencyGraph.Constraint> allConstraints,
+ TextDirection textDirection) {
DependencyGraph.ViewData source = constraint.from;
DependencyGraph.ViewData target = constraint.to;
@@ -89,7 +95,8 @@
JComponent targetComponent = graphics.getTarget();
Rectangle sourceBounds = sourceNode.getBounds(targetComponent);
Rectangle targetBounds = targetNode.getBounds(targetComponent);
- paintConstraint(graphics, constraint.type, sourceNode, sourceBounds, targetNode, targetBounds, allConstraints, false /* highlightTargetEdge */);
+ paintConstraint(graphics, constraint.type, sourceNode, sourceBounds, targetNode, targetBounds,
+ allConstraints, false /* highlightTargetEdge */, textDirection);
}
/**
@@ -103,7 +110,8 @@
public static void paintSelectionFeedback(DesignerGraphics graphics,
RadViewComponent parentNode,
List<? extends RadViewComponent> childNodes,
- boolean showDependents) {
+ boolean showDependents,
+ TextDirection textDirection) {
DependencyGraph dependencyGraph = DependencyGraph.get(parentNode);
Set<RadViewComponent> horizontalDeps = dependencyGraph.dependsOn(childNodes, false /* vertical */);
@@ -131,18 +139,18 @@
// Paint all incoming constraints
if (showDependents) {
- paintConstraints(graphics, view.dependedOnBy);
+ paintConstraints(graphics, view.dependedOnBy, textDirection);
}
// Paint all outgoing constraints
- paintConstraints(graphics, view.dependsOn);
+ paintConstraints(graphics, view.dependsOn, textDirection);
}
}
/**
* Paints a set of constraints.
*/
- private static void paintConstraints(DesignerGraphics graphics, List<DependencyGraph.Constraint> constraints) {
+ private static void paintConstraints(DesignerGraphics graphics, List<DependencyGraph.Constraint> constraints, TextDirection textDirection) {
Set<DependencyGraph.Constraint> mutableConstraintSet = new HashSet<DependencyGraph.Constraint>(constraints);
// WORKAROUND! Hide alignBottom attachments if we also have a alignBaseline
@@ -164,7 +172,7 @@
// paintConstraint can digest more than one constraint, so we need to keep
// checking to see if the given constraint is still relevant.
if (mutableConstraintSet.contains(constraint)) {
- paintConstraint(graphics, constraint, mutableConstraintSet);
+ paintConstraint(graphics, constraint, mutableConstraintSet, textDirection);
}
}
}
@@ -180,7 +188,8 @@
RadViewComponent targetNode,
Rectangle targetBounds,
@Nullable Set<DependencyGraph.Constraint> allConstraints,
- boolean highlightTargetEdge) {
+ boolean highlightTargetEdge,
+ TextDirection textDirection) {
SegmentType sourceSegmentTypeX = type.sourceSegmentTypeX;
SegmentType sourceSegmentTypeY = type.sourceSegmentTypeY;
@@ -201,20 +210,20 @@
// Corner constraint?
if (allConstraints != null && (type == ConstraintType.LAYOUT_ABOVE || type == ConstraintType.LAYOUT_BELOW || type == ConstraintType.LAYOUT_LEFT_OF || type == ConstraintType.LAYOUT_RIGHT_OF)) {
- if (paintCornerConstraint(graphics, type, sourceNode, sourceBounds, targetNode, targetBounds, allConstraints)) {
+ if (paintCornerConstraint(graphics, type, sourceNode, sourceBounds, targetNode, targetBounds, allConstraints, textDirection)) {
return;
}
}
// Vertical constraint?
if (sourceSegmentTypeX == SegmentType.UNKNOWN) {
- paintVerticalConstraint(graphics, type, sourceNode, sourceBounds, targetNode, targetBounds, highlightTargetEdge);
+ paintVerticalConstraint(graphics, type, sourceNode, sourceBounds, targetNode, targetBounds, highlightTargetEdge, textDirection);
return;
}
// Horizontal constraint?
if (sourceSegmentTypeY == SegmentType.UNKNOWN) {
- paintHorizontalConstraint(graphics, type, sourceNode, sourceBounds, targetNode, targetBounds, highlightTargetEdge);
+ paintHorizontalConstraint(graphics, type, sourceNode, sourceBounds, targetNode, targetBounds, highlightTargetEdge, textDirection);
return;
}
@@ -273,7 +282,8 @@
Rectangle sourceBounds,
RadViewComponent targetNode,
Rectangle targetBounds,
- Set<DependencyGraph.Constraint> allConstraints) {
+ Set<DependencyGraph.Constraint> allConstraints,
+ TextDirection textDirection) {
SegmentType sourceSegmentTypeX = type.sourceSegmentTypeX;
SegmentType sourceSegmentTypeY = type.sourceSegmentTypeY;
@@ -323,7 +333,7 @@
}
int x1, y1, x2, y2;
- if (sourceSegmentTypeX == SegmentType.LEFT) {
+ if (textDirection.isLeftSegment(sourceSegmentTypeX)) {
x1 = sourceBounds.x + 1 * sourceBounds.width / 4;
}
else {
@@ -335,7 +345,7 @@
else {
y1 = sourceBounds.y + 3 * sourceBounds.height / 4;
}
- if (targetSegmentTypeX == SegmentType.LEFT) {
+ if (textDirection.isLeftSegment(targetSegmentTypeX)) {
x2 = targetBounds.x + 1 * targetBounds.width / 4;
}
else {
@@ -386,7 +396,8 @@
Rectangle sourceBounds,
RadViewComponent targetNode,
Rectangle targetBounds,
- boolean highlightTargetEdge) {
+ boolean highlightTargetEdge,
+ TextDirection textDirection) {
SegmentType sourceSegmentTypeY = type.sourceSegmentTypeY;
SegmentType targetSegmentTypeY = type.targetSegmentTypeY;
JComponent targetComponent = graphics.getTarget();
@@ -627,7 +638,8 @@
Rectangle sourceBounds,
RadViewComponent targetNode,
Rectangle targetBounds,
- boolean highlightTargetEdge) {
+ boolean highlightTargetEdge,
+ TextDirection textDirection) {
SegmentType sourceSegmentTypeX = type.sourceSegmentTypeX;
SegmentType targetSegmentTypeX = type.targetSegmentTypeX;
JComponent targetComponent = graphics.getTarget();
@@ -638,8 +650,8 @@
// See paintVerticalConstraint for explanations of the various cases.
- int sourceX = sourceSegmentTypeX.getX(sourceNode, sourceBounds);
- int targetX = targetSegmentTypeX == SegmentType.UNKNOWN ? sourceX : targetSegmentTypeX.getX(targetNode, targetBounds);
+ int sourceX = sourceSegmentTypeX.getX(textDirection, sourceNode, sourceBounds);
+ int targetX = targetSegmentTypeX == SegmentType.UNKNOWN ? sourceX : targetSegmentTypeX.getX(textDirection, targetNode, targetBounds);
if (highlightTargetEdge && type.isRelativeToParentEdge()) {
graphics.useStyle(DROP_ZONE_ACTIVE);
@@ -654,7 +666,7 @@
int center = (maxTop + minBottom) / 2;
if (center > sourceBounds.y && center < GuidelineHandler.y2(sourceBounds)) {
// See if we should draw a margin line
- if (targetSegmentTypeX == SegmentType.RIGHT && targetMargins.right > 5) {
+ if (textDirection.isRightSegment(targetSegmentTypeX) && targetMargins.right > 5) {
int sharedX = targetX + targetMargins.right;
if (sourceX > sharedX + 2) { // Skip when source falls on the margin line
graphics.useStyle(GUIDELINE_DASHED);
@@ -671,7 +683,7 @@
}
return;
}
- else if (targetSegmentTypeX == SegmentType.LEFT && targetMargins.left > 5) {
+ else if (textDirection.isLeftSegment(targetSegmentTypeX) && targetMargins.left > 5) {
int sharedX = targetX - targetMargins.left;
if (sourceX < sharedX - 2) {
graphics.useStyle(GUIDELINE_DASHED);
@@ -688,7 +700,7 @@
}
if (sourceX == targetX) {
- if (sourceSegmentTypeX == SegmentType.RIGHT) {
+ if (textDirection.isRightSegment(sourceSegmentTypeX)) {
sourceX -= 2 * ARROW_SIZE;
}
else if (sourceSegmentTypeX == SegmentType.LEFT) {
@@ -708,12 +720,12 @@
// Segment line
// Compute overlap region and pick the middle
- int sharedX = targetSegmentTypeX == SegmentType.UNKNOWN ? sourceX : targetSegmentTypeX.getX(targetNode, targetBounds);
+ int sharedX = targetSegmentTypeX == SegmentType.UNKNOWN ? sourceX : targetSegmentTypeX.getX(textDirection, targetNode, targetBounds);
if (type.relativeToMargin) {
- if (targetSegmentTypeX == SegmentType.LEFT) {
+ if (textDirection.isLeftSegment(targetSegmentTypeX)) {
sharedX -= targetMargins.left;
}
- else if (targetSegmentTypeX == SegmentType.RIGHT) {
+ else if (textDirection.isRightSegment(targetSegmentTypeX)) {
sharedX += targetMargins.right;
}
}
@@ -755,7 +767,7 @@
// Draw the line from the target to the horizontal shared edge
int ty = GuidelineHandler.centerY(targetBounds);
- if (targetSegmentTypeX == SegmentType.LEFT) {
+ if (textDirection.isLeftSegment(targetSegmentTypeX)) {
int tx = targetBounds.x;
int margin = targetMargins.left;
if (margin == 0 || !type.relativeToMargin) {
@@ -766,7 +778,7 @@
}
}
else {
- assert targetSegmentTypeX == SegmentType.RIGHT;
+ assert textDirection.isRightSegment(targetSegmentTypeX);
int tx = GuidelineHandler.x2(targetBounds);
int margin = targetMargins.right;
if (margin == 0 || !type.relativeToMargin) {
diff --git a/android/src/com/intellij/android/designer/model/layout/relative/ConstraintType.java b/android/src/com/intellij/android/designer/model/layout/relative/ConstraintType.java
index 6153071..7159582 100644
--- a/android/src/com/intellij/android/designer/model/layout/relative/ConstraintType.java
+++ b/android/src/com/intellij/android/designer/model/layout/relative/ConstraintType.java
@@ -37,8 +37,12 @@
ALIGN_BOTTOM(ATTR_LAYOUT_ALIGN_BOTTOM, null, SegmentType.BOTTOM, null, SegmentType.BOTTOM, false, true, false, false),
ALIGN_LEFT(ATTR_LAYOUT_ALIGN_LEFT, SegmentType.LEFT, null, SegmentType.LEFT, null, false, false, true, false),
ALIGN_RIGHT(ATTR_LAYOUT_ALIGN_RIGHT, SegmentType.RIGHT, null, SegmentType.RIGHT, null, false, false, true, false),
+ LAYOUT_ALIGN_START(ATTR_LAYOUT_ALIGN_START, SegmentType.START, null, SegmentType.START, null, false, false, true, false),
+ LAYOUT_ALIGN_END(ATTR_LAYOUT_ALIGN_END, SegmentType.END, null, SegmentType.END, null, false, false, true, false),
LAYOUT_LEFT_OF(ATTR_LAYOUT_TO_LEFT_OF, SegmentType.RIGHT, null, SegmentType.LEFT, null, false, false, true, true),
LAYOUT_RIGHT_OF(ATTR_LAYOUT_TO_RIGHT_OF, SegmentType.LEFT, null, SegmentType.RIGHT, null, false, false, true, true),
+ LAYOUT_ALIGN_START_OF(ATTR_LAYOUT_TO_START_OF, SegmentType.START, null, SegmentType.START, null, false, false, true, false),
+ LAYOUT_ALIGN_END_OF(ATTR_LAYOUT_TO_END_OF, SegmentType.END, null, SegmentType.END, null, false, false, true, false),
ALIGN_PARENT_TOP(ATTR_LAYOUT_ALIGN_PARENT_TOP, null, SegmentType.TOP, null, SegmentType.TOP, true, true, false, false),
ALIGN_BASELINE(ATTR_LAYOUT_ALIGN_BASELINE, null, SegmentType.BASELINE, null, SegmentType.BASELINE, false, true, false, false),
ALIGN_PARENT_LEFT(ATTR_LAYOUT_ALIGN_PARENT_LEFT, SegmentType.LEFT, null, SegmentType.LEFT, null, true, false, true, false),
@@ -223,6 +227,20 @@
break;
case BASELINE:
return ALIGN_BASELINE;
+ case START:
+ switch (to) {
+ case START:
+ return LAYOUT_ALIGN_START;
+ case END:
+ return LAYOUT_ALIGN_END_OF;
+ }
+ case END:
+ switch (to) {
+ case START:
+ return LAYOUT_ALIGN_START_OF;
+ case END:
+ return LAYOUT_ALIGN_END;
+ }
}
return null;
diff --git a/android/src/com/intellij/android/designer/model/layout/relative/GuidelineHandler.java b/android/src/com/intellij/android/designer/model/layout/relative/GuidelineHandler.java
index 986c113..de29c78 100644
--- a/android/src/com/intellij/android/designer/model/layout/relative/GuidelineHandler.java
+++ b/android/src/com/intellij/android/designer/model/layout/relative/GuidelineHandler.java
@@ -19,7 +19,9 @@
import com.android.tools.idea.designer.Segment;
import com.android.tools.idea.designer.SegmentType;
import com.intellij.android.designer.AndroidDesignerUtils;
+import com.intellij.android.designer.designSurface.AndroidDesignerEditorPanel;
import com.intellij.android.designer.model.RadViewComponent;
+import com.intellij.android.designer.model.layout.TextDirection;
import com.intellij.designer.designSurface.OperationContext;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.util.text.StringUtil;
@@ -48,6 +50,7 @@
* like move and resize, and performs various constraint computations.
*/
public class GuidelineHandler {
+
/**
* A dependency graph for the relative layout recording constraint relationships
*/
@@ -217,6 +220,11 @@
protected String myErrorMessage;
/**
+ * Is the operation running on an RTL locale?
+ */
+ protected final TextDirection myTextDirection;
+
+ /**
* Construct a new {@link GuidelineHandler} for the given relative layout.
*
* @param layout the RelativeLayout to handle
@@ -225,6 +233,9 @@
this.layout = layout;
myContext = context;
+ AndroidDesignerEditorPanel panel = AndroidDesignerUtils.getPanel(myContext.getArea());
+ myTextDirection = TextDirection.fromAndroidDesignerEditorPanel(panel);
+
myHorizontalEdges = new ArrayList<Segment>();
myVerticalEdges = new ArrayList<Segment>();
myCenterVertEdges = new ArrayList<Segment>();
@@ -422,7 +433,7 @@
return dragged == SegmentType.TOP || dragged == SegmentType.BOTTOM;
case LEFT:
case RIGHT:
- return dragged == SegmentType.LEFT || dragged == SegmentType.RIGHT;
+ return dragged == SegmentType.LEFT || dragged == SegmentType.RIGHT || dragged == SegmentType.START || dragged == SegmentType.END;
// Center horizontal, center vertical and Baseline only matches the same
// type, and only within the matching distance -- no margins!
@@ -572,6 +583,8 @@
clearAttribute(n, ANDROID_URI, ATTR_LAYOUT_ALIGN_START);
clearAttribute(n, ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF);
clearAttribute(n, ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL);
+ clearAttribute(n, ANDROID_URI, myTextDirection.getAttrLeft());
+ clearAttribute(n, ANDROID_URI, myTextDirection.getAttrLeftOf());
}
if (myMoveRight) {
@@ -582,6 +595,8 @@
clearAttribute(n, ANDROID_URI, ATTR_LAYOUT_ALIGN_END);
clearAttribute(n, ANDROID_URI, ATTR_LAYOUT_TO_LEFT_OF);
clearAttribute(n, ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL);
+ clearAttribute(n, ANDROID_URI, myTextDirection.getAttrRight());
+ clearAttribute(n, ANDROID_URI, myTextDirection.getAttrRightOf());
}
if (myMoveTop && myCurrentTopMatch != null) {
@@ -600,7 +615,7 @@
if (myMoveLeft && myCurrentLeftMatch != null) {
String constraint = myCurrentLeftMatch.getConstraint(true);
- String rtlConstraint = myCurrentLeftMatch.getRtlConstraint(true);
+ String rtlConstraint = myCurrentLeftMatch.getRtlConstraint(myTextDirection, true);
if (rtlConstraint != null && supportsStartEnd()) {
if (requiresRightLeft()) {
applyConstraint(n, constraint);
@@ -613,7 +628,7 @@
if (myMoveRight && myCurrentRightMatch != null) {
String constraint = myCurrentRightMatch.getConstraint(true);
- String rtlConstraint = myCurrentRightMatch.getRtlConstraint(true);
+ String rtlConstraint = myCurrentRightMatch.getRtlConstraint(myTextDirection, true);
if (rtlConstraint != null && supportsStartEnd()) {
if (requiresRightLeft()) {
applyConstraint(n, constraint);
@@ -625,10 +640,26 @@
}
if (myMoveLeft) {
- applyMargin(n, ATTR_LAYOUT_MARGIN_LEFT, getLeftMarginDp());
+ if (supportsStartEnd()) {
+ if (requiresRightLeft()) {
+ applyMargin(n, ATTR_LAYOUT_MARGIN_LEFT, getLeftMarginDp());
+ }
+ applyMargin(n, myTextDirection.getAttrMarginLeft(), getLeftMarginDp());
+ }
+ else {
+ applyMargin(n, ATTR_LAYOUT_MARGIN_LEFT, getLeftMarginDp());
+ }
}
if (myMoveRight) {
- applyMargin(n, ATTR_LAYOUT_MARGIN_RIGHT, getRightMarginDp());
+ if (supportsStartEnd()) {
+ if (requiresRightLeft()) {
+ applyMargin(n, ATTR_LAYOUT_MARGIN_RIGHT, getRightMarginDp());
+ }
+ applyMargin(n, myTextDirection.getAttrMarginRight(), getRightMarginDp());
+ }
+ else {
+ applyMargin(n, ATTR_LAYOUT_MARGIN_RIGHT, getRightMarginDp());
+ }
}
if (myMoveTop) {
applyMargin(n, ATTR_LAYOUT_MARGIN_TOP, getTopMarginDp());
@@ -689,6 +720,8 @@
for (ConstraintType type : ConstraintType.values()) {
clearAttribute(node, ANDROID_URI, type.name);
}
+ clearAttribute(node, ANDROID_URI, ATTR_LAYOUT_MARGIN_START);
+ clearAttribute(node, ANDROID_URI, ATTR_LAYOUT_MARGIN_END);
clearAttribute(node, ANDROID_URI, ATTR_LAYOUT_MARGIN_LEFT);
clearAttribute(node, ANDROID_URI, ATTR_LAYOUT_MARGIN_RIGHT);
clearAttribute(node, ANDROID_URI, ATTR_LAYOUT_MARGIN_TOP);
@@ -971,8 +1004,8 @@
}
// Prefer matching top/left edges before matching bottom/right edges
- int orientation1 = (m1.with.edgeType == SegmentType.LEFT || m1.with.edgeType == SegmentType.TOP) ? -1 : 1;
- int orientation2 = (m2.with.edgeType == SegmentType.LEFT || m2.with.edgeType == SegmentType.TOP) ? -1 : 1;
+ int orientation1 = (myTextDirection.isLeftSegment(m1.with.edgeType) || m1.with.edgeType == SegmentType.TOP) ? -1 : 1;
+ int orientation2 = (myTextDirection.isLeftSegment(m2.with.edgeType) || m2.with.edgeType == SegmentType.TOP) ? -1 : 1;
if (orientation1 != orientation2) {
return orientation1 - orientation2;
}
diff --git a/android/src/com/intellij/android/designer/model/layout/relative/Match.java b/android/src/com/intellij/android/designer/model/layout/relative/Match.java
index 49d211b..7a97173 100644
--- a/android/src/com/intellij/android/designer/model/layout/relative/Match.java
+++ b/android/src/com/intellij/android/designer/model/layout/relative/Match.java
@@ -17,6 +17,7 @@
import com.android.tools.idea.designer.Segment;
import com.intellij.android.designer.designSurface.feedbacks.TextFeedback;
+import com.intellij.android.designer.model.layout.TextDirection;
import com.intellij.ui.SimpleTextAttributes;
import org.jetbrains.annotations.Nullable;
@@ -136,20 +137,20 @@
}
@Nullable
- public String getRtlConstraint(boolean generateId) {
+ public String getRtlConstraint(TextDirection textDirection, boolean generateId) {
switch (type) {
case ALIGN_LEFT:
- return replaceAttribute(getConstraint(generateId), ATTR_LAYOUT_ALIGN_LEFT, ATTR_LAYOUT_ALIGN_START);
+ return replaceAttribute(getConstraint(generateId), ATTR_LAYOUT_ALIGN_LEFT, textDirection.getAttrLeft());
case LAYOUT_LEFT_OF:
- return replaceAttribute(getConstraint(generateId), ATTR_LAYOUT_TO_LEFT_OF, ATTR_LAYOUT_TO_START_OF);
+ return replaceAttribute(getConstraint(generateId), ATTR_LAYOUT_TO_LEFT_OF, textDirection.getAttrLeftOf());
case ALIGN_RIGHT:
- return replaceAttribute(getConstraint(generateId), ATTR_LAYOUT_ALIGN_RIGHT, ATTR_LAYOUT_ALIGN_END);
+ return replaceAttribute(getConstraint(generateId), ATTR_LAYOUT_ALIGN_RIGHT, textDirection.getAttrRight());
case LAYOUT_RIGHT_OF:
- return replaceAttribute(getConstraint(generateId), ATTR_LAYOUT_TO_RIGHT_OF, ATTR_LAYOUT_TO_END_OF);
+ return replaceAttribute(getConstraint(generateId), ATTR_LAYOUT_TO_RIGHT_OF, textDirection.getAttrRightOf());
case ALIGN_PARENT_LEFT:
- return replaceAttribute(getConstraint(generateId), ATTR_LAYOUT_ALIGN_PARENT_LEFT, ATTR_LAYOUT_ALIGN_PARENT_START);
+ return replaceAttribute(getConstraint(generateId), ATTR_LAYOUT_ALIGN_PARENT_LEFT, textDirection.getAttrAlignParentLeft());
case ALIGN_PARENT_RIGHT:
- return replaceAttribute(getConstraint(generateId), ATTR_LAYOUT_ALIGN_PARENT_RIGHT, ATTR_LAYOUT_ALIGN_PARENT_END);
+ return replaceAttribute(getConstraint(generateId), ATTR_LAYOUT_ALIGN_PARENT_RIGHT, textDirection.getAttrAlignParentRight());
}
return null;
}
diff --git a/android/src/com/intellij/android/designer/model/layout/relative/MoveHandler.java b/android/src/com/intellij/android/designer/model/layout/relative/MoveHandler.java
index 172d8e2..7b4cbc7 100644
--- a/android/src/com/intellij/android/designer/model/layout/relative/MoveHandler.java
+++ b/android/src/com/intellij/android/designer/model/layout/relative/MoveHandler.java
@@ -50,9 +50,7 @@
* @param elements the elements being dragged in the move operation
* @param context the applicable {@link com.intellij.designer.designSurface.OperationContext}
*/
- public MoveHandler(@NotNull RadViewComponent layout,
- @NotNull List<RadViewComponent> elements,
- @NotNull OperationContext context) {
+ public MoveHandler(@NotNull RadViewComponent layout, @NotNull List<RadViewComponent> elements, @NotNull OperationContext context) {
super(layout, context);
// Compute list of nodes being dragged within the layout, if any
@@ -97,7 +95,7 @@
@Override
protected void snapVertical(Segment vEdge, int x, Rectangle newBounds) {
int maxDistance = MAX_MATCH_DISTANCE;
- if (vEdge.edgeType == SegmentType.LEFT) {
+ if (myTextDirection.isLeftSegment(vEdge.edgeType)) {
int margin = !mySnap ? 0 : abs(newBounds.x - x);
if (margin > maxDistance) {
myLeftMargin = margin;
@@ -106,7 +104,7 @@
newBounds.x = x;
}
}
- else if (vEdge.edgeType == SegmentType.RIGHT) {
+ else if (myTextDirection.isRightSegment(vEdge.edgeType)) {
int margin = !mySnap ? 0 : abs(newBounds.x - (x - newBounds.width));
if (margin > maxDistance) {
myRightMargin = margin;
@@ -181,10 +179,15 @@
edge = new Segment(y2(b), b.x, x2(b), null, null, SegmentType.BOTTOM, NO_MARGIN);
addClosest(edge, myHorizontalEdges, horizontalMatches);
+ // We add the LEFT and RIGHT segments. Also we add the START and END segments that will change based on the current RTL view context.
edge = new Segment(b.x, b.y, y2(b), null, null, SegmentType.LEFT, NO_MARGIN);
List<Match> verticalMatches = findClosest(edge, myVerticalEdges);
+ edge = new Segment(b.x, b.y, y2(b), null, null, myTextDirection.getLeftSegment(), NO_MARGIN);
+ addClosest(edge, myVerticalEdges, verticalMatches);
edge = new Segment(x2(b), b.y, y2(b), null, null, SegmentType.RIGHT, NO_MARGIN);
addClosest(edge, myVerticalEdges, verticalMatches);
+ edge = new Segment(x2(b), b.y, y2(b), null, null, myTextDirection.getRightSegment(), NO_MARGIN);
+ addClosest(edge, myVerticalEdges, verticalMatches);
// Match center
edge = new Segment(centerX(b), b.y, y2(b), null, null, SegmentType.CENTER_VERTICAL, NO_MARGIN);
@@ -254,10 +257,10 @@
snapVertical(match.with, match.edge.at, myBounds);
- if (match.with.edgeType == SegmentType.LEFT) {
+ if (myTextDirection.isLeftSegment(match.with.edgeType)) {
myCurrentLeftMatch = match;
}
- else if (match.with.edgeType == SegmentType.RIGHT) {
+ else if (myTextDirection.isRightSegment(match.with.edgeType)) {
myCurrentRightMatch = match;
}
else {
diff --git a/android/src/com/intellij/android/designer/model/layout/relative/RelativeLayoutDecorator.java b/android/src/com/intellij/android/designer/model/layout/relative/RelativeLayoutDecorator.java
index 1273394..6be909c 100644
--- a/android/src/com/intellij/android/designer/model/layout/relative/RelativeLayoutDecorator.java
+++ b/android/src/com/intellij/android/designer/model/layout/relative/RelativeLayoutDecorator.java
@@ -15,8 +15,11 @@
*/
package com.intellij.android.designer.model.layout.relative;
+import com.intellij.android.designer.AndroidDesignerUtils;
+import com.intellij.android.designer.designSurface.AndroidDesignerEditorPanel;
import com.intellij.android.designer.designSurface.graphics.DesignerGraphics;
import com.intellij.android.designer.model.RadViewComponent;
+import com.intellij.android.designer.model.layout.TextDirection;
import com.intellij.designer.designSurface.DecorationLayer;
import com.intellij.designer.designSurface.StaticDecorator;
import com.intellij.designer.model.RadComponent;
@@ -46,10 +49,11 @@
if (container instanceof RadViewComponent) {
DesignerGraphics graphics = new DesignerGraphics(g, layer);
RadViewComponent parent = (RadViewComponent)container;
-
+ AndroidDesignerEditorPanel panel = AndroidDesignerUtils.getPanel(layer.getArea());
List<RadViewComponent> childNodes = RadViewComponent.getViewComponents(selection);
boolean showDependents = parent.getChildren().size() > childNodes.size();
- ConstraintPainter.paintSelectionFeedback(graphics, parent, childNodes, showDependents);
+ ConstraintPainter
+ .paintSelectionFeedback(graphics, parent, childNodes, showDependents, TextDirection.fromAndroidDesignerEditorPanel(panel));
}
}
}
diff --git a/android/src/com/intellij/android/designer/model/layout/relative/ResizeHandler.java b/android/src/com/intellij/android/designer/model/layout/relative/ResizeHandler.java
index 54aebb7..16fc857 100644
--- a/android/src/com/intellij/android/designer/model/layout/relative/ResizeHandler.java
+++ b/android/src/com/intellij/android/designer/model/layout/relative/ResizeHandler.java
@@ -204,10 +204,10 @@
assert myHorizontalEdgeType == null;
}
- if (myVerticalEdgeType == SegmentType.LEFT) {
+ if (myTextDirection.isLeftSegment(myVerticalEdgeType)) {
vEdge = new Segment(b.x, b.y, y2(b), child, childId, myVerticalEdgeType, NO_MARGIN);
}
- else if (myVerticalEdgeType == SegmentType.RIGHT) {
+ else if (myTextDirection.isRightSegment(myVerticalEdgeType)) {
vEdge = new Segment(x2(b), b.y, y2(b), child, childId, myVerticalEdgeType, NO_MARGIN);
}
else {
@@ -253,10 +253,10 @@
// Snap
snapVertical(vEdge, match.edge.at, newBounds);
- if (vEdge.edgeType == SegmentType.LEFT) {
+ if (myTextDirection.isLeftSegment(vEdge.edgeType)) {
myCurrentLeftMatch = match;
}
- else if (vEdge.edgeType == SegmentType.RIGHT) {
+ else if (myTextDirection.isRightSegment(vEdge.edgeType)) {
myCurrentRightMatch = match;
}
else {
diff --git a/android/src/org/jetbrains/android/AndroidIdIndex.java b/android/src/org/jetbrains/android/AndroidIdIndex.java
index 193bff5..394d52e 100644
--- a/android/src/org/jetbrains/android/AndroidIdIndex.java
+++ b/android/src/org/jetbrains/android/AndroidIdIndex.java
@@ -91,6 +91,12 @@
@Override
public Set<String> read(@NotNull DataInput in) throws IOException {
final int size = in.readInt();
+
+ if (size < 0 || size > 65535) { // 65K: maximum number of resources for a given type
+ // Something is very wrong (corrupt index); trigger an index rebuild.
+ throw new IOException("Corrupt Index: Size " + size);
+ }
+
final Set<String> result = new HashSet<String>(size);
for (int i = 0; i < size; i++) {
diff --git a/android/src/org/jetbrains/android/AndroidValueResourcesIndex.java b/android/src/org/jetbrains/android/AndroidValueResourcesIndex.java
index 864fec0..efcf096 100644
--- a/android/src/org/jetbrains/android/AndroidValueResourcesIndex.java
+++ b/android/src/org/jetbrains/android/AndroidValueResourcesIndex.java
@@ -200,6 +200,11 @@
public Set<MyResourceInfo> read(@NotNull DataInput in) throws IOException {
final int size = in.readInt();
+ if (size < 0 || size > 65535) {
+ // Something is very wrong; trigger an index rebuild
+ throw new IOException("Corrupt Index: Size " + size);
+ }
+
if (size == 0) {
return Collections.emptySet();
}
diff --git a/android/src/org/jetbrains/android/inspections/lint/IntellijLintProject.java b/android/src/org/jetbrains/android/inspections/lint/IntellijLintProject.java
index 91d0f9e..fe8389a 100644
--- a/android/src/org/jetbrains/android/inspections/lint/IntellijLintProject.java
+++ b/android/src/org/jetbrains/android/inspections/lint/IntellijLintProject.java
@@ -20,6 +20,7 @@
import com.android.sdklib.AndroidTargetHash;
import com.android.sdklib.AndroidVersion;
import com.android.tools.idea.gradle.IdeaAndroidProject;
+import com.android.tools.idea.gradle.util.GradleUtil;
import com.android.tools.idea.model.AndroidModuleInfo;
import com.android.tools.idea.model.ManifestInfo;
import com.android.tools.lint.client.api.LintClient;
@@ -302,37 +303,6 @@
// the gradle data instead
}
- protected static boolean dependsOn(@NonNull Dependencies dependencies, @NonNull String artifact) {
- for (AndroidLibrary library : dependencies.getLibraries()) {
- if (dependsOn(library, artifact)) {
- return true;
- }
- }
- return false;
- }
-
- protected static boolean dependsOn(@NonNull AndroidLibrary library, @NonNull String artifact) {
- if (SUPPORT_LIB_ARTIFACT.equals(artifact)) {
- if (library.getJarFile().getName().startsWith("support-v4-")) {
- return true;
- }
-
- } else if (APPCOMPAT_LIB_ARTIFACT.equals(artifact)) {
- File bundle = library.getBundle();
- if (bundle.getName().startsWith("appcompat-v7-")) {
- return true;
- }
- }
-
- for (AndroidLibrary dependency : library.getLibraryDependencies()) {
- if (dependsOn(dependency, artifact)) {
- return true;
- }
- }
-
- return false;
- }
-
protected static boolean depsDependsOn(@NonNull Project project, @NonNull String artifact) {
// Checks project dependencies only; used when there is no model
for (Project dependency : project.getDirectLibraries()) {
@@ -815,9 +785,7 @@
if (SUPPORT_LIB_ARTIFACT.equals(artifact)) {
if (mSupportLib == null) {
if (myFacet.isGradleProject() && myFacet.getIdeaAndroidProject() != null) {
- IdeaAndroidProject gradleProject = myFacet.getIdeaAndroidProject();
- Dependencies dependencies = gradleProject.getSelectedVariant().getMainArtifact().getDependencies();
- mSupportLib = dependsOn(dependencies, artifact);
+ mSupportLib = GradleUtil.dependsOn(myFacet.getIdeaAndroidProject(), artifact);
} else {
mSupportLib = depsDependsOn(this, artifact);
}
@@ -826,15 +794,19 @@
} else if (APPCOMPAT_LIB_ARTIFACT.equals(artifact)) {
if (mAppCompat == null) {
if (myFacet.isGradleProject() && myFacet.getIdeaAndroidProject() != null) {
- IdeaAndroidProject gradleProject = myFacet.getIdeaAndroidProject();
- Dependencies dependencies = gradleProject.getSelectedVariant().getMainArtifact().getDependencies();
- mAppCompat = dependsOn(dependencies, artifact);
+ mAppCompat = GradleUtil.dependsOn(myFacet.getIdeaAndroidProject(), artifact);
} else {
mAppCompat = depsDependsOn(this, artifact);
}
}
return mAppCompat;
} else {
+ // Some other (not yet directly cached result)
+ if (myFacet.isGradleProject() && myFacet.getIdeaAndroidProject() != null
+ && GradleUtil.dependsOn(myFacet.getIdeaAndroidProject(), artifact)) {
+ return true;
+ }
+
return super.dependsOn(artifact);
}
}
@@ -955,15 +927,20 @@
public Boolean dependsOn(@NonNull String artifact) {
if (SUPPORT_LIB_ARTIFACT.equals(artifact)) {
if (mSupportLib == null) {
- mSupportLib = dependsOn(myLibrary, artifact);
+ mSupportLib = GradleUtil.dependsOn(myLibrary, artifact, true);
}
return mSupportLib;
} else if (APPCOMPAT_LIB_ARTIFACT.equals(artifact)) {
if (mAppCompat == null) {
- mAppCompat = dependsOn(myLibrary, artifact);
+ mAppCompat = GradleUtil.dependsOn(myLibrary, artifact, true);
}
return mAppCompat;
} else {
+ // Some other (not yet directly cached result)
+ if (GradleUtil.dependsOn(myLibrary, artifact, true)) {
+ return true;
+ }
+
return super.dependsOn(artifact);
}
}
diff --git a/android/src/org/jetbrains/android/sdk/AndroidSdkUtils.java b/android/src/org/jetbrains/android/sdk/AndroidSdkUtils.java
index 564277c..0a45b0e 100644
--- a/android/src/org/jetbrains/android/sdk/AndroidSdkUtils.java
+++ b/android/src/org/jetbrains/android/sdk/AndroidSdkUtils.java
@@ -60,6 +60,7 @@
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.*;
import com.intellij.util.ObjectUtils;
@@ -91,6 +92,11 @@
public static final String SDK_NAME_PREFIX = "Android ";
public static final String DEFAULT_JDK_NAME = "JDK";
+ // TODO: Update these to a stable link
+ private static final String MAC_SDK_URL = "http://dl.google.com/android/android-sdk_r22.6.2-macosx.zip";
+ private static final String LINUX_SDK_URL = "http://dl.google.com/android/android-sdk_r22.6.2-linux.tgz";
+ private static final String WINDOWS_SDK_URL = "http://dl.google.com/android/android-sdk_r22.6.2-windows.zip";
+
private static AndroidSdkData ourSdkData;
private AndroidSdkUtils() {
@@ -371,6 +377,7 @@
sdkModificator.setSdkAdditionalData(data);
if (addRoots) {
+ sdkModificator.removeAllRoots();
for (OrderRoot orderRoot : getLibraryRootsForTarget(target, androidSdk.getHomePath(), true)) {
sdkModificator.addRoot(orderRoot.getFile(), orderRoot.getType());
}
@@ -472,12 +479,8 @@
continue;
}
AndroidSdkAdditionalData data = (AndroidSdkAdditionalData)originalData;
- AndroidPlatform androidPlatform = data.getAndroidPlatform();
- if (androidPlatform == null) {
- continue;
- }
String sdkHomePath = sdk.getHomePath();
- if (!foundSdkHomePaths.contains(sdkHomePath) && targetHash.equals(androidPlatform.getTarget().hashString())) {
+ if (!foundSdkHomePaths.contains(sdkHomePath) && targetHash.equals(data.getBuildTargetHashString())) {
if (VersionCheck.isCompatibleVersion(sdkHomePath)) {
return sdk;
}
@@ -489,7 +492,6 @@
}
if (!notCompatibleSdks.isEmpty()) {
- // We got here because we have SDKs but none of them have a compatible Tools version. Pick the first one.
return notCompatibleSdks.get(0);
}
@@ -901,6 +903,22 @@
return bridge;
}
+ /**
+ * @return URL for the Android SDK download for the current platform. <code>null</code> means the platform was not recognized
+ */
+ @Nullable
+ public static String getSdkDownloadUrl() {
+ if (SystemInfo.isLinux) {
+ return LINUX_SDK_URL;
+ } else if (SystemInfo.isWindows) {
+ return WINDOWS_SDK_URL;
+ } else if (SystemInfo.isMac) {
+ return MAC_SDK_URL;
+ } else {
+ return null;
+ }
+ }
+
private static class MyMonitorBridgeConnectionTask extends Task.Modal {
private final Future<AndroidDebugBridge> myFuture;
private boolean myCancelled; // set/read only on EDT
diff --git a/android/testSrc/com/android/tools/idea/gradle/eclipse/GradleImportTest.java b/android/testSrc/com/android/tools/idea/gradle/eclipse/GradleImportTest.java
index 524f650..8900c55 100644
--- a/android/testSrc/com/android/tools/idea/gradle/eclipse/GradleImportTest.java
+++ b/android/testSrc/com/android/tools/idea/gradle/eclipse/GradleImportTest.java
@@ -17,6 +17,8 @@
import com.google.common.io.Files;
import org.jetbrains.android.AndroidTestCase;
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
@@ -262,6 +264,10 @@
new File(unhandled.getParentFile(), "unhandledDir4").mkdirs();
new File(projectDir, "lint.xml").createNewFile();
+ // Make sure we handle less common file extensions: see issue 78459
+ createIcon(projectDir, "other_icon.PNG");
+ createLayout(projectDir, "other_layout.XML");
+
// Project being imported
assertEquals(""
+ ".classpath\n"
@@ -280,6 +286,10 @@
+ "res\n"
+ " drawable\n"
+ " ic_launcher.xml\n"
+ + " drawable-hdpi\n"
+ + " other_icon.PNG\n"
+ + " layout\n"
+ + " other_layout.XML\n"
+ " values\n"
+ " strings.xml\n"
+ "src\n"
@@ -310,6 +320,8 @@
+ "* lint.xml => app/lint.xml\n"
+ "* res/ => app/src/main/res/\n"
+ "* src/ => app/src/main/java/\n"
+ + "* other_icon.PNG => other_icon.png\n"
+ + "* other_layout.XML => other_layout.xml\n"
+ MSG_FOOTER,
true /* checkBuild */);
@@ -328,6 +340,10 @@
+ " res\n"
+ " drawable\n"
+ " ic_launcher.xml\n"
+ + " drawable-hdpi\n"
+ + " other_icon.png\n"
+ + " layout\n"
+ + " other_layout.xml\n"
+ " values\n"
+ " strings.xml\n"
+ "build.gradle\n"
@@ -3854,9 +3870,9 @@
sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
+ " package=\"").append(packageName).append("\"\n"
- + " android:versionCode=\"1\"\n"
- + " android:versionName=\"1.0\" >\n"
- + "\n");
+ + " android:versionCode=\"1\"\n"
+ + " android:versionName=\"1.0\" >\n"
+ + "\n");
if (minSdkVersion != -1 || targetSdkVersion != -1) {
sb.append(" <uses-sdk\n");
if (minSdkVersion >= 1) {
@@ -3956,18 +3972,39 @@
+ "</resources>", strings, UTF_8);
}
- @SuppressWarnings("ResultOfMethodCallIgnored")
private static void createDefaultIcon(File dir) throws IOException {
- File strings = new File(dir, "res" + separator + "drawable" + separator +
- "ic_launcher.xml");
+ createIcon(dir, "ic_launcher.xml");
+ }
+
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ private static void createIcon(File dir, String name) throws IOException {
+ boolean isPng = name.toLowerCase(Locale.US).endsWith(DOT_PNG);
+ String folder = isPng ? "drawable-hdpi" : "drawable";
+ File icon = new File(dir, "res" + separator + folder + separator + name);
+ icon.getParentFile().mkdirs();
+ if (isPng) {
+ //noinspection UndesirableClassUsage
+ BufferedImage image = new BufferedImage(50, 50, BufferedImage.TYPE_INT_ARGB);
+ ImageIO.write(image, "PNG", icon);
+ } else {
+ assertTrue("Unsupported file extension for " + name, name.toLowerCase(Locale.US).endsWith(DOT_XML));
+ Files.write("" +
+ "<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n" +
+ " <solid android:color=\"#00000000\"/>\n" +
+ " <stroke android:width=\"1dp\" color=\"#ff000000\"/>\n" +
+ " <padding android:left=\"1dp\" android:top=\"1dp\"\n" +
+ " android:right=\"1dp\" android:bottom=\"1dp\" />\n" +
+ "</shape>", icon, UTF_8);
+ }
+ }
+
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ private static void createLayout(File dir, String name) throws IOException {
+ File strings = new File(dir, "res" + separator + "layout" + separator + name);
strings.getParentFile().mkdirs();
Files.write(""
- + "<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n"
- + " <solid android:color=\"#00000000\"/>\n"
- + " <stroke android:width=\"1dp\" color=\"#ff000000\"/>\n"
- + " <padding android:left=\"1dp\" android:top=\"1dp\"\n"
- + " android:right=\"1dp\" android:bottom=\"1dp\" />\n"
- + "</shape>", strings, UTF_8);
+ + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ + "<merge/>\n", strings, UTF_8);
}
private static void deleteDir(File root) {
diff --git a/android/testSrc/com/android/tools/idea/gradle/project/SdkSyncTest.java b/android/testSrc/com/android/tools/idea/gradle/project/SdkSyncTest.java
index 0bcce2a..bea24af 100644
--- a/android/testSrc/com/android/tools/idea/gradle/project/SdkSyncTest.java
+++ b/android/testSrc/com/android/tools/idea/gradle/project/SdkSyncTest.java
@@ -42,7 +42,7 @@
}
public void testSyncIdeAndProjectAndroidHomesWithIdeSdkAndNoProjectSdk() throws Exception {
- DefaultSdks.setDefaultAndroidHome(myAndroidSdkPath);
+ DefaultSdks.setDefaultAndroidHome(myAndroidSdkPath, null);
SdkSync.syncIdeAndProjectAndroidHomes(myLocalProperties);
@@ -50,7 +50,7 @@
}
public void testSyncIdeAndProjectAndroidHomesWithIdeSdkAndInvalidProjectSdk() throws Exception {
- DefaultSdks.setDefaultAndroidHome(myAndroidSdkPath);
+ DefaultSdks.setDefaultAndroidHome(myAndroidSdkPath, null);
myLocalProperties.setAndroidSdkPath(new File("randomPath"));
myLocalProperties.save();
@@ -95,6 +95,7 @@
SdkSync.syncIdeAndProjectAndroidHomes(myLocalProperties, task);
fail("Expecting ExternalSystemException");
} catch (ExternalSystemException e) {
+ // expected
}
assertNull(DefaultSdks.getDefaultAndroidHome());
diff --git a/android/testSrc/com/android/tools/idea/rendering/AarResourceClassGeneratorTest.java b/android/testSrc/com/android/tools/idea/rendering/AarResourceClassGeneratorTest.java
index b60fd5b..03dee6d 100644
--- a/android/testSrc/com/android/tools/idea/rendering/AarResourceClassGeneratorTest.java
+++ b/android/testSrc/com/android/tools/idea/rendering/AarResourceClassGeneratorTest.java
@@ -40,7 +40,7 @@
"values/styles.xml", "" +
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<resources>\n" +
- " <style name=\"MyTheme\" parent=\"android:Theme.Light\">\n" +
+ " <style name=\"MyTheme.Dark\" parent=\"android:Theme.Light\">\n" +
" <item name=\"android:textColor\">#999999</item>\n" +
" <item name=\"foo\">?android:colorForeground</item>\n" +
" </style>\n" +
@@ -155,6 +155,15 @@
Object gravityValue = field1.get(null);
Object layoutColumnSpanValue = clz.getField("layout_columnSpan").get(null);
+ // Test style class
+ name = "my.test.pkg.R$style";
+ clz = generateClass(generator, name);
+ assertNotNull(clz);
+ r = clz.newInstance();
+ assertEquals(name, clz.getName());
+ assertTrue(Modifier.isPublic(clz.getModifiers()));
+ assertTrue(Modifier.isFinal(clz.getModifiers()));
+ assertFalse(Modifier.isInterface(clz.getModifiers()));
// Test styleable class!
name = "my.test.pkg.R$styleable";
diff --git a/android/testSrc/com/android/tools/idea/sdk/DefaultSdksTest.java b/android/testSrc/com/android/tools/idea/sdk/DefaultSdksTest.java
index 10199db..a833cc2 100644
--- a/android/testSrc/com/android/tools/idea/sdk/DefaultSdksTest.java
+++ b/android/testSrc/com/android/tools/idea/sdk/DefaultSdksTest.java
@@ -91,7 +91,7 @@
localProperties.setAndroidSdkPath("");
localProperties.save();
- List<Sdk> sdks = DefaultSdks.setDefaultAndroidHome(myAndroidSdkPath);
+ List<Sdk> sdks = DefaultSdks.setDefaultAndroidHome(myAndroidSdkPath, null);
assertOneSdkPerAvailableTarget(sdks);
localProperties = new LocalProperties(myProject);
diff --git a/android/testSrc/com/android/tools/idea/sdk/SdkLoggerIntegrationTest.java b/android/testSrc/com/android/tools/idea/sdk/SdkLoggerIntegrationTest.java
new file mode 100644
index 0000000..156aea1
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/sdk/SdkLoggerIntegrationTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.sdk;
+
+import com.intellij.testFramework.fixtures.IdeaProjectTestFixture;
+import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory;
+import com.intellij.testFramework.fixtures.JavaTestFixtureFactory;
+import com.intellij.testFramework.fixtures.TestFixtureBuilder;
+import org.jetbrains.android.AndroidTestBase;
+import org.mockito.Mockito;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+public final class SdkLoggerIntegrationTest extends AndroidTestBase {
+ private static void verifyParsing(String message, String title, String description, int progress) {
+ SdkLoggerIntegration logger = process(message);
+ verify(logger).setTitle(title);
+ verify(logger).setDescription(description);
+ verify(logger).setProgress(progress);
+ verify(logger, never()).lineAdded("");
+ }
+
+ private static SdkLoggerIntegration process(String message) {
+ SdkLoggerIntegration mock = Mockito.spy(new DummySdkLoggerIntegration());
+ mock.info("%s\n", message);
+ return mock;
+ }
+
+ private static void verifyIgnored(String message) {
+ SdkLoggerIntegration mock = process(message);
+ verify(mock, never()).setTitle("");
+ verify(mock, never()).setDescription("");
+ verify(mock, never()).lineAdded("");
+ verify(mock, never()).setProgress(0);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ final TestFixtureBuilder<IdeaProjectTestFixture> projectBuilder =
+ IdeaTestFixtureFactory.getFixtureFactory().createFixtureBuilder(getName());
+ myFixture = JavaTestFixtureFactory.getFixtureFactory().createCodeInsightFixture(projectBuilder.getFixture());
+ myFixture.setUp();
+ myFixture.setTestDataPath(getTestDataPath());
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ myFixture.tearDown();
+ super.tearDown();
+ }
+
+ public void testIgnoredStrings() {
+ verifyIgnored("Refresh Sources:");
+ verifyIgnored(" Fetching http://dl-ssl.google.com/android/repository/addons_list-2.xml");
+ verifyIgnored(" Validate XML");
+ verifyIgnored(" Parse XML");
+ verifyIgnored(" Fetched Add-ons List successfully");
+ verifyIgnored("-------------------------------");
+ verifyIgnored("License id: android-sdk-license-cafebabe");
+ verifyIgnored("Used by: \n" +
+ " - Android SDK Platform-tools, revision 21\n" +
+ " - SDK Platform Android 5.0, API 21, revision 1");
+ }
+
+ public void testDownloadProgress() {
+ SdkLoggerIntegration mock = process(" Downloading SDK Platform Android 5.0, API 21, revision 1");
+ verify(mock).setTitle("Downloading SDK Platform Android 5.0, API 21, revision 1");
+ verify(mock, never()).setDescription("");
+ verify(mock, never()).setProgress(0);
+ verify(mock, never()).lineAdded("");
+
+ verifyParsing(" (14%, 4674 KiB/s, 12 seconds left)", "", "14%, 4674 KiB/s, 12 seconds left", 14);
+ }
+
+ public void testUnpackingProgress() {
+ verifyParsing(" Unzipping SDK Platform Android 5.0, API 21, revision 1 (1%)",
+ "Unzipping SDK Platform Android 5.0, API 21, revision 1",
+ "1%", 1);
+ verifyParsing(" Unzipping SDK Platform Android 5.0, API 21, revision 1 (54%)",
+ "Unzipping SDK Platform Android 5.0, API 21, revision 1", "54%", 54);
+ }
+
+ /**
+ * This concrete class is needed as Mockito cannot spy on abstract classes.
+ */
+ private static class DummySdkLoggerIntegration extends SdkLoggerIntegration {
+ @Override
+ protected void setProgress(int progress) {
+ // Needed for mock object
+ }
+
+ @Override
+ protected void setDescription(String description) {
+ // Needed for mock object
+ }
+
+ @Override
+ protected void setTitle(String title) {
+ // Needed for mock object
+ }
+
+ @Override
+ protected void lineAdded(String string) {
+ // Needed for mock object
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/testSrc/com/android/tools/idea/templates/FmHasDependencyMethodTest.java b/android/testSrc/com/android/tools/idea/templates/FmHasDependencyMethodTest.java
new file mode 100644
index 0000000..111a0ca
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/templates/FmHasDependencyMethodTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.templates;
+
+import com.google.common.collect.Maps;
+import freemarker.template.SimpleScalar;
+import freemarker.template.TemplateBooleanModel;
+import freemarker.template.TemplateModelException;
+import junit.framework.TestCase;
+
+import java.util.*;
+
+@SuppressWarnings("javadoc")
+public class FmHasDependencyMethodTest extends TestCase {
+
+ private static Map<String, Object> createMap(Object... keysAndValues) {
+ HashMap<String, Object> map = Maps.newHashMap();
+ for (int i = 0; i < keysAndValues.length; i+= 2) {
+ String key = (String)keysAndValues[i];
+ Object value = keysAndValues[i + 1];
+ map.put(key, value);
+ }
+
+ return map;
+ }
+
+ @SuppressWarnings("rawtypes")
+ private static void check(boolean expected, String s, Map<String, Object> paramMap) throws TemplateModelException {
+ FmHasDependencyMethod method = new FmHasDependencyMethod(paramMap);
+ List list = Collections.singletonList(new SimpleScalar(s));
+ assertEquals(expected, ((TemplateBooleanModel)method.exec(list)).getAsBoolean());
+ }
+
+ public void testEmpty() throws Exception {
+ check(false, "com.android.support:support-v4", createMap());
+ }
+
+ public void testProvidedDependencies() throws Exception {
+ check(false, "com.android.support:support-v4", createMap(
+ TemplateMetadata.ATTR_DEPENDENCIES_LIST,
+ Arrays.asList("com.android.support:appcompat-v7:21.0.0")));
+
+ check(true, "com.android.support:support-v4", createMap(
+ TemplateMetadata.ATTR_DEPENDENCIES_LIST,
+ Arrays.asList("com.android.support:appcompat-v7:21.0.0", "com.android.support:support-v4:v21.+")));
+ }
+
+ public void testAppCompatBasedOnMinAndCompileApis() throws Exception {
+ // Too old: appcompat requires API 8
+ check(false, "com.android.support:appcompat-v7", createMap(TemplateMetadata.ATTR_BUILD_API, 21,
+ TemplateMetadata.ATTR_MIN_API_LEVEL, 4));
+ // Too high: not needed with minSdkVersion 21
+ check(false, "com.android.support:appcompat-v7", createMap(TemplateMetadata.ATTR_BUILD_API, 21,
+ TemplateMetadata.ATTR_MIN_API_LEVEL, 21));
+
+ // Not needed when minSdkVersion >= 14 and compileSdkVersion < 21
+ check(false, "com.android.support:appcompat-v7", createMap(TemplateMetadata.ATTR_BUILD_API, 18,
+ TemplateMetadata.ATTR_MIN_API_LEVEL, 15));
+
+ // Use it with minSdkVersion 21
+ check(true, "com.android.support:appcompat-v7", createMap(TemplateMetadata.ATTR_BUILD_API, 21,
+ TemplateMetadata.ATTR_MIN_API_LEVEL, 15));
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/welcome/InstallComponentsPathTest.java b/android/testSrc/com/android/tools/idea/welcome/InstallComponentsPathTest.java
new file mode 100644
index 0000000..370d70f
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/welcome/InstallComponentsPathTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tools.idea.welcome;
+
+import com.android.SdkConstants;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.SdkManager;
+import com.android.sdklib.internal.repository.updater.SdkUpdaterNoWindow;
+import com.android.sdklib.repository.FullRevision;
+import com.android.sdklib.repository.MajorRevision;
+import com.android.sdklib.repository.descriptors.PkgDesc;
+import com.android.sdklib.repository.descriptors.PkgType;
+import com.android.sdklib.repository.local.LocalPkgInfo;
+import com.android.tools.idea.sdk.SdkLoggerIntegration;
+import com.android.utils.ILogger;
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
+import com.google.common.collect.*;
+import com.google.common.io.Files;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.testFramework.fixtures.IdeaProjectTestFixture;
+import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory;
+import com.intellij.testFramework.fixtures.JavaTestFixtureFactory;
+import com.intellij.testFramework.fixtures.TestFixtureBuilder;
+import org.jetbrains.android.AndroidTestBase;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Assume;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+
+import static com.google.common.base.Predicates.in;
+import static com.google.common.base.Predicates.not;
+
+/**
+ * Test installing Android SDK logic from the first run wizard.
+ * <p/>
+ * This test is not meant to be ran on build farm, only on the local system.
+ */
+public class InstallComponentsPathTest extends AndroidTestBase {
+ private File tempDir;
+
+ @Override
+ public void setUp() throws Exception {
+ Assume.assumeTrue("Android SDK install test is disabled", Boolean.getBoolean("test.androidsdk.install"));
+ super.setUp();
+ final TestFixtureBuilder<IdeaProjectTestFixture> projectBuilder =
+ IdeaTestFixtureFactory.getFixtureFactory().createFixtureBuilder(getName());
+ myFixture = JavaTestFixtureFactory.getFixtureFactory().createCodeInsightFixture(projectBuilder.getFixture());
+ myFixture.setUp();
+ myFixture.setTestDataPath(getTestDataPath());
+ tempDir = Files.createTempDir();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ FileUtil.delete(tempDir);
+ myFixture.tearDown();
+ super.tearDown();
+ }
+
+ public void testDownloadSeed() throws WizardException {
+ File destination = new File(tempDir, "android-sdk-seed-install");
+ boolean result = InstallComponentsPath.downloadAndUnzipSdkSeed(new InstallContext(tempDir), destination, 1);
+ assertTrue("Operation was canceled", result);
+ assertTrue("Destination should exist", destination.isDirectory());
+
+ File[] children = destination.listFiles();
+ assertTrue("Destination is empty", children != null && children.length > 0);
+ Set<String> expectedDirs = Sets.newHashSet(SdkConstants.FD_ADDONS, SdkConstants.FD_PLATFORMS, SdkConstants.FD_TOOLS);
+ for (File child : children) {
+ expectedDirs.remove(child.getName());
+ }
+ assertEquals("Missing folders: " + Joiner.on(", ").join(expectedDirs), 0, expectedDirs.size());
+ }
+
+ public void testNewInstall() throws IOException, WizardException {
+ File destination = new File(tempDir, getName());
+ InstallContext context = new InstallContext(tempDir);
+ InstallComponentsPath.downloadAndUnzipSdkSeed(context, destination, 1);
+ ILogger log = new LoggerForTest();
+ SdkManager manager = SdkManager.createManager(destination.getAbsolutePath(), log);
+ if (manager == null) {
+ throw new IOException("SDK not found");
+ }
+
+ PkgDesc.Builder[] packages = AndroidSdk.getPackages();
+ Set<String> toInstall = Sets.newHashSet();
+ for (PkgDesc.Builder sdkPackage : packages) {
+ if (sdkPackage != null) {
+ toInstall.add(sdkPackage.create().getInstallId());
+ }
+ }
+
+ InstallComponentsPath.setupSdkComponents(context, destination, Collections.singleton(new AndroidSdk()), 1);
+ manager.reloadSdk(log);
+ LocalPkgInfo[] installedPkgs = manager.getLocalSdk().getPkgsInfos(EnumSet.allOf(PkgType.class));
+
+ assertEquals(9, toInstall.size());
+ final Map<String, LocalPkgInfo> installed = Maps.newHashMap();
+ for (LocalPkgInfo info : installedPkgs) {
+ installed.put(info.getDesc().getInstallId(), info);
+ }
+
+ System.out.println("Packages list: \n\t" + Joiner.on("\n\t").join(toInstall));
+
+ // TODO: This package is not available for install
+ toInstall.remove("sample-21");
+ // TODO: Why is this always installed?
+ installed.remove("extra-intel-hardware_accelerated_execution_manager");
+
+ Set<String> all = ImmutableSet.copyOf(Iterables.concat(toInstall, installed.keySet()));
+
+ Set<String> notInstalled = ImmutableSet.copyOf(Iterables.filter(all, not(in(installed.keySet()))));
+ Set<String> shouldntBeenInstalled = ImmutableSet.copyOf(Iterables.filter(all, not(in(toInstall))));
+
+ assertEquals("Not installed: " + Joiner.on(", ").join(notInstalled), 0, notInstalled.size());
+ assertEquals("Were installed: " + Joiner.on(", ").join(Iterables.transform(shouldntBeenInstalled, new Function<String, String>() {
+ @Override
+ public String apply(String input) {
+ return String.format("%s (%s)", input, installed.get(input).getShortDescription());
+ }
+ })), 0, shouldntBeenInstalled.size());
+ }
+
+ private static class LoggerForTest extends SdkLoggerIntegration {
+ private String myTitle = null;
+
+ @Override
+ protected void setProgress(int progress) {
+ // No need.
+ }
+
+ @Override
+ protected void setDescription(String description) {
+ // No spamming
+ }
+
+ @Override
+ protected void setTitle(String title) {
+ if (!StringUtil.isEmptyOrSpaces(title) && !Objects.equal(title, myTitle)) {
+ System.out.println(title);
+ myTitle = title;
+ }
+ }
+
+ @Override
+ protected void lineAdded(String string) {
+ System.out.println(string);
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/testSrc/org/jetbrains/android/sdk/AndroidSdkDataTest.java b/android/testSrc/org/jetbrains/android/sdk/AndroidSdkDataTest.java
index 40cb496..65d30c6 100644
--- a/android/testSrc/org/jetbrains/android/sdk/AndroidSdkDataTest.java
+++ b/android/testSrc/org/jetbrains/android/sdk/AndroidSdkDataTest.java
@@ -56,7 +56,7 @@
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
- DefaultSdks.setDefaultAndroidHome(sdkDir);
+ DefaultSdks.setDefaultAndroidHome(sdkDir, null);
ProjectRootManager.getInstance(getProject()).setProjectSdk(DefaultSdks.getEligibleAndroidSdks().get(0));
}
});