MainActivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
package vandy.mooc; import java.io.File; import android.app.Activity; import android.app.FragmentManager; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.util.Log; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.webkit.DownloadListener; import android.webkit.URLUtil; import android.widget.EditText; import android.widget.Toast; /** * A main Activity that prompts the user for a URL to an image and then uses * Intents and other Activities to download the image and view it. */ public class MainActivity extends LifecycleLoggingActivity implements DownloadAndFilterImageTaskFragment.TaskCallbacks { /** * Debugging tag used by the Android logger. */ private final String TAG = getClass().getSimpleName(); /** * A value that uniquely identifies the request to download an image. */ protected static final int DOWNLOAD_IMAGE_REQUEST = 1; private static final String TAG_DL_IMG_TASK_FRAGMENT = "download_image"; /** * EditText field for entering the desired URL to an image. */ private EditText mUrlEditText; /** * URL for the image that's downloaded by default if the user doesn't * specify otherwise. */ private Uri mDefaultUrl = Uri .parse("http://www.dre.vanderbilt.edu/~schmidt/robot.png"); private DownloadAndFilterImageTaskFragment mTaskFragment; /** * Hook method called when a new instance of Activity is created. One time * initialization code goes here, e.g., UI layout and some class scope * variable initialization. * * @param Bundle * object that contains saved state information. */ @Override protected void onCreate(Bundle savedInstanceState) { // Always call super class for necessary // initialization/implementation. // @@ TODO -- you fill in here. super.onCreate(savedInstanceState); // Set the default layout. // @@ TODO -- you fill in here. setContentView(R.layout.main_activity); // Cache the EditText that holds the urls entered by the user // (if any). // @@ TODO -- you fill in here. mUrlEditText = (EditText) findViewById(R.id.url); FragmentManager fm = getFragmentManager(); mTaskFragment = (DownloadAndFilterImageTaskFragment) fm .findFragmentByTag(TAG_DL_IMG_TASK_FRAGMENT); // If the Fragment is non-null, then it is currently being // retained across a configuration change. if (mTaskFragment == null) { mTaskFragment = new DownloadAndFilterImageTaskFragment(); fm.beginTransaction().add(mTaskFragment, TAG_DL_IMG_TASK_FRAGMENT) .commit(); } } /** * Called by the Android Activity framework when the user clicks the * "Download Image" button. * * @param view * The view. */ public void downloadImage(View view) { try { // Hide the keyboard. hideKeyboard(this, mUrlEditText.getWindowToken()); // execute the async tasks to download and filter the downloaded // image. Uri uri = getUrl(); mTaskFragment.executeTask(uri); } catch (Exception e) { e.printStackTrace(); } } /** * Factory method that returns an Intent for viewing the downloaded image in * the Gallery app. */ private Intent makeGalleryIntent(String pathToImageFile) { // Create an intent that will start the Gallery app to view // the image. // TODO -- you fill in here, replacing "false" with the proper // code. Log.d(TAG, ">>>>>>>> pathToImageFile=" + pathToImageFile); // It doesn't seem to be able to parse the path to Uri. // Will convert from File to Uri. File file = new File(pathToImageFile); Uri imageData = Uri.fromFile(file); // Uri imageData = Uri.parse(pathToImageFile); Intent galleryIntent = new Intent(Intent.ACTION_VIEW); galleryIntent.setDataAndType(imageData, "image/*"); return galleryIntent; } /** * Get the URL to download based on user input. */ protected Uri getUrl() { Uri url = null; // Get the text the user typed in the edit text (if anything). url = Uri.parse(mUrlEditText.getText().toString()); // If the user didn't provide a URL then use the default. String uri = url.toString(); if (uri == null || uri.equals("")) url = mDefaultUrl; // Do a sanity check to ensure the URL is valid, popping up a // toast if the URL is invalid. // @@ TODO -- you fill in here, replacing "true" with the // proper code. if (URLUtil.isValidUrl(url.toString())) return url; else { Toast.makeText(this, "Invalid URL", Toast.LENGTH_SHORT).show(); return null; } } /** * This method is used to hide a keyboard after a user has finished typing * the url. */ public void hideKeyboard(Activity activity, IBinder windowToken) { InputMethodManager mgr = (InputMethodManager) activity .getSystemService(Context.INPUT_METHOD_SERVICE); mgr.hideSoftInputFromWindow(windowToken, 0); } @Override public void onDownloadAndFilterImagePreExecute() { } @Override public void onDownloadAndFilterImageProgressUpdate(int percent) { } @Override public void onDownloadAndFilterImageCancelled() { } @Override public void onDownloadAndFilterImagePostExecute(Uri result) { if (null == result) return; Intent galleryIntent = makeGalleryIntent(result.toString()); startActivity(galleryIntent); } } |
DownloadAndFilterImageTaskFragment.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
package vandy.mooc; import android.app.Activity; import android.app.Fragment; import android.content.Context; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import android.widget.Toast; public class DownloadAndFilterImageTaskFragment extends Fragment { /** * Callback interface through which the fragment will report the task's * progress and results back to the Activity. */ interface TaskCallbacks { void onDownloadAndFilterImagePreExecute(); void onDownloadAndFilterImageProgressUpdate(int percent); void onDownloadAndFilterImageCancelled(); void onDownloadAndFilterImagePostExecute(Uri result); } private static final String TAG = DownloadAndFilterImageTaskFragment.class .getSimpleName(); private TaskCallbacks mCallbacks; private Context mApplicationContext; /** * Hold a reference to the parent Activity so we can report the task's * current progress and results. The Android framework will pass us a * reference to the newly created Activity after each configuration change. */ @Override public void onAttach(Activity activity) { super.onAttach(activity); Log.d(TAG, "DownloadImageTaskFragment onAttach!"); mCallbacks = (TaskCallbacks) activity; mApplicationContext = activity.getApplicationContext(); } /** * Set the callback to null so we don't accidentally leak the Activity * instance. */ @Override public void onDetach() { super.onDetach(); Log.d(TAG, "DownloadImageTaskFragment onDetach!"); mCallbacks = null; mApplicationContext = null; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Retain this fragment across configuration changes. setRetainInstance(true); } /** * execute the DownloadImageAsyncTask then the FilterImageAsyncTask * * @param uri */ public void executeTask(Uri uri) { Log.d(TAG, "executeTask!"); DownloadImageAsyncTask downloadTask = new DownloadImageAsyncTask(); downloadTask.execute(uri); } /** * A Contained Download Image AsyncTask to work with this Fragment * * @author bwoo * */ private class DownloadImageAsyncTask extends AsyncTask<Uri, Void, Uri> { @Override protected Uri doInBackground(Uri... params) { Uri downloadedUri = Utils.downloadImage(mApplicationContext, params[0]); return downloadedUri; } /** * After download is complete, this will trigger the * ImageFilterAsyncTask */ @Override protected void onPostExecute(Uri result) { if (null == result) { Toast toast = Toast.makeText(mApplicationContext, "Unable to download image", Toast.LENGTH_SHORT); toast.show(); return; } ImageFilterAsyncTask imageFilterTask = new ImageFilterAsyncTask(); imageFilterTask.execute(result); } } /** * ImageFilterAsyncTask to convert image to greyscale. */ private class ImageFilterAsyncTask extends AsyncTask<Uri, Void, Uri> { @Override protected Uri doInBackground(Uri... params) { Uri filteredFile = Utils.grayScaleFilter(mApplicationContext, params[0]); return filteredFile; } /** * Return result to the Activity. */ @Override protected void onPostExecute(Uri result) { if (mCallbacks != null) mCallbacks.onDownloadAndFilterImagePostExecute(result); } } } |
Utils.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 |
package vandy.mooc; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.net.URL; import java.util.Locale; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.net.Uri; import android.os.Environment; import android.provider.MediaStore; import android.provider.MediaStore.Images; import android.util.Base64; import android.util.Log; import android.widget.Toast; /** * This helper class encapsulates several static methods that are used * to download image files. */ public class Utils { /** * Used for debugging. */ private final static String TAG = "Utils"; /** * If you have access to a stable Internet connection for testing * purposes, feel free to change this variable to false so it * actually downloads the image from a remote server. */ static final boolean DOWNLOAD_OFFLINE = false; /** * The resource that we write to the file system in offline * mode. */ static final int OFFLINE_TEST_IMAGE = R.raw.dougs; /** * The file name that we should use to store the image in offline * mode. */ static final String OFFLINE_FILENAME = "dougs.jpg"; /** * Apply a grayscale filter to the @a imageEntity and return it. */ public static Uri grayScaleFilter(Context context, Uri pathToImageFile) { Bitmap grayScaleImage = null; try (InputStream inputStream = new FileInputStream(pathToImageFile.toString())) { Bitmap originalImage = BitmapFactory.decodeStream(inputStream); // Bail out of we get an invalid bitmap. if (originalImage == null) return null; grayScaleImage = originalImage.copy(originalImage.getConfig(), true); } catch (Exception e) { e.printStackTrace(); return null; } boolean hasTransparent = grayScaleImage.hasAlpha(); int width = grayScaleImage.getWidth(); int height = grayScaleImage.getHeight(); // A common pixel-by-pixel grayscale conversion algorithm // using values obtained from en.wikipedia.org/wiki/Grayscale. for (int i = 0; i < height; ++i) { for (int j = 0; j < width; ++j) { // Check if the pixel is transparent in the original // by checking if the alpha is 0 if (hasTransparent && ((grayScaleImage.getPixel(j, i) & 0xff000000) >> 24) == 0) { continue; } // Convert the pixel to grayscale. int pixel = grayScaleImage.getPixel(j, i); int grayScale = (int) (Color.red(pixel) * .299 + Color.green(pixel) * .587 + Color.blue(pixel) * .114); grayScaleImage.setPixel(j, i, Color.rgb(grayScale, grayScale, grayScale) ); } } return Utils.createDirectoryAndSaveFile (context, grayScaleImage, // Name of the image file that we're filtering. pathToImageFile.toString()); } /** * Download the image located at the provided Internet url using * the URL class, store it on the android file system using a * FileOutputStream, and return the path to the image file on * disk. * * @param context the context in which to write the file. * @param url the web url. * * @return the absolute path to the downloaded image file on the file system. */ public static Uri downloadImage(Context context, Uri url) { try { if (!isExternalStorageWritable()) { Log.d(TAG, "external storage is not writable"); return null; } // Input stream. InputStream inputStream; // Filename that we're downloading (or opening). String filename; // If we're offline, open the image in our resources. if (DOWNLOAD_OFFLINE) { // Get a stream from the image resource. inputStream = context.getResources().openRawResource(OFFLINE_TEST_IMAGE); filename = OFFLINE_FILENAME; // Otherwise, download the file requested by the user. } else { // Download the contents at the URL, which should // reference an image. inputStream = (InputStream) new URL(url.toString()).getContent(); filename = url.toString(); } // Decode the InputStream into a Bitmap image. Bitmap bitmap = BitmapFactory.decodeStream(inputStream); // Bail out of we get an invalid bitmap. if (bitmap == null) return null; else // Create an output file and save the image into it. return Utils.createDirectoryAndSaveFile(context, bitmap, filename); } catch (Exception e) { Log.e(TAG, "Exception while downloading. Returning null."); Log.e(TAG, e.toString()); e.printStackTrace(); return null; } } /** * Decode an InputStream into a Bitmap and store it in a file on * the device. * * @param context the context in which to write the file. * @param image the image to save * @param fileName name of the file. * * @return the absolute path to the downloaded image file on the file system. */ private static Uri createDirectoryAndSaveFile(Context context, Bitmap image, String fileName) { File directory = new File(Environment.getExternalStoragePublicDirectory (Environment.DIRECTORY_DCIM) + "/ImageDir"); if (!directory.exists()) { File newDirectory = new File(directory.getAbsolutePath()); newDirectory.mkdirs(); } File file = new File(directory, getTemporaryFilename(fileName)); if (file.exists()) file.delete(); try (FileOutputStream outputStream = new FileOutputStream(file)) { image.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); outputStream.flush(); } catch (Exception e) { e.printStackTrace(); // Indicate a failure. return null; } // Get the absolute path of the image. String absolutePathToImage = file.getAbsolutePath(); // Provide metadata so the downloaded image is viewable in the // Gallery. ContentValues values = new ContentValues(); values.put(Images.Media.TITLE, fileName); values.put(Images.Media.DESCRIPTION, fileName); values.put(Images.Media.DATE_TAKEN, System.currentTimeMillis ()); values.put(Images.ImageColumns.BUCKET_DISPLAY_NAME, file.getName().toLowerCase(Locale.US)); values.put("_data", absolutePathToImage); ContentResolver cr = context.getContentResolver(); // Store the metadata for the image into the Gallery. cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); Log.d(TAG, "absolute path to image file is " + absolutePathToImage); return Uri.parse(absolutePathToImage); } /** * This method checks if we can write image to external storage * * @return true if an image can be written, and false otherwise */ private static boolean isExternalStorageWritable() { return Environment.MEDIA_MOUNTED.equals (Environment.getExternalStorageState()); } /** * Create a temporary filename to store the result of a download. * * @param url Name of the URL. * @return String containing the temporary filename. */ static private String getTemporaryFilename(final String url) { // This is what you'd normally call to get a unique temporary // filename, but for testing purposes we always name the file // the same to avoid filling up student phones with numerous // files! // // return Base64.encodeToString(url.getBytes(), // Base64.NO_WRAP) // + System.currentTimeMillis()); return Base64.encodeToString(url.getBytes(), Base64.NO_WRAP); } /** * Show a toast message. */ public static void showToast(Context context, String message) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); } } |
LifecycleLoggingActivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
package vandy.mooc; import android.app.Activity; import android.os.Bundle; import android.util.Log; /** * This abstract class extends the Activity class and overrides lifecycle * callbacks for logging various lifecycle events. */ public abstract class LifecycleLoggingActivity extends Activity { /** * Debugging tag used by the Android logger. */ private final String TAG = getClass().getSimpleName(); /** * Hook method called when a new instance of Activity is created. One time * initialization code should go here e.g. UI layout, some class scope * variable initialization. if finish() is called from onCreate no other * lifecycle callbacks are called except for onDestroy(). * * @param Bundle * object that contains saved state information. */ @Override protected void onCreate(Bundle savedInstanceState) { // Always call super class for necessary // initialization/implementation. super.onCreate(savedInstanceState); if (savedInstanceState != null) { // The activity is being re-created. Use the // savedInstanceState bundle for initializations either // during onCreate or onRestoreInstanceState(). Log.d(TAG, "onCreate(): activity re-created from savedInstanceState"); } else { // Activity is being created anew. No prior saved // instance state information available in Bundle object. Log.d(TAG, "onCreate(): activity created anew"); } } /** * Hook method called after onCreate() or after onRestart() (when the * activity is being restarted from stopped state). Should re-acquire * resources relinquished when activity was stopped (onStop()) or acquire * those resources for the first time after onCreate(). */ @Override protected void onStart() { // Always call super class for necessary // initialization/implementation. // TODO - you fill in here. super.onStart(); Log.d(TAG, "onStart(): activity starting"); } /** * Hook method called after onRestoreStateInstance(Bundle) only if there is * a prior saved instance state in Bundle object. onResume() is called * immediately after onStart(). onResume() is called when user resumes * activity from paused state (onPause()) User can begin interacting with * activity. Place to start animations, acquire exclusive resources, such as * the camera. */ @Override protected void onResume() { // Always call super class for necessary // initialization/implementation and then log which lifecycle // hook method is being called. // TODO - you fill in here. super.onResume(); Log.d(TAG, "onResume(): activity resuming"); } /** * Hook method called when an Activity loses focus but is still visible in * background. May be followed by onStop() or onResume(). Delegate more CPU * intensive operation to onStop for seamless transition to next activity. * Save persistent state (onSaveInstanceState()) in case app is killed. * Often used to release exclusive resources. */ @Override protected void onPause() { // Always call super class for necessary // initialization/implementation and then log which lifecycle // hook method is being called. // TODO - you fill in here. super.onPause(); Log.d(TAG, "onPause(): activity pausing"); } /** * Called when Activity is no longer visible. Release resources that may * cause memory leak. Save instance state (onSaveInstanceState()) in case * activity is killed. */ @Override protected void onStop() { // Always call super class for necessary // initialization/implementation and then log which lifecycle // hook method is being called. // TODO - you fill in here. super.onStop(); Log.d(TAG, "onStop(): activity stopping"); } /** * Hook method called when user restarts a stopped activity. Is followed by * a call to onStart() and onResume(). */ @Override protected void onRestart() { // Always call super class for necessary // initialization/implementation and then log which lifecycle // hook method is being called. // TODO - you fill in here. super.onRestart(); Log.d(TAG, "onRestart(): activity restarting"); } /** * Hook method that gives a final chance to release resources and stop * spawned threads. onDestroy() may not always be called-when system kills * hosting process */ @Override protected void onDestroy() { // Always call super class for necessary // initialization/implementation and then log which lifecycle // hook method is being called. // TODO - you fill in here. super.onDestroy(); Log.d(TAG, "onDestroy(): activity is being destroyed"); } } |
main_activity.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_height="match_parent" android:layout_width="match_parent"> <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="14dp" android:text="@string/defaultURL" /> <EditText android:id="@+id/url" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:inputType="text|textMultiLine|textUri" android:hint="@string/enter_url" android:ems="10" > </EditText> <Button android:id="@+id/button1" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="18dp" android:onClick="downloadImage" android:text="@string/download_image" /> </LinearLayout> |
AndroidManifest.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="vandy.mooc" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="22" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <application android:label="@string/app_name" > <activity android:name=".MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".DownloadImageActivity" android:label="@string/download_image_activity" > <intent-filter> <action android:name="android.intent.action.WEB_SEARCH" /> <category android:name="android.intent.category.DEFAULT" /> <data android:scheme="http" /> <data android:scheme="https" /> </intent-filter> </activity> </application> </manifest> |