package de.jeisfeld.augendiagnoselib.util.imagefile;
import java.util.HashMap;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import de.jeisfeld.augendiagnoselib.Application;
import de.jeisfeld.augendiagnoselib.R;
import de.jeisfeld.augendiagnoselib.util.DialogUtil;
import de.jeisfeld.augendiagnoselib.util.PreferenceUtil;
import de.jeisfeld.augendiagnoselib.util.TrackingUtil;
import de.jeisfeld.augendiagnoselib.util.TrackingUtil.Category;
import de.jeisfeld.augendiagnoselib.util.imagefile.JpegMetadataUtil.ExifStorageException;
/**
* Utility class to help storing metadata in jpg files in a synchronized way, preventing to store the same file twice in
* parallel.
*/
public final class JpegSynchronizationUtil {
/**
* Hide default constructor.
*/
private JpegSynchronizationUtil() {
throw new UnsupportedOperationException();
}
/**
* Storage for currently running save tasks.
*/
private static final HashMap<String, JpegMetadata> RUNNING_SAVE_REQUESTS = new HashMap<>();
/**
* Storage for queued save tasks.
*/
private static final HashMap<String, JpegMetadata> QUEUED_SAVE_REQUESTS = new HashMap<>();
/**
* The tag for logging.
*/
private static final String TAG = Application.TAG + ".JSU";
/**
* This method handles a request to retrieve metadata for a file. If there is no running async task to update
* metadata for this file, then the data is taken directly from the file. Otherwise, it is taken from the last
* metadata to be stored for this file.
*
* @param pathname the path of the jpg file.
* @return null for non-JPEG files. The metadata from the file if readable. Otherwise empty metadata.
*/
public static JpegMetadata getJpegMetadata(@NonNull final String pathname) {
JpegMetadata cachedMetadata = null;
try {
JpegMetadataUtil.checkJpeg(pathname);
}
catch (Exception e) {
Log.w(TAG, e.getMessage());
return null;
}
synchronized (JpegSynchronizationUtil.class) {
if (QUEUED_SAVE_REQUESTS.containsKey(pathname)) {
cachedMetadata = QUEUED_SAVE_REQUESTS.get(pathname);
}
else if (RUNNING_SAVE_REQUESTS.containsKey(pathname)) {
cachedMetadata = RUNNING_SAVE_REQUESTS.get(pathname);
}
}
if (cachedMetadata != null) {
Log.i(TAG, "Retrieve cached metadata for file " + pathname);
return cachedMetadata;
}
else {
try {
return JpegMetadataUtil.getMetadata(pathname);
}
catch (Exception e) {
Log.e(TAG, "Failed to retrieve metadata for file " + pathname, e);
return new JpegMetadata();
}
}
}
/**
* This method handles a request to update metadata on a file. If no such request on the file is in process, then an
* async task is started to update the metadata. Otherwise, it is put on the queue.
*
* @param pathname the path of the jpg file.
* @param metadata the metadata.
*/
public static void storeJpegMetadata(@NonNull final String pathname, final JpegMetadata metadata) {
try {
JpegMetadataUtil.checkJpeg(pathname);
}
catch (Exception e) {
Log.w(TAG, e.getMessage());
return;
}
synchronized (JpegSynchronizationUtil.class) {
if (RUNNING_SAVE_REQUESTS.containsKey(pathname)) {
QUEUED_SAVE_REQUESTS.put(pathname, metadata);
}
else {
triggerJpegSaverTask(pathname, metadata);
}
}
}
/**
* Do cleanup from the last JpegSaverTask and trigger the next task on the same file, if existing.
*
* @param pathname The path of the jpg file.
*/
private static void triggerNextFromQueue(final String pathname) {
synchronized (JpegSynchronizationUtil.class) {
RUNNING_SAVE_REQUESTS.remove(pathname);
if (QUEUED_SAVE_REQUESTS.containsKey(pathname)) {
Log.i(TAG, "Executing queued store request for file " + pathname);
JpegMetadata newMetadata = QUEUED_SAVE_REQUESTS.get(pathname);
QUEUED_SAVE_REQUESTS.remove(pathname);
triggerJpegSaverTask(pathname, newMetadata);
}
}
}
/**
* Utility method to start the JpegSaverTask so save a jpg file with metadata.
*
* @param pathname the path of the jpg file.
* @param metadata the metadata.
*/
private static void triggerJpegSaverTask(final String pathname, final JpegMetadata metadata) {
RUNNING_SAVE_REQUESTS.put(pathname, metadata);
JpegSaverTask task = new JpegSaverTask(pathname, metadata);
task.execute();
PreferenceUtil.incrementCounter(R.string.key_statistics_countsave);
TrackingUtil.sendEvent(Category.EVENT_USER, "Save image", null);
}
/**
* Get information if there is an image in the process of being saved.
*
* @return true if an image is currently saved.
*/
public static boolean isSaving() {
return RUNNING_SAVE_REQUESTS.size() > 0;
}
/**
* Task to save a JPEG file asynchronously with changed metadata.
*/
private static final class JpegSaverTask extends AsyncTask<Void, Void, Exception> {
/**
* The path of the jpg file.
*/
private final String mPathname;
/**
* The changed metadata.
*/
private final JpegMetadata mMetadata;
/**
* Constructor for the task.
*
* @param pathname the path of the jpg file.
* @param metadata the metadata.
*/
private JpegSaverTask(final String pathname, final JpegMetadata metadata) {
this.mPathname = pathname;
this.mMetadata = metadata;
}
@Override
protected void onPreExecute() {
Log.d(TAG, "Starting thread to save file " + mPathname);
}
@Override
protected Exception doInBackground(final Void... nothing) {
try {
JpegMetadataUtil.changeMetadata(mPathname, mMetadata);
return null;
}
catch (Exception e) {
return e;
}
}
@Override
protected void onPostExecute(@Nullable final Exception e) {
if (e != null) {
if (e instanceof ExifStorageException) {
Log.e(TAG, "Failed to save file " + mPathname, e);
DialogUtil.displayToast(Application.getAppContext(),
R.string.message_dialog_failed_to_store_exif, mPathname);
}
else {
Log.e(TAG, "Failed to store EXIF data for file " + mPathname, e);
DialogUtil.displayToast(Application.getAppContext(),
R.string.message_dialog_failed_to_store_metadata, mPathname);
}
}
else {
Log.d(TAG, "Successfully saved file " + mPathname);
}
triggerNextFromQueue(mPathname);
}
}
}