| /* |
| * Copyright (C) 2011 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.lint.checks; |
| |
| import static com.android.SdkConstants.ANDROID_MANIFEST_XML; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.when; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.builder.model.AndroidProject; |
| import com.android.builder.model.ApiVersion; |
| import com.android.builder.model.BuildType; |
| import com.android.builder.model.BuildTypeContainer; |
| import com.android.builder.model.ProductFlavor; |
| import com.android.builder.model.ProductFlavorContainer; |
| import com.android.builder.model.SourceProvider; |
| import com.android.builder.model.SourceProviderContainer; |
| import com.android.builder.model.Variant; |
| import com.android.tools.lint.client.api.LintClient; |
| import com.android.tools.lint.detector.api.Detector; |
| import com.android.tools.lint.detector.api.Issue; |
| import com.android.tools.lint.detector.api.Project; |
| import com.google.common.collect.Lists; |
| |
| import java.io.File; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| @SuppressWarnings("javadoc") |
| public class ManifestDetectorTest extends AbstractCheckTest { |
| @Override |
| protected Detector getDetector() { |
| return new ManifestDetector(); |
| } |
| |
| private Set<Issue> mEnabled = new HashSet<Issue>(); |
| |
| @Override |
| protected TestConfiguration getConfiguration(LintClient client, Project project) { |
| return new TestConfiguration(client, project, null) { |
| @Override |
| public boolean isEnabled(@NonNull Issue issue) { |
| return super.isEnabled(issue) && mEnabled.contains(issue); |
| } |
| }; |
| } |
| |
| public void testOrderOk() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.ORDER); |
| assertEquals( |
| "No warnings.", |
| lintProject( |
| "AndroidManifest.xml", |
| "res/values/strings.xml")); |
| } |
| |
| public void testBrokenOrder() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.ORDER); |
| assertEquals( |
| "AndroidManifest.xml:16: Warning: <uses-sdk> tag appears after <application> tag [ManifestOrder]\n" + |
| " <uses-sdk android:minSdkVersion=\"Froyo\" />\n" + |
| " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + |
| "0 errors, 1 warnings\n" + |
| "", |
| |
| lintProject( |
| "broken-manifest.xml=>AndroidManifest.xml", |
| "res/values/strings.xml")); |
| } |
| |
| public void testMissingUsesSdk() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.USES_SDK); |
| assertEquals( |
| "AndroidManifest.xml: Warning: Manifest should specify a minimum API level with <uses-sdk android:minSdkVersion=\"?\" />; if it really supports all versions of Android set it to 1. [UsesMinSdkAttributes]\n" + |
| "0 errors, 1 warnings\n", |
| lintProject( |
| "missingusessdk.xml=>AndroidManifest.xml", |
| "res/values/strings.xml")); |
| } |
| |
| public void testMissingUsesSdkInGradle() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.SET_VERSION); |
| assertEquals("" |
| + "No warnings.", |
| lintProject("missingusessdk.xml=>AndroidManifest.xml", |
| "multiproject/library.properties=>build.gradle")); // dummy; only name counts |
| } |
| |
| public void testMissingMinSdk() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.USES_SDK); |
| assertEquals( |
| "AndroidManifest.xml:7: Warning: <uses-sdk> tag should specify a minimum API level with android:minSdkVersion=\"?\" [UsesMinSdkAttributes]\n" + |
| " <uses-sdk android:targetSdkVersion=\"10\" />\n" + |
| " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + |
| "0 errors, 1 warnings\n" + |
| "", |
| lintProject( |
| "missingmin.xml=>AndroidManifest.xml", |
| "res/values/strings.xml")); |
| } |
| |
| public void testMissingTargetSdk() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.USES_SDK); |
| assertEquals( |
| "AndroidManifest.xml:7: Warning: <uses-sdk> tag should specify a target API level (the highest verified version; when running on later versions, compatibility behaviors may be enabled) with android:targetSdkVersion=\"?\" [UsesMinSdkAttributes]\n" + |
| " <uses-sdk android:minSdkVersion=\"10\" />\n" + |
| " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + |
| "0 errors, 1 warnings\n", |
| lintProject( |
| "missingtarget.xml=>AndroidManifest.xml", |
| "res/values/strings.xml")); |
| } |
| |
| public void testOldTargetSdk() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.TARGET_NEWER); |
| assertEquals( |
| "AndroidManifest.xml:7: Warning: Not targeting the latest versions of Android; compatibility modes apply. Consider testing and updating this version. Consult the android.os.Build.VERSION_CODES javadoc for details. [OldTargetApi]\n" + |
| " <uses-sdk android:minSdkVersion=\"10\" android:targetSdkVersion=\"14\" />\n" + |
| " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + |
| "0 errors, 1 warnings\n", |
| lintProject( |
| "oldtarget.xml=>AndroidManifest.xml", |
| "res/values/strings.xml")); |
| } |
| |
| public void testMultipleSdk() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.MULTIPLE_USES_SDK); |
| assertEquals( |
| "AndroidManifest.xml:8: Error: There should only be a single <uses-sdk> element in the manifest: merge these together [MultipleUsesSdk]\n" + |
| " <uses-sdk android:targetSdkVersion=\"14\" />\n" + |
| " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + |
| " AndroidManifest.xml:7: Also appears here\n" + |
| " AndroidManifest.xml:9: Also appears here\n" + |
| "1 errors, 0 warnings\n", |
| |
| lintProject( |
| "multiplesdk.xml=>AndroidManifest.xml", |
| "res/values/strings.xml")); |
| } |
| |
| public void testWrongLocation() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.WRONG_PARENT); |
| assertEquals( |
| "AndroidManifest.xml:8: Error: The <uses-sdk> element must be a direct child of the <manifest> root element [WrongManifestParent]\n" + |
| " <uses-sdk android:minSdkVersion=\"Froyo\" />\n" + |
| " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + |
| "AndroidManifest.xml:9: Error: The <uses-permission> element must be a direct child of the <manifest> root element [WrongManifestParent]\n" + |
| " <uses-permission />\n" + |
| " ~~~~~~~~~~~~~~~~~~~\n" + |
| "AndroidManifest.xml:10: Error: The <permission> element must be a direct child of the <manifest> root element [WrongManifestParent]\n" + |
| " <permission />\n" + |
| " ~~~~~~~~~~~~~~\n" + |
| "AndroidManifest.xml:11: Error: The <permission-tree> element must be a direct child of the <manifest> root element [WrongManifestParent]\n" + |
| " <permission-tree />\n" + |
| " ~~~~~~~~~~~~~~~~~~~\n" + |
| "AndroidManifest.xml:12: Error: The <permission-group> element must be a direct child of the <manifest> root element [WrongManifestParent]\n" + |
| " <permission-group />\n" + |
| " ~~~~~~~~~~~~~~~~~~~~\n" + |
| "AndroidManifest.xml:14: Error: The <uses-sdk> element must be a direct child of the <manifest> root element [WrongManifestParent]\n" + |
| " <uses-sdk />\n" + |
| " ~~~~~~~~~~~~\n" + |
| "AndroidManifest.xml:15: Error: The <uses-configuration> element must be a direct child of the <manifest> root element [WrongManifestParent]\n" + |
| " <uses-configuration />\n" + |
| " ~~~~~~~~~~~~~~~~~~~~~~\n" + |
| "AndroidManifest.xml:16: Error: The <uses-feature> element must be a direct child of the <manifest> root element [WrongManifestParent]\n" + |
| " <uses-feature />\n" + |
| " ~~~~~~~~~~~~~~~~\n" + |
| "AndroidManifest.xml:17: Error: The <supports-screens> element must be a direct child of the <manifest> root element [WrongManifestParent]\n" + |
| " <supports-screens />\n" + |
| " ~~~~~~~~~~~~~~~~~~~~\n" + |
| "AndroidManifest.xml:18: Error: The <compatible-screens> element must be a direct child of the <manifest> root element [WrongManifestParent]\n" + |
| " <compatible-screens />\n" + |
| " ~~~~~~~~~~~~~~~~~~~~~~\n" + |
| "AndroidManifest.xml:19: Error: The <supports-gl-texture> element must be a direct child of the <manifest> root element [WrongManifestParent]\n" + |
| " <supports-gl-texture />\n" + |
| " ~~~~~~~~~~~~~~~~~~~~~~~\n" + |
| "AndroidManifest.xml:24: Error: The <uses-library> element must be a direct child of the <application> element [WrongManifestParent]\n" + |
| " <uses-library />\n" + |
| " ~~~~~~~~~~~~~~~~\n" + |
| "AndroidManifest.xml:25: Error: The <activity> element must be a direct child of the <application> element [WrongManifestParent]\n" + |
| " <activity android:name=\".HelloWorld\"\n" + |
| " ^\n" + |
| "13 errors, 0 warnings\n" + |
| "", |
| |
| lintProject("broken-manifest2.xml=>AndroidManifest.xml")); |
| } |
| |
| public void testDuplicateActivity() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.DUPLICATE_ACTIVITY); |
| assertEquals( |
| "AndroidManifest.xml:16: Error: Duplicate registration for activity com.example.helloworld.HelloWorld [DuplicateActivity]\n" + |
| " <activity android:name=\"com.example.helloworld.HelloWorld\"\n" + |
| " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + |
| "1 errors, 0 warnings\n" + |
| "", |
| |
| lintProject( |
| "duplicate-manifest.xml=>AndroidManifest.xml", |
| "res/values/strings.xml")); |
| } |
| |
| public void testDuplicateActivityAcrossSourceSets() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.DUPLICATE_ACTIVITY); |
| File master = getProjectDir("MasterProject", |
| // Master project |
| "AndroidManifest.xml=>AndroidManifest.xml", |
| "multiproject/main-merge.properties=>project.properties", |
| "multiproject/MainCode.java.txt=>src/foo/main/MainCode.java" |
| ); |
| File library = getProjectDir("LibraryProject", |
| // Library project |
| "AndroidManifest.xml=>AndroidManifest.xml", |
| "multiproject/library.properties=>project.properties", |
| "multiproject/LibraryCode.java.txt=>src/foo/library/LibraryCode.java", |
| "multiproject/strings.xml=>res/values/strings.xml" |
| ); |
| assertEquals("No warnings.", |
| checkLint(Arrays.asList(master, library))); |
| } |
| |
| public void testIgnoreDuplicateActivity() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.DUPLICATE_ACTIVITY); |
| assertEquals( |
| "No warnings.", |
| |
| lintProject( |
| "duplicate-manifest-ignore.xml=>AndroidManifest.xml", |
| "res/values/strings.xml")); |
| } |
| |
| public void testAllowBackup() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.ALLOW_BACKUP); |
| assertEquals( |
| "AndroidManifest.xml:9: Warning: Should explicitly set android:allowBackup to " + |
| "true or false (it's true by default, and that can have some security " + |
| "implications for the application's data) [AllowBackup]\n" + |
| " <application\n" + |
| " ^\n" + |
| "0 errors, 1 warnings\n", |
| lintProject( |
| "AndroidManifest.xml", |
| "apicheck/minsdk14.xml=>AndroidManifest.xml", |
| "res/values/strings.xml")); |
| } |
| |
| public void testAllowBackupOk() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.ALLOW_BACKUP); |
| assertEquals( |
| "No warnings.", |
| lintProject( |
| "allowbackup.xml=>AndroidManifest.xml", |
| "res/values/strings.xml")); |
| } |
| |
| public void testAllowBackupOk2() throws Exception { |
| // Requires build api >= 4 |
| mEnabled = Collections.singleton(ManifestDetector.ALLOW_BACKUP); |
| assertEquals( |
| "No warnings.", |
| lintProject( |
| "apicheck/minsdk1.xml=>AndroidManifest.xml", |
| "res/values/strings.xml")); |
| } |
| |
| public void testAllowBackupOk3() throws Exception { |
| // Not flagged in library projects |
| mEnabled = Collections.singleton(ManifestDetector.ALLOW_BACKUP); |
| assertEquals( |
| "No warnings.", |
| lintProject( |
| "AndroidManifest.xml", |
| "multiproject/library.properties=>project.properties", |
| "res/values/strings.xml")); |
| } |
| |
| public void testAllowIgnore() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.ALLOW_BACKUP); |
| assertEquals( |
| "No warnings.", |
| lintProject( |
| "allowbackup_ignore.xml=>AndroidManifest.xml", |
| "res/values/strings.xml")); |
| } |
| |
| public void testDuplicatePermissions() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.UNIQUE_PERMISSION); |
| assertEquals( |
| "AndroidManifest.xml:12: Error: Permission name SEND_SMS is not unique (appears in both foo.permission.SEND_SMS and bar.permission.SEND_SMS) [UniquePermission]\n" + |
| " <permission android:name=\"bar.permission.SEND_SMS\"\n" + |
| " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + |
| " AndroidManifest.xml:9: Previous permission here\n" + |
| "1 errors, 0 warnings\n", |
| |
| lintProject( |
| "duplicate_permissions1.xml=>AndroidManifest.xml", |
| "res/values/strings.xml")); |
| } |
| |
| public void testDuplicatePermissionsMultiProject() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.UNIQUE_PERMISSION); |
| |
| File master = getProjectDir("MasterProject", |
| // Master project |
| "duplicate_permissions2.xml=>AndroidManifest.xml", |
| "multiproject/main-merge.properties=>project.properties", |
| "multiproject/MainCode.java.txt=>src/foo/main/MainCode.java" |
| ); |
| File library = getProjectDir("LibraryProject", |
| // Library project |
| "duplicate_permissions3.xml=>AndroidManifest.xml", |
| "multiproject/library.properties=>project.properties", |
| "multiproject/LibraryCode.java.txt=>src/foo/library/LibraryCode.java", |
| "multiproject/strings.xml=>res/values/strings.xml" |
| ); |
| assertEquals( |
| "LibraryProject/AndroidManifest.xml:9: Error: Permission name SEND_SMS is not unique (appears in both foo.permission.SEND_SMS and bar.permission.SEND_SMS) [UniquePermission]\n" + |
| " <permission android:name=\"bar.permission.SEND_SMS\"\n" + |
| " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + |
| "1 errors, 0 warnings\n", |
| |
| checkLint(Arrays.asList(master, library))); |
| } |
| |
| public void testMissingVersion() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.SET_VERSION); |
| assertEquals("" |
| + "AndroidManifest.xml:2: Warning: Should set android:versionCode to specify the application version [MissingVersion]\n" |
| + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" |
| + "^\n" |
| + "AndroidManifest.xml:2: Warning: Should set android:versionName to specify the application version [MissingVersion]\n" |
| + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" |
| + "^\n" |
| + "0 errors, 2 warnings\n", |
| lintProject("no_version.xml=>AndroidManifest.xml")); |
| } |
| |
| public void testVersionNotMissingInGradleProjects() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.SET_VERSION); |
| assertEquals("" |
| + "No warnings.", |
| lintProject("no_version.xml=>AndroidManifest.xml", |
| "multiproject/library.properties=>build.gradle")); // dummy; only name counts |
| } |
| |
| public void testIllegalReference() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.ILLEGAL_REFERENCE); |
| assertEquals("" |
| + "AndroidManifest.xml:4: Warning: The android:versionCode cannot be a resource url, it must be a literal integer [IllegalResourceRef]\n" |
| + " android:versionCode=\"@dimen/versionCode\"\n" |
| + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" |
| + "AndroidManifest.xml:7: Warning: The android:minSdkVersion cannot be a resource url, it must be a literal integer (or string if a preview codename) [IllegalResourceRef]\n" |
| + " <uses-sdk android:minSdkVersion=\"@dimen/minSdkVersion\" android:targetSdkVersion=\"@dimen/targetSdkVersion\" />\n" |
| + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" |
| + "AndroidManifest.xml:7: Warning: The android:targetSdkVersion cannot be a resource url, it must be a literal integer (or string if a preview codename) [IllegalResourceRef]\n" |
| + " <uses-sdk android:minSdkVersion=\"@dimen/minSdkVersion\" android:targetSdkVersion=\"@dimen/targetSdkVersion\" />\n" |
| + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" |
| + "0 errors, 3 warnings\n", |
| |
| lintProject("illegal_version.xml=>AndroidManifest.xml")); |
| } |
| |
| public void testDuplicateUsesFeature() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.DUPLICATE_USES_FEATURE); |
| assertEquals( |
| "AndroidManifest.xml:11: Warning: Duplicate declaration of uses-feature android.hardware.camera [DuplicateUsesFeature]\n" + |
| " <uses-feature android:name=\"android.hardware.camera\"/>\n" + |
| " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + |
| "0 errors, 1 warnings\n", |
| lintProject( |
| "duplicate_uses_feature.xml=>AndroidManifest.xml", |
| "res/values/strings.xml")); |
| } |
| |
| public void testDuplicateUsesFeatureOk() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.DUPLICATE_USES_FEATURE); |
| assertEquals( |
| "No warnings.", |
| lintProject( |
| "duplicate_uses_feature_ok.xml=>AndroidManifest.xml", |
| "res/values/strings.xml")); |
| } |
| |
| public void testMissingApplicationIcon() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.APPLICATION_ICON); |
| assertEquals( |
| "AndroidManifest.xml:9: Warning: Should explicitly set android:icon, there is no default [MissingApplicationIcon]\n" + |
| " <application\n" + |
| " ^\n" + |
| "0 errors, 1 warnings\n", |
| lintProject( |
| "missing_application_icon.xml=>AndroidManifest.xml", |
| "res/values/strings.xml")); |
| } |
| |
| public void testMissingApplicationIconInLibrary() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.APPLICATION_ICON); |
| assertEquals( |
| "No warnings.", |
| lintProject( |
| "missing_application_icon.xml=>AndroidManifest.xml", |
| "multiproject/library.properties=>project.properties", |
| "res/values/strings.xml")); |
| } |
| |
| public void testMissingApplicationIconOk() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.APPLICATION_ICON); |
| assertEquals( |
| "No warnings.", |
| lintProject( |
| "AndroidManifest.xml", |
| "res/values/strings.xml")); |
| } |
| |
| public void testDeviceAdmin() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.DEVICE_ADMIN); |
| assertEquals("" |
| + "AndroidManifest.xml:31: Warning: You must have an intent filter for action android.app.action.DEVICE_ADMIN_ENABLED [DeviceAdmin]\n" |
| + " <meta-data android:name=\"android.app.device_admin\"\n" |
| + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" |
| + "AndroidManifest.xml:44: Warning: You must have an intent filter for action android.app.action.DEVICE_ADMIN_ENABLED [DeviceAdmin]\n" |
| + " <meta-data android:name=\"android.app.device_admin\"\n" |
| + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" |
| + "AndroidManifest.xml:56: Warning: You must have an intent filter for action android.app.action.DEVICE_ADMIN_ENABLED [DeviceAdmin]\n" |
| + " <meta-data android:name=\"android.app.device_admin\"\n" |
| + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" |
| + "0 errors, 3 warnings\n", |
| lintProject("deviceadmin.xml=>AndroidManifest.xml")); |
| } |
| |
| public void testMockLocations() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.MOCK_LOCATION); |
| assertEquals("" |
| + "AndroidManifest.xml:9: Error: Mock locations should only be requested in a test or debug-specific manifest file (typically src/debug/AndroidManifest.xml) [MockLocation]\n" |
| + " <uses-permission android:name=\"android.permission.ACCESS_MOCK_LOCATION\" /> \n" |
| + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" |
| + "1 errors, 0 warnings\n", |
| lintProject( |
| "mock_location.xml=>AndroidManifest.xml", |
| "mock_location.xml=>debug/AndroidManifest.xml", |
| "mock_location.xml=>test/AndroidManifest.xml", |
| "multiproject/library.properties=>build.gradle")); // dummy; only name counts |
| // TODO: When we have an instantiatable gradle model, test with real model and verify |
| // that a manifest file in a debug build type does not get flagged. |
| } |
| |
| public void testMockLocationsOk() throws Exception { |
| // Not a Gradle project |
| mEnabled = Collections.singleton(ManifestDetector.MOCK_LOCATION); |
| assertEquals("" |
| + "No warnings.", |
| lintProject( |
| "mock_location.xml=>AndroidManifest.xml")); |
| } |
| |
| public void testGradleOverrides() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.GRADLE_OVERRIDES); |
| assertEquals("" |
| + "AndroidManifest.xml:4: Warning: This versionCode value (1) is not used; it is always overridden by the value specified in the Gradle build script (2) [GradleOverrides]\n" |
| + " android:versionCode=\"1\"\n" |
| + " ~~~~~~~~~~~~~~~~~~~~~~~\n" |
| + "AndroidManifest.xml:5: Warning: This versionName value (1.0) is not used; it is always overridden by the value specified in the Gradle build script (MyName) [GradleOverrides]\n" |
| + " android:versionName=\"1.0\" >\n" |
| + " ~~~~~~~~~~~~~~~~~~~~~~~~~\n" |
| + "AndroidManifest.xml:7: Warning: This minSdkVersion value (14) is not used; it is always overridden by the value specified in the Gradle build script (5) [GradleOverrides]\n" |
| + " <uses-sdk android:minSdkVersion=\"14\" android:targetSdkVersion=\"17\" />\n" |
| + " ~~~~~~~~~~~~~~~~~~~~~~~~~~\n" |
| + "AndroidManifest.xml:7: Warning: This targetSdkVersion value (17) is not used; it is always overridden by the value specified in the Gradle build script (16) [GradleOverrides]\n" |
| + " <uses-sdk android:minSdkVersion=\"14\" android:targetSdkVersion=\"17\" />\n" |
| + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" |
| + "0 errors, 4 warnings\n", |
| lintProject( |
| "gradle_override.xml=>AndroidManifest.xml", |
| "multiproject/library.properties=>build.gradle")); // dummy; only name counts |
| } |
| |
| public void testGradleOverridesOk() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.GRADLE_OVERRIDES); |
| // (See custom logic in #createClient which returns -1/null for the merged flavor |
| // from this test, and not from testGradleOverrides) |
| assertEquals("" |
| + "No warnings.", |
| lintProject( |
| "gradle_override.xml=>AndroidManifest.xml", |
| "multiproject/library.properties=>build.gradle")); // dummy; only name counts |
| } |
| |
| public void testManifestPackagePlaceholder() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.GRADLE_OVERRIDES); |
| assertEquals("" |
| + "AndroidManifest.xml:3: Warning: Cannot use placeholder for the package in the manifest; set applicationId in build.gradle instead [GradleOverrides]\n" |
| + " package=\"${packageName}\" >\n" |
| + " ~~~~~~~~~~~~~~~~~~~~~~~~\n" |
| + "0 errors, 1 warnings\n", |
| |
| lintProject( |
| "gradle_override_placeholder.xml=>AndroidManifest.xml", |
| "multiproject/library.properties=>build.gradle")); // dummy; only name counts |
| } |
| |
| public void testMipMap() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.MIPMAP); |
| assertEquals("No warnings.", |
| |
| lintProject( |
| "mipmap.xml=>AndroidManifest.xml")); |
| } |
| |
| public void testMipMapWithDensityFiltering() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.MIPMAP); |
| assertEquals("" |
| + "AndroidManifest.xml:9: Warning: Should use @mipmap instead of @drawable for launcher icons [MipmapIcons]\n" |
| + " android:icon=\"@drawable/ic_launcher\"\n" |
| + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" |
| + "AndroidManifest.xml:14: Warning: Should use @mipmap instead of @drawable for launcher icons [MipmapIcons]\n" |
| + " android:icon=\"@drawable/activity1\"\n" |
| + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" |
| + "0 errors, 2 warnings\n", |
| |
| lintProject( |
| "mipmap.xml=>AndroidManifest.xml")); |
| } |
| |
| public void testFullBackupContentBoolean() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.ALLOW_BACKUP); |
| assertEquals("No warnings.", |
| |
| lintProjectIncrementally( |
| "AndroidManifest.xml", |
| 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" |
| + "\n" |
| + " <application\n" |
| + " android:allowBackup=\"true\"\n" |
| + " android:fullBackupContent=\"true\"\n" |
| + " android:label=\"@string/app_name\"\n" |
| + " android:theme=\"@style/AppTheme\" >\n" |
| + " </application>\n" |
| + "\n" |
| + "</manifest>\n"))); |
| } |
| |
| public void testFullBackupContentMissing() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.ALLOW_BACKUP); |
| assertEquals("" |
| + "AndroidManifest.xml:7: Warning: Missing <full-backup-content> resource [AllowBackup]\n" |
| + " android:fullBackupContent=\"@xml/backup\"\n" |
| + " ~~~~~~~~~~~\n" |
| + "0 errors, 1 warnings\n", |
| |
| lintProjectIncrementally( |
| "AndroidManifest.xml", |
| 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" |
| + "\n" |
| + " <application\n" |
| + " android:allowBackup=\"true\"\n" |
| + " android:fullBackupContent=\"@xml/backup\"\n" |
| + " android:label=\"@string/app_name\"\n" |
| + " android:theme=\"@style/AppTheme\" >\n" |
| + " </application>\n" |
| + "\n" |
| + "</manifest>\n"))); |
| } |
| |
| public void testFullBackupContentOk() throws Exception { |
| mEnabled = Collections.singleton(ManifestDetector.ALLOW_BACKUP); |
| assertEquals("No warnings.", |
| |
| lintProjectIncrementally( |
| "AndroidManifest.xml", |
| 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" |
| + "\n" |
| + " <application\n" |
| + " android:allowBackup=\"true\"\n" |
| + " android:fullBackupContent=\"@xml/backup\"\n" |
| + " android:label=\"@string/app_name\"\n" |
| + " android:theme=\"@style/AppTheme\" >\n" |
| + " </application>\n" |
| + "\n" |
| + "</manifest>\n"), |
| xml("res/xml/backup.xml", "" |
| + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" |
| + "<full-backup-content>\n" |
| + " <include domain=\"file\" path=\"dd\"/>\n" |
| + " <exclude domain=\"file\" path=\"dd/fo3o.txt\"/>\n" |
| + " <exclude domain=\"file\" path=\"dd/ss/foo.txt\"/>\n" |
| + "</full-backup-content>"))); |
| } |
| |
| public void testHasBackupSpecifiedInTarget23() 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=\"23\" />" |
| + "\n" |
| + " <application\n" |
| + " android:fullBackupContent=\"no\"\n" |
| + " android:label=\"@string/app_name\"\n" |
| + " android:theme=\"@style/AppTheme\" >\n" |
| + " </application>\n" |
| + "\n" |
| + "</manifest>\n"))); |
| } |
| |
| 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" |
| + " <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\" >\n" |
| + " </application>\n" |
| + "\n" |
| + "</manifest>\n"))); |
| } |
| |
| public void testMissingBackupWithoutGcmPreTarget23() 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 testMissingBackupWithGcmPreTarget23() 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" |
| + " <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=\"21\" />" |
| + "\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"))); |
| } |
| |
| // Custom project which locates all manifest files in the project rather than just |
| // being hardcoded to the root level |
| |
| @Override |
| protected TestLintClient createClient() { |
| if ("testMipMapWithDensityFiltering".equals(getName())) { |
| // Set up a mock project model for the resource configuration test(s) |
| // where we provide a subset of densities to be included |
| return new TestLintClient() { |
| @NonNull |
| @Override |
| protected Project createProject(@NonNull File dir, @NonNull File referenceDir) { |
| return new Project(this, dir, referenceDir) { |
| @Override |
| public boolean isGradleProject() { |
| return true; |
| } |
| |
| @Nullable |
| @Override |
| public AndroidProject getGradleProjectModel() { |
| /* |
| Simulate variant freeBetaDebug in this setup: |
| defaultConfig { |
| ... |
| resConfigs "cs" |
| } |
| flavorDimensions "pricing", "releaseType" |
| productFlavors { |
| beta { |
| flavorDimension "releaseType" |
| resConfig "en", "de" |
| resConfigs "nodpi", "hdpi" |
| } |
| normal { flavorDimension "releaseType" } |
| free { flavorDimension "pricing" } |
| paid { flavorDimension "pricing" } |
| } |
| */ |
| ProductFlavor flavorFree = mock(ProductFlavor.class); |
| when(flavorFree.getName()).thenReturn("free"); |
| when(flavorFree.getResourceConfigurations()) |
| .thenReturn(Collections.<String>emptyList()); |
| |
| ProductFlavor flavorNormal = mock(ProductFlavor.class); |
| when(flavorNormal.getName()).thenReturn("normal"); |
| when(flavorNormal.getResourceConfigurations()) |
| .thenReturn(Collections.<String>emptyList()); |
| |
| ProductFlavor flavorPaid = mock(ProductFlavor.class); |
| when(flavorPaid.getName()).thenReturn("paid"); |
| when(flavorPaid.getResourceConfigurations()) |
| .thenReturn(Collections.<String>emptyList()); |
| |
| ProductFlavor flavorBeta = mock(ProductFlavor.class); |
| when(flavorBeta.getName()).thenReturn("beta"); |
| List<String> resConfigs = Arrays.asList("hdpi", "en", "de", "nodpi"); |
| when(flavorBeta.getResourceConfigurations()).thenReturn(resConfigs); |
| |
| ProductFlavor defaultFlavor = mock(ProductFlavor.class); |
| when(defaultFlavor.getName()).thenReturn("main"); |
| when(defaultFlavor.getResourceConfigurations()).thenReturn( |
| Collections.singleton("cs")); |
| |
| ProductFlavorContainer containerBeta = |
| mock(ProductFlavorContainer.class); |
| when(containerBeta.getProductFlavor()).thenReturn(flavorBeta); |
| |
| ProductFlavorContainer containerFree = |
| mock(ProductFlavorContainer.class); |
| when(containerFree.getProductFlavor()).thenReturn(flavorFree); |
| |
| ProductFlavorContainer containerPaid = |
| mock(ProductFlavorContainer.class); |
| when(containerPaid.getProductFlavor()).thenReturn(flavorPaid); |
| |
| ProductFlavorContainer containerNormal = |
| mock(ProductFlavorContainer.class); |
| when(containerNormal.getProductFlavor()).thenReturn(flavorNormal); |
| |
| ProductFlavorContainer defaultContainer = |
| mock(ProductFlavorContainer.class); |
| when(defaultContainer.getProductFlavor()).thenReturn(defaultFlavor); |
| |
| List<ProductFlavorContainer> containers = Arrays.asList( |
| containerPaid, containerFree, containerNormal, containerBeta |
| ); |
| |
| AndroidProject project = mock(AndroidProject.class); |
| when(project.getProductFlavors()).thenReturn(containers); |
| when(project.getDefaultConfig()).thenReturn(defaultContainer); |
| return project; |
| } |
| |
| @Nullable |
| @Override |
| public Variant getCurrentVariant() { |
| List<String> productFlavorNames = Arrays.asList("free", "beta"); |
| Variant mock = mock(Variant.class); |
| when(mock.getProductFlavors()).thenReturn(productFlavorNames); |
| return mock; |
| } |
| }; |
| } |
| }; |
| } |
| if (mEnabled.contains(ManifestDetector.MOCK_LOCATION)) { |
| return new TestLintClient() { |
| @NonNull |
| @Override |
| protected Project createProject(@NonNull File dir, @NonNull File referenceDir) { |
| return new Project(this, dir, referenceDir) { |
| @NonNull |
| @Override |
| public List<File> getManifestFiles() { |
| if (mManifestFiles == null) { |
| mManifestFiles = Lists.newArrayList(); |
| addManifestFiles(mDir); |
| } |
| |
| return mManifestFiles; |
| } |
| |
| private void addManifestFiles(File dir) { |
| if (dir.getName().equals(ANDROID_MANIFEST_XML)) { |
| mManifestFiles.add(dir); |
| } else if (dir.isDirectory()) { |
| File[] files = dir.listFiles(); |
| if (files != null) { |
| for (File file : files) { |
| addManifestFiles(file); |
| } |
| } |
| } |
| } |
| |
| @NonNull SourceProvider createSourceProvider(File manifest) { |
| SourceProvider provider = mock(SourceProvider.class); |
| when(provider.getManifestFile()).thenReturn(manifest); |
| return provider; |
| } |
| |
| @Nullable |
| @Override |
| public AndroidProject getGradleProjectModel() { |
| if (!isGradleProject()) { |
| return null; |
| } |
| |
| File main = new File(mDir, ANDROID_MANIFEST_XML); |
| File debug = new File(mDir, "debug" + File.separator + ANDROID_MANIFEST_XML); |
| File test = new File(mDir, "test" + File.separator + ANDROID_MANIFEST_XML); |
| |
| SourceProvider defaultSourceProvider = createSourceProvider(main); |
| SourceProvider debugSourceProvider = createSourceProvider(debug); |
| SourceProvider testSourceProvider = createSourceProvider(test); |
| |
| ProductFlavorContainer defaultConfig = mock(ProductFlavorContainer.class); |
| when(defaultConfig.getSourceProvider()).thenReturn(defaultSourceProvider); |
| |
| BuildType buildType = mock(BuildType.class); |
| when(buildType.isDebuggable()).thenReturn(true); |
| |
| BuildTypeContainer buildTypeContainer = mock(BuildTypeContainer.class); |
| when(buildTypeContainer.getBuildType()).thenReturn(buildType); |
| when(buildTypeContainer.getSourceProvider()).thenReturn(debugSourceProvider); |
| List<BuildTypeContainer> buildTypes = Lists.newArrayList(buildTypeContainer); |
| |
| SourceProviderContainer extraProvider = mock(SourceProviderContainer.class); |
| when(extraProvider.getArtifactName()).thenReturn(AndroidProject.ARTIFACT_ANDROID_TEST); |
| when(extraProvider.getSourceProvider()).thenReturn(testSourceProvider); |
| List<SourceProviderContainer> extraProviders = Lists.newArrayList(extraProvider); |
| |
| ProductFlavorContainer productFlavorContainer = mock(ProductFlavorContainer.class); |
| when(productFlavorContainer.getExtraSourceProviders()).thenReturn(extraProviders); |
| List<ProductFlavorContainer> productFlavors = Lists.newArrayList(productFlavorContainer); |
| |
| AndroidProject project = mock(AndroidProject.class); |
| when(project.getDefaultConfig()).thenReturn(defaultConfig); |
| when(project.getBuildTypes()).thenReturn(buildTypes); |
| when(project.getProductFlavors()).thenReturn(productFlavors); |
| return project; |
| } |
| }; |
| } |
| }; |
| } else if (mEnabled.contains(ManifestDetector.GRADLE_OVERRIDES)) { |
| return new TestLintClient() { |
| @NonNull |
| @Override |
| protected Project createProject(@NonNull File dir, @NonNull File referenceDir) { |
| return new Project(this, dir, referenceDir) { |
| @Override |
| public boolean isGradleProject() { |
| return true; |
| } |
| |
| @Nullable |
| @Override |
| public Variant getCurrentVariant() { |
| ProductFlavor flavor = mock(ProductFlavor.class); |
| if (getName().equals("ManifestDetectorTest_testGradleOverridesOk") || |
| getName().equals( |
| "ManifestDetectorTest_testManifestPackagePlaceholder")) { |
| when(flavor.getMinSdkVersion()).thenReturn(null); |
| when(flavor.getTargetSdkVersion()).thenReturn(null); |
| when(flavor.getVersionCode()).thenReturn(null); |
| when(flavor.getVersionName()).thenReturn(null); |
| } else { |
| assertEquals(getName(), "ManifestDetectorTest_testGradleOverrides"); |
| |
| ApiVersion apiMock = mock(ApiVersion.class); |
| when(apiMock.getApiLevel()).thenReturn(5); |
| when(apiMock.getApiString()).thenReturn("5"); |
| when(flavor.getMinSdkVersion()).thenReturn(apiMock); |
| |
| apiMock = mock(ApiVersion.class); |
| when(apiMock.getApiLevel()).thenReturn(16); |
| when(apiMock.getApiString()).thenReturn("16"); |
| when(flavor.getTargetSdkVersion()).thenReturn(apiMock); |
| |
| when(flavor.getVersionCode()).thenReturn(2); |
| when(flavor.getVersionName()).thenReturn("MyName"); |
| } |
| |
| Variant mock = mock(Variant.class); |
| when(mock.getMergedFlavor()).thenReturn(flavor); |
| return mock; |
| } |
| }; |
| } |
| }; |
| |
| } |
| return super.createClient(); |
| } |
| } |