1 /*
2  * Copyright (C) 2017 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 package com.android.documentsui.inspector;
17 
18 import android.content.Context;
19 import android.content.res.Resources;
20 import android.location.Address;
21 import android.location.Geocoder;
22 import android.media.ExifInterface;
23 import android.media.MediaMetadata;
24 import android.os.AsyncTask;
25 import android.os.Bundle;
26 import android.provider.DocumentsContract;
27 import androidx.annotation.VisibleForTesting;
28 import android.text.format.DateUtils;
29 import android.util.AttributeSet;
30 
31 import com.android.documentsui.R;
32 import com.android.documentsui.base.DocumentInfo;
33 import com.android.documentsui.base.Shared;
34 import com.android.documentsui.inspector.InspectorController.MediaDisplay;
35 import com.android.documentsui.inspector.InspectorController.TableDisplay;
36 
37 import java.io.IOException;
38 import java.util.function.Consumer;
39 
40 import javax.annotation.Nullable;
41 
42 /**
43  * Organizes and Displays the debug information about a file. This view
44  * should only be made visible when build is debuggable and system policies
45  * allow debug "stuff".
46  */
47 public class MediaView extends TableView implements MediaDisplay {
48 
49     private final Resources mResources;
50     private final Context mContext;
51 
MediaView(Context context)52     public MediaView(Context context) {
53         this(context, null);
54     }
55 
MediaView(Context context, AttributeSet attrs)56     public MediaView(Context context, AttributeSet attrs) {
57         this(context, attrs, 0);
58     }
59 
MediaView(Context context, AttributeSet attrs, int defStyleAttr)60     public MediaView(Context context, AttributeSet attrs, int defStyleAttr) {
61         super(context, attrs, defStyleAttr);
62         mContext = context;
63         mResources = context.getResources();
64     }
65 
66     @Override
accept(DocumentInfo doc, Bundle metadata, @Nullable Runnable geoClickListener)67     public void accept(DocumentInfo doc, Bundle metadata, @Nullable Runnable geoClickListener) {
68         putTitle("", true);
69 
70         Bundle exif = metadata.getBundle(DocumentsContract.METADATA_EXIF);
71         if (exif != null) {
72             showExifData(this, mResources, doc, exif, geoClickListener, this::getAddress);
73         }
74 
75         Bundle video = metadata.getBundle(Shared.METADATA_KEY_VIDEO);
76         if (video != null) {
77             showVideoData(this, mResources, doc, video, geoClickListener);
78         }
79 
80         Bundle audio = metadata.getBundle(Shared.METADATA_KEY_AUDIO);
81         if (audio != null) {
82             showAudioData(this, audio);
83         }
84 
85         setVisible(!isEmpty());
86     }
87 
88     @VisibleForTesting
showAudioData(TableDisplay table, Bundle tags)89     public static void showAudioData(TableDisplay table, Bundle tags) {
90 
91         if (tags.containsKey(MediaMetadata.METADATA_KEY_ARTIST)) {
92             table.put(R.string.metadata_artist, tags.getString(MediaMetadata.METADATA_KEY_ARTIST));
93         }
94 
95         if (tags.containsKey(MediaMetadata.METADATA_KEY_COMPOSER)) {
96             table.put(R.string.metadata_composer,
97                     tags.getString(MediaMetadata.METADATA_KEY_COMPOSER));
98         }
99 
100         if (tags.containsKey(MediaMetadata.METADATA_KEY_ALBUM)) {
101             table.put(R.string.metadata_album, tags.getString(MediaMetadata.METADATA_KEY_ALBUM));
102         }
103 
104         if (tags.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
105             int millis = tags.getInt(MediaMetadata.METADATA_KEY_DURATION);
106             table.put(R.string.metadata_duration, DateUtils.formatElapsedTime(millis / 1000));
107         }
108     }
109 
110     @VisibleForTesting
showVideoData( TableDisplay table, Resources resources, DocumentInfo doc, Bundle tags, @Nullable Runnable geoClickListener)111     public static void showVideoData(
112             TableDisplay table,
113             Resources resources,
114             DocumentInfo doc,
115             Bundle tags,
116             @Nullable Runnable geoClickListener) {
117 
118         addDimensionsRow(table, resources, tags);
119 
120         if (MetadataUtils.hasVideoCoordinates(tags)) {
121             float[] coords = MetadataUtils.getVideoCoords(tags);
122             showCoordiantes(table, resources, coords, geoClickListener);
123         }
124 
125         if (tags.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
126             int millis = tags.getInt(MediaMetadata.METADATA_KEY_DURATION);
127             table.put(R.string.metadata_duration, DateUtils.formatElapsedTime(millis / 1000));
128         }
129     }
130 
131     @VisibleForTesting
showExifData( TableDisplay table, Resources resources, DocumentInfo doc, Bundle tags, @Nullable Runnable geoClickListener, Consumer<float[]> geoAddressFetcher)132     public static void showExifData(
133             TableDisplay table,
134             Resources resources,
135             DocumentInfo doc,
136             Bundle tags,
137             @Nullable Runnable geoClickListener,
138             Consumer<float[]> geoAddressFetcher) {
139 
140         addDimensionsRow(table, resources, tags);
141 
142         if (tags.containsKey(ExifInterface.TAG_DATETIME)) {
143             String date = tags.getString(ExifInterface.TAG_DATETIME);
144             table.put(R.string.metadata_date_time, date);
145         }
146 
147         if (tags.containsKey(ExifInterface.TAG_GPS_ALTITUDE)) {
148             double altitude = tags.getDouble(ExifInterface.TAG_GPS_ALTITUDE);
149             table.put(R.string.metadata_altitude, String.valueOf(altitude));
150         }
151 
152         if (tags.containsKey(ExifInterface.TAG_MAKE) || tags.containsKey(ExifInterface.TAG_MODEL)) {
153                 String make = tags.getString(ExifInterface.TAG_MAKE);
154                 String model = tags.getString(ExifInterface.TAG_MODEL);
155                 make = make != null ? make : "";
156                 model = model != null ? model : "";
157                 table.put(
158                         R.string.metadata_camera,
159                         resources.getString(R.string.metadata_camera_format, make, model));
160         }
161 
162         if (tags.containsKey(ExifInterface.TAG_APERTURE)) {
163             table.put(R.string.metadata_aperture, resources.getString(
164                     R.string.metadata_aperture_format, tags.getDouble(ExifInterface.TAG_APERTURE)));
165         }
166 
167         if (tags.containsKey(ExifInterface.TAG_SHUTTER_SPEED_VALUE)) {
168             String shutterSpeed = String.valueOf(
169                     formatShutterSpeed(tags.getDouble(ExifInterface.TAG_SHUTTER_SPEED_VALUE)));
170             table.put(R.string.metadata_shutter_speed, shutterSpeed);
171         }
172 
173         if (tags.containsKey(ExifInterface.TAG_FOCAL_LENGTH)) {
174             double length = tags.getDouble(ExifInterface.TAG_FOCAL_LENGTH);
175             table.put(R.string.metadata_focal_length,
176                     String.format(resources.getString(R.string.metadata_focal_format), length));
177         }
178 
179         if (tags.containsKey(ExifInterface.TAG_ISO_SPEED_RATINGS)) {
180             int iso = tags.getInt(ExifInterface.TAG_ISO_SPEED_RATINGS);
181             table.put(R.string.metadata_iso_speed_ratings,
182                     String.format(resources.getString(R.string.metadata_iso_format), iso));
183         }
184 
185         if (MetadataUtils.hasExifGpsFields(tags)) {
186             float[] coords = MetadataUtils.getExifGpsCoords(tags);
187             showCoordiantes(table, resources, coords, geoClickListener);
188             geoAddressFetcher.accept(coords);
189         }
190     }
191 
showCoordiantes( TableDisplay table, Resources resources, float[] coords, @Nullable Runnable geoClickListener)192     private static void showCoordiantes(
193             TableDisplay table,
194             Resources resources,
195             float[] coords,
196             @Nullable Runnable geoClickListener) {
197 
198         String value = resources.getString(
199                 R.string.metadata_coordinates_format, coords[0], coords[1]);
200         if (geoClickListener != null) {
201             table.put(
202                     R.string.metadata_coordinates,
203                     value,
204                     view -> {
205                         geoClickListener.run();
206                     }
207             );
208         } else {
209             table.put(R.string.metadata_coordinates, value);
210         }
211     }
212 
213     /**
214      * Attempts to retrieve an approximate address and displays the address if it can find one.
215      * @param coords the coordinates that gets an address.
216      */
getAddress(float[] coords)217     private void getAddress(float[] coords) {
218         new AsyncTask<Float, Void, Address>() {
219             @Override
220             protected Address doInBackground(Float... coords) {
221                 assert (coords.length == 2);
222                 Geocoder geocoder = new Geocoder(mContext);
223                 try {
224                     Address address = geocoder.getFromLocation(coords[0], // latitude
225                             coords[1], // longitude
226                             1 // amount of results returned
227                     ).get(0);
228                     return address;
229                 } catch (IOException e) {
230                     return null;
231                 }
232             }
233             @Override
234             protected void onPostExecute(@Nullable Address address) {
235                 if (address != null) {
236                     TableDisplay table = MediaView.this;
237                     if (address.getMaxAddressLineIndex() >= 0) {
238                         String formattedAddress;
239                         StringBuilder addressBuilder = new StringBuilder("");
240                         addressBuilder.append(address.getAddressLine(0));
241                         for (int i = 1; i <= address.getMaxAddressLineIndex(); i++) {
242                             addressBuilder.append("\n");
243                             String addressLine = address.getAddressLine(i);
244                             if (addressLine != null) {
245                                 addressBuilder.append(addressLine);
246                             }
247                         }
248                         formattedAddress = addressBuilder.toString();
249                         table.put(R.string.metadata_address, formattedAddress);
250                     } else if (address.getLocality() != null) {
251                         table.put(R.string.metadata_address, address.getLocality());
252                     } else if (address.getSubAdminArea() != null) {
253                         table.put(R.string.metadata_address, address.getSubAdminArea());
254                     } else if (address.getAdminArea() != null) {
255                         table.put(R.string.metadata_address, address.getAdminArea());
256                     } else if (address.getCountryName() != null) {
257                         table.put(R.string.metadata_address, address.getCountryName());
258                     }                }
259             }
260         }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, coords[0], coords[1]);
261     }
262 
263     /**
264      * @param speed a value n, where shutter speed equals 1/(2^n)
265      * @return a String containing either a fraction that displays 1 over a positive integer, or a
266      * double rounded to one decimal, depending on if 1/(2^n) is less than or greater than 1,
267      * respectively.
268      */
formatShutterSpeed(double speed)269     private static String formatShutterSpeed(double speed) {
270         if (speed <= 0) {
271             double shutterSpeed = Math.pow(2, -1 * speed);
272             String formattedSpeed = String.valueOf(Math.round(shutterSpeed * 10.0) / 10.0);
273             return formattedSpeed;
274         } else {
275             int approximateSpeedDenom = (int) Math.pow(2, speed) + 1;
276             String formattedSpeed = "1/" + String.valueOf(approximateSpeedDenom);
277             return formattedSpeed;
278         }
279     }
280 
281     /**
282      * @param table
283      * @param resources
284      * @param tags
285      */
addDimensionsRow(TableDisplay table, Resources resources, Bundle tags)286     private static void addDimensionsRow(TableDisplay table, Resources resources, Bundle tags) {
287         if (tags.containsKey(ExifInterface.TAG_IMAGE_WIDTH)
288             && tags.containsKey(ExifInterface.TAG_IMAGE_LENGTH)) {
289             int width = tags.getInt(ExifInterface.TAG_IMAGE_WIDTH);
290             int height = tags.getInt(ExifInterface.TAG_IMAGE_LENGTH);
291             float megaPixels = height * width / 1000000f;
292             table.put(R.string.metadata_dimensions,
293                     resources.getString(
294                             R.string.metadata_dimensions_format, width, height, megaPixels));
295         }
296     }
297 }
298