blob: b2ae7582b3b40e2601c569f0a490ef861822d476 [file] [log] [blame]
/*
* 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<float[]> 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<Float, Void, Address>() {
@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));
}
}
}