package edu.vanderbilt.cs282.feisele; import java.net.MalformedURLException; import java.net.URL; import android.app.PendingIntent; import android.app.ProgressDialog; import android.content.Intent; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Messenger; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.text.Editable; import android.util.Log; import android.view.View; import android.widget.EditText; import android.widget.Toast; import edu.vanderbilt.cs282.feisele.DownloadFragment.OnDownloadHandler; import edu.vanderbilt.cs282.feisele.ThreadedDownloadService.DownloadMethod; /** * * An activity which prompts the user for an image to download. <h2>Program * Description</h2> * <p> * This assignment builds upon the various Android concurrency models from third * assignment to give you experience using an Android Service to download bitmap * images from a web server and display them via an Activity. The application * has the same interface as in assignment 3 and works as follows: * <ol> * <li>The Activity provides a menu of buttons and displays a default image</li> * <li>The user is prompted to enter the URL for a new bitmap image.</li> * <li>After entering the desired URL, the user can select one of several * buttons that provide different ways to download the image concurrently.</li> * <li>Upon making the selection, a progress dialog is displayed when * downloading the designated image.</li> * <li>After the URL download has completed it will be displayed in an * ImageView.</li> * <li>The user can reset the image to its default contents by clicking the * "Reset Image" button.</li> * <p> * note: the default image is configured via an XML resource file and the * default image itself is part of the project's assets. * * <h2>Fault Handling</h2> * If there is a problem in the entered URL a toast is displayed indicating the * problem. * * <h2>Details</h2> * <ul> * <li> * It contains a DownloadActivity class that inherits from Activity and uses the * XML layout containing a TextView object that prompts for the URL of the * bitmap file and stores the entered URL in an EditText object. * <li> * It uses four Button objects with the labels "Run Thread Messenger", * "Run Thread PendingIntent", "Run Async Receiver", and "Reset Image" to run * the corresponding hook methods that use the URL provided by the user to start * a ThreadedDownloadService that downloads the designated bitmap file via the * following two Android concurrency and response models * <li> * The DownloadService component must run in a separate process than the * DownloadActivity component. * <li> * The Button objects that initiate the downloading of the bitmap file must be * connected to the corresponding DownloadActivity.run*() methods via the * appropriate android:onClick="..." attributes. * </ul> * * @author "Fred Eisele" <phreed@gmail.com> * */ public class DownloadActivity extends LifecycleLoggingActivity implements OnDownloadHandler { static private final String TAG = "Threaded Download Activity"; private EditText urlEditText = null; private DownloadFragment imageFragment = null; private ProgressDialog progress; /** * The fragment is used to preserve state across various changes. * <p> * In this implementation fragment is not intimately tied to a single * activity instance. When the activity is restarted a new activity instance * is (may be) created. The fragment persists across this restart and it * must be attached to the new activity instance. For this reason the * convenient "<fragment>" xml element cannot be used. A ViewGroup (in this * case a FrameLayout is used. * <p> * * @see http * ://developer.android.com/training/basics/fragments/fragment-ui.html * #AddAtRuntime * * @param savedInstanceState */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.threaded_download); this.urlEditText = (EditText) findViewById(R.id.edit_image_url); final FragmentManager fm = this.getSupportFragmentManager(); final Fragment fobj = fm.findFragmentById(R.id.fragment_container); if (fobj == null) { final FragmentTransaction txn = fm.beginTransaction(); this.imageFragment = new DownloadFragment(); txn.add(R.id.fragment_container, this.imageFragment); txn.commit(); } else { this.imageFragment = (DownloadFragment) fobj; } } final static String PROGRESS_RUNNING_STATE_KEY = "progress_running_state_key"; /** * If the download is ongoing then the progress indicator will need to be * started. Set a flag in the fragment indicating that there is a pending * download as well. */ @Override public void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); final boolean wasProgressRunning = savedInstanceState .getBoolean(PROGRESS_RUNNING_STATE_KEY); if (wasProgressRunning) Log.v(TAG, "progress was running "); final boolean isDownloadStillPending = this.imageFragment.downloadPending .get(); if (wasProgressRunning & isDownloadStillPending) this.startProgress("progress still pending"); } @Override public void onSaveInstanceState(Bundle savedInstanceState) { final boolean progressIsRunning = this.isProgressRunning(); this.imageFragment.downloadPending.set(progressIsRunning); savedInstanceState.putBoolean(PROGRESS_RUNNING_STATE_KEY, this.isProgressRunning()); super.onSaveInstanceState(savedInstanceState); Log.d(TAG, "onSaveInstanceState"); } /** * Shut things down that aren't needed. */ @Override public void onStop() { super.onStop(); this.stopProgress(); this.stopService(this.newServiceIntent()); } /** * Handle the result from the PendingIntent generated by the download * service. Note that this will cause the activity to restart (I think) so * that the fragment is not really needed to persist the state. * * @see(runThreadWithPendingIntent) method. */ @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (resultCode) { case ThreadedDownloadService.RESULT_BITMAP_ID: final String faultMsg = data .getStringExtra(ThreadedDownloadService.RESULT_FAULT); if (faultMsg != null) { // TODO } final String bitmapFilePath = data .getStringExtra(ThreadedDownloadService.RESULT_BITMAP_FILE); this.imageFragment.loadBitmap(bitmapFilePath); this.stopProgress(); } } /** * Initialize and configure the progress dialog. Record the fact that a * download is expected in the fragment. */ private void startProgress(CharSequence msg) { Log.d(TAG, "startProgress"); this.imageFragment.downloadPending.set(true); this.progress = new ProgressDialog(this); this.progress.setTitle(R.string.dialog_progress_title); this.progress.setMessage(msg); this.progress.setProgressStyle(ProgressDialog.STYLE_SPINNER); this.progress.setProgress(0); this.progress.show(); } /** * The progress can be null when the activity is stopped. The progress is * not dismissed unless it is showing. Record the fact that progress in * complete in the fragment. */ private void stopProgress() { Log.d(TAG, "stopProgress"); this.imageFragment.downloadPending.set(false); if (this.isProgressRunning()) this.progress.dismiss(); } /** * Check to see if the progress indicator is active. * * @return */ private boolean isProgressRunning() { if (this.progress == null) return false; if (!this.progress.isShowing()) return false; return true; } /** * Progress dialog can be shut down the */ public void onComplete() { Log.d(TAG, "onComplete"); this.runOnUiThread(new Runnable() { final DownloadActivity master = DownloadActivity.this; public void run() { master.stopProgress(); } }); } /** * Should any of the downloads fail produce a warning indicator on the url * field. As this is most likely called from the background thread * performing the download the field update is forced to the ui thread. */ public void onFault(final CharSequence msg) { this.runOnUiThread(new Runnable() { final CharSequence msg_ = msg; final DownloadActivity master = DownloadActivity.this; public void run() { Log.d(TAG, "onFault"); Toast.makeText(master, msg, Toast.LENGTH_LONG).show(); final Drawable dr = master.getResources().getDrawable( R.drawable.indicator_input_warn); dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight()); master.urlEditText.setError(msg_, dr); master.stopProgress(); } }); } /** * Extract the url from the edit text widget. Check that the string in the * widget is a proper url. If the field is empty then use the value provided * as the hint. If the field is invalid * <ul> * <li>return a null indicating that the action should not be performed.</li> * <li>generate a toast informing the operator of his error</li> * <li>mark the field as having an error</li> * </ul> * * @return */ private Uri getValidUrlFromWidget() { final Editable urlEditable = this.urlEditText.getText(); if (urlEditable.length() < 1) { return getUrlFromHint(this.urlEditText.getHint()); } final String uriStr = urlEditable.toString(); if (uriStr == null) { return getUrlFromHint(this.urlEditText.getHint()); } try { /** a cheap parse, not exactly correct, I'll get this next time */ new URL(uriStr); return Uri.parse(uriStr); } catch (MalformedURLException e) { Log.w(TAG, "bad uri string"); } final CharSequence errorMsg = this.getResources().getText( R.string.error_malformed_url); this.urlEditText.setError(errorMsg); Toast.makeText(this, errorMsg, Toast.LENGTH_LONG).show(); return null; } /** * If the operator has not actually entered a uri then get the one provided * as a hint. * * @param seq * @return */ private Uri getUrlFromHint(final CharSequence seq) { final String uriStr = seq.toString(); if (uriStr == null) { return Uri.parse(this.getResources() .getText(R.string.prompt_image_url).toString()); } return Uri.parse(uriStr); } /** * Load the default image from the assets. * * @param view */ public void resetImage(View view) { Log.d(TAG, "resetImage"); this.imageFragment.resetImage(view); } /** * The service is started with an intent indicating the URI of the file to * download for the bitmap. * <p> * The action of the returning intent is also provided as an extra. * * @param view */ public void runAsyncTaskWithReceiver(View view) { Log.d(TAG, "runAsyncTaskWithReceiver"); final Intent request = newRequestIntent(); if (request != null) { request.putExtra(ThreadedDownloadService.DOWNLOAD_METHOD, DownloadMethod.ASYNC_TASK_BROADCAST.asParcelable()); runDownload(request, R.string.message_progress_async_task_w_receiver); } } /** * The service is started with an intent indicating the URI of the file to * download for the bitmap. * <p> * The action of the returning intent is also provided as an extra. * * @param view */ public void runThreadWithMessenger(View view) { Log.d(TAG, "runThreadWithMessenger"); final Messenger messenger = new Messenger(DownloadFragment.msgHandler); final Intent request = newRequestIntent(); if (request != null) { request.putExtra(ThreadedDownloadService.MESSENGER_KEY, messenger); request.putExtra(ThreadedDownloadService.DOWNLOAD_METHOD, DownloadMethod.THREAD_MESSENGER.asParcelable()); runDownload(request, R.string.message_progress_thread_w_messenger); } } /** * The service is started with an intent indicating the URI of the file to * download for the bitmap. The action of the returning intent is also * provided as an extra. * * @param view */ public void runThreadWithPendingIntent(View view) { Log.d(TAG, "runThreadWithPendingIntent"); final PendingIntent pi = this.createPendingResult( ThreadedDownloadService.RESULT_BITMAP_ID, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT); final Intent request = newRequestIntent(); if (request != null) { request.putExtra(ThreadedDownloadService.PENDING_INTENT_KEY, pi); request.putExtra(ThreadedDownloadService.DOWNLOAD_METHOD, DownloadMethod.THREAD_PENDING_INTENT.asParcelable()); runDownload(request, R.string.message_progress_thread_w_pending_intent); } } /** * A factory method for producing intents which request a download. * * @return the */ private Intent newRequestIntent() { final Intent request = newServiceIntent(); final Uri uri = getValidUrlFromWidget(); if (uri == null) return null; Log.v(TAG, "downloading " + uri); request.setData(uri); return request; } /** * A factory method for producing intents suitable for starting the target * service. * * @return */ private Intent newServiceIntent() { return new Intent(this, ThreadedDownloadService.class); } /** * A method for starting the service and the progress dialog. * * @param request * @param msg */ private void runDownload(Intent request, int msg) { this.startService(request); this.startProgress(this.getResources().getText(msg)); } }