package com.droidmapper.util; import android.content.ContentResolver; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.hardware.Camera; import android.location.Location; import android.media.ExifInterface; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.provider.MediaStore; import android.util.Log; import com.droidmapper.CameraActivity; import com.droidmapper.R; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.Vector; /** * This is a utility class that provides a way to its clients to buffer taken photos in a queue, and * to save them on local storage one by one.<br> * <b>Note:</b> Currently this thread, after receiving stop command, stops immediately dropping all * remaining queued tasks. If this is unwanted, because the app might/will lose a few photos, this * class should be modified to first finish queued tasks and then exit. */ @Deprecated public class PhotoProcessorThread extends Thread { private static final String TAG = PhotoProcessorThread.class.getName(); private volatile Camera.Parameters camParams; private volatile boolean halt; private final SimpleDateFormat dateFormat, exifGpsDateFormat, exifDateFormat; private final DropboxUploaderThread dbUpldrThread; private final ContentResolver contentResolver; private final String imgDescriptionTxt; private final Vector<JobStruct> queue; private final CameraActivity activity; private final File mediaStorageDir; private final Object lock; /** * Default constructor. It creates an instance of this class using the Context supplied as * parameter. * * @param activity The CameraActivity this class is running in, through which it can * access the current theme, resources, etc. * @param dbUpldrThread The Dropbox uploader thread that should upload to Dropbox all the photos * saved by this thread. */ public PhotoProcessorThread(CameraActivity activity, DropboxUploaderThread dbUpldrThread) { if (activity == null) { throw new NullPointerException("Activity param can't be null."); } if (dbUpldrThread == null) { throw new NullPointerException("DropboxUploaderThread param can't be null."); } this.activity = activity; this.dbUpldrThread = dbUpldrThread; // Create queue that will buffer taken photos: queue = new Vector<JobStruct>(); // Create this thread's lock(used for synchronization): lock = new Object(); // A flag that we use to signal this thread to stop itself: halt = false; // ContentResolver used to add the captured photos to the device gallery: contentResolver = activity.getContentResolver(); // Description for photos taken by this app, used in device's gallery app: imgDescriptionTxt = activity.getString(R.string.ppThread_photo_description); // Create(if it does not exist) and initialize the directory in which the images will be saved: File picsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); mediaStorageDir = new File(picsDir, activity.getString(R.string.app_name)); if (!mediaStorageDir.exists()) { mediaStorageDir.mkdirs(); } // Create a date format using which we will format photos timestamps and create their file // names: dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS"); // And date formats for exif tags: exifGpsDateFormat = new SimpleDateFormat("yyyy:MM:dd", Locale.ENGLISH); exifDateFormat = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss"); } /** * In a background thread write the queued photos on local storage. */ @Override public void run() { Log.d(TAG, "run() :: Start"); while (!halt) { if (queue.isEmpty()) { // If there are no images in the queue, sleep: synchronized (lock) { if (!halt) { try { lock.wait(); } catch (InterruptedException e) { } } } } else { // Get new photo to save: JobStruct job = queue.remove(0); byte[] data = job.data; // Create its file: String tsText = dateFormat.format(new Date(job.timestamp)); String filename = tsText + ".jpg"; String filePath = mediaStorageDir.getPath() + File.separator + filename; File photoFile = new File(filePath); // If needed fix the photo rotation: BitmapFactory.Options bfOptions = new BitmapFactory.Options(); bfOptions.inJustDecodeBounds = true; BitmapFactory.decodeByteArray(data, 0, data.length, bfOptions); if ((job.devOrienAtCapture == 0 || job.devOrienAtCapture == 180) && bfOptions.outWidth > bfOptions.outHeight) { Bitmap src = BitmapFactory.decodeByteArray(data, 0, data.length); Matrix matrix = new Matrix(); matrix.postRotate(90); Bitmap out = Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, true); ByteArrayOutputStream baos = new ByteArrayOutputStream(); out.compress(Bitmap.CompressFormat.JPEG, 100, baos); data = baos.toByteArray(); try { baos.close(); } catch (IOException e) { e.printStackTrace(); } } // Write photo data to the created file: FileOutputStream fos = null; try { fos = new FileOutputStream(photoFile); fos.write(data); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } // Add EXIF data to the captured photo: Date date = new Date(); try { ExifInterface exif = new ExifInterface(filePath); if (job.deviceLocation != null) { exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE, GpsUtil.convert(job.deviceLocation.getLatitude())); exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, GpsUtil.latitudeRef(job.deviceLocation.getLatitude())); exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, GpsUtil.convert(job.deviceLocation.getLongitude())); exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, GpsUtil.longitudeRef(job.deviceLocation.getLongitude())); double alt = job.deviceLocation.getAltitude(); if (alt >= 0) { exif.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, String.valueOf(0)); } else { exif.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, String.valueOf(1)); } exif.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, String.valueOf(Math.round(alt))); exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, exifGpsDateFormat.format(date)); exif.setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, job.deviceLocation.getProvider()); } exif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(bfOptions.outWidth)); exif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(bfOptions.outHeight)); exif.setAttribute(ExifInterface.TAG_DATETIME, exifDateFormat.format(date)); exif.setAttribute(ExifInterface.TAG_MAKE, Build.MANUFACTURER); exif.setAttribute(ExifInterface.TAG_MODEL, Build.MODEL); // if(job.devOrienAtCapture == 0 || job.devOrienAtCapture == 360){ // exif.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(ExifInterface.ORIENTATION_NORMAL)); // } else if(job.devOrienAtCapture == 90){ // exif.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(ExifInterface.ORIENTATION_ROTATE_90)); // } else if(job.devOrienAtCapture == 180){ // exif.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(ExifInterface.ORIENTATION_ROTATE_180)); // } else if(job.devOrienAtCapture == 270){ // exif.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(ExifInterface.ORIENTATION_ROTATE_270)); // } if (camParams != null) { String fm = camParams.getFlashMode(); if (fm == null || fm.equals(Camera.Parameters.FLASH_MODE_OFF)) { exif.setAttribute(ExifInterface.TAG_FLASH, String.valueOf(0)); } else { exif.setAttribute(ExifInterface.TAG_FLASH, String.valueOf(0)); } float fl = camParams.getFocalLength(); exif.setAttribute(ExifInterface.TAG_FOCAL_LENGTH, String.valueOf(fl)); String wb = camParams.getWhiteBalance(); if (wb != null) { if (wb.equals(Camera.Parameters.WHITE_BALANCE_AUTO)) { exif.setAttribute(ExifInterface.TAG_WHITE_BALANCE, String.valueOf(ExifInterface.WHITEBALANCE_AUTO)); } else { exif.setAttribute(ExifInterface.TAG_WHITE_BALANCE, String.valueOf(ExifInterface.WHITEBALANCE_MANUAL)); } } String ap = camParams.get("aperture"); if (ap != null) { exif.setAttribute(ExifInterface.TAG_APERTURE, ap); } } // TAG_EXPOSURE_TIME TAG_ISO exif.saveAttributes(); } catch (IOException e) { e.printStackTrace(); } // Add the saved photo to the device gallery: try { MediaStore.Images.Media.insertImage(contentResolver, filePath, filename, imgDescriptionTxt); String urlToAddedImage = MediaStore.Images.Media.insertImage(contentResolver, filePath, filename, imgDescriptionTxt); Log.d(TAG, "PhotoProcessorThread.run() :: urlToAddedImage = " + urlToAddedImage); String pathToAddedImage = Util.getFilePathFromUri(activity, Uri.parse(urlToAddedImage)); Log.d(TAG, "PhotoProcessorThread.run() :: pathToAddedImage = " + pathToAddedImage); Util.copyExifTags(filePath, pathToAddedImage, bfOptions.outWidth, bfOptions.outHeight); } catch (FileNotFoundException e) { e.printStackTrace(); } // Upload the saved photo to Dropbox: dbUpldrThread.queuePhoto(filePath); activity.postLastCapturedPhotoFilenameUpdate(filename); } } Log.d(TAG, "run() :: Stop"); } /** * Add a new photo to the queue to be saved on local storage. * * @param data Byte array containing the image data. * @param timestamp System time at which the image was taken. * @param devOrienAtCapture Device orientation at the time the photo is taken. * @param deviceLocation Location at which the photo was captured. */ public void queuePhoto(byte[] data, long timestamp, int devOrienAtCapture, Location deviceLocation) { Log.d(TAG, "queuePhoto() :: Already in queue " + queue.size()); // Don't let the queue have more than 3 photos to prevent out of memory exceptions: if (queue.size() > 2) { queue.remove(0); } // Add the new photo to the queue: queue.add(new JobStruct(data, timestamp, devOrienAtCapture, deviceLocation)); // Notify the thread about this(it might be sleeping): synchronized (lock) { lock.notifyAll(); } } /** * Stop this thread. */ public void halt() { Log.d(TAG, "halt()"); // Set the stop flag: halt = true; // Notify the thread about this(it might be sleeping): synchronized (lock) { lock.notifyAll(); } } public void setCamParams(Camera.Parameters camParams) { this.camParams = camParams; } /** * A helper class used to hold photo timestamp and image data. */ private class JobStruct { private final Location deviceLocation; private final int devOrienAtCapture; private final long timestamp; private final byte[] data; /** * Default constructor. Creates a new instance of this struct using the given parameters. * * @param data Byte array containing the image data. * @param timestamp System time at which the image was taken. * @param devOrienAtCapture Device orientation at the time the photo is taken. * @param deviceLocation Location at which the photo was captured */ public JobStruct(byte[] data, long timestamp, int devOrienAtCapture, Location deviceLocation) { this.deviceLocation = deviceLocation; this.data = data; this.timestamp = timestamp; this.devOrienAtCapture = devOrienAtCapture; } } }