Move default MimeMap implementation to frameworks.

This is the second attempt to submit this CL. The first attempt
regressed on app startup because RuntimeInit installed the
custom MimeMap from commonInit() which runs post-fork of the zygote,
but that was fixed by installing it pre-fork.

This CL topic moves the default MimeMap implementation to frameworks.
Libcore starts with a minimal implementation sufficient to pass
CtsLibcoreTestCases,  but frameworks can inject the real implementation.
Before this CL topic, the data files and logic (MimeMapImpl) were part of
core-*.jar on device; after this CL, they instead live in framework.jar.

Tests from MimeMapTest that check behavior of that default
implementation also move to a non-libcore CTS test.

Planned work for follow-up CL:
 1. Make CTS more opinionated, with a plan to assert that all of
    the default mappings are present. How exactly the expectated
    mapping will be bundled in CTS is still TBD.
 2. Add a vendor.mime.types file (defaults to empty) where vendors
    can add additional mappings; I plan to make it such that mappings
    in that file are parsed last but never override any earlier
    mappings, as if each mime type / file extension was prefixed
    with '?'.
 3. Perhaps enforce that public APIs android.webkit.MimeTypeMap
    and java.net.URLConnection.getFileNameMap() behave consistently
    with MimeMap.getDefault().

Test: atest CtsLibcoreTestCases
Test: atest CtsMimeMapTestCases
Test: Checked that CtsLibcoreTestCases still passes on a build that
      is missing the MimeMap.setDefault() call from RuntimeInit.java.
Test: Checked that app startup time does not regress as part of this
      CL topic - see http://b/136256059#comment17
Bug: 136256059

Change-Id: I716914bf1a7e6205e539f0551f010615dacb17a8
diff --git a/Android.bp b/Android.bp
index 7577353..05675df 100644
--- a/Android.bp
+++ b/Android.bp
@@ -112,6 +112,14 @@
 }
 
 filegroup {
+    name: "framework-mime-sources",
+    srcs: [
+        "mime/java/**/*.java",
+    ],
+    path: "mime/java",
+}
+
+filegroup {
     name: "framework-opengl-sources",
     srcs: [
         "opengl/java/**/*.java",
@@ -176,6 +184,7 @@
         ":framework-mca-effect-sources",
         ":framework-mca-filterfw-sources",
         ":framework-mca-filterpacks-sources",
+        ":framework-mime-sources",
         ":framework-opengl-sources",
         ":framework-rs-sources",
         ":framework-sax-sources",
@@ -310,7 +319,10 @@
 
     jarjar_rules: ":framework-jarjar-rules",
 
-    static_libs: ["framework-internal-utils"],
+    static_libs: [
+        "framework-internal-utils",
+        "mimemap",
+    ],
 
     required: [
         // TODO: remove gps_debug when the build system propagates "required" properly.
diff --git a/core/java/com/android/internal/os/RuntimeInit.java b/core/java/com/android/internal/os/RuntimeInit.java
index d6caa09..89b0250 100644
--- a/core/java/com/android/internal/os/RuntimeInit.java
+++ b/core/java/com/android/internal/os/RuntimeInit.java
@@ -20,6 +20,7 @@
 import android.app.ActivityManager;
 import android.app.ActivityThread;
 import android.app.ApplicationErrorReport;
+import android.content.type.MimeMapImpl;
 import android.os.Build;
 import android.os.DeadObjectException;
 import android.os.Debug;
@@ -33,6 +34,9 @@
 import com.android.server.NetworkManagementSocketTagger;
 import dalvik.system.RuntimeHooks;
 import dalvik.system.VMRuntime;
+
+import libcore.net.MimeMap;
+
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
@@ -199,6 +203,13 @@
     public static void preForkInit() {
         if (DEBUG) Slog.d(TAG, "Entered preForkInit.");
         RuntimeInit.enableDdms();
+        /*
+         * Replace libcore's minimal default mapping between MIME types and file
+         * extensions with a mapping that's suitable for Android. Android's mapping
+         * contains many more entries that are derived from IANA registrations but
+         * with several customizations (extensions, overrides).
+         */
+        MimeMap.setDefault(MimeMapImpl.createDefaultInstance());
     }
 
     @UnsupportedAppUsage
diff --git a/mime/Android.bp b/mime/Android.bp
new file mode 100644
index 0000000..9303755
--- /dev/null
+++ b/mime/Android.bp
@@ -0,0 +1,43 @@
+// Copyright (C) 2019 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.
+
+java_library {
+    name: "mimemap",
+    visibility: [
+        "//cts/tests/tests/mimemap:__subpackages__",
+        "//frameworks/base:__subpackages__",
+    ],
+
+    srcs: [
+        "java/android/content/type/MimeMapImpl.java",
+    ],
+
+    java_resources: [
+        ":debian.mime.types",
+        ":android.mime.types",
+    ],
+
+    sdk_version: "core_platform",
+}
+
+filegroup {
+    name: "android.mime.types",
+    visibility: [
+        "//visibility:private",
+    ],
+    path: "java-res/",
+    srcs: [
+        "java-res/android.mime.types",
+    ],
+}
diff --git a/mime/java-res/android.mime.types b/mime/java-res/android.mime.types
new file mode 100644
index 0000000..1ca912e
--- /dev/null
+++ b/mime/java-res/android.mime.types
@@ -0,0 +1,146 @@
+
+###############################################################################
+#
+# Android-specific MIME type <-> extension mappings
+#
+# Each line below defines an mapping from one MIME type to the first of the
+# listed extensions, and from listed extension back to the MIME type.
+# A mapping overrides any previous mapping _from_ that same MIME type or
+# extension (put() semantics), unless that MIME type / extension is prefixed with '?'
+# (putIfAbsent() semantics).
+#
+#
+###############################################################################
+#
+# EXAMPLES
+#
+# A line of the form:
+#
+#    ?mime ext1 ?ext2 ext3
+#
+# affects the current mappings along the lines of the following pseudo code:
+#
+#    mimeToExt.putIfAbsent("mime", "ext1");
+#    extToMime.put("ext1", "mime");
+#    extToMime.putIfAbsent("ext2", "mime");
+#    extToMime.put("ext3", "mime");
+#
+# The line:
+#
+#     ?text/plain txt
+#
+# leaves any earlier mapping for "text/plain" untouched, or maps that MIME type
+# to the file extension ".txt" if there is no earlier mapping. The line also
+# sets the mapping from file extension ".txt" to be the MIME type "text/plain",
+# regardless of whether a previous mapping existed.
+#
+###############################################################################
+
+
+# File extensions that Android wants to override to point to the given MIME type.
+#
+# After processing a line of the form:
+# ?<mimeType> <extension1> <extension2>
+# If <mimeType> was not already mapped to an extension then it will be
+# mapped to <extension1>.
+# <extension1> and <extension2> are mapped (or remapped) to <mimeType>.
+
+?application/epub+zip epub
+?application/pkix-cert cer
+?application/rss+xml rss
+?application/vnd.android.ota ota
+?application/vnd.apple.mpegurl m3u8
+?application/vnd.ms-pki.stl stl
+?application/vnd.ms-powerpoint pot
+?application/vnd.ms-wpl wpl
+?application/vnd.stardivision.impress sdp
+?application/vnd.stardivision.writer vor
+?application/vnd.youtube.yt yt
+?application/x-android-drm-fl fl
+?application/x-flac flac
+?application/x-font pcf
+?application/x-mpegurl m3u m3u8
+?application/x-pem-file pem
+?application/x-pkcs12 p12 pfx
+?application/x-webarchive webarchive
+?application/x-webarchive-xml webarchivexml
+?application/x-x509-server-cert crt
+?application/x-x509-user-cert crt
+
+?audio/3gpp 3gpp
+?audio/aac-adts aac
+?audio/imelody imy
+?audio/midi rtttl xmf
+?audio/mobile-xmf mxmf
+?audio/mp4 m4a
+?audio/mpegurl m3u
+?audio/sp-midi smf
+?audio/x-matroska mka
+?audio/x-pn-realaudio ra
+
+?image/bmp bmp
+?image/heic heic
+?image/heic-sequence heics
+?image/heif heif hif
+?image/heif-sequence heifs
+?image/ico cur
+?image/webp webp
+?image/x-adobe-dng dng
+?image/x-fuji-raf raf
+?image/x-icon ico
+?image/x-nikon-nrw nrw
+?image/x-panasonic-rw2 rw2
+?image/x-pentax-pef pef
+?image/x-samsung-srw srw
+?image/x-sony-arw arw
+
+?text/comma-separated-values csv
+?text/plain diff po
+?text/rtf rtf
+?text/text phps
+?text/xml xml
+?text/x-vcard vcf
+
+?video/3gpp2 3gpp2 3g2
+?video/3gpp 3gpp
+?video/avi avi
+?video/m4v m4v
+?video/mp2p mpeg
+?video/mp2t m2ts mts
+?video/mp2ts ts
+?video/vnd.youtube.yt yt
+?video/x-webex wrf
+
+# Optional additions that should not override any previous mapping.
+
+?application/x-wifi-config ?xml
+
+# Special cases where Android has a strong opinion about mappings, so we
+# define them very last and make them override in both directions (no "?").
+#
+# Lines here are of the form:
+# <mimeType> <extension1> <extension2> ...
+#
+# After processing each line,
+#   <mimeType> is mapped to <extension1>
+#   <extension1>, <extension2>, ... are all mapped to <mimeType>
+# This overrides any mappings for this <mimeType> / for these extensions
+# that may have been defined earlier.
+
+application/pgp-signature pgp
+application/x-x509-ca-cert crt
+audio/aac aac
+audio/basic snd
+audio/flac flac
+audio/midi rtx
+audio/mpeg mp3 m4a m4r
+audio/x-mpegurl m3u m3u8
+image/jpeg jpg
+image/x-ms-bmp bmp
+text/plain txt
+text/x-c++hdr hpp
+text/x-c++src cpp
+video/3gpp 3gpp
+video/mpeg mpeg
+video/quicktime mov
+video/x-matroska mkv
diff --git a/mime/java/android/content/type/MimeMapImpl.java b/mime/java/android/content/type/MimeMapImpl.java
new file mode 100644
index 0000000..c904ea3
--- /dev/null
+++ b/mime/java/android/content/type/MimeMapImpl.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2019 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 android.content.type;
+
+import libcore.net.MimeMap;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Default implementation of {@link MimeMap}, a bidirectional mapping between
+ * MIME types and file extensions.
+ *
+ * This default mapping is loaded from data files that start with some mappings
+ * recognized by IANA plus some custom extensions and overrides.
+ *
+ * @hide
+ */
+public class MimeMapImpl extends MimeMap {
+
+    /**
+     * Creates and returns a new {@link MimeMapImpl} instance that implements.
+     * Android's default mapping between MIME types and extensions.
+     */
+    public static MimeMapImpl createDefaultInstance() {
+        return parseFromResources("/mime.types", "/android.mime.types");
+    }
+
+    private static final Pattern SPLIT_PATTERN = Pattern.compile("\\s+");
+
+    /**
+     * Note: These maps only contain lowercase keys/values, regarded as the
+     * {@link #toLowerCase(String) canonical form}.
+     *
+     * <p>This is the case for both extensions and MIME types. The mime.types
+     * data file contains examples of mixed-case MIME types, but some applications
+     * use the lowercase version of these same types. RFC 2045 section 2 states
+     * that MIME types are case insensitive.
+     */
+    private final Map<String, String> mMimeTypeToExtension;
+    private final Map<String, String> mExtensionToMimeType;
+
+    public MimeMapImpl(Map<String, String> mimeTypeToExtension,
+            Map<String, String> extensionToMimeType) {
+        this.mMimeTypeToExtension = new HashMap<>(mimeTypeToExtension);
+        for (Map.Entry<String, String> entry : mimeTypeToExtension.entrySet()) {
+            checkValidMimeType(entry.getKey());
+            checkValidExtension(entry.getValue());
+        }
+        this.mExtensionToMimeType = new HashMap<>(extensionToMimeType);
+        for (Map.Entry<String, String> entry : extensionToMimeType.entrySet()) {
+            checkValidExtension(entry.getKey());
+            checkValidMimeType(entry.getValue());
+        }
+    }
+
+    private static void checkValidMimeType(String s) {
+        if (MimeMap.isNullOrEmpty(s) || !s.equals(MimeMap.toLowerCase(s))) {
+            throw new IllegalArgumentException("Invalid MIME type: " + s);
+        }
+    }
+
+    private static void checkValidExtension(String s) {
+        if (MimeMap.isNullOrEmpty(s) || !s.equals(MimeMap.toLowerCase(s))) {
+            throw new IllegalArgumentException("Invalid extension: " + s);
+        }
+    }
+
+    static MimeMapImpl parseFromResources(String... resourceNames) {
+        Map<String, String> mimeTypeToExtension = new HashMap<>();
+        Map<String, String> extensionToMimeType = new HashMap<>();
+        for (String resourceName : resourceNames) {
+            parseTypes(mimeTypeToExtension, extensionToMimeType, resourceName);
+        }
+        return new MimeMapImpl(mimeTypeToExtension, extensionToMimeType);
+    }
+
+    /**
+     * An element of a *mime.types file: A MIME type or an extension, with an optional
+     * prefix of "?" (if not overriding an earlier value).
+     */
+    private static class Element {
+        public final boolean keepExisting;
+        public final String s;
+
+        Element(boolean keepExisting, String value) {
+            this.keepExisting = keepExisting;
+            this.s = toLowerCase(value);
+            if (value.isEmpty()) {
+                throw new IllegalArgumentException();
+            }
+        }
+
+        public String toString() {
+            return keepExisting ? ("?" + s) : s;
+        }
+    }
+
+    private static String maybePut(Map<String, String> map, Element keyElement, String value) {
+        if (keyElement.keepExisting) {
+            return map.putIfAbsent(keyElement.s, value);
+        } else {
+            return map.put(keyElement.s, value);
+        }
+    }
+
+    private static void parseTypes(Map<String, String> mimeTypeToExtension,
+            Map<String, String> extensionToMimeType, String resource) {
+        try (BufferedReader r = new BufferedReader(
+                new InputStreamReader(MimeMapImpl.class.getResourceAsStream(resource)))) {
+            String line;
+            while ((line = r.readLine()) != null) {
+                int commentPos = line.indexOf('#');
+                if (commentPos >= 0) {
+                    line = line.substring(0, commentPos);
+                }
+                line = line.trim();
+                // The first time a MIME type is encountered it is mapped to the first extension
+                // listed in its line. The first time an extension is encountered it is mapped
+                // to the MIME type.
+                //
+                // When encountering a previously seen MIME type or extension, then by default
+                // the later ones override earlier mappings (put() semantics); however if a MIME
+                // type or extension is prefixed with '?' then any earlier mapping _from_ that
+                // MIME type / extension is kept (putIfAbsent() semantics).
+                final String[] split = SPLIT_PATTERN.split(line);
+                if (split.length <= 1) {
+                    // Need mimeType + at least one extension to make a mapping.
+                    // "mime.types" files may also contain lines with just a mimeType without
+                    // an extension but we skip them as they provide no mapping info.
+                    continue;
+                }
+                List<Element> lineElements = new ArrayList<>(split.length);
+                for (String s : split) {
+                    boolean keepExisting = s.startsWith("?");
+                    if (keepExisting) {
+                        s = s.substring(1);
+                    }
+                    if (s.isEmpty()) {
+                        throw new IllegalArgumentException("Invalid entry in '" + line + "'");
+                    }
+                    lineElements.add(new Element(keepExisting, s));
+                }
+
+                // MIME type -> first extension (one mapping)
+                // This will override any earlier mapping from this MIME type to another
+                // extension, unless this MIME type was prefixed with '?'.
+                Element mimeElement = lineElements.get(0);
+                List<Element> extensionElements = lineElements.subList(1, lineElements.size());
+                String firstExtension = extensionElements.get(0).s;
+                maybePut(mimeTypeToExtension, mimeElement, firstExtension);
+
+                // extension -> MIME type (one or more mappings).
+                // This will override any earlier mapping from this extension to another
+                // MIME type, unless this extension was prefixed with '?'.
+                for (Element extensionElement : extensionElements) {
+                    maybePut(extensionToMimeType, extensionElement, mimeElement.s);
+                }
+            }
+        } catch (IOException | RuntimeException e) {
+            throw new RuntimeException("Failed to parse " + resource, e);
+        }
+    }
+
+    @Override
+    protected String guessExtensionFromLowerCaseMimeType(String mimeType) {
+        return mMimeTypeToExtension.get(mimeType);
+    }
+
+    @Override
+    protected String guessMimeTypeFromLowerCaseExtension(String extension) {
+        return mExtensionToMimeType.get(extension);
+    }
+}