• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.providers.media.util;
18 
19 import android.graphics.BitmapFactory;
20 import android.graphics.ImageDecoder;
21 import android.graphics.drawable.AnimatedImageDrawable;
22 import android.graphics.drawable.Drawable;
23 import android.media.ExifInterface;
24 import android.os.Trace;
25 import android.provider.MediaStore.Files.FileColumns;
26 import android.util.Log;
27 
28 import org.xmlpull.v1.XmlPullParser;
29 import org.xmlpull.v1.XmlPullParserException;
30 import org.xmlpull.v1.XmlPullParserFactory;
31 
32 import java.io.FileInputStream;
33 import java.io.IOException;
34 import java.io.File;
35 import java.io.StringReader;
36 import java.nio.charset.StandardCharsets;
37 
38 /**
39  * Class to detect and return special format for a media file.
40  */
41 public class SpecialFormatDetector {
42     private static final String TAG = "SpecialFormatDetector";
43     // These are the known MotionPhoto attribute names
44     private static final String[] MOTION_PHOTO_ATTRIBUTE_NAMES = {
45             "Camera:MotionPhoto", // Motion Photo V1
46             "GCamera:MotionPhoto", // Motion Photo V1 (legacy element naming)
47             "Camera:MicroVideo", // Micro Video V1b
48             "GCamera:MicroVideo", // Micro Video V1b (legacy element naming)
49     };
50 
51     private static final String[] DESCRIPTION_MICRO_VIDEO_OFFSET_ATTRIBUTE_NAMES = {
52             "Camera:MicroVideoOffset", // Micro Video V1b
53             "GCamera:MicroVideoOffset", // Micro Video V1b (legacy element naming)
54     };
55 
56     private static final String XMP_META_TAG = "x:xmpmeta";
57     private static final String XMP_RDF_DESCRIPTION_TAG = "rdf:Description";
58 
59     private static final String XMP_CONTAINER_PREFIX = "Container";
60     private static final String XMP_GCONTAINER_PREFIX = "GContainer";
61 
62     private static final String XMP_ITEM_PREFIX = "Item";
63     private static final String XMP_GCONTAINER_ITEM_PREFIX =
64             XMP_GCONTAINER_PREFIX + XMP_ITEM_PREFIX;
65 
66     private static final String XMP_DIRECTORY_TAG = ":Directory";
67     private static final String XMP_CONTAINER_DIRECTORY_PREFIX =
68             XMP_CONTAINER_PREFIX + XMP_DIRECTORY_TAG;
69     private static final String XMP_GCONTAINER_DIRECTORY_PREFIX =
70             XMP_GCONTAINER_PREFIX + XMP_DIRECTORY_TAG;
71 
72     private static final String SEMANTIC_PRIMARY = "Primary";
73     private static final String SEMANTIC_MOTION_PHOTO = "MotionPhoto";
74 
75     /**
76      * {@return} special format for a file
77      */
detect(File file)78     public static int detect(File file) throws Exception {
79         try (FileInputStream is = new FileInputStream(file)) {
80             final ExifInterface exif = new ExifInterface(is);
81             return detect(exif, file);
82         }
83     }
84 
85     /**
86      * {@return} special format for a file
87      */
detect(ExifInterface exif, File file)88     public static int detect(ExifInterface exif, File file) throws Exception {
89         if (isMotionPhoto(exif)) {
90             return FileColumns._SPECIAL_FORMAT_MOTION_PHOTO;
91         }
92 
93         return detectGifOrAnimatedWebp(file);
94     }
95 
96     /**
97      * Checks file metadata to detect if the given file is a GIF or Animated Webp.
98      *
99      * Note: This does not respect file extension.
100      *
101      * @return {@link FileColumns#_SPECIAL_FORMAT_GIF} if the file is a GIF file or
102      *         {@link FileColumns#_SPECIAL_FORMAT_ANIMATED_WEBP} if the file is an Animated Webp
103      *         file. Otherwise returns {@link FileColumns#_SPECIAL_FORMAT_NONE}
104      */
detectGifOrAnimatedWebp(File file)105     private static int detectGifOrAnimatedWebp(File file) throws IOException {
106         final BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
107         // Set options such that the image is not decoded to a bitmap, as we only want mimetype
108         // options
109         bitmapOptions.inSampleSize = 1;
110         bitmapOptions.inJustDecodeBounds = true;
111         BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOptions);
112 
113         if (bitmapOptions.outMimeType.equalsIgnoreCase("image/gif")) {
114             return FileColumns._SPECIAL_FORMAT_GIF;
115         }
116         if (bitmapOptions.outMimeType.equalsIgnoreCase("image/webp") &&
117                 isAnimatedWebp(file)) {
118             return FileColumns._SPECIAL_FORMAT_ANIMATED_WEBP;
119         }
120         return FileColumns._SPECIAL_FORMAT_NONE;
121     }
122 
isAnimatedWebp(File file)123     private static boolean isAnimatedWebp(File file) throws IOException {
124         final ImageDecoder.Source source = ImageDecoder.createSource(file);
125         final Drawable drawable = ImageDecoder.decodeDrawable(source);
126         return (drawable instanceof AnimatedImageDrawable);
127     }
128 
isMotionPhoto(ExifInterface exif)129     private static boolean isMotionPhoto(ExifInterface exif) throws Exception {
130         if (!exif.hasAttribute(ExifInterface.TAG_XMP)) {
131             return false;
132         }
133         final String xmp = new String(exif.getAttributeBytes(ExifInterface.TAG_XMP),
134                 StandardCharsets.UTF_8);
135 
136         // The below logic is copied from ExoPlayer#XmpMotionPhotoDescriptionParser class
137         XmlPullParserFactory xmlPullParserFactory = XmlPullParserFactory.newInstance();
138         XmlPullParser xpp = xmlPullParserFactory.newPullParser();
139         xpp.setInput(new StringReader(xmp));
140         xpp.next();
141         if (!isStartTag(xpp, XMP_META_TAG)) {
142             Log.d(TAG, "Couldn't find xmp metadata");
143             return false;
144         }
145 
146         Trace.beginSection("motionPhotoDetectionUsingXpp");
147         try {
148             return isMotionPhoto(xpp);
149         } finally {
150             Trace.endSection();
151         }
152     }
153 
isMotionPhoto(XmlPullParser xpp)154     private static boolean isMotionPhoto(XmlPullParser xpp) throws Exception {
155         boolean isMotionPhotoAttributesFound = false;
156 
157         do {
158             xpp.next();
159             if (!isStartTag(xpp)) {
160                 continue;
161             }
162 
163             switch (xpp.getName()) {
164                 case XMP_RDF_DESCRIPTION_TAG:
165                     if (!isMotionPhotoFlagSet(xpp)) {
166                         // The motion photo flag is not set, so the file should not be treated as a
167                         // motion photo.
168                         return false;
169                     }
170                     isMotionPhotoAttributesFound = isMicroVideoPresent(xpp);
171                     break;
172                 case XMP_CONTAINER_DIRECTORY_PREFIX:
173                     isMotionPhotoAttributesFound = isMotionPhotoDirectory(xpp, XMP_CONTAINER_PREFIX,
174                         XMP_ITEM_PREFIX);
175                     break;
176                 case XMP_GCONTAINER_DIRECTORY_PREFIX:
177                     isMotionPhotoAttributesFound = isMotionPhotoDirectory(xpp,
178                             XMP_GCONTAINER_PREFIX, XMP_GCONTAINER_ITEM_PREFIX);
179                     break;
180                 default: // do nothing
181             }
182 
183             // Return early if motion photo attributes were found in the xpp,
184             // otherwise continue looking
185             if (isMotionPhotoAttributesFound) {
186                 return true;
187             }
188 
189         } while (!isEndTag(xpp, XMP_META_TAG) && xpp.getEventType() != XmlPullParser.END_DOCUMENT);
190 
191         return false;
192     }
193 
isMotionPhotoDirectory(XmlPullParser xpp, String containerNamespacePrefix, String itemNamespacePrefix)194     private static boolean isMotionPhotoDirectory(XmlPullParser xpp,
195             String containerNamespacePrefix, String itemNamespacePrefix)
196             throws XmlPullParserException, IOException {
197         final String itemTagName = containerNamespacePrefix + ":Item";
198         final String directoryTagName = containerNamespacePrefix + ":Directory";
199         final String mimeAttributeName = itemNamespacePrefix + ":Mime";
200         final String semanticAttributeName = itemNamespacePrefix + ":Semantic";
201         final String lengthAttributeName = itemNamespacePrefix + ":Length";
202         boolean isPrimaryImagePresent = false;
203         boolean isMotionPhotoPresent = false;
204 
205         do {
206             xpp.next();
207             if (!isStartTag(xpp, itemTagName)) {
208                 continue;
209             }
210 
211             String semantic = getAttributeValue(xpp, semanticAttributeName);
212             if (getAttributeValue(xpp, mimeAttributeName) == null || semantic == null) {
213                 // Required values are missing.
214                 return false;
215             }
216 
217             switch (semantic) {
218                 case SEMANTIC_PRIMARY:
219                     isPrimaryImagePresent = true;
220                     break;
221                 case SEMANTIC_MOTION_PHOTO:
222                     String length = getAttributeValue(xpp, lengthAttributeName);
223                     isMotionPhotoPresent = (length != null && Integer.parseInt(length) > 0);
224                     break;
225                 default: // do nothing
226             }
227 
228             if (isMotionPhotoPresent && isPrimaryImagePresent) {
229                 return true;
230             }
231         } while (!isEndTag(xpp, directoryTagName) &&
232                 xpp.getEventType() != XmlPullParser.END_DOCUMENT);
233         // We need a primary item (photo) and at least one secondary item (video).
234         return false;
235     }
236 
isMicroVideoPresent(XmlPullParser xpp)237     private static boolean isMicroVideoPresent(XmlPullParser xpp) {
238         for (String attributeName : DESCRIPTION_MICRO_VIDEO_OFFSET_ATTRIBUTE_NAMES) {
239             String attributeValue = getAttributeValue(xpp, attributeName);
240             if (attributeValue != null) {
241                 long microVideoOffset = Long.parseLong(attributeValue);
242                 return microVideoOffset > 0;
243             }
244         }
245         return false;
246     }
247 
isMotionPhotoFlagSet(XmlPullParser xpp)248     private static boolean isMotionPhotoFlagSet(XmlPullParser xpp) {
249         for (String attributeName : MOTION_PHOTO_ATTRIBUTE_NAMES) {
250             String attributeValue = getAttributeValue(xpp, attributeName);
251             if (attributeValue != null) {
252                 int motionPhotoFlag = Integer.parseInt(attributeValue);
253                 return motionPhotoFlag == 1;
254             }
255         }
256         return false;
257     }
258 
getAttributeValue(XmlPullParser xpp, String attributeName)259     private static String getAttributeValue(XmlPullParser xpp, String attributeName) {
260         for (int i = 0; i < xpp.getAttributeCount(); i++) {
261             if (xpp.getAttributeName(i).equals(attributeName)) {
262                 return xpp.getAttributeValue(i);
263             }
264         }
265         return null;
266     }
267 
268     /**
269      * Returns whether the current event is an end tag with the specified name.
270      *
271      * @param xpp The {@link XmlPullParser} to query.
272      * @param name The specified name.
273      * @return Whether the current event is an end tag.
274      * @throws XmlPullParserException If an error occurs querying the parser.
275      */
isEndTag(XmlPullParser xpp, String name)276     private static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException {
277         return xpp.getEventType() == XmlPullParser.END_TAG && xpp.getName().equals(name);
278     }
279 
280     /**
281      * Returns whether the current event is a start tag with the specified name.
282      *
283      * @param xpp The {@link XmlPullParser} to query.
284      * @param name The specified name.
285      * @return Whether the current event is a start tag with the specified name.
286      * @throws XmlPullParserException If an error occurs querying the parser.
287      */
isStartTag(XmlPullParser xpp, String name)288     private static boolean isStartTag(XmlPullParser xpp, String name)
289             throws XmlPullParserException {
290         return isStartTag(xpp) && xpp.getName().equals(name);
291     }
292 
isStartTag(XmlPullParser xpp)293     private static boolean isStartTag(XmlPullParser xpp) throws XmlPullParserException {
294         return xpp.getEventType() == XmlPullParser.START_TAG;
295     }
296 }
297