blob: 17703af5e3855acd00016644d389865e77f38520 [file] [log] [blame]
/*
* 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 org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.FilterData;
import com.android.build.OutputFile;
import com.android.builder.model.AndroidArtifact;
import com.android.builder.model.AndroidArtifactOutput;
import com.android.builder.model.AndroidProject;
import com.android.builder.model.ProductFlavor;
import com.android.builder.model.ProductFlavorContainer;
import com.android.builder.model.Variant;
import com.android.tools.lint.client.api.LintClient;
import com.android.tools.lint.client.api.LintDriver;
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 com.google.common.collect.Sets;
import org.mockito.stubbing.OngoingStubbing;
import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@SuppressWarnings("javadoc")
public class IconDetectorTest extends AbstractCheckTest {
@Override
protected Detector getDetector() {
return new IconDetector();
}
private Set<Issue> mEnabled = new HashSet<Issue>();
private boolean mAbbreviate;
private static final Set<Issue> ALL = new HashSet<Issue>();
static {
ALL.add(IconDetector.DUPLICATES_CONFIGURATIONS);
ALL.add(IconDetector.DUPLICATES_NAMES);
ALL.add(IconDetector.GIF_USAGE);
ALL.add(IconDetector.ICON_DENSITIES);
ALL.add(IconDetector.ICON_DIP_SIZE);
ALL.add(IconDetector.ICON_EXTENSION);
ALL.add(IconDetector.ICON_LOCATION);
ALL.add(IconDetector.ICON_MISSING_FOLDER);
ALL.add(IconDetector.ICON_NODPI);
ALL.add(IconDetector.ICON_COLORS);
ALL.add(IconDetector.ICON_XML_AND_PNG);
ALL.add(IconDetector.ICON_LAUNCHER_SHAPE);
ALL.add(IconDetector.ICON_MIX_9PNG);
}
@Override
protected void setUp() throws Exception {
super.setUp();
mAbbreviate = true;
}
@Override
protected void configureDriver(LintDriver driver) {
driver.setAbbreviating(mAbbreviate);
}
@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 test() throws Exception {
mEnabled = ALL;
assertEquals(
"res/drawable-mdpi/sample_icon.gif: Warning: Using the .gif format for bitmaps is discouraged [GifUsage]\n" +
"res/drawable/ic_launcher.png: Warning: The ic_launcher.png icon has identical contents in the following configuration folders: drawable-mdpi, drawable [IconDuplicatesConfig]\n" +
" res/drawable-mdpi/ic_launcher.png: <No location-specific message\n" +
"res/drawable/ic_launcher.png: Warning: Found bitmap drawable res/drawable/ic_launcher.png in densityless folder [IconLocation]\n" +
"res/drawable-hdpi: Warning: Missing the following drawables in drawable-hdpi: sample_icon.gif (found in drawable-mdpi) [IconDensities]\n" +
"res: Warning: Missing density variation folders in res: drawable-xhdpi, drawable-xxhdpi, drawable-xxxhdpi [IconMissingDensityFolder]\n" +
"0 errors, 5 warnings\n" +
"",
lintProject(
// Use minSDK4 to ensure that we get warnings about missing drawables
"apicheck/minsdk4.xml=>AndroidManifest.xml",
"res/drawable/ic_launcher.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher.png",
"res/drawable-mdpi/sample_icon.gif",
// Make a dummy file named .svn to make sure it doesn't get seen as
// an icon name
"res/drawable-mdpi/sample_icon.gif=>res/drawable-hdpi/.svn",
"res/drawable-hdpi/ic_launcher.png"));
}
public void testMixed() throws Exception {
mEnabled = Collections.singleton(IconDetector.ICON_XML_AND_PNG);
assertEquals(
"res/drawable/background.xml: Warning: The following images appear both as density independent .xml files and as bitmap files: res/drawable-mdpi/background.png, res/drawable/background.xml [IconXmlAndPng]\n" +
" res/drawable-mdpi/background.png: <No location-specific message\n" +
"0 errors, 1 warnings\n",
lintProject(
"apicheck/minsdk4.xml=>AndroidManifest.xml",
"apicheck/minsdk4.xml=>res/drawable/background.xml",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/background.png"));
}
public void testApi1() throws Exception {
mEnabled = ALL;
assertEquals(
"No warnings.",
lintProject(
// manifest file which specifies uses sdk = 2
"apicheck/minsdk2.xml=>AndroidManifest.xml",
"res/drawable/ic_launcher.png"));
}
public void test2() throws Exception {
mEnabled = ALL;
assertEquals(
"res/drawable-hdpi/other.9.png: Warning: The following unrelated icon files have identical contents: appwidget_bg.9.png, other.9.png [IconDuplicates]\n" +
" res/drawable-hdpi/appwidget_bg.9.png: <No location-specific message\n" +
"res/drawable-hdpi/unrelated.png: Warning: The following unrelated icon files have identical contents: ic_launcher.png, unrelated.png [IconDuplicates]\n" +
" res/drawable-hdpi/ic_launcher.png: <No location-specific message\n" +
"res: Warning: Missing density variation folders in res: drawable-mdpi, drawable-xhdpi, drawable-xxhdpi, drawable-xxxhdpi [IconMissingDensityFolder]\n" +
"0 errors, 3 warnings\n",
lintProject(
"res/drawable-hdpi/unrelated.png",
"res/drawable-hdpi/appwidget_bg.9.png",
"res/drawable-hdpi/appwidget_bg_focus.9.png",
"res/drawable-hdpi/other.9.png",
"res/drawable-hdpi/ic_launcher.png"
));
}
public void testNoDpi() throws Exception {
mEnabled = ALL;
assertEquals(
"res/drawable-mdpi/frame.png: Warning: The following images appear in both -nodpi and in a density folder: frame.png [IconNoDpi]\n" +
"res/drawable-xlarge-nodpi-v11/frame.png: Warning: The frame.png icon has identical contents in the following configuration folders: drawable-mdpi, drawable-nodpi, drawable-xlarge-nodpi-v11 [IconDuplicatesConfig]\n" +
" res/drawable-nodpi/frame.png: <No location-specific message\n" +
" res/drawable-mdpi/frame.png: <No location-specific message\n" +
"res: Warning: Missing density variation folders in res: drawable-hdpi, drawable-xhdpi, drawable-xxhdpi, drawable-xxxhdpi [IconMissingDensityFolder]\n" +
"0 errors, 3 warnings\n" +
"",
lintProject(
"res/drawable-mdpi/frame.png",
"res/drawable-nodpi/frame.png",
"res/drawable-xlarge-nodpi-v11/frame.png"));
}
public void testNoDpi2() throws Exception {
mEnabled = ALL;
// Having additional icon names in the no-dpi folder should not cause any complaints
assertEquals(
"res/drawable-xxxhdpi/frame.png: Warning: The image frame.png varies significantly in its density-independent (dip) size across the various density versions: drawable-ldpi/frame.png: 629x387 dp (472x290 px), drawable-mdpi/frame.png: 472x290 dp (472x290 px), drawable-hdpi/frame.png: 315x193 dp (472x290 px), drawable-xhdpi/frame.png: 236x145 dp (472x290 px), drawable-xxhdpi/frame.png: 157x97 dp (472x290 px), drawable-xxxhdpi/frame.png: 118x73 dp (472x290 px) [IconDipSize]\n" +
" res/drawable-xxhdpi/frame.png: <No location-specific message\n" +
" res/drawable-xhdpi/frame.png: <No location-specific message\n" +
" res/drawable-hdpi/frame.png: <No location-specific message\n" +
" res/drawable-mdpi/frame.png: <No location-specific message\n" +
" res/drawable-ldpi/frame.png: <No location-specific message\n" +
"res/drawable-xxxhdpi/frame.png: Warning: The following unrelated icon files have identical contents: frame.png, frame.png, frame.png, file1.png, file2.png, frame.png, frame.png, frame.png [IconDuplicates]\n" +
" res/drawable-xxhdpi/frame.png: <No location-specific message\n" +
" res/drawable-xhdpi/frame.png: <No location-specific message\n" +
" res/drawable-nodpi/file2.png: <No location-specific message\n" +
" res/drawable-nodpi/file1.png: <No location-specific message\n" +
" res/drawable-mdpi/frame.png: <No location-specific message\n" +
" res/drawable-ldpi/frame.png: <No location-specific message\n" +
" res/drawable-hdpi/frame.png: <No location-specific message\n" +
"0 errors, 2 warnings\n" +
"",
lintProject(
"res/drawable-mdpi/frame.png=>res/drawable-mdpi/frame.png",
"res/drawable-mdpi/frame.png=>res/drawable-hdpi/frame.png",
"res/drawable-mdpi/frame.png=>res/drawable-ldpi/frame.png",
"res/drawable-mdpi/frame.png=>res/drawable-xhdpi/frame.png",
"res/drawable-mdpi/frame.png=>res/drawable-xxhdpi/frame.png",
"res/drawable-mdpi/frame.png=>res/drawable-xxxhdpi/frame.png",
"res/drawable-mdpi/frame.png=>res/drawable-nodpi/file1.png",
"res/drawable-mdpi/frame.png=>res/drawable-nodpi/file2.png"));
}
public void testNoDpiMix() throws Exception {
mEnabled = ALL;
assertEquals(
"res/drawable-mdpi/frame.xml: Warning: The following images appear in both -nodpi and in a density folder: frame.png, frame.xml [IconNoDpi]\n" +
" res/drawable-mdpi/frame.png: <No location-specific message\n" +
"res/drawable-nodpi/frame.xml: Warning: The following images appear both as density independent .xml files and as bitmap files: res/drawable-mdpi/frame.png, res/drawable-nodpi/frame.xml [IconXmlAndPng]\n" +
" res/drawable-mdpi/frame.png: <No location-specific message\n" +
"res: Warning: Missing density variation folders in res: drawable-hdpi, drawable-xhdpi, drawable-xxhdpi, drawable-xxxhdpi [IconMissingDensityFolder]\n" +
"0 errors, 3 warnings\n",
lintProject(
"res/drawable-mdpi/frame.png",
"res/drawable/states.xml=>res/drawable-nodpi/frame.xml"));
}
public void testMixedFormat() throws Exception {
mEnabled = ALL;
// Test having a mixture of .xml and .png resources for the same name
// Make sure we don't get:
// drawable-hdpi: Warning: Missing the following drawables in drawable-hdpi: f.png (found in drawable-mdpi)
// drawable-xhdpi: Warning: Missing the following drawables in drawable-xhdpi: f.png (found in drawable-mdpi)
assertEquals(
"res/drawable-xxxhdpi/f.xml: Warning: The following images appear both as density independent .xml files and as bitmap files: res/drawable-hdpi/f.xml, res/drawable-mdpi/f.png [IconXmlAndPng]\n" +
" res/drawable-xxhdpi/f.xml: <No location-specific message\n" +
" res/drawable-xhdpi/f.xml: <No location-specific message\n" +
" res/drawable-mdpi/f.png: <No location-specific message\n" +
" res/drawable-hdpi/f.xml: <No location-specific message\n" +
"0 errors, 1 warnings\n",
lintProject(
"res/drawable-mdpi/frame.png=>res/drawable-mdpi/f.png",
"res/drawable/states.xml=>res/drawable-hdpi/f.xml",
"res/drawable/states.xml=>res/drawable-xhdpi/f.xml",
"res/drawable/states.xml=>res/drawable-xxhdpi/f.xml",
"res/drawable/states.xml=>res/drawable-xxxhdpi/f.xml"));
}
public void testMisleadingFileName() throws Exception {
mEnabled = Collections.singleton(IconDetector.ICON_EXTENSION);
assertEquals(
"res/drawable-mdpi/frame.gif: Warning: Misleading file extension; named .gif but the file format is png [IconExtension]\n" +
"res/drawable-mdpi/frame.jpg: Warning: Misleading file extension; named .jpg but the file format is png [IconExtension]\n" +
"res/drawable-mdpi/myjpg.png: Warning: Misleading file extension; named .png but the file format is JPEG [IconExtension]\n" +
"res/drawable-mdpi/sample_icon.jpeg: Warning: Misleading file extension; named .jpeg but the file format is gif [IconExtension]\n" +
"res/drawable-mdpi/sample_icon.jpg: Warning: Misleading file extension; named .jpg but the file format is gif [IconExtension]\n" +
"res/drawable-mdpi/sample_icon.png: Warning: Misleading file extension; named .png but the file format is gif [IconExtension]\n" +
"0 errors, 6 warnings\n",
lintProject(
"res/drawable-mdpi/sample_icon.jpg=>res/drawable-mdpi/myjpg.jpg", // VALID
"res/drawable-mdpi/sample_icon.jpg=>res/drawable-mdpi/myjpg.jpeg", // VALID
"res/drawable-mdpi/frame.png=>res/drawable-mdpi/frame.gif",
"res/drawable-mdpi/frame.png=>res/drawable-mdpi/frame.jpg",
"res/drawable-mdpi/sample_icon.jpg=>res/drawable-mdpi/myjpg.png",
"res/drawable-mdpi/sample_icon.gif=>res/drawable-mdpi/sample_icon.jpg",
"res/drawable-mdpi/sample_icon.gif=>res/drawable-mdpi/sample_icon.jpeg",
"res/drawable-mdpi/sample_icon.gif=>res/drawable-mdpi/sample_icon.png"));
}
public void testColors() throws Exception {
mEnabled = Collections.singleton(IconDetector.ICON_COLORS);
assertEquals(
"res/drawable-mdpi/ic_menu_my_action.png: Warning: Action Bar icons should use a single gray color (#333333 for light themes (with 60%/30% opacity for enabled/disabled), and #FFFFFF with opacity 80%/30% for dark themes [IconColors]\n" +
"res/drawable-mdpi-v11/ic_stat_my_notification.png: Warning: Notification icons must be entirely white [IconColors]\n" +
"res/drawable-mdpi-v9/ic_stat_my_notification2.png: Warning: Notification icons must be entirely white [IconColors]\n" +
"0 errors, 3 warnings\n",
lintProject(
"apicheck/minsdk14.xml=>AndroidManifest.xml",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_menu_my_action.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi-v11/ic_stat_my_notification.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi-v9/ic_stat_my_notification2.png",
"res/drawable-mdpi/ic_menu_add_clip_normal.png")); // OK
}
public void testNotActionBarIcons() throws Exception {
mEnabled = Collections.singleton(IconDetector.ICON_COLORS);
assertEquals(
"No warnings.",
// No Java code designates the menu as an action bar menu
lintProject(
"apicheck/minsdk14.xml=>AndroidManifest.xml",
"res/menu/menu.xml",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/icon1.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/icon2.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/icon3.png", // Not action bar
"res/drawable-mdpi/ic_menu_add_clip_normal.png")); // OK
}
public void testActionBarIcons() throws Exception {
mEnabled = Collections.singleton(IconDetector.ICON_COLORS);
assertEquals(
"res/drawable-mdpi/icon1.png: Warning: Action Bar icons should use a single gray color (#333333 for light themes (with 60%/30% opacity for enabled/disabled), and #FFFFFF with opacity 80%/30% for dark themes [IconColors]\n" +
"res/drawable-mdpi/icon2.png: Warning: Action Bar icons should use a single gray color (#333333 for light themes (with 60%/30% opacity for enabled/disabled), and #FFFFFF with opacity 80%/30% for dark themes [IconColors]\n" +
"0 errors, 2 warnings\n",
lintProject(
"apicheck/minsdk14.xml=>AndroidManifest.xml",
"res/menu/menu.xml",
"src/test/pkg/ActionBarTest.java.txt=>src/test/pkg/ActionBarTest.java",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/icon1.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/icon2.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/icon3.png", // Not action bar
"res/drawable-mdpi/ic_menu_add_clip_normal.png")); // OK
}
public void testOkActionBarIcons() throws Exception {
mEnabled = Collections.singleton(IconDetector.ICON_COLORS);
assertEquals(
"No warnings.",
lintProject(
"apicheck/minsdk14.xml=>AndroidManifest.xml",
"res/menu/menu.xml",
"res/drawable-mdpi/ic_menu_add_clip_normal.png=>res/drawable-mdpi/icon1.png",
"res/drawable-mdpi/ic_menu_add_clip_normal.png=>res/drawable-mdpi/icon2.png"));
}
public void testNotificationIcons() throws Exception {
mEnabled = Collections.singleton(IconDetector.ICON_COLORS);
assertEquals(
"res/drawable-mdpi/icon1.png: Warning: Notification icons must be entirely white [IconColors]\n" +
"res/drawable-mdpi/icon2.png: Warning: Notification icons must be entirely white [IconColors]\n" +
"res/drawable-mdpi/icon3.png: Warning: Notification icons must be entirely white [IconColors]\n" +
"res/drawable-mdpi/icon4.png: Warning: Notification icons must be entirely white [IconColors]\n" +
"res/drawable-mdpi/icon5.png: Warning: Notification icons must be entirely white [IconColors]\n" +
"0 errors, 5 warnings\n",
lintProject(
"apicheck/minsdk14.xml=>AndroidManifest.xml",
"src/test/pkg/NotificationTest.java.txt=>src/test/pkg/NotificationTest.java",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/icon1.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/icon2.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/icon3.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/icon4.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/icon5.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/icon6.png", // not a notification
"res/drawable/ic_launcher.png=>res/drawable-mdpi/icon7.png", // ditto
"res/drawable-mdpi/ic_menu_add_clip_normal.png")); // OK
}
public void testOkNotificationIcons() throws Exception {
mEnabled = Collections.singleton(IconDetector.ICON_COLORS);
assertEquals(
"No warnings.",
lintProject(
"apicheck/minsdk14.xml=>AndroidManifest.xml",
"src/test/pkg/NotificationTest.java.txt=>src/test/pkg/NotificationTest.java",
"res/drawable-mdpi/ic_menu_add_clip_normal.png=>res/drawable-mdpi/icon1.png",
"res/drawable-mdpi/ic_menu_add_clip_normal.png=>res/drawable-mdpi/icon2.png",
"res/drawable-mdpi/ic_menu_add_clip_normal.png=>res/drawable-mdpi/icon3.png",
"res/drawable-mdpi/ic_menu_add_clip_normal.png=>res/drawable-mdpi/icon4.png",
"res/drawable-mdpi/ic_menu_add_clip_normal.png=>res/drawable-mdpi/icon5.png"));
}
public void testExpectedSize() throws Exception {
mEnabled = Collections.singleton(IconDetector.ICON_EXPECTED_SIZE);
assertEquals(
"res/drawable-mdpi/ic_launcher.png: Warning: Incorrect icon size for drawable-mdpi/ic_launcher.png: expected 48x48, but was 24x24 [IconExpectedSize]\n" +
"res/drawable-mdpi/icon1.png: Warning: Incorrect icon size for drawable-mdpi/icon1.png: expected 32x32, but was 48x48 [IconExpectedSize]\n" +
"res/drawable-mdpi/icon3.png: Warning: Incorrect icon size for drawable-mdpi/icon3.png: expected 24x24, but was 48x48 [IconExpectedSize]\n" +
"0 errors, 3 warnings\n",
lintProject(
"apicheck/minsdk14.xml=>AndroidManifest.xml",
"src/test/pkg/NotificationTest.java.txt=>src/test/pkg/NotificationTest.java",
"res/menu/menu.xml",
"src/test/pkg/ActionBarTest.java.txt=>src/test/pkg/ActionBarTest.java",
// 3 wrong-sized icons:
"res/drawable/ic_launcher.png=>res/drawable-mdpi/icon1.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/icon3.png",
"res/drawable-mdpi/stat_notify_alarm.png=>res/drawable-mdpi/ic_launcher.png",
// OK sizes
"res/drawable-mdpi/ic_menu_add_clip_normal.png=>res/drawable-mdpi/icon2.png",
"res/drawable-mdpi/stat_notify_alarm.png=>res/drawable-mdpi/icon4.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher2.png"
));
}
public void testAbbreviate() throws Exception {
mEnabled = Collections.singleton(IconDetector.ICON_DENSITIES);
assertEquals(
"res/drawable-hdpi: Warning: Missing the following drawables in drawable-hdpi: " +
"ic_launcher10.png, ic_launcher11.png, ic_launcher12.png, ic_launcher2.png, " +
"ic_launcher3.png... (6 more) [IconDensities]\n" +
"res/drawable-xhdpi: Warning: Missing the following drawables in drawable-xhdpi: " +
"ic_launcher10.png, ic_launcher11.png, ic_launcher12.png, ic_launcher2.png, " +
"ic_launcher3.png... (6 more) [IconDensities]\n" +
"0 errors, 2 warnings\n",
lintProject(
// Use minSDK4 to ensure that we get warnings about missing drawables
"apicheck/minsdk4.xml=>AndroidManifest.xml",
"res/drawable/ic_launcher.png=>res/drawable-hdpi/ic_launcher1.png",
"res/drawable/ic_launcher.png=>res/drawable-xhdpi/ic_launcher1.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher1.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher2.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher3.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher4.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher5.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher6.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher7.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher8.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher9.webp",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher10.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher11.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher12.png"
));
}
public void testShowAll() throws Exception {
mEnabled = Collections.singleton(IconDetector.ICON_DENSITIES);
mAbbreviate = false;
assertEquals(
"res/drawable-hdpi: Warning: Missing the following drawables in drawable-hdpi: " +
"ic_launcher10.png, ic_launcher11.png, ic_launcher12.png, ic_launcher2.png, " +
"ic_launcher3.png, ic_launcher4.png, ic_launcher5.png, ic_launcher6.png, " +
"ic_launcher7.png, ic_launcher8.png, ic_launcher9.png [IconDensities]\n" +
"res/drawable-xhdpi: Warning: Missing the following drawables in drawable-xhdpi: " +
"ic_launcher10.png, ic_launcher11.png, ic_launcher12.png, ic_launcher2.png," +
" ic_launcher3.png, ic_launcher4.png, ic_launcher5.png, ic_launcher6.png, " +
"ic_launcher7.png, ic_launcher8.png, ic_launcher9.png [IconDensities]\n" +
"0 errors, 2 warnings\n",
lintProject(
// Use minSDK4 to ensure that we get warnings about missing drawables
"apicheck/minsdk4.xml=>AndroidManifest.xml",
"res/drawable/ic_launcher.png=>res/drawable-hdpi/ic_launcher1.png",
"res/drawable/ic_launcher.png=>res/drawable-xhdpi/ic_launcher1.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher1.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher2.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher3.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher4.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher5.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher6.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher7.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher8.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher9.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher10.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher11.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher12.png"
));
}
public void testIgnoreMissingFolders() throws Exception {
mEnabled = Collections.singleton(IconDetector.ICON_DENSITIES);
assertEquals(
"No warnings.",
lintProject(
// Use minSDK4 to ensure that we get warnings about missing drawables
"apicheck/minsdk4.xml=>AndroidManifest.xml",
"ignoremissing.xml=>lint.xml",
"res/drawable/ic_launcher.png=>res/drawable-hdpi/ic_launcher1.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher1.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher2.png"
));
}
public void testSquareLauncher() throws Exception {
mEnabled = Collections.singleton(IconDetector.ICON_LAUNCHER_SHAPE);
assertEquals(
"res/drawable-hdpi/ic_launcher_filled.png: Warning: Launcher icons should not fill every pixel of their square region; see the design guide for details [IconLauncherShape]\n" +
"0 errors, 1 warnings\n",
lintProject(
"apicheck/minsdk4.xml=>AndroidManifest.xml",
"res/drawable-hdpi/filled.png=>res/drawable-hdpi/ic_launcher_filled.png",
"res/drawable-mdpi/sample_icon.gif=>res/drawable-mdpi/ic_launcher_2.gif"
));
}
public void testMixNinePatch() throws Exception {
// https://code.google.com/p/android/issues/detail?id=43075
mEnabled = Collections.singleton(IconDetector.ICON_MIX_9PNG);
assertEquals(""
+ "res/drawable-mdpi/ic_launcher_filled.png: Warning: The files ic_launcher_filled.png and ic_launcher_filled.9.png clash; both will map to @drawable/ic_launcher_filled [IconMixedNinePatch]\n"
+ " res/drawable-hdpi/ic_launcher_filled.png: <No location-specific message\n"
+ " res/drawable-hdpi/ic_launcher_filled.9.png: <No location-specific message\n"
+ "0 errors, 1 warnings\n",
lintProject(
"apicheck/minsdk4.xml=>AndroidManifest.xml",
"res/drawable-hdpi/filled.png=>res/drawable-mdpi/ic_launcher_filled.png",
"res/drawable-hdpi/filled.png=>res/drawable-hdpi/ic_launcher_filled.png",
"res/drawable-hdpi/filled.png=>res/drawable-hdpi/ic_launcher_filled.9.png",
"res/drawable-mdpi/sample_icon.gif=>res/drawable-mdpi/ic_launcher_2.gif"
));
}
public void test67486() throws Exception {
// Regression test for https://code.google.com/p/android/issues/detail?id=67486
mEnabled = Collections.singleton(IconDetector.ICON_COLORS);
assertEquals("No warnings.",
lintProject(
"apicheck/minsdk14.xml=>AndroidManifest.xml",
"res/drawable-xhdpi/ic_stat_notify.png=>res/drawable-xhdpi/ic_stat_notify.png"
));
}
public void testDuplicatesWithDpNames() throws Exception {
// Regression test for https://code.google.com/p/android/issues/detail?id=74584
mEnabled = Collections.singleton(IconDetector.DUPLICATES_NAMES);
assertEquals("No warnings.",
lintProject(
"res/drawable-hdpi/unrelated.png=>res/drawable-mdpi/foo_72dp.png",
"res/drawable-hdpi/unrelated.png=>res/drawable-xhdpi/foo_36dp.png"
));
}
public void testClaimedSize() throws Exception {
// Check that icons which declare a dp size actually correspond to that dp size
mEnabled = Collections.singleton(IconDetector.ICON_DIP_SIZE);
assertEquals(""
+ "res/drawable-xhdpi/foo_30dp.png: Warning: Suspicious file name foo_30dp.png: The implied 30 dp size does not match the actual dp size (pixel size 72\u00d772 in a drawable-xhdpi folder computes to 36\u00d736 dp) [IconDipSize]\n"
+ "res/drawable-mdpi/foo_80dp.png: Warning: Suspicious file name foo_80dp.png: The implied 80 dp size does not match the actual dp size (pixel size 72\u00d772 in a drawable-mdpi folder computes to 72\u00d772 dp) [IconDipSize]\n"
+ "0 errors, 2 warnings\n",
lintProject(
"res/drawable-hdpi/unrelated.png=>res/drawable-mdpi/foo_72dp.png", // ok
"res/drawable-hdpi/unrelated.png=>res/drawable-mdpi/foo_80dp.png", // wrong
"res/drawable-hdpi/unrelated.png=>res/drawable-xhdpi/foo_36dp.png", // ok
"res/drawable-hdpi/unrelated.png=>res/drawable-xhdpi/foo_35dp.png", // ~ok
"res/drawable-hdpi/unrelated.png=>res/drawable-xhdpi/foo_30dp.png" // wrong
));
}
public void testResConfigs1() throws Exception {
// resConfigs in the Gradle model sets up the specific set of resource configs
// that are included in the packaging: we use this to limit the set of required
// densities
mEnabled = Sets.newHashSet(IconDetector.ICON_DENSITIES, IconDetector.ICON_MISSING_FOLDER);
assertEquals(""
+ "res: Warning: Missing density variation folders in res: drawable-hdpi [IconMissingDensityFolder]\n"
+ "0 errors, 1 warnings\n",
lintProject(
"res/drawable-mdpi/frame.png",
"res/drawable-nodpi/frame.png",
"res/drawable-xlarge-nodpi-v11/frame.png"));
}
public void testResConfigs2() throws Exception {
mEnabled = Sets.newHashSet(IconDetector.ICON_DENSITIES, IconDetector.ICON_MISSING_FOLDER);
assertEquals(""
+ "res/drawable-hdpi: Warning: Missing the following drawables in drawable-hdpi: sample_icon.gif (found in drawable-mdpi) [IconDensities]\n"
+ "0 errors, 1 warnings\n",
lintProject(
// Use minSDK4 to ensure that we get warnings about missing drawables
"apicheck/minsdk4.xml=>AndroidManifest.xml",
"res/drawable/ic_launcher.png",
"res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher.png",
"res/drawable/ic_launcher.png=>res/drawable-xhdpi/ic_launcher.png",
"res/drawable-mdpi/sample_icon.gif",
"res/drawable-hdpi/ic_launcher.png"));
}
public void testSplits1() throws Exception {
// splits in the Gradle model sets up the specific set of resource configs
// that are included in the packaging: we use this to limit the set of required
// densities
mEnabled = Sets.newHashSet(IconDetector.ICON_DENSITIES, IconDetector.ICON_MISSING_FOLDER);
assertEquals(""
+ "res: Warning: Missing density variation folders in res: drawable-hdpi [IconMissingDensityFolder]\n"
+ "0 errors, 1 warnings\n",
lintProject(
"res/drawable-mdpi/frame.png",
"res/drawable-nodpi/frame.png",
"res/drawable-xlarge-nodpi-v11/frame.png"));
}
@Override
protected TestLintClient createClient() {
String testName = getName();
if (testName.startsWith("testResConfigs")) {
return createClientForTestResConfigs();
} else if (testName.startsWith("testSplits")) {
return createClientForTestSplits();
} else {
return super.createClient();
}
}
private TestLintClient createClientForTestResConfigs() {
// 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 "mdpi"
}
flavorDimensions "pricing", "releaseType"
productFlavors {
beta {
flavorDimension "releaseType"
resConfig "en"
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", "nodpi");
when(flavorBeta.getResourceConfigurations()).thenReturn(resConfigs);
ProductFlavor defaultFlavor = mock(ProductFlavor.class);
when(defaultFlavor.getName()).thenReturn("main");
when(defaultFlavor.getResourceConfigurations()).thenReturn(
Collections.singleton("mdpi"));
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;
}
};
}
};
}
private TestLintClient createClientForTestSplits() {
// 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 debug in this setup:
splits {
density {
enable true
reset()
include "mdpi", "hdpi"
}
}
*/
ProductFlavor defaultFlavor = mock(ProductFlavor.class);
when(defaultFlavor.getName()).thenReturn("main");
when(defaultFlavor.getResourceConfigurations()).thenReturn(
Collections.<String>emptyList());
ProductFlavorContainer defaultContainer =
mock(ProductFlavorContainer.class);
when(defaultContainer.getProductFlavor()).thenReturn(defaultFlavor);
AndroidProject project = mock(AndroidProject.class);
when(project.getProductFlavors()).thenReturn(
Collections.<ProductFlavorContainer>emptyList());
when(project.getDefaultConfig()).thenReturn(defaultContainer);
return project;
}
@Nullable
@Override
public Variant getCurrentVariant() {
Collection<AndroidArtifactOutput> outputs = Lists.newArrayList();
outputs.add(createAndroidArtifactOutput("", ""));
outputs.add(createAndroidArtifactOutput("DENSITY", "mdpi"));
outputs.add(createAndroidArtifactOutput("DENSITY", "hdpi"));
AndroidArtifact mainArtifact = mock(AndroidArtifact.class);
when(mainArtifact.getOutputs()).thenReturn(outputs);
List<String> productFlavorNames = Collections.emptyList();
Variant mock = mock(Variant.class);
when(mock.getProductFlavors()).thenReturn(productFlavorNames);
when(mock.getMainArtifact()).thenReturn(mainArtifact);
return mock;
}
private AndroidArtifactOutput createAndroidArtifactOutput(
@NonNull String filterType,
@NonNull String identifier) {
AndroidArtifactOutput artifactOutput = mock(
AndroidArtifactOutput.class);
OutputFile outputFile = mock(OutputFile.class);
if (filterType.isEmpty()) {
when(outputFile.getFilterTypes())
.thenReturn(Collections.<String>emptyList());
when(outputFile.getFilters())
.thenReturn(Collections.<FilterData>emptyList());
} else {
when(outputFile.getFilterTypes())
.thenReturn(Collections.singletonList(filterType));
List<FilterData> filters = Lists.newArrayList();
FilterData filter = mock(FilterData.class);
when(filter.getFilterType()).thenReturn(filterType);
when(filter.getIdentifier()).thenReturn(identifier);
filters.add(filter);
when(outputFile.getFilters()).thenReturn(filters);
}
// Work around wildcard capture
//when(artifactOutput.getOutputs()).thenReturn(outputFiles);
List<OutputFile> outputFiles = Collections.singletonList(outputFile);
OngoingStubbing<? extends Collection<? extends OutputFile>> when = when(
artifactOutput.getOutputs());
//noinspection unchecked,RedundantCast
((OngoingStubbing<Collection<? extends OutputFile>>) (OngoingStubbing<?>) when)
.thenReturn(outputFiles);
return artifactOutput;
}
};
}
};
}
}