Merge "Move default MimeMap implementation to frameworks." am: 19c3bdc88a am: 3f1d5d92fc am: 1b6d718387
am: 4999ce080d

Change-Id: Id03e7205f9f89ed34e53a4db7ff42f47138f1b48
diff --git a/Android.bp b/Android.bp
index 46377dc..731a672 100644
--- a/Android.bp
+++ b/Android.bp
@@ -177,6 +177,7 @@
     jarjar_rules: ":framework-jarjar-rules",
 
     static_libs: [
+        "mimemap",
         "apex_aidl_interface-java",
         "suspend_control_aidl_interface-java",
         "framework-protos",
diff --git a/core/java/com/android/internal/os/RuntimeInit.java b/core/java/com/android/internal/os/RuntimeInit.java
index 1de2e72..918d577 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;
@@ -210,6 +214,11 @@
         RuntimeHooks.setTimeZoneIdSupplier(() -> SystemProperties.get("persist.sys.timezone"));
 
         /*
+         * Set a default mapping between MIME types and file extensions.
+         */
+        MimeMap.setDefault(MimeMapImpl.createDefaultInstance());
+
+        /*
          * Sets handler for java.util.logging to use Android log facilities.
          * The odd "new instance-and-then-throw-away" is a mirror of how
          * the "java.util.logging.config.class" system property works. We
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);
+    }
+}