Merge "Adds a timeline data method to save speed samples." into studio-1.4-dev
diff --git a/build-system/gradle-core/src/main/groovy/com/android/build/gradle/AndroidGradleOptions.java b/build-system/gradle-core/src/main/groovy/com/android/build/gradle/AndroidGradleOptions.java
index 89970b9..277f08a 100644
--- a/build-system/gradle-core/src/main/groovy/com/android/build/gradle/AndroidGradleOptions.java
+++ b/build-system/gradle-core/src/main/groovy/com/android/build/gradle/AndroidGradleOptions.java
@@ -82,6 +82,10 @@
return getString(project, AndroidProject.PROPERTY_APK_LOCATION);
}
+ public static boolean isIntegrationTest() {
+ return Boolean.parseBoolean(System.getenv("INTEGRATION_TEST"));
+ }
+
@Nullable
public static Integer getThreadPoolSize(@NonNull Project project) {
Integer size = getInteger(project, PROPERTY_THREAD_POOL_SIZE);
diff --git a/build-system/gradle-core/src/main/groovy/com/android/build/gradle/ReportingPlugin.groovy b/build-system/gradle-core/src/main/groovy/com/android/build/gradle/ReportingPlugin.groovy
index 9871cf7..4f834bd 100644
--- a/build-system/gradle-core/src/main/groovy/com/android/build/gradle/ReportingPlugin.groovy
+++ b/build-system/gradle-core/src/main/groovy/com/android/build/gradle/ReportingPlugin.groovy
@@ -49,7 +49,6 @@
mergeReportsTask.group = JavaBasePlugin.VERIFICATION_GROUP
mergeReportsTask.description = "Merges all the Android test reports from the sub projects."
mergeReportsTask.reportType = ReportType.MULTI_PROJECT
- mergeReportsTask.setVariantName("")
mergeReportsTask.conventionMapping.resultsDir = {
String location = extension.resultsDir != null ?
diff --git a/build-system/gradle-core/src/main/groovy/com/android/build/gradle/internal/TaskManager.java b/build-system/gradle-core/src/main/groovy/com/android/build/gradle/internal/TaskManager.java
index 670c225..a392c57 100644
--- a/build-system/gradle-core/src/main/groovy/com/android/build/gradle/internal/TaskManager.java
+++ b/build-system/gradle-core/src/main/groovy/com/android/build/gradle/internal/TaskManager.java
@@ -1247,7 +1247,7 @@
mainConnectedTask.setDescription("Installs and runs instrumentation "
+ "tests for all flavors on connected devices.");
mainConnectedTask.setReportType(ReportType.MULTI_FLAVOR);
- mainConnectedTask.setVariantName("");
+
ConventionMappingHelper.map(mainConnectedTask, "resultsDir",
new Callable<File>() {
@Override
diff --git a/build-system/gradle-core/src/main/groovy/com/android/build/gradle/internal/tasks/AndroidReportTask.java b/build-system/gradle-core/src/main/groovy/com/android/build/gradle/internal/tasks/AndroidReportTask.java
index 38bb68c..9917600 100644
--- a/build-system/gradle-core/src/main/groovy/com/android/build/gradle/internal/tasks/AndroidReportTask.java
+++ b/build-system/gradle-core/src/main/groovy/com/android/build/gradle/internal/tasks/AndroidReportTask.java
@@ -24,6 +24,7 @@
import com.android.utils.FileUtils;
import com.google.common.collect.Lists;
+import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.OutputDirectory;
@@ -38,7 +39,7 @@
* Task doing test report aggregation.
*/
-public class AndroidReportTask extends BaseTask implements AndroidTestTask {
+public class AndroidReportTask extends DefaultTask implements AndroidTestTask {
private final List<AndroidTestTask> subTasks = Lists.newArrayList();
diff --git a/build-system/gradle-core/src/main/groovy/com/android/build/gradle/internal/tasks/IncrementalTask.java b/build-system/gradle-core/src/main/groovy/com/android/build/gradle/internal/tasks/IncrementalTask.java
index f28be32..e60fa86 100644
--- a/build-system/gradle-core/src/main/groovy/com/android/build/gradle/internal/tasks/IncrementalTask.java
+++ b/build-system/gradle-core/src/main/groovy/com/android/build/gradle/internal/tasks/IncrementalTask.java
@@ -14,10 +14,13 @@
* limitations under the License.
*/
package com.android.build.gradle.internal.tasks;
+import com.android.annotations.NonNull;
import com.android.ide.common.res2.FileStatus;
import com.android.ide.common.res2.SourceSet;
+import com.android.utils.FileUtils;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
+import com.google.common.io.Files;
import org.gradle.api.Action;
import org.gradle.api.tasks.Optional;
@@ -33,6 +36,8 @@
public abstract class IncrementalTask extends BaseTask {
+ public static final String MARKER_NAME = "build_was_incremental";
+
private File incrementalFolder;
public void setIncrementalFolder(File incrementalFolder) {
@@ -44,6 +49,19 @@
return incrementalFolder;
}
+ public void setIncrementalMarker() throws IOException {
+ Files.touch(getIncrementalMarkerFile());
+ }
+
+ public void clearIncrementalMarker() throws IOException {
+ FileUtils.deleteIfExists(getIncrementalMarkerFile());
+ }
+
+ @NonNull
+ private File getIncrementalMarkerFile() {
+ return new File(getIncrementalFolder(), MARKER_NAME);
+ }
+
/**
* Whether this task can support incremental update.
*
diff --git a/build-system/gradle-core/src/main/groovy/com/android/build/gradle/tasks/MergeResources.java b/build-system/gradle-core/src/main/groovy/com/android/build/gradle/tasks/MergeResources.java
index 5805f27..a264d05 100644
--- a/build-system/gradle-core/src/main/groovy/com/android/build/gradle/tasks/MergeResources.java
+++ b/build-system/gradle-core/src/main/groovy/com/android/build/gradle/tasks/MergeResources.java
@@ -18,6 +18,7 @@
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.gradle.AndroidConfig;
+import com.android.build.gradle.AndroidGradleOptions;
import com.android.build.gradle.internal.scope.ConventionMappingHelper;
import com.android.build.gradle.internal.scope.TaskConfigAction;
import com.android.build.gradle.internal.scope.VariantScope;
@@ -128,6 +129,10 @@
@Override
protected void doFullTaskAction() throws IOException {
+ if (AndroidGradleOptions.isIntegrationTest()) {
+ clearIncrementalMarker();
+ }
+
// this is full run, clean the previous output
File destinationDir = getOutputDir();
FileUtils.emptyFolder(destinationDir);
@@ -163,6 +168,10 @@
@Override
protected void doIncrementalTaskAction(Map<File, FileStatus> changedInputs) throws IOException {
+ if (AndroidGradleOptions.isIntegrationTest()) {
+ setIncrementalMarker();
+ }
+
// create a merger and load the known state.
ResourceMerger merger = new ResourceMerger();
try {
diff --git a/build-system/integration-test/build.gradle b/build-system/integration-test/build.gradle
index 36a3527..0463f56 100644
--- a/build-system/integration-test/build.gradle
+++ b/build-system/integration-test/build.gradle
@@ -38,6 +38,7 @@
CUSTOM_JACK: System.env.CUSTOM_JACK,
DEBUG_INNER_TEST: System.env.DEBUG_INNER_TEST,
RECORD_SPANS: System.env.RECORD_SPANS,
+ INTEGRATION_TEST: "true",
].findAll { it.value != null }
// These tasks will not depend on publishLocal, so they will run integration
diff --git a/build-system/integration-test/src/test/groovy/com/android/build/gradle/integration/application/VectorDrawableTest.groovy b/build-system/integration-test/src/test/groovy/com/android/build/gradle/integration/application/VectorDrawableTest.groovy
index 9b39669..43227a9 100644
--- a/build-system/integration-test/src/test/groovy/com/android/build/gradle/integration/application/VectorDrawableTest.groovy
+++ b/build-system/integration-test/src/test/groovy/com/android/build/gradle/integration/application/VectorDrawableTest.groovy
@@ -16,6 +16,7 @@
package com.android.build.gradle.integration.application
import com.android.build.gradle.integration.common.fixture.GradleTestProject
+import com.android.build.gradle.internal.tasks.IncrementalTask
import com.android.utils.FileUtils
import com.google.common.io.Files
import groovy.transform.CompileStatic
@@ -310,7 +311,10 @@
}
private void checkIncrementalBuild() {
- // Do nothing for now, the incremental marker was removed.
- // TODO: remove the method or re-enable incremental markers.
+ def marker = FileUtils.join(
+ project.testDir, "build", "intermediates", "incremental", "mergeResources",
+ "debug", IncrementalTask.MARKER_NAME)
+
+ assertThat(marker).exists()
}
}
diff --git a/common/src/main/java/com/android/utils/FileUtils.java b/common/src/main/java/com/android/utils/FileUtils.java
index 897300a..0243717 100644
--- a/common/src/main/java/com/android/utils/FileUtils.java
+++ b/common/src/main/java/com/android/utils/FileUtils.java
@@ -71,6 +71,20 @@
}
}
+ public static void delete(File file) throws IOException {
+ boolean result = file.delete();
+ if (!result) {
+ throw new IOException("Failed to delete " + file.getAbsolutePath());
+ }
+ }
+
+ public static void deleteIfExists(File file) throws IOException {
+ boolean result = file.delete();
+ if (!result && file.exists()) {
+ throw new IOException("Failed to delete " + file.getAbsolutePath());
+ }
+ }
+
public static File join(File dir, String... paths) {
return new File(dir, Joiner.on(File.separatorChar).join(paths));
}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/AdbHelper.java b/ddmlib/src/main/java/com/android/ddmlib/AdbHelper.java
index b390f93..18767b1 100644
--- a/ddmlib/src/main/java/com/android/ddmlib/AdbHelper.java
+++ b/ddmlib/src/main/java/com/android/ddmlib/AdbHelper.java
@@ -19,7 +19,6 @@
import com.android.annotations.Nullable;
import com.android.ddmlib.log.LogReceiver;
-import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
@@ -75,8 +74,7 @@
* @throws AdbCommandRejectedException if adb rejects the command
*/
public static SocketChannel open(InetSocketAddress adbSockAddr,
- Device device, int devicePort)
- throws IOException, TimeoutException, AdbCommandRejectedException {
+ Device device, int devicePort) throws IOException, TimeoutException, AdbCommandRejectedException {
SocketChannel adbChan = SocketChannel.open(adbSockAddr);
try {
@@ -125,8 +123,7 @@
* @throws IOException in case of I/O error on the connection.
*/
public static SocketChannel createPassThroughConnection(InetSocketAddress adbSockAddr,
- Device device, int pid)
- throws TimeoutException, AdbCommandRejectedException, IOException {
+ Device device, int pid) throws TimeoutException, AdbCommandRejectedException, IOException {
SocketChannel adbChan = SocketChannel.open(adbSockAddr);
try {
@@ -275,8 +272,7 @@
* @throws AdbCommandRejectedException if adb rejects the command
* @throws IOException in case of I/O error on the connection.
*/
- static RawImage getFrameBuffer(InetSocketAddress adbSockAddr, Device device, long timeout,
- TimeUnit unit)
+ static RawImage getFrameBuffer(InetSocketAddress adbSockAddr, Device device, long timeout, TimeUnit unit)
throws TimeoutException, AdbCommandRejectedException, IOException {
RawImage imageParams = new RawImage();
@@ -351,11 +347,9 @@
*/
@Deprecated
static void executeRemoteCommand(InetSocketAddress adbSockAddr,
- String command, IDevice device, IShellOutputReceiver rcvr, int maxTimeToOutputResponse)
- throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
- IOException {
- executeRemoteCommand(adbSockAddr, command, device, rcvr, maxTimeToOutputResponse,
- TimeUnit.MILLISECONDS);
+ String command, IDevice device, IShellOutputReceiver rcvr, int maxTimeToOutputResponse)
+ throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
+ executeRemoteCommand(adbSockAddr, command, device, rcvr, maxTimeToOutputResponse, TimeUnit.MILLISECONDS);
}
/**
@@ -381,9 +375,8 @@
* @see DdmPreferences#getTimeOut()
*/
static void executeRemoteCommand(InetSocketAddress adbSockAddr,
- String command, IDevice device, IShellOutputReceiver rcvr, long maxTimeToOutputResponse,
- TimeUnit maxTimeUnits) throws TimeoutException, AdbCommandRejectedException,
- ShellCommandUnresponsiveException, IOException {
+ String command, IDevice device, IShellOutputReceiver rcvr, long maxTimeToOutputResponse, TimeUnit maxTimeUnits)
+ throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
executeRemoteCommand(adbSockAddr, AdbService.SHELL, command, device, rcvr, maxTimeToOutputResponse,
maxTimeUnits, null /* inputStream */);
@@ -453,8 +446,7 @@
adbChan.configureBlocking(false);
// if the device is not -1, then we first tell adb we're looking to
- // talk
- // to a specific device
+ // talk to a specific device
setDevice(adbChan, device);
byte[] request = formAdbRequest(adbService.name().toLowerCase() + ":" + command); //$NON-NLS-1$
@@ -511,7 +503,11 @@
throw new ShellCommandUnresponsiveException();
}
Thread.sleep(wait);
- } catch (InterruptedException ie) {
+ }
+ catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ // Throw a timeout exception in place of interrupted exception to avoid API changes.
+ throw new TimeoutException("executeRemoteCommand interrupted with immediate timeout via interruption.");
}
} else {
// reset timeout
@@ -594,7 +590,11 @@
} else if (count == 0) {
try {
Thread.sleep(WAIT_TIME * 5);
- } catch (InterruptedException ie) {
+ }
+ catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ // Throw a timeout exception in place of interrupted exception to avoid API changes.
+ throw new TimeoutException("runLogService interrupted with immediate timeout via interruption.");
}
} else {
if (rcvr != null) {
@@ -629,7 +629,7 @@
*/
public static void createForward(InetSocketAddress adbSockAddr, Device device,
String localPortSpec, String remotePortSpec)
- throws TimeoutException, AdbCommandRejectedException, IOException {
+ throws TimeoutException, AdbCommandRejectedException, IOException {
SocketChannel adbChan = null;
try {
@@ -673,7 +673,7 @@
*/
public static void removeForward(InetSocketAddress adbSockAddr, Device device,
String localPortSpec, String remotePortSpec)
- throws TimeoutException, AdbCommandRejectedException, IOException {
+ throws TimeoutException, AdbCommandRejectedException, IOException {
SocketChannel adbChan = null;
try {
@@ -748,8 +748,7 @@
* @param length the length to read or -1 to fill the data buffer completely
* @param timeout The timeout value in ms. A timeout of zero means "wait forever".
*/
- static void read(SocketChannel chan, byte[] data, int length, long timeout)
- throws TimeoutException, IOException {
+ static void read(SocketChannel chan, byte[] data, int length, long timeout) throws TimeoutException, IOException {
ByteBuffer buf = ByteBuffer.wrap(data, 0, length != -1 ? length : data.length);
int numWaits = 0;
@@ -766,10 +765,14 @@
Log.d("ddms", "read: timeout");
throw new TimeoutException();
}
- // non-blocking spin
try {
+ // non-blocking spin
Thread.sleep(WAIT_TIME);
- } catch (InterruptedException ie) {
+ }
+ catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ // Throw a timeout exception in place of interrupted exception to avoid API changes.
+ throw new TimeoutException("Read interrupted with immediate timeout via interruption.");
}
numWaits++;
} else {
@@ -801,8 +804,7 @@
* @throws TimeoutException in case of timeout on the connection.
* @throws IOException in case of I/O error on the connection.
*/
- static void write(SocketChannel chan, byte[] data, int length, int timeout)
- throws TimeoutException, IOException {
+ static void write(SocketChannel chan, byte[] data, int length, int timeout) throws TimeoutException, IOException {
ByteBuffer buf = ByteBuffer.wrap(data, 0, length != -1 ? length : data.length);
int numWaits = 0;
@@ -819,10 +821,14 @@
Log.d("ddms", "write: timeout");
throw new TimeoutException();
}
- // non-blocking spin
try {
+ // non-blocking spin
Thread.sleep(WAIT_TIME);
- } catch (InterruptedException ie) {
+ }
+ catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ // Throw a timeout exception in place of interrupted exception to avoid API changes.
+ throw new TimeoutException("Write interrupted with immediate timeout via interruption.");
}
numWaits++;
} else {
@@ -866,8 +872,8 @@
* @throws AdbCommandRejectedException if adb rejects the command
* @throws IOException in case of I/O error on the connection.
*/
- public static void reboot(String into, InetSocketAddress adbSockAddr,
- Device device) throws TimeoutException, AdbCommandRejectedException, IOException {
+ public static void reboot(String into, InetSocketAddress adbSockAddr, Device device)
+ throws TimeoutException, AdbCommandRejectedException, IOException {
byte[] request;
if (into == null) {
request = formAdbRequest("reboot:"); //$NON-NLS-1$
diff --git a/ddmlib/src/main/java/com/android/ddmlib/Device.java b/ddmlib/src/main/java/com/android/ddmlib/Device.java
index 1cb1a4c..5f61fe4 100644
--- a/ddmlib/src/main/java/com/android/ddmlib/Device.java
+++ b/ddmlib/src/main/java/com/android/ddmlib/Device.java
@@ -395,17 +395,23 @@
return mHardwareCharacteristics.contains(feature.getCharacteristic());
}
- private int getApiLevel() {
+ @Override
+ public int getApiLevel() {
if (mApiLevel > 0) {
return mApiLevel;
}
+ String buildApi = getProperty(PROP_BUILD_API_LEVEL);
+ if (buildApi == null) {
+ throw new IllegalStateException("Unexpected error: Device does not have a build API level.");
+ }
+
try {
- String buildApi = getProperty(PROP_BUILD_API_LEVEL);
- mApiLevel = buildApi == null ? -1 : Integer.parseInt(buildApi);
+ mApiLevel = Integer.parseInt(buildApi);
return mApiLevel;
- } catch (Exception e) {
- return -1;
+ }
+ catch (NumberFormatException e) {
+ throw new IllegalStateException("Unexpected error: Build API level '" + buildApi + "' is not an integer: ");
}
}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/IDevice.java b/ddmlib/src/main/java/com/android/ddmlib/IDevice.java
index be45449..1e86e02 100644
--- a/ddmlib/src/main/java/com/android/ddmlib/IDevice.java
+++ b/ddmlib/src/main/java/com/android/ddmlib/IDevice.java
@@ -636,4 +636,9 @@
* @return the user's region, or null if it's unknown
*/
String getRegion();
-}
\ No newline at end of file
+
+ /**
+ * Returns the API level of the device.
+ */
+ int getApiLevel();
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/NativeStackCallInfo.java b/ddmlib/src/main/java/com/android/ddmlib/NativeStackCallInfo.java
index 72d7c77..4785833 100644
--- a/ddmlib/src/main/java/com/android/ddmlib/NativeStackCallInfo.java
+++ b/ddmlib/src/main/java/com/android/ddmlib/NativeStackCallInfo.java
@@ -65,7 +65,7 @@
} catch (NumberFormatException e) {
// do nothing, the line number will stay at -1
}
- if (m.groupCount() == 3) {
+ if (m.groupCount() == 3 && m.group(3) != null) {
// A discriminator was found, add that in the source file name.
mSourceFile += m.group(3);
}
diff --git a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/GradleDetector.java b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/GradleDetector.java
index 33e49e6..b99b81c 100644
--- a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/GradleDetector.java
+++ b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/GradleDetector.java
@@ -16,6 +16,7 @@
package com.android.tools.lint.checks;
import static com.android.SdkConstants.FD_BUILD_TOOLS;
+import static com.android.SdkConstants.FN_BUILD_GRADLE;
import static com.android.SdkConstants.GRADLE_PLUGIN_MINIMUM_VERSION;
import static com.android.SdkConstants.GRADLE_PLUGIN_RECOMMENDED_VERSION;
import static com.android.ide.common.repository.GradleCoordinate.COMPARE_PLUS_HIGHER;
@@ -27,6 +28,7 @@
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
+import com.android.builder.model.AndroidArtifact;
import com.android.builder.model.AndroidLibrary;
import com.android.builder.model.Dependencies;
import com.android.builder.model.MavenCoordinates;
@@ -43,12 +45,16 @@
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
+import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.detector.api.TextFormat;
+import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
+import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
import java.io.BufferedReader;
import java.io.File;
@@ -59,9 +65,11 @@
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* Checks Gradle files for potential errors
@@ -241,6 +249,9 @@
/** Previous plugin id for libraries */
public static final String OLD_LIB_PLUGIN_ID = "android-library";
+ /** Group ID for GMS */
+ public static final String GMS_GROUP_ID = "com.google.android.gms";
+
private int mMinSdkVersion;
private int mCompileSdkVersion;
private int mTargetSdkVersion;
@@ -794,7 +805,7 @@
+ "compileSdkVersion < 21 is not necessary");
}
return;
- } else if ("com.google.android.gms".equals(dependency.getGroupId())
+ } else if (GMS_GROUP_ID.equals(dependency.getGroupId())
&& dependency.getArtifactId() != null) {
// 5.2.08 is not supported; special case and warn about this
@@ -889,13 +900,18 @@
@Nullable
private static PreciseRevision getLatestVersionFromRemoteRepo(@NonNull LintClient client,
@NonNull GradleCoordinate dependency, boolean firstRowOnly, boolean allowPreview) {
+ String groupId = dependency.getGroupId();
+ String artifactId = dependency.getArtifactId();
+ if (groupId == null || artifactId == null) {
+ return null;
+ }
StringBuilder query = new StringBuilder();
String encoding = UTF_8.name();
try {
query.append("http://search.maven.org/solrsearch/select?q=g:%22");
- query.append(URLEncoder.encode(dependency.getGroupId(), encoding));
+ query.append(URLEncoder.encode(groupId, encoding));
query.append("%22+AND+a:%22");
- query.append(URLEncoder.encode(dependency.getArtifactId(), encoding));
+ query.append(URLEncoder.encode(artifactId, encoding));
} catch (UnsupportedEncodingException ee) {
return null;
}
@@ -1071,6 +1087,13 @@
}
}
+ /**
+ * If incrementally editing a single build.gradle file, tracks whether we've already
+ * transitively checked GMS versions such that we don't flag the same error on every
+ * single dependency declaration
+ */
+ private boolean mCheckedGms;
+
private void checkPlayServices(Context context, GradleCoordinate dependency, Object cookie) {
String groupId = dependency.getGroupId();
String artifactId = dependency.getArtifactId();
@@ -1087,6 +1110,91 @@
checkLocalMavenVersions(context, dependency, cookie, groupId, artifactId,
repository);
}
+
+ if (!mCheckedGms) {
+ mCheckedGms = true;
+ // Incremental analysis only? If so, tie the check to
+ // a specific GMS play dependency if only, such that it's highlighted
+ // in the editor
+ if (!context.getScope().contains(Scope.ALL_RESOURCE_FILES)) {
+ // Incremental editing: try flagging them in this file!
+ checkConsistentPlayServices(context, cookie);
+ }
+ }
+ }
+
+ private void checkConsistentPlayServices(@NonNull Context context,
+ @Nullable Object cookie) {
+ // Make sure we're using a consistent version across all play services libraries
+ // (b/22709708)
+
+ Project project = context.getMainProject();
+ if (!project.isGradleProject()) {
+ return;
+ }
+ Variant variant = project.getCurrentVariant();
+ if (variant == null) {
+ return;
+ }
+ AndroidArtifact artifact = variant.getMainArtifact();
+ Collection<AndroidLibrary> libraries = artifact.getDependencies().getLibraries();
+ Multimap<String, MavenCoordinates> versionToCoordinate = ArrayListMultimap.create();
+ for (AndroidLibrary library : libraries) {
+ addGmsLibraryVersions(versionToCoordinate, library);
+ }
+ Set<String> versions = versionToCoordinate.keySet();
+ if (versions.size() > 1) {
+ List<String> sortedVersions = Lists.newArrayList(versions);
+ Collections.sort(sortedVersions, Collections.reverseOrder());
+ MavenCoordinates c1 = versionToCoordinate.get(sortedVersions.get(0)).iterator().next();
+ MavenCoordinates c2 = versionToCoordinate.get(sortedVersions.get(1)).iterator().next();
+ // Not using toString because in the IDE, these are model proxies which display garbage output
+ String example1 = c1.getGroupId() + ":" + c1.getArtifactId() + ":" + c1 .getVersion();
+ String example2 = c2.getGroupId() + ":" + c2.getArtifactId() + ":" + c2 .getVersion();
+ String message = "All com.google.android.gms libraries must use the exact same "
+ + "version specification (mixing versions can lead to runtime crashes). "
+ + "Found versions " + Joiner.on(", ").join(sortedVersions) + ". "
+ + "Examples include " + example1 + " and " + example2;
+
+ if (cookie != null) {
+ report(context, cookie, COMPATIBILITY, message);
+ } else {
+ // Associate the error with the top level build.gradle file, if found
+ // (if not, fall back to the project directory). This is necessary because
+ // we're doing this analysis based on the Gradle interpreted model, not from
+ // parsing Gradle files - and the model doesn't provide source positions.
+ File dir = context.getProject().getDir();
+ Location location;
+ File topLevel = new File(dir, FN_BUILD_GRADLE);
+ if (topLevel.exists()) {
+ location = Location.create(topLevel);
+ } else {
+ location = Location.create(dir);
+ }
+ context.report(COMPATIBILITY, location, message);
+ }
+ }
+ }
+
+ private static void addGmsLibraryVersions(@NonNull Multimap<String, MavenCoordinates> versions,
+ @NonNull AndroidLibrary library) {
+ MavenCoordinates coordinates = library.getResolvedCoordinates();
+ if (coordinates != null && coordinates.getGroupId().equals(GMS_GROUP_ID)) {
+ versions.put(coordinates.getVersion(), coordinates);
+ }
+
+ for (AndroidLibrary dependency : library.getLibraryDependencies()) {
+ addGmsLibraryVersions(versions, dependency);
+ }
+ }
+
+ @Override
+ public void afterCheckProject(@NonNull Context context) {
+ if (context.getProject() == context.getMainProject() &&
+ // Full analysis? Don't tie check to any specific Gradle DSL element
+ context.getScope().contains(Scope.ALL_RESOURCE_FILES)) {
+ checkConsistentPlayServices(context, null);
+ }
}
private void checkLocalMavenVersions(Context context, GradleCoordinate dependency,
diff --git a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ManifestDetector.java b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ManifestDetector.java
index 66537f5..decacff 100644
--- a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ManifestDetector.java
+++ b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ManifestDetector.java
@@ -39,6 +39,7 @@
import static com.android.SdkConstants.TAG_USES_LIBRARY;
import static com.android.SdkConstants.TAG_USES_PERMISSION;
import static com.android.SdkConstants.TAG_USES_SDK;
+import static com.android.SdkConstants.VALUE_FALSE;
import static com.android.xml.AndroidManifest.NODE_ACTION;
import static com.android.xml.AndroidManifest.NODE_DATA;
import static com.android.xml.AndroidManifest.NODE_METADATA;
@@ -193,6 +194,15 @@
Severity.FATAL,
IMPLEMENTATION);
+ /**
+ * Documentation URL for app backup.
+ * <p>
+ * TODO: Replace with stable API doc reference once this moves out of preview
+ * (tracked in https://code.google.com/p/android/issues/detail?id=182113)
+ */
+ private static final String BACKUP_DOCUMENTATION_URL
+ = "https://developer.android.com/preview/backup/index.html";
+
/** Not explicitly defining allowBackup */
public static final Issue ALLOW_BACKUP = Issue.create(
"AllowBackup", //$NON-NLS-1$
@@ -223,8 +233,9 @@
Category.SECURITY,
3,
Severity.WARNING,
- IMPLEMENTATION).addMoreInfo(
- "http://developer.android.com/reference/android/R.attr.html#allowBackup");
+ IMPLEMENTATION)
+ .addMoreInfo(BACKUP_DOCUMENTATION_URL)
+ .addMoreInfo("http://developer.android.com/reference/android/R.attr.html#allowBackup");
/** Conflicting permission names */
public static final Issue UNIQUE_PERMISSION = Issue.create(
@@ -799,7 +810,8 @@
if (tag.equals(TAG_APPLICATION)) {
mSeenApplication = true;
boolean recordLocation = false;
- if (element.hasAttributeNS(ANDROID_URI, ATTR_ALLOW_BACKUP)
+ String allowBackup = element.getAttributeNS(ANDROID_URI, ATTR_ALLOW_BACKUP);
+ if (allowBackup != null && !allowBackup.isEmpty()
|| context.getDriver().isSuppressed(context, ALLOW_BACKUP, element)) {
mSeenAllowBackup = true;
} else {
@@ -830,18 +842,27 @@
context.report(ALLOW_BACKUP, fullBackupNode, location,
"Missing `<full-backup-content>` resource");
}
- } else if (fullBackupNode == null && context.getMainProject().getTargetSdk() >= 23) {
- Location location = context.getLocation(element);
- context.report(ALLOW_BACKUP, element, location,
- "Should explicitly set `android:fullBackupContent` to `true` or `false` "
- + "to opt-in to or out of full app data back-up and restore, or "
- + "alternatively to an `@xml` resource which specifies which "
- + "files to backup");
- } else if (fullBackupNode == null && hasGcmReceiver(element)) {
- Location location = context.getLocation(element);
- context.report(ALLOW_BACKUP, element, location,
- "Should explicitly set `android:fullBackupContent` to avoid backing up "
- + "the GCM device specific regId.");
+ } else if (fullBackupNode == null && !VALUE_FALSE.equals(allowBackup)
+ && context.getMainProject().getTargetSdk() >= 23) {
+ if (hasGcmReceiver(element)) {
+ Location location = context.getLocation(element);
+ context.report(ALLOW_BACKUP, element, location, ""
+ + "On SDK version 23 and up, your app data will be automatically "
+ + "backed up, and restored on app install. Your GCM regid will not "
+ + "work across restores, so you must ensure that it is excluded "
+ + "from the back-up set. Use the attribute "
+ + "`android:fullBackupContent` to specify an `@xml` resource which "
+ + "configures which files to backup. More info: "
+ + BACKUP_DOCUMENTATION_URL);
+ } else {
+ Location location = context.getLocation(element);
+ context.report(ALLOW_BACKUP, element, location, ""
+ + "On SDK version 23 and up, your app data will be automatically "
+ + "backed up and restored on app install. Consider adding the "
+ + "attribute `android:fullBackupContent` to specify an `@xml` "
+ + "resource which configures which files to backup. More info: "
+ + BACKUP_DOCUMENTATION_URL);
+ }
}
} else if (mSeenApplication) {
if (context.isEnabled(ORDER)) {
diff --git a/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/GradleDetectorTest.java b/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/GradleDetectorTest.java
index 71e07a2..4ac43b7 100644
--- a/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/GradleDetectorTest.java
+++ b/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/GradleDetectorTest.java
@@ -76,6 +76,7 @@
import java.io.File;
import java.io.IOException;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -83,8 +84,9 @@
import java.util.Set;
/**
- * <b>NOTE</b>: Most GradleDetector unit tests are in the Studio plugin, as tests
- * for IntellijGradleDetector
+ * NOTE: Many of these tests are duplicated in the Android Studio plugin to
+ * test the custom GradleDetector subclass, IntellijGradleDetector, which
+ * customizes some behavior to be based on top of PSI rather than the Groovy parser.
*/
public class GradleDetectorTest extends AbstractCheckTest {
@@ -526,7 +528,6 @@
lintProject("gradle/PreviewDependencies.gradle=>build.gradle"));
}
-
public void testDependenciesInVariables() throws Exception {
mEnabled = Collections.singleton(DEPENDENCY);
assertEquals(""
@@ -538,6 +539,37 @@
lintProject("gradle/DependenciesVariable.gradle=>build.gradle"));
}
+ public void testPlayServiceConsistency() throws Exception {
+ // Requires custom model mocks
+ mEnabled = Collections.singleton(COMPATIBILITY);
+ assertEquals(""
+ + "build.gradle:4: Error: All com.google.android.gms libraries must use the "
+ + "exact same version specification (mixing versions can lead to runtime "
+ + "crashes). Found versions 7.5.0, 7.3.0. Examples include "
+ + "com.google.android.gms:play-services-wearable:7.5.0 and "
+ + "com.google.android.gms:play-services-location:7.3.0 [GradleCompatible]\n"
+ + " compile 'com.google.android.gms:play-services-wearable:7.5.0'\n"
+ + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ + "1 errors, 0 warnings\n",
+
+ lintProjectIncrementally("build.gradle",
+ "gradle/PlayServices2.gradle=>build.gradle"));
+ }
+
+ public void testPlayServiceConsistencyNonIncremental() throws Exception {
+ // Requires custom model mocks
+ mEnabled = Collections.singleton(COMPATIBILITY);
+ assertEquals(""
+ + "build.gradle: Error: All com.google.android.gms libraries must use "
+ + "the exact same version specification (mixing versions can lead to "
+ + "runtime crashes). Found versions 7.5.0, 7.3.0. Examples include "
+ + "com.google.android.gms:play-services-wearable:7.5.0 and "
+ + "com.google.android.gms:play-services-location:7.3.0 [GradleCompatible]\n"
+ + "1 errors, 0 warnings\n",
+
+ lintProject("gradle/PlayServices2.gradle=>build.gradle"));
+ }
+
@Override
protected void checkReportedError(@NonNull Context context, @NonNull Issue issue,
@NonNull Severity severity, @Nullable Location location, @NonNull String message) {
@@ -638,11 +670,8 @@
@NonNull
@Override
protected Project createProject(@NonNull File dir, @NonNull File referenceDir) {
- if (!"testDependenciesInVariables".equals(getName())) {
- return super.createProject(dir, referenceDir);
- }
-
- return new Project(this, dir, referenceDir) {
+ if ("testDependenciesInVariables".equals(getName())) {
+ return new Project(this, dir, referenceDir) {
@Override
public boolean isGradleProject() {
return true;
@@ -676,7 +705,64 @@
when(variant.getMainArtifact()).thenReturn(artifact);
return variant;
}
- };
+ };
+ }
+
+ if ("testPlayServiceConsistency".equals(getName())
+ || "testPlayServiceConsistencyNonIncremental".equals(getName())) {
+ return new Project(this, dir, referenceDir) {
+ @Override
+ public boolean isGradleProject() {
+ return true;
+ }
+
+ @Nullable
+ @Override
+ public Variant getCurrentVariant() {
+ /*
+ Simulate variant which has an AndroidLibrary with
+ resolved coordinates
+
+ b//22709708
+ com.google.android.gms:play-services-location:7.3.0
+ com.google.android.gms:play-services-wearable:7.5.0
+ */
+ MavenCoordinates coordinates1 = mock(MavenCoordinates.class);
+ when(coordinates1.getGroupId()).thenReturn("com.google.android.gms");
+ when(coordinates1.getArtifactId()).thenReturn("play-services-location");
+ when(coordinates1.getVersion()).thenReturn("7.3.0");
+ when(coordinates1.toString()).thenReturn("com.google.android.gms:play-services-location:7.3.0");
+
+ MavenCoordinates coordinates2 = mock(MavenCoordinates.class);
+ when(coordinates2.getGroupId()).thenReturn("com.google.android.gms");
+ when(coordinates2.getArtifactId()).thenReturn("play-services-wearable");
+ when(coordinates2.getVersion()).thenReturn("7.5.0");
+ when(coordinates2.toString()).thenReturn("com.google.android.gms:play-services-wearable:7.5.0");
+
+ AndroidLibrary library1 = mock(AndroidLibrary.class);
+ when(library1.getResolvedCoordinates()).thenReturn(coordinates1);
+ when(library1.getLintJar()).thenReturn(new File("lint.jar"));
+
+ AndroidLibrary library2 = mock(AndroidLibrary.class);
+ when(library2.getResolvedCoordinates()).thenReturn(coordinates2);
+ when(library2.getLintJar()).thenReturn(new File("lint.jar"));
+
+ List<AndroidLibrary> libraries = Arrays.asList(library1, library2);
+
+ Dependencies dependencies = mock(Dependencies.class);
+ when(dependencies.getLibraries()).thenReturn(libraries);
+
+ AndroidArtifact artifact = mock(AndroidArtifact.class);
+ when(artifact.getDependencies()).thenReturn(dependencies);
+
+ Variant variant = mock(Variant.class);
+ when(variant.getMainArtifact()).thenReturn(artifact);
+ return variant;
+ }
+ };
+ }
+
+ return super.createProject(dir, referenceDir);
}
};
}
diff --git a/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/ManifestDetectorTest.java b/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/ManifestDetectorTest.java
index 31fedb1..db62652 100644
--- a/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/ManifestDetectorTest.java
+++ b/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/ManifestDetectorTest.java
@@ -636,7 +636,11 @@
public void testMissingBackupInTarget23() throws Exception {
mEnabled = Collections.singleton(ManifestDetector.ALLOW_BACKUP);
assertEquals(""
- + "AndroidManifest.xml:5: Warning: Should explicitly set android:fullBackupContent to true or false to opt-in to or out of full app data back-up and restore, or alternatively to an @xml resource which specifies which files to backup [AllowBackup]\n"
+ + "AndroidManifest.xml:5: Warning: On SDK version 23 and up, your app data will "
+ + "be automatically backed up and restored on app install. Consider "
+ + "adding the attribute android:fullBackupContent to specify an @xml "
+ + "resource which configures which files to backup. More info: "
+ + "https://developer.android.com/preview/backup/index.html [AllowBackup]\n"
+ " <application\n"
+ " ^\n"
+ "0 errors, 1 warnings\n",
@@ -656,6 +660,25 @@
+ "</manifest>\n")));
}
+ public void testMissingBackupInPreTarget23() throws Exception {
+ mEnabled = Collections.singleton(ManifestDetector.ALLOW_BACKUP);
+ assertEquals("No warnings.",
+
+ lintProject(
+ xml("AndroidManifest.xml", ""
+ + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
+ + " package=\"com.example.helloworld\" >\n"
+ + " <uses-sdk android:targetSdkVersion=\"21\" />"
+ + "\n"
+ + " <application\n"
+ + " android:label=\"@string/app_name\"\n"
+ + " android:theme=\"@style/AppTheme\" >\n"
+ + " </application>\n"
+ + "\n"
+ + "</manifest>\n")));
+ }
+
public void testMissingBackupWithoutGcmPreTarget23() throws Exception {
mEnabled = Collections.singleton(ManifestDetector.ALLOW_BACKUP);
assertEquals("No warnings.",
@@ -675,10 +698,14 @@
+ "</manifest>\n")));
}
- public void testMissingBackupWithGcmPreTarget23() throws Exception {
+ public void testMissingBackupWithoutGcmPostTarget23() throws Exception {
mEnabled = Collections.singleton(ManifestDetector.ALLOW_BACKUP);
assertEquals(""
- + "AndroidManifest.xml:5: Warning: Should explicitly set android:fullBackupContent to avoid backing up the GCM device specific regId. [AllowBackup]\n"
+ + "AndroidManifest.xml:5: Warning: On SDK version 23 and up, your app "
+ + "data will be automatically backed up and restored on app install. "
+ + "Consider adding the attribute android:fullBackupContent to specify "
+ + "an @xml resource which configures which files to backup. "
+ + "More info: https://developer.android.com/preview/backup/index.html [AllowBackup]\n"
+ " <application\n"
+ " ^\n"
+ "0 errors, 1 warnings\n",
@@ -688,6 +715,25 @@
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
+ " package=\"com.example.helloworld\" >\n"
+ + " <uses-sdk android:targetSdkVersion=\"23\" />"
+ + "\n"
+ + " <application\n"
+ + " android:label=\"@string/app_name\"\n"
+ + " android:theme=\"@style/AppTheme\" >\n"
+ + " </application>\n"
+ + "\n"
+ + "</manifest>\n")));
+ }
+
+ public void testMissingBackupWithGcmPreTarget23() throws Exception {
+ mEnabled = Collections.singleton(ManifestDetector.ALLOW_BACKUP);
+ assertEquals("No warnings.",
+
+ lintProject(
+ xml("AndroidManifest.xml", ""
+ + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
+ + " package=\"com.example.helloworld\" >\n"
+ " <uses-sdk android:targetSdkVersion=\"21\" />"
+ "\n"
+ " <application\n"
@@ -706,6 +752,72 @@
+ "</manifest>\n")));
}
+ public void testMissingBackupWithGcmPostTarget23() throws Exception {
+ mEnabled = Collections.singleton(ManifestDetector.ALLOW_BACKUP);
+ assertEquals(""
+ + "AndroidManifest.xml:5: Warning: On SDK version 23 and up, your app "
+ + "data will be automatically backed up, and restored on app install. "
+ + "Your GCM regid will not work across restores, so you must ensure that "
+ + "it is excluded from the back-up set. Use the attribute "
+ + "android:fullBackupContent to specify an @xml resource which "
+ + "configures which files to backup. More info: "
+ + "https://developer.android.com/preview/backup/index.html [AllowBackup]\n"
+ + " <application\n"
+ + " ^\n"
+ + "0 errors, 1 warnings\n",
+
+ lintProject(
+ xml("AndroidManifest.xml", ""
+ + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
+ + " package=\"com.example.helloworld\" >\n"
+ + " <uses-sdk android:targetSdkVersion=\"23\" />"
+ + "\n"
+ + " <application\n"
+ + " android:label=\"@string/app_name\"\n"
+ + " android:theme=\"@style/AppTheme\" >"
+ + " <receiver\n"
+ + " android:name=\".GcmBroadcastReceiver\"\n"
+ + " android:permission=\"com.google.android.c2dm.permission.SEND\" >\n"
+ + " <intent-filter>\n"
+ + " <action android:name=\"com.google.android.c2dm.intent.RECEIVE\" />\n"
+ + " <category android:name=\"com.example.gcm\" />\n"
+ + " </intent-filter>\n"
+ + " </receiver>\n"
+ + " </application>\n"
+ + "\n"
+ + "</manifest>\n")));
+ }
+
+ public void testNoMissingFullBackupWithDoNotAllowBackup() throws Exception {
+ // Regression test for https://code.google.com/p/android/issues/detail?id=181805
+ mEnabled = Collections.singleton(ManifestDetector.ALLOW_BACKUP);
+ assertEquals("No warnings.",
+
+ lintProject(
+ xml("AndroidManifest.xml", ""
+ + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
+ + " package=\"com.example.helloworld\" >\n"
+ + " <uses-sdk android:targetSdkVersion=\"21\" />"
+ + "\n"
+ + " <application\n"
+ + " android:label=\"@string/app_name\"\n"
+ + " android:allowBackup=\"false\"\n"
+ + " android:theme=\"@style/AppTheme\" >"
+ + " <receiver\n"
+ + " android:name=\".GcmBroadcastReceiver\"\n"
+ + " android:permission=\"com.google.android.c2dm.permission.SEND\" >\n"
+ + " <intent-filter>\n"
+ + " <action android:name=\"com.google.android.c2dm.intent.RECEIVE\" />\n"
+ + " <category android:name=\"com.example.gcm\" />\n"
+ + " </intent-filter>\n"
+ + " </receiver>\n"
+ + " </application>\n"
+ + "\n"
+ + "</manifest>\n")));
+ }
+
// Custom project which locates all manifest files in the project rather than just
// being hardcoded to the root level
diff --git a/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/data/gradle/PlayServices2.gradle b/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/data/gradle/PlayServices2.gradle
new file mode 100644
index 0000000..34f329d
--- /dev/null
+++ b/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/data/gradle/PlayServices2.gradle
@@ -0,0 +1,6 @@
+apply plugin: 'android'
+
+dependencies {
+ compile 'com.google.android.gms:play-services-wearable:7.5.0'
+ compile 'com.google.android.gms:play-services-location:7.3.0'
+}
diff --git a/sdk-common/src/main/java/com/android/ide/common/resources/ResourceResolver.java b/sdk-common/src/main/java/com/android/ide/common/resources/ResourceResolver.java
index 23f471b..ca1c93d 100644
--- a/sdk-common/src/main/java/com/android/ide/common/resources/ResourceResolver.java
+++ b/sdk-common/src/main/java/com/android/ide/common/resources/ResourceResolver.java
@@ -853,7 +853,7 @@
@Override
public ResourceValue findResValue(String reference, boolean forceFrameworkOnly) {
- if (!mLookupChain.isEmpty() && reference.startsWith(PREFIX_RESOURCE_REF)) {
+ if (!mLookupChain.isEmpty() && reference != null && reference.startsWith(PREFIX_RESOURCE_REF)) {
ResourceValue prev = mLookupChain.get(mLookupChain.size() - 1);
if (!reference.equals(prev.getValue())) {
ResourceValue next = new ResourceValue(prev.getResourceType(), prev.getName(),