1 /* 2 * Copyright (C) 2013 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.camera.ui; 18 19 import android.app.AlertDialog; 20 import android.app.Dialog; 21 import android.content.Context; 22 import android.content.DialogInterface; 23 import android.graphics.Bitmap; 24 import android.graphics.BitmapFactory; 25 import android.text.format.Formatter; 26 import android.view.LayoutInflater; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.widget.BaseAdapter; 30 import android.widget.ListView; 31 import android.widget.TextView; 32 33 import com.android.camera.data.MediaDetails; 34 import com.android.camera2.R; 35 36 import java.text.DecimalFormat; 37 import java.text.NumberFormat; 38 import java.util.ArrayList; 39 import java.util.Locale; 40 import java.util.Map.Entry; 41 42 /** 43 * Displays details (such as Exif) of a local media item. 44 */ 45 public class DetailsDialog { 46 47 /** 48 * Creates a dialog for showing media data. 49 * 50 * @param context the Android context. 51 * @param mediaDetails the media details to display. 52 * @return A dialog that can be made visible to show the media details. 53 */ 54 public static Dialog create(Context context, MediaDetails mediaDetails) { 55 ListView detailsList = (ListView) LayoutInflater.from(context).inflate( 56 R.layout.details_list, null, false); 57 detailsList.setAdapter(new DetailsAdapter(context, mediaDetails)); 58 59 final AlertDialog.Builder builder = 60 new AlertDialog.Builder(context); 61 return builder.setTitle(R.string.details).setView(detailsList) 62 .setPositiveButton(R.string.close, new DialogInterface.OnClickListener() { 63 @Override 64 public void onClick(DialogInterface dialog, int whichButton) { 65 dialog.dismiss(); 66 } 67 }).create(); 68 } 69 70 /** 71 * An adapter for feeding a details list view with the contents of a 72 * {@link MediaDetails} instance. 73 */ 74 private static class DetailsAdapter extends BaseAdapter { 75 private final Context mContext; 76 private final MediaDetails mMediaDetails; 77 private final ArrayList<String> mItems; 78 private final Locale mDefaultLocale = Locale.getDefault(); 79 private final DecimalFormat mDecimalFormat = new DecimalFormat(".####"); 80 private int mWidthIndex = -1; 81 private int mHeightIndex = -1; 82 83 public DetailsAdapter(Context context, MediaDetails details) { 84 mContext = context; 85 mMediaDetails = details; 86 mItems = new ArrayList<String>(details.size()); 87 setDetails(context, details); 88 } 89 90 private void setDetails(Context context, MediaDetails details) { 91 boolean resolutionIsValid = true; 92 String path = null; 93 for (Entry<Integer, Object> detail : details) { 94 String value; 95 switch (detail.getKey()) { 96 case MediaDetails.INDEX_SIZE: { 97 value = Formatter.formatFileSize( 98 context, (Long) detail.getValue()); 99 break; 100 } 101 case MediaDetails.INDEX_WHITE_BALANCE: { 102 value = "1".equals(detail.getValue()) 103 ? context.getString(R.string.manual) 104 : context.getString(R.string.auto); 105 break; 106 } 107 case MediaDetails.INDEX_FLASH: { 108 MediaDetails.FlashState flash = 109 (MediaDetails.FlashState) detail.getValue(); 110 // TODO: camera doesn't fill in the complete values, 111 // show more information when it is fixed. 112 if (flash.isFlashFired()) { 113 value = context.getString(R.string.flash_on); 114 } else { 115 value = context.getString(R.string.flash_off); 116 } 117 break; 118 } 119 case MediaDetails.INDEX_EXPOSURE_TIME: { 120 value = (String) detail.getValue(); 121 double time = Double.valueOf(value); 122 if (time < 1.0f) { 123 value = String.format(mDefaultLocale, "%d/%d", 1, 124 (int) (0.5f + 1 / time)); 125 } else { 126 int integer = (int) time; 127 time -= integer; 128 value = String.valueOf(integer) + "''"; 129 if (time > 0.0001) { 130 value += String.format(mDefaultLocale, " %d/%d", 1, 131 (int) (0.5f + 1 / time)); 132 } 133 } 134 break; 135 } 136 case MediaDetails.INDEX_WIDTH: 137 mWidthIndex = mItems.size(); 138 if (detail.getValue().toString().equalsIgnoreCase("0")) { 139 value = context.getString(R.string.unknown); 140 resolutionIsValid = false; 141 } else { 142 value = toLocalInteger(detail.getValue()); 143 } 144 break; 145 case MediaDetails.INDEX_HEIGHT: { 146 mHeightIndex = mItems.size(); 147 if (detail.getValue().toString().equalsIgnoreCase("0")) { 148 value = context.getString(R.string.unknown); 149 resolutionIsValid = false; 150 } else { 151 value = toLocalInteger(detail.getValue()); 152 } 153 break; 154 } 155 case MediaDetails.INDEX_PATH: 156 // Prepend the new-line as a) paths are usually long, so 157 // the formatting is better and b) an RTL UI will see it 158 // as a separate section and interpret it for what it 159 // is, rather than trying to make it RTL (which messes 160 // up the path). 161 value = "\n" + detail.getValue().toString(); 162 path = detail.getValue().toString(); 163 break; 164 case MediaDetails.INDEX_ORIENTATION: 165 value = toLocalInteger(detail.getValue()); 166 break; 167 case MediaDetails.INDEX_ISO: 168 value = toLocalNumber(Integer.parseInt((String) detail.getValue())); 169 break; 170 case MediaDetails.INDEX_FOCAL_LENGTH: 171 double focalLength = Double.parseDouble(detail.getValue().toString()); 172 value = toLocalNumber(focalLength); 173 break; 174 default: { 175 Object valueObj = detail.getValue(); 176 // This shouldn't happen, log its key to help us 177 // diagnose the problem. 178 if (valueObj == null) { 179 fail("%s's value is Null", 180 getDetailsName(context, 181 detail.getKey())); 182 } 183 value = valueObj.toString(); 184 } 185 } 186 int key = detail.getKey(); 187 if (details.hasUnit(key)) { 188 value = String.format("%s: %s %s", getDetailsName( 189 context, key), value, context.getString(details.getUnit(key))); 190 } else { 191 value = String.format("%s: %s", getDetailsName( 192 context, key), value); 193 } 194 mItems.add(value); 195 } 196 if (!resolutionIsValid) { 197 resolveResolution(path); 198 } 199 } 200 201 public void resolveResolution(String path) { 202 Bitmap bitmap = BitmapFactory.decodeFile(path); 203 if (bitmap == null) 204 return; 205 onResolutionAvailable(bitmap.getWidth(), bitmap.getHeight()); 206 } 207 208 @Override 209 public boolean areAllItemsEnabled() { 210 return false; 211 } 212 213 @Override 214 public boolean isEnabled(int position) { 215 return false; 216 } 217 218 @Override 219 public int getCount() { 220 return mItems.size(); 221 } 222 223 @Override 224 public Object getItem(int position) { 225 return mMediaDetails.getDetail(position); 226 } 227 228 @Override 229 public long getItemId(int position) { 230 return position; 231 } 232 233 @Override 234 public View getView(int position, View convertView, ViewGroup parent) { 235 TextView tv; 236 if (convertView == null) { 237 tv = (TextView) LayoutInflater.from(mContext).inflate( 238 R.layout.details, parent, false); 239 } else { 240 tv = (TextView) convertView; 241 } 242 tv.setText(mItems.get(position)); 243 return tv; 244 } 245 246 public void onResolutionAvailable(int width, int height) { 247 if (width == 0 || height == 0) 248 return; 249 // Update the resolution with the new width and height 250 String widthString = String.format(mDefaultLocale, "%s: %d", 251 getDetailsName( 252 mContext, MediaDetails.INDEX_WIDTH), width); 253 String heightString = String.format(mDefaultLocale, "%s: %d", 254 getDetailsName( 255 mContext, MediaDetails.INDEX_HEIGHT), height); 256 mItems.set(mWidthIndex, String.valueOf(widthString)); 257 mItems.set(mHeightIndex, String.valueOf(heightString)); 258 notifyDataSetChanged(); 259 } 260 261 /** 262 * Converts the given integer (given as String or Integer object) to a 263 * localized String version. 264 */ 265 private String toLocalInteger(Object valueObj) { 266 if (valueObj instanceof Integer) { 267 return toLocalNumber((Integer) valueObj); 268 } else { 269 String value = valueObj.toString(); 270 try { 271 value = toLocalNumber(Integer.parseInt(value)); 272 } catch (NumberFormatException ex) { 273 // Just keep the current "value" if we cannot 274 // parse it as a fallback. 275 } 276 return value; 277 } 278 } 279 280 /** Converts the given integer to a localized String version. */ 281 private String toLocalNumber(int n) { 282 return String.format(mDefaultLocale, "%d", n); 283 } 284 285 /** Converts the given double to a localized String version. */ 286 private String toLocalNumber(double n) { 287 return mDecimalFormat.format(n); 288 } 289 } 290 291 public static String getDetailsName(Context context, int key) { 292 switch (key) { 293 case MediaDetails.INDEX_TITLE: 294 return context.getString(R.string.title); 295 case MediaDetails.INDEX_DESCRIPTION: 296 return context.getString(R.string.description); 297 case MediaDetails.INDEX_DATETIME: 298 return context.getString(R.string.time); 299 case MediaDetails.INDEX_LOCATION: 300 return context.getString(R.string.location); 301 case MediaDetails.INDEX_PATH: 302 return context.getString(R.string.path); 303 case MediaDetails.INDEX_WIDTH: 304 return context.getString(R.string.width); 305 case MediaDetails.INDEX_HEIGHT: 306 return context.getString(R.string.height); 307 case MediaDetails.INDEX_ORIENTATION: 308 return context.getString(R.string.orientation); 309 case MediaDetails.INDEX_DURATION: 310 return context.getString(R.string.duration); 311 case MediaDetails.INDEX_MIMETYPE: 312 return context.getString(R.string.mimetype); 313 case MediaDetails.INDEX_SIZE: 314 return context.getString(R.string.file_size); 315 case MediaDetails.INDEX_MAKE: 316 return context.getString(R.string.maker); 317 case MediaDetails.INDEX_MODEL: 318 return context.getString(R.string.model); 319 case MediaDetails.INDEX_FLASH: 320 return context.getString(R.string.flash); 321 case MediaDetails.INDEX_APERTURE: 322 return context.getString(R.string.aperture); 323 case MediaDetails.INDEX_FOCAL_LENGTH: 324 return context.getString(R.string.focal_length); 325 case MediaDetails.INDEX_WHITE_BALANCE: 326 return context.getString(R.string.white_balance); 327 case MediaDetails.INDEX_EXPOSURE_TIME: 328 return context.getString(R.string.exposure_time); 329 case MediaDetails.INDEX_ISO: 330 return context.getString(R.string.iso); 331 default: 332 return "Unknown key" + key; 333 } 334 } 335 336 /** 337 * Throw an assertion error wit the given message. 338 * 339 * @param message the message, can contain placeholders. 340 * @param args if he message contains placeholders, these values will be 341 * used to fill them. 342 */ 343 private static void fail(String message, Object... args) { 344 throw new AssertionError( 345 args.length == 0 ? message : String.format(message, args)); 346 } 347 } 348