Merge Studio 2.2.2
diff --git a/adt-branding/src/idea/AndroidStudioApplicationInfo.xml b/adt-branding/src/idea/AndroidStudioApplicationInfo.xml
index 6da2e22..2a390c6 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 major="2" minor="2" micro="0" patch="12" full="{0}.{1}" eap="false" />
+ <version major="2" minor="2" micro="2" patch="0" full="{0}.{1}.{2}" eap="false" />
<company name="Google" url="http://developer.android.com"/>
<build number="__BUILD_NUMBER__" date="__BUILD_DATE__" apiVersion="AI-145.1617.8"/>
<install-over minbuild="0.1" maxbuild="999.999999" version="0"/>
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 f6c5b76..377ca37 100755
--- a/android/guiTestSrc/com/android/tools/idea/tests/gui/layout/NewProjectTest.java
+++ b/android/guiTestSrc/com/android/tools/idea/tests/gui/layout/NewProjectTest.java
@@ -150,7 +150,7 @@
// This warning is wrong: http://b.android.com/192605
" Android > Lint > Usability",
- " Missing support for Google App Indexing",
+ " Missing support for Firebase App Indexing",
" app",
" App is not indexable by Google Search; consider adding at least one Activity with an ACTION-VIEW intent filter. See issue explanation for more details."));
}
diff --git a/android/resources/messages/AndroidBundle.properties b/android/resources/messages/AndroidBundle.properties
index c5059ef..8d6020d 100644
--- a/android/resources/messages/AndroidBundle.properties
+++ b/android/resources/messages/AndroidBundle.properties
@@ -453,9 +453,9 @@
android.lint.inspections.full.backup.content=Valid Full Backup Content File
android.lint.inspections.get.instance=Cipher.getInstance with ECB
android.lint.inspections.gif.usage=Using .gif format for bitmaps is discouraged
-android.lint.inspections.google.app.indexing.api.warning=Missing support for Google App Indexing Api
-android.lint.inspections.google.app.indexing.url.error=URL not supported by app for Google App Indexing
-android.lint.inspections.google.app.indexing.warning=Missing support for Google App Indexing
+android.lint.inspections.google.app.indexing.api.warning=Missing support for Firebase App Indexing Api
+android.lint.inspections.google.app.indexing.url.error=URL not supported by app for Firebase App Indexing
+android.lint.inspections.google.app.indexing.warning=Missing support for Firebase App Indexing
android.lint.inspections.gradle.compatible=Incompatible Gradle Versions
android.lint.inspections.gradle.dependency=Obsolete Gradle Dependency
android.lint.inspections.gradle.deprecated=Deprecated Gradle Construct
@@ -787,7 +787,7 @@
instant.run.notification.ir.disabled.for.current.variant=Instant Run is disabled for current variant.<br>\
Some settings (e.g. using jack) are currently not compatible with Instant Run. <a href="learnmore">Learn more</a>.
instant.run.notification.ir.disabled.multidex.requires.21=Instant Run does not support deploying build variants with multidex enabled, to a target with API level 20 or below.<br>\
- To use Instant Run with a multidex enabled build variant, deploy to a target with API level 21 or higher.");
+ To use Instant Run with a multidex enabled build variant, deploy to a target with API level 21 or higher.
instant.run.notification.ir.disabled.multiple.devices=Instant Run is disabled:<br>\
Instant Run does not support deploying to multiple targets.<br>\
To enable Instant Run, deploy to a single target.
@@ -835,4 +835,30 @@
You may need to <a href="restart">restart</a>{0} the current activity to see the changes.\n\
You can also <a href="configure">configure</a> Instant Run to restart activities automatically.
-instant.run.notification.nochanges=No changes to deploy.
\ No newline at end of file
+instant.run.notification.nochanges=No changes to deploy.
+
+instant.run.flr.banner.subtitle=<html>\
+ We want to make Instant Run perfect, but we need more info about your project to investigate issues.<br>\
+ Please help us troubleshoot and fix Instant Run issues by doing the following:<br>\
+ <ol>\
+ <li>Re-enable Instant Run and activate extra logging</li>\
+ <li>Reproduce the Instant Run issue</li>\
+ <li>Immediately after reproducing the issue, click <b> Help | Report Instant Run Issue... </b> to send us the issue report.</li>\
+ </ol>\
+ </html>
+
+instant.run.flr.dialog.description=Please describe your Instant Run Issue:
+instant.run.flr.dialog.includeslogs=<html>To help Google troubleshoot and fix Instant Run issues, the following log files need to be sent<br>\
+ along with this issue report. These logs contain project details that may include personally<br>\
+ identifiable information (e.g. project paths on disk).<br><br>\
+ These logs will only be viewable by Google, and will be used for the sole purpose of troubleshooting<br>\
+ Instant Run issues.</html>
+instant.run.flr.would.you.like.to.enable=In order to report an Instant Run issue to Google, you need to first enable Instant Run with extra logging.\n\
+ Would you like to help Google troubleshoot and fix Instant Run issues?
+instant.run.flr.dialog.title=Report Instant Run Issue
+instant.run.flr.howto=<html>Thank you! We have now enabled Instant Run and extra logging to diagnose issues.<br>\
+ Next time you hit an Instant Run issue, you can click on <b> Help | Report Instant Run Issue... </b><br>\
+ to send us an error report.<br><br>\
+ If you change your mind, you can disable extra logging for Instant Run by going to <br>\
+ <b>Settings | Build, Execution, Deployment | Instant Run</b><br>\
+ and uncheck 'Log extra info for Instant Run'</html>
diff --git a/android/src/META-INF/androidstudio.xml b/android/src/META-INF/androidstudio.xml
index 7bde7ca..3da850e 100755
--- a/android/src/META-INF/androidstudio.xml
+++ b/android/src/META-INF/androidstudio.xml
@@ -39,6 +39,10 @@
<add-to-group group-id="ToolbarRunGroup" anchor="before" relative-to-action="Stop" />
</action>
+ <action id="Android.InstantRunFeedback" class="com.android.tools.idea.fd.actions.SubmitFeedback">
+ <add-to-group group-id="HelpMenu" anchor="after" relative-to-action="SendFeedback" />
+ </action>
+
<action id="AndroidAddRTLSupport" class="com.android.tools.idea.actions.AndroidAddRtlSupportAction"
text="Add RTL Support Where Possible..." description="Add right-to-left (RTL) support where possible">
<add-to-group group-id="RefactoringMenu"/>
diff --git a/android/src/META-INF/plugin.xml b/android/src/META-INF/plugin.xml
index cdbbe9f..1922533 100755
--- a/android/src/META-INF/plugin.xml
+++ b/android/src/META-INF/plugin.xml
@@ -4,7 +4,7 @@
<description>
Supports the development of Open Handset Alliance Android applications with IntelliJ IDEA.
</description>
- <version>10.2.2</version>
+ <version>10.2.2.2</version>
<vendor>JetBrains</vendor>
<depends>JUnit</depends>
@@ -568,9 +568,9 @@
<globalInspection hasStaticDescription="true" shortName="AndroidLintFullBackupContent" displayName="Valid Full Backup Content File" groupKey="android.lint.inspections.group.name.correctness" bundle="messages.AndroidBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintFullBackupContentInspection"/>
<globalInspection hasStaticDescription="true" shortName="AndroidLintGetInstance" displayName="Cipher.getInstance with ECB" groupKey="android.lint.inspections.group.name.security" bundle="messages.AndroidBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintGetInstanceInspection"/>
<globalInspection hasStaticDescription="true" shortName="AndroidLintGifUsage" displayName="Using .gif format for bitmaps is discouraged" groupKey="android.lint.inspections.group.name.usability.icons" bundle="messages.AndroidBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintGifUsageInspection"/>
- <globalInspection hasStaticDescription="true" shortName="AndroidLintGoogleAppIndexingApiWarning" displayName="Missing support for Google App Indexing Api" groupKey="android.lint.inspections.group.name.usability" bundle="messages.AndroidBundle" enabledByDefault="false" level="WARNING" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintGoogleAppIndexingApiWarningInspection"/>
- <globalInspection hasStaticDescription="true" shortName="AndroidLintGoogleAppIndexingUrlError" displayName="URL not supported by app for Google App Indexing" groupKey="android.lint.inspections.group.name.usability" bundle="messages.AndroidBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintGoogleAppIndexingUrlErrorInspection"/>
- <globalInspection hasStaticDescription="true" shortName="AndroidLintGoogleAppIndexingWarning" displayName="Missing support for Google App Indexing" groupKey="android.lint.inspections.group.name.usability" bundle="messages.AndroidBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintGoogleAppIndexingWarningInspection"/>
+ <globalInspection hasStaticDescription="true" shortName="AndroidLintGoogleAppIndexingApiWarning" displayName="Missing support for Firebase App Indexing Api" groupKey="android.lint.inspections.group.name.usability" bundle="messages.AndroidBundle" enabledByDefault="false" level="WARNING" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintGoogleAppIndexingApiWarningInspection"/>
+ <globalInspection hasStaticDescription="true" shortName="AndroidLintGoogleAppIndexingUrlError" displayName="URL not supported by app for Firebase App Indexing" groupKey="android.lint.inspections.group.name.usability" bundle="messages.AndroidBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintGoogleAppIndexingUrlErrorInspection"/>
+ <globalInspection hasStaticDescription="true" shortName="AndroidLintGoogleAppIndexingWarning" displayName="Missing support for Firebase App Indexing" groupKey="android.lint.inspections.group.name.usability" bundle="messages.AndroidBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintGoogleAppIndexingWarningInspection"/>
<globalInspection hasStaticDescription="true" shortName="AndroidLintGradleCompatible" displayName="Incompatible Gradle Versions" groupKey="android.lint.inspections.group.name.correctness" bundle="messages.AndroidBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintGradleCompatibleInspection"/>
<globalInspection hasStaticDescription="true" shortName="AndroidLintGradleDependency" displayName="Obsolete Gradle Dependency" groupKey="android.lint.inspections.group.name.correctness" bundle="messages.AndroidBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintGradleDependencyInspection"/>
<globalInspection hasStaticDescription="true" shortName="AndroidLintGradleDeprecated" displayName="Deprecated Gradle Construct" groupKey="android.lint.inspections.group.name.correctness" bundle="messages.AndroidBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLintGradleDeprecatedInspection"/>
@@ -832,6 +832,7 @@
<projectService serviceImplementation="com.android.tools.idea.gradle.invoker.messages.GradleBuildTreeViewConfiguration"/>
<projectService serviceImplementation="com.android.tools.idea.run.DevicePickerStateService" />
<projectService serviceImplementation="com.android.tools.idea.fd.InstantRunStatsService" />
+ <projectService serviceImplementation="com.android.tools.idea.fd.FlightRecorder" />
<projectService serviceInterface="com.android.tools.idea.gradle.compiler.AndroidGradleBuildConfiguration"
serviceImplementation="com.android.tools.idea.gradle.compiler.AndroidGradleBuildConfiguration"/>
@@ -1109,6 +1110,7 @@
<java.elementFinder implementation="com.android.tools.idea.databinding.BrClassFinder" id="dataBinding.BrClassFinder" order="first, before java"/>
<java.elementFinder implementation="com.android.tools.idea.databinding.DataBindingClassFinder" id="dataBinding.BindingClassFinder" order="first, before java"/>
<java.elementFinder implementation="com.android.tools.idea.databinding.DataBindingComponentClassFinder" id="dataBinding.ComponentClassFinder" order="first, before java"/>
+ <java.elementFinder implementation="com.android.tools.idea.databinding.DataBindingPackageFinder" id="dataBinding.DataBindingPackageFinder" order="last, after java"/>
<psi.referenceContributor implementation="com.android.tools.idea.lang.databinding.DataBindingXmlReferenceContributor"/>
<completion.contributor implementationClass="com.android.tools.idea.lang.databinding.DataBindingCompletionContributor" language="AndroidDataBinding"/>
</extensions>
diff --git a/android/src/com/android/tools/idea/apk/viewer/ApkFileSystem.java b/android/src/com/android/tools/idea/apk/viewer/ApkFileSystem.java
index a68b0d0..55acc5b 100644
--- a/android/src/com/android/tools/idea/apk/viewer/ApkFileSystem.java
+++ b/android/src/com/android/tools/idea/apk/viewer/ApkFileSystem.java
@@ -28,7 +28,7 @@
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.vfs.impl.ArchiveHandler;
-import com.intellij.openapi.vfs.impl.ZipHandler;
+import com.intellij.openapi.vfs.impl.jar.JarHandler;
import com.intellij.openapi.vfs.newvfs.ArchiveFileSystem;
import com.intellij.openapi.vfs.newvfs.VfsImplUtil;
import com.intellij.util.io.URLUtil;
@@ -76,7 +76,7 @@
@NotNull
@Override
protected ArchiveHandler getHandler(@NotNull VirtualFile entryFile) {
- return VfsImplUtil.getHandler(this, entryFile, ZipHandler::new);
+ return VfsImplUtil.getHandler(this, entryFile, JarHandler::new);
}
/**
diff --git a/android/src/com/android/tools/idea/databinding/BrClassFinder.java b/android/src/com/android/tools/idea/databinding/BrClassFinder.java
index 8bd76fc..fa14cf0 100644
--- a/android/src/com/android/tools/idea/databinding/BrClassFinder.java
+++ b/android/src/com/android/tools/idea/databinding/BrClassFinder.java
@@ -89,15 +89,7 @@
@Nullable
@Override
public PsiPackage findPackage(@NotNull String qualifiedName) {
- if (!myComponent.hasAnyDataBindingEnabledFacet()) {
- return null;
- }
- for (AndroidFacet facet : myComponent.getDataBindingEnabledFacets()) {
- String generatedPackageName = DataBindingUtil.getGeneratedPackageName(facet);
- if (generatedPackageName.equals(qualifiedName)) {
- return myComponent.getOrCreateDataBindingPsiPackage(generatedPackageName);
- }
- }
+ // DO NOT find package. BR package is the same as R and it always exists
return null;
}
}
diff --git a/android/src/com/android/tools/idea/databinding/DataBindingClassFinder.java b/android/src/com/android/tools/idea/databinding/DataBindingClassFinder.java
index a55ac4a..866f19a 100644
--- a/android/src/com/android/tools/idea/databinding/DataBindingClassFinder.java
+++ b/android/src/com/android/tools/idea/databinding/DataBindingClassFinder.java
@@ -17,75 +17,23 @@
import com.android.tools.idea.res.DataBindingInfo;
import com.android.tools.idea.res.LocalResourceRepository;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElementFinder;
import com.intellij.psi.PsiPackage;
import com.intellij.psi.search.GlobalSearchScope;
-import com.intellij.psi.util.CachedValue;
-import com.intellij.psi.util.CachedValuesManager;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import java.util.Collections;
-import java.util.List;
import java.util.Map;
-import java.util.Set;
/**
* PsiElementFinder extensions that finds classes generated for layout files.
*/
public class DataBindingClassFinder extends PsiElementFinder {
- private final CachedValue<Map<String, PsiPackage>> myPackageCache;
private final DataBindingProjectComponent myComponent;
public DataBindingClassFinder(DataBindingProjectComponent component) {
myComponent = component;
- myPackageCache = CachedValuesManager.getManager(myComponent.getProject()).createCachedValue(
- new ProjectResourceCachedValueProvider<Map<String, PsiPackage>, Set<String>>(myComponent) {
-
- @NotNull
- @Override
- protected Map<String, PsiPackage> merge(List<Set<String>> results) {
- Map<String, PsiPackage> merged = Maps.newHashMap();
- for (Set<String> result : results) {
- for (String qualifiedPackage : result) {
- if (!merged.containsKey(qualifiedPackage)) {
- merged.put(qualifiedPackage, myComponent.getOrCreateDataBindingPsiPackage(qualifiedPackage));
- }
- }
- }
- return merged;
- }
-
- @Override
- ResourceCacheValueProvider<Set<String>> createCacheProvider(AndroidFacet facet) {
- return new ResourceCacheValueProvider<Set<String>>(facet) {
- @Override
- Set<String> doCompute() {
- LocalResourceRepository moduleResources = getFacet().getModuleResources(true);
- if (moduleResources == null) {
- return Collections.emptySet();
- }
- Map<String, DataBindingInfo> dataBindingResourceFiles = moduleResources.getDataBindingResourceFiles();
- if (dataBindingResourceFiles == null) {
- return Collections.emptySet();
- }
- Set<String> result = Sets.newHashSet();
- for (DataBindingInfo info : dataBindingResourceFiles.values()) {
- result.add(info.getPackageName());
- }
- return result;
- }
-
- @Override
- Set<String> defaultValue() {
- return Collections.emptySet();
- }
- };
- }
- }, false);
}
@Nullable
@@ -128,9 +76,8 @@
@Nullable
@Override
public PsiPackage findPackage(@NotNull String qualifiedName) {
- if (!myComponent.hasAnyDataBindingEnabledFacet()) {
- return null;
- }
- return myPackageCache.getValue().get(qualifiedName);
+ // data binding packages are found only if corresponding java packages does not exists. For those, we have DataBindingPackageFinder
+ // which has a low priority.
+ return null;
}
}
diff --git a/android/src/com/android/tools/idea/databinding/DataBindingPackageFinder.java b/android/src/com/android/tools/idea/databinding/DataBindingPackageFinder.java
new file mode 100644
index 0000000..1aba538
--- /dev/null
+++ b/android/src/com/android/tools/idea/databinding/DataBindingPackageFinder.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.databinding;
+
+import com.android.tools.idea.res.DataBindingInfo;
+import com.android.tools.idea.res.LocalResourceRepository;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElementFinder;
+import com.intellij.psi.PsiPackage;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.util.CachedValue;
+import com.intellij.psi.util.CachedValuesManager;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This element finder has minimum priority and only finds packages that are missing in the app.
+ * See {@link DataBindingClassFinder}, {@link DataBindingComponentClassFinder} and {@link BrClassFinder} for actual classes.
+ */
+public class DataBindingPackageFinder extends PsiElementFinder {
+ private final DataBindingProjectComponent myComponent;
+ private final CachedValue<Map<String, PsiPackage>> myPackageCache;
+
+
+ public DataBindingPackageFinder(final DataBindingProjectComponent component) {
+ myComponent = component;
+ myPackageCache = CachedValuesManager.getManager(myComponent.getProject()).createCachedValue(
+ new ProjectResourceCachedValueProvider<Map<String, PsiPackage>, Set<String>>(myComponent) {
+
+ @NotNull
+ @Override
+ protected Map<String, PsiPackage> merge(List<Set<String>> results) {
+ Map<String, PsiPackage> merged = Maps.newHashMap();
+ for (Set<String> result : results) {
+ for (String qualifiedPackage : result) {
+ if (!merged.containsKey(qualifiedPackage)) {
+ merged.put(qualifiedPackage, myComponent.getOrCreateDataBindingPsiPackage(qualifiedPackage));
+ }
+ }
+ }
+ return merged;
+ }
+
+ @Override
+ ResourceCacheValueProvider<Set<String>> createCacheProvider(AndroidFacet facet) {
+ return new ResourceCacheValueProvider<Set<String>>(facet) {
+ @Override
+ Set<String> doCompute() {
+ LocalResourceRepository moduleResources = getFacet().getModuleResources(true);
+ if (moduleResources == null) {
+ return Collections.emptySet();
+ }
+ Map<String, DataBindingInfo> dataBindingResourceFiles = moduleResources.getDataBindingResourceFiles();
+ if (dataBindingResourceFiles == null) {
+ return Collections.emptySet();
+ }
+ Set<String> result = Sets.newHashSet();
+ for (DataBindingInfo info : dataBindingResourceFiles.values()) {
+ result.add(info.getPackageName());
+ }
+ return result;
+ }
+
+ @Override
+ Set<String> defaultValue() {
+ return Collections.emptySet();
+ }
+ };
+ }
+ }, false);
+ }
+
+ @Nullable
+ @Override
+ public PsiClass findClass(@NotNull String qualifiedName, @NotNull GlobalSearchScope scope) {
+ return null;
+ }
+
+ @NotNull
+ @Override
+ public PsiClass[] findClasses(@NotNull String qualifiedName, @NotNull GlobalSearchScope scope) {
+ return PsiClass.EMPTY_ARRAY;
+ }
+
+ @Nullable
+ @Override
+ public PsiPackage findPackage(@NotNull String qualifiedName) {
+ if (!myComponent.hasAnyDataBindingEnabledFacet()) {
+ return null;
+ }
+ return myPackageCache.getValue().get(qualifiedName);
+ }
+}
diff --git a/android/src/com/android/tools/idea/databinding/DataBindingUtil.java b/android/src/com/android/tools/idea/databinding/DataBindingUtil.java
index 83f6b85..42cef5b 100644
--- a/android/src/com/android/tools/idea/databinding/DataBindingUtil.java
+++ b/android/src/com/android/tools/idea/databinding/DataBindingUtil.java
@@ -396,9 +396,6 @@
@Override
PsiMethod[] doCompute() {
List<PsiDataBindingResourceItem> variables = myInfo.getItems(DataBindingResourceType.VARIABLE);
- if (variables.isEmpty()) {
- return PsiMethod.EMPTY_ARRAY;
- }
List<PsiMethod> methods = Lists.newArrayListWithCapacity(variables.size() * 2 + STATIC_METHOD_COUNT);
PsiElementFactory factory = PsiElementFactory.SERVICE.getInstance(myInfo.getProject());
for (PsiDataBindingResourceItem variable : variables) {
diff --git a/android/src/com/android/tools/idea/fd/FlightRecorder.java b/android/src/com/android/tools/idea/fd/FlightRecorder.java
new file mode 100644
index 0000000..b370bc5
--- /dev/null
+++ b/android/src/com/android/tools/idea/fd/FlightRecorder.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.fd;
+
+import com.android.ddmlib.IDevice;
+import com.android.tools.fd.client.InstantRunBuildInfo;
+import com.android.tools.idea.ddms.DevicePropertyUtil;
+import com.android.tools.idea.logcat.AndroidLogcatService;
+import com.android.tools.idea.run.AndroidDevice;
+import com.android.utils.FileUtils;
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.PathManager;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.file.*;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class FlightRecorder {
+ private static final String FD_FLR_LOGS = "flr";
+ private static final String FN_GRADLE_LOG = "build.log";
+ private static final String FN_GRADLE_PROFILE = "profile.log";
+ private static final String FN_BUILD_INFO = "build-info.xml";
+
+ // Number of previous build logs to keep around.
+ // Ideally, we'd trim based on a more sophisticated logic that knows about APK installs, but for now
+ // we take the simple approach of keeping around logs from the last N builds.
+ private static final int MAX_LOG_ENTRY_COUNT = 10;
+
+ // A path specific to this project within which all flight recorder logs are saved
+ private final Path myBasePath;
+
+ // Log buffer for runtime logs
+ private final LogcatRecorder myLogcatRecorder;
+
+ // Time (in default timezone to match idea.log) at which the last build output was saved.
+ private LocalDateTime myTimestamp;
+
+ public static FlightRecorder get(@NotNull Project project) {
+ return ServiceManager.getService(project, FlightRecorder.class);
+ }
+
+ private FlightRecorder(@NotNull Project project) {
+ Path logs = Paths.get(PathManager.getLogPath());
+ Path flr = Paths.get(FD_FLR_LOGS, project.getLocationHash());
+ myBasePath = logs.resolve(flr);
+ myLogcatRecorder = new LogcatRecorder(AndroidLogcatService.getInstance());
+ }
+
+ public void saveBuildOutput(@NotNull String gradleOutput, @NotNull InstantRunBuildProgressListener instantRunProgressListener) {
+ myTimestamp = now();
+ ApplicationManager.getApplication().executeOnPooledThread(
+ new BuildOutputRecorderTask(myBasePath, myTimestamp, gradleOutput, instantRunProgressListener));
+ }
+
+ public void saveBuildInfo(@NotNull InstantRunBuildInfo instantRunBuildInfo) {
+ if (myTimestamp == null) {
+ // this would indicate an error since we didn't get the build output before,
+ // but the inconsistency doesn't matter for logging purposes
+ myTimestamp = now();
+ }
+
+ ApplicationManager.getApplication().executeOnPooledThread(
+ new BuildInfoRecorderTask(myBasePath, myTimestamp, instantRunBuildInfo));
+ }
+
+ public void setLaunchTarget(@NotNull AndroidDevice device) {
+ try {
+ Path deviceLog = myBasePath.resolve(timeStampToFolder(myTimestamp)).resolve(getDeviceLogFileName(device));
+ Files.write(deviceLog, new byte[0]);
+ }
+ catch (IOException e) {
+ Logger.getInstance(FlightRecorder.class).info("Unable to record deployment device info", e);
+ }
+
+ // start monitoring logcat if device is online
+ if (device.getLaunchedDevice().isDone()) {
+ try {
+ IDevice d = device.getLaunchedDevice().get();
+ myLogcatRecorder.startMonitoring(d, myTimestamp);
+ }
+ catch (InterruptedException | ExecutionException e) {
+ Logger.getInstance(FlightRecorder.class).info("Unable to start recording logcat", e);
+ }
+ }
+ }
+
+ // We need very little detail about the device itself, so we can encode it directly in the file name
+ private static String getDeviceLogFileName(@NotNull AndroidDevice device) {
+ // for avds, everything we need is already included in idea.log
+ if (device.isVirtual()) {
+ return "TARGET-AVD";
+ }
+
+ ListenableFuture<IDevice> launchedDevice = device.getLaunchedDevice();
+ if (!launchedDevice.isDone()) {
+ return "OFFLINE";
+ }
+
+ IDevice d;
+ try {
+ d = launchedDevice.get();
+ }
+ catch (InterruptedException | ExecutionException e) {
+ return "OFFLINE";
+ }
+
+ return DevicePropertyUtil.getManufacturer(d, "unknown").replace(' ', '.') +
+ '-' +
+ DevicePropertyUtil.getModel(d, "unknown").replace(' ', '.');
+ }
+
+ @NotNull
+ private static LocalDateTime now() {
+ // Use default timezone since the idea.log uses that and we want to correlate between the two
+ return LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+ }
+
+ @NotNull
+ static String timeStampToFolder(@NotNull LocalDateTime dateTime) {
+ return dateTime
+ .truncatedTo(ChronoUnit.SECONDS)
+ .toString()
+ .replace(':', '.');
+ }
+
+ @NotNull
+ static LocalDateTime folderToTimeStamp(@NotNull Path path) {
+ String fileName = path.getFileName().toString();
+ return LocalDateTime.parse(fileName.replace('.', ':'));
+ }
+
+ static void trimOldLogs(@NotNull Path root, int count) {
+ // a filter that only return folders with name matching a timestamp
+ DirectoryStream.Filter<Path> filter = entry -> {
+ if (!Files.isDirectory(entry)) {
+ return false;
+ }
+
+ try {
+ folderToTimeStamp(entry);
+ return true;
+ } catch (DateTimeParseException e) {
+ return false;
+ }
+ };
+
+ // collect all folders matching the above filter
+ List<Path> allLogs;
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(root, filter)) {
+ allLogs = Lists.newArrayList(stream.iterator());
+ } catch (IOException e) {
+ Logger.getInstance(FlightRecorder.class).warn("Unable to cleanup flight recorder logs", e);
+ return;
+ }
+
+ // sort the folders by timestamp and only keep the most recent count entries
+ allLogs.stream()
+ .sorted((p1, p2) -> folderToTimeStamp(p2).compareTo(folderToTimeStamp(p1))) // note: reverse sort
+ .skip(count)
+ .forEach(path -> {
+ try {
+ FileUtils.deleteDirectoryContents(path.toFile());
+ }
+ catch (IOException ignored) {
+ }
+
+ try {
+ Files.delete(path);
+ }
+ catch (IOException ignored) {
+ }
+ });
+ }
+
+ @NotNull
+ public List<Path> getAllLogs() {
+ List<Path> logs = new ArrayList<>();
+
+ Path logsHome = Paths.get(PathManager.getLogPath());
+ if (logsHome == null) {
+ return logs;
+ }
+
+ logs.addAll(getIdeaLogs(logsHome));
+ logs.addAll(getFlightRecorderLogs());
+
+ Path logcatPath = logsHome.resolve("logcat.txt");
+ try {
+ Files.write(logcatPath, myLogcatRecorder.getLogs(), Charsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ logs.add(logcatPath);
+ }
+ catch (IOException e) {
+ Logger.getInstance(FlightRecorder.class).info("Unexpected error saving logcat", e);
+ }
+
+ return logs;
+ }
+
+ @NotNull
+ private static List<Path> getIdeaLogs(@NotNull Path logsHome) {
+ try (Stream<Path> stream = Files.list(logsHome)) {
+ return stream
+ .filter(p -> Files.isReadable(p) && p.getFileName().toString().startsWith("idea.log"))
+ .sorted((p1, p2) -> Long.compare(p2.toFile().lastModified(), p1.toFile().lastModified()))
+ .limit(3)
+ .collect(Collectors.toList());
+ }
+ catch (IOException e) {
+ return ImmutableList.of();
+ }
+ }
+
+ @NotNull
+ private List<Path> getFlightRecorderLogs() {
+ try {
+ List<Path> list = new ArrayList<>(100);
+ Files.walkFileTree(myBasePath, new SimpleFileVisitor<Path>() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ Path fileName = file.getFileName();
+ if (fileName != null && fileName.toString().startsWith(".")) { // skip past .DS_Store etc
+ return FileVisitResult.CONTINUE;
+ }
+
+ list.add(file);
+ // arbitrary limit to avoid uploading tons of unnecessary files
+ return list.size() > 100 ? FileVisitResult.TERMINATE : FileVisitResult.CONTINUE;
+ }
+ });
+ return list;
+ }
+ catch (IOException e) {
+ return ImmutableList.of();
+ }
+ }
+
+ @NotNull
+ public String getPresentablePath(@NotNull Path file) {
+ if (file.startsWith(myBasePath)) {
+ return myBasePath.relativize(file).toString();
+ }
+
+ return file.getFileName().toString();
+ }
+
+ private static class BuildOutputRecorderTask implements Runnable {
+ private final Path myLogsRoot;
+ private final Path myCurrentLogPath;
+ private final String myGradleOutput;
+ private final InstantRunBuildProgressListener myBuildProgressListener;
+
+ private BuildOutputRecorderTask(@NotNull Path logsRoot,
+ @NotNull LocalDateTime dateTime,
+ @NotNull String gradleOuput,
+ @NotNull InstantRunBuildProgressListener buildProgressListener) {
+ myLogsRoot = logsRoot;
+ myCurrentLogPath = logsRoot.resolve(timeStampToFolder(dateTime));
+ myGradleOutput = gradleOuput;
+ myBuildProgressListener = buildProgressListener;
+ }
+
+ @Override
+ public void run() {
+ saveCurrentLog();
+ trimOldLogs(myLogsRoot, MAX_LOG_ENTRY_COUNT);
+ }
+
+ private void saveCurrentLog() {
+ try {
+ Files.createDirectories(myCurrentLogPath);
+ Files.write(myCurrentLogPath.resolve(FN_GRADLE_LOG), myGradleOutput.getBytes(Charsets.UTF_8));
+
+ try (BufferedWriter bw = Files.newBufferedWriter(myCurrentLogPath.resolve(FN_GRADLE_PROFILE), Charsets.UTF_8)) {
+ myBuildProgressListener.serializeTo(bw);
+ }
+
+ } catch (IOException e) {
+ Logger.getInstance(FlightRecorder.class).info("Unable to save gradle output logs", e);
+ }
+ }
+ }
+
+ private static class BuildInfoRecorderTask implements Runnable {
+ private final Path myBasePath;
+ private final InstantRunBuildInfo myBuildInfo;
+
+ public BuildInfoRecorderTask(Path logsRoot,
+ LocalDateTime dateTime,
+ InstantRunBuildInfo instantRunBuildInfo) {
+ myBasePath = logsRoot.resolve(timeStampToFolder(dateTime));
+ myBuildInfo = instantRunBuildInfo;
+ }
+
+ @Override
+ public void run() {
+ try {
+ Files.createDirectories(myBasePath);
+
+ try (BufferedWriter bw = Files.newBufferedWriter(myBasePath.resolve(FN_BUILD_INFO), Charsets.UTF_8)) {
+ myBuildInfo.serializeTo(bw);
+ }
+ } catch (IOException e) {
+ Logger.getInstance(FlightRecorder.class).info("Unable to build info", e);
+ }
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/fd/InstantRunBuildProgressListener.java b/android/src/com/android/tools/idea/fd/InstantRunBuildProgressListener.java
new file mode 100644
index 0000000..630ba83
--- /dev/null
+++ b/android/src/com/android/tools/idea/fd/InstantRunBuildProgressListener.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.fd;
+
+import org.gradle.tooling.events.ProgressEvent;
+import org.gradle.tooling.events.ProgressListener;
+import org.gradle.tooling.events.task.TaskOperationResult;
+import org.gradle.tooling.events.task.internal.DefaultTaskFinishEvent;
+import org.gradle.tooling.events.task.internal.DefaultTaskSkippedResult;
+import org.gradle.tooling.events.task.internal.DefaultTaskSuccessResult;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+public class InstantRunBuildProgressListener implements ProgressListener {
+ private final List<ProgressEvent> myEvents = new ArrayList<>(512);
+
+ @Override
+ public void statusChanged(ProgressEvent event) {
+ myEvents.add(event);
+ }
+
+ public void serializeTo(@NotNull Writer writer) throws IOException {
+ if (myEvents.isEmpty()) {
+ writer.append("No events");
+ return;
+ }
+
+ long startTime = myEvents.get(0).getEventTime();
+
+ for (ProgressEvent event : myEvents) {
+ writer.append(String.format(Locale.US, "%10d", (event.getEventTime() - startTime)));
+ writer.append(' ');
+
+ writer.append(event.toString());
+ writer.append('\n');
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/fd/InstantRunBuilder.java b/android/src/com/android/tools/idea/fd/InstantRunBuilder.java
index 1273bf7..93436e3 100644
--- a/android/src/com/android/tools/idea/fd/InstantRunBuilder.java
+++ b/android/src/com/android/tools/idea/fd/InstantRunBuilder.java
@@ -31,6 +31,7 @@
import com.android.tools.idea.run.InstalledApkCache;
import com.android.tools.idea.run.InstalledPatchCache;
import com.android.tools.idea.run.util.MultiUserUtils;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.hash.HashCode;
import com.intellij.openapi.application.ApplicationManager;
@@ -43,9 +44,7 @@
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
+import java.util.*;
import java.util.concurrent.TimeUnit;
import static com.android.builder.model.AndroidProject.PROPERTY_OPTIONAL_COMPILATION_STEPS;
@@ -60,6 +59,7 @@
private final RunAsValidator myRunAsValidator;
private final InstalledApkCache myInstalledApkCache;
private final InstantRunClientDelegate myInstantRunClientDelegate;
+ private final boolean myFlightRecorderEnabled;
public InstantRunBuilder(@Nullable IDevice device,
@NotNull InstantRunContext instantRunContext,
@@ -70,6 +70,7 @@
instantRunContext,
runConfigContext,
tasksProvider,
+ InstantRunSettings.isRecorderEnabled(),
runAsValidator,
ServiceManager.getService(InstalledApkCache.class),
new InstantRunClientDelegate() {
@@ -81,6 +82,7 @@
@NotNull InstantRunContext instantRunContext,
@NotNull AndroidRunConfigContext runConfigContext,
@NotNull InstantRunTasksProvider tasksProvider,
+ boolean enableFlightRecorder,
@NotNull RunAsValidator runAsValidator,
@NotNull InstalledApkCache installedApkCache,
@NotNull InstantRunClientDelegate delegate) {
@@ -88,6 +90,7 @@
myInstantRunContext = instantRunContext;
myRunContext = runConfigContext;
myTasksProvider = tasksProvider;
+ myFlightRecorderEnabled = enableFlightRecorder;
myRunAsValidator = runAsValidator;
myInstalledApkCache = installedApkCache;
myInstantRunClientDelegate = delegate;
@@ -107,6 +110,7 @@
FileChangeListener.Changes fileChanges = myInstantRunContext.getFileChangesAndReset();
args.addAll(getInstantRunArguments(buildSelection.mode, fileChanges));
+ args.addAll(getFlightRecorderArguments());
List<String> tasks = new LinkedList<>();
if (buildSelection.mode == BuildMode.CLEAN) {
@@ -257,8 +261,6 @@
}
private static List<String> getInstantRunArguments(@NotNull BuildMode buildMode, @Nullable FileChangeListener.Changes changes) {
- List<String> args = Lists.newArrayListWithExpectedSize(3);
-
// TODO: Add a user-level setting to disable this?
// TODO: Use constants from AndroidProject once we import the new model jar.
@@ -278,9 +280,12 @@
sb.append(",").append("FULL_APK"); //TODO: Replace with enum reference after next model drop.
}
- args.add(sb.toString());
+ return Collections.singletonList(sb.toString());
+ }
- return args;
+ @NotNull
+ private List<String> getFlightRecorderArguments() {
+ return myFlightRecorderEnabled ? ImmutableList.of("--info") : ImmutableList.of();
}
private static void appendChangeInfo(@NotNull StringBuilder sb, @Nullable FileChangeListener.Changes changes) {
diff --git a/android/src/com/android/tools/idea/fd/InstantRunConfigurable.form b/android/src/com/android/tools/idea/fd/InstantRunConfigurable.form
index c82f9f9..7d2f7f0 100644
--- a/android/src/com/android/tools/idea/fd/InstantRunConfigurable.form
+++ b/android/src/com/android/tools/idea/fd/InstantRunConfigurable.form
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.android.tools.idea.fd.InstantRunConfigurable">
- <grid id="27dc6" binding="myContentPanel" layout-manager="GridLayoutManager" row-count="7" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+ <grid id="27dc6" binding="myContentPanel" layout-manager="GridLayoutManager" row-count="10" 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>
- <xy x="20" y="20" width="563" height="250"/>
+ <xy x="20" y="20" width="787" height="523"/>
</constraints>
<properties/>
<border type="none"/>
@@ -18,7 +18,7 @@
</component>
<component id="3e826" class="com.intellij.ui.components.JBCheckBox" binding="myRestartActivityCheckBox">
<constraints>
- <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="2" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Restart activity on code changes"/>
@@ -26,7 +26,7 @@
</component>
<vspacer id="a26c5">
<constraints>
- <grid row="6" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
+ <grid row="8" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<component id="72ff4" class="com.intellij.ui.components.JBLabel" binding="myGradleLabel">
@@ -46,7 +46,7 @@
</component>
<component id="cad68" class="com.intellij.ui.components.JBCheckBox" binding="myShowToastCheckBox">
<constraints>
- <grid row="4" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ <grid row="4" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="2" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Show toasts in the running app when changes are applied"/>
@@ -54,12 +54,76 @@
</component>
<component id="dbc36" class="com.intellij.ui.components.JBCheckBox" binding="myShowIrStatusNotifications">
<constraints>
- <grid row="5" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ <grid row="5" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="2" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Show Instant Run status notifications"/>
</properties>
</component>
+ <component id="34eaf" class="com.intellij.ui.components.JBCheckBox" binding="myEnableRecorder">
+ <constraints>
+ <grid row="6" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="2" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value="Log extra info to help Google troubleshoot Instant Run issues (Recommended)"/>
+ </properties>
+ </component>
+ <grid id="cbc71" binding="myHelpGooglePanel" layout-manager="GridLayoutManager" row-count="4" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+ <margin top="10" left="10" bottom="10" right="10"/>
+ <constraints>
+ <grid row="9" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties/>
+ <border type="none"/>
+ <children>
+ <component id="6d4cb" class="com.intellij.ui.components.JBLabel" binding="myHavingTroubleLabel">
+ <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"/>
+ </constraints>
+ <properties>
+ <fontColor value="NORMAL"/>
+ <text value="Having trouble with Instant Run?"/>
+ </properties>
+ </component>
+ <vspacer id="68131">
+ <constraints>
+ <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
+ </constraints>
+ </vspacer>
+ <component id="22da" class="com.intellij.ui.components.JBLabel">
+ <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"/>
+ </constraints>
+ <properties>
+ <fontColor value="BRIGHTER"/>
+ <text resource-bundle="messages/AndroidBundle" key="instant.run.flr.banner.subtitle"/>
+ </properties>
+ </component>
+ <component id="b0758" class="com.intellij.ui.HyperlinkLabel" binding="myReenableLink">
+ <constraints>
+ <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties/>
+ </component>
+ </children>
+ </grid>
+ <grid id="b6358" layout-manager="FlowLayout" hgap="0" vgap="5" flow-align="0">
+ <constraints>
+ <grid row="7" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="5" use-parent-layout="false"/>
+ </constraints>
+ <properties/>
+ <border type="none"/>
+ <children>
+ <component id="d9cef" class="com.intellij.ui.HyperlinkLabel" binding="myExtraInfoHyperlink">
+ <constraints/>
+ <properties/>
+ </component>
+ <component id="9f49d" class="com.intellij.ui.HyperlinkLabel" binding="myPrivacyPolicyLink">
+ <constraints/>
+ <properties/>
+ </component>
+ </children>
+ </grid>
</children>
</grid>
</form>
diff --git a/android/src/com/android/tools/idea/fd/InstantRunConfigurable.java b/android/src/com/android/tools/idea/fd/InstantRunConfigurable.java
index a31e932..f99e95a 100644
--- a/android/src/com/android/tools/idea/fd/InstantRunConfigurable.java
+++ b/android/src/com/android/tools/idea/fd/InstantRunConfigurable.java
@@ -25,6 +25,7 @@
import com.android.tools.idea.gradle.project.GradleSyncListener;
import com.android.tools.idea.gradle.util.GradleUtil;
import com.android.tools.idea.sdk.progress.StudioLoggerProgressIndicator;
+import com.intellij.ide.BrowserUtil;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
@@ -34,8 +35,10 @@
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.ui.HyperlinkLabel;
+import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBCheckBox;
import com.intellij.ui.components.JBLabel;
+import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.Nls;
@@ -63,9 +66,37 @@
private JBCheckBox myShowToastCheckBox;
private JBCheckBox myShowIrStatusNotifications;
+ private JBCheckBox myEnableRecorder;
+ private HyperlinkLabel myExtraInfoHyperlink;
+ private HyperlinkLabel myPrivacyPolicyLink;
+
+ private HyperlinkLabel myReenableLink;
+ private JPanel myHelpGooglePanel;
+ private JBLabel myHavingTroubleLabel;
+
public InstantRunConfigurable() {
+ myExtraInfoHyperlink.setHtmlText("Learn more about <a href=\"more\">what is logged</a>,");
+ myExtraInfoHyperlink.addHyperlinkListener(e -> BrowserUtil.browse("https://developer.android.com/r/studio-ui/ir-flight-recorder.html"));
+
+ myPrivacyPolicyLink.setHtmlText("and our <a href=\"privacy\">privacy policy.</a>");
+ myPrivacyPolicyLink.addHyperlinkListener(e -> BrowserUtil.browse("https://www.google.com/policies/privacy/"));
+
+ myHelpGooglePanel.setBackground(UIUtil.getPanelBackground().brighter());
+ myHelpGooglePanel.setBorder(BorderFactory.createLineBorder(JBColor.GRAY));
+ myHavingTroubleLabel.setFont(JBUI.Fonts.label().asBold().biggerOn(1.2f));
+
+ myReenableLink.setHtmlText("<a href=\"reenable\">Re-enable and activate extra logging</a>");
+ myReenableLink.addHyperlinkListener(e -> {
+ myInstantRunCheckBox.setSelected(true);
+ enableIrOptions(true);
+ myEnableRecorder.setSelected(true);
+ });
+
+ myInstantRunCheckBox.addActionListener(e -> enableIrOptions(myInstantRunCheckBox.isSelected()));
+
myBuildConfiguration = InstantRunConfiguration.getInstance();
updateLinkState();
+ enableIrOptions(myBuildConfiguration.INSTANT_RUN);
}
@NotNull
@@ -103,7 +134,8 @@
return myBuildConfiguration.INSTANT_RUN != isInstantRunEnabled() ||
myBuildConfiguration.RESTART_ACTIVITY != isRestartActivity() ||
myBuildConfiguration.SHOW_TOAST != isShowToast() ||
- myBuildConfiguration.SHOW_IR_STATUS_NOTIFICATIONS != isShowStatusNotifications();
+ myBuildConfiguration.SHOW_IR_STATUS_NOTIFICATIONS != isShowStatusNotifications() ||
+ myBuildConfiguration.ENABLE_RECORDER != isEnableRecorder();
}
@Override
@@ -112,6 +144,7 @@
myBuildConfiguration.RESTART_ACTIVITY = isRestartActivity();
myBuildConfiguration.SHOW_TOAST = isShowToast();
myBuildConfiguration.SHOW_IR_STATUS_NOTIFICATIONS = isShowStatusNotifications();
+ myBuildConfiguration.ENABLE_RECORDER = isEnableRecorder();
for (Project project : ProjectManager.getInstance().getOpenProjects()) {
if (project.isDefault()) {
@@ -127,6 +160,7 @@
myRestartActivityCheckBox.setSelected(myBuildConfiguration.RESTART_ACTIVITY);
myShowToastCheckBox.setSelected(myBuildConfiguration.SHOW_TOAST);
myShowIrStatusNotifications.setSelected(myBuildConfiguration.SHOW_IR_STATUS_NOTIFICATIONS);
+ myEnableRecorder.setSelected(myBuildConfiguration.ENABLE_RECORDER);
}
@Override
@@ -149,6 +183,10 @@
return myShowIrStatusNotifications.isSelected();
}
+ private boolean isEnableRecorder() {
+ return myEnableRecorder.isSelected();
+ }
+
private void createUIComponents() {
myOldVersionLabel = new HyperlinkLabel();
setSyncLinkMessage("");
@@ -186,9 +224,17 @@
boolean enabled = isGradle && isCurrentPlugin;
myInstantRunCheckBox.setEnabled(isGradle); // allow turning off instant run even if the plugin is not the latest
+ enableIrOptions(enabled);
+ }
+
+ private void enableIrOptions(boolean enabled) {
myRestartActivityCheckBox.setEnabled(enabled);
myShowToastCheckBox.setEnabled(enabled);
myShowIrStatusNotifications.setEnabled(enabled);
+ myEnableRecorder.setEnabled(enabled);
+ myExtraInfoHyperlink.setEnabled(enabled);
+
+ myHelpGooglePanel.setVisible(!enabled);
}
@Override
diff --git a/android/src/com/android/tools/idea/fd/InstantRunConfiguration.java b/android/src/com/android/tools/idea/fd/InstantRunConfiguration.java
index 913cedd..7c11b8e 100644
--- a/android/src/com/android/tools/idea/fd/InstantRunConfiguration.java
+++ b/android/src/com/android/tools/idea/fd/InstantRunConfiguration.java
@@ -29,8 +29,7 @@
public boolean RESTART_ACTIVITY = true;
public boolean SHOW_TOAST = true;
public boolean SHOW_IR_STATUS_NOTIFICATIONS = true;
- public boolean COLD_SWAP = true;
- public String COLD_SWAP_MODE;
+ public boolean ENABLE_RECORDER = false;
public static InstantRunConfiguration getInstance() {
return ServiceManager.getService(InstantRunConfiguration.class);
diff --git a/android/src/com/android/tools/idea/fd/InstantRunNotificationProvider.java b/android/src/com/android/tools/idea/fd/InstantRunNotificationProvider.java
index 6120b66..c386dff 100644
--- a/android/src/com/android/tools/idea/fd/InstantRunNotificationProvider.java
+++ b/android/src/com/android/tools/idea/fd/InstantRunNotificationProvider.java
@@ -36,14 +36,13 @@
BuildCause.APP_NOT_INSTALLED
);
- private static final Map<BuildCause, String> ourNotificationsByCause = new ImmutableMap.Builder<BuildCause, String>()
+ private static final Map<BuildCause, String> ourFullBuildNotificationsByCause = new ImmutableMap.Builder<BuildCause, String>()
.put(BuildCause.USER_REQUESTED_CLEAN_BUILD, AndroidBundle.message("instant.run.notification.cleanbuild.on.user.request"))
.put(BuildCause.MISMATCHING_TIMESTAMPS, AndroidBundle.message("instant.run.notification.cleanbuild.mismatching.timestamps"))
.put(BuildCause.API_TOO_LOW_FOR_INSTANT_RUN, AndroidBundle.message("instant.run.notification.fullbuild.api.less.than.15"))
.put(BuildCause.MANIFEST_RESOURCE_CHANGED, AndroidBundle.message("instant.run.notification.fullbuild.manifestresourcechanged"))
.put(BuildCause.FREEZE_SWAP_REQUIRES_API21, AndroidBundle.message("instant.run.notification.fullbuild.api.less.than.21"))
.put(BuildCause.FREEZE_SWAP_REQUIRES_WORKING_RUN_AS, AndroidBundle.message("instant.run.notification.fullbuild.broken.runas"))
- .put(BuildCause.APP_USES_MULTIPLE_PROCESSES, AndroidBundle.message("instant.run.notification.coldswap.multiprocess"))
.build();
private final BuildSelection myBuildSelection;
@@ -67,8 +66,8 @@
return null;
}
- if (ourNotificationsByCause.containsKey(buildCause)) {
- return ourNotificationsByCause.get(buildCause);
+ if (ourFullBuildNotificationsByCause.containsKey(buildCause)) {
+ return ourFullBuildNotificationsByCause.get(buildCause);
}
if (buildCause == BuildCause.APP_NOT_RUNNING) {
@@ -100,6 +99,10 @@
}
}
else if (buildMode == BuildMode.COLD) {
+ if (buildCause == BuildCause.APP_USES_MULTIPLE_PROCESSES) {
+ return AndroidBundle.message("instant.run.notification.coldswap.multiprocess");
+ }
+
// we requested a cold swap build, so mention why we requested such a build
sb.append(' ').append(buildCause).append('.');
}
diff --git a/android/src/com/android/tools/idea/fd/InstantRunSettings.java b/android/src/com/android/tools/idea/fd/InstantRunSettings.java
index 2e5e5d2..79f8d0e 100644
--- a/android/src/com/android/tools/idea/fd/InstantRunSettings.java
+++ b/android/src/com/android/tools/idea/fd/InstantRunSettings.java
@@ -29,6 +29,11 @@
return configuration.INSTANT_RUN;
}
+ public static void setInstantRunEnabled(boolean en) {
+ InstantRunConfiguration configuration = InstantRunConfiguration.getInstance();
+ configuration.INSTANT_RUN = en;
+ }
+
/** Is showing toasts enabled in the given project */
public static boolean isShowToastEnabled() {
InstantRunConfiguration configuration = InstantRunConfiguration.getInstance();
@@ -50,4 +55,14 @@
InstantRunConfiguration configuration = InstantRunConfiguration.getInstance();
configuration.SHOW_IR_STATUS_NOTIFICATIONS = en;
}
+
+ public static boolean isRecorderEnabled() {
+ InstantRunConfiguration configuration = InstantRunConfiguration.getInstance();
+ return configuration.ENABLE_RECORDER;
+ }
+
+ public static void setRecorderEnabled(boolean en) {
+ InstantRunConfiguration configuration = InstantRunConfiguration.getInstance();
+ configuration.ENABLE_RECORDER = en;
+ }
}
diff --git a/android/src/com/android/tools/idea/fd/LogcatRecorder.java b/android/src/com/android/tools/idea/fd/LogcatRecorder.java
new file mode 100644
index 0000000..6ece112
--- /dev/null
+++ b/android/src/com/android/tools/idea/fd/LogcatRecorder.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.fd;
+
+import com.android.annotations.concurrency.GuardedBy;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.NullOutputReceiver;
+import com.android.ddmlib.logcat.LogCatMessage;
+import com.android.tools.idea.logcat.AndroidLogcatService;
+import com.google.common.collect.EvictingQueue;
+import com.google.common.collect.ImmutableList;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.util.text.StringUtil;
+import org.jetbrains.annotations.NotNull;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * {@link LogcatRecorder} records logcat output from the instant run runtime library on a given device into an in memory ring buffer.
+ * This class is solely intended for use by the {@link FlightRecorder}, and hence behaves in a form that is useful for later viewing:
+ * <ul>
+ * <li>Maintains a single log buffer across devices - we are interested in all runs of the project across all devices</li>
+ * <li>Adds an entry containing the timestamp on each launch to help correlate with other logs</li>
+ * </ul>
+ */
+public class LogcatRecorder {
+ private static final int BUFSIZE = 8192;
+
+ private final Object LOCK = new Object();
+
+ @GuardedBy("LOCK")
+ private final EvictingQueue<String> myLogs = EvictingQueue.create(BUFSIZE);
+
+ private final AndroidLogcatService myLogcatService;
+
+ // Device currently being monitored
+ private AtomicReference<IDevice> myDeviceRef = new AtomicReference<>();
+ private AndroidLogcatService.LogLineListener myLogListener;
+
+ public LogcatRecorder(@NotNull AndroidLogcatService logcatService) {
+ myLogcatService = logcatService;
+ myLogListener = new MyLogLineListener();
+ }
+
+ public void startMonitoring(@NotNull IDevice device, @NotNull LocalDateTime buildTimeStamp) {
+ IDevice old = myDeviceRef.getAndSet(device);
+ if (old != device) {
+ if (old != null) {
+ myLogcatService.removeListener(old, myLogListener);
+ }
+ myLogcatService.addListener(device, myLogListener);
+
+ enableInstantRunLog(device);
+ }
+
+ addLog("------------Launch on " + device.getName() + " @ " + buildTimeStamp.toString());
+ }
+
+ private static void enableInstantRunLog(IDevice device) {
+ ApplicationManager.getApplication().executeOnPooledThread(() -> {
+ try {
+ device.executeShellCommand("setprop log.tag.InstantRun VERBOSE", new NullOutputReceiver());
+ }
+ catch (Exception ignored) {
+ }
+ });
+ }
+
+ public List<String> getLogs() {
+ synchronized (LOCK) {
+ return ImmutableList.copyOf(myLogs);
+ }
+ }
+
+ private void addLog(@NotNull String s) {
+ synchronized (LOCK) {
+ myLogs.add(s);
+ }
+ }
+
+ private class MyLogLineListener implements AndroidLogcatService.LogLineListener {
+ @Override
+ public void receiveLogLine(@NotNull LogCatMessage line) {
+ if ("InstantRun".equals(line.getTag())) { // only save logs from the instant run library
+ addLog(line.toString());
+ }
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/fd/actions/InstantRunFeedbackDialog.form b/android/src/com/android/tools/idea/fd/actions/InstantRunFeedbackDialog.form
new file mode 100644
index 0000000..f5b3855
--- /dev/null
+++ b/android/src/com/android/tools/idea/fd/actions/InstantRunFeedbackDialog.form
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.android.tools.idea.fd.actions.InstantRunFeedbackDialog">
+ <grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="4" 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>
+ <xy x="20" y="20" width="733" height="434"/>
+ </constraints>
+ <properties/>
+ <border type="none"/>
+ <children>
+ <component id="3f6b1" class="com.intellij.ui.components.JBLabel">
+ <constraints>
+ <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text resource-bundle="messages/AndroidBundle" key="instant.run.flr.dialog.description"/>
+ </properties>
+ </component>
+ <component id="c168d" class="com.intellij.ui.components.JBLabel">
+ <constraints>
+ <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text resource-bundle="messages/AndroidBundle" key="instant.run.flr.dialog.includeslogs"/>
+ </properties>
+ </component>
+ <scrollpane id="5750c" class="com.intellij.ui.components.JBScrollPane">
+ <constraints>
+ <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties/>
+ <border type="none"/>
+ <children>
+ <component id="9ed1" class="javax.swing.JTextArea" binding="myIssueTextArea">
+ <constraints/>
+ <properties>
+ <rows value="5"/>
+ </properties>
+ </component>
+ </children>
+ </scrollpane>
+ <scrollpane id="15233" class="com.intellij.ui.components.JBScrollPane">
+ <constraints>
+ <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties/>
+ <border type="none"/>
+ <children>
+ <component id="de46d" class="com.intellij.ui.components.JBList" binding="myFilesList">
+ <constraints/>
+ <properties/>
+ </component>
+ </children>
+ </scrollpane>
+ </children>
+ </grid>
+</form>
diff --git a/android/src/com/android/tools/idea/fd/actions/InstantRunFeedbackDialog.java b/android/src/com/android/tools/idea/fd/actions/InstantRunFeedbackDialog.java
new file mode 100644
index 0000000..26a0254
--- /dev/null
+++ b/android/src/com/android/tools/idea/fd/actions/InstantRunFeedbackDialog.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.fd.actions;
+
+import com.android.tools.idea.fd.FlightRecorder;
+import com.intellij.ide.actions.ShowFilePathAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.openapi.ui.ValidationInfo;
+import com.intellij.ui.*;
+import com.intellij.ui.SingleSelectionModel;
+import com.intellij.ui.components.JBList;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Document;
+import java.awt.*;
+import java.awt.event.MouseEvent;
+import java.nio.file.Path;
+import java.util.List;
+
+public class InstantRunFeedbackDialog extends DialogWrapper {
+ private final List<Path> myLogs;
+
+ private JPanel myPanel;
+ private JTextArea myIssueTextArea;
+ private JBList myFilesList;
+ private String myIssueText;
+
+ protected InstantRunFeedbackDialog(@NotNull Project project) {
+ super(project);
+
+ myFilesList.setVisibleRowCount(4);
+ myFilesList.setEmptyText("No Log Files found");
+ myLogs = FlightRecorder.get(project).getAllLogs();
+ myFilesList.setModel(new CollectionListModel<>(myLogs));
+ myFilesList.setCellRenderer(new ColoredListCellRenderer<Path>() {
+ @Override
+ protected void customizeCellRenderer(JList list, Path value, int index, boolean selected, boolean hasFocus) {
+ append(value.toString());
+ }
+ });
+
+ myFilesList.setSelectionModel(new SingleSelectionModel());
+ myFilesList.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+ new ClickListener() {
+ @Override
+ public boolean onClick(@NotNull MouseEvent event, int clickCount) {
+ if (event.getButton() == MouseEvent.BUTTON1) {
+ Object selectedValue = myFilesList.getSelectedValue();
+ if (selectedValue instanceof Path) {
+ ShowFilePathAction.openFile(((Path)selectedValue).toFile());
+ return true;
+ }
+ }
+ return false;
+ }
+ }.installOn(myFilesList);
+
+ setTitle("Report Instant Run Issue");
+ setModal(true);
+ init();
+ }
+
+ @Nullable
+ @Override
+ protected JComponent createCenterPanel() {
+ return myPanel;
+ }
+
+ @NotNull
+ public List<Path> getLogs() {
+ return myLogs;
+ }
+
+ @NotNull
+ public String getIssueText() {
+ return myIssueText;
+ }
+
+ @Override
+ protected void doOKAction() {
+ myIssueText = getIssueReport();
+ super.doOKAction();
+ }
+
+ @Nullable
+ @Override
+ protected ValidationInfo doValidate() {
+ String issueReport = getIssueReport();
+ if (issueReport.isEmpty()) {
+ return new ValidationInfo("Please describe the issue", myIssueTextArea);
+ }
+ return super.doValidate();
+ }
+
+ private String getIssueReport() {
+ Document document = myIssueTextArea.getDocument();
+ try {
+ return document.getText(0, document.getLength());
+ }
+ catch (BadLocationException e) { // can't happen since we explicitly get from [0..length]
+ assert false : e.toString();
+ return e.toString();
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/fd/actions/SubmitFeedback.java b/android/src/com/android/tools/idea/fd/actions/SubmitFeedback.java
new file mode 100644
index 0000000..0b9caa9
--- /dev/null
+++ b/android/src/com/android/tools/idea/fd/actions/SubmitFeedback.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.fd.actions;
+
+import com.android.tools.idea.fd.FlightRecorder;
+import com.android.tools.idea.fd.InstantRunSettings;
+import com.android.tools.idea.fd.crash.GoogleCrash;
+import com.google.common.escape.Escaper;
+import com.google.common.net.UrlEscapers;
+import com.intellij.ide.BrowserUtil;
+import com.intellij.notification.NotificationGroup;
+import com.intellij.notification.NotificationType;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.application.ApplicationInfo;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.Task;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import org.jetbrains.android.util.AndroidBundle;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.TimeUnit;
+
+public class SubmitFeedback extends DumbAwareAction {
+ private static final NotificationGroup FLR_NOTIFICATION_GROUP = NotificationGroup.balloonGroup("instant.run.flight.recorder");
+
+ public SubmitFeedback() {
+ super("Report Instant Run Issue...");
+ }
+
+ @Override
+ public void update(AnActionEvent e) {
+ Project project = e.getProject();
+ getTemplatePresentation().setVisible(project != null && !project.isDefault());
+ }
+
+ @Override
+ public void actionPerformed(AnActionEvent e) {
+ Project project = e.getProject();
+ if (project == null) {
+ Logger.getInstance(SubmitFeedback.class).info("Unable to identify current project");
+ return;
+ }
+
+ if (!InstantRunSettings.isInstantRunEnabled() || !InstantRunSettings.isRecorderEnabled()) {
+ int result = Messages.showYesNoDialog(
+ project,
+ AndroidBundle.message("instant.run.flr.would.you.like.to.enable"),
+ AndroidBundle.message("instant.run.flr.dialog.title"),
+ "Yes, I'd like to help",
+ "Cancel",
+ Messages.getQuestionIcon());
+ if (result == Messages.NO) {
+ return;
+ }
+
+ InstantRunSettings.setInstantRunEnabled(true);
+ InstantRunSettings.setRecorderEnabled(true);
+ Messages.showInfoMessage(project,
+ AndroidBundle.message("instant.run.flr.howto"),
+ AndroidBundle.message("instant.run.flr.dialog.title"));
+ return;
+ }
+
+ InstantRunFeedbackDialog dialog = new InstantRunFeedbackDialog(project);
+ boolean ok = dialog.showAndGet();
+ if (ok) {
+ new Task.Backgroundable(project, "Submitting Instant Run Issue") {
+ public CompletableFuture<String> myReport;
+
+ @Override
+ public void run(@NotNull ProgressIndicator indicator) {
+ myReport =
+ GoogleCrash.getInstance().submit(FlightRecorder.get(project), dialog.getIssueText(), dialog.getLogs());
+
+ while (!myReport.isDone()) {
+ try {
+ myReport.get(200, TimeUnit.MILLISECONDS);
+ }
+ catch (Exception ignored) {
+ }
+
+ if (indicator.isCanceled()) {
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onSuccess() {
+ if (myReport.isDone()) {
+ String reportId;
+ try {
+ reportId = myReport.getNow("00");
+ }
+ catch (CancellationException e) {
+ Logger.getInstance(SubmitFeedback.class).info("Submission of flight recorder logs cancelled");
+ return;
+ }
+ catch (CompletionException e) {
+ FLR_NOTIFICATION_GROUP.createNotification("Unexpected error while submitting instant run logs: " + e.getMessage(),
+ NotificationType.ERROR);
+ Logger.getInstance(SubmitFeedback.class).info(e);
+ return;
+ }
+ String message = String.format("<html>Thank you for submitting the bug report.<br>" +
+ "If you would like to follow up on this report, please file a bug at <a href=\"bug\">b.android.com</a> and specify the report id '%1$s'<html>",
+ reportId);
+ FLR_NOTIFICATION_GROUP
+ .createNotification("", message, NotificationType.INFORMATION, (notification, event) -> {
+ Escaper escaper = UrlEscapers.urlFormParameterEscaper();
+ String comment = String.format("Build: %1$s\nInstant Run Report: %2$s",
+ ApplicationInfo.getInstance().getFullVersion(),
+ reportId);
+ String url = String.format("https://code.google.com/p/android/issues/entry?template=%1$s&comment=%2$s&status=New",
+ escaper.escape("Android Studio Instant Run Bug"),
+ escaper.escape(comment));
+ BrowserUtil.browse(url);
+ })
+ .notify(project);
+ }
+ }
+ }.queue();
+ }
+ }
+}
diff --git a/android/src/com/android/tools/idea/fd/crash/GoogleCrash.java b/android/src/com/android/tools/idea/fd/crash/GoogleCrash.java
new file mode 100644
index 0000000..3911ea8
--- /dev/null
+++ b/android/src/com/android/tools/idea/fd/crash/GoogleCrash.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.fd.crash;
+
+import com.android.tools.analytics.Anonymizer;
+import com.android.tools.idea.fd.FlightRecorder;
+import com.android.utils.NullLogger;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.intellij.ide.util.PropertiesComponent;
+import com.intellij.openapi.application.ApplicationInfo;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.updateSettings.impl.UpdateChecker;
+import com.intellij.openapi.util.SystemInfo;
+import com.intellij.openapi.util.io.FileUtilRt;
+import com.intellij.openapi.util.text.StringUtil;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.HttpResponseException;
+import org.apache.http.client.entity.GzipCompressingEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.*;
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryUsage;
+import java.lang.management.RuntimeMXBean;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ForkJoinPool;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * {@link GoogleCrash} provides APIs to upload crash reports to Google crash reporting service.
+ * @see <a href="http://go/studio-g3doc/implementation/crash">Crash Backend</a> for more information.
+ */
+public class GoogleCrash {
+ private static final boolean UNIT_TEST_MODE = ApplicationManager.getApplication() == null;
+ private static final boolean DEBUG_BUILD = !UNIT_TEST_MODE && ApplicationManager.getApplication().isInternal();
+
+ // Send crashes during development to the staging backend
+ private static final String CRASH_URL =
+ (UNIT_TEST_MODE || DEBUG_BUILD) ? "https://clients2.google.com/cr/staging_report" : "https://clients2.google.com/cr/report";
+
+ @Nullable
+ private static final String ANONYMIZED_UID = getAnonymizedUid();
+ private static final String LOCALE = Locale.getDefault() == null ? "unknown" : Locale.getDefault().toString();
+
+ // The standard keys expected by crash backend. The product id and version are required, others are optional.
+ static final String KEY_PRODUCT_ID = "productId";
+ static final String KEY_VERSION = "version";
+
+ private static GoogleCrash ourInstance;
+ private final String myCrashUrl;
+
+ @Nullable
+ private static String getAnonymizedUid() {
+ if (UNIT_TEST_MODE) {
+ return "UnitTest";
+ }
+
+ try {
+ return Anonymizer.anonymizeUtf8(new NullLogger(), UpdateChecker.getInstallationUID(PropertiesComponent.getInstance()));
+ }
+ catch (IOException e) {
+ return null;
+ }
+ }
+
+ private GoogleCrash() {
+ this(CRASH_URL);
+ }
+
+ @VisibleForTesting
+ GoogleCrash(@NotNull String crashUrl) {
+ myCrashUrl = crashUrl;
+ }
+
+ public CompletableFuture<String> submit(@NotNull FlightRecorder flightRecorder, @NotNull String issueText, @NotNull List<Path> logFiles) {
+ CompletableFuture<String> future = new CompletableFuture<>();
+ ForkJoinPool.commonPool().submit(() -> {
+ try {
+ HttpClient client = HttpClients.createDefault();
+ HttpResponse response = client.execute(createPost(flightRecorder, issueText, logFiles));
+ StatusLine statusLine = response.getStatusLine();
+ if (statusLine.getStatusCode() >= 300) {
+ future.completeExceptionally(new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase()));
+ if (DEBUG_BUILD) {
+ //noinspection UseOfSystemOutOrSystemErr
+ System.out.println("Error submitting report: " + statusLine);
+ }
+ return;
+ }
+
+ HttpEntity entity = response.getEntity();
+ if (entity == null) {
+ future.completeExceptionally(new NullPointerException("Empty response entity"));
+ return;
+ }
+
+ String reportId = EntityUtils.toString(entity);
+ if (DEBUG_BUILD) {
+ //noinspection UseOfSystemOutOrSystemErr
+ System.out.println("Report submitted: http://go/crash-staging/" + reportId);
+ }
+ future.complete(reportId);
+ }
+ catch (IOException e) {
+ future.completeExceptionally(e);
+ }
+ });
+ return future;
+ }
+
+ @NotNull
+ private HttpUriRequest createPost(@NotNull FlightRecorder flightRecorder, @NotNull String issueText, @NotNull List<Path> logFiles) {
+ HttpPost post = new HttpPost(myCrashUrl);
+
+ ApplicationInfo applicationInfo = getApplicationInfo();
+
+ String strictVersion = applicationInfo == null ? "0.0.0.0" : applicationInfo.getStrictVersion();
+
+ MultipartEntityBuilder builder = MultipartEntityBuilder.create();
+
+ // key names recognized by crash
+ builder.addTextBody(KEY_PRODUCT_ID, "AndroidStudio");
+ builder.addTextBody(KEY_VERSION, strictVersion);
+ builder.addTextBody("exception_info", getUniqueStackTrace());
+ builder.addTextBody("user_report", issueText);
+
+ if (ANONYMIZED_UID != null) {
+ builder.addTextBody("guid", ANONYMIZED_UID);
+ }
+ RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
+ builder.addTextBody("ptime", Long.toString(runtimeMXBean.getUptime()));
+
+ // product specific key value pairs
+ builder.addTextBody("fullVersion", applicationInfo == null ? "0.0.0.0" : applicationInfo.getFullVersion());
+
+ builder.addTextBody("osName", StringUtil.notNullize(SystemInfo.OS_NAME));
+ builder.addTextBody("osVersion", StringUtil.notNullize(SystemInfo.OS_VERSION));
+ builder.addTextBody("osArch", StringUtil.notNullize(SystemInfo.OS_ARCH));
+ builder.addTextBody("locale", StringUtil.notNullize(LOCALE));
+
+ builder.addTextBody("vmName", StringUtil.notNullize(runtimeMXBean.getVmName()));
+ builder.addTextBody("vmVendor", StringUtil.notNullize(runtimeMXBean.getVmVendor()));
+ builder.addTextBody("vmVersion", StringUtil.notNullize(runtimeMXBean.getVmVersion()));
+
+ MemoryUsage usage = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
+ builder.addTextBody("heapUsed", Long.toString(usage.getUsed()));
+ builder.addTextBody("heapCommitted", Long.toString(usage.getCommitted()));
+ builder.addTextBody("heapMax", Long.toString(usage.getMax()));
+
+ // add report specific data
+ builder.addTextBody("Type", "InstantRunFlightRecorder");
+ addFlightRecorderLogs(builder, flightRecorder, logFiles);
+
+ post.setEntity(new GzipCompressingEntity(builder.build()));
+ return post;
+ }
+
+ static String getUniqueStackTrace() {
+ StringBuilder sb = new StringBuilder(100);
+ sb.append("com.android.InstantRunException: Flight Recorder Information: ");
+ sb.append(System.currentTimeMillis());
+ sb.append('\n');
+ sb.append("\tat ");
+
+ int i = 0;
+ for (String u : Splitter.on('-').split(UUID.randomUUID().toString())) {
+ sb.append('p');
+ sb.append(u);
+ sb.append('.');
+ }
+ sb.append("FlightRecorder.report(Flight");
+ sb.append(System.currentTimeMillis());
+ sb.append("Recorder.java:500)");
+
+ return sb.toString();
+ }
+
+ private static void addFlightRecorderLogs(@NotNull MultipartEntityBuilder builder,
+ @NotNull FlightRecorder flightRecorder,
+ @NotNull List<Path> logFiles) {
+ try {
+ // Crash backend restricts uploads to 1.2M total, so we need to zip up all the files together.
+ Path path = zipFiles(flightRecorder, logFiles);
+ builder.addBinaryBody(path.getFileName().toString(),
+ Files.readAllBytes(path),
+ ContentType.APPLICATION_OCTET_STREAM,
+ path.getFileName().toString());
+ }
+ catch (IOException e) {
+ builder.addTextBody("IOError", e.toString());
+ }
+ }
+
+ @NotNull
+ private static Path zipFiles(@NotNull FlightRecorder flightRecorder, @NotNull List<Path> logFiles)
+ throws IOException {
+ Path tempFile = Files.createTempFile("flr", ".zip");
+ String baseName = FileUtilRt.getNameWithoutExtension(tempFile.getFileName().toString());
+
+ byte[] data = new byte[4096];
+ int i;
+
+ try (FileOutputStream fos = new FileOutputStream(tempFile.toFile());
+ BufferedOutputStream bos = new BufferedOutputStream(fos);
+ ZipOutputStream zos = new ZipOutputStream(bos)) {
+ zos.setMethod(ZipOutputStream.DEFLATED);
+
+ for (Path file : logFiles) {
+ String name = baseName + "/" + flightRecorder.getPresentablePath(file);
+ zos.putNextEntry(new ZipEntry(name.replace(File.separatorChar, '/')));
+
+ try (InputStream is = Files.newInputStream(file);
+ BufferedInputStream bis = new BufferedInputStream(is)) {
+ while ((i = bis.read(data)) > 0) {
+ zos.write(data, 0, i);
+ }
+ }
+ }
+ }
+
+ return tempFile;
+ }
+
+ @Nullable
+ private static ApplicationInfo getApplicationInfo() {
+ // We obtain the ApplicationInfo only if running with an application instance. Otherwise, a call to a ServiceManager never returns..
+ return ApplicationManager.getApplication() == null ? null : ApplicationInfo.getInstance();
+ }
+
+ public static synchronized GoogleCrash getInstance() {
+ if (ourInstance == null) {
+ ourInstance = new GoogleCrash();
+ }
+
+ return ourInstance;
+ }
+}
+
diff --git a/android/src/com/android/tools/idea/gradle/dsl/model/dependencies/ArtifactDependencySpec.java b/android/src/com/android/tools/idea/gradle/dsl/model/dependencies/ArtifactDependencySpec.java
index 0d84fbe..1b36650 100644
--- a/android/src/com/android/tools/idea/gradle/dsl/model/dependencies/ArtifactDependencySpec.java
+++ b/android/src/com/android/tools/idea/gradle/dsl/model/dependencies/ArtifactDependencySpec.java
@@ -116,8 +116,7 @@
this.extension = emptyToNull(extension);
}
- @Override
- public boolean equals(Object o) {
+ public boolean equalsIgnoreVersion(Object o) {
if (this == o) {
return true;
}
@@ -127,12 +126,20 @@
ArtifactDependencySpec that = (ArtifactDependencySpec)o;
return Objects.equal(name, that.name) &&
Objects.equal(group, that.group) &&
- Objects.equal(version, that.version) &&
Objects.equal(classifier, that.classifier) &&
Objects.equal(extension, that.extension);
}
@Override
+ public boolean equals(Object o) {
+ if (equalsIgnoreVersion(o)) {
+ ArtifactDependencySpec that = (ArtifactDependencySpec)o;
+ return Objects.equal(version, that.version);
+ }
+ return false;
+ }
+
+ @Override
public int hashCode() {
return Objects.hashCode(name, group, version, classifier, extension);
}
diff --git a/android/src/com/android/tools/idea/gradle/invoker/GradleTasksExecutor.java b/android/src/com/android/tools/idea/gradle/invoker/GradleTasksExecutor.java
index 1c99ebe..b139035 100644
--- a/android/src/com/android/tools/idea/gradle/invoker/GradleTasksExecutor.java
+++ b/android/src/com/android/tools/idea/gradle/invoker/GradleTasksExecutor.java
@@ -21,6 +21,9 @@
import com.android.ide.common.blame.SourceFilePosition;
import com.android.ide.common.blame.SourcePosition;
import com.android.ide.common.blame.parser.PatternAwareOutputParser;
+import com.android.tools.idea.fd.InstantRunBuildProgressListener;
+import com.android.tools.idea.fd.FlightRecorder;
+import com.android.tools.idea.fd.InstantRunSettings;
import com.android.tools.idea.gradle.GradleModel;
import com.android.tools.idea.gradle.compiler.AndroidGradleBuildConfiguration;
import com.android.tools.idea.gradle.facet.AndroidGradleFacet;
@@ -288,6 +291,7 @@
GradleOutputForwarder output = new GradleOutputForwarder(consoleView);
BuildException buildError = null;
+ InstantRunBuildProgressListener instantRunProgressListener = null;
ExternalSystemTaskId id = myContext.getTaskId();
CancellationTokenSource cancellationTokenSource = GradleConnector.newCancellationTokenSource();
try {
@@ -357,6 +361,11 @@
}
});
+ if (InstantRunSettings.isInstantRunEnabled() && InstantRunSettings.isRecorderEnabled()) {
+ instantRunProgressListener = new InstantRunBuildProgressListener();
+ launcher.addProgressListener(instantRunProgressListener);
+ }
+
launcher.run();
}
catch (BuildException e) {
@@ -368,6 +377,9 @@
finally {
myContext.dropCancellationInfoFor(id);
String gradleOutput = output.toString();
+ if (instantRunProgressListener != null) {
+ FlightRecorder.get(myProject).saveBuildOutput(gradleOutput, instantRunProgressListener);
+ }
Application application = ApplicationManager.getApplication();
if (isGuiTestingMode()) {
String testOutput = application.getUserData(GRADLE_BUILD_OUTPUT_IN_GUI_TEST_KEY);
diff --git a/android/src/com/android/tools/idea/gradle/structure/services/GradleOperations.java b/android/src/com/android/tools/idea/gradle/structure/services/GradleOperations.java
index 82382ec..f5e290c 100644
--- a/android/src/com/android/tools/idea/gradle/structure/services/GradleOperations.java
+++ b/android/src/com/android/tools/idea/gradle/structure/services/GradleOperations.java
@@ -29,6 +29,7 @@
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
+import com.intellij.util.text.VersionComparatorUtil;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -89,7 +90,9 @@
for (ArtifactDependencyModel dependency : dependenciesModel.artifacts()) {
ArtifactDependencySpec spec = ArtifactDependencySpec.create(dependency);
for (String dependencyValue : metadata.getDependencies()) {
- if (spec.equals(ArtifactDependencySpec.create(dependencyValue))) {
+ ArtifactDependencySpec value = ArtifactDependencySpec.create(dependencyValue);
+ // Ensure that the found version is at least the target version.
+ if (value.equalsIgnoreVersion(spec) && VersionComparatorUtil.compare(spec.version, value.version) >= 0) {
return true;
}
}
diff --git a/android/src/com/android/tools/idea/npw/deprecated/ConfigureAndroidProjectPath.java b/android/src/com/android/tools/idea/npw/deprecated/ConfigureAndroidProjectPath.java
index f7b8fac..29430c7 100755
--- a/android/src/com/android/tools/idea/npw/deprecated/ConfigureAndroidProjectPath.java
+++ b/android/src/com/android/tools/idea/npw/deprecated/ConfigureAndroidProjectPath.java
@@ -108,18 +108,6 @@
BuildToolInfo buildTool = sdkHandler.getLatestBuildTool(progress, false);
Revision minimumRequiredBuildToolVersion = Revision.parseRevision(SdkConstants.MIN_BUILD_TOOLS_VERSION);
- // TODO: remove once maven dependency downloading is available in studio
- StudioSdkUtil.reloadRemoteSdkWithModalProgress();
- GradleCoordinate constraintCoordinate = GradleCoordinate.parseCoordinateString(SdkConstants.CONSTRAINT_LAYOUT_LIB_ARTIFACT + ":+");
- RepositoryPackages packages = sdkHandler.getSdkManager(progress).getPackages();
- RepoPackage constraintPackage = SdkMavenRepository.findBestPackageMatching(constraintCoordinate, packages.getLocalPackages().values());
- if (constraintPackage == null) {
- constraintPackage = SdkMavenRepository.findBestPackageMatching(constraintCoordinate, packages.getRemotePackages().values());
- if (constraintPackage != null) {
- state.listPush(WizardConstants.INSTALL_REQUESTS_KEY, constraintPackage.getPath());
- }
- }
-
if (buildTool != null && buildTool.getRevision().compareTo(minimumRequiredBuildToolVersion) >= 0) {
state.put(WizardConstants.BUILD_TOOLS_VERSION_KEY, buildTool.getRevision().toString());
}
diff --git a/android/src/com/android/tools/idea/rendering/RenderErrorPanel.java b/android/src/com/android/tools/idea/rendering/RenderErrorPanel.java
index 3f099a8..cc63a13 100644
--- a/android/src/com/android/tools/idea/rendering/RenderErrorPanel.java
+++ b/android/src/com/android/tools/idea/rendering/RenderErrorPanel.java
@@ -218,6 +218,11 @@
return;
}
+ if (myHTMLViewer == null) {
+ // Already disposed
+ return;
+ }
+
mySeverity = severity;
if (html == null) {
myResult = null;
@@ -251,6 +256,11 @@
}
private void setupStyle() {
+ if (myHTMLViewer != null) {
+ // Already disposed
+ return;
+ }
+
// Make the scrollPane transparent
JViewport viewPort = myScrollPane.getViewport();
viewPort.setOpaque(false);
@@ -275,7 +285,7 @@
}
public int getPreferredHeight(@SuppressWarnings("UnusedParameters") int width) {
- return myHTMLViewer.getPreferredSize().height;
+ return myHTMLViewer != null ? myHTMLViewer.getPreferredSize().height : 0;
}
@Nullable
@@ -1299,6 +1309,10 @@
@SuppressWarnings({"HardCodedStringLiteral"})
private void showEmpty() {
UIUtil.invokeLaterIfNeeded(() -> {
+ if (myHTMLViewer == null) {
+ // Already disposed
+ return;
+ }
try {
myHTMLViewer.read(new StringReader("<html><body></body></html>"), null);
}
diff --git a/android/src/com/android/tools/idea/run/AndroidLaunchTasksProviderFactory.java b/android/src/com/android/tools/idea/run/AndroidLaunchTasksProviderFactory.java
index dcee05d..d7891c9 100644
--- a/android/src/com/android/tools/idea/run/AndroidLaunchTasksProviderFactory.java
+++ b/android/src/com/android/tools/idea/run/AndroidLaunchTasksProviderFactory.java
@@ -15,14 +15,14 @@
*/
package com.android.tools.idea.run;
-import com.android.tools.idea.fd.InstantRunBuildAnalyzer;
-import com.android.tools.idea.fd.InstantRunContext;
-import com.android.tools.idea.fd.InstantRunStatsService;
+import com.android.tools.fd.client.InstantRunBuildInfo;
+import com.android.tools.idea.fd.*;
import com.android.tools.idea.run.tasks.LaunchTasksProvider;
import com.android.tools.idea.run.tasks.LaunchTasksProviderFactory;
import com.android.tools.idea.run.tasks.UpdateSessionTasksProvider;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.project.Project;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -33,6 +33,7 @@
private final AndroidFacet myFacet;
private final ApplicationIdProvider myApplicationIdProvider;
private final ApkProvider myApkProvider;
+ private final DeviceFutures myDeviceFutures;
private final LaunchOptions myLaunchOptions;
private final ProcessHandler myPreviousSessionProcessHandler;
private final InstantRunContext myInstantRunContext;
@@ -42,6 +43,7 @@
@NotNull AndroidFacet facet,
@NotNull ApplicationIdProvider applicationIdProvider,
@NotNull ApkProvider apkProvider,
+ @NotNull DeviceFutures deviceFutures,
@NotNull LaunchOptions launchOptions,
@Nullable ProcessHandler processHandler,
@Nullable InstantRunContext instantRunContext) {
@@ -50,6 +52,7 @@
myFacet = facet;
myApplicationIdProvider = applicationIdProvider;
myApkProvider = apkProvider;
+ myDeviceFutures = deviceFutures;
myLaunchOptions = launchOptions;
myPreviousSessionProcessHandler = processHandler;
myInstantRunContext = instantRunContext;
@@ -58,11 +61,20 @@
@NotNull
@Override
public LaunchTasksProvider get() {
- InstantRunStatsService.get(myEnv.getProject()).notifyDeployStarted();
+ Project project = myEnv.getProject();
+ InstantRunStatsService.get(project).notifyDeployStarted();
InstantRunBuildAnalyzer analyzer = null;
- if (myInstantRunContext != null && myInstantRunContext.getInstantRunBuildInfo() != null) {
- analyzer = new InstantRunBuildAnalyzer(myEnv.getProject(), myInstantRunContext, myPreviousSessionProcessHandler);
+ InstantRunBuildInfo instantRunBuildInfo = myInstantRunContext != null ? myInstantRunContext.getInstantRunBuildInfo() : null;
+ if (instantRunBuildInfo != null) {
+ analyzer = new InstantRunBuildAnalyzer(project, myInstantRunContext, myPreviousSessionProcessHandler);
+
+ if (InstantRunSettings.isRecorderEnabled()) {
+ if (!myDeviceFutures.getDevices().isEmpty()) { // Instant Run is guaranteed to be for exactly 1 device
+ FlightRecorder.get(project).setLaunchTarget(myDeviceFutures.getDevices().get(0));
+ }
+ FlightRecorder.get(project).saveBuildInfo(instantRunBuildInfo);
+ }
}
if (analyzer != null && analyzer.canReuseProcessHandler()) {
diff --git a/android/src/com/android/tools/idea/run/AndroidRunConfigurationBase.java b/android/src/com/android/tools/idea/run/AndroidRunConfigurationBase.java
index 1c90943..2ea3399 100755
--- a/android/src/com/android/tools/idea/run/AndroidRunConfigurationBase.java
+++ b/android/src/com/android/tools/idea/run/AndroidRunConfigurationBase.java
@@ -59,7 +59,6 @@
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.WriteExternalException;
import icons.AndroidIcons;
-import org.gradle.tooling.internal.consumer.DefaultGradleConnector;
import org.jdom.Element;
import org.jetbrains.android.actions.AndroidEnableAdbServiceAction;
import org.jetbrains.android.facet.AndroidFacet;
@@ -77,8 +76,6 @@
import java.util.concurrent.TimeUnit;
import static com.android.tools.idea.fd.gradle.InstantRunGradleSupport.*;
-import static com.android.tools.idea.fd.gradle.InstantRunGradleSupport.LEGACY_MULTIDEX_REQUIRES_ART;
-import static com.android.tools.idea.fd.gradle.InstantRunGradleSupport.VARIANT_DOES_NOT_SUPPORT_INSTANT_RUN;
import static com.android.tools.idea.gradle.util.Projects.requiredAndroidModelMissing;
public abstract class AndroidRunConfigurationBase extends ModuleBasedConfiguration<JavaRunConfigurationModule> implements
@@ -497,7 +494,7 @@
ApkProvider apkProvider = getApkProvider(facet, applicationIdProvider);
LaunchTasksProviderFactory providerFactory =
- new AndroidLaunchTasksProviderFactory(this, env, facet, applicationIdProvider, apkProvider, launchOptions, processHandler,
+ new AndroidLaunchTasksProviderFactory(this, env, facet, applicationIdProvider, apkProvider, deviceFutures, launchOptions, processHandler,
instantRunContext);
InstantRunStatsService.get(project).notifyBuildStarted();
diff --git a/android/src/com/android/tools/idea/templates/recipe/DefaultRecipeExecutor.java b/android/src/com/android/tools/idea/templates/recipe/DefaultRecipeExecutor.java
index aa860d9..049e20a 100644
--- a/android/src/com/android/tools/idea/templates/recipe/DefaultRecipeExecutor.java
+++ b/android/src/com/android/tools/idea/templates/recipe/DefaultRecipeExecutor.java
@@ -146,7 +146,7 @@
DependenciesModel buildscriptDependencies = buildModel.buildscript().dependencies();
ArtifactDependencyModel targetDependencyModel = null;
for (ArtifactDependencyModel dependencyModel : buildscriptDependencies.artifacts(CLASSPATH_CONFIGURATION_NAME)) {
- if(equalsIgnoreVersion(toBeAddedDependency, ArtifactDependencySpec.create(dependencyModel))) {
+ if (toBeAddedDependency.equalsIgnoreVersion(ArtifactDependencySpec.create(dependencyModel))) {
targetDependencyModel = dependencyModel;
}
}
@@ -175,13 +175,6 @@
myNeedsGradleSync = true;
}
- private static boolean equalsIgnoreVersion(@NotNull ArtifactDependencySpec spec1, @NotNull ArtifactDependencySpec spec2) {
- return Objects.equal(spec1.name, spec2.name) &&
- Objects.equal(spec1.group, spec2.group) &&
- Objects.equal(spec1.classifier, spec2.classifier) &&
- Objects.equal(spec1.extension, spec2.extension);
- }
-
@NotNull
private static String formatClasspath(@NotNull String dependency) {
return "buildscript {" + LINE_SEPARATOR +
diff --git a/android/src/org/jetbrains/android/util/AndroidResourceUtil.java b/android/src/org/jetbrains/android/util/AndroidResourceUtil.java
index 20f0135..18fb639 100644
--- a/android/src/org/jetbrains/android/util/AndroidResourceUtil.java
+++ b/android/src/org/jetbrains/android/util/AndroidResourceUtil.java
@@ -1433,6 +1433,11 @@
assert rootTag != null;
final XmlElementFactory elementFactory = XmlElementFactory.getInstance(file.getProject());
+ if (StringUtil.isEmpty(namespaceUri)) {
+ // The style attribute has an empty namespaceUri:
+ return "";
+ }
+
String prefix = rootTag.getPrefixByNamespace(namespaceUri);
if (prefix != null) {
return prefix;
diff --git a/android/testData/projects/projectWithDataBinding/app/src/main/res/layout/no_variable_layout.xml b/android/testData/projects/projectWithDataBinding/app/src/main/res/layout/no_variable_layout.xml
new file mode 100644
index 0000000..94ac371
--- /dev/null
+++ b/android/testData/projects/projectWithDataBinding/app/src/main/res/layout/no_variable_layout.xml
@@ -0,0 +1,36 @@
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<layout>
+ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ tools:context="com.android.example.appwithdatabinding.MainActivity">
+
+ <TextView
+ android:id="@+id/view1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ <View
+ android:id="@+id/view2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ </RelativeLayout>
+</layout>
diff --git a/android/testSrc/com/android/tools/idea/fd/FlightRecorderTest.java b/android/testSrc/com/android/tools/idea/fd/FlightRecorderTest.java
new file mode 100644
index 0000000..bcc664c
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/fd/FlightRecorderTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.fd;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+
+import static org.junit.Assert.*;
+
+public class FlightRecorderTest {
+ @Rule
+ public TemporaryFolder myFolder = new TemporaryFolder();
+
+ @Test
+ public void folderNameConversions() {
+ LocalDateTime dateTime = LocalDateTime.parse("2016-09-23T11:13:41");
+ assertEquals("2016-09-23T11.13.41", FlightRecorder.timeStampToFolder(dateTime));
+ assertEquals(dateTime, FlightRecorder.folderToTimeStamp(Paths.get("/foo/bar/2016-09-23T11.13.41")));
+ }
+
+ @Test
+ public void trimOldLogs() throws IOException {
+ LocalDateTime now = LocalDateTime.now();
+ Random random = new Random(System.currentTimeMillis());
+
+ // create a bunch of log folders at different times in the past
+ List<LocalDateTime> instants = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ LocalDateTime instant = now.minus(random.nextInt(500), ChronoUnit.SECONDS);
+ instants.add(instant);
+
+ // create a log folder and add some logs within it
+ File logFolder = myFolder.newFolder(FlightRecorder.timeStampToFolder(instant));
+ Files.write("", new File(logFolder, "build.log"), Charsets.UTF_8);
+ }
+
+ Collections.sort(instants);
+
+ int count = 3;
+ FlightRecorder.trimOldLogs(myFolder.getRoot().toPath(), count);
+
+ for (int i = 0; i < instants.size() - count; i++) {
+ File f = new File(myFolder.getRoot(), FlightRecorder.timeStampToFolder(instants.get(i)));
+
+ if (i < instants.size() - count) {
+ assertFalse("Stale log folder still exists", f.exists());
+ } else {
+ assertTrue("New log folder unexpectedly trimmed", f.exists());
+ }
+ }
+ }
+}
diff --git a/android/testSrc/com/android/tools/idea/fd/InstantRunBuilderTest.java b/android/testSrc/com/android/tools/idea/fd/InstantRunBuilderTest.java
index 646c3ea..1f81f06 100644
--- a/android/testSrc/com/android/tools/idea/fd/InstantRunBuilderTest.java
+++ b/android/testSrc/com/android/tools/idea/fd/InstantRunBuilderTest.java
@@ -159,8 +159,8 @@
myInstantRunClientDelegate = createInstantRunClientDelegate();
myBuilder =
- new InstantRunBuilder(myDevice, myInstantRunContext, myRunConfigContext, myTasksProvider, ourRunAsSupported, myInstalledApkCache,
- myInstantRunClientDelegate);
+ new InstantRunBuilder(myDevice, myInstantRunContext, myRunConfigContext, myTasksProvider, false,
+ ourRunAsSupported, myInstalledApkCache, myInstantRunClientDelegate);
}
@NotNull
@@ -186,8 +186,8 @@
@Test
public void cleanBuildIfNoDevice() throws Exception {
InstantRunBuilder builder =
- new InstantRunBuilder(null, myInstantRunContext, myRunConfigContext, myTasksProvider, ourRunAsSupported, myInstalledApkCache,
- myInstantRunClientDelegate);
+ new InstantRunBuilder(null, myInstantRunContext, myRunConfigContext, myTasksProvider, false,
+ ourRunAsSupported, myInstalledApkCache, myInstantRunClientDelegate);
builder.build(myTaskRunner, Arrays.asList("-Pdevice.api=14", "-Pprofiling=on"));
assertEquals(
"gradlew -Pdevice.api=14 -Pprofiling=on -Pandroid.optional.compilation=INSTANT_DEV,FULL_APK clean :app:gen :app:assemble",
@@ -337,8 +337,8 @@
setUpDeviceForHotSwap();
myBuilder =
- new InstantRunBuilder(myDevice, myInstantRunContext, myRunConfigContext, myTasksProvider, ourRunAsNotSupported, myInstalledApkCache,
- myInstantRunClientDelegate);
+ new InstantRunBuilder(myDevice, myInstantRunContext, myRunConfigContext, myTasksProvider, false,
+ ourRunAsNotSupported, myInstalledApkCache, myInstantRunClientDelegate);
myBuilder.build(myTaskRunner, Collections.emptyList());
assertEquals(
"gradlew -Pandroid.optional.compilation=INSTANT_DEV,FULL_APK :app:assemble",
@@ -411,6 +411,17 @@
myTaskRunner.getBuilds());
}
+ @Test
+ public void flightRecorderOptions() throws Exception {
+ InstantRunBuilder builder =
+ new InstantRunBuilder(null, myInstantRunContext, myRunConfigContext, myTasksProvider, true,
+ ourRunAsSupported, myInstalledApkCache, myInstantRunClientDelegate);
+ builder.build(myTaskRunner, Arrays.asList("-Pdevice.api=14", "-Pprofiling=on"));
+ assertEquals(
+ "gradlew -Pdevice.api=14 -Pprofiling=on -Pandroid.optional.compilation=INSTANT_DEV,FULL_APK --info clean :app:gen :app:assemble",
+ myTaskRunner.getBuilds());
+ }
+
private void setUpDeviceForHotSwap() {
HashCode resourcesHash = HashCode.fromInt(1);
myInstalledPatchCache.setInstalledManifestResourcesHash(myDevice, APPLICATION_ID, resourcesHash);
diff --git a/android/testSrc/com/android/tools/idea/fd/InstantRunNotificationProviderTest.java b/android/testSrc/com/android/tools/idea/fd/InstantRunNotificationProviderTest.java
index a16c408..f287564 100644
--- a/android/testSrc/com/android/tools/idea/fd/InstantRunNotificationProviderTest.java
+++ b/android/testSrc/com/android/tools/idea/fd/InstantRunNotificationProviderTest.java
@@ -64,4 +64,17 @@
InstantRunNotificationProvider provider = new InstantRunNotificationProvider(buildSelection, DeployType.HOTSWAP, "");
assertEquals(AndroidBundle.message("instant.run.notification.hotswap", ""), provider.getNotificationText());
}
+
+ @Test
+ public void multiProcessOnApi19() {
+ BuildSelection buildSelection = new BuildSelection(BuildMode.COLD, BuildCause.APP_USES_MULTIPLE_PROCESSES);
+
+ // if we generated an apk, then we shouldn't talk about multi process
+ InstantRunNotificationProvider provider = new InstantRunNotificationProvider(buildSelection, DeployType.FULLAPK, "");
+ assertEquals("Instant Run re-installed and restarted the app", provider.getNotificationText());
+
+ // but if we generated cold swap patches, then we should specify that we did so because of multi process
+ provider = new InstantRunNotificationProvider(buildSelection, DeployType.DEX, "");
+ assertEquals(AndroidBundle.message("instant.run.notification.coldswap.multiprocess"), provider.getNotificationText());
+ }
}
diff --git a/android/testSrc/com/android/tools/idea/gradle/customizer/dependency/TransitiveDependencySetupTest.java b/android/testSrc/com/android/tools/idea/gradle/customizer/dependency/TransitiveDependencySetupTest.java
index 41b376a..ff09939 100644
--- a/android/testSrc/com/android/tools/idea/gradle/customizer/dependency/TransitiveDependencySetupTest.java
+++ b/android/testSrc/com/android/tools/idea/gradle/customizer/dependency/TransitiveDependencySetupTest.java
@@ -35,6 +35,12 @@
* Integration test that verifies that transitive dependencies are set up correctly.
*/
public class TransitiveDependencySetupTest extends AndroidGradleTestCase {
+
+ @Override
+ protected void runTest() throws Throwable {
+ // Ignore this whole class. See http://b.android.com/221883.
+ }
+
@Override
protected void tearDown() throws Exception {
super.tearDown();
@@ -170,4 +176,4 @@
}
return allLibraries;
}
-}
\ No newline at end of file
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/dsl/model/dependencies/ArtifactDependencySpecTest.java b/android/testSrc/com/android/tools/idea/gradle/dsl/model/dependencies/ArtifactDependencySpecTest.java
index eeb2c42..22d7e51 100644
--- a/android/testSrc/com/android/tools/idea/gradle/dsl/model/dependencies/ArtifactDependencySpecTest.java
+++ b/android/testSrc/com/android/tools/idea/gradle/dsl/model/dependencies/ArtifactDependencySpecTest.java
@@ -96,4 +96,36 @@
myDependency.extension = "ext";
assertEquals("group:name:version:classifier@ext", myDependency.compactNotation());
}
+
+ @Test
+ public void testEqualsFalse() {
+ myDependency = ArtifactDependencySpec.create("org.gradle.test.classifiers:service:1.0.0-alpha4:jdk15@jar");
+ ArtifactDependencySpec theirDependency =
+ ArtifactDependencySpec.create("org.gradle.test.classifiers:service:1.0.0-beta1:jdk15@jar");
+ assertFalse(myDependency.equals(theirDependency));
+ }
+
+ @Test
+ public void testEqualsTrue() {
+ myDependency = ArtifactDependencySpec.create("org.gradle.test.classifiers:service:1.0.0-alpha4:jdk15@jar");
+ ArtifactDependencySpec theirDependency =
+ ArtifactDependencySpec.create("org.gradle.test.classifiers:service:1.0.0-alpha4:jdk15@jar");
+ assertTrue(myDependency.equals(theirDependency));
+ }
+
+ @Test
+ public void testEqualsIgnoreVersionFalse() {
+ myDependency = ArtifactDependencySpec.create("org.gradle.test.classifiers:service:1.0.0-alpha4:jdk15@jar");
+ ArtifactDependencySpec theirDependency =
+ ArtifactDependencySpec.create("org.gradle.test.classifiers:service:1.0.0-alpha4:jdk16@jar");
+ assertFalse(myDependency.equalsIgnoreVersion(theirDependency));
+ }
+
+ @Test
+ public void testeEqualsIgnoreVersionTrue() {
+ myDependency = ArtifactDependencySpec.create("org.gradle.test.classifiers:service:1.0.0-alpha4:jdk15@jar");
+ ArtifactDependencySpec theirDependency =
+ ArtifactDependencySpec.create("org.gradle.test.classifiers:service:1.0.0-beta1:jdk15@jar");
+ assertTrue(myDependency.equalsIgnoreVersion(theirDependency));
+ }
}
\ No newline at end of file
diff --git a/android/testSrc/com/android/tools/idea/gradle/testing/AndroidJunitPatcherWithTestArtifactTest.java b/android/testSrc/com/android/tools/idea/gradle/testing/AndroidJunitPatcherWithTestArtifactTest.java
index 2cf2027..8d9b61e 100644
--- a/android/testSrc/com/android/tools/idea/gradle/testing/AndroidJunitPatcherWithTestArtifactTest.java
+++ b/android/testSrc/com/android/tools/idea/gradle/testing/AndroidJunitPatcherWithTestArtifactTest.java
@@ -25,7 +25,8 @@
public class AndroidJunitPatcherWithTestArtifactTest extends AndroidGradleTestCase {
- public void testRemoveAndroidTestClasspath() throws Exception {
+ // See http://b.android.com/221883.
+ public void ignore_testRemoveAndroidTestClasspath() throws Exception {
loadProject("projects/sync/multiproject", false);
JUnitPatcher myPatcher = new AndroidJunitPatcher();
diff --git a/android/testSrc/com/android/tools/idea/gradle/testing/TestArtifactSearchScopesTest.java b/android/testSrc/com/android/tools/idea/gradle/testing/TestArtifactSearchScopesTest.java
index ecab3ff..1048414 100644
--- a/android/testSrc/com/android/tools/idea/gradle/testing/TestArtifactSearchScopesTest.java
+++ b/android/testSrc/com/android/tools/idea/gradle/testing/TestArtifactSearchScopesTest.java
@@ -71,7 +71,8 @@
assertFalse(scopes.getAndroidTestExcludeScope().accept(module3RsRoot));
}
- public void testLibrariesExcluding() throws Exception {
+ // See http://b.android.com/221883.
+ public void ignore_testLibrariesExcluding() throws Exception {
TestArtifactSearchScopes scopes = loadMultiProjectAndTestScopes();
LibraryTable libraryTable = LibraryTablesRegistrar.getInstance().getLibraryTable(myFixture.getProject());
diff --git a/android/testSrc/com/android/tools/idea/run/GradleApkProviderTest.java b/android/testSrc/com/android/tools/idea/run/GradleApkProviderTest.java
index f369456..40fd093 100644
--- a/android/testSrc/com/android/tools/idea/run/GradleApkProviderTest.java
+++ b/android/testSrc/com/android/tools/idea/run/GradleApkProviderTest.java
@@ -69,7 +69,7 @@
if (modelVersion != null) {
if (modelVersion.compareIgnoringQualifiers("2.2.0") < 0
// Packaging reverted in alpha4?
- || modelVersion.compareTo("2.2.0-alpha4") >= 0) {
+ || modelVersion.compareTo("2.2.0-alpha4") == 0) {
assertThat(path).endsWith(getName() + "-debug-androidTest-unaligned.apk");
}
else {
diff --git a/android/testSrc/org/jetbrains/android/databinding/GeneratedCodeMatchTest.java b/android/testSrc/org/jetbrains/android/databinding/GeneratedCodeMatchTest.java
index 4dbe588..1d8257d 100644
--- a/android/testSrc/org/jetbrains/android/databinding/GeneratedCodeMatchTest.java
+++ b/android/testSrc/org/jetbrains/android/databinding/GeneratedCodeMatchTest.java
@@ -71,7 +71,7 @@
}
}
- public void ignore_testGeneratedCodeMatch() throws Exception {
+ public void testGeneratedCodeMatch() throws Exception {
File projectFolder = virtualToIoFile(myFixture.getProject().getBaseDir());
createGradlePropertiesFile(projectFolder);
loadProject("projects/projectWithDataBinding");
diff --git a/designer/src/com/android/tools/idea/uibuilder/handlers/constraint/ConstraintModel.java b/designer/src/com/android/tools/idea/uibuilder/handlers/constraint/ConstraintModel.java
index 7c9b10b..8a71977 100644
--- a/designer/src/com/android/tools/idea/uibuilder/handlers/constraint/ConstraintModel.java
+++ b/designer/src/com/android/tools/idea/uibuilder/handlers/constraint/ConstraintModel.java
@@ -528,14 +528,16 @@
}
}
+ boolean saveXML = false;
+
// Make sure the components exist
for (NlComponent component : components) {
- createSolverWidgetFromComponent(component);
+ saveXML |= createSolverWidgetFromComponent(component);
}
// Now update our widget from the list of components...
for (NlComponent component : components) {
- updateSolverWidgetFromComponent(component, deepUpdate);
+ saveXML |= updateSolverWidgetFromComponent(component, deepUpdate);
}
if (USE_GUIDELINES_DURING_DND) {
@@ -547,7 +549,9 @@
}
// Update the ConstraintLayout instances
- updateConstraintLayoutRoots(myWidgetsScene.getRoot());
+ if (!saveXML) {
+ updateConstraintLayoutRoots(myWidgetsScene.getRoot());
+ }
// Finally, layout using our model.
WidgetContainer root = myWidgetsScene.getRoot();
@@ -557,6 +561,9 @@
root.layout();
}
}
+ if (saveXML) {
+ saveToXML(true);
+ }
}
/**
@@ -582,15 +589,17 @@
* Create the widget associated to a component if necessary.
*
* @param component the component we want to represent
+ * @return true if we need to save the XML
*/
- private void createSolverWidgetFromComponent(@NotNull NlComponent component) {
+ private boolean createSolverWidgetFromComponent(@NotNull NlComponent component) {
ConstraintWidget widget = myWidgetsScene.getWidget(component);
+ boolean saveXML = false;
if (widget != null && isConstraintLayout(component)) {
if (!(widget instanceof ConstraintWidgetContainer)) {
if (widget instanceof WidgetContainer) {
ConstraintWidgetContainer container = new ConstraintWidgetContainer();
myWidgetsScene.transformContainerToContainer((WidgetContainer)widget, container);
- setupConstraintWidget(component, container);
+ saveXML = setupConstraintWidget(component, container);
widget = container;
}
else {
@@ -640,7 +649,7 @@
}
}
}
- setupConstraintWidget(component, widget);
+ saveXML |= setupConstraintWidget(component, widget);
myWidgetsScene.setWidget(widget);
if (USE_GUIDELINES_DURING_DND) {
if (dropWidget) {
@@ -652,8 +661,9 @@
}
for (NlComponent child : component.getChildren()) {
- createSolverWidgetFromComponent(child);
+ saveXML |= createSolverWidgetFromComponent(child);
}
+ return saveXML;
}
private static boolean isConstraintLayout(@NotNull NlComponent component) {
@@ -676,8 +686,9 @@
*
* @param component
* @param widget
+ * @return true if we need to save the XML
*/
- private void setupConstraintWidget(@NotNull NlComponent component, ConstraintWidget widget) {
+ private boolean setupConstraintWidget(@NotNull NlComponent component, ConstraintWidget widget) {
WidgetDecorator blueprintDecorator = createDecorator(component, widget);
WidgetDecorator androidDecorator = createDecorator(component, widget);
blueprintDecorator.setStateModel(this);
@@ -692,7 +703,7 @@
companion.setWidgetTag(component);
widget.setCompanionWidget(companion);
widget.setDebugName(component.getId());
- ConstraintUtilities.updateWidget(this, widget, component);
+ return ConstraintUtilities.updateWidget(this, widget, component);
}
/**
@@ -740,7 +751,7 @@
* @param component the component we want to update from
* @param deepUpdate do a thorough update or not
*/
- private void updateSolverWidgetFromComponent(@NotNull NlComponent component, boolean deepUpdate) {
+ private boolean updateSolverWidgetFromComponent(@NotNull NlComponent component, boolean deepUpdate) {
ConstraintWidget widget = myWidgetsScene.getWidget(component);
if (USE_GUIDELINES_DURING_DND) {
if (myDragDropWidget != null) {
@@ -749,22 +760,23 @@
if (companion.getWidgetModel() == component) {
saveToXML(true); // will retrigger an update
myDragDropWidget = null;
- return;
+ return false;
}
}
}
- ConstraintUtilities.updateWidget(this, widget, component);
+ boolean saveXML = ConstraintUtilities.updateWidget(this, widget, component);
for (NlComponent child : component.getChildren()) {
- updateSolverWidgetFromComponent(child, deepUpdate);
+ saveXML |= updateSolverWidgetFromComponent(child, deepUpdate);
}
+ return saveXML;
}
/**
* Traverse the hierarchy to find all ConstraintLayout instances
* and update them. We set all the wrap_content sizes of the ConstraintLayout children
* from layout lib
+ * @param container
*
- * @param container
*/
private void updateConstraintLayoutRoots(WidgetContainer container) {
if (container == null) {
diff --git a/designer/src/com/android/tools/idea/uibuilder/handlers/constraint/ConstraintUtilities.java b/designer/src/com/android/tools/idea/uibuilder/handlers/constraint/ConstraintUtilities.java
index 86421a3..c94e4b3 100644
--- a/designer/src/com/android/tools/idea/uibuilder/handlers/constraint/ConstraintUtilities.java
+++ b/designer/src/com/android/tools/idea/uibuilder/handlers/constraint/ConstraintUtilities.java
@@ -29,6 +29,7 @@
import com.android.tools.sherpa.drawing.decorator.TextWidget;
import com.android.tools.sherpa.drawing.decorator.WidgetDecorator;
import com.android.tools.sherpa.interaction.WidgetInteractionTargets;
+import com.android.tools.sherpa.scout.Scout;
import com.android.tools.sherpa.structure.WidgetCompanion;
import com.android.tools.sherpa.structure.WidgetsScene;
import com.intellij.openapi.application.ApplicationManager;
@@ -39,6 +40,7 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.util.ArrayList;
import java.util.Collection;
/**
@@ -46,6 +48,9 @@
*/
public class ConstraintUtilities {
+ final static int MINIMUM_SIZE = 48; // in dp
+ final static int MINIMUM_SIZE_EXPAND = 6; // in dp
+
/**
* Return the corresponding margin attribute for the given anchor
*
@@ -908,12 +913,13 @@
* @param constraintModel the constraint model we are working with
* @param widget constraint widget
* @param component the model component
+ * @return true if need to save the xml
*/
- static void updateWidget(@NotNull ConstraintModel constraintModel,
+ static boolean updateWidget(@NotNull ConstraintModel constraintModel,
@Nullable ConstraintWidget widget,
@Nullable NlComponent component) {
if (component == null || widget == null) {
- return;
+ return false;
}
AttributesTransaction attributes = component.startAttributeTransaction();
@@ -969,53 +975,7 @@
}
}
- // FIXME: need to agree on the correct magic value for this rather than simply using zero.
- String layout_width = attributes.getAttribute(SdkConstants.ANDROID_URI, SdkConstants.ATTR_LAYOUT_WIDTH);
- if (component.w == 0 || getLayoutDimensionDpValue(component, layout_width) == 0) {
- widget.setHorizontalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.ANY);
- }
- else if (layout_width != null && layout_width.equalsIgnoreCase(SdkConstants.VALUE_WRAP_CONTENT)) {
- widget.setWrapWidth(widget.getWidth());
- widget.setHorizontalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.WRAP_CONTENT);
- }
- else if (layout_width != null && layout_width.equalsIgnoreCase(SdkConstants.VALUE_MATCH_PARENT)) {
- widget.setWrapWidth(widget.getWidth());
- if (isWidgetInsideConstraintLayout(widget)) {
- if (widget.getAnchor(ConstraintAnchor.Type.LEFT).isConnected()
- && widget.getAnchor(ConstraintAnchor.Type.RIGHT).isConnected()) {
- widget.setHorizontalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.ANY);
- }
- else {
- widget.setHorizontalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.FIXED);
- }
- }
- }
- else {
- widget.setHorizontalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.FIXED);
- }
- String layout_height = attributes.getAttribute(SdkConstants.ANDROID_URI, SdkConstants.ATTR_LAYOUT_HEIGHT);
- if (component.h == 0 || getLayoutDimensionDpValue(component, layout_height) == 0) {
- widget.setVerticalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.ANY);
- }
- else if (layout_height != null && layout_height.equalsIgnoreCase(SdkConstants.VALUE_WRAP_CONTENT)) {
- widget.setWrapHeight(widget.getHeight());
- widget.setVerticalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.WRAP_CONTENT);
- }
- else if (layout_height != null && layout_height.equalsIgnoreCase(SdkConstants.VALUE_MATCH_PARENT)) {
- widget.setWrapHeight(widget.getHeight());
- if (isWidgetInsideConstraintLayout(widget)) {
- if ((widget.getAnchor(ConstraintAnchor.Type.TOP).isConnected()
- && widget.getAnchor(ConstraintAnchor.Type.BOTTOM).isConnected())) {
- widget.setVerticalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.ANY);
- }
- else {
- widget.setVerticalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.FIXED);
- }
- }
- }
- else {
- widget.setVerticalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.FIXED);
- }
+ // First set the origin of the widget
int x = constraintModel.pxToDp(component.x);
int y = constraintModel.pxToDp(component.y);
@@ -1058,6 +1018,92 @@
widget.forceUpdateDrawPosition();
}
+ boolean overrideDimension = false;
+
+ // FIXME: need to agree on the correct magic value for this rather than simply using zero.
+ String layout_width = attributes.getAttribute(SdkConstants.ANDROID_URI, SdkConstants.ATTR_LAYOUT_WIDTH);
+ if (component.w == 0 || getLayoutDimensionDpValue(component, layout_width) == 0) {
+ widget.setHorizontalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.ANY);
+ }
+ else if (layout_width != null && layout_width.equalsIgnoreCase(SdkConstants.VALUE_WRAP_CONTENT)) {
+ if (widget.getWidth() < MINIMUM_SIZE && widget instanceof WidgetContainer
+ && ((WidgetContainer) widget).getChildren().size() == 0) {
+ widget.setWidth(MINIMUM_SIZE);
+ widget.setHorizontalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.FIXED);
+ overrideDimension = true;
+ } else {
+ widget.setWrapWidth(widget.getWidth());
+ widget.setHorizontalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.WRAP_CONTENT);
+ }
+ }
+ else if (layout_width != null && layout_width.equalsIgnoreCase(SdkConstants.VALUE_MATCH_PARENT)) {
+ if (isWidgetInsideConstraintLayout(widget)) {
+ if (widget.getAnchor(ConstraintAnchor.Type.LEFT).isConnected()
+ && widget.getAnchor(ConstraintAnchor.Type.RIGHT).isConnected()) {
+ widget.setHorizontalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.ANY);
+ }
+ else {
+ widget.setHorizontalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.FIXED);
+ widget.setWidth(MINIMUM_SIZE_EXPAND);
+ int height = widget.getHeight();
+ ConstraintWidget.DimensionBehaviour verticalBehaviour = widget.getVerticalDimensionBehaviour();
+ if (height <= 1 && widget instanceof WidgetContainer) {
+ widget.setHeight(MINIMUM_SIZE_EXPAND);
+ }
+ ArrayList<ConstraintWidget> widgets = new ArrayList<>();
+ widgets.add(widget);
+ Scout.arrangeWidgets(Scout.Arrange.ExpandHorizontally, widgets, true);
+ widget.setHeight(height);
+ widget.setVerticalDimensionBehaviour(verticalBehaviour);
+ overrideDimension = true;
+ }
+ }
+ }
+ else {
+ widget.setHorizontalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.FIXED);
+ }
+ String layout_height = attributes.getAttribute(SdkConstants.ANDROID_URI, SdkConstants.ATTR_LAYOUT_HEIGHT);
+ if (component.h == 0 || getLayoutDimensionDpValue(component, layout_height) == 0) {
+ widget.setVerticalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.ANY);
+ }
+ else if (layout_height != null && layout_height.equalsIgnoreCase(SdkConstants.VALUE_WRAP_CONTENT)) {
+ if (widget.getHeight() < MINIMUM_SIZE && widget instanceof WidgetContainer
+ && ((WidgetContainer) widget).getChildren().size() == 0) {
+ widget.setHeight(MINIMUM_SIZE);
+ widget.setVerticalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.FIXED);
+ overrideDimension = true;
+ } else {
+ widget.setWrapHeight(widget.getHeight());
+ widget.setVerticalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.WRAP_CONTENT);
+ }
+ }
+ else if (layout_height != null && layout_height.equalsIgnoreCase(SdkConstants.VALUE_MATCH_PARENT)) {
+ if (isWidgetInsideConstraintLayout(widget)) {
+ if ((widget.getAnchor(ConstraintAnchor.Type.TOP).isConnected()
+ && widget.getAnchor(ConstraintAnchor.Type.BOTTOM).isConnected())) {
+ widget.setVerticalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.ANY);
+ }
+ else {
+ widget.setVerticalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.FIXED);
+ widget.setHeight(MINIMUM_SIZE_EXPAND);
+ int width = widget.getWidth();
+ ConstraintWidget.DimensionBehaviour horizontalBehaviour = widget.getHorizontalDimensionBehaviour();
+ if (width <= 1 && widget instanceof WidgetContainer) {
+ widget.setWidth(MINIMUM_SIZE_EXPAND);
+ }
+ ArrayList<ConstraintWidget> widgets = new ArrayList<>();
+ widgets.add(widget);
+ Scout.arrangeWidgets(Scout.Arrange.ExpandVertically, widgets, true);
+ widget.setWidth(width);
+ widget.setHorizontalDimensionBehaviour(horizontalBehaviour);
+ overrideDimension = true;
+ }
+ }
+ }
+ else {
+ widget.setVerticalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.FIXED);
+ }
+
widget.setBaselineDistance(constraintModel.pxToDp(component.getBaseline()));
widget.resetAnchors();
@@ -1136,6 +1182,8 @@
//noinspection ConstantConditions
textWidget.setTextSize(constraintModel.pxToDp(size));
}
+
+ return overrideDimension; // if true, need to update the XML
}
/**
diff --git a/designer/src/com/android/tools/idea/uibuilder/property/NlIdPropertyItem.java b/designer/src/com/android/tools/idea/uibuilder/property/NlIdPropertyItem.java
index 62476b2..fc005f6 100644
--- a/designer/src/com/android/tools/idea/uibuilder/property/NlIdPropertyItem.java
+++ b/designer/src/com/android/tools/idea/uibuilder/property/NlIdPropertyItem.java
@@ -23,6 +23,7 @@
import com.intellij.openapi.ui.DialogBuilder;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlTag;
@@ -157,6 +158,6 @@
}
}
- super.setValue(value != null ? NEW_ID_PREFIX + value : null);
+ super.setValue(!StringUtil.isEmpty(newId) ? NEW_ID_PREFIX + newId : null);
}
}
diff --git a/designer/src/com/android/tools/idea/uibuilder/property/NlPropertiesPanel.java b/designer/src/com/android/tools/idea/uibuilder/property/NlPropertiesPanel.java
index 827cdf2..6d1476b 100644
--- a/designer/src/com/android/tools/idea/uibuilder/property/NlPropertiesPanel.java
+++ b/designer/src/com/android/tools/idea/uibuilder/property/NlPropertiesPanel.java
@@ -46,6 +46,8 @@
public class NlPropertiesPanel extends JPanel implements ViewAllPropertiesAction.Model {
private static final String CARD_ADVANCED = "table";
private static final String CARD_DEFAULT = "default";
+ private static final int VERTICAL_SCROLLING_UNIT_INCREMENT = 50;
+ private static final int VERTICAL_SCROLLING_BLOCK_INCREMENT = 25;
private final PTable myTable;
private final JPanel myTablePanel;
@@ -87,7 +89,10 @@
myCardPanel.add(CARD_DEFAULT, ScrollPaneFactory.createScrollPane(myInspectorPanel,
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER));
- myCardPanel.add(CARD_ADVANCED, ScrollPaneFactory.createScrollPane(myTablePanel));
+ JScrollPane tableScrollPane = ScrollPaneFactory.createScrollPane(myTablePanel);
+ tableScrollPane.getVerticalScrollBar().setUnitIncrement(VERTICAL_SCROLLING_UNIT_INCREMENT);
+ tableScrollPane.getVerticalScrollBar().setBlockIncrement(VERTICAL_SCROLLING_BLOCK_INCREMENT);
+ myCardPanel.add(CARD_ADVANCED, tableScrollPane);
myComponents = Collections.emptyList();
myProperties = Collections.emptyList();
}
diff --git a/designer/src/com/android/tools/idea/uibuilder/property/editors/NlEnumEditor.java b/designer/src/com/android/tools/idea/uibuilder/property/editors/NlEnumEditor.java
index 8684cf4..597f91b 100644
--- a/designer/src/com/android/tools/idea/uibuilder/property/editors/NlEnumEditor.java
+++ b/designer/src/com/android/tools/idea/uibuilder/property/editors/NlEnumEditor.java
@@ -104,6 +104,8 @@
case ATTR_DROPDOWN_WIDTH:
case ATTR_ON_CLICK:
return true;
+ case ATTR_ID:
+ return false;
case ATTR_STYLE:
String tagName = property.getTagName();
return tagName != null && StyleFilter.hasWidgetStyles(property.getModel().getProject(), property.getResolver(), tagName);
diff --git a/designer/testSrc/com/android/tools/idea/uibuilder/palette/PaletteTestCase.java b/designer/testSrc/com/android/tools/idea/uibuilder/palette/PaletteTestCase.java
index 73c803b..9573b47 100644
--- a/designer/testSrc/com/android/tools/idea/uibuilder/palette/PaletteTestCase.java
+++ b/designer/testSrc/com/android/tools/idea/uibuilder/palette/PaletteTestCase.java
@@ -575,7 +575,7 @@
}
public void assertTabItem(@NotNull Palette.BaseItem item) {
- assertStandardView(item, TAB_ITEM, DESIGN_LIB_ARTIFACT, 1.0);
+ assertNoPreviewView(item, TAB_ITEM, DESIGN_LIB_ARTIFACT);
}
public void assertNestedScrollViewItem(@NotNull Palette.BaseItem item) {
@@ -607,7 +607,9 @@
}
public void assertTextInputLayoutItem(@NotNull Palette.BaseItem item) {
- assertLimitedHeightLayout(item, TEXT_INPUT_LAYOUT, DESIGN_LIB_ARTIFACT);
+ checkItem(item, TEXT_INPUT_LAYOUT, STANDARD_VIEW.getTitle(TEXT_INPUT_LAYOUT), STANDARD_LAYOUT.getIcon(TEXT_INPUT_LAYOUT),
+ TEXT_INPUT_LAYOUT_XML, NO_PREVIEW, NO_PREVIEW,
+ DESIGN_LIB_ARTIFACT, NO_SCALE);
}
public void assertCardView(@NotNull Palette.BaseItem item) {
@@ -623,6 +625,17 @@
}
@Language("XML")
+ private static final String TEXT_INPUT_LAYOUT_XML =
+ "<android.support.design.widget.TextInputLayout\n" +
+ " android:layout_width=\"match_parent\"\n" +
+ " android:layout_height=\"wrap_content\">\n" +
+ " <EditText\n" +
+ " android:layout_width=\"match_parent\"\n" +
+ " android:layout_height=\"wrap_content\"\n" +
+ " android:hint=\"hint\" />\n" +
+ " </android.support.design.widget.TextInputLayout>\n";
+
+ @Language("XML")
private static final String TOOLBAR_XML =
"<android.support.v7.widget.Toolbar\n" +
" android:layout_width=\"match_parent\"\n" +