diff options
-rw-r--r-- | Android.bp | 14 | ||||
-rw-r--r-- | core/java/com/android/internal/os/RuntimeInit.java | 11 | ||||
-rw-r--r-- | mime/Android.bp | 43 | ||||
-rw-r--r-- | mime/java-res/android.mime.types | 146 | ||||
-rw-r--r-- | mime/java/android/content/type/MimeMapImpl.java | 194 |
5 files changed, 407 insertions, 1 deletions
diff --git a/Android.bp b/Android.bp index 75773535610f..05675df20575 100644 --- a/Android.bp +++ b/Android.bp @@ -112,6 +112,14 @@ filegroup { } 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 @@ filegroup { ":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 @@ java_defaults { 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 d6caa0930243..89b0250c505b 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.annotation.UnsupportedAppUsage; 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.internal.logging.AndroidConfig; 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 class RuntimeInit { 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 000000000000..9303755ba73d --- /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 000000000000..1ca912e8510b --- /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 000000000000..c904ea3f9b60 --- /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); + } +} |