blob: 5924db87f3fe042cb572148559e0504de38f5a1c [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.build.gradle.integration.application;
import static com.android.build.gradle.integration.common.fixture.GradleTestProject.ApkType;
import static com.android.build.gradle.tasks.ResourceUsageAnalyzer.REPLACE_DELETED_WITH_EMPTY;
import static com.android.builder.internal.packaging.ApkCreatorType.APK_FLINGER;
import static com.android.builder.internal.packaging.ApkCreatorType.APK_Z_FILE_CREATOR;
import static com.android.testutils.truth.ZipFileSubject.assertThat;
import static com.google.common.truth.Truth.assertThat;
import static java.io.File.separator;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import com.android.annotations.NonNull;
import com.android.build.gradle.integration.common.fixture.BaseGradleExecutor;
import com.android.build.gradle.integration.common.fixture.GradleTestProject;
import com.android.build.gradle.integration.common.utils.GradleTestProjectUtils;
import com.android.build.gradle.integration.common.utils.TestFileUtils;
import com.android.build.gradle.internal.res.shrinker.DummyContent;
import com.android.build.gradle.internal.scope.InternalArtifactType;
import com.android.build.gradle.options.BooleanOption;
import com.android.build.gradle.options.OptionalBooleanOption;
import com.android.builder.internal.packaging.ApkCreatorType;
import com.android.builder.model.AndroidProject;
import com.android.builder.model.CodeShrinker;
import com.android.testutils.apk.Apk;
import com.android.testutils.apk.Zip;
import com.android.utils.FileUtils;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarInputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
/** Assemble tests for shrink. */
@RunWith(Parameterized.class)
public class ShrinkResourcesOldShrinkerTest {
@Rule
public GradleTestProject project =
GradleTestProject.builder()
.fromTestProject("shrink")
// http://b/149978740
.withConfigurationCaching(BaseGradleExecutor.ConfigurationCaching.OFF)
.create();
@Parameterized.Parameters(name = "shrinker={0} bundle={1} apkCreatorType={2} useRTxt={3}")
public static Iterable<Object[]> data() {
return ImmutableList.of(
// R classes and old resource shrinker.
new Object[]{
CodeShrinker.PROGUARD, ApkPipeline.NO_BUNDLE, APK_Z_FILE_CREATOR, false
},
new Object[]{CodeShrinker.R8, ApkPipeline.NO_BUNDLE, APK_Z_FILE_CREATOR, false},
new Object[]{CodeShrinker.PROGUARD, ApkPipeline.BUNDLE, APK_Z_FILE_CREATOR, false},
new Object[]{CodeShrinker.R8, ApkPipeline.BUNDLE, APK_Z_FILE_CREATOR, false},
new Object[]{CodeShrinker.PROGUARD, ApkPipeline.NO_BUNDLE, APK_FLINGER, false},
new Object[]{CodeShrinker.R8, ApkPipeline.NO_BUNDLE, APK_FLINGER, false},
// R text files and old resource shrinker.
new Object[]{
CodeShrinker.PROGUARD, ApkPipeline.NO_BUNDLE, APK_Z_FILE_CREATOR, true
},
new Object[]{CodeShrinker.R8, ApkPipeline.NO_BUNDLE, APK_Z_FILE_CREATOR, true},
new Object[]{CodeShrinker.PROGUARD, ApkPipeline.BUNDLE, APK_Z_FILE_CREATOR, true},
new Object[]{CodeShrinker.R8, ApkPipeline.BUNDLE, APK_Z_FILE_CREATOR, true},
new Object[]{CodeShrinker.PROGUARD, ApkPipeline.NO_BUNDLE, APK_FLINGER, true},
new Object[]{CodeShrinker.R8, ApkPipeline.NO_BUNDLE, APK_FLINGER, true}
);
}
@Parameterized.Parameter
public CodeShrinker shrinker;
@Parameterized.Parameter(1)
public ApkPipeline apkPipeline;
@Parameterized.Parameter(2)
public ApkCreatorType apkCreatorType;
@Parameterized.Parameter(3)
public Boolean useRTxt;
private enum ApkPipeline {
NO_BUNDLE("assemble", ""),
BUNDLE("package", "UniversalApk"),
;
private final String taskPrefix;
private final String taskSuffix;
ApkPipeline(String taskPrefix, String taskSuffix) {
this.taskPrefix = taskPrefix;
this.taskSuffix = taskSuffix;
}
String taskName(String variant) {
return taskPrefix + variant + taskSuffix;
}
}
private Apk getApk(@NonNull ApkType apkType) throws IOException {
switch (apkPipeline) {
case NO_BUNDLE:
return project.getApk(apkType);
case BUNDLE:
return project.getBundleUniversalApk(apkType);
}
throw new IllegalStateException();
}
private File getCompressed(@NonNull GradleTestProject project) {
switch (apkPipeline) {
case NO_BUNDLE:
return project.getIntermediateFile(
"shrunk_processed_res", "release", "resources-release-stripped.ap_");
case BUNDLE:
return project.getIntermediateFile(
"legacy_shrunk_linked_res_for_bundle",
"release",
"shrunk-bundled-res.ap_");
}
throw new IllegalStateException();
}
private File getUncompressed(@NonNull GradleTestProject project) {
switch (apkPipeline) {
case NO_BUNDLE:
return project.getIntermediateFile(
"processed_res",
"release",
"out",
"resources-release.ap_");
case BUNDLE:
return project.getIntermediateFile(
"linked_res_for_bundle",
"release",
"bundled-res.ap_");
}
throw new IllegalStateException();
}
private byte[] getIntermediateCompressedXml() {
switch (apkPipeline) {
case NO_BUNDLE:
return DummyContent.TINY_BINARY_XML;
case BUNDLE:
return DummyContent.TINY_PROTO_XML;
}
throw new IllegalStateException();
}
private String getIntermediateResourceTableName() {
switch (apkPipeline) {
case NO_BUNDLE:
//noinspection SpellCheckingInspection
return "resources.arsc";
case BUNDLE:
return "resources.pb";
}
throw new IllegalStateException();
}
private String getIntermediateResourceTableNameAndMethod() {
switch (apkPipeline) {
case NO_BUNDLE:
//noinspection SpellCheckingInspection
return " stored resources.arsc";
case BUNDLE:
return "deflated resources.pb";
}
throw new IllegalStateException();
}
private void useIntermediateResourceTableName(@NonNull List<String> expected) {
switch (apkPipeline) {
case NO_BUNDLE:
return;
case BUNDLE:
expected.set(expected.indexOf("resources.arsc"), "resources.pb");
return;
}
throw new IllegalStateException();
}
@Test
public void checkShrinkResources() throws Exception {
TestFileUtils.appendToFile(
project.getBuildFile(),
"android.buildTypes.release.useProguard = " + (shrinker == CodeShrinker.PROGUARD));
GradleTestProjectUtils.setApkCreatorType(project, apkCreatorType);
project.executor()
.with(OptionalBooleanOption.INTERNAL_ONLY_ENABLE_R8, shrinker == CodeShrinker.R8)
.with(BooleanOption.ENABLE_R_TXT_RESOURCE_SHRINKING, useRTxt)
.with(BooleanOption.ENABLE_NEW_RESOURCE_SHRINKER, false)
.run(
"clean",
apkPipeline.taskName("Release"),
apkPipeline.taskName("Debug"),
apkPipeline.taskName("MinifyDontShrink"));
File intermediates = project.file("build/" + AndroidProject.FD_INTERMEDIATES);
// The release target has shrinking enabled.
// The minifyDontShrink target has proguard but no shrinking enabled.
// The debug target has neither proguard nor shrinking enabled.
Apk apkRelease = getApk(ApkType.RELEASE);
Apk apkDebug = getApk(ApkType.DEBUG);
Apk apkProguardOnly = getApk(ApkType.of("minifyDontShrink", false));
assertTrue(apkDebug.toString() + " is not a file", Files.isRegularFile(apkDebug.getFile()));
assertTrue(
apkRelease.toString() + " is not a file",
Files.isRegularFile(apkRelease.getFile()));
assertTrue(
apkProguardOnly.toString() + " is not a file",
Files.isRegularFile(apkProguardOnly.getFile()));
File compressed = getCompressed(project);
File uncompressed = getUncompressed(project);
assertTrue(compressed.toString() + " is not a file", compressed.isFile());
assertTrue(uncompressed.toString() + " is not a file", uncompressed.isFile());
// Check that there is no shrinking in the other two targets:
assertTrue(
FileUtils.join(
intermediates,
"processed_res",
"debug",
"out",
"resources-debug.ap_")
.exists());
assertFalse(
FileUtils.join(
intermediates,
"shrunk_processed_res/debug"
+ separator
+ "resources-debug-stripped.ap_")
.exists());
assertTrue(
FileUtils.join(
intermediates,
"processed_res",
"minifyDontShrink",
"out",
"resources-minifyDontShrink.ap_")
.exists());
assertFalse(
new File(
intermediates,
"shrunk_processed_res/minifyDontShrink"
+ separator
+ "resources-minifyDontShrink-stripped.ap_")
.exists());
List<String> expectedUnstrippedApk =
ImmutableList.of(
"AndroidManifest.xml",
"classes.dex",
"res/drawable/force_remove.xml",
"res/raw/keep.xml",
"res/layout/l_used_a.xml",
"res/layout/l_used_b2.xml",
"res/layout/l_used_c.xml",
"res/layout/lib_unused.xml",
"res/layout/marked_as_used_by_old.xml",
"res/layout-v17/notification_action.xml",
"res/layout-v21/notification_action.xml",
"res/layout/notification_action.xml",
"res/drawable-v21/notification_action_background.xml",
"res/layout-v17/notification_action_tombstone.xml",
"res/layout-v21/notification_action_tombstone.xml",
"res/layout/notification_action_tombstone.xml",
"res/drawable/notification_bg.xml",
"res/drawable/notification_bg_low.xml",
"res/drawable-hdpi-v4/notification_bg_low_normal.9.png",
"res/drawable-mdpi-v4/notification_bg_low_normal.9.png",
"res/drawable-xhdpi-v4/notification_bg_low_normal.9.png",
"res/drawable-hdpi-v4/notification_bg_low_pressed.9.png",
"res/drawable-mdpi-v4/notification_bg_low_pressed.9.png",
"res/drawable-xhdpi-v4/notification_bg_low_pressed.9.png",
"res/drawable-hdpi-v4/notification_bg_normal.9.png",
"res/drawable-mdpi-v4/notification_bg_normal.9.png",
"res/drawable-xhdpi-v4/notification_bg_normal.9.png",
"res/drawable-hdpi-v4/notification_bg_normal_pressed.9.png",
"res/drawable-mdpi-v4/notification_bg_normal_pressed.9.png",
"res/drawable-xhdpi-v4/notification_bg_normal_pressed.9.png",
"res/drawable/notification_icon_background.xml",
"res/layout/notification_media_action.xml",
"res/layout/notification_media_cancel_action.xml",
"res/layout-v17/notification_template_big_media.xml",
"res/layout/notification_template_big_media.xml",
"res/layout-v17/notification_template_big_media_custom.xml",
"res/layout/notification_template_big_media_custom.xml",
"res/layout-v17/notification_template_big_media_narrow.xml",
"res/layout/notification_template_big_media_narrow.xml",
"res/layout-v17/notification_template_big_media_narrow_custom.xml",
"res/layout/notification_template_big_media_narrow_custom.xml",
"res/layout-v16/notification_template_custom_big.xml",
"res/layout-v17/notification_template_custom_big.xml",
"res/layout-v21/notification_template_custom_big.xml",
"res/layout-v21/notification_template_icon_group.xml",
"res/layout/notification_template_icon_group.xml",
"res/layout-v17/notification_template_lines_media.xml",
"res/layout/notification_template_lines_media.xml",
"res/layout-v17/notification_template_media.xml",
"res/layout/notification_template_media.xml",
"res/layout-v17/notification_template_media_custom.xml",
"res/layout/notification_template_media_custom.xml",
"res/layout/notification_template_part_chronometer.xml",
"res/layout/notification_template_part_time.xml",
"res/drawable/notification_tile_bg.xml",
"res/drawable-hdpi-v4/notify_panel_notification_icon_bg.png",
"res/drawable-mdpi-v4/notify_panel_notification_icon_bg.png",
"res/drawable-xhdpi-v4/notify_panel_notification_icon_bg.png",
"res/layout/prefix_3_suffix.xml",
"res/layout/prefix_used_1.xml",
"res/layout/prefix_used_2.xml",
"resources.arsc",
"res/layout/unused1.xml",
"res/layout/unused2.xml",
"res/drawable/unused9.xml",
"res/drawable/unused10.xml",
"res/drawable/unused11.xml",
"res/menu/unused12.xml",
"res/layout/unused13.xml",
"res/layout/unused14.xml",
"res/layout/used1.xml",
"res/layout/used2.xml",
"res/layout/used3.xml",
"res/layout/used4.xml",
"res/layout/used5.xml",
"res/layout/used6.xml",
"res/layout/used7.xml",
"res/layout/used8.xml",
"res/drawable/used9.xml",
"res/drawable/used10.xml",
"res/drawable/used11.xml",
"res/drawable/used12.xml",
"res/menu/used13.xml",
"res/layout/used14.xml",
"res/drawable/used15.xml",
"res/layout/used16.xml",
"res/layout/used17.xml",
"res/layout/used18.xml",
"res/layout/used19.xml",
"res/layout/used20.xml");
List<String> expectedStrippedApkContents =
ImmutableList.of(
"AndroidManifest.xml",
"classes.dex",
"res/layout/l_used_a.xml",
"res/layout/l_used_b2.xml",
"res/layout/l_used_c.xml",
"res/layout/marked_as_used_by_old.xml",
"res/layout/prefix_3_suffix.xml",
"res/layout/prefix_used_1.xml",
"res/layout/prefix_used_2.xml",
"resources.arsc",
"res/layout/used1.xml",
"res/layout/used2.xml",
"res/layout/used3.xml",
"res/layout/used4.xml",
"res/layout/used5.xml",
"res/layout/used6.xml",
"res/layout/used7.xml",
"res/layout/used8.xml",
"res/drawable/used9.xml",
"res/drawable/used10.xml",
"res/drawable/used11.xml",
"res/drawable/used12.xml",
"res/menu/used13.xml",
"res/layout/used14.xml",
"res/drawable/used15.xml",
"res/layout/used16.xml",
"res/layout/used17.xml",
"res/layout/used18.xml",
"res/layout/used19.xml",
"res/layout/used20.xml");
List<String> expectedOptimizeApkContents =
ImmutableList.of(
"res/09.9.png",
"res/11.xml",
"res/2P.xml",
"res/3Y.xml",
"res/3j.xml",
"res/3m.xml",
"res/3m1.xml",
"res/4W.xml",
"res/4c.xml",
"res/4u.xml",
"res/56.xml",
"res/5M.xml",
"res/8V.9.png",
"res/8r.xml",
"res/93.9.png",
"res/A1.xml",
"AndroidManifest.xml",
"res/BB.xml",
"res/C7.xml",
"res/Cv.xml",
"res/DS.xml",
"res/E1.xml",
"res/Eq.xml",
"res/FZ.xml",
"res/GQ.xml",
"res/HC.xml",
"res/Hs.xml",
"res/JX.xml",
"res/Jv.xml",
"res/LD.png",
"res/Lb.xml",
"res/NR.xml",
"res/Nf.xml",
"res/O3.9.png",
"res/Ot.png",
"res/PP.xml",
"res/Pg.xml",
"res/Pq.9.png",
"res/QR.xml",
"res/Qv.png",
"res/SH.xml",
"res/SS.xml",
"res/T2.9.png",
"res/TW.xml",
"res/Tq.xml",
"res/UT.xml",
"res/WV.xml",
"res/WV1.xml",
"res/WV2.xml",
"res/WV3.xml",
"res/WV4.xml",
"res/WV5.xml",
"res/WV6.xml",
"res/WV7.xml",
"res/X8.xml",
"res/XB.xml",
"res/Xs.9.png",
"res/Z2.xml",
"res/ZX.xml",
"res/cH.xml",
"classes.dex",
"res/dH.9.png",
"res/eK.9.png",
"res/fS.xml",
"res/f_.xml",
"res/gW.xml",
"res/hC.xml",
"res/hj.9.png",
"res/iL.xml",
"res/jK.9.png",
"res/kI.xml",
"res/kM.xml",
"res/m1.xml",
"res/n1.xml",
"res/nD.xml",
"res/oy.xml",
"res/pA.xml",
"res/q6.xml",
"res/qj.xml",
"res/r2.xml",
"res/rK.xml",
"resources.arsc",
"res/tf.xml",
"res/tr.9.png",
"res/u6.xml",
"res/um.xml",
"res/yX.xml",
"res/yX1.xml",
"res/ya.xml",
"res/yp.xml");
if (REPLACE_DELETED_WITH_EMPTY) {
// If replacing deleted files with empty files, the file list will include
// the "unused" files too, though they will be much smaller. This is checked
// later on in the test.
expectedStrippedApkContents =
ImmutableList.of(
"AndroidManifest.xml",
"classes.dex",
"res/drawable/force_remove.xml",
"res/raw/keep.xml",
"res/layout/l_used_a.xml",
"res/layout/l_used_b2.xml",
"res/layout/l_used_c.xml",
"res/layout/lib_unused.xml",
"res/layout/marked_as_used_by_old.xml",
"res/layout-v17/notification_action.xml",
"res/layout-v21/notification_action.xml",
"res/layout/notification_action.xml",
"res/drawable-v21/notification_action_background.xml",
"res/layout-v17/notification_action_tombstone.xml",
"res/layout-v21/notification_action_tombstone.xml",
"res/layout/notification_action_tombstone.xml",
"res/drawable/notification_bg.xml",
"res/drawable/notification_bg_low.xml",
"res/drawable-hdpi-v4/notification_bg_low_normal.9.png",
"res/drawable-mdpi-v4/notification_bg_low_normal.9.png",
"res/drawable-xhdpi-v4/notification_bg_low_normal.9.png",
"res/drawable-hdpi-v4/notification_bg_low_pressed.9.png",
"res/drawable-mdpi-v4/notification_bg_low_pressed.9.png",
"res/drawable-xhdpi-v4/notification_bg_low_pressed.9.png",
"res/drawable-hdpi-v4/notification_bg_normal.9.png",
"res/drawable-mdpi-v4/notification_bg_normal.9.png",
"res/drawable-xhdpi-v4/notification_bg_normal.9.png",
"res/drawable-hdpi-v4/notification_bg_normal_pressed.9.png",
"res/drawable-mdpi-v4/notification_bg_normal_pressed.9.png",
"res/drawable-xhdpi-v4/notification_bg_normal_pressed.9.png",
"res/drawable/notification_icon_background.xml",
"res/layout/notification_media_action.xml",
"res/layout/notification_media_cancel_action.xml",
"res/layout-v17/notification_template_big_media.xml",
"res/layout/notification_template_big_media.xml",
"res/layout-v17/notification_template_big_media_custom.xml",
"res/layout/notification_template_big_media_custom.xml",
"res/layout-v17/notification_template_big_media_narrow.xml",
"res/layout/notification_template_big_media_narrow.xml",
"res/layout-v17/notification_template_big_media_narrow_custom.xml",
"res/layout/notification_template_big_media_narrow_custom.xml",
"res/layout-v16/notification_template_custom_big.xml",
"res/layout-v17/notification_template_custom_big.xml",
"res/layout-v21/notification_template_custom_big.xml",
"res/layout-v21/notification_template_icon_group.xml",
"res/layout/notification_template_icon_group.xml",
"res/layout-v17/notification_template_lines_media.xml",
"res/layout/notification_template_lines_media.xml",
"res/layout-v17/notification_template_media.xml",
"res/layout/notification_template_media.xml",
"res/layout-v17/notification_template_media_custom.xml",
"res/layout/notification_template_media_custom.xml",
"res/layout/notification_template_part_chronometer.xml",
"res/layout/notification_template_part_time.xml",
"res/drawable/notification_tile_bg.xml",
"res/drawable-hdpi-v4/notify_panel_notification_icon_bg.png",
"res/drawable-mdpi-v4/notify_panel_notification_icon_bg.png",
"res/drawable-xhdpi-v4/notify_panel_notification_icon_bg.png",
"res/layout/prefix_3_suffix.xml",
"res/layout/prefix_used_1.xml",
"res/layout/prefix_used_2.xml",
"resources.arsc",
"res/layout/unused1.xml",
"res/layout/unused2.xml",
"res/drawable/unused9.xml",
"res/drawable/unused10.xml",
"res/drawable/unused11.xml",
"res/menu/unused12.xml",
"res/layout/unused13.xml",
"res/layout/unused14.xml",
"res/layout/used1.xml",
"res/layout/used2.xml",
"res/layout/used3.xml",
"res/layout/used4.xml",
"res/layout/used5.xml",
"res/layout/used6.xml",
"res/layout/used7.xml",
"res/layout/used8.xml",
"res/drawable/used9.xml",
"res/drawable/used10.xml",
"res/drawable/used11.xml",
"res/drawable/used12.xml",
"res/menu/used13.xml",
"res/layout/used14.xml",
"res/drawable/used15.xml",
"res/layout/used16.xml",
"res/layout/used17.xml",
"res/layout/used18.xml",
"res/layout/used19.xml",
"res/layout/used20.xml");
}
// Should not have any unused resources in the compressed list
if (!REPLACE_DELETED_WITH_EMPTY) {
assertThat(Joiner.on('\n').join(expectedStrippedApkContents)).doesNotContain("unused");
}
// Should have *all* the used resources, currently 1-20
for (int i = 1; i <= 20; i++) {
String name = "/used" + i + ".";
assertTrue(
"Missing used" + i + " in " + expectedStrippedApkContents,
expectedStrippedApkContents.stream().anyMatch((it) -> it.contains(name)));
}
// Check that the uncompressed resources (.ap_) for the release target have everything
// we expect
List<String> expectedUncompressed = new ArrayList<>(expectedUnstrippedApk);
expectedUncompressed.remove("classes.dex");
useIntermediateResourceTableName(expectedUncompressed);
assertThat(dumpZipContents(uncompressed))
.named("uncompressed")
.containsExactlyElementsIn(expectedUncompressed)
.inOrder();
// The debug target should have everything there in the APK
assertThat(dumpZipContents(apkDebug.getFile()))
.containsExactlyElementsIn(expectedUnstrippedApk)
.inOrder();
if (FileUtils.join(
intermediates,
InternalArtifactType.OPTIMIZED_PROCESSED_RES.INSTANCE.getFolderName())
.exists()) {
assertThat(dumpZipContents(apkProguardOnly.getFile()))
.containsExactlyElementsIn(expectedOptimizeApkContents)
.inOrder();
} else {
assertThat(dumpZipContents(apkProguardOnly.getFile()))
.containsExactlyElementsIn(expectedUnstrippedApk)
.inOrder();
}
// Make sure force_remove was replaced with a small file if replacing rather than removing
if (REPLACE_DELETED_WITH_EMPTY) {
try (Zip it = new Zip(compressed)) {
assertThat(it)
.containsFileWithContent(
"res/drawable/force_remove.xml", getIntermediateCompressedXml());
}
}
// Check the compressed .ap_:
List<String> actualCompressed = dumpZipContents(compressed);
List<String> expectedCompressed = new ArrayList<>(expectedStrippedApkContents);
expectedCompressed.remove("classes.dex");
useIntermediateResourceTableName(expectedCompressed);
assertThat(actualCompressed).containsExactlyElementsIn(expectedCompressed).inOrder();
if (!REPLACE_DELETED_WITH_EMPTY) {
assertThat(Joiner.on('\n').join(expectedCompressed)).doesNotContain("unused");
}
if (FileUtils.join(
intermediates,
InternalArtifactType.OPTIMIZED_PROCESSED_RES.INSTANCE.getFolderName())
.exists()) {
assertThat(dumpZipContents(apkRelease.getFile()))
.named("strippedApkContents")
.containsExactlyElementsIn(expectedOptimizeApkContents)
.inOrder();
} else {
assertThat(dumpZipContents(apkRelease.getFile()))
.named("strippedApkContents")
.containsExactlyElementsIn(expectedStrippedApkContents)
.inOrder();
}
// Bundle handles splits anyway.
if (apkPipeline == ApkPipeline.NO_BUNDLE) {
// Check splits -- just sample one of them
//noinspection SpellCheckingInspection
compressed =
project.file(
"abisplits/build/intermediates/shrunk_processed_res/release/resources-arm64-v8a-release-stripped.ap_");
//noinspection SpellCheckingInspection
uncompressed =
project.file(
"abisplits/build/intermediates/processed_res/release/out/resources-arm64-v8aRelease.ap_");
assertTrue(compressed.toString() + " is not a file", compressed.isFile());
assertTrue(uncompressed.toString() + " is not a file", uncompressed.isFile());
//noinspection SpellCheckingInspection
if (REPLACE_DELETED_WITH_EMPTY) {
assertThat(dumpZipContents(compressed))
.containsExactly(
"AndroidManifest.xml",
"resources.arsc",
"res/layout/unused.xml",
"res/layout/used.xml")
.inOrder();
} else {
assertThat(dumpZipContents(compressed))
.containsExactly(
"AndroidManifest.xml", "resources.arsc", "res/layout/used.xml")
.inOrder();
}
//noinspection SpellCheckingInspection
assertThat(dumpZipContents(uncompressed))
.containsExactly(
"AndroidManifest.xml",
"resources.arsc",
"res/layout/unused.xml",
"res/layout/used.xml")
.inOrder();
}
// Check WebView string handling (android_res strings etc)
//noinspection SpellCheckingInspection
uncompressed = getUncompressed(project.getSubproject("webview"));
//noinspection SpellCheckingInspection
compressed = getCompressed(project.getSubproject("webview"));
assertTrue(uncompressed.toString() + " is not a file", uncompressed.isFile());
assertTrue(compressed.toString() + " is not a file", compressed.isFile());
//noinspection SpellCheckingInspection
assertThat(dumpZipContents(uncompressed))
.containsExactly(
"AndroidManifest.xml",
"res/xml/my_xml.xml",
getIntermediateResourceTableName(),
"res/raw/unknown",
"res/raw/unused_icon.png",
"res/raw/unused_index.html",
"res/drawable/used1.xml",
"res/raw/used_icon.png",
"res/raw/used_icon2.png",
"res/raw/used_index.html",
"res/raw/used_index2.html",
"res/raw/used_index3.html",
"res/layout/used_layout1.xml",
"res/layout/used_layout2.xml",
"res/layout/used_layout3.xml",
"res/raw/used_script.js",
"res/raw/used_styles.css",
"res/layout/webview.xml");
//noinspection SpellCheckingInspection
if (REPLACE_DELETED_WITH_EMPTY) {
assertThat(dumpZipContents(compressed))
.containsExactly(
"AndroidManifest.xml",
"res/xml/my_xml.xml",
getIntermediateResourceTableName(),
"res/raw/unknown",
"res/raw/unused_icon.png",
"res/raw/unused_index.html",
"res/drawable/used1.xml",
"res/raw/used_icon.png",
"res/raw/used_icon2.png",
"res/raw/used_index.html",
"res/raw/used_index2.html",
"res/raw/used_index3.html",
"res/layout/used_layout1.xml",
"res/layout/used_layout2.xml",
"res/layout/used_layout3.xml",
"res/raw/used_script.js",
"res/raw/used_styles.css",
"res/layout/webview.xml")
.inOrder();
} else {
assertThat(dumpZipContents(compressed))
.containsExactly(
"AndroidManifest.xml",
getIntermediateResourceTableName(),
"res/raw/unknown",
"res/drawable/used1.xml",
"res/raw/used_icon.png",
"res/raw/used_icon2.png",
"res/raw/used_index.html",
"res/raw/used_index2.html",
"res/raw/used_index3.html",
"res/layout/used_layout1.xml",
"res/layout/used_layout2.xml",
"res/layout/used_layout3.xml",
"res/raw/used_script.js",
"res/raw/used_styles.css",
"res/layout/webview.xml")
.inOrder();
}
// Check stored vs deflated state:
// This is the state of the original source _ap file:
assertThat(dumpZipContents(uncompressed, true))
.containsExactly(
getIntermediateResourceTableNameAndMethod(),
"deflated AndroidManifest.xml",
"deflated res/xml/my_xml.xml",
"deflated res/raw/unknown",
" stored res/raw/unused_icon.png",
"deflated res/raw/unused_index.html",
"deflated res/drawable/used1.xml",
" stored res/raw/used_icon.png",
" stored res/raw/used_icon2.png",
"deflated res/raw/used_index.html",
"deflated res/raw/used_index2.html",
"deflated res/raw/used_index3.html",
"deflated res/layout/used_layout1.xml",
"deflated res/layout/used_layout2.xml",
"deflated res/layout/used_layout3.xml",
"deflated res/raw/used_script.js",
"deflated res/raw/used_styles.css",
"deflated res/layout/webview.xml");
// This is the state of the rewritten ap_ file: the zip states should match
if (REPLACE_DELETED_WITH_EMPTY) {
assertThat(dumpZipContents(compressed, true))
.containsExactly(
getIntermediateResourceTableNameAndMethod(),
"deflated AndroidManifest.xml",
"deflated res/xml/my_xml.xml",
"deflated res/raw/unknown",
" stored res/raw/unused_icon.png",
"deflated res/raw/unused_index.html",
"deflated res/drawable/used1.xml",
" stored res/raw/used_icon.png",
" stored res/raw/used_icon2.png",
"deflated res/raw/used_index.html",
"deflated res/raw/used_index2.html",
"deflated res/raw/used_index3.html",
"deflated res/layout/used_layout1.xml",
"deflated res/layout/used_layout2.xml",
"deflated res/layout/used_layout3.xml",
"deflated res/raw/used_script.js",
"deflated res/raw/used_styles.css",
"deflated res/layout/webview.xml");
} else {
assertThat(dumpZipContents(compressed, true))
.containsExactly(
getIntermediateResourceTableNameAndMethod(),
"deflated AndroidManifest.xml",
"deflated res/raw/unknown",
"deflated res/drawable/used1.xml",
" stored res/raw/used_icon.png",
" stored res/raw/used_icon2.png",
"deflated res/raw/used_index.html",
"deflated res/raw/used_index2.html",
"deflated res/raw/used_index3.html",
"deflated res/layout/used_layout1.xml",
"deflated res/layout/used_layout2.xml",
"deflated res/layout/used_layout3.xml",
"deflated res/raw/used_script.js",
"deflated res/raw/used_styles.css",
"deflated res/layout/webview.xml");
}
// Make sure the (remaining) binary contents of the files in the compressed APK are
// identical to the ones in uncompressed:
FileInputStream fis1 = new FileInputStream(compressed);
JarInputStream zis1 = new JarInputStream(fis1);
FileInputStream fis2 = new FileInputStream(uncompressed);
JarInputStream zis2 = new JarInputStream(fis2);
ZipEntry entry1 = zis1.getNextEntry();
ZipEntry entry2 = zis2.getNextEntry();
while (entry1 != null) {
String name1 = entry1.getName();
String name2 = entry2.getName();
while (!name1.equals(name2)) {
// uncompressed should contain a superset of all the names in compressed
entry2 = zis2.getNextJarEntry();
name2 = entry2.getName();
}
assertEquals(name1, name2);
if (!entry1.isDirectory()) {
assertEquals(name1, entry1.getMethod(), entry2.getMethod());
byte[] bytes1 = ByteStreams.toByteArray(zis1);
byte[] bytes2 = ByteStreams.toByteArray(zis2);
if (REPLACE_DELETED_WITH_EMPTY) {
switch (name1) {
case "res/xml/my_xml.xml":
assertThat(bytes1)
.named(name1)
.isEqualTo(getIntermediateCompressedXml());
break;
case "res/raw/unused_icon.png":
assertThat(bytes1)
.named(name1)
.isEqualTo(DummyContent.TINY_PNG);
break;
case "res/raw/unused_index.html":
assertThat(bytes1).named(name1).isEmpty();
break;
default:
assertThat(bytes1).named(name1).isEqualTo(bytes2);
break;
}
} else {
assertTrue(name1, Arrays.equals(bytes1, bytes2));
}
} else {
assertTrue(entry2.isDirectory());
}
entry1 = zis1.getNextEntry();
entry2 = zis2.getNextEntry();
}
zis1.close();
zis2.close();
//noinspection SpellCheckingInspection
uncompressed = getUncompressed(project.getSubproject("keep"));
//noinspection SpellCheckingInspection
compressed = getCompressed(project.getSubproject("keep"));
assertTrue(uncompressed.toString() + " is not a file", uncompressed.isFile());
assertTrue(compressed.toString() + " is not a file", compressed.isFile());
//noinspection SpellCheckingInspection
assertThat(dumpZipContents(uncompressed))
.containsExactly(
"AndroidManifest.xml",
"res/raw/keep.xml",
getIntermediateResourceTableName(),
"res/layout/unused1.xml",
"res/layout/unused2.xml",
"res/layout/used1.xml")
.inOrder();
//noinspection SpellCheckingInspection
if (REPLACE_DELETED_WITH_EMPTY) {
assertThat(dumpZipContents(compressed))
.containsExactly(
"AndroidManifest.xml",
"res/raw/keep.xml",
getIntermediateResourceTableName(),
"res/layout/unused1.xml",
"res/layout/unused2.xml",
"res/layout/used1.xml")
.inOrder();
} else {
assertThat(dumpZipContents(compressed))
.containsExactly(
"AndroidManifest.xml",
getIntermediateResourceTableName(),
"res/layout/used1.xml")
.inOrder();
}
// Check R class keep rule is removed from proguard files when R.txt is used when useRTxt is
// enabled and kept when useRTxt is disabled.
File proguardFilesIntermediateDir = project.getIntermediateFile("proguard-files");
assertThat(containsRClassKeepRule(proguardFilesIntermediateDir)).isEqualTo(!useRTxt);
}
/** Checks if a Proguard directory files contain a keep rule to keep R class members. */
private static boolean containsRClassKeepRule(File keepRulesDir) throws IOException {
assertTrue(keepRulesDir.isDirectory());
File[] keepRuleFiles = keepRulesDir.listFiles();
assertThat(keepRuleFiles).isNotNull();
for (File file : keepRuleFiles) {
try (Stream<String> stream = Files.lines(file.toPath())) {
if (stream.parallel()
.anyMatch(x -> x.contains("-keepclassmembers class **.R$* {"))) {
return true;
}
}
}
return false;
}
private static List<String> getZipPaths(File zipFile, boolean includeMethod)
throws IOException {
List<String> lines = Lists.newArrayList();
Closer closer = Closer.create();
try (ZipFile zf = new ZipFile(zipFile)) {
Enumeration<? extends ZipEntry> entries = zf.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String path = entry.getName();
if (includeMethod) {
String method;
switch (entry.getMethod()) {
case ZipEntry.STORED:
method = " stored";
break;
case ZipEntry.DEFLATED:
method = "deflated";
break;
default:
method = " unknown";
break;
}
path = method + " " + path;
}
lines.add(path);
}
} catch (Throwable t) {
throw closer.rethrow(t);
} finally {
closer.close();
}
return lines;
}
private static List<String> dumpZipContents(File zipFile) throws IOException {
return dumpZipContents(zipFile, false);
}
private static List<String> dumpZipContents(Path zipFile) throws IOException {
return dumpZipContents(zipFile.toFile(), false);
}
private static List<String> dumpZipContents(File zipFile, final boolean includeMethod)
throws IOException {
List<String> lines = getZipPaths(zipFile, includeMethod);
// Remove META-INF statements
lines.removeIf(s -> s.startsWith("META-INF/"));
// Remove resource files generated by the Bundle Tool.
lines.removeIf(s -> s.matches("res/xml/splits(\\d+).xml"));
// Sort by base name (and numeric sort such that unused10 comes after unused9)
final Pattern pattern = Pattern.compile("(.*[^\\d])(\\d+)(\\..+)?");
lines.sort(
(line1, line2) -> {
String name1 = line1.substring(line1.lastIndexOf('/') + 1);
String name2 = line2.substring(line2.lastIndexOf('/') + 1);
int delta = name1.compareTo(name2);
if (delta != 0) {
// Try to do numeric sort
Matcher match1 = pattern.matcher(name1);
if (match1.matches()) {
Matcher match2 = pattern.matcher(name2);
//noinspection ConstantConditions
if (match2.matches() && match1.group(1).equals(match2.group(1))) {
//noinspection ConstantConditions
int num1 = Integer.parseInt(match1.group(2));
//noinspection ConstantConditions
int num2 = Integer.parseInt(match2.group(2));
if (num1 != num2) {
return num1 - num2;
}
}
}
return delta;
}
if (includeMethod) {
line1 = line1.substring(10);
line2 = line2.substring(10);
}
return line1.compareTo(line2);
});
return ImmutableList.copyOf(lines);
}
}