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 addressBuilder.append(address.getAddressLine(i)); 244 } 245 formattedAddress = addressBuilder.toString(); 246 table.put(R.string.metadata_address, formattedAddress); 247 } else if (address.getLocality() != null) { 248 table.put(R.string.metadata_address, address.getLocality()); 249 } else if (address.getSubAdminArea() != null) { 250 table.put(R.string.metadata_address, address.getSubAdminArea()); 251 } else if (address.getAdminArea() != null) { 252 table.put(R.string.metadata_address, address.getAdminArea()); 253 } else if (address.getCountryName() != null) { 254 table.put(R.string.metadata_address, address.getCountryName()); 255 } } 256 } 257 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, coords[0], coords[1]); 258 } 259 260 /** 261 * @param speed a value n, where shutter speed equals 1/(2^n) 262 * @return a String containing either a fraction that displays 1 over a positive integer, or a 263 * double rounded to one decimal, depending on if 1/(2^n) is less than or greater than 1, 264 * respectively. 265 */ formatShutterSpeed(double speed)266 private static String formatShutterSpeed(double speed) { 267 if (speed <= 0) { 268 double shutterSpeed = Math.pow(2, -1 * speed); 269 String formattedSpeed = String.valueOf(Math.round(shutterSpeed * 10.0) / 10.0); 270 return formattedSpeed; 271 } else { 272 int approximateSpeedDenom = (int) Math.pow(2, speed) + 1; 273 String formattedSpeed = "1/" + String.valueOf(approximateSpeedDenom); 274 return formattedSpeed; 275 } 276 } 277 278 /** 279 * @param table 280 * @param resources 281 * @param tags 282 */ addDimensionsRow(TableDisplay table, Resources resources, Bundle tags)283 private static void addDimensionsRow(TableDisplay table, Resources resources, Bundle tags) { 284 if (tags.containsKey(ExifInterface.TAG_IMAGE_WIDTH) 285 && tags.containsKey(ExifInterface.TAG_IMAGE_LENGTH)) { 286 int width = tags.getInt(ExifInterface.TAG_IMAGE_WIDTH); 287 int height = tags.getInt(ExifInterface.TAG_IMAGE_LENGTH); 288 float megaPixels = height * width / 1000000f; 289 table.put(R.string.metadata_dimensions, 290 resources.getString( 291 R.string.metadata_dimensions_format, width, height, megaPixels)); 292 } 293 } 294 } 295