/* * Copyright (C) 2017 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 com.android.documentsui.inspector; import android.content.Context; import android.content.res.Resources; import android.location.Address; import android.location.Geocoder; import android.media.ExifInterface; import android.media.MediaMetadata; import android.os.AsyncTask; import android.os.Bundle; import android.provider.DocumentsContract; import android.support.annotation.VisibleForTesting; import android.text.format.DateUtils; import android.util.AttributeSet; import com.android.documentsui.R; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.Shared; import com.android.documentsui.inspector.InspectorController.MediaDisplay; import com.android.documentsui.inspector.InspectorController.TableDisplay; import java.io.IOException; import java.util.function.Consumer; import javax.annotation.Nullable; /** * Organizes and Displays the debug information about a file. This view * should only be made visible when build is debuggable and system policies * allow debug "stuff". */ public class MediaView extends TableView implements MediaDisplay { private final Resources mResources; private final Context mContext; public MediaView(Context context) { this(context, null); } public MediaView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MediaView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mContext = context; mResources = context.getResources(); } @Override public void accept(DocumentInfo doc, Bundle metadata, @Nullable Runnable geoClickListener) { setTitle(R.string.inspector_metadata_section, true); Bundle exif = metadata.getBundle(DocumentsContract.METADATA_EXIF); if (exif != null) { showExifData(this, mResources, doc, exif, geoClickListener, this::getAddress); } Bundle video = metadata.getBundle(Shared.METADATA_KEY_VIDEO); if (video != null) { showVideoData(this, mResources, doc, video, geoClickListener); } Bundle audio = metadata.getBundle(Shared.METADATA_KEY_AUDIO); if (audio != null) { showAudioData(this, audio); } setVisible(!isEmpty()); } @VisibleForTesting public static void showAudioData(TableDisplay table, Bundle tags) { if (tags.containsKey(MediaMetadata.METADATA_KEY_ARTIST)) { table.put(R.string.metadata_artist, tags.getString(MediaMetadata.METADATA_KEY_ARTIST)); } if (tags.containsKey(MediaMetadata.METADATA_KEY_COMPOSER)) { table.put(R.string.metadata_composer, tags.getString(MediaMetadata.METADATA_KEY_COMPOSER)); } if (tags.containsKey(MediaMetadata.METADATA_KEY_ALBUM)) { table.put(R.string.metadata_album, tags.getString(MediaMetadata.METADATA_KEY_ALBUM)); } if (tags.containsKey(MediaMetadata.METADATA_KEY_DURATION)) { int millis = tags.getInt(MediaMetadata.METADATA_KEY_DURATION); table.put(R.string.metadata_duration, DateUtils.formatElapsedTime(millis / 1000)); } } @VisibleForTesting public static void showVideoData( TableDisplay table, Resources resources, DocumentInfo doc, Bundle tags, @Nullable Runnable geoClickListener) { addDimensionsRow(table, resources, tags); if (MetadataUtils.hasVideoCoordinates(tags)) { float[] coords = MetadataUtils.getVideoCoords(tags); showCoordiantes(table, resources, coords, geoClickListener); } if (tags.containsKey(MediaMetadata.METADATA_KEY_DURATION)) { int millis = tags.getInt(MediaMetadata.METADATA_KEY_DURATION); table.put(R.string.metadata_duration, DateUtils.formatElapsedTime(millis / 1000)); } } @VisibleForTesting public static void showExifData( TableDisplay table, Resources resources, DocumentInfo doc, Bundle tags, @Nullable Runnable geoClickListener, Consumer geoAddressFetcher) { addDimensionsRow(table, resources, tags); if (tags.containsKey(ExifInterface.TAG_DATETIME)) { String date = tags.getString(ExifInterface.TAG_DATETIME); table.put(R.string.metadata_date_time, date); } if (tags.containsKey(ExifInterface.TAG_GPS_ALTITUDE)) { double altitude = tags.getDouble(ExifInterface.TAG_GPS_ALTITUDE); table.put(R.string.metadata_altitude, String.valueOf(altitude)); } if (tags.containsKey(ExifInterface.TAG_MAKE) || tags.containsKey(ExifInterface.TAG_MODEL)) { String make = tags.getString(ExifInterface.TAG_MAKE); String model = tags.getString(ExifInterface.TAG_MODEL); make = make != null ? make : ""; model = model != null ? model : ""; table.put( R.string.metadata_camera, resources.getString(R.string.metadata_camera_format, make, model)); } if (tags.containsKey(ExifInterface.TAG_APERTURE)) { table.put(R.string.metadata_aperture, resources.getString( R.string.metadata_aperture_format, tags.getDouble(ExifInterface.TAG_APERTURE))); } if (tags.containsKey(ExifInterface.TAG_SHUTTER_SPEED_VALUE)) { String shutterSpeed = String.valueOf( formatShutterSpeed(tags.getDouble(ExifInterface.TAG_SHUTTER_SPEED_VALUE))); table.put(R.string.metadata_shutter_speed, shutterSpeed); } if (tags.containsKey(ExifInterface.TAG_FOCAL_LENGTH)) { double length = tags.getDouble(ExifInterface.TAG_FOCAL_LENGTH); table.put(R.string.metadata_focal_length, String.format(resources.getString(R.string.metadata_focal_format), length)); } if (tags.containsKey(ExifInterface.TAG_ISO_SPEED_RATINGS)) { int iso = tags.getInt(ExifInterface.TAG_ISO_SPEED_RATINGS); table.put(R.string.metadata_iso_speed_ratings, String.format(resources.getString(R.string.metadata_iso_format), iso)); } if (MetadataUtils.hasExifGpsFields(tags)) { float[] coords = MetadataUtils.getExifGpsCoords(tags); showCoordiantes(table, resources, coords, geoClickListener); geoAddressFetcher.accept(coords); } } private static void showCoordiantes( TableDisplay table, Resources resources, float[] coords, @Nullable Runnable geoClickListener) { String value = resources.getString( R.string.metadata_coordinates_format, coords[0], coords[1]); if (geoClickListener != null) { table.put( R.string.metadata_coordinates, value, view -> { geoClickListener.run(); } ); } else { table.put(R.string.metadata_coordinates, value); } } /** * Attempts to retrieve an approximate address and displays the address if it can find one. * @param coords the coordinates that gets an address. */ private void getAddress(float[] coords) { new AsyncTask() { @Override protected Address doInBackground(Float... coords) { assert (coords.length == 2); Geocoder geocoder = new Geocoder(mContext); try { Address address = geocoder.getFromLocation(coords[0], // latitude coords[1], // longitude 1 // amount of results returned ).get(0); return address; } catch (IOException e) { return null; } } @Override protected void onPostExecute(@Nullable Address address) { if (address != null) { TableDisplay table = MediaView.this; if (address.getMaxAddressLineIndex() >= 0) { String formattedAddress; StringBuilder addressBuilder = new StringBuilder(""); addressBuilder.append(address.getAddressLine(0)); for (int i = 1; i < address.getMaxAddressLineIndex(); i++) { addressBuilder.append("\n"); addressBuilder.append(address.getAddressLine(i)); } formattedAddress = addressBuilder.toString(); table.put(R.string.metadata_address, formattedAddress); } else if (address.getLocality() != null) { table.put(R.string.metadata_address, address.getLocality()); } else if (address.getSubAdminArea() != null) { table.put(R.string.metadata_address, address.getSubAdminArea()); } else if (address.getAdminArea() != null) { table.put(R.string.metadata_address, address.getAdminArea()); } else if (address.getCountryName() != null) { table.put(R.string.metadata_address, address.getCountryName()); } } } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, coords[0], coords[1]); } /** * @param speed a value n, where shutter speed equals 1/(2^n) * @return a String containing either a fraction that displays 1 over a positive integer, or a * double rounded to one decimal, depending on if 1/(2^n) is less than or greater than 1, * respectively. */ private static String formatShutterSpeed(double speed) { if (speed <= 0) { double shutterSpeed = Math.pow(2, -1 * speed); String formattedSpeed = String.valueOf(Math.round(shutterSpeed * 10.0) / 10.0); return formattedSpeed; } else { int approximateSpeedDenom = (int) Math.pow(2, speed) + 1; String formattedSpeed = "1/" + String.valueOf(approximateSpeedDenom); return formattedSpeed; } } /** * @param table * @param resources * @param tags */ private static void addDimensionsRow(TableDisplay table, Resources resources, Bundle tags) { if (tags.containsKey(ExifInterface.TAG_IMAGE_WIDTH) && tags.containsKey(ExifInterface.TAG_IMAGE_LENGTH)) { int width = tags.getInt(ExifInterface.TAG_IMAGE_WIDTH); int height = tags.getInt(ExifInterface.TAG_IMAGE_LENGTH); float megaPixels = height * width / 1000000f; table.put(R.string.metadata_dimensions, resources.getString( R.string.metadata_dimensions_format, width, height, megaPixels)); } } }