Support for writing multiple maxTargetSdk APIs to one file.

Since the rest of the support for per-SDK level enforcement is not in
place yet, we just write APIs with maxTargetSsk or P and Q to the same
file.

Test: atest class2greylisttest
Bug: 114361293
Change-Id: Ie6f3e8abf5480fc885230947ce2be19e326a309f
diff --git a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
index 9262076..53157a3 100644
--- a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
+++ b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
@@ -33,10 +33,12 @@
 import java.io.File;
 import java.io.IOException;
 import java.nio.charset.Charset;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -71,9 +73,10 @@
                 .hasArgs()
                 .withDescription(
                         "Specify file to write greylist to. Can be specified multiple times. " +
-                        "Format is either just a filename, or \"int:filename\". If an integer is " +
-                        "given, members with a matching maxTargetSdk are written to the file; if " +
-                        "no integer is given, members with no maxTargetSdk are written.")
+                        "Format is either just a filename, or \"int[,int,...]:filename\". If " +
+                        "integers are given, members with matching maxTargetSdk values are " +
+                        "written to the file; if no integer or \"none\" is given, members with " +
+                        "no maxTargetSdk are written.")
                 .create("g"));
         options.addOption(OptionBuilder
                 .withLongOpt("write-whitelist")
@@ -204,15 +207,22 @@
     static Map<Integer, String> readGreylistMap(Status status, String[] argValues) {
         Map<Integer, String> map = new HashMap<>();
         for (String sdkFile : argValues) {
-            Integer maxTargetSdk = null;
+            List<Integer> maxTargetSdks = new ArrayList<>();
             String filename;
             int colonPos = sdkFile.indexOf(':');
             if (colonPos != -1) {
-                try {
-                    maxTargetSdk = Integer.valueOf(sdkFile.substring(0, colonPos));
-                } catch (NumberFormatException nfe) {
-                    status.error("Not a valid integer: %s from argument value '%s'",
-                            sdkFile.substring(0, colonPos), sdkFile);
+                String[] targets = sdkFile.substring(0, colonPos).split(",");
+                for (String target : targets) {
+                    if ("none".equals(target)) {
+                        maxTargetSdks.add(null);
+                    } else {
+                        try {
+                            maxTargetSdks.add(Integer.valueOf(target));
+                        } catch (NumberFormatException nfe) {
+                            status.error("Not a valid integer: %s from argument value '%s'",
+                                    sdkFile.substring(0, colonPos), sdkFile);
+                        }
+                    }
                 }
                 filename = sdkFile.substring(colonPos + 1);
                 if (filename.length() == 0) {
@@ -220,13 +230,16 @@
                             filename, sdkFile);
                 }
             } else {
-                maxTargetSdk = null;
+                maxTargetSdks.add(null);
                 filename = sdkFile;
             }
-            if (map.containsKey(maxTargetSdk)) {
-                status.error("Multiple output files for maxTargetSdk %s", maxTargetSdk);
-            } else {
-                map.put(maxTargetSdk, filename);
+            for (Integer maxTargetSdk : maxTargetSdks) {
+                if (map.containsKey(maxTargetSdk)) {
+                    status.error("Multiple output files for maxTargetSdk %s",
+                            maxTargetSdk == null ? "none" : maxTargetSdk);
+                } else {
+                    map.put(maxTargetSdk, filename);
+                }
             }
         }
         return map;
diff --git a/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java
index 9f33467..bfd2310 100644
--- a/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java
+++ b/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java
@@ -1,5 +1,7 @@
 package com.android.class2greylist;
 
+import com.google.common.annotations.VisibleForTesting;
+
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
@@ -20,11 +22,16 @@
         return new PrintStream(new FileOutputStream(new File(filename)));
     }
 
-    private static Map<Integer, PrintStream> openFiles(
+    @VisibleForTesting
+    public static Map<Integer, PrintStream> openFiles(
             Map<Integer, String> filenames) throws FileNotFoundException {
+        Map<String, PrintStream> streamsByName = new HashMap<>();
         Map<Integer, PrintStream> streams = new HashMap<>();
         for (Map.Entry<Integer, String> entry : filenames.entrySet()) {
-            streams.put(entry.getKey(), openFile(entry.getValue()));
+            if (!streamsByName.containsKey(entry.getValue())) {
+                streamsByName.put(entry.getValue(), openFile(entry.getValue()));
+            }
+            streams.put(entry.getKey(), streamsByName.get(entry.getValue()));
         }
         return streams;
     }
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java b/tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java
index cb75dd3..b87a5b1 100644
--- a/tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java
+++ b/tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java
@@ -56,6 +56,31 @@
     }
 
     @Test
+    public void testReadGreylistMapNone() throws IOException {
+        Map<Integer, String> map = Class2Greylist.readGreylistMap(mStatus,
+                new String[]{"none:noApi"});
+        verifyZeroInteractions(mStatus);
+        assertThat(map).containsExactly(null, "noApi");
+    }
+
+    @Test
+    public void testReadGreylistMapMulti() throws IOException {
+        Map<Integer, String> map = Class2Greylist.readGreylistMap(mStatus,
+                new String[]{"1,none:noOr1Api", "3:apiThree"});
+        verifyZeroInteractions(mStatus);
+        assertThat(map).containsExactly(null, "noOr1Api", 1, "noOr1Api", 3, "apiThree");
+    }
+
+    @Test
+    public void testReadGreylistMapMulti2() throws IOException {
+        Map<Integer, String> map = Class2Greylist.readGreylistMap(mStatus,
+                new String[]{"1,none,2,3,4:allApi"});
+        verifyZeroInteractions(mStatus);
+        assertThat(map).containsExactly(
+                null, "allApi", 1, "allApi", 2, "allApi", 3, "allApi", 4, "allApi");
+    }
+
+    @Test
     public void testReadGreylistMapDuplicate() throws IOException {
         Class2Greylist.readGreylistMap(mStatus,
                 new String[]{"noApi", "1:apiOne", "1:anotherOne"});
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/FileWritingGreylistConsumerTest.java b/tools/class2greylist/test/src/com/android/class2greylist/FileWritingGreylistConsumerTest.java
new file mode 100644
index 0000000..1e1b1df
--- /dev/null
+++ b/tools/class2greylist/test/src/com/android/class2greylist/FileWritingGreylistConsumerTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2018 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.class2greylist;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.mockito.Mock;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public class FileWritingGreylistConsumerTest {
+
+    @Mock
+    Status mStatus;
+    @Rule
+    public TestName mTestName = new TestName();
+    private int mFileNameSeq = 0;
+    private final List<String> mTempFiles = new ArrayList<>();
+
+    @Before
+    public void setup() throws IOException {
+        System.out.println(String.format("\n============== STARTING TEST: %s ==============\n",
+                mTestName.getMethodName()));
+        initMocks(this);
+    }
+
+    @After
+    public void removeTempFiles() {
+        for (String name : mTempFiles) {
+            new File(name).delete();
+        }
+    }
+
+    private String tempFileName() {
+        String name = String.format(Locale.US, "/tmp/test-%s-%d",
+                mTestName.getMethodName(), mFileNameSeq++);
+        mTempFiles.add(name);
+        return name;
+    }
+
+    @Test
+    public void testSimpleMap() throws FileNotFoundException {
+        Map<Integer, PrintStream> streams = FileWritingGreylistConsumer.openFiles(
+                ImmutableMap.of(1, tempFileName(), 2, tempFileName()));
+        assertThat(streams.keySet()).containsExactly(1, 2);
+        assertThat(streams.get(1)).isNotNull();
+        assertThat(streams.get(2)).isNotNull();
+        assertThat(streams.get(2)).isNotSameAs(streams.get(1));
+    }
+
+    @Test
+    public void testCommonMappings() throws FileNotFoundException {
+        String name = tempFileName();
+        Map<Integer, PrintStream> streams = FileWritingGreylistConsumer.openFiles(
+                ImmutableMap.of(1, name, 2, name));
+        assertThat(streams.keySet()).containsExactly(1, 2);
+        assertThat(streams.get(1)).isNotNull();
+        assertThat(streams.get(2)).isSameAs(streams.get(1));
+    }
+}