/* * @copyright 2012 Philip Warner * @license GNU General Public License * * This file is part of Book Catalogue. * * Book Catalogue is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Book Catalogue is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Book Catalogue. If not, see <http://www.gnu.org/licenses/>. */ package com.eleybourn.bookcatalogue.utils; import java.util.ArrayList; import android.annotation.TargetApi; import android.app.Activity; import android.app.Dialog; import android.app.ProgressDialog; import android.content.DialogInterface; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.support.v4.app.FragmentActivity; import android.widget.Toast; import com.eleybourn.bookcatalogue.BookCatalogueApp; import com.eleybourn.bookcatalogue.R; import com.eleybourn.bookcatalogue.compat.BookCatalogueDialogFragment; import com.eleybourn.bookcatalogue.utils.SimpleTaskQueue.SimpleTask; import com.eleybourn.bookcatalogue.utils.SimpleTaskQueue.SimpleTaskContext; /** * Fragment Class to wrap a trivial progress dialog arounf (generally) a single task. * * @author pjw */ public class SimpleTaskQueueProgressFragment extends BookCatalogueDialogFragment { /** The underlying task queue */ private final SimpleTaskQueue mQueue; /** Handler so we can detect UI thread */ private Handler mHandler = new Handler(); /** List of messages queued; only used if activity not present when showToast() is called */ private ArrayList<String> mMessages = null; /** Flag indicating dialog was cancelled */ private boolean mWasCancelled = false; /** Max value of progress (for determinate progress) */ private String mMessage = null; /** Max value of progress (for determinate progress) */ private int mMax; /** Current value of progress (for determinate progress) */ private int mProgress = 0; /** Flag indicating underlying field has changed so that progress dialog will be updated */ private boolean mMessageChanged = false; /** Flag indicating underlying field has changed so that progress dialog will be updated */ private boolean mProgressChanged = false; /** Flag indicating underlying field has changed so that progress dialog will be updated */ private boolean mMaxChanged = false; /** Flag indicating underlying field has changed so that progress dialog will be updated */ private boolean mNumberFormatChanged = false; /** Format of number part of dialog */ private String mNumberFormat = null; /** Unique ID for this task. Can be used like menu or activity IDs */ private int mTaskId; /** Flag, defaults to true, that can be set by tasks and is passed to listeners */ private boolean mSuccess = true; /** List of messages to be sent to the underlying activity, but not yet sent */ private ArrayList<TaskMessage> mTaskMessages = new ArrayList<TaskMessage>(); /** Each message has a single method to deliver it and will only be called * when the underlying Activity is actually present. */ private static interface TaskMessage { public void deliver(Activity a); } /** Listener for OnTaskFinished messages */ public interface OnTaskFinishedListener { public void onTaskFinished(SimpleTaskQueueProgressFragment fragment, int taskId, boolean success, boolean cancelled, FragmentTask task); } /** Listener for OnAllTasksFinished messages */ public interface OnAllTasksFinishedListener { public void onAllTasksFinished(SimpleTaskQueueProgressFragment fragment, int taskId, boolean success, boolean cancelled); } /** * TaskFinished message. * * We only deliver onFinish() to the FragmentTask when the activity is present. */ private class TaskFinishedMessage implements TaskMessage { FragmentTask mTask; Exception mException; public TaskFinishedMessage(FragmentTask task, Exception e) { mTask = task; mException = e; } @Override public void deliver(Activity a) { try { mTask.onFinish(SimpleTaskQueueProgressFragment.this, mException); } catch (Exception e) { Logger.logError(e); } try { if (a instanceof OnTaskFinishedListener) { ((OnTaskFinishedListener)a).onTaskFinished(SimpleTaskQueueProgressFragment.this, mTaskId, mSuccess, mWasCancelled, mTask); } } catch (Exception e) { Logger.logError(e); } } } /** * AllTasksFinished message. */ private class AllTasksFinishedMessage implements TaskMessage { public AllTasksFinishedMessage() { } @Override public void deliver(Activity a) { if (a instanceof OnAllTasksFinishedListener) { ((OnAllTasksFinishedListener)a).onAllTasksFinished(SimpleTaskQueueProgressFragment.this, mTaskId, mSuccess, mWasCancelled); } dismiss(); } } /** * Queue a TaskMessage and then try to process the queue. */ private void queueMessage(TaskMessage m) { synchronized(mTaskMessages) { mTaskMessages.add(m); } deliverMessages(); } /** * Queue a TaskFinished message */ private void queueTaskFinished(FragmentTask t, Exception e) { queueMessage(new TaskFinishedMessage(t, e)); } /** * Queue an AllTasksFinished message */ private void queueAllTasksFinished() { queueMessage(new AllTasksFinishedMessage()); } /** * If we have an Activity, deliver the current queue. */ private void deliverMessages() { Activity a = getActivity(); if (a != null) { ArrayList<TaskMessage> toDeliver = new ArrayList<TaskMessage>(); int count = 0; do { synchronized(mTaskMessages) { toDeliver.addAll(mTaskMessages); mTaskMessages.clear(); } count = toDeliver.size(); for(TaskMessage m: toDeliver) { try { m.deliver(a); } catch (Exception e) { Logger.logError(e); } } toDeliver.clear(); } while (count > 0); } } /** * Convenience routine to show a dialog fragment and start the task * * @param context Activity of caller * @param message Message to display * @param task Task to run */ public static SimpleTaskQueueProgressFragment runTaskWithProgress(final FragmentActivity context, int message, FragmentTask task, boolean isIndeterminate, int taskId) { SimpleTaskQueueProgressFragment frag = SimpleTaskQueueProgressFragment.newInstance(message, isIndeterminate, taskId); frag.enqueue(task); frag.show(context.getSupportFragmentManager(), (String) null); return frag; } /** * Interface for 'FragmentTask' objects. Closely based on SimpleTask, but takes the fragment as a parameter * to all calls. * * @author pjw */ public interface FragmentTask { /** Run the task in it's own thread * @throws Exception */ public void run(SimpleTaskQueueProgressFragment fragment, SimpleTaskContext taskContext) throws Exception; /** Called in UI thread after task complete * @param exception TODO*/ public void onFinish(SimpleTaskQueueProgressFragment fragment, Exception exception); } /** * Trivial implementation of FragmentTask that never calls onFinish(). The setState()/getState() * calles can be used to store state info by a caller, eg. if they override requiresOnFinish() etc. * * @author pjw */ public abstract static class FragmentTaskAbstract implements FragmentTask { private int mState = 0; @Override public void onFinish(SimpleTaskQueueProgressFragment fragment, Exception exception) { if (exception != null) { Logger.logError(exception); Toast.makeText(fragment.getActivity(), R.string.unexpected_error, Toast.LENGTH_LONG).show(); } } public void setState(int state) { mState = state; } public int getState() { return mState; } } /** * A SimpleTask wrapper for a FragmentTask. * * @author pjw */ private class FragmentTaskWrapper implements SimpleTask { private FragmentTask mInnerTask; public FragmentTaskWrapper(FragmentTask task) { mInnerTask = task; } @Override public void run(SimpleTaskContext taskContext) throws Exception { try { mInnerTask.run(SimpleTaskQueueProgressFragment.this, taskContext); } catch (Exception e) { mSuccess = false; throw e; } } @Override public void onFinish(Exception e) { SimpleTaskQueueProgressFragment.this.queueTaskFinished(mInnerTask, e); } } /** * Constructor */ public SimpleTaskQueueProgressFragment() { mQueue = new SimpleTaskQueue("FragmentQueue"); mQueue.setTaskFinishListener(mTaskFinishListener); } /** * Utility routine to display a Toast message or queue it as appropriate. * @param id */ public void showToast(final int id) { if (id != 0) { // We don't use getString() because we have no guarantee this // object is associated with an activity when this is called, and // for whatever reason the implementation requires it. showToast(BookCatalogueApp.getResourceString(id)); } } /** * Utility routine to display a Toast message or queue it as appropriate. * @param id */ public void showToast(final String message) { // Can only display in main thread. if (Looper.getMainLooper().getThread() == Thread.currentThread() ) { synchronized(this) { if (this.getActivity() != null) { Toast.makeText(this.getActivity(), message, Toast.LENGTH_LONG).show(); } else { // Assume the toast message was sent before the fragment was displayed; this // list will be read in onAttach if (mMessages == null) mMessages = new ArrayList<String>(); mMessages.add(message); } } } else { // Post() it to main thread. mHandler.post(new Runnable() { @Override public void run() { showToast(message); } }); } } /** * Post a runnable to the UI thread * * @param r */ public void post(Runnable r) { mHandler.post(r); } /** * Enqueue a task for this fragment * * @param task */ public void enqueue(FragmentTask task) { mQueue.enqueue(new FragmentTaskWrapper(task)); } public static SimpleTaskQueueProgressFragment newInstance(int title, boolean isIndeterminate, int taskId) { SimpleTaskQueueProgressFragment frag = new SimpleTaskQueueProgressFragment(); Bundle args = new Bundle(); args.putInt("title", title); args.putInt("taskId", taskId); args.putBoolean("isIndeterminate", isIndeterminate); frag.setArguments(args); return frag; } /** * Ensure activity supports event */ @Override public void onAttach(Activity a) { super.onAttach(a); synchronized(this) { if (mMessages != null) { for(String message: mMessages) { if (message != null && !message.equals("")) Toast.makeText(a, message, Toast.LENGTH_LONG).show(); } mMessages.clear(); } } //if (! (a instanceof OnSyncTaskCompleteListener)) // throw new RuntimeException("Activity " + a.getClass().getSimpleName() + " must implement OnSyncTaskCompleteListener"); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // VERY IMPORTANT. We do not want this destroyed! setRetainInstance(true); mTaskId = getArguments().getInt("taskId"); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // Deliver any outstanding messages deliverMessages(); // If no tasks left, exit if (!mQueue.hasActiveTasks()) { System.out.println("STQPF: Tasks finished while activity absent, closing"); dismiss(); } } /** * Create the underlying dialog */ @Override public Dialog onCreateDialog(Bundle savedInstanceState) { ProgressDialog dialog = new ProgressDialog(getActivity()); dialog.setCancelable(true); dialog.setCanceledOnTouchOutside(false); int msg = getArguments().getInt("title"); if (msg != 0) dialog.setMessage(getActivity().getString(msg)); final boolean isIndet = getArguments().getBoolean("isIndeterminate"); dialog.setIndeterminate(isIndet); if (isIndet) { dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); } else { dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); } // We can't use "this.requestUpdateProgress()" because getDialog() will still return null if (!isIndet) { dialog.setMax(mMax); dialog.setProgress(mProgress); if (mMessage != null) dialog.setMessage(mMessage); setDialogNumberFormat(dialog); } return dialog; } @Override public void onCancel(DialogInterface dialog) { super.onCancel(dialog); mWasCancelled = true; mQueue.finish(); } @Override public void onResume() { super.onResume(); // If task finished, dismiss. if (!mQueue.hasActiveTasks()) dismiss(); } /** * Dismiss dialog if all tasks finished */ private SimpleTaskQueue.OnTaskFinishListener mTaskFinishListener = new SimpleTaskQueue.OnTaskFinishListener() { @Override public void onTaskFinish(SimpleTask task, Exception e) { // If there are no more tasks, close this dialog if (!mQueue.hasActiveTasks()) { queueAllTasksFinished(); } } }; /** Accessor */ public boolean isCancelled() { return mWasCancelled; } /** Accessor */ public boolean getSuccess() { return mSuccess; } /** Accessor */ public void setSuccess(boolean success) { mSuccess = success; } /** Flag indicating a Refresher has been posted but not run yet */ private boolean mRefresherQueued = false; /** * Runnable object to refresh the dialog */ private Runnable mRefresher = new Runnable() { @Override public void run() { synchronized(mRefresher) { mRefresherQueued = false; updateProgress(); } } }; /** * Refresh the dialog, or post a refresh to the UI thread */ private void requestUpdateProgress() { System.out.println("STQPF: " + mMessage + " (" + mProgress + "/" + mMax + ")"); if (Thread.currentThread() == mHandler.getLooper().getThread()) { updateProgress(); } else { synchronized(mRefresher) { if (!mRefresherQueued) { mHandler.post(mRefresher); mRefresherQueued = true; } } } } /** * Convenience method to step the progress by 1. * * @param message */ public void step(String message) { step(message, 1); } /** * Convenience method to step the progress by the passed delta * * @param message */ public void step(String message, int delta) { synchronized(this) { if (message != null) { mMessage = message; mMessageChanged = true; } mProgress += delta; mProgressChanged = true; } requestUpdateProgress(); } /** * Direct update of message and progress value * * @param message * @param progress */ public void onProgress(String message, int progress) { synchronized(this) { if (message != null) { mMessage = message; mMessageChanged = true; } mProgress = progress; mProgressChanged = true; } requestUpdateProgress(); } /** * Method, run in the UI thread, that updates the various dialog fields. */ private void updateProgress() { ProgressDialog d = (ProgressDialog)getDialog(); if (d != null) { synchronized(this) { if (mMaxChanged) { d.setMax(mMax); mMaxChanged = false; } if (mNumberFormatChanged) { if (Build.VERSION.SDK_INT >= 11) { // Called in a separate function so we can set API attributes setDialogNumberFormat(d); } mNumberFormatChanged = false; } if (mMessageChanged) { d.setMessage(mMessage); mMessageChanged = false; } if (mProgressChanged) { d.setProgress(mProgress); mProgressChanged = false; } } } } /** * Set the number format on API >= 11 * @param d */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) private void setDialogNumberFormat(ProgressDialog d) { if (Build.VERSION.SDK_INT >= 11) { try { d.setProgressNumberFormat(mNumberFormat); } catch (Exception e) { // Ignore and log; Android 3.2 seems not to like NULL format despite docs, // and this is a non-critical feature Logger.logError(e); } } } /** * Set the progress max value * * @param max */ public void setMax(int max) { mMax = max; mMaxChanged = true; requestUpdateProgress(); } /** * Set the progress number format, if the API will support it * * @param max */ public void setNumberFormat(String format) { if (Build.VERSION.SDK_INT >= 11) { synchronized(this) { mNumberFormat = format; mNumberFormatChanged = true; } requestUpdateProgress(); } } /** * Work-around for bug in compatibility library: * * http://code.google.com/p/android/issues/detail?id=17423 */ @Override public void onDestroyView() { if (getDialog() != null && getRetainInstance()) getDialog().setDismissMessage(null); super.onDestroyView(); } }