Updating code sample for "displaying bitmaps efficiently" training class.
Changes:
 -Use updated versions of ImageWorker & ImageCache from I/O 2012 app
 -Use copied DiskLruCache from system (rather than custom)
 -Use copied AsyncTask from system (to keep behavior consistent)
 -Ensure no strict mode violations or lint errors
 -Other misc bug fixes
 -Move single-use static methods in Utils to corresponding class

Change-Id: If21e045db1e1a80391169f3c9c9258d48345ab6b
diff --git a/samples/training/bitmapfun/AndroidManifest.xml b/samples/training/bitmapfun/AndroidManifest.xml
index 4a6f0f5..cabb442 100644
--- a/samples/training/bitmapfun/AndroidManifest.xml
+++ b/samples/training/bitmapfun/AndroidManifest.xml
@@ -22,7 +22,7 @@
 
     <uses-sdk
         android:minSdkVersion="7"
-        android:targetSdkVersion="15" />
+        android:targetSdkVersion="16" />
 
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
@@ -36,7 +36,10 @@
         <activity
             android:name=".ui.ImageDetailActivity"
             android:label="@string/app_name"
+            android:parentActivityName=".ui.ImageGridActivity"
             android:theme="@style/AppTheme.FullScreen" >
+            <meta-data android:name="android.support.PARENT_ACTIVITY"
+                       android:value=".ui.ImageGridActivity" />
         </activity>
         <activity
             android:name=".ui.ImageGridActivity"
diff --git a/samples/training/bitmapfun/libs/android-support-v4.jar b/samples/training/bitmapfun/libs/android-support-v4.jar
index 99e063b..feaf44f 100644
--- a/samples/training/bitmapfun/libs/android-support-v4.jar
+++ b/samples/training/bitmapfun/libs/android-support-v4.jar
Binary files differ
diff --git a/samples/training/bitmapfun/project.properties b/samples/training/bitmapfun/project.properties
index 0840b4a..9b84a6b 100644
--- a/samples/training/bitmapfun/project.properties
+++ b/samples/training/bitmapfun/project.properties
@@ -11,4 +11,4 @@
 #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
 
 # Project target.
-target=android-15
+target=android-16
diff --git a/samples/training/bitmapfun/res/drawable/photogrid_list_selector.xml b/samples/training/bitmapfun/res/drawable/photogrid_list_selector.xml
new file mode 100644
index 0000000..19d8670
--- /dev/null
+++ b/samples/training/bitmapfun/res/drawable/photogrid_list_selector.xml
@@ -0,0 +1,31 @@
+<!--
+  Copyright (C) 2012 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:state_pressed="true">
+        <shape>
+            <solid android:color="@color/grid_state_pressed" />
+        </shape>
+    </item>
+    <item android:state_focused="true">
+        <shape>
+            <solid android:color="@color/grid_state_focused" />
+        </shape>
+    </item>
+    <item android:drawable="@android:color/transparent" />
+
+</selector>
diff --git a/samples/training/bitmapfun/res/layout/image_detail_fragment.xml b/samples/training/bitmapfun/res/layout/image_detail_fragment.xml
index 2d9dfcb..ff616da 100644
--- a/samples/training/bitmapfun/res/layout/image_detail_fragment.xml
+++ b/samples/training/bitmapfun/res/layout/image_detail_fragment.xml
@@ -16,12 +16,10 @@
 -->
 
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/frameLayout"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent" >
 
     <ProgressBar
-        android:id="@+id/progressBar"
         style="?android:attr/progressBarStyleLarge"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
diff --git a/samples/training/bitmapfun/res/menu/main_menu.xml b/samples/training/bitmapfun/res/menu/main_menu.xml
index 0e727d0..35dad09 100644
--- a/samples/training/bitmapfun/res/menu/main_menu.xml
+++ b/samples/training/bitmapfun/res/menu/main_menu.xml
@@ -1,4 +1,19 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2012 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
 <menu xmlns:android="http://schemas.android.com/apk/res/android" >
 
     <item
diff --git a/samples/training/bitmapfun/res/values-large/dimens.xml b/samples/training/bitmapfun/res/values-large/dimens.xml
index 503f267..c681876 100644
--- a/samples/training/bitmapfun/res/values-large/dimens.xml
+++ b/samples/training/bitmapfun/res/values-large/dimens.xml
@@ -17,7 +17,7 @@
 
 <resources>
 
-    <dimen name="image_thumbnail_size">150dp</dimen>
-    <dimen name="image_thumbnail_spacing">1dp</dimen>
+    <dimen name="image_thumbnail_size">148dp</dimen>
+    <dimen name="image_thumbnail_spacing">2dp</dimen>
 
 </resources>
\ No newline at end of file
diff --git a/samples/training/bitmapfun/res/values/colors.xml b/samples/training/bitmapfun/res/values/colors.xml
new file mode 100644
index 0000000..7e4a4fe
--- /dev/null
+++ b/samples/training/bitmapfun/res/values/colors.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2012 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<resources>
+
+    <color name="grid_state_pressed">#BB7dbcd3</color>
+    <color name="grid_state_focused">#777dbcd3</color>
+
+</resources>
diff --git a/samples/training/bitmapfun/res/values/strings.xml b/samples/training/bitmapfun/res/values/strings.xml
index b77f768..8108c23 100644
--- a/samples/training/bitmapfun/res/values/strings.xml
+++ b/samples/training/bitmapfun/res/values/strings.xml
@@ -19,12 +19,13 @@
 
     <string name="app_name">BitmapFun</string>
     <string name="app_description">This is a sample application for the Android Training class
-        "Displaying Bitmaps Efficiently"
+        &quot;Displaying Bitmaps Efficiently&quot;
         (http://developer.android.com/training/displaying-bitmaps/display-bitmap.html). It is not
         designed to be a full reference application but to demonstrate the concepts discussed in
         training course.</string>
     <string name="clear_cache_menu">Clear Caches</string>
-    <string name="clear_cache_complete">Caches have been cleared</string>
+    <string name="clear_cache_complete_toast">Caches have been cleared</string>
     <string name="imageview_description">Image Thumbnail</string>
+    <string name="no_network_connection_toast">No network connection found</string>
 
 </resources>
\ No newline at end of file
diff --git a/samples/training/bitmapfun/res/values/styles.xml b/samples/training/bitmapfun/res/values/styles.xml
index 3e72fd3..0f1a018 100644
--- a/samples/training/bitmapfun/res/values/styles.xml
+++ b/samples/training/bitmapfun/res/values/styles.xml
@@ -22,8 +22,8 @@
     <style name="AppTheme.FullScreen" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen" />
 
     <style name="PhotoGridLayout">
-        <item name="android:drawSelectorOnTop">false</item>
-        <item name="android:listSelector">@null</item>
+        <item name="android:drawSelectorOnTop">true</item>
+        <item name="android:listSelector">@drawable/photogrid_list_selector</item>
     </style>
 
 </resources>
\ No newline at end of file
diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/provider/Images.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/provider/Images.java
index 5c9ef5c..809d73b 100644
--- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/provider/Images.java
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/provider/Images.java
@@ -16,8 +16,6 @@
 
 package com.example.android.bitmapfun.provider;
 
-import com.example.android.bitmapfun.util.ImageWorker.ImageWorkerAdapter;
-
 /**
  * Some simple test data to use for this sample app.
  */
@@ -28,45 +26,45 @@
      * used to fetch the URLs.
      */
     public final static String[] imageUrls = new String[] {
-        "https://lh6.googleusercontent.com/-jZgveEqb6pg/T3R4kXScycI/AAAAAAAAAE0/xQ7CvpfXDzc/s1024/sample_image_01.jpg",
-        "https://lh4.googleusercontent.com/-K2FMuOozxU0/T3R4lRAiBTI/AAAAAAAAAE8/a3Eh9JvnnzI/s1024/sample_image_02.jpg",
-        "https://lh5.googleusercontent.com/-SCS5C646rxM/T3R4l7QB6xI/AAAAAAAAAFE/xLcuVv3CUyA/s1024/sample_image_03.jpg",
-        "https://lh6.googleusercontent.com/-f0NJR6-_Thg/T3R4mNex2wI/AAAAAAAAAFI/45oug4VE8MI/s1024/sample_image_04.jpg",
-        "https://lh3.googleusercontent.com/-n-xcJmiI0pg/T3R4mkSchHI/AAAAAAAAAFU/EoiNNb7kk3A/s1024/sample_image_05.jpg",
-        "https://lh3.googleusercontent.com/-X43vAudm7f4/T3R4nGSChJI/AAAAAAAAAFk/3bna6D-2EE8/s1024/sample_image_06.jpg",
-        "https://lh5.googleusercontent.com/-MpZneqIyjXU/T3R4nuGO1aI/AAAAAAAAAFg/r09OPjLx1ZY/s1024/sample_image_07.jpg",
-        "https://lh6.googleusercontent.com/-ql3YNfdClJo/T3XvW9apmFI/AAAAAAAAAL4/_6HFDzbahc4/s1024/sample_image_08.jpg",
-        "https://lh5.googleusercontent.com/-Pxa7eqF4cyc/T3R4oasvPEI/AAAAAAAAAF0/-uYDH92h8LA/s1024/sample_image_09.jpg",
-        "https://lh4.googleusercontent.com/-Li-rjhFEuaI/T3R4o-VUl4I/AAAAAAAAAF8/5E5XdMnP1oE/s1024/sample_image_10.jpg",
-        "https://lh5.googleusercontent.com/-_HU4fImgFhA/T3R4pPVIwWI/AAAAAAAAAGA/0RfK_Vkgth4/s1024/sample_image_11.jpg",
-        "https://lh6.googleusercontent.com/-0gnNrVjwa0Y/T3R4peGYJwI/AAAAAAAAAGU/uX_9wvRPM9I/s1024/sample_image_12.jpg",
-        "https://lh3.googleusercontent.com/-HBxuzALS_Zs/T3R4qERykaI/AAAAAAAAAGQ/_qQ16FaZ1q0/s1024/sample_image_13.jpg",
-        "https://lh4.googleusercontent.com/-cKojDrARNjQ/T3R4qfWSGPI/AAAAAAAAAGY/MR5dnbNaPyY/s1024/sample_image_14.jpg",
-        "https://lh3.googleusercontent.com/-WujkdYfcyZ8/T3R4qrIMGUI/AAAAAAAAAGk/277LIdgvnjg/s1024/sample_image_15.jpg",
-        "https://lh6.googleusercontent.com/-FMHR7Vy3PgI/T3R4rOXlEKI/AAAAAAAAAGs/VeXrDNDBkaw/s1024/sample_image_16.jpg",
-        "https://lh4.googleusercontent.com/-mrR0AJyNTH0/T3R4rZs6CuI/AAAAAAAAAG0/UE1wQqCOqLA/s1024/sample_image_17.jpg",
-        "https://lh6.googleusercontent.com/-z77w0eh3cow/T3R4rnLn05I/AAAAAAAAAG4/BaerfWoNucU/s1024/sample_image_18.jpg",
-        "https://lh5.googleusercontent.com/-aWVwh1OU5Bk/T3R4sAWw0yI/AAAAAAAAAHE/4_KAvJttFwA/s1024/sample_image_19.jpg",
-        "https://lh6.googleusercontent.com/-q-js52DMnWQ/T3R4tZhY2sI/AAAAAAAAAHM/A8kjp2Ivdqg/s1024/sample_image_20.jpg",
-        "https://lh5.googleusercontent.com/-_jIzvvzXKn4/T3R4t7xpdVI/AAAAAAAAAHU/7QC6eZ10jgs/s1024/sample_image_21.jpg",
-        "https://lh3.googleusercontent.com/-lnGi4IMLpwU/T3R4uCMa7vI/AAAAAAAAAHc/1zgzzz6qTpk/s1024/sample_image_22.jpg",
-        "https://lh5.googleusercontent.com/-fFCzKjFPsPc/T3R4u0SZPFI/AAAAAAAAAHk/sbgjzrktOK0/s1024/sample_image_23.jpg",
-        "https://lh4.googleusercontent.com/-8TqoW5gBE_Y/T3R4vBS3NPI/AAAAAAAAAHs/EZYvpNsaNXk/s1024/sample_image_24.jpg",
-        "https://lh6.googleusercontent.com/-gc4eQ3ySdzs/T3R4vafoA7I/AAAAAAAAAH4/yKii5P6tqDE/s1024/sample_image_25.jpg",
-        "https://lh5.googleusercontent.com/--NYOPCylU7Q/T3R4vjAiWkI/AAAAAAAAAH8/IPNx5q3ptRA/s1024/sample_image_26.jpg",
-        "https://lh6.googleusercontent.com/-9IJM8so4vCI/T3R4vwJO2yI/AAAAAAAAAIE/ljlr-cwuqZM/s1024/sample_image_27.jpg",
-        "https://lh4.googleusercontent.com/-KW6QwOHfhBs/T3R4w0RsQiI/AAAAAAAAAIM/uEFLVgHPFCk/s1024/sample_image_28.jpg",
-        "https://lh4.googleusercontent.com/-z2557Ec1ctY/T3R4x3QA2hI/AAAAAAAAAIk/9-GzPL1lTWE/s1024/sample_image_29.jpg",
-        "https://lh5.googleusercontent.com/-LaKXAn4Kr1c/T3R4yc5b4lI/AAAAAAAAAIY/fMgcOVQfmD0/s1024/sample_image_30.jpg",
-        "https://lh4.googleusercontent.com/-F9LRToJoQdo/T3R4yrLtyQI/AAAAAAAAAIg/ri9uUCWuRmo/s1024/sample_image_31.jpg",
-        "https://lh4.googleusercontent.com/-6X-xBwP-QpI/T3R4zGVboII/AAAAAAAAAIs/zYH4PjjngY0/s1024/sample_image_32.jpg",
-        "https://lh5.googleusercontent.com/-VdLRjbW4LAs/T3R4zXu3gUI/AAAAAAAAAIw/9aFp9t7mCPg/s1024/sample_image_33.jpg",
-        "https://lh6.googleusercontent.com/-gL6R17_fDJU/T3R4zpIXGjI/AAAAAAAAAI8/Q2Vjx-L9X20/s1024/sample_image_34.jpg",
-        "https://lh3.googleusercontent.com/-1fGH4YJXEzo/T3R40Y1B7KI/AAAAAAAAAJE/MnTsa77g-nk/s1024/sample_image_35.jpg",
-        "https://lh4.googleusercontent.com/-Ql0jHSrea-A/T3R403mUfFI/AAAAAAAAAJM/qzI4SkcH9tY/s1024/sample_image_36.jpg",
-        "https://lh5.googleusercontent.com/-BL5FIBR_tzI/T3R41DA0AKI/AAAAAAAAAJk/GZfeeb-SLM0/s1024/sample_image_37.jpg",
-        "https://lh4.googleusercontent.com/-wF2Vc9YDutw/T3R41fR2BCI/AAAAAAAAAJc/JdU1sHdMRAk/s1024/sample_image_38.jpg",
-        "https://lh6.googleusercontent.com/-ZWHiPehwjTI/T3R41zuaKCI/AAAAAAAAAJg/hR3QJ1v3REg/s1024/sample_image_39.jpg",
+        "http://lh6.googleusercontent.com/-jZgveEqb6pg/T3R4kXScycI/AAAAAAAAAE0/xQ7CvpfXDzc/s1024/sample_image_01.jpg",
+        "http://lh4.googleusercontent.com/-K2FMuOozxU0/T3R4lRAiBTI/AAAAAAAAAE8/a3Eh9JvnnzI/s1024/sample_image_02.jpg",
+        "http://lh5.googleusercontent.com/-SCS5C646rxM/T3R4l7QB6xI/AAAAAAAAAFE/xLcuVv3CUyA/s1024/sample_image_03.jpg",
+        "http://lh6.googleusercontent.com/-f0NJR6-_Thg/T3R4mNex2wI/AAAAAAAAAFI/45oug4VE8MI/s1024/sample_image_04.jpg",
+        "http://lh3.googleusercontent.com/-n-xcJmiI0pg/T3R4mkSchHI/AAAAAAAAAFU/EoiNNb7kk3A/s1024/sample_image_05.jpg",
+        "http://lh3.googleusercontent.com/-X43vAudm7f4/T3R4nGSChJI/AAAAAAAAAFk/3bna6D-2EE8/s1024/sample_image_06.jpg",
+        "http://lh5.googleusercontent.com/-MpZneqIyjXU/T3R4nuGO1aI/AAAAAAAAAFg/r09OPjLx1ZY/s1024/sample_image_07.jpg",
+        "http://lh6.googleusercontent.com/-ql3YNfdClJo/T3XvW9apmFI/AAAAAAAAAL4/_6HFDzbahc4/s1024/sample_image_08.jpg",
+        "http://lh5.googleusercontent.com/-Pxa7eqF4cyc/T3R4oasvPEI/AAAAAAAAAF0/-uYDH92h8LA/s1024/sample_image_09.jpg",
+        "http://lh4.googleusercontent.com/-Li-rjhFEuaI/T3R4o-VUl4I/AAAAAAAAAF8/5E5XdMnP1oE/s1024/sample_image_10.jpg",
+        "http://lh5.googleusercontent.com/-_HU4fImgFhA/T3R4pPVIwWI/AAAAAAAAAGA/0RfK_Vkgth4/s1024/sample_image_11.jpg",
+        "http://lh6.googleusercontent.com/-0gnNrVjwa0Y/T3R4peGYJwI/AAAAAAAAAGU/uX_9wvRPM9I/s1024/sample_image_12.jpg",
+        "http://lh3.googleusercontent.com/-HBxuzALS_Zs/T3R4qERykaI/AAAAAAAAAGQ/_qQ16FaZ1q0/s1024/sample_image_13.jpg",
+        "http://lh4.googleusercontent.com/-cKojDrARNjQ/T3R4qfWSGPI/AAAAAAAAAGY/MR5dnbNaPyY/s1024/sample_image_14.jpg",
+        "http://lh3.googleusercontent.com/-WujkdYfcyZ8/T3R4qrIMGUI/AAAAAAAAAGk/277LIdgvnjg/s1024/sample_image_15.jpg",
+        "http://lh6.googleusercontent.com/-FMHR7Vy3PgI/T3R4rOXlEKI/AAAAAAAAAGs/VeXrDNDBkaw/s1024/sample_image_16.jpg",
+        "http://lh4.googleusercontent.com/-mrR0AJyNTH0/T3R4rZs6CuI/AAAAAAAAAG0/UE1wQqCOqLA/s1024/sample_image_17.jpg",
+        "http://lh6.googleusercontent.com/-z77w0eh3cow/T3R4rnLn05I/AAAAAAAAAG4/BaerfWoNucU/s1024/sample_image_18.jpg",
+        "http://lh5.googleusercontent.com/-aWVwh1OU5Bk/T3R4sAWw0yI/AAAAAAAAAHE/4_KAvJttFwA/s1024/sample_image_19.jpg",
+        "http://lh6.googleusercontent.com/-q-js52DMnWQ/T3R4tZhY2sI/AAAAAAAAAHM/A8kjp2Ivdqg/s1024/sample_image_20.jpg",
+        "http://lh5.googleusercontent.com/-_jIzvvzXKn4/T3R4t7xpdVI/AAAAAAAAAHU/7QC6eZ10jgs/s1024/sample_image_21.jpg",
+        "http://lh3.googleusercontent.com/-lnGi4IMLpwU/T3R4uCMa7vI/AAAAAAAAAHc/1zgzzz6qTpk/s1024/sample_image_22.jpg",
+        "http://lh5.googleusercontent.com/-fFCzKjFPsPc/T3R4u0SZPFI/AAAAAAAAAHk/sbgjzrktOK0/s1024/sample_image_23.jpg",
+        "http://lh4.googleusercontent.com/-8TqoW5gBE_Y/T3R4vBS3NPI/AAAAAAAAAHs/EZYvpNsaNXk/s1024/sample_image_24.jpg",
+        "http://lh6.googleusercontent.com/-gc4eQ3ySdzs/T3R4vafoA7I/AAAAAAAAAH4/yKii5P6tqDE/s1024/sample_image_25.jpg",
+        "http://lh5.googleusercontent.com/--NYOPCylU7Q/T3R4vjAiWkI/AAAAAAAAAH8/IPNx5q3ptRA/s1024/sample_image_26.jpg",
+        "http://lh6.googleusercontent.com/-9IJM8so4vCI/T3R4vwJO2yI/AAAAAAAAAIE/ljlr-cwuqZM/s1024/sample_image_27.jpg",
+        "http://lh4.googleusercontent.com/-KW6QwOHfhBs/T3R4w0RsQiI/AAAAAAAAAIM/uEFLVgHPFCk/s1024/sample_image_28.jpg",
+        "http://lh4.googleusercontent.com/-z2557Ec1ctY/T3R4x3QA2hI/AAAAAAAAAIk/9-GzPL1lTWE/s1024/sample_image_29.jpg",
+        "http://lh5.googleusercontent.com/-LaKXAn4Kr1c/T3R4yc5b4lI/AAAAAAAAAIY/fMgcOVQfmD0/s1024/sample_image_30.jpg",
+        "http://lh4.googleusercontent.com/-F9LRToJoQdo/T3R4yrLtyQI/AAAAAAAAAIg/ri9uUCWuRmo/s1024/sample_image_31.jpg",
+        "http://lh4.googleusercontent.com/-6X-xBwP-QpI/T3R4zGVboII/AAAAAAAAAIs/zYH4PjjngY0/s1024/sample_image_32.jpg",
+        "http://lh5.googleusercontent.com/-VdLRjbW4LAs/T3R4zXu3gUI/AAAAAAAAAIw/9aFp9t7mCPg/s1024/sample_image_33.jpg",
+        "http://lh6.googleusercontent.com/-gL6R17_fDJU/T3R4zpIXGjI/AAAAAAAAAI8/Q2Vjx-L9X20/s1024/sample_image_34.jpg",
+        "http://lh3.googleusercontent.com/-1fGH4YJXEzo/T3R40Y1B7KI/AAAAAAAAAJE/MnTsa77g-nk/s1024/sample_image_35.jpg",
+        "http://lh4.googleusercontent.com/-Ql0jHSrea-A/T3R403mUfFI/AAAAAAAAAJM/qzI4SkcH9tY/s1024/sample_image_36.jpg",
+        "http://lh5.googleusercontent.com/-BL5FIBR_tzI/T3R41DA0AKI/AAAAAAAAAJk/GZfeeb-SLM0/s1024/sample_image_37.jpg",
+        "http://lh4.googleusercontent.com/-wF2Vc9YDutw/T3R41fR2BCI/AAAAAAAAAJc/JdU1sHdMRAk/s1024/sample_image_38.jpg",
+        "http://lh6.googleusercontent.com/-ZWHiPehwjTI/T3R41zuaKCI/AAAAAAAAAJg/hR3QJ1v3REg/s1024/sample_image_39.jpg",
     };
 
     /**
@@ -74,74 +72,44 @@
      * should be used to fetch the URLs.
      */
     public final static String[] imageThumbUrls = new String[] {
-        "https://lh6.googleusercontent.com/-jZgveEqb6pg/T3R4kXScycI/AAAAAAAAAE0/xQ7CvpfXDzc/s160-c/sample_image_01.jpg",
-        "https://lh4.googleusercontent.com/-K2FMuOozxU0/T3R4lRAiBTI/AAAAAAAAAE8/a3Eh9JvnnzI/s160-c/sample_image_02.jpg",
-        "https://lh5.googleusercontent.com/-SCS5C646rxM/T3R4l7QB6xI/AAAAAAAAAFE/xLcuVv3CUyA/s160-c/sample_image_03.jpg",
-        "https://lh6.googleusercontent.com/-f0NJR6-_Thg/T3R4mNex2wI/AAAAAAAAAFI/45oug4VE8MI/s160-c/sample_image_04.jpg",
-        "https://lh3.googleusercontent.com/-n-xcJmiI0pg/T3R4mkSchHI/AAAAAAAAAFU/EoiNNb7kk3A/s160-c/sample_image_05.jpg",
-        "https://lh3.googleusercontent.com/-X43vAudm7f4/T3R4nGSChJI/AAAAAAAAAFk/3bna6D-2EE8/s160-c/sample_image_06.jpg",
-        "https://lh5.googleusercontent.com/-MpZneqIyjXU/T3R4nuGO1aI/AAAAAAAAAFg/r09OPjLx1ZY/s160-c/sample_image_07.jpg",
-        "https://lh6.googleusercontent.com/-ql3YNfdClJo/T3XvW9apmFI/AAAAAAAAAL4/_6HFDzbahc4/s160-c/sample_image_08.jpg",
-        "https://lh5.googleusercontent.com/-Pxa7eqF4cyc/T3R4oasvPEI/AAAAAAAAAF0/-uYDH92h8LA/s160-c/sample_image_09.jpg",
-        "https://lh4.googleusercontent.com/-Li-rjhFEuaI/T3R4o-VUl4I/AAAAAAAAAF8/5E5XdMnP1oE/s160-c/sample_image_10.jpg",
-        "https://lh5.googleusercontent.com/-_HU4fImgFhA/T3R4pPVIwWI/AAAAAAAAAGA/0RfK_Vkgth4/s160-c/sample_image_11.jpg",
-        "https://lh6.googleusercontent.com/-0gnNrVjwa0Y/T3R4peGYJwI/AAAAAAAAAGU/uX_9wvRPM9I/s160-c/sample_image_12.jpg",
-        "https://lh3.googleusercontent.com/-HBxuzALS_Zs/T3R4qERykaI/AAAAAAAAAGQ/_qQ16FaZ1q0/s160-c/sample_image_13.jpg",
-        "https://lh4.googleusercontent.com/-cKojDrARNjQ/T3R4qfWSGPI/AAAAAAAAAGY/MR5dnbNaPyY/s160-c/sample_image_14.jpg",
-        "https://lh3.googleusercontent.com/-WujkdYfcyZ8/T3R4qrIMGUI/AAAAAAAAAGk/277LIdgvnjg/s160-c/sample_image_15.jpg",
-        "https://lh6.googleusercontent.com/-FMHR7Vy3PgI/T3R4rOXlEKI/AAAAAAAAAGs/VeXrDNDBkaw/s160-c/sample_image_16.jpg",
-        "https://lh4.googleusercontent.com/-mrR0AJyNTH0/T3R4rZs6CuI/AAAAAAAAAG0/UE1wQqCOqLA/s160-c/sample_image_17.jpg",
-        "https://lh6.googleusercontent.com/-z77w0eh3cow/T3R4rnLn05I/AAAAAAAAAG4/BaerfWoNucU/s160-c/sample_image_18.jpg",
-        "https://lh5.googleusercontent.com/-aWVwh1OU5Bk/T3R4sAWw0yI/AAAAAAAAAHE/4_KAvJttFwA/s160-c/sample_image_19.jpg",
-        "https://lh6.googleusercontent.com/-q-js52DMnWQ/T3R4tZhY2sI/AAAAAAAAAHM/A8kjp2Ivdqg/s160-c/sample_image_20.jpg",
-        "https://lh5.googleusercontent.com/-_jIzvvzXKn4/T3R4t7xpdVI/AAAAAAAAAHU/7QC6eZ10jgs/s160-c/sample_image_21.jpg",
-        "https://lh3.googleusercontent.com/-lnGi4IMLpwU/T3R4uCMa7vI/AAAAAAAAAHc/1zgzzz6qTpk/s160-c/sample_image_22.jpg",
-        "https://lh5.googleusercontent.com/-fFCzKjFPsPc/T3R4u0SZPFI/AAAAAAAAAHk/sbgjzrktOK0/s160-c/sample_image_23.jpg",
-        "https://lh4.googleusercontent.com/-8TqoW5gBE_Y/T3R4vBS3NPI/AAAAAAAAAHs/EZYvpNsaNXk/s160-c/sample_image_24.jpg",
-        "https://lh6.googleusercontent.com/-gc4eQ3ySdzs/T3R4vafoA7I/AAAAAAAAAH4/yKii5P6tqDE/s160-c/sample_image_25.jpg",
-        "https://lh5.googleusercontent.com/--NYOPCylU7Q/T3R4vjAiWkI/AAAAAAAAAH8/IPNx5q3ptRA/s160-c/sample_image_26.jpg",
-        "https://lh6.googleusercontent.com/-9IJM8so4vCI/T3R4vwJO2yI/AAAAAAAAAIE/ljlr-cwuqZM/s160-c/sample_image_27.jpg",
-        "https://lh4.googleusercontent.com/-KW6QwOHfhBs/T3R4w0RsQiI/AAAAAAAAAIM/uEFLVgHPFCk/s160-c/sample_image_28.jpg",
-        "https://lh4.googleusercontent.com/-z2557Ec1ctY/T3R4x3QA2hI/AAAAAAAAAIk/9-GzPL1lTWE/s160-c/sample_image_29.jpg",
-        "https://lh5.googleusercontent.com/-LaKXAn4Kr1c/T3R4yc5b4lI/AAAAAAAAAIY/fMgcOVQfmD0/s160-c/sample_image_30.jpg",
-        "https://lh4.googleusercontent.com/-F9LRToJoQdo/T3R4yrLtyQI/AAAAAAAAAIg/ri9uUCWuRmo/s160-c/sample_image_31.jpg",
-        "https://lh4.googleusercontent.com/-6X-xBwP-QpI/T3R4zGVboII/AAAAAAAAAIs/zYH4PjjngY0/s160-c/sample_image_32.jpg",
-        "https://lh5.googleusercontent.com/-VdLRjbW4LAs/T3R4zXu3gUI/AAAAAAAAAIw/9aFp9t7mCPg/s160-c/sample_image_33.jpg",
-        "https://lh6.googleusercontent.com/-gL6R17_fDJU/T3R4zpIXGjI/AAAAAAAAAI8/Q2Vjx-L9X20/s160-c/sample_image_34.jpg",
-        "https://lh3.googleusercontent.com/-1fGH4YJXEzo/T3R40Y1B7KI/AAAAAAAAAJE/MnTsa77g-nk/s160-c/sample_image_35.jpg",
-        "https://lh4.googleusercontent.com/-Ql0jHSrea-A/T3R403mUfFI/AAAAAAAAAJM/qzI4SkcH9tY/s160-c/sample_image_36.jpg",
-        "https://lh5.googleusercontent.com/-BL5FIBR_tzI/T3R41DA0AKI/AAAAAAAAAJk/GZfeeb-SLM0/s160-c/sample_image_37.jpg",
-        "https://lh4.googleusercontent.com/-wF2Vc9YDutw/T3R41fR2BCI/AAAAAAAAAJc/JdU1sHdMRAk/s160-c/sample_image_38.jpg",
-        "https://lh6.googleusercontent.com/-ZWHiPehwjTI/T3R41zuaKCI/AAAAAAAAAJg/hR3QJ1v3REg/s160-c/sample_image_39.jpg",
-    };
-
-    /**
-     * Simple static adapter to use for images.
-     */
-    public final static ImageWorkerAdapter imageWorkerUrlsAdapter = new ImageWorkerAdapter() {
-        @Override
-        public Object getItem(int num) {
-            return Images.imageUrls[num];
-        }
-
-        @Override
-        public int getSize() {
-            return Images.imageUrls.length;
-        }
-    };
-
-    /**
-     * Simple static adapter to use for image thumbnails.
-     */
-    public final static ImageWorkerAdapter imageThumbWorkerUrlsAdapter = new ImageWorkerAdapter() {
-        @Override
-        public Object getItem(int num) {
-            return Images.imageThumbUrls[num];
-        }
-
-        @Override
-        public int getSize() {
-            return Images.imageThumbUrls.length;
-        }
+        "http://lh6.googleusercontent.com/-jZgveEqb6pg/T3R4kXScycI/AAAAAAAAAE0/xQ7CvpfXDzc/s160-c/sample_image_01.jpg",
+        "http://lh4.googleusercontent.com/-K2FMuOozxU0/T3R4lRAiBTI/AAAAAAAAAE8/a3Eh9JvnnzI/s160-c/sample_image_02.jpg",
+        "http://lh5.googleusercontent.com/-SCS5C646rxM/T3R4l7QB6xI/AAAAAAAAAFE/xLcuVv3CUyA/s160-c/sample_image_03.jpg",
+        "http://lh6.googleusercontent.com/-f0NJR6-_Thg/T3R4mNex2wI/AAAAAAAAAFI/45oug4VE8MI/s160-c/sample_image_04.jpg",
+        "http://lh3.googleusercontent.com/-n-xcJmiI0pg/T3R4mkSchHI/AAAAAAAAAFU/EoiNNb7kk3A/s160-c/sample_image_05.jpg",
+        "http://lh3.googleusercontent.com/-X43vAudm7f4/T3R4nGSChJI/AAAAAAAAAFk/3bna6D-2EE8/s160-c/sample_image_06.jpg",
+        "http://lh5.googleusercontent.com/-MpZneqIyjXU/T3R4nuGO1aI/AAAAAAAAAFg/r09OPjLx1ZY/s160-c/sample_image_07.jpg",
+        "http://lh6.googleusercontent.com/-ql3YNfdClJo/T3XvW9apmFI/AAAAAAAAAL4/_6HFDzbahc4/s160-c/sample_image_08.jpg",
+        "http://lh5.googleusercontent.com/-Pxa7eqF4cyc/T3R4oasvPEI/AAAAAAAAAF0/-uYDH92h8LA/s160-c/sample_image_09.jpg",
+        "http://lh4.googleusercontent.com/-Li-rjhFEuaI/T3R4o-VUl4I/AAAAAAAAAF8/5E5XdMnP1oE/s160-c/sample_image_10.jpg",
+        "http://lh5.googleusercontent.com/-_HU4fImgFhA/T3R4pPVIwWI/AAAAAAAAAGA/0RfK_Vkgth4/s160-c/sample_image_11.jpg",
+        "http://lh6.googleusercontent.com/-0gnNrVjwa0Y/T3R4peGYJwI/AAAAAAAAAGU/uX_9wvRPM9I/s160-c/sample_image_12.jpg",
+        "http://lh3.googleusercontent.com/-HBxuzALS_Zs/T3R4qERykaI/AAAAAAAAAGQ/_qQ16FaZ1q0/s160-c/sample_image_13.jpg",
+        "http://lh4.googleusercontent.com/-cKojDrARNjQ/T3R4qfWSGPI/AAAAAAAAAGY/MR5dnbNaPyY/s160-c/sample_image_14.jpg",
+        "http://lh3.googleusercontent.com/-WujkdYfcyZ8/T3R4qrIMGUI/AAAAAAAAAGk/277LIdgvnjg/s160-c/sample_image_15.jpg",
+        "http://lh6.googleusercontent.com/-FMHR7Vy3PgI/T3R4rOXlEKI/AAAAAAAAAGs/VeXrDNDBkaw/s160-c/sample_image_16.jpg",
+        "http://lh4.googleusercontent.com/-mrR0AJyNTH0/T3R4rZs6CuI/AAAAAAAAAG0/UE1wQqCOqLA/s160-c/sample_image_17.jpg",
+        "http://lh6.googleusercontent.com/-z77w0eh3cow/T3R4rnLn05I/AAAAAAAAAG4/BaerfWoNucU/s160-c/sample_image_18.jpg",
+        "http://lh5.googleusercontent.com/-aWVwh1OU5Bk/T3R4sAWw0yI/AAAAAAAAAHE/4_KAvJttFwA/s160-c/sample_image_19.jpg",
+        "http://lh6.googleusercontent.com/-q-js52DMnWQ/T3R4tZhY2sI/AAAAAAAAAHM/A8kjp2Ivdqg/s160-c/sample_image_20.jpg",
+        "http://lh5.googleusercontent.com/-_jIzvvzXKn4/T3R4t7xpdVI/AAAAAAAAAHU/7QC6eZ10jgs/s160-c/sample_image_21.jpg",
+        "http://lh3.googleusercontent.com/-lnGi4IMLpwU/T3R4uCMa7vI/AAAAAAAAAHc/1zgzzz6qTpk/s160-c/sample_image_22.jpg",
+        "http://lh5.googleusercontent.com/-fFCzKjFPsPc/T3R4u0SZPFI/AAAAAAAAAHk/sbgjzrktOK0/s160-c/sample_image_23.jpg",
+        "http://lh4.googleusercontent.com/-8TqoW5gBE_Y/T3R4vBS3NPI/AAAAAAAAAHs/EZYvpNsaNXk/s160-c/sample_image_24.jpg",
+        "http://lh6.googleusercontent.com/-gc4eQ3ySdzs/T3R4vafoA7I/AAAAAAAAAH4/yKii5P6tqDE/s160-c/sample_image_25.jpg",
+        "http://lh5.googleusercontent.com/--NYOPCylU7Q/T3R4vjAiWkI/AAAAAAAAAH8/IPNx5q3ptRA/s160-c/sample_image_26.jpg",
+        "http://lh6.googleusercontent.com/-9IJM8so4vCI/T3R4vwJO2yI/AAAAAAAAAIE/ljlr-cwuqZM/s160-c/sample_image_27.jpg",
+        "http://lh4.googleusercontent.com/-KW6QwOHfhBs/T3R4w0RsQiI/AAAAAAAAAIM/uEFLVgHPFCk/s160-c/sample_image_28.jpg",
+        "http://lh4.googleusercontent.com/-z2557Ec1ctY/T3R4x3QA2hI/AAAAAAAAAIk/9-GzPL1lTWE/s160-c/sample_image_29.jpg",
+        "http://lh5.googleusercontent.com/-LaKXAn4Kr1c/T3R4yc5b4lI/AAAAAAAAAIY/fMgcOVQfmD0/s160-c/sample_image_30.jpg",
+        "http://lh4.googleusercontent.com/-F9LRToJoQdo/T3R4yrLtyQI/AAAAAAAAAIg/ri9uUCWuRmo/s160-c/sample_image_31.jpg",
+        "http://lh4.googleusercontent.com/-6X-xBwP-QpI/T3R4zGVboII/AAAAAAAAAIs/zYH4PjjngY0/s160-c/sample_image_32.jpg",
+        "http://lh5.googleusercontent.com/-VdLRjbW4LAs/T3R4zXu3gUI/AAAAAAAAAIw/9aFp9t7mCPg/s160-c/sample_image_33.jpg",
+        "http://lh6.googleusercontent.com/-gL6R17_fDJU/T3R4zpIXGjI/AAAAAAAAAI8/Q2Vjx-L9X20/s160-c/sample_image_34.jpg",
+        "http://lh3.googleusercontent.com/-1fGH4YJXEzo/T3R40Y1B7KI/AAAAAAAAAJE/MnTsa77g-nk/s160-c/sample_image_35.jpg",
+        "http://lh4.googleusercontent.com/-Ql0jHSrea-A/T3R403mUfFI/AAAAAAAAAJM/qzI4SkcH9tY/s160-c/sample_image_36.jpg",
+        "http://lh5.googleusercontent.com/-BL5FIBR_tzI/T3R41DA0AKI/AAAAAAAAAJk/GZfeeb-SLM0/s160-c/sample_image_37.jpg",
+        "http://lh4.googleusercontent.com/-wF2Vc9YDutw/T3R41fR2BCI/AAAAAAAAAJc/JdU1sHdMRAk/s160-c/sample_image_38.jpg",
+        "http://lh6.googleusercontent.com/-ZWHiPehwjTI/T3R41zuaKCI/AAAAAAAAAJg/hR3QJ1v3REg/s160-c/sample_image_39.jpg",
     };
 }
diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailActivity.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailActivity.java
index c7ee8cd..acd483f 100644
--- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailActivity.java
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailActivity.java
@@ -16,32 +16,28 @@
 
 package com.example.android.bitmapfun.ui;
 
-import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
 import android.app.ActionBar;
-import android.content.Intent;
 import android.os.Bundle;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentActivity;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.FragmentStatePagerAdapter;
+import android.support.v4.app.NavUtils;
 import android.support.v4.view.ViewPager;
 import android.util.DisplayMetrics;
 import android.view.Menu;
-import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
-import android.view.ViewGroup;
 import android.view.WindowManager.LayoutParams;
 import android.widget.Toast;
 
+import com.example.android.bitmapfun.BuildConfig;
 import com.example.android.bitmapfun.R;
 import com.example.android.bitmapfun.provider.Images;
-import com.example.android.bitmapfun.util.DiskLruCache;
 import com.example.android.bitmapfun.util.ImageCache;
 import com.example.android.bitmapfun.util.ImageFetcher;
-import com.example.android.bitmapfun.util.ImageResizer;
-import com.example.android.bitmapfun.util.ImageWorker;
 import com.example.android.bitmapfun.util.Utils;
 
 public class ImageDetailActivity extends FragmentActivity implements OnClickListener {
@@ -49,51 +45,59 @@
     public static final String EXTRA_IMAGE = "extra_image";
 
     private ImagePagerAdapter mAdapter;
-    private ImageResizer mImageWorker;
+    private ImageFetcher mImageFetcher;
     private ViewPager mPager;
 
-    @SuppressLint("NewApi")
+    @TargetApi(11)
     @Override
     public void onCreate(Bundle savedInstanceState) {
+        if (BuildConfig.DEBUG) {
+            Utils.enableStrictMode();
+        }
         super.onCreate(savedInstanceState);
         setContentView(R.layout.image_detail_pager);
 
         // Fetch screen height and width, to use as our max size when loading images as this
         // activity runs full screen
-        final DisplayMetrics displaymetrics = new DisplayMetrics();
-        getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
-        final int height = displaymetrics.heightPixels;
-        final int width = displaymetrics.widthPixels;
-        final int longest = height > width ? height : width;
+        final DisplayMetrics displayMetrics = new DisplayMetrics();
+        getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
+        final int height = displayMetrics.heightPixels;
+        final int width = displayMetrics.widthPixels;
 
-        // The ImageWorker takes care of loading images into our ImageView children asynchronously
-        mImageWorker = new ImageFetcher(this, longest);
-        mImageWorker.setAdapter(Images.imageWorkerUrlsAdapter);
-        mImageWorker.setImageCache(ImageCache.findOrCreateCache(this, IMAGE_CACHE_DIR));
-        mImageWorker.setImageFadeIn(false);
+        // For this sample we'll use half of the longest width to resize our images. As the
+        // image scaling ensures the image is larger than this, we should be left with a
+        // resolution that is appropriate for both portrait and landscape. For best image quality
+        // we shouldn't divide by 2, but this will use more memory and require a larger memory
+        // cache.
+        final int longest = (height > width ? height : width) / 2;
+
+        ImageCache.ImageCacheParams cacheParams =
+                new ImageCache.ImageCacheParams(this, IMAGE_CACHE_DIR);
+        cacheParams.setMemCacheSizePercent(this, 0.25f); // Set memory cache to 25% of mem class
+
+        // The ImageFetcher takes care of loading images into our ImageView children asynchronously
+        mImageFetcher = new ImageFetcher(this, longest);
+        mImageFetcher.addImageCache(getSupportFragmentManager(), cacheParams);
+        mImageFetcher.setImageFadeIn(false);
 
         // Set up ViewPager and backing adapter
-        mAdapter = new ImagePagerAdapter(getSupportFragmentManager(),
-                mImageWorker.getAdapter().getSize());
+        mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), Images.imageUrls.length);
         mPager = (ViewPager) findViewById(R.id.pager);
         mPager.setAdapter(mAdapter);
         mPager.setPageMargin((int) getResources().getDimension(R.dimen.image_detail_pager_margin));
+        mPager.setOffscreenPageLimit(2);
 
         // Set up activity to go full screen
         getWindow().addFlags(LayoutParams.FLAG_FULLSCREEN);
 
-        // Enable some additional newer visibility and ActionBar features to create a more immersive
-        // photo viewing experience
-        if (Utils.hasActionBar()) {
+        // Enable some additional newer visibility and ActionBar features to create a more
+        // immersive photo viewing experience
+        if (Utils.hasHoneycomb()) {
             final ActionBar actionBar = getActionBar();
 
-            // Enable "up" navigation on ActionBar icon and hide title text
-            actionBar.setDisplayHomeAsUpEnabled(true);
+            // Hide title text and set home as up
             actionBar.setDisplayShowTitleEnabled(false);
-
-            // Start low profile mode and hide ActionBar
-            mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
-            actionBar.hide();
+            actionBar.setDisplayHomeAsUpEnabled(true);
 
             // Hide and show the ActionBar as the visibility changes
             mPager.setOnSystemUiVisibilityChangeListener(
@@ -107,6 +111,10 @@
                             }
                         }
                     });
+
+            // Start low profile mode and hide ActionBar
+            mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
+            actionBar.hide();
         }
 
         // Set the current item based on the extra passed in to this activity
@@ -117,22 +125,34 @@
     }
 
     @Override
+    public void onResume() {
+        super.onResume();
+        mImageFetcher.setExitTasksEarly(false);
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        mImageFetcher.setExitTasksEarly(true);
+        mImageFetcher.flushCache();
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        mImageFetcher.closeCache();
+    }
+
+    @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
             case android.R.id.home:
-                // Home or "up" navigation
-                final Intent intent = new Intent(this, ImageGridActivity.class);
-                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-                startActivity(intent);
+                NavUtils.navigateUpFromSameTask(this);
                 return true;
             case R.id.clear_cache:
-                final ImageCache cache = mImageWorker.getImageCache();
-                if (cache != null) {
-                    mImageWorker.getImageCache().clearCaches();
-                    DiskLruCache.clearCache(this, ImageFetcher.HTTP_CACHE_DIR);
-                    Toast.makeText(this, R.string.clear_cache_complete,
-                            Toast.LENGTH_SHORT).show();
-                }
+                mImageFetcher.clearCache();
+                Toast.makeText(
+                        this, R.string.clear_cache_complete_toast,Toast.LENGTH_SHORT).show();
                 return true;
         }
         return super.onOptionsItemSelected(item);
@@ -140,18 +160,15 @@
 
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
-        MenuInflater inflater = getMenuInflater();
-        inflater.inflate(R.menu.main_menu, menu);
+        getMenuInflater().inflate(R.menu.main_menu, menu);
         return true;
     }
 
     /**
-     * Called by the ViewPager child fragments to load images via the one ImageWorker
-     *
-     * @return
+     * Called by the ViewPager child fragments to load images via the one ImageFetcher
      */
-    public ImageWorker getImageWorker() {
-        return mImageWorker;
+    public ImageFetcher getImageFetcher() {
+        return mImageFetcher;
     }
 
     /**
@@ -174,15 +191,7 @@
 
         @Override
         public Fragment getItem(int position) {
-            return ImageDetailFragment.newInstance(position);
-        }
-
-        @Override
-        public void destroyItem(ViewGroup container, int position, Object object) {
-            final ImageDetailFragment fragment = (ImageDetailFragment) object;
-            // As the item gets destroyed we try and cancel any existing work.
-            fragment.cancelWork();
-            super.destroyItem(container, position, object);
+            return ImageDetailFragment.newInstance(Images.imageUrls[position]);
         }
     }
 
@@ -190,7 +199,7 @@
      * Set on the ImageView in the ViewPager children fragments, to enable/disable low profile mode
      * when the ImageView is touched.
      */
-    @SuppressLint("NewApi")
+    @TargetApi(11)
     @Override
     public void onClick(View v) {
         final int vis = mPager.getSystemUiVisibility();
diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailFragment.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailFragment.java
index e2fd703..a0b3855 100644
--- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailFragment.java
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailFragment.java
@@ -18,6 +18,7 @@
 
 import android.os.Bundle;
 import android.support.v4.app.Fragment;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.OnClickListener;
@@ -25,6 +26,7 @@
 import android.widget.ImageView;
 
 import com.example.android.bitmapfun.R;
+import com.example.android.bitmapfun.util.ImageFetcher;
 import com.example.android.bitmapfun.util.ImageWorker;
 import com.example.android.bitmapfun.util.Utils;
 
@@ -32,22 +34,22 @@
  * This fragment will populate the children of the ViewPager from {@link ImageDetailActivity}.
  */
 public class ImageDetailFragment extends Fragment {
-    private static final String IMAGE_DATA_EXTRA = "resId";
-    private int mImageNum;
+    private static final String IMAGE_DATA_EXTRA = "extra_image_data";
+    private String mImageUrl;
     private ImageView mImageView;
-    private ImageWorker mImageWorker;
+    private ImageFetcher mImageFetcher;
 
     /**
      * Factory method to generate a new instance of the fragment given an image number.
      *
-     * @param imageNum The image number within the parent adapter to load
+     * @param imageUrl The image url to load
      * @return A new instance of ImageDetailFragment with imageNum extras
      */
-    public static ImageDetailFragment newInstance(int imageNum) {
+    public static ImageDetailFragment newInstance(String imageUrl) {
         final ImageDetailFragment f = new ImageDetailFragment();
 
         final Bundle args = new Bundle();
-        args.putInt(IMAGE_DATA_EXTRA, imageNum);
+        args.putString(IMAGE_DATA_EXTRA, imageUrl);
         f.setArguments(args);
 
         return f;
@@ -59,13 +61,13 @@
     public ImageDetailFragment() {}
 
     /**
-     * Populate image number from extra, use the convenience factory method
-     * {@link ImageDetailFragment#newInstance(int)} to create this fragment.
+     * Populate image using a url from extras, use the convenience factory method
+     * {@link ImageDetailFragment#newInstance(String)} to create this fragment.
      */
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
+        mImageUrl = getArguments() != null ? getArguments().getString(IMAGE_DATA_EXTRA) : null;
     }
 
     @Override
@@ -84,23 +86,23 @@
         // Use the parent activity to load the image asynchronously into the ImageView (so a single
         // cache can be used over all pages in the ViewPager
         if (ImageDetailActivity.class.isInstance(getActivity())) {
-            mImageWorker = ((ImageDetailActivity) getActivity()).getImageWorker();
-            mImageWorker.loadImage(mImageNum, mImageView);
+            mImageFetcher = ((ImageDetailActivity) getActivity()).getImageFetcher();
+            mImageFetcher.loadImage(mImageUrl, mImageView);
         }
 
         // Pass clicks on the ImageView to the parent activity to handle
-        if (OnClickListener.class.isInstance(getActivity()) && Utils.hasActionBar()) {
+        if (OnClickListener.class.isInstance(getActivity()) && Utils.hasHoneycomb()) {
             mImageView.setOnClickListener((OnClickListener) getActivity());
         }
     }
 
-    /**
-     * Cancels the asynchronous work taking place on the ImageView, called by the adapter backing
-     * the ViewPager when the child is destroyed.
-     */
-    public void cancelWork() {
-        ImageWorker.cancelWork(mImageView);
-        mImageView.setImageDrawable(null);
-        mImageView = null;
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (mImageView != null) {
+            // Cancel any pending image work
+            ImageWorker.cancelWork(mImageView);
+            mImageView.setImageDrawable(null);
+        }
     }
 }
diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridActivity.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridActivity.java
index 28d97b3..2e00930 100644
--- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridActivity.java
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridActivity.java
@@ -16,6 +16,9 @@
 
 package com.example.android.bitmapfun.ui;
 
+import com.example.android.bitmapfun.BuildConfig;
+import com.example.android.bitmapfun.util.Utils;
+
 import android.os.Bundle;
 import android.support.v4.app.FragmentActivity;
 import android.support.v4.app.FragmentTransaction;
@@ -24,10 +27,13 @@
  * Simple FragmentActivity to hold the main {@link ImageGridFragment} and not much else.
  */
 public class ImageGridActivity extends FragmentActivity {
-    private static final String TAG = "ImageGridFragment";
+    private static final String TAG = "ImageGridActivity";
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
+        if (BuildConfig.DEBUG) {
+            Utils.enableStrictMode();
+        }
         super.onCreate(savedInstanceState);
 
         if (getSupportFragmentManager().findFragmentByTag(TAG) == null) {
diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridFragment.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridFragment.java
index 495d405..8a8bcf0 100644
--- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridFragment.java
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridFragment.java
@@ -16,6 +16,8 @@
 
 package com.example.android.bitmapfun.ui;
 
+import android.annotation.TargetApi;
+import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
@@ -40,11 +42,8 @@
 import com.example.android.bitmapfun.BuildConfig;
 import com.example.android.bitmapfun.R;
 import com.example.android.bitmapfun.provider.Images;
-import com.example.android.bitmapfun.util.DiskLruCache;
-import com.example.android.bitmapfun.util.ImageCache;
 import com.example.android.bitmapfun.util.ImageCache.ImageCacheParams;
 import com.example.android.bitmapfun.util.ImageFetcher;
-import com.example.android.bitmapfun.util.ImageResizer;
 import com.example.android.bitmapfun.util.Utils;
 
 /**
@@ -52,7 +51,7 @@
  * implementation with the key addition being the ImageWorker class w/ImageCache to load children
  * asynchronously, keeping the UI nice and smooth and caching thumbnails for quick retrieval. The
  * cache is retained over configuration changes like orientation change so the images are populated
- * quickly as the user rotates the device.
+ * quickly if, for example, the user rotates the device.
  */
 public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
     private static final String TAG = "ImageGridFragment";
@@ -61,7 +60,7 @@
     private int mImageThumbSize;
     private int mImageThumbSpacing;
     private ImageAdapter mAdapter;
-    private ImageResizer mImageWorker;
+    private ImageFetcher mImageFetcher;
 
     /**
      * Empty constructor as per the Fragment documentation
@@ -78,22 +77,15 @@
 
         mAdapter = new ImageAdapter(getActivity());
 
-        ImageCacheParams cacheParams = new ImageCacheParams(IMAGE_CACHE_DIR);
+        ImageCacheParams cacheParams = new ImageCacheParams(getActivity(), IMAGE_CACHE_DIR);
 
-        // Allocate a third of the per-app memory limit to the bitmap memory cache. This value
-        // should be chosen carefully based on a number of factors. Refer to the corresponding
-        // Android Training class for more discussion:
-        // http://developer.android.com/training/displaying-bitmaps/
-        // In this case, we aren't using memory for much else other than this activity and the
-        // ImageDetailActivity so a third lets us keep all our sample image thumbnails in memory
-        // at once.
-        cacheParams.memCacheSize = 1024 * 1024 * Utils.getMemoryClass(getActivity()) / 3;
+        // Set memory cache to 25% of mem class
+        cacheParams.setMemCacheSizePercent(getActivity(), 0.25f);
 
-        // The ImageWorker takes care of loading images into our ImageView children asynchronously
-        mImageWorker = new ImageFetcher(getActivity(), mImageThumbSize);
-        mImageWorker.setAdapter(Images.imageThumbWorkerUrlsAdapter);
-        mImageWorker.setLoadingImage(R.drawable.empty_photo);
-        mImageWorker.setImageCache(ImageCache.findOrCreateCache(getActivity(), cacheParams));
+        // The ImageFetcher takes care of loading images into our ImageView children asynchronously
+        mImageFetcher = new ImageFetcher(getActivity(), mImageThumbSize);
+        mImageFetcher.setLoadingImage(R.drawable.empty_photo);
+        mImageFetcher.addImageCache(getActivity().getSupportFragmentManager(), cacheParams);
     }
 
     @Override
@@ -104,6 +96,22 @@
         final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
         mGridView.setAdapter(mAdapter);
         mGridView.setOnItemClickListener(this);
+        mGridView.setOnScrollListener(new AbsListView.OnScrollListener() {
+            @Override
+            public void onScrollStateChanged(AbsListView absListView, int scrollState) {
+                // Pause fetcher to ensure smoother scrolling when flinging
+                if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
+                    mImageFetcher.setPauseWork(true);
+                } else {
+                    mImageFetcher.setPauseWork(false);
+                }
+            }
+
+            @Override
+            public void onScroll(AbsListView absListView, int firstVisibleItem,
+                    int visibleItemCount, int totalItemCount) {
+            }
+        });
 
         // This listener is used to get the final width of the GridView and then calculate the
         // number of columns and the width of each column. The width of each column is variable
@@ -135,21 +143,38 @@
     @Override
     public void onResume() {
         super.onResume();
-        mImageWorker.setExitTasksEarly(false);
+        mImageFetcher.setExitTasksEarly(false);
         mAdapter.notifyDataSetChanged();
     }
 
     @Override
     public void onPause() {
         super.onPause();
-        mImageWorker.setExitTasksEarly(true);
+        mImageFetcher.setExitTasksEarly(true);
+        mImageFetcher.flushCache();
     }
 
     @Override
+    public void onDestroy() {
+        super.onDestroy();
+        mImageFetcher.closeCache();
+    }
+
+    @TargetApi(16)
+    @Override
     public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
         final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
         i.putExtra(ImageDetailActivity.EXTRA_IMAGE, (int) id);
-        startActivity(i);
+        if (Utils.hasJellyBean()) {
+            // makeThumbnailScaleUpAnimation() looks kind of ugly here as the loading spinner may
+            // show plus the thumbnail image in GridView is cropped. so using
+            // makeScaleUpAnimation() instead.
+            ActivityOptions options =
+                    ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.getWidth(), v.getHeight());
+            getActivity().startActivity(i, options.toBundle());
+        } else {
+            startActivity(i);
+        }
     }
 
     @Override
@@ -161,13 +186,9 @@
     public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
             case R.id.clear_cache:
-                final ImageCache cache = mImageWorker.getImageCache();
-                if (cache != null) {
-                    mImageWorker.getImageCache().clearCaches();
-                    DiskLruCache.clearCache(getActivity(), ImageFetcher.HTTP_CACHE_DIR);
-                    Toast.makeText(getActivity(), R.string.clear_cache_complete,
-                            Toast.LENGTH_SHORT).show();
-                }
+                mImageFetcher.clearCache();
+                Toast.makeText(getActivity(), R.string.clear_cache_complete_toast,
+                        Toast.LENGTH_SHORT).show();
                 return true;
         }
         return super.onOptionsItemSelected(item);
@@ -183,7 +204,7 @@
         private final Context mContext;
         private int mItemHeight = 0;
         private int mNumColumns = 0;
-        private int mActionBarHeight = -1;
+        private int mActionBarHeight = 0;
         private GridView.LayoutParams mImageViewLayoutParams;
 
         public ImageAdapter(Context context) {
@@ -191,18 +212,25 @@
             mContext = context;
             mImageViewLayoutParams = new GridView.LayoutParams(
                     LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+            // Calculate ActionBar height
+            TypedValue tv = new TypedValue();
+            if (context.getTheme().resolveAttribute(
+                    android.R.attr.actionBarSize, tv, true)) {
+                mActionBarHeight = TypedValue.complexToDimensionPixelSize(
+                        tv.data, context.getResources().getDisplayMetrics());
+            }
         }
 
         @Override
         public int getCount() {
-            // Size of adapter + number of columns for top empty row
-            return mImageWorker.getAdapter().getSize() + mNumColumns;
+            // Size + number of columns for top empty row
+            return Images.imageThumbUrls.length + mNumColumns;
         }
 
         @Override
         public Object getItem(int position) {
             return position < mNumColumns ?
-                    null : mImageWorker.getAdapter().getItem(position - mNumColumns);
+                    null : Images.imageThumbUrls[position - mNumColumns];
         }
 
         @Override
@@ -233,18 +261,6 @@
                 if (convertView == null) {
                     convertView = new View(mContext);
                 }
-                // Calculate ActionBar height
-                if (mActionBarHeight < 0) {
-                    TypedValue tv = new TypedValue();
-                    if (mContext.getTheme().resolveAttribute(
-                            android.R.attr.actionBarSize, tv, true)) {
-                        mActionBarHeight = TypedValue.complexToDimensionPixelSize(
-                                tv.data, mContext.getResources().getDisplayMetrics());
-                    } else {
-                        // No ActionBar style (pre-Honeycomb or ActionBar not in theme)
-                        mActionBarHeight = 0;
-                    }
-                }
                 // Set empty view with height of ActionBar
                 convertView.setLayoutParams(new AbsListView.LayoutParams(
                         ViewGroup.LayoutParams.MATCH_PARENT, mActionBarHeight));
@@ -268,7 +284,7 @@
 
             // Finally load the image asynchronously into the ImageView, this also takes care of
             // setting a placeholder image while the background thread runs
-            mImageWorker.loadImage(position - mNumColumns, imageView);
+            mImageFetcher.loadImage(Images.imageThumbUrls[position - mNumColumns], imageView);
             return imageView;
         }
 
@@ -285,7 +301,7 @@
             mItemHeight = height;
             mImageViewLayoutParams =
                     new GridView.LayoutParams(LayoutParams.MATCH_PARENT, mItemHeight);
-            mImageWorker.setImageSize(height);
+            mImageFetcher.setImageSize(height);
             notifyDataSetChanged();
         }
 
diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/AsyncTask.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/AsyncTask.java
new file mode 100644
index 0000000..018ce1a
--- /dev/null
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/AsyncTask.java
@@ -0,0 +1,693 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bitmapfun.util;
+
+import android.annotation.TargetApi;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Process;
+
+import java.util.ArrayDeque;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * *************************************
+ * Copied from JB release framework:
+ * https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/os/AsyncTask.java
+ *
+ * so that threading behavior on all OS versions is the same and we can tweak behavior by using
+ * executeOnExecutor() if needed.
+ *
+ * There are 3 changes in this copy of AsyncTask:
+ *    -pre-HC a single thread executor is used for serial operation
+ *    (Executors.newSingleThreadExecutor) and is the default
+ *    -the default THREAD_POOL_EXECUTOR was changed to use DiscardOldestPolicy
+ *    -a new fixed thread pool called DUAL_THREAD_EXECUTOR was added
+ * *************************************
+ *
+ * <p>AsyncTask enables proper and easy use of the UI thread. This class allows to
+ * perform background operations and publish results on the UI thread without
+ * having to manipulate threads and/or handlers.</p>
+ *
+ * <p>AsyncTask is designed to be a helper class around {@link Thread} and {@link Handler}
+ * and does not constitute a generic threading framework. AsyncTasks should ideally be
+ * used for short operations (a few seconds at the most.) If you need to keep threads
+ * running for long periods of time, it is highly recommended you use the various APIs
+ * provided by the <code>java.util.concurrent</code> pacakge such as {@link Executor},
+ * {@link ThreadPoolExecutor} and {@link FutureTask}.</p>
+ *
+ * <p>An asynchronous task is defined by a computation that runs on a background thread and
+ * whose result is published on the UI thread. An asynchronous task is defined by 3 generic
+ * types, called <code>Params</code>, <code>Progress</code> and <code>Result</code>,
+ * and 4 steps, called <code>onPreExecute</code>, <code>doInBackground</code>,
+ * <code>onProgressUpdate</code> and <code>onPostExecute</code>.</p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using tasks and threads, read the
+ * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html">Processes and
+ * Threads</a> developer guide.</p>
+ * </div>
+ *
+ * <h2>Usage</h2>
+ * <p>AsyncTask must be subclassed to be used. The subclass will override at least
+ * one method ({@link #doInBackground}), and most often will override a
+ * second one ({@link #onPostExecute}.)</p>
+ *
+ * <p>Here is an example of subclassing:</p>
+ * <pre class="prettyprint">
+ * private class DownloadFilesTask extends AsyncTask&lt;URL, Integer, Long&gt; {
+ *     protected Long doInBackground(URL... urls) {
+ *         int count = urls.length;
+ *         long totalSize = 0;
+ *         for (int i = 0; i < count; i++) {
+ *             totalSize += Downloader.downloadFile(urls[i]);
+ *             publishProgress((int) ((i / (float) count) * 100));
+ *             // Escape early if cancel() is called
+ *             if (isCancelled()) break;
+ *         }
+ *         return totalSize;
+ *     }
+ *
+ *     protected void onProgressUpdate(Integer... progress) {
+ *         setProgressPercent(progress[0]);
+ *     }
+ *
+ *     protected void onPostExecute(Long result) {
+ *         showDialog("Downloaded " + result + " bytes");
+ *     }
+ * }
+ * </pre>
+ *
+ * <p>Once created, a task is executed very simply:</p>
+ * <pre class="prettyprint">
+ * new DownloadFilesTask().execute(url1, url2, url3);
+ * </pre>
+ *
+ * <h2>AsyncTask's generic types</h2>
+ * <p>The three types used by an asynchronous task are the following:</p>
+ * <ol>
+ *     <li><code>Params</code>, the type of the parameters sent to the task upon
+ *     execution.</li>
+ *     <li><code>Progress</code>, the type of the progress units published during
+ *     the background computation.</li>
+ *     <li><code>Result</code>, the type of the result of the background
+ *     computation.</li>
+ * </ol>
+ * <p>Not all types are always used by an asynchronous task. To mark a type as unused,
+ * simply use the type {@link Void}:</p>
+ * <pre>
+ * private class MyTask extends AsyncTask&lt;Void, Void, Void&gt; { ... }
+ * </pre>
+ *
+ * <h2>The 4 steps</h2>
+ * <p>When an asynchronous task is executed, the task goes through 4 steps:</p>
+ * <ol>
+ *     <li>{@link #onPreExecute()}, invoked on the UI thread immediately after the task
+ *     is executed. This step is normally used to setup the task, for instance by
+ *     showing a progress bar in the user interface.</li>
+ *     <li>{@link #doInBackground}, invoked on the background thread
+ *     immediately after {@link #onPreExecute()} finishes executing. This step is used
+ *     to perform background computation that can take a long time. The parameters
+ *     of the asynchronous task are passed to this step. The result of the computation must
+ *     be returned by this step and will be passed back to the last step. This step
+ *     can also use {@link #publishProgress} to publish one or more units
+ *     of progress. These values are published on the UI thread, in the
+ *     {@link #onProgressUpdate} step.</li>
+ *     <li>{@link #onProgressUpdate}, invoked on the UI thread after a
+ *     call to {@link #publishProgress}. The timing of the execution is
+ *     undefined. This method is used to display any form of progress in the user
+ *     interface while the background computation is still executing. For instance,
+ *     it can be used to animate a progress bar or show logs in a text field.</li>
+ *     <li>{@link #onPostExecute}, invoked on the UI thread after the background
+ *     computation finishes. The result of the background computation is passed to
+ *     this step as a parameter.</li>
+ * </ol>
+ *
+ * <h2>Cancelling a task</h2>
+ * <p>A task can be cancelled at any time by invoking {@link #cancel(boolean)}. Invoking
+ * this method will cause subsequent calls to {@link #isCancelled()} to return true.
+ * After invoking this method, {@link #onCancelled(Object)}, instead of
+ * {@link #onPostExecute(Object)} will be invoked after {@link #doInBackground(Object[])}
+ * returns. To ensure that a task is cancelled as quickly as possible, you should always
+ * check the return value of {@link #isCancelled()} periodically from
+ * {@link #doInBackground(Object[])}, if possible (inside a loop for instance.)</p>
+ *
+ * <h2>Threading rules</h2>
+ * <p>There are a few threading rules that must be followed for this class to
+ * work properly:</p>
+ * <ul>
+ *     <li>The AsyncTask class must be loaded on the UI thread. This is done
+ *     automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.</li>
+ *     <li>The task instance must be created on the UI thread.</li>
+ *     <li>{@link #execute} must be invoked on the UI thread.</li>
+ *     <li>Do not call {@link #onPreExecute()}, {@link #onPostExecute},
+ *     {@link #doInBackground}, {@link #onProgressUpdate} manually.</li>
+ *     <li>The task can be executed only once (an exception will be thrown if
+ *     a second execution is attempted.)</li>
+ * </ul>
+ *
+ * <h2>Memory observability</h2>
+ * <p>AsyncTask guarantees that all callback calls are synchronized in such a way that the following
+ * operations are safe without explicit synchronizations.</p>
+ * <ul>
+ *     <li>Set member fields in the constructor or {@link #onPreExecute}, and refer to them
+ *     in {@link #doInBackground}.
+ *     <li>Set member fields in {@link #doInBackground}, and refer to them in
+ *     {@link #onProgressUpdate} and {@link #onPostExecute}.
+ * </ul>
+ *
+ * <h2>Order of execution</h2>
+ * <p>When first introduced, AsyncTasks were executed serially on a single background
+ * thread. Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed
+ * to a pool of threads allowing multiple tasks to operate in parallel. Starting with
+ * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are executed on a single
+ * thread to avoid common application errors caused by parallel execution.</p>
+ * <p>If you truly want parallel execution, you can invoke
+ * {@link #executeOnExecutor(java.util.concurrent.Executor, Object[])} with
+ * {@link #THREAD_POOL_EXECUTOR}.</p>
+ */
+public abstract class AsyncTask<Params, Progress, Result> {
+    private static final String LOG_TAG = "AsyncTask";
+
+    private static final int CORE_POOL_SIZE = 5;
+    private static final int MAXIMUM_POOL_SIZE = 128;
+    private static final int KEEP_ALIVE = 1;
+
+    private static final ThreadFactory  sThreadFactory = new ThreadFactory() {
+        private final AtomicInteger mCount = new AtomicInteger(1);
+
+        public Thread newThread(Runnable r) {
+            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
+        }
+    };
+
+    private static final BlockingQueue<Runnable> sPoolWorkQueue =
+            new LinkedBlockingQueue<Runnable>(10);
+
+    /**
+     * An {@link Executor} that can be used to execute tasks in parallel.
+     */
+    public static final Executor THREAD_POOL_EXECUTOR
+            = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
+            TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory,
+            new ThreadPoolExecutor.DiscardOldestPolicy());
+
+    /**
+     * An {@link Executor} that executes tasks one at a time in serial
+     * order.  This serialization is global to a particular process.
+     */
+    public static final Executor SERIAL_EXECUTOR = Utils.hasHoneycomb() ? new SerialExecutor() :
+            Executors.newSingleThreadExecutor(sThreadFactory);
+
+    public static final Executor DUAL_THREAD_EXECUTOR =
+            Executors.newFixedThreadPool(2, sThreadFactory);
+
+    private static final int MESSAGE_POST_RESULT = 0x1;
+    private static final int MESSAGE_POST_PROGRESS = 0x2;
+
+    private static final InternalHandler sHandler = new InternalHandler();
+
+    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
+    private final WorkerRunnable<Params, Result> mWorker;
+    private final FutureTask<Result> mFuture;
+
+    private volatile Status mStatus = Status.PENDING;
+
+    private final AtomicBoolean mCancelled = new AtomicBoolean();
+    private final AtomicBoolean mTaskInvoked = new AtomicBoolean();
+
+    @TargetApi(11)
+    private static class SerialExecutor implements Executor {
+        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
+        Runnable mActive;
+
+        public synchronized void execute(final Runnable r) {
+            mTasks.offer(new Runnable() {
+                public void run() {
+                    try {
+                        r.run();
+                    } finally {
+                        scheduleNext();
+                    }
+                }
+            });
+            if (mActive == null) {
+                scheduleNext();
+            }
+        }
+
+        protected synchronized void scheduleNext() {
+            if ((mActive = mTasks.poll()) != null) {
+                THREAD_POOL_EXECUTOR.execute(mActive);
+            }
+        }
+    }
+
+    /**
+     * Indicates the current status of the task. Each status will be set only once
+     * during the lifetime of a task.
+     */
+    public enum Status {
+        /**
+         * Indicates that the task has not been executed yet.
+         */
+        PENDING,
+        /**
+         * Indicates that the task is running.
+         */
+        RUNNING,
+        /**
+         * Indicates that {@link AsyncTask#onPostExecute} has finished.
+         */
+        FINISHED,
+    }
+
+    /** @hide Used to force static handler to be created. */
+    public static void init() {
+        sHandler.getLooper();
+    }
+
+    /** @hide */
+    public static void setDefaultExecutor(Executor exec) {
+        sDefaultExecutor = exec;
+    }
+
+    /**
+     * Creates a new asynchronous task. This constructor must be invoked on the UI thread.
+     */
+    public AsyncTask() {
+        mWorker = new WorkerRunnable<Params, Result>() {
+            public Result call() throws Exception {
+                mTaskInvoked.set(true);
+
+                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+                //noinspection unchecked
+                return postResult(doInBackground(mParams));
+            }
+        };
+
+        mFuture = new FutureTask<Result>(mWorker) {
+            @Override
+            protected void done() {
+                try {
+                    postResultIfNotInvoked(get());
+                } catch (InterruptedException e) {
+                    android.util.Log.w(LOG_TAG, e);
+                } catch (ExecutionException e) {
+                    throw new RuntimeException("An error occured while executing doInBackground()",
+                            e.getCause());
+                } catch (CancellationException e) {
+                    postResultIfNotInvoked(null);
+                }
+            }
+        };
+    }
+
+    private void postResultIfNotInvoked(Result result) {
+        final boolean wasTaskInvoked = mTaskInvoked.get();
+        if (!wasTaskInvoked) {
+            postResult(result);
+        }
+    }
+
+    private Result postResult(Result result) {
+        @SuppressWarnings("unchecked")
+        Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
+                new AsyncTaskResult<Result>(this, result));
+        message.sendToTarget();
+        return result;
+    }
+
+    /**
+     * Returns the current status of this task.
+     *
+     * @return The current status.
+     */
+    public final Status getStatus() {
+        return mStatus;
+    }
+
+    /**
+     * Override this method to perform a computation on a background thread. The
+     * specified parameters are the parameters passed to {@link #execute}
+     * by the caller of this task.
+     *
+     * This method can call {@link #publishProgress} to publish updates
+     * on the UI thread.
+     *
+     * @param params The parameters of the task.
+     *
+     * @return A result, defined by the subclass of this task.
+     *
+     * @see #onPreExecute()
+     * @see #onPostExecute
+     * @see #publishProgress
+     */
+    protected abstract Result doInBackground(Params... params);
+
+    /**
+     * Runs on the UI thread before {@link #doInBackground}.
+     *
+     * @see #onPostExecute
+     * @see #doInBackground
+     */
+    protected void onPreExecute() {
+    }
+
+    /**
+     * <p>Runs on the UI thread after {@link #doInBackground}. The
+     * specified result is the value returned by {@link #doInBackground}.</p>
+     *
+     * <p>This method won't be invoked if the task was cancelled.</p>
+     *
+     * @param result The result of the operation computed by {@link #doInBackground}.
+     *
+     * @see #onPreExecute
+     * @see #doInBackground
+     * @see #onCancelled(Object)
+     */
+    @SuppressWarnings({"UnusedDeclaration"})
+    protected void onPostExecute(Result result) {
+    }
+
+    /**
+     * Runs on the UI thread after {@link #publishProgress} is invoked.
+     * The specified values are the values passed to {@link #publishProgress}.
+     *
+     * @param values The values indicating progress.
+     *
+     * @see #publishProgress
+     * @see #doInBackground
+     */
+    @SuppressWarnings({"UnusedDeclaration"})
+    protected void onProgressUpdate(Progress... values) {
+    }
+
+    /**
+     * <p>Runs on the UI thread after {@link #cancel(boolean)} is invoked and
+     * {@link #doInBackground(Object[])} has finished.</p>
+     *
+     * <p>The default implementation simply invokes {@link #onCancelled()} and
+     * ignores the result. If you write your own implementation, do not call
+     * <code>super.onCancelled(result)</code>.</p>
+     *
+     * @param result The result, if any, computed in
+     *               {@link #doInBackground(Object[])}, can be null
+     *
+     * @see #cancel(boolean)
+     * @see #isCancelled()
+     */
+    @SuppressWarnings({"UnusedParameters"})
+    protected void onCancelled(Result result) {
+        onCancelled();
+    }
+
+    /**
+     * <p>Applications should preferably override {@link #onCancelled(Object)}.
+     * This method is invoked by the default implementation of
+     * {@link #onCancelled(Object)}.</p>
+     *
+     * <p>Runs on the UI thread after {@link #cancel(boolean)} is invoked and
+     * {@link #doInBackground(Object[])} has finished.</p>
+     *
+     * @see #onCancelled(Object)
+     * @see #cancel(boolean)
+     * @see #isCancelled()
+     */
+    protected void onCancelled() {
+    }
+
+    /**
+     * Returns <tt>true</tt> if this task was cancelled before it completed
+     * normally. If you are calling {@link #cancel(boolean)} on the task,
+     * the value returned by this method should be checked periodically from
+     * {@link #doInBackground(Object[])} to end the task as soon as possible.
+     *
+     * @return <tt>true</tt> if task was cancelled before it completed
+     *
+     * @see #cancel(boolean)
+     */
+    public final boolean isCancelled() {
+        return mCancelled.get();
+    }
+
+    /**
+     * <p>Attempts to cancel execution of this task.  This attempt will
+     * fail if the task has already completed, already been cancelled,
+     * or could not be cancelled for some other reason. If successful,
+     * and this task has not started when <tt>cancel</tt> is called,
+     * this task should never run. If the task has already started,
+     * then the <tt>mayInterruptIfRunning</tt> parameter determines
+     * whether the thread executing this task should be interrupted in
+     * an attempt to stop the task.</p>
+     *
+     * <p>Calling this method will result in {@link #onCancelled(Object)} being
+     * invoked on the UI thread after {@link #doInBackground(Object[])}
+     * returns. Calling this method guarantees that {@link #onPostExecute(Object)}
+     * is never invoked. After invoking this method, you should check the
+     * value returned by {@link #isCancelled()} periodically from
+     * {@link #doInBackground(Object[])} to finish the task as early as
+     * possible.</p>
+     *
+     * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
+     *        task should be interrupted; otherwise, in-progress tasks are allowed
+     *        to complete.
+     *
+     * @return <tt>false</tt> if the task could not be cancelled,
+     *         typically because it has already completed normally;
+     *         <tt>true</tt> otherwise
+     *
+     * @see #isCancelled()
+     * @see #onCancelled(Object)
+     */
+    public final boolean cancel(boolean mayInterruptIfRunning) {
+        mCancelled.set(true);
+        return mFuture.cancel(mayInterruptIfRunning);
+    }
+
+    /**
+     * Waits if necessary for the computation to complete, and then
+     * retrieves its result.
+     *
+     * @return The computed result.
+     *
+     * @throws CancellationException If the computation was cancelled.
+     * @throws ExecutionException If the computation threw an exception.
+     * @throws InterruptedException If the current thread was interrupted
+     *         while waiting.
+     */
+    public final Result get() throws InterruptedException, ExecutionException {
+        return mFuture.get();
+    }
+
+    /**
+     * Waits if necessary for at most the given time for the computation
+     * to complete, and then retrieves its result.
+     *
+     * @param timeout Time to wait before cancelling the operation.
+     * @param unit The time unit for the timeout.
+     *
+     * @return The computed result.
+     *
+     * @throws CancellationException If the computation was cancelled.
+     * @throws ExecutionException If the computation threw an exception.
+     * @throws InterruptedException If the current thread was interrupted
+     *         while waiting.
+     * @throws TimeoutException If the wait timed out.
+     */
+    public final Result get(long timeout, TimeUnit unit) throws InterruptedException,
+            ExecutionException, TimeoutException {
+        return mFuture.get(timeout, unit);
+    }
+
+    /**
+     * Executes the task with the specified parameters. The task returns
+     * itself (this) so that the caller can keep a reference to it.
+     *
+     * <p>Note: this function schedules the task on a queue for a single background
+     * thread or pool of threads depending on the platform version.  When first
+     * introduced, AsyncTasks were executed serially on a single background thread.
+     * Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed
+     * to a pool of threads allowing multiple tasks to operate in parallel. Starting
+     * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are back to being
+     * executed on a single thread to avoid common application errors caused
+     * by parallel execution.  If you truly want parallel execution, you can use
+     * the {@link #executeOnExecutor} version of this method
+     * with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings
+     * on its use.
+     *
+     * <p>This method must be invoked on the UI thread.
+     *
+     * @param params The parameters of the task.
+     *
+     * @return This instance of AsyncTask.
+     *
+     * @throws IllegalStateException If {@link #getStatus()} returns either
+     *         {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
+     *
+     * @see #executeOnExecutor(java.util.concurrent.Executor, Object[])
+     * @see #execute(Runnable)
+     */
+    public final AsyncTask<Params, Progress, Result> execute(Params... params) {
+        return executeOnExecutor(sDefaultExecutor, params);
+    }
+
+    /**
+     * Executes the task with the specified parameters. The task returns
+     * itself (this) so that the caller can keep a reference to it.
+     *
+     * <p>This method is typically used with {@link #THREAD_POOL_EXECUTOR} to
+     * allow multiple tasks to run in parallel on a pool of threads managed by
+     * AsyncTask, however you can also use your own {@link Executor} for custom
+     * behavior.
+     *
+     * <p><em>Warning:</em> Allowing multiple tasks to run in parallel from
+     * a thread pool is generally <em>not</em> what one wants, because the order
+     * of their operation is not defined.  For example, if these tasks are used
+     * to modify any state in common (such as writing a file due to a button click),
+     * there are no guarantees on the order of the modifications.
+     * Without careful work it is possible in rare cases for the newer version
+     * of the data to be over-written by an older one, leading to obscure data
+     * loss and stability issues.  Such changes are best
+     * executed in serial; to guarantee such work is serialized regardless of
+     * platform version you can use this function with {@link #SERIAL_EXECUTOR}.
+     *
+     * <p>This method must be invoked on the UI thread.
+     *
+     * @param exec The executor to use.  {@link #THREAD_POOL_EXECUTOR} is available as a
+     *              convenient process-wide thread pool for tasks that are loosely coupled.
+     * @param params The parameters of the task.
+     *
+     * @return This instance of AsyncTask.
+     *
+     * @throws IllegalStateException If {@link #getStatus()} returns either
+     *         {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
+     *
+     * @see #execute(Object[])
+     */
+    public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
+            Params... params) {
+        if (mStatus != Status.PENDING) {
+            switch (mStatus) {
+                case RUNNING:
+                    throw new IllegalStateException("Cannot execute task:"
+                            + " the task is already running.");
+                case FINISHED:
+                    throw new IllegalStateException("Cannot execute task:"
+                            + " the task has already been executed "
+                            + "(a task can be executed only once)");
+            }
+        }
+
+        mStatus = Status.RUNNING;
+
+        onPreExecute();
+
+        mWorker.mParams = params;
+        exec.execute(mFuture);
+
+        return this;
+    }
+
+    /**
+     * Convenience version of {@link #execute(Object...)} for use with
+     * a simple Runnable object. See {@link #execute(Object[])} for more
+     * information on the order of execution.
+     *
+     * @see #execute(Object[])
+     * @see #executeOnExecutor(java.util.concurrent.Executor, Object[])
+     */
+    public static void execute(Runnable runnable) {
+        sDefaultExecutor.execute(runnable);
+    }
+
+    /**
+     * This method can be invoked from {@link #doInBackground} to
+     * publish updates on the UI thread while the background computation is
+     * still running. Each call to this method will trigger the execution of
+     * {@link #onProgressUpdate} on the UI thread.
+     *
+     * {@link #onProgressUpdate} will note be called if the task has been
+     * canceled.
+     *
+     * @param values The progress values to update the UI with.
+     *
+     * @see #onProgressUpdate
+     * @see #doInBackground
+     */
+    protected final void publishProgress(Progress... values) {
+        if (!isCancelled()) {
+            sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
+                    new AsyncTaskResult<Progress>(this, values)).sendToTarget();
+        }
+    }
+
+    private void finish(Result result) {
+        if (isCancelled()) {
+            onCancelled(result);
+        } else {
+            onPostExecute(result);
+        }
+        mStatus = Status.FINISHED;
+    }
+
+    private static class InternalHandler extends Handler {
+        @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
+        @Override
+        public void handleMessage(Message msg) {
+            AsyncTaskResult result = (AsyncTaskResult) msg.obj;
+            switch (msg.what) {
+                case MESSAGE_POST_RESULT:
+                    // There is only one result
+                    result.mTask.finish(result.mData[0]);
+                    break;
+                case MESSAGE_POST_PROGRESS:
+                    result.mTask.onProgressUpdate(result.mData);
+                    break;
+            }
+        }
+    }
+
+    private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
+        Params[] mParams;
+    }
+
+    @SuppressWarnings({"RawUseOfParameterizedType"})
+    private static class AsyncTaskResult<Data> {
+        final AsyncTask mTask;
+        final Data[] mData;
+
+        AsyncTaskResult(AsyncTask task, Data... data) {
+            mTask = task;
+            mData = data;
+        }
+    }
+}
\ No newline at end of file
diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/DiskLruCache.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/DiskLruCache.java
index a9f2166..26cdbd7 100644
--- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/DiskLruCache.java
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/DiskLruCache.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012 The Android Open Source Project
+ * Copyright (C) 2011 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,321 +16,938 @@
 
 package com.example.android.bitmapfun.util;
 
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.CompressFormat;
-import android.graphics.BitmapFactory;
-import android.os.Environment;
-import android.util.Log;
-
-import com.example.android.bitmapfun.BuildConfig;
-
-import java.io.BufferedOutputStream;
+import java.io.BufferedInputStream;
+import java.io.BufferedWriter;
+import java.io.Closeable;
+import java.io.EOFException;
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
-import java.io.FilenameFilter;
+import java.io.FileWriter;
+import java.io.FilterOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.util.Collections;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.Array;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.Map;
-import java.util.Map.Entry;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
 
 /**
- * A simple disk LRU bitmap cache to illustrate how a disk cache would be used for bitmap caching. A
- * much more robust and efficient disk LRU cache solution can be found in the ICS source code
- * (libcore/luni/src/main/java/libcore/io/DiskLruCache.java) and is preferable to this simple
- * implementation.
+ ******************************************************************************
+ * Taken from the JB source code, can be found in:
+ * libcore/luni/src/main/java/libcore/io/DiskLruCache.java
+ * or direct link:
+ * https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java
+ ******************************************************************************
+ *
+ * A cache that uses a bounded amount of space on a filesystem. Each cache
+ * entry has a string key and a fixed number of values. Values are byte
+ * sequences, accessible as streams or files. Each value must be between {@code
+ * 0} and {@code Integer.MAX_VALUE} bytes in length.
+ *
+ * <p>The cache stores its data in a directory on the filesystem. This
+ * directory must be exclusive to the cache; the cache may delete or overwrite
+ * files from its directory. It is an error for multiple processes to use the
+ * same cache directory at the same time.
+ *
+ * <p>This cache limits the number of bytes that it will store on the
+ * filesystem. When the number of stored bytes exceeds the limit, the cache will
+ * remove entries in the background until the limit is satisfied. The limit is
+ * not strict: the cache may temporarily exceed it while waiting for files to be
+ * deleted. The limit does not include filesystem overhead or the cache
+ * journal so space-sensitive applications should set a conservative limit.
+ *
+ * <p>Clients call {@link #edit} to create or update the values of an entry. An
+ * entry may have only one editor at one time; if a value is not available to be
+ * edited then {@link #edit} will return null.
+ * <ul>
+ *     <li>When an entry is being <strong>created</strong> it is necessary to
+ *         supply a full set of values; the empty value should be used as a
+ *         placeholder if necessary.
+ *     <li>When an entry is being <strong>edited</strong>, it is not necessary
+ *         to supply data for every value; values default to their previous
+ *         value.
+ * </ul>
+ * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
+ * or {@link Editor#abort}. Committing is atomic: a read observes the full set
+ * of values as they were before or after the commit, but never a mix of values.
+ *
+ * <p>Clients call {@link #get} to read a snapshot of an entry. The read will
+ * observe the value at the time that {@link #get} was called. Updates and
+ * removals after the call do not impact ongoing reads.
+ *
+ * <p>This class is tolerant of some I/O errors. If files are missing from the
+ * filesystem, the corresponding entries will be dropped from the cache. If
+ * an error occurs while writing a cache value, the edit will fail silently.
+ * Callers should handle other problems by catching {@code IOException} and
+ * responding appropriately.
  */
-public class DiskLruCache {
-    private static final String TAG = "DiskLruCache";
-    private static final String CACHE_FILENAME_PREFIX = "cache_";
-    private static final int MAX_REMOVALS = 4;
-    private static final int INITIAL_CAPACITY = 32;
-    private static final float LOAD_FACTOR = 0.75f;
+public final class DiskLruCache implements Closeable {
+    static final String JOURNAL_FILE = "journal";
+    static final String JOURNAL_FILE_TMP = "journal.tmp";
+    static final String MAGIC = "libcore.io.DiskLruCache";
+    static final String VERSION_1 = "1";
+    static final long ANY_SEQUENCE_NUMBER = -1;
+    private static final String CLEAN = "CLEAN";
+    private static final String DIRTY = "DIRTY";
+    private static final String REMOVE = "REMOVE";
+    private static final String READ = "READ";
 
-    private final File mCacheDir;
-    private int cacheSize = 0;
-    private int cacheByteSize = 0;
-    private final int maxCacheItemSize = 64; // 64 item default
-    private long maxCacheByteSize = 1024 * 1024 * 5; // 5MB default
-    private CompressFormat mCompressFormat = CompressFormat.JPEG;
-    private int mCompressQuality = 70;
+    private static final Charset UTF_8 = Charset.forName("UTF-8");
+    private static final int IO_BUFFER_SIZE = 8 * 1024;
 
-    private final Map<String, String> mLinkedHashMap =
-            Collections.synchronizedMap(new LinkedHashMap<String, String>(
-                    INITIAL_CAPACITY, LOAD_FACTOR, true));
-
-    /**
-     * A filename filter to use to identify the cache filenames which have CACHE_FILENAME_PREFIX
-     * prepended.
-     */
-    private static final FilenameFilter cacheFileFilter = new FilenameFilter() {
-        @Override
-        public boolean accept(File dir, String filename) {
-            return filename.startsWith(CACHE_FILENAME_PREFIX);
-        }
-    };
-
-    /**
-     * Used to fetch an instance of DiskLruCache.
+    /*
+     * This cache uses a journal file named "journal". A typical journal file
+     * looks like this:
+     *     libcore.io.DiskLruCache
+     *     1
+     *     100
+     *     2
      *
-     * @param context
-     * @param cacheDir
-     * @param maxByteSize
-     * @return
+     *     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
+     *     DIRTY 335c4c6028171cfddfbaae1a9c313c52
+     *     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
+     *     REMOVE 335c4c6028171cfddfbaae1a9c313c52
+     *     DIRTY 1ab96a171faeeee38496d8b330771a7a
+     *     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
+     *     READ 335c4c6028171cfddfbaae1a9c313c52
+     *     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
+     *
+     * The first five lines of the journal form its header. They are the
+     * constant string "libcore.io.DiskLruCache", the disk cache's version,
+     * the application's version, the value count, and a blank line.
+     *
+     * Each of the subsequent lines in the file is a record of the state of a
+     * cache entry. Each line contains space-separated values: a state, a key,
+     * and optional state-specific values.
+     *   o DIRTY lines track that an entry is actively being created or updated.
+     *     Every successful DIRTY action should be followed by a CLEAN or REMOVE
+     *     action. DIRTY lines without a matching CLEAN or REMOVE indicate that
+     *     temporary files may need to be deleted.
+     *   o CLEAN lines track a cache entry that has been successfully published
+     *     and may be read. A publish line is followed by the lengths of each of
+     *     its values.
+     *   o READ lines track accesses for LRU.
+     *   o REMOVE lines track entries that have been deleted.
+     *
+     * The journal file is appended to as cache operations occur. The journal may
+     * occasionally be compacted by dropping redundant lines. A temporary file named
+     * "journal.tmp" will be used during compaction; that file should be deleted if
+     * it exists when the cache is opened.
      */
-    public static DiskLruCache openCache(Context context, File cacheDir, long maxByteSize) {
-        if (!cacheDir.exists()) {
-            cacheDir.mkdir();
-        }
 
-        if (cacheDir.isDirectory() && cacheDir.canWrite()
-                && Utils.getUsableSpace(cacheDir) > maxByteSize) {
-            return new DiskLruCache(cacheDir, maxByteSize);
-        }
+    private final File directory;
+    private final File journalFile;
+    private final File journalFileTmp;
+    private final int appVersion;
+    private final long maxSize;
+    private final int valueCount;
+    private long size = 0;
+    private Writer journalWriter;
+    private final LinkedHashMap<String, Entry> lruEntries
+            = new LinkedHashMap<String, Entry>(0, 0.75f, true);
+    private int redundantOpCount;
 
-        return null;
+    /**
+     * To differentiate between old and current snapshots, each entry is given
+     * a sequence number each time an edit is committed. A snapshot is stale if
+     * its sequence number is not equal to its entry's sequence number.
+     */
+    private long nextSequenceNumber = 0;
+
+    /* From java.util.Arrays */
+    @SuppressWarnings("unchecked")
+    private static <T> T[] copyOfRange(T[] original, int start, int end) {
+        final int originalLength = original.length; // For exception priority compatibility.
+        if (start > end) {
+            throw new IllegalArgumentException();
+        }
+        if (start < 0 || start > originalLength) {
+            throw new ArrayIndexOutOfBoundsException();
+        }
+        final int resultLength = end - start;
+        final int copyLength = Math.min(resultLength, originalLength - start);
+        final T[] result = (T[]) Array
+                .newInstance(original.getClass().getComponentType(), resultLength);
+        System.arraycopy(original, start, result, 0, copyLength);
+        return result;
     }
 
     /**
-     * Constructor that should not be called directly, instead use
-     * {@link DiskLruCache#openCache(Context, File, long)} which runs some extra checks before
-     * creating a DiskLruCache instance.
-     *
-     * @param cacheDir
-     * @param maxByteSize
+     * Returns the remainder of 'reader' as a string, closing it when done.
      */
-    private DiskLruCache(File cacheDir, long maxByteSize) {
-        mCacheDir = cacheDir;
-        maxCacheByteSize = maxByteSize;
-    }
-
-    /**
-     * Add a bitmap to the disk cache.
-     *
-     * @param key A unique identifier for the bitmap.
-     * @param data The bitmap to store.
-     */
-    public void put(String key, Bitmap data) {
-        synchronized (mLinkedHashMap) {
-            if (mLinkedHashMap.get(key) == null) {
-                try {
-                    final String file = createFilePath(mCacheDir, key);
-                    if (writeBitmapToFile(data, file)) {
-                        put(key, file);
-                        flushCache();
-                    }
-                } catch (final FileNotFoundException e) {
-                    Log.e(TAG, "Error in put: " + e.getMessage());
-                } catch (final IOException e) {
-                    Log.e(TAG, "Error in put: " + e.getMessage());
-                }
+    public static String readFully(Reader reader) throws IOException {
+        try {
+            StringWriter writer = new StringWriter();
+            char[] buffer = new char[1024];
+            int count;
+            while ((count = reader.read(buffer)) != -1) {
+                writer.write(buffer, 0, count);
             }
+            return writer.toString();
+        } finally {
+            reader.close();
         }
     }
 
-    private void put(String key, String file) {
-        mLinkedHashMap.put(key, file);
-        cacheSize = mLinkedHashMap.size();
-        cacheByteSize += new File(file).length();
+    /**
+     * Returns the ASCII characters up to but not including the next "\r\n", or
+     * "\n".
+     *
+     * @throws java.io.EOFException if the stream is exhausted before the next newline
+     *     character.
+     */
+    public static String readAsciiLine(InputStream in) throws IOException {
+        // TODO: support UTF-8 here instead
+
+        StringBuilder result = new StringBuilder(80);
+        while (true) {
+            int c = in.read();
+            if (c == -1) {
+                throw new EOFException();
+            } else if (c == '\n') {
+                break;
+            }
+
+            result.append((char) c);
+        }
+        int length = result.length();
+        if (length > 0 && result.charAt(length - 1) == '\r') {
+            result.setLength(length - 1);
+        }
+        return result.toString();
     }
 
     /**
-     * Flush the cache, removing oldest entries if the total size is over the specified cache size.
-     * Note that this isn't keeping track of stale files in the cache directory that aren't in the
-     * HashMap. If the images and keys in the disk cache change often then they probably won't ever
-     * be removed.
+     * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null.
      */
-    private void flushCache() {
-        Entry<String, String> eldestEntry;
-        File eldestFile;
-        long eldestFileSize;
-        int count = 0;
-
-        while (count < MAX_REMOVALS &&
-                (cacheSize > maxCacheItemSize || cacheByteSize > maxCacheByteSize)) {
-            eldestEntry = mLinkedHashMap.entrySet().iterator().next();
-            eldestFile = new File(eldestEntry.getValue());
-            eldestFileSize = eldestFile.length();
-            mLinkedHashMap.remove(eldestEntry.getKey());
-            eldestFile.delete();
-            cacheSize = mLinkedHashMap.size();
-            cacheByteSize -= eldestFileSize;
-            count++;
-            if (BuildConfig.DEBUG) {
-                Log.d(TAG, "flushCache - Removed cache file, " + eldestFile + ", "
-                        + eldestFileSize);
+    public static void closeQuietly(Closeable closeable) {
+        if (closeable != null) {
+            try {
+                closeable.close();
+            } catch (RuntimeException rethrown) {
+                throw rethrown;
+            } catch (Exception ignored) {
             }
         }
     }
 
     /**
-     * Get an image from the disk cache.
-     *
-     * @param key The unique identifier for the bitmap
-     * @return The bitmap or null if not found
+     * Recursively delete everything in {@code dir}.
      */
-    public Bitmap get(String key) {
-        synchronized (mLinkedHashMap) {
-            final String file = mLinkedHashMap.get(key);
-            if (file != null) {
-                if (BuildConfig.DEBUG) {
-                    Log.d(TAG, "Disk cache hit");
+    // TODO: this should specify paths as Strings rather than as Files
+    public static void deleteContents(File dir) throws IOException {
+        File[] files = dir.listFiles();
+        if (files == null) {
+            throw new IllegalArgumentException("not a directory: " + dir);
+        }
+        for (File file : files) {
+            if (file.isDirectory()) {
+                deleteContents(file);
+            }
+            if (!file.delete()) {
+                throw new IOException("failed to delete file: " + file);
+            }
+        }
+    }
+
+    /** This cache uses a single background thread to evict entries. */
+    private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
+            60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+    private final Callable<Void> cleanupCallable = new Callable<Void>() {
+        @Override public Void call() throws Exception {
+            synchronized (DiskLruCache.this) {
+                if (journalWriter == null) {
+                    return null; // closed
                 }
-                return BitmapFactory.decodeFile(file);
-            } else {
-                final String existingFile = createFilePath(mCacheDir, key);
-                if (new File(existingFile).exists()) {
-                    put(key, existingFile);
-                    if (BuildConfig.DEBUG) {
-                        Log.d(TAG, "Disk cache hit (existing file)");
-                    }
-                    return BitmapFactory.decodeFile(existingFile);
+                trimToSize();
+                if (journalRebuildRequired()) {
+                    rebuildJournal();
+                    redundantOpCount = 0;
                 }
             }
             return null;
         }
+    };
+
+    private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
+        this.directory = directory;
+        this.appVersion = appVersion;
+        this.journalFile = new File(directory, JOURNAL_FILE);
+        this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
+        this.valueCount = valueCount;
+        this.maxSize = maxSize;
     }
 
     /**
-     * Checks if a specific key exist in the cache.
+     * Opens the cache in {@code directory}, creating a cache if none exists
+     * there.
      *
-     * @param key The unique identifier for the bitmap
-     * @return true if found, false otherwise
+     * @param directory a writable directory
+     * @param appVersion
+     * @param valueCount the number of values per cache entry. Must be positive.
+     * @param maxSize the maximum number of bytes this cache should use to store
+     * @throws IOException if reading or writing the cache directory fails
      */
-    public boolean containsKey(String key) {
-        // See if the key is in our HashMap
-        if (mLinkedHashMap.containsKey(key)) {
-            return true;
+    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
+            throws IOException {
+        if (maxSize <= 0) {
+            throw new IllegalArgumentException("maxSize <= 0");
+        }
+        if (valueCount <= 0) {
+            throw new IllegalArgumentException("valueCount <= 0");
         }
 
-        // Now check if there's an actual file that exists based on the key
-        final String existingFile = createFilePath(mCacheDir, key);
-        if (new File(existingFile).exists()) {
-            // File found, add it to the HashMap for future use
-            put(key, existingFile);
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * Removes all disk cache entries from this instance cache dir
-     */
-    public void clearCache() {
-        DiskLruCache.clearCache(mCacheDir);
-    }
-
-    /**
-     * Removes all disk cache entries from the application cache directory in the uniqueName
-     * sub-directory.
-     *
-     * @param context The context to use
-     * @param uniqueName A unique cache directory name to append to the app cache directory
-     */
-    public static void clearCache(Context context, String uniqueName) {
-        File cacheDir = getDiskCacheDir(context, uniqueName);
-        clearCache(cacheDir);
-    }
-
-    /**
-     * Removes all disk cache entries from the given directory. This should not be called directly,
-     * call {@link DiskLruCache#clearCache(Context, String)} or {@link DiskLruCache#clearCache()}
-     * instead.
-     *
-     * @param cacheDir The directory to remove the cache files from
-     */
-    private static void clearCache(File cacheDir) {
-        final File[] files = cacheDir.listFiles(cacheFileFilter);
-        for (int i=0; i<files.length; i++) {
-            files[i].delete();
-        }
-    }
-
-    /**
-     * Get a usable cache directory (external if available, internal otherwise).
-     *
-     * @param context The context to use
-     * @param uniqueName A unique directory name to append to the cache dir
-     * @return The cache dir
-     */
-    public static File getDiskCacheDir(Context context, String uniqueName) {
-
-        // Check if media is mounted or storage is built-in, if so, try and use external cache dir
-        // otherwise use internal cache dir
-        final String cachePath =
-                Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED ||
-                        !Utils.isExternalStorageRemovable() ?
-                        Utils.getExternalCacheDir(context).getPath() :
-                        context.getCacheDir().getPath();
-
-        return new File(cachePath + File.separator + uniqueName);
-    }
-
-    /**
-     * Creates a constant cache file path given a target cache directory and an image key.
-     *
-     * @param cacheDir
-     * @param key
-     * @return
-     */
-    public static String createFilePath(File cacheDir, String key) {
-        try {
-            // Use URLEncoder to ensure we have a valid filename, a tad hacky but it will do for
-            // this example
-            return cacheDir.getAbsolutePath() + File.separator +
-                    CACHE_FILENAME_PREFIX + URLEncoder.encode(key.replace("*", ""), "UTF-8");
-        } catch (final UnsupportedEncodingException e) {
-            Log.e(TAG, "createFilePath - " + e);
-        }
-
-        return null;
-    }
-
-    /**
-     * Create a constant cache file path using the current cache directory and an image key.
-     *
-     * @param key
-     * @return
-     */
-    public String createFilePath(String key) {
-        return createFilePath(mCacheDir, key);
-    }
-
-    /**
-     * Sets the target compression format and quality for images written to the disk cache.
-     *
-     * @param compressFormat
-     * @param quality
-     */
-    public void setCompressParams(CompressFormat compressFormat, int quality) {
-        mCompressFormat = compressFormat;
-        mCompressQuality = quality;
-    }
-
-    /**
-     * Writes a bitmap to a file. Call {@link DiskLruCache#setCompressParams(CompressFormat, int)}
-     * first to set the target bitmap compression and format.
-     *
-     * @param bitmap
-     * @param file
-     * @return
-     */
-    private boolean writeBitmapToFile(Bitmap bitmap, String file)
-            throws IOException, FileNotFoundException {
-
-        OutputStream out = null;
-        try {
-            out = new BufferedOutputStream(new FileOutputStream(file), Utils.IO_BUFFER_SIZE);
-            return bitmap.compress(mCompressFormat, mCompressQuality, out);
-        } finally {
-            if (out != null) {
-                out.close();
+        // prefer to pick up where we left off
+        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
+        if (cache.journalFile.exists()) {
+            try {
+                cache.readJournal();
+                cache.processJournal();
+                cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
+                        IO_BUFFER_SIZE);
+                return cache;
+            } catch (IOException journalIsCorrupt) {
+//                System.logW("DiskLruCache " + directory + " is corrupt: "
+//                        + journalIsCorrupt.getMessage() + ", removing");
+                cache.delete();
             }
         }
+
+        // create a new empty cache
+        directory.mkdirs();
+        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
+        cache.rebuildJournal();
+        return cache;
+    }
+
+    private void readJournal() throws IOException {
+        InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);
+        try {
+            String magic = readAsciiLine(in);
+            String version = readAsciiLine(in);
+            String appVersionString = readAsciiLine(in);
+            String valueCountString = readAsciiLine(in);
+            String blank = readAsciiLine(in);
+            if (!MAGIC.equals(magic)
+                    || !VERSION_1.equals(version)
+                    || !Integer.toString(appVersion).equals(appVersionString)
+                    || !Integer.toString(valueCount).equals(valueCountString)
+                    || !"".equals(blank)) {
+                throw new IOException("unexpected journal header: ["
+                        + magic + ", " + version + ", " + valueCountString + ", " + blank + "]");
+            }
+
+            while (true) {
+                try {
+                    readJournalLine(readAsciiLine(in));
+                } catch (EOFException endOfJournal) {
+                    break;
+                }
+            }
+        } finally {
+            closeQuietly(in);
+        }
+    }
+
+    private void readJournalLine(String line) throws IOException {
+        String[] parts = line.split(" ");
+        if (parts.length < 2) {
+            throw new IOException("unexpected journal line: " + line);
+        }
+
+        String key = parts[1];
+        if (parts[0].equals(REMOVE) && parts.length == 2) {
+            lruEntries.remove(key);
+            return;
+        }
+
+        Entry entry = lruEntries.get(key);
+        if (entry == null) {
+            entry = new Entry(key);
+            lruEntries.put(key, entry);
+        }
+
+        if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
+            entry.readable = true;
+            entry.currentEditor = null;
+            entry.setLengths(copyOfRange(parts, 2, parts.length));
+        } else if (parts[0].equals(DIRTY) && parts.length == 2) {
+            entry.currentEditor = new Editor(entry);
+        } else if (parts[0].equals(READ) && parts.length == 2) {
+            // this work was already done by calling lruEntries.get()
+        } else {
+            throw new IOException("unexpected journal line: " + line);
+        }
+    }
+
+    /**
+     * Computes the initial size and collects garbage as a part of opening the
+     * cache. Dirty entries are assumed to be inconsistent and will be deleted.
+     */
+    private void processJournal() throws IOException {
+        deleteIfExists(journalFileTmp);
+        for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
+            Entry entry = i.next();
+            if (entry.currentEditor == null) {
+                for (int t = 0; t < valueCount; t++) {
+                    size += entry.lengths[t];
+                }
+            } else {
+                entry.currentEditor = null;
+                for (int t = 0; t < valueCount; t++) {
+                    deleteIfExists(entry.getCleanFile(t));
+                    deleteIfExists(entry.getDirtyFile(t));
+                }
+                i.remove();
+            }
+        }
+    }
+
+    /**
+     * Creates a new journal that omits redundant information. This replaces the
+     * current journal if it exists.
+     */
+    private synchronized void rebuildJournal() throws IOException {
+        if (journalWriter != null) {
+            journalWriter.close();
+        }
+
+        Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
+        writer.write(MAGIC);
+        writer.write("\n");
+        writer.write(VERSION_1);
+        writer.write("\n");
+        writer.write(Integer.toString(appVersion));
+        writer.write("\n");
+        writer.write(Integer.toString(valueCount));
+        writer.write("\n");
+        writer.write("\n");
+
+        for (Entry entry : lruEntries.values()) {
+            if (entry.currentEditor != null) {
+                writer.write(DIRTY + ' ' + entry.key + '\n');
+            } else {
+                writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+            }
+        }
+
+        writer.close();
+        journalFileTmp.renameTo(journalFile);
+        journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
+    }
+
+    private static void deleteIfExists(File file) throws IOException {
+//        try {
+//            Libcore.os.remove(file.getPath());
+//        } catch (ErrnoException errnoException) {
+//            if (errnoException.errno != OsConstants.ENOENT) {
+//                throw errnoException.rethrowAsIOException();
+//            }
+//        }
+        if (file.exists() && !file.delete()) {
+            throw new IOException();
+        }
+    }
+
+    /**
+     * Returns a snapshot of the entry named {@code key}, or null if it doesn't
+     * exist is not currently readable. If a value is returned, it is moved to
+     * the head of the LRU queue.
+     */
+    public synchronized Snapshot get(String key) throws IOException {
+        checkNotClosed();
+        validateKey(key);
+        Entry entry = lruEntries.get(key);
+        if (entry == null) {
+            return null;
+        }
+
+        if (!entry.readable) {
+            return null;
+        }
+
+        /*
+         * Open all streams eagerly to guarantee that we see a single published
+         * snapshot. If we opened streams lazily then the streams could come
+         * from different edits.
+         */
+        InputStream[] ins = new InputStream[valueCount];
+        try {
+            for (int i = 0; i < valueCount; i++) {
+                ins[i] = new FileInputStream(entry.getCleanFile(i));
+            }
+        } catch (FileNotFoundException e) {
+            // a file must have been deleted manually!
+            return null;
+        }
+
+        redundantOpCount++;
+        journalWriter.append(READ + ' ' + key + '\n');
+        if (journalRebuildRequired()) {
+            executorService.submit(cleanupCallable);
+        }
+
+        return new Snapshot(key, entry.sequenceNumber, ins);
+    }
+
+    /**
+     * Returns an editor for the entry named {@code key}, or null if another
+     * edit is in progress.
+     */
+    public Editor edit(String key) throws IOException {
+        return edit(key, ANY_SEQUENCE_NUMBER);
+    }
+
+    private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
+        checkNotClosed();
+        validateKey(key);
+        Entry entry = lruEntries.get(key);
+        if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
+                && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
+            return null; // snapshot is stale
+        }
+        if (entry == null) {
+            entry = new Entry(key);
+            lruEntries.put(key, entry);
+        } else if (entry.currentEditor != null) {
+            return null; // another edit is in progress
+        }
+
+        Editor editor = new Editor(entry);
+        entry.currentEditor = editor;
+
+        // flush the journal before creating files to prevent file leaks
+        journalWriter.write(DIRTY + ' ' + key + '\n');
+        journalWriter.flush();
+        return editor;
+    }
+
+    /**
+     * Returns the directory where this cache stores its data.
+     */
+    public File getDirectory() {
+        return directory;
+    }
+
+    /**
+     * Returns the maximum number of bytes that this cache should use to store
+     * its data.
+     */
+    public long maxSize() {
+        return maxSize;
+    }
+
+    /**
+     * Returns the number of bytes currently being used to store the values in
+     * this cache. This may be greater than the max size if a background
+     * deletion is pending.
+     */
+    public synchronized long size() {
+        return size;
+    }
+
+    private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
+        Entry entry = editor.entry;
+        if (entry.currentEditor != editor) {
+            throw new IllegalStateException();
+        }
+
+        // if this edit is creating the entry for the first time, every index must have a value
+        if (success && !entry.readable) {
+            for (int i = 0; i < valueCount; i++) {
+                if (!entry.getDirtyFile(i).exists()) {
+                    editor.abort();
+                    throw new IllegalStateException("edit didn't create file " + i);
+                }
+            }
+        }
+
+        for (int i = 0; i < valueCount; i++) {
+            File dirty = entry.getDirtyFile(i);
+            if (success) {
+                if (dirty.exists()) {
+                    File clean = entry.getCleanFile(i);
+                    dirty.renameTo(clean);
+                    long oldLength = entry.lengths[i];
+                    long newLength = clean.length();
+                    entry.lengths[i] = newLength;
+                    size = size - oldLength + newLength;
+                }
+            } else {
+                deleteIfExists(dirty);
+            }
+        }
+
+        redundantOpCount++;
+        entry.currentEditor = null;
+        if (entry.readable | success) {
+            entry.readable = true;
+            journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+            if (success) {
+                entry.sequenceNumber = nextSequenceNumber++;
+            }
+        } else {
+            lruEntries.remove(entry.key);
+            journalWriter.write(REMOVE + ' ' + entry.key + '\n');
+        }
+
+        if (size > maxSize || journalRebuildRequired()) {
+            executorService.submit(cleanupCallable);
+        }
+    }
+
+    /**
+     * We only rebuild the journal when it will halve the size of the journal
+     * and eliminate at least 2000 ops.
+     */
+    private boolean journalRebuildRequired() {
+        final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;
+        return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD
+                && redundantOpCount >= lruEntries.size();
+    }
+
+    /**
+     * Drops the entry for {@code key} if it exists and can be removed. Entries
+     * actively being edited cannot be removed.
+     *
+     * @return true if an entry was removed.
+     */
+    public synchronized boolean remove(String key) throws IOException {
+        checkNotClosed();
+        validateKey(key);
+        Entry entry = lruEntries.get(key);
+        if (entry == null || entry.currentEditor != null) {
+            return false;
+        }
+
+        for (int i = 0; i < valueCount; i++) {
+            File file = entry.getCleanFile(i);
+            if (!file.delete()) {
+                throw new IOException("failed to delete " + file);
+            }
+            size -= entry.lengths[i];
+            entry.lengths[i] = 0;
+        }
+
+        redundantOpCount++;
+        journalWriter.append(REMOVE + ' ' + key + '\n');
+        lruEntries.remove(key);
+
+        if (journalRebuildRequired()) {
+            executorService.submit(cleanupCallable);
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns true if this cache has been closed.
+     */
+    public boolean isClosed() {
+        return journalWriter == null;
+    }
+
+    private void checkNotClosed() {
+        if (journalWriter == null) {
+            throw new IllegalStateException("cache is closed");
+        }
+    }
+
+    /**
+     * Force buffered operations to the filesystem.
+     */
+    public synchronized void flush() throws IOException {
+        checkNotClosed();
+        trimToSize();
+        journalWriter.flush();
+    }
+
+    /**
+     * Closes this cache. Stored values will remain on the filesystem.
+     */
+    public synchronized void close() throws IOException {
+        if (journalWriter == null) {
+            return; // already closed
+        }
+        for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
+            if (entry.currentEditor != null) {
+                entry.currentEditor.abort();
+            }
+        }
+        trimToSize();
+        journalWriter.close();
+        journalWriter = null;
+    }
+
+    private void trimToSize() throws IOException {
+        while (size > maxSize) {
+//            Map.Entry<String, Entry> toEvict = lruEntries.eldest();
+            final Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
+            remove(toEvict.getKey());
+        }
+    }
+
+    /**
+     * Closes the cache and deletes all of its stored values. This will delete
+     * all files in the cache directory including files that weren't created by
+     * the cache.
+     */
+    public void delete() throws IOException {
+        close();
+        deleteContents(directory);
+    }
+
+    private void validateKey(String key) {
+        if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {
+            throw new IllegalArgumentException(
+                    "keys must not contain spaces or newlines: \"" + key + "\"");
+        }
+    }
+
+    private static String inputStreamToString(InputStream in) throws IOException {
+        return readFully(new InputStreamReader(in, UTF_8));
+    }
+
+    /**
+     * A snapshot of the values for an entry.
+     */
+    public final class Snapshot implements Closeable {
+        private final String key;
+        private final long sequenceNumber;
+        private final InputStream[] ins;
+
+        private Snapshot(String key, long sequenceNumber, InputStream[] ins) {
+            this.key = key;
+            this.sequenceNumber = sequenceNumber;
+            this.ins = ins;
+        }
+
+        /**
+         * Returns an editor for this snapshot's entry, or null if either the
+         * entry has changed since this snapshot was created or if another edit
+         * is in progress.
+         */
+        public Editor edit() throws IOException {
+            return DiskLruCache.this.edit(key, sequenceNumber);
+        }
+
+        /**
+         * Returns the unbuffered stream with the value for {@code index}.
+         */
+        public InputStream getInputStream(int index) {
+            return ins[index];
+        }
+
+        /**
+         * Returns the string value for {@code index}.
+         */
+        public String getString(int index) throws IOException {
+            return inputStreamToString(getInputStream(index));
+        }
+
+        @Override public void close() {
+            for (InputStream in : ins) {
+                closeQuietly(in);
+            }
+        }
+    }
+
+    /**
+     * Edits the values for an entry.
+     */
+    public final class Editor {
+        private final Entry entry;
+        private boolean hasErrors;
+
+        private Editor(Entry entry) {
+            this.entry = entry;
+        }
+
+        /**
+         * Returns an unbuffered input stream to read the last committed value,
+         * or null if no value has been committed.
+         */
+        public InputStream newInputStream(int index) throws IOException {
+            synchronized (DiskLruCache.this) {
+                if (entry.currentEditor != this) {
+                    throw new IllegalStateException();
+                }
+                if (!entry.readable) {
+                    return null;
+                }
+                return new FileInputStream(entry.getCleanFile(index));
+            }
+        }
+
+        /**
+         * Returns the last committed value as a string, or null if no value
+         * has been committed.
+         */
+        public String getString(int index) throws IOException {
+            InputStream in = newInputStream(index);
+            return in != null ? inputStreamToString(in) : null;
+        }
+
+        /**
+         * Returns a new unbuffered output stream to write the value at
+         * {@code index}. If the underlying output stream encounters errors
+         * when writing to the filesystem, this edit will be aborted when
+         * {@link #commit} is called. The returned output stream does not throw
+         * IOExceptions.
+         */
+        public OutputStream newOutputStream(int index) throws IOException {
+            synchronized (DiskLruCache.this) {
+                if (entry.currentEditor != this) {
+                    throw new IllegalStateException();
+                }
+                return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
+            }
+        }
+
+        /**
+         * Sets the value at {@code index} to {@code value}.
+         */
+        public void set(int index, String value) throws IOException {
+            Writer writer = null;
+            try {
+                writer = new OutputStreamWriter(newOutputStream(index), UTF_8);
+                writer.write(value);
+            } finally {
+                closeQuietly(writer);
+            }
+        }
+
+        /**
+         * Commits this edit so it is visible to readers.  This releases the
+         * edit lock so another edit may be started on the same key.
+         */
+        public void commit() throws IOException {
+            if (hasErrors) {
+                completeEdit(this, false);
+                remove(entry.key); // the previous entry is stale
+            } else {
+                completeEdit(this, true);
+            }
+        }
+
+        /**
+         * Aborts this edit. This releases the edit lock so another edit may be
+         * started on the same key.
+         */
+        public void abort() throws IOException {
+            completeEdit(this, false);
+        }
+
+        private class FaultHidingOutputStream extends FilterOutputStream {
+            private FaultHidingOutputStream(OutputStream out) {
+                super(out);
+            }
+
+            @Override public void write(int oneByte) {
+                try {
+                    out.write(oneByte);
+                } catch (IOException e) {
+                    hasErrors = true;
+                }
+            }
+
+            @Override public void write(byte[] buffer, int offset, int length) {
+                try {
+                    out.write(buffer, offset, length);
+                } catch (IOException e) {
+                    hasErrors = true;
+                }
+            }
+
+            @Override public void close() {
+                try {
+                    out.close();
+                } catch (IOException e) {
+                    hasErrors = true;
+                }
+            }
+
+            @Override public void flush() {
+                try {
+                    out.flush();
+                } catch (IOException e) {
+                    hasErrors = true;
+                }
+            }
+        }
+    }
+
+    private final class Entry {
+        private final String key;
+
+        /** Lengths of this entry's files. */
+        private final long[] lengths;
+
+        /** True if this entry has ever been published */
+        private boolean readable;
+
+        /** The ongoing edit or null if this entry is not being edited. */
+        private Editor currentEditor;
+
+        /** The sequence number of the most recently committed edit to this entry. */
+        private long sequenceNumber;
+
+        private Entry(String key) {
+            this.key = key;
+            this.lengths = new long[valueCount];
+        }
+
+        public String getLengths() throws IOException {
+            StringBuilder result = new StringBuilder();
+            for (long size : lengths) {
+                result.append(' ').append(size);
+            }
+            return result.toString();
+        }
+
+        /**
+         * Set lengths using decimal numbers like "10123".
+         */
+        private void setLengths(String[] strings) throws IOException {
+            if (strings.length != valueCount) {
+                throw invalidLengths(strings);
+            }
+
+            try {
+                for (int i = 0; i < strings.length; i++) {
+                    lengths[i] = Long.parseLong(strings[i]);
+                }
+            } catch (NumberFormatException e) {
+                throw invalidLengths(strings);
+            }
+        }
+
+        private IOException invalidLengths(String[] strings) throws IOException {
+            throw new IOException("unexpected journal line: " + Arrays.toString(strings));
+        }
+
+        public File getCleanFile(int i) {
+            return new File(directory, key + "." + i);
+        }
+
+        public File getDirtyFile(int i) {
+            return new File(directory, key + "." + i + ".tmp");
+        }
     }
 }
diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageCache.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageCache.java
index 63eaea4..6995eab 100644
--- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageCache.java
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageCache.java
@@ -16,16 +16,28 @@
 
 package com.example.android.bitmapfun.util;
 
+import android.annotation.TargetApi;
+import android.app.ActivityManager;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.CompressFormat;
-import android.support.v4.app.FragmentActivity;
+import android.graphics.BitmapFactory;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.StatFs;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
 import android.support.v4.util.LruCache;
 import android.util.Log;
 
 import com.example.android.bitmapfun.BuildConfig;
 
 import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 
 /**
  * This class holds our bitmap caches (memory and disk).
@@ -42,23 +54,27 @@
     // Compression settings when writing images to disk cache
     private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG;
     private static final int DEFAULT_COMPRESS_QUALITY = 70;
+    private static final int DISK_CACHE_INDEX = 0;
 
     // Constants to easily toggle various caches
     private static final boolean DEFAULT_MEM_CACHE_ENABLED = true;
     private static final boolean DEFAULT_DISK_CACHE_ENABLED = true;
     private static final boolean DEFAULT_CLEAR_DISK_CACHE_ON_START = false;
+    private static final boolean DEFAULT_INIT_DISK_CACHE_ON_CREATE = false;
 
-    private DiskLruCache mDiskCache;
+    private DiskLruCache mDiskLruCache;
     private LruCache<String, Bitmap> mMemoryCache;
+    private ImageCacheParams mCacheParams;
+    private final Object mDiskCacheLock = new Object();
+    private boolean mDiskCacheStarting = true;
 
     /**
      * Creating a new ImageCache object using the specified parameters.
      *
-     * @param context The context to use
      * @param cacheParams The cache parameters to use to initialize the cache
      */
-    public ImageCache(Context context, ImageCacheParams cacheParams) {
-        init(context, cacheParams);
+    public ImageCache(ImageCacheParams cacheParams) {
+        init(cacheParams);
     }
 
     /**
@@ -68,43 +84,29 @@
      * @param uniqueName A unique name that will be appended to the cache directory
      */
     public ImageCache(Context context, String uniqueName) {
-        init(context, new ImageCacheParams(uniqueName));
-    }
-
-    /**
-     * Find and return an existing ImageCache stored in a {@link RetainFragment}, if not found a new
-     * one is created with defaults and saved to a {@link RetainFragment}.
-     *
-     * @param activity The calling {@link FragmentActivity}
-     * @param uniqueName A unique name to append to the cache directory
-     * @return An existing retained ImageCache object or a new one if one did not exist.
-     */
-    public static ImageCache findOrCreateCache(
-            final FragmentActivity activity, final String uniqueName) {
-        return findOrCreateCache(activity, new ImageCacheParams(uniqueName));
+        init(new ImageCacheParams(context, uniqueName));
     }
 
     /**
      * Find and return an existing ImageCache stored in a {@link RetainFragment}, if not found a new
      * one is created using the supplied params and saved to a {@link RetainFragment}.
      *
-     * @param activity The calling {@link FragmentActivity}
+     * @param fragmentManager The fragment manager to use when dealing with the retained fragment.
      * @param cacheParams The cache parameters to use if creating the ImageCache
      * @return An existing retained ImageCache object or a new one if one did not exist
      */
     public static ImageCache findOrCreateCache(
-            final FragmentActivity activity, ImageCacheParams cacheParams) {
+            FragmentManager fragmentManager, ImageCacheParams cacheParams) {
 
         // Search for, or create an instance of the non-UI RetainFragment
-        final RetainFragment mRetainFragment = RetainFragment.findOrCreateRetainFragment(
-                activity.getSupportFragmentManager());
+        final RetainFragment mRetainFragment = findOrCreateRetainFragment(fragmentManager);
 
         // See if we already have an ImageCache stored in RetainFragment
         ImageCache imageCache = (ImageCache) mRetainFragment.getObject();
 
         // No existing ImageCache, create one and store it in RetainFragment
         if (imageCache == null) {
-            imageCache = new ImageCache(activity, cacheParams);
+            imageCache = new ImageCache(cacheParams);
             mRetainFragment.setObject(imageCache);
         }
 
@@ -114,36 +116,75 @@
     /**
      * Initialize the cache, providing all parameters.
      *
-     * @param context The context to use
      * @param cacheParams The cache parameters to initialize the cache
      */
-    private void init(Context context, ImageCacheParams cacheParams) {
-        final File diskCacheDir = DiskLruCache.getDiskCacheDir(context, cacheParams.uniqueName);
-
-        // Set up disk cache
-        if (cacheParams.diskCacheEnabled) {
-            mDiskCache = DiskLruCache.openCache(context, diskCacheDir, cacheParams.diskCacheSize);
-            mDiskCache.setCompressParams(cacheParams.compressFormat, cacheParams.compressQuality);
-            if (cacheParams.clearDiskCacheOnStart) {
-                mDiskCache.clearCache();
-            }
-        }
+    private void init(ImageCacheParams cacheParams) {
+        mCacheParams = cacheParams;
 
         // Set up memory cache
-        if (cacheParams.memoryCacheEnabled) {
-            mMemoryCache = new LruCache<String, Bitmap>(cacheParams.memCacheSize) {
+        if (mCacheParams.memoryCacheEnabled) {
+            if (BuildConfig.DEBUG) {
+                Log.d(TAG, "Memory cache created (size = " + mCacheParams.memCacheSize + ")");
+            }
+            mMemoryCache = new LruCache<String, Bitmap>(mCacheParams.memCacheSize) {
                 /**
-                 * Measure item size in bytes rather than units which is more practical for a bitmap
-                 * cache
+                 * Measure item size in bytes rather than units which is more practical
+                 * for a bitmap cache
                  */
                 @Override
                 protected int sizeOf(String key, Bitmap bitmap) {
-                    return Utils.getBitmapSize(bitmap);
+                    return getBitmapSize(bitmap);
                 }
             };
         }
+
+        // By default the disk cache is not initialized here as it should be initialized
+        // on a separate thread due to disk access.
+        if (cacheParams.initDiskCacheOnCreate) {
+            // Set up disk cache
+            initDiskCache();
+        }
     }
 
+    /**
+     * Initializes the disk cache.  Note that this includes disk access so this should not be
+     * executed on the main/UI thread. By default an ImageCache does not initialize the disk
+     * cache when it is created, instead you should call initDiskCache() to initialize it on a
+     * background thread.
+     */
+    public void initDiskCache() {
+        // Set up disk cache
+        synchronized (mDiskCacheLock) {
+            if (mDiskLruCache == null || mDiskLruCache.isClosed()) {
+                File diskCacheDir = mCacheParams.diskCacheDir;
+                if (mCacheParams.diskCacheEnabled && diskCacheDir != null) {
+                    if (!diskCacheDir.exists()) {
+                        diskCacheDir.mkdirs();
+                    }
+                    if (getUsableSpace(diskCacheDir) > mCacheParams.diskCacheSize) {
+                        try {
+                            mDiskLruCache = DiskLruCache.open(
+                                    diskCacheDir, 1, 1, mCacheParams.diskCacheSize);
+                            if (BuildConfig.DEBUG) {
+                                Log.d(TAG, "Disk cache initialized");
+                            }
+                        } catch (final IOException e) {
+                            mCacheParams.diskCacheDir = null;
+                            Log.e(TAG, "initDiskCache - " + e);
+                        }
+                    }
+                }
+            }
+            mDiskCacheStarting = false;
+            mDiskCacheLock.notifyAll();
+        }
+    }
+
+    /**
+     * Adds a bitmap to both memory and disk cache.
+     * @param data Unique identifier for the bitmap to store
+     * @param bitmap The bitmap to store
+     */
     public void addBitmapToCache(String data, Bitmap bitmap) {
         if (data == null || bitmap == null) {
             return;
@@ -154,9 +195,37 @@
             mMemoryCache.put(data, bitmap);
         }
 
-        // Add to disk cache
-        if (mDiskCache != null && !mDiskCache.containsKey(data)) {
-            mDiskCache.put(data, bitmap);
+        synchronized (mDiskCacheLock) {
+            // Add to disk cache
+            if (mDiskLruCache != null) {
+                final String key = hashKeyForDisk(data);
+                OutputStream out = null;
+                try {
+                    DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
+                    if (snapshot == null) {
+                        final DiskLruCache.Editor editor = mDiskLruCache.edit(key);
+                        if (editor != null) {
+                            out = editor.newOutputStream(DISK_CACHE_INDEX);
+                            bitmap.compress(
+                                    mCacheParams.compressFormat, mCacheParams.compressQuality, out);
+                            editor.commit();
+                            out.close();
+                        }
+                    } else {
+                        snapshot.getInputStream(DISK_CACHE_INDEX).close();
+                    }
+                } catch (final IOException e) {
+                    Log.e(TAG, "addBitmapToCache - " + e);
+                } catch (Exception e) {
+                    Log.e(TAG, "addBitmapToCache - " + e);
+                } finally {
+                    try {
+                        if (out != null) {
+                            out.close();
+                        }
+                    } catch (IOException e) {}
+                }
+            }
         }
     }
 
@@ -186,32 +255,324 @@
      * @return The bitmap if found in cache, null otherwise
      */
     public Bitmap getBitmapFromDiskCache(String data) {
-        if (mDiskCache != null) {
-            return mDiskCache.get(data);
+        final String key = hashKeyForDisk(data);
+        synchronized (mDiskCacheLock) {
+            while (mDiskCacheStarting) {
+                try {
+                    mDiskCacheLock.wait();
+                } catch (InterruptedException e) {}
+            }
+            if (mDiskLruCache != null) {
+                InputStream inputStream = null;
+                try {
+                    final DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
+                    if (snapshot != null) {
+                        if (BuildConfig.DEBUG) {
+                            Log.d(TAG, "Disk cache hit");
+                        }
+                        inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
+                        if (inputStream != null) {
+                            final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
+                            return bitmap;
+                        }
+                    }
+                } catch (final IOException e) {
+                    Log.e(TAG, "getBitmapFromDiskCache - " + e);
+                } finally {
+                    try {
+                        if (inputStream != null) {
+                            inputStream.close();
+                        }
+                    } catch (IOException e) {}
+                }
+            }
+            return null;
         }
-        return null;
     }
 
-    public void clearCaches() {
-        mDiskCache.clearCache();
-        mMemoryCache.evictAll();
+    /**
+     * Clears both the memory and disk cache associated with this ImageCache object. Note that
+     * this includes disk access so this should not be executed on the main/UI thread.
+     */
+    public void clearCache() {
+        if (mMemoryCache != null) {
+            mMemoryCache.evictAll();
+            if (BuildConfig.DEBUG) {
+                Log.d(TAG, "Memory cache cleared");
+            }
+        }
+
+        synchronized (mDiskCacheLock) {
+            mDiskCacheStarting = true;
+            if (mDiskLruCache != null && !mDiskLruCache.isClosed()) {
+                try {
+                    mDiskLruCache.delete();
+                    if (BuildConfig.DEBUG) {
+                        Log.d(TAG, "Disk cache cleared");
+                    }
+                } catch (IOException e) {
+                    Log.e(TAG, "clearCache - " + e);
+                }
+                mDiskLruCache = null;
+                initDiskCache();
+            }
+        }
+    }
+
+    /**
+     * Flushes the disk cache associated with this ImageCache object. Note that this includes
+     * disk access so this should not be executed on the main/UI thread.
+     */
+    public void flush() {
+        synchronized (mDiskCacheLock) {
+            if (mDiskLruCache != null) {
+                try {
+                    mDiskLruCache.flush();
+                    if (BuildConfig.DEBUG) {
+                        Log.d(TAG, "Disk cache flushed");
+                    }
+                } catch (IOException e) {
+                    Log.e(TAG, "flush - " + e);
+                }
+            }
+        }
+    }
+
+    /**
+     * Closes the disk cache associated with this ImageCache object. Note that this includes
+     * disk access so this should not be executed on the main/UI thread.
+     */
+    public void close() {
+        synchronized (mDiskCacheLock) {
+            if (mDiskLruCache != null) {
+                try {
+                    if (!mDiskLruCache.isClosed()) {
+                        mDiskLruCache.close();
+                        mDiskLruCache = null;
+                        if (BuildConfig.DEBUG) {
+                            Log.d(TAG, "Disk cache closed");
+                        }
+                    }
+                } catch (IOException e) {
+                    Log.e(TAG, "close - " + e);
+                }
+            }
+        }
     }
 
     /**
      * A holder class that contains cache parameters.
      */
     public static class ImageCacheParams {
-        public String uniqueName;
         public int memCacheSize = DEFAULT_MEM_CACHE_SIZE;
         public int diskCacheSize = DEFAULT_DISK_CACHE_SIZE;
+        public File diskCacheDir;
         public CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT;
         public int compressQuality = DEFAULT_COMPRESS_QUALITY;
         public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED;
         public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED;
         public boolean clearDiskCacheOnStart = DEFAULT_CLEAR_DISK_CACHE_ON_START;
+        public boolean initDiskCacheOnCreate = DEFAULT_INIT_DISK_CACHE_ON_CREATE;
 
-        public ImageCacheParams(String uniqueName) {
-            this.uniqueName = uniqueName;
+        public ImageCacheParams(Context context, String uniqueName) {
+            diskCacheDir = getDiskCacheDir(context, uniqueName);
+        }
+
+        public ImageCacheParams(File diskCacheDir) {
+            this.diskCacheDir = diskCacheDir;
+        }
+
+        /**
+         * Sets the memory cache size based on a percentage of the device memory class.
+         * Eg. setting percent to 0.2 would set the memory cache to one fifth of the device memory
+         * class. Throws {@link IllegalArgumentException} if percent is < 0.05 or > .8.
+         *
+         * This value should be chosen carefully based on a number of factors
+         * Refer to the corresponding Android Training class for more discussion:
+         * http://developer.android.com/training/displaying-bitmaps/
+         *
+         * @param context Context to use to fetch memory class
+         * @param percent Percent of memory class to use to size memory cache
+         */
+        public void setMemCacheSizePercent(Context context, float percent) {
+            if (percent < 0.05f || percent > 0.8f) {
+                throw new IllegalArgumentException("setMemCacheSizePercent - percent must be "
+                        + "between 0.05 and 0.8 (inclusive)");
+            }
+            memCacheSize = Math.round(percent * getMemoryClass(context) * 1024 * 1024);
+        }
+
+        private static int getMemoryClass(Context context) {
+            return ((ActivityManager) context.getSystemService(
+                    Context.ACTIVITY_SERVICE)).getMemoryClass();
         }
     }
+
+    /**
+     * Get a usable cache directory (external if available, internal otherwise).
+     *
+     * @param context The context to use
+     * @param uniqueName A unique directory name to append to the cache dir
+     * @return The cache dir
+     */
+    public static File getDiskCacheDir(Context context, String uniqueName) {
+        // Check if media is mounted or storage is built-in, if so, try and use external cache dir
+        // otherwise use internal cache dir
+        final String cachePath =
+                Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
+                        !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
+                                context.getCacheDir().getPath();
+
+        return new File(cachePath + File.separator + uniqueName);
+    }
+
+    /**
+     * A hashing method that changes a string (like a URL) into a hash suitable for using as a
+     * disk filename.
+     */
+    public static String hashKeyForDisk(String key) {
+        String cacheKey;
+        try {
+            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
+            mDigest.update(key.getBytes());
+            cacheKey = bytesToHexString(mDigest.digest());
+        } catch (NoSuchAlgorithmException e) {
+            cacheKey = String.valueOf(key.hashCode());
+        }
+        return cacheKey;
+    }
+
+    private static String bytesToHexString(byte[] bytes) {
+        // http://stackoverflow.com/questions/332079
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < bytes.length; i++) {
+            String hex = Integer.toHexString(0xFF & bytes[i]);
+            if (hex.length() == 1) {
+                sb.append('0');
+            }
+            sb.append(hex);
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Get the size in bytes of a bitmap.
+     * @param bitmap
+     * @return size in bytes
+     */
+    @TargetApi(12)
+    public static int getBitmapSize(Bitmap bitmap) {
+        if (Utils.hasHoneycombMR1()) {
+            return bitmap.getByteCount();
+        }
+        // Pre HC-MR1
+        return bitmap.getRowBytes() * bitmap.getHeight();
+    }
+
+    /**
+     * Check if external storage is built-in or removable.
+     *
+     * @return True if external storage is removable (like an SD card), false
+     *         otherwise.
+     */
+    @TargetApi(9)
+    public static boolean isExternalStorageRemovable() {
+        if (Utils.hasGingerbread()) {
+            return Environment.isExternalStorageRemovable();
+        }
+        return true;
+    }
+
+    /**
+     * Get the external app cache directory.
+     *
+     * @param context The context to use
+     * @return The external cache dir
+     */
+    @TargetApi(8)
+    public static File getExternalCacheDir(Context context) {
+        if (Utils.hasFroyo()) {
+            return context.getExternalCacheDir();
+        }
+
+        // Before Froyo we need to construct the external cache dir ourselves
+        final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/";
+        return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir);
+    }
+
+    /**
+     * Check how much usable space is available at a given path.
+     *
+     * @param path The path to check
+     * @return The space available in bytes
+     */
+    @TargetApi(9)
+    public static long getUsableSpace(File path) {
+        if (Utils.hasGingerbread()) {
+            return path.getUsableSpace();
+        }
+        final StatFs stats = new StatFs(path.getPath());
+        return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
+    }
+
+    /**
+     * Locate an existing instance of this Fragment or if not found, create and
+     * add it using FragmentManager.
+     *
+     * @param fm The FragmentManager manager to use.
+     * @return The existing instance of the Fragment or the new instance if just
+     *         created.
+     */
+    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
+        // Check to see if we have retained the worker fragment.
+        RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG);
+
+        // If not retained (or first time running), we need to create and add it.
+        if (mRetainFragment == null) {
+            mRetainFragment = new RetainFragment();
+            fm.beginTransaction().add(mRetainFragment, TAG).commitAllowingStateLoss();
+        }
+
+        return mRetainFragment;
+    }
+
+    /**
+     * A simple non-UI Fragment that stores a single Object and is retained over configuration
+     * changes. It will be used to retain the ImageCache object.
+     */
+    public static class RetainFragment extends Fragment {
+        private Object mObject;
+
+        /**
+         * Empty constructor as per the Fragment documentation
+         */
+        public RetainFragment() {}
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+
+            // Make sure this Fragment is retained over a configuration change
+            setRetainInstance(true);
+        }
+
+        /**
+         * Store a single object in this Fragment.
+         *
+         * @param object The object to store
+         */
+        public void setObject(Object object) {
+            mObject = object;
+        }
+
+        /**
+         * Get the stored object.
+         *
+         * @return The stored object
+         */
+        public Object getObject() {
+            return mObject;
+        }
+    }
+
 }
diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageFetcher.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageFetcher.java
index 8b19dc3..7084845 100644
--- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageFetcher.java
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageFetcher.java
@@ -20,17 +20,20 @@
 import android.graphics.Bitmap;
 import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
+import android.os.Build;
 import android.util.Log;
 import android.widget.Toast;
 
 import com.example.android.bitmapfun.BuildConfig;
+import com.example.android.bitmapfun.R;
 
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.File;
-import java.io.FileOutputStream;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
 import java.io.IOException;
-import java.io.InputStream;
+import java.io.OutputStream;
 import java.net.HttpURLConnection;
 import java.net.URL;
 
@@ -40,7 +43,14 @@
 public class ImageFetcher extends ImageResizer {
     private static final String TAG = "ImageFetcher";
     private static final int HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10MB
-    public static final String HTTP_CACHE_DIR = "http";
+    private static final String HTTP_CACHE_DIR = "http";
+    private static final int IO_BUFFER_SIZE = 8 * 1024;
+
+    private DiskLruCache mHttpDiskCache;
+    private File mHttpCacheDir;
+    private boolean mHttpDiskCacheStarting = true;
+    private final Object mHttpDiskCacheLock = new Object();
+    private static final int DISK_CACHE_INDEX = 0;
 
     /**
      * Initialize providing a target image width and height for the processing images.
@@ -67,19 +77,103 @@
 
     private void init(Context context) {
         checkConnection(context);
+        mHttpCacheDir = ImageCache.getDiskCacheDir(context, HTTP_CACHE_DIR);
+    }
+
+    @Override
+    protected void initDiskCacheInternal() {
+        super.initDiskCacheInternal();
+        initHttpDiskCache();
+    }
+
+    private void initHttpDiskCache() {
+        if (!mHttpCacheDir.exists()) {
+            mHttpCacheDir.mkdirs();
+        }
+        synchronized (mHttpDiskCacheLock) {
+            if (ImageCache.getUsableSpace(mHttpCacheDir) > HTTP_CACHE_SIZE) {
+                try {
+                    mHttpDiskCache = DiskLruCache.open(mHttpCacheDir, 1, 1, HTTP_CACHE_SIZE);
+                    if (BuildConfig.DEBUG) {
+                        Log.d(TAG, "HTTP cache initialized");
+                    }
+                } catch (IOException e) {
+                    mHttpDiskCache = null;
+                }
+            }
+            mHttpDiskCacheStarting = false;
+            mHttpDiskCacheLock.notifyAll();
+        }
+    }
+
+    @Override
+    protected void clearCacheInternal() {
+        super.clearCacheInternal();
+        synchronized (mHttpDiskCacheLock) {
+            if (mHttpDiskCache != null && !mHttpDiskCache.isClosed()) {
+                try {
+                    mHttpDiskCache.delete();
+                    if (BuildConfig.DEBUG) {
+                        Log.d(TAG, "HTTP cache cleared");
+                    }
+                } catch (IOException e) {
+                    Log.e(TAG, "clearCacheInternal - " + e);
+                }
+                mHttpDiskCache = null;
+                mHttpDiskCacheStarting = true;
+                initHttpDiskCache();
+            }
+        }
+    }
+
+    @Override
+    protected void flushCacheInternal() {
+        super.flushCacheInternal();
+        synchronized (mHttpDiskCacheLock) {
+            if (mHttpDiskCache != null) {
+                try {
+                    mHttpDiskCache.flush();
+                    if (BuildConfig.DEBUG) {
+                        Log.d(TAG, "HTTP cache flushed");
+                    }
+                } catch (IOException e) {
+                    Log.e(TAG, "flush - " + e);
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void closeCacheInternal() {
+        super.closeCacheInternal();
+        synchronized (mHttpDiskCacheLock) {
+            if (mHttpDiskCache != null) {
+                try {
+                    if (!mHttpDiskCache.isClosed()) {
+                        mHttpDiskCache.close();
+                        mHttpDiskCache = null;
+                        if (BuildConfig.DEBUG) {
+                            Log.d(TAG, "HTTP cache closed");
+                        }
+                    }
+                } catch (IOException e) {
+                    Log.e(TAG, "closeCacheInternal - " + e);
+                }
+            }
+        }
     }
 
     /**
-     * Simple network connection check.
-     *
-     * @param context
-     */
+    * Simple network connection check.
+    *
+    * @param context
+    */
     private void checkConnection(Context context) {
         final ConnectivityManager cm =
                 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
         final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
         if (networkInfo == null || !networkInfo.isConnectedOrConnecting()) {
-            Toast.makeText(context, "No network connection found.", Toast.LENGTH_LONG).show();
+            Toast.makeText(context, R.string.no_network_connection_toast, Toast.LENGTH_LONG).show();
             Log.e(TAG, "checkConnection - no connection found");
         }
     }
@@ -96,15 +190,65 @@
             Log.d(TAG, "processBitmap - " + data);
         }
 
-        // Download a bitmap, write it to a file
-        final File f = downloadBitmap(mContext, data);
+        final String key = ImageCache.hashKeyForDisk(data);
+        FileDescriptor fileDescriptor = null;
+        FileInputStream fileInputStream = null;
+        DiskLruCache.Snapshot snapshot;
+        synchronized (mHttpDiskCacheLock) {
+            // Wait for disk cache to initialize
+            while (mHttpDiskCacheStarting) {
+                try {
+                    mHttpDiskCacheLock.wait();
+                } catch (InterruptedException e) {}
+            }
 
-        if (f != null) {
-            // Return a sampled down version
-            return decodeSampledBitmapFromFile(f.toString(), mImageWidth, mImageHeight);
+            if (mHttpDiskCache != null) {
+                try {
+                    snapshot = mHttpDiskCache.get(key);
+                    if (snapshot == null) {
+                        if (BuildConfig.DEBUG) {
+                            Log.d(TAG, "processBitmap, not found in http cache, downloading...");
+                        }
+                        DiskLruCache.Editor editor = mHttpDiskCache.edit(key);
+                        if (editor != null) {
+                            if (downloadUrlToStream(data,
+                                    editor.newOutputStream(DISK_CACHE_INDEX))) {
+                                editor.commit();
+                            } else {
+                                editor.abort();
+                            }
+                        }
+                        snapshot = mHttpDiskCache.get(key);
+                    }
+                    if (snapshot != null) {
+                        fileInputStream =
+                                (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
+                        fileDescriptor = fileInputStream.getFD();
+                    }
+                } catch (IOException e) {
+                    Log.e(TAG, "processBitmap - " + e);
+                } catch (IllegalStateException e) {
+                    Log.e(TAG, "processBitmap - " + e);
+                } finally {
+                    if (fileDescriptor == null && fileInputStream != null) {
+                        try {
+                            fileInputStream.close();
+                        } catch (IOException e) {}
+                    }
+                }
+            }
         }
 
-        return null;
+        Bitmap bitmap = null;
+        if (fileDescriptor != null) {
+            bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth, mImageHeight);
+        }
+        if (fileInputStream != null) {
+            try {
+                fileInputStream.close();
+            } catch (IOException e) {}
+        }
+        return bitmap;
     }
 
     @Override
@@ -113,65 +257,54 @@
     }
 
     /**
-     * Download a bitmap from a URL, write it to a disk and return the File pointer. This
-     * implementation uses a simple disk cache.
+     * Download a bitmap from a URL and write the content to an output stream.
      *
-     * @param context The context to use
      * @param urlString The URL to fetch
-     * @return A File pointing to the fetched bitmap
+     * @return true if successful, false otherwise
      */
-    public static File downloadBitmap(Context context, String urlString) {
-        final File cacheDir = DiskLruCache.getDiskCacheDir(context, HTTP_CACHE_DIR);
-
-        final DiskLruCache cache =
-                DiskLruCache.openCache(context, cacheDir, HTTP_CACHE_SIZE);
-
-        final File cacheFile = new File(cache.createFilePath(urlString));
-
-        if (cache.containsKey(urlString)) {
-            if (BuildConfig.DEBUG) {
-                Log.d(TAG, "downloadBitmap - found in http cache - " + urlString);
-            }
-            return cacheFile;
-        }
-
-        if (BuildConfig.DEBUG) {
-            Log.d(TAG, "downloadBitmap - downloading - " + urlString);
-        }
-
-        Utils.disableConnectionReuseIfNecessary();
+    public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
+        disableConnectionReuseIfNecessary();
         HttpURLConnection urlConnection = null;
         BufferedOutputStream out = null;
+        BufferedInputStream in = null;
 
         try {
             final URL url = new URL(urlString);
             urlConnection = (HttpURLConnection) url.openConnection();
-            final InputStream in =
-                    new BufferedInputStream(urlConnection.getInputStream(), Utils.IO_BUFFER_SIZE);
-            out = new BufferedOutputStream(new FileOutputStream(cacheFile), Utils.IO_BUFFER_SIZE);
+            in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
+            out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
 
             int b;
             while ((b = in.read()) != -1) {
                 out.write(b);
             }
-
-            return cacheFile;
-
+            return true;
         } catch (final IOException e) {
             Log.e(TAG, "Error in downloadBitmap - " + e);
         } finally {
             if (urlConnection != null) {
                 urlConnection.disconnect();
             }
-            if (out != null) {
-                try {
+            try {
+                if (out != null) {
                     out.close();
-                } catch (final IOException e) {
-                    Log.e(TAG, "Error in downloadBitmap - " + e);
                 }
-            }
+                if (in != null) {
+                    in.close();
+                }
+            } catch (final IOException e) {}
         }
+        return false;
+    }
 
-        return null;
+    /**
+     * Workaround for bug pre-Froyo, see here for more info:
+     * http://android-developers.blogspot.com/2011/09/androids-http-clients.html
+     */
+    public static void disableConnectionReuseIfNecessary() {
+        // HTTP connection reuse which was buggy pre-froyo
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) {
+            System.setProperty("http.keepAlive", "false");
+        }
     }
 }
diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageResizer.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageResizer.java
index 18d1f82..f533231 100644
--- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageResizer.java
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageResizer.java
@@ -24,13 +24,15 @@
 
 import com.example.android.bitmapfun.BuildConfig;
 
+import java.io.FileDescriptor;
+
 /**
  * A simple subclass of {@link ImageWorker} that resizes images from resources given a target width
  * and height. Useful for when the input images might be too large to simply load directly into
  * memory.
  */
 public class ImageResizer extends ImageWorker {
-    private static final String TAG = "ImageWorker";
+    private static final String TAG = "ImageResizer";
     protected int mImageWidth;
     protected int mImageHeight;
 
@@ -88,8 +90,7 @@
         if (BuildConfig.DEBUG) {
             Log.d(TAG, "processBitmap - " + resId);
         }
-        return decodeSampledBitmapFromResource(
-                mContext.getResources(), resId, mImageWidth, mImageHeight);
+        return decodeSampledBitmapFromResource(mResources, resId, mImageWidth, mImageHeight);
     }
 
     @Override
@@ -132,7 +133,7 @@
      * @return A bitmap sampled down from the original with the same aspect ratio and dimensions
      *         that are equal to or greater than the requested width and height
      */
-    public static synchronized Bitmap decodeSampledBitmapFromFile(String filename,
+    public static Bitmap decodeSampledBitmapFromFile(String filename,
             int reqWidth, int reqHeight) {
 
         // First decode with inJustDecodeBounds=true to check dimensions
@@ -149,6 +150,31 @@
     }
 
     /**
+     * Decode and sample down a bitmap from a file input stream to the requested width and height.
+     *
+     * @param fileDescriptor The file descriptor to read from
+     * @param reqWidth The requested width of the resulting bitmap
+     * @param reqHeight The requested height of the resulting bitmap
+     * @return A bitmap sampled down from the original with the same aspect ratio and dimensions
+     *         that are equal to or greater than the requested width and height
+     */
+    public static Bitmap decodeSampledBitmapFromDescriptor(
+            FileDescriptor fileDescriptor, int reqWidth, int reqHeight) {
+
+        // First decode with inJustDecodeBounds=true to check dimensions
+        final BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inJustDecodeBounds = true;
+        BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
+
+        // Calculate inSampleSize
+        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
+
+        // Decode bitmap with inSampleSize set
+        options.inJustDecodeBounds = false;
+        return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
+    }
+
+    /**
      * Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding
      * bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates
      * the closest inSampleSize that will result in the final decoded bitmap having a width and
diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageWorker.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageWorker.java
index a0d2693..32c43b2 100644
--- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageWorker.java
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageWorker.java
@@ -24,7 +24,7 @@
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.TransitionDrawable;
-import android.os.AsyncTask;
+import android.support.v4.app.FragmentManager;
 import android.util.Log;
 import android.widget.ImageView;
 
@@ -42,15 +42,22 @@
     private static final int FADE_IN_TIME = 200;
 
     private ImageCache mImageCache;
+    private ImageCache.ImageCacheParams mImageCacheParams;
     private Bitmap mLoadingBitmap;
     private boolean mFadeInBitmap = true;
     private boolean mExitTasksEarly = false;
+    protected boolean mPauseWork = false;
+    private final Object mPauseWorkLock = new Object();
 
-    protected Context mContext;
-    protected ImageWorkerAdapter mImageWorkerAdapter;
+    protected Resources mResources;
+
+    private static final int MESSAGE_CLEAR = 0;
+    private static final int MESSAGE_INIT_DISK_CACHE = 1;
+    private static final int MESSAGE_FLUSH = 2;
+    private static final int MESSAGE_CLOSE = 3;
 
     protected ImageWorker(Context context) {
-        mContext = context;
+        mResources = context.getResources();
     }
 
     /**
@@ -65,6 +72,10 @@
      * @param imageView The ImageView to bind the downloaded image to.
      */
     public void loadImage(Object data, ImageView imageView) {
+        if (data == null) {
+            return;
+        }
+
         Bitmap bitmap = null;
 
         if (mImageCache != null) {
@@ -77,29 +88,13 @@
         } else if (cancelPotentialWork(data, imageView)) {
             final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
             final AsyncDrawable asyncDrawable =
-                    new AsyncDrawable(mContext.getResources(), mLoadingBitmap, task);
+                    new AsyncDrawable(mResources, mLoadingBitmap, task);
             imageView.setImageDrawable(asyncDrawable);
-            task.execute(data);
-        }
-    }
 
-    /**
-     * Load an image specified from a set adapter into an ImageView (override
-     * {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and disk
-     * cache will be used if an {@link ImageCache} has been set using
-     * {@link ImageWorker#setImageCache(ImageCache)}. If the image is found in the memory cache, it
-     * is set immediately, otherwise an {@link AsyncTask} will be created to asynchronously load the
-     * bitmap. {@link ImageWorker#setAdapter(ImageWorkerAdapter)} must be called before using this
-     * method.
-     *
-     * @param data The URL of the image to download.
-     * @param imageView The ImageView to bind the downloaded image to.
-     */
-    public void loadImage(int num, ImageView imageView) {
-        if (mImageWorkerAdapter != null) {
-            loadImage(mImageWorkerAdapter.getItem(num), imageView);
-        } else {
-            throw new NullPointerException("Data not set, must call setAdapter() first.");
+            // NOTE: This uses a custom version of AsyncTask that has been pulled from the
+            // framework and slightly modified. Refer to the docs at the top of the class
+            // for more info on what was changed.
+            task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR, data);
         }
     }
 
@@ -118,26 +113,36 @@
      * @param resId
      */
     public void setLoadingImage(int resId) {
-        mLoadingBitmap = BitmapFactory.decodeResource(mContext.getResources(), resId);
+        mLoadingBitmap = BitmapFactory.decodeResource(mResources, resId);
     }
 
     /**
-     * Set the {@link ImageCache} object to use with this ImageWorker.
-     *
-     * @param cacheCallback
+     * Adds an {@link ImageCache} to this worker in the background (to prevent disk access on UI
+     * thread).
+     * @param fragmentManager
+     * @param cacheParams
      */
-    public void setImageCache(ImageCache cacheCallback) {
-        mImageCache = cacheCallback;
+    public void addImageCache(FragmentManager fragmentManager,
+            ImageCache.ImageCacheParams cacheParams) {
+        mImageCacheParams = cacheParams;
+        setImageCache(ImageCache.findOrCreateCache(fragmentManager, mImageCacheParams));
+        new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE);
     }
 
-    public ImageCache getImageCache() {
-        return mImageCache;
+    /**
+     * Sets the {@link ImageCache} object to use with this ImageWorker. Usually you will not need
+     * to call this directly, instead use {@link ImageWorker#addImageCache} which will create and
+     * add the {@link ImageCache} object in a background thread (to ensure no disk access on the
+     * main/UI thread).
+     *
+     * @param imageCache
+     */
+    public void setImageCache(ImageCache imageCache) {
+        mImageCache = imageCache;
     }
 
     /**
      * If set to true, the image will fade-in once it has been loaded by the background thread.
-     *
-     * @param fadeIn
      */
     public void setImageFadeIn(boolean fadeIn) {
         mFadeInBitmap = fadeIn;
@@ -158,6 +163,10 @@
      */
     protected abstract Bitmap processBitmap(Object data);
 
+    /**
+     * Cancels any pending work attached to the provided ImageView.
+     * @param imageView
+     */
     public static void cancelWork(ImageView imageView) {
         final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
         if (bitmapWorkerTask != null) {
@@ -225,10 +234,23 @@
          */
         @Override
         protected Bitmap doInBackground(Object... params) {
+            if (BuildConfig.DEBUG) {
+                Log.d(TAG, "doInBackground - starting work");
+            }
+
             data = params[0];
             final String dataString = String.valueOf(data);
             Bitmap bitmap = null;
 
+            // Wait here if work is paused and the task is not cancelled
+            synchronized (mPauseWorkLock) {
+                while (mPauseWork && !isCancelled()) {
+                    try {
+                        mPauseWorkLock.wait();
+                    } catch (InterruptedException e) {}
+                }
+            }
+
             // If the image cache is available and this task has not been cancelled by another
             // thread and the ImageView that was originally bound to this task is still bound back
             // to this task and our "exit early" flag is not set then try and fetch the bitmap from
@@ -255,6 +277,10 @@
                 mImageCache.addBitmapToCache(dataString, bitmap);
             }
 
+            if (BuildConfig.DEBUG) {
+                Log.d(TAG, "doInBackground - finished work");
+            }
+
             return bitmap;
         }
 
@@ -270,10 +296,21 @@
 
             final ImageView imageView = getAttachedImageView();
             if (bitmap != null && imageView != null) {
+                if (BuildConfig.DEBUG) {
+                    Log.d(TAG, "onPostExecute - setting bitmap");
+                }
                 setImageBitmap(imageView, bitmap);
             }
         }
 
+        @Override
+        protected void onCancelled(Bitmap bitmap) {
+            super.onCancelled(bitmap);
+            synchronized (mPauseWorkLock) {
+                mPauseWorkLock.notifyAll();
+            }
+        }
+
         /**
          * Returns the ImageView associated with this task as long as the ImageView's task still
          * points to this task as well. Returns null otherwise.
@@ -301,7 +338,6 @@
 
         public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
             super(res, bitmap);
-
             bitmapWorkerTaskReference =
                 new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
         }
@@ -323,11 +359,11 @@
             final TransitionDrawable td =
                     new TransitionDrawable(new Drawable[] {
                             new ColorDrawable(android.R.color.transparent),
-                            new BitmapDrawable(mContext.getResources(), bitmap)
+                            new BitmapDrawable(mResources, bitmap)
                     });
             // Set background to loading bitmap
             imageView.setBackgroundDrawable(
-                    new BitmapDrawable(mContext.getResources(), mLoadingBitmap));
+                    new BitmapDrawable(mResources, mLoadingBitmap));
 
             imageView.setImageDrawable(td);
             td.startTransition(FADE_IN_TIME);
@@ -336,29 +372,71 @@
         }
     }
 
-    /**
-     * Set the simple adapter which holds the backing data.
-     *
-     * @param adapter
-     */
-    public void setAdapter(ImageWorkerAdapter adapter) {
-        mImageWorkerAdapter = adapter;
+    public void setPauseWork(boolean pauseWork) {
+        synchronized (mPauseWorkLock) {
+            mPauseWork = pauseWork;
+            if (!mPauseWork) {
+                mPauseWorkLock.notifyAll();
+            }
+        }
     }
 
-    /**
-     * Get the current adapter.
-     *
-     * @return
-     */
-    public ImageWorkerAdapter getAdapter() {
-        return mImageWorkerAdapter;
+    protected class CacheAsyncTask extends AsyncTask<Object, Void, Void> {
+
+        @Override
+        protected Void doInBackground(Object... params) {
+            switch ((Integer)params[0]) {
+                case MESSAGE_CLEAR:
+                    clearCacheInternal();
+                    break;
+                case MESSAGE_INIT_DISK_CACHE:
+                    initDiskCacheInternal();
+                    break;
+                case MESSAGE_FLUSH:
+                    flushCacheInternal();
+                    break;
+                case MESSAGE_CLOSE:
+                    closeCacheInternal();
+                    break;
+            }
+            return null;
+        }
     }
 
-    /**
-     * A very simple adapter for use with ImageWorker class and subclasses.
-     */
-    public static abstract class ImageWorkerAdapter {
-        public abstract Object getItem(int num);
-        public abstract int getSize();
+    protected void initDiskCacheInternal() {
+        if (mImageCache != null) {
+            mImageCache.initDiskCache();
+        }
+    }
+
+    protected void clearCacheInternal() {
+        if (mImageCache != null) {
+            mImageCache.clearCache();
+        }
+    }
+
+    protected void flushCacheInternal() {
+        if (mImageCache != null) {
+            mImageCache.flush();
+        }
+    }
+
+    protected void closeCacheInternal() {
+        if (mImageCache != null) {
+            mImageCache.close();
+            mImageCache = null;
+        }
+    }
+
+    public void clearCache() {
+        new CacheAsyncTask().execute(MESSAGE_CLEAR);
+    }
+
+    public void flushCache() {
+        new CacheAsyncTask().execute(MESSAGE_FLUSH);
+    }
+
+    public void closeCache() {
+        new CacheAsyncTask().execute(MESSAGE_CLOSE);
     }
 }
diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/RetainFragment.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/RetainFragment.java
deleted file mode 100644
index 3ee9cd6..0000000
--- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/RetainFragment.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.example.android.bitmapfun.util;
-
-import android.os.Bundle;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentManager;
-
-/**
- * A simple non-UI Fragment that stores a single Object and is retained over configuration changes.
- * In this sample it will be used to retain the ImageCache object.
- */
-public class RetainFragment extends Fragment {
-    private static final String TAG = "RetainFragment";
-    private Object mObject;
-
-    /**
-     * Empty constructor as per the Fragment documentation
-     */
-    public RetainFragment() {}
-
-    /**
-     * Locate an existing instance of this Fragment or if not found, create and
-     * add it using FragmentManager.
-     *
-     * @param fm The FragmentManager manager to use.
-     * @return The existing instance of the Fragment or the new instance if just
-     *         created.
-     */
-    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
-        // Check to see if we have retained the worker fragment.
-        RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG);
-
-        // If not retained (or first time running), we need to create and add it.
-        if (mRetainFragment == null) {
-            mRetainFragment = new RetainFragment();
-            fm.beginTransaction().add(mRetainFragment, TAG).commit();
-        }
-
-        return mRetainFragment;
-    }
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        // Make sure this Fragment is retained over a configuration change
-        setRetainInstance(true);
-    }
-
-    /**
-     * Store a single object in this Fragment.
-     *
-     * @param object The object to store
-     */
-    public void setObject(Object object) {
-        mObject = object;
-    }
-
-    /**
-     * Get the stored object.
-     *
-     * @return The stored object
-     */
-    public Object getObject() {
-        return mObject;
-    }
-
-}
diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/Utils.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/Utils.java
index 544df33..52a99f9 100644
--- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/Utils.java
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/Utils.java
@@ -16,131 +16,61 @@
 
 package com.example.android.bitmapfun.util;
 
-import android.annotation.SuppressLint;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.os.Build;
-import android.os.Environment;
-import android.os.StatFs;
+import com.example.android.bitmapfun.ui.ImageDetailActivity;
+import com.example.android.bitmapfun.ui.ImageGridActivity;
 
-import java.io.File;
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.os.StrictMode;
 
 /**
  * Class containing some static utility methods.
  */
 public class Utils {
-    public static final int IO_BUFFER_SIZE = 8 * 1024;
-
     private Utils() {};
 
-    /**
-     * Workaround for bug pre-Froyo, see here for more info:
-     * http://android-developers.blogspot.com/2011/09/androids-http-clients.html
-     */
-    public static void disableConnectionReuseIfNecessary() {
-        // HTTP connection reuse which was buggy pre-froyo
-        if (hasHttpConnectionBug()) {
-            System.setProperty("http.keepAlive", "false");
+    @TargetApi(11)
+    public static void enableStrictMode() {
+        if (Utils.hasGingerbread()) {
+            StrictMode.ThreadPolicy.Builder threadPolicyBuilder =
+                    new StrictMode.ThreadPolicy.Builder()
+                            .detectAll()
+                            .penaltyLog();
+            StrictMode.VmPolicy.Builder vmPolicyBuilder =
+                    new StrictMode.VmPolicy.Builder()
+                            .detectAll()
+                            .penaltyLog();
+
+            if (Utils.hasHoneycomb()) {
+                threadPolicyBuilder.penaltyFlashScreen();
+                vmPolicyBuilder
+                        .setClassInstanceLimit(ImageGridActivity.class, 1)
+                        .setClassInstanceLimit(ImageDetailActivity.class, 1);
+            }
+            StrictMode.setThreadPolicy(threadPolicyBuilder.build());
+            StrictMode.setVmPolicy(vmPolicyBuilder.build());
         }
     }
 
-    /**
-     * Get the size in bytes of a bitmap.
-     * @param bitmap
-     * @return size in bytes
-     */
-    @SuppressLint("NewApi")
-    public static int getBitmapSize(Bitmap bitmap) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
-            return bitmap.getByteCount();
-        }
-        // Pre HC-MR1
-        return bitmap.getRowBytes() * bitmap.getHeight();
-    }
-
-    /**
-     * Check if external storage is built-in or removable.
-     *
-     * @return True if external storage is removable (like an SD card), false
-     *         otherwise.
-     */
-    @SuppressLint("NewApi")
-    public static boolean isExternalStorageRemovable() {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
-            return Environment.isExternalStorageRemovable();
-        }
-        return true;
-    }
-
-    /**
-     * Get the external app cache directory.
-     *
-     * @param context The context to use
-     * @return The external cache dir
-     */
-    @SuppressLint("NewApi")
-    public static File getExternalCacheDir(Context context) {
-        if (hasExternalCacheDir()) {
-            return context.getExternalCacheDir();
-        }
-
-        // Before Froyo we need to construct the external cache dir ourselves
-        final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/";
-        return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir);
-    }
-
-    /**
-     * Check how much usable space is available at a given path.
-     *
-     * @param path The path to check
-     * @return The space available in bytes
-     */
-    @SuppressLint("NewApi")
-    public static long getUsableSpace(File path) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
-            return path.getUsableSpace();
-        }
-        final StatFs stats = new StatFs(path.getPath());
-        return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
-    }
-
-    /**
-     * Get the memory class of this device (approx. per-app memory limit)
-     *
-     * @param context
-     * @return
-     */
-    public static int getMemoryClass(Context context) {
-        return ((ActivityManager) context.getSystemService(
-                Context.ACTIVITY_SERVICE)).getMemoryClass();
-    }
-
-    /**
-     * Check if OS version has a http URLConnection bug. See here for more information:
-     * http://android-developers.blogspot.com/2011/09/androids-http-clients.html
-     *
-     * @return
-     */
-    public static boolean hasHttpConnectionBug() {
-        return Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO;
-    }
-
-    /**
-     * Check if OS version has built-in external cache dir method.
-     *
-     * @return
-     */
-    public static boolean hasExternalCacheDir() {
+    public static boolean hasFroyo() {
+        // Can use static final constants like FROYO, declared in later versions
+        // of the OS since they are inlined at compile time. This is guaranteed behavior.
         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO;
     }
 
-    /**
-     * Check if ActionBar is available.
-     *
-     * @return
-     */
-    public static boolean hasActionBar() {
+    public static boolean hasGingerbread() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD;
+    }
+
+    public static boolean hasHoneycomb() {
         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
     }
+
+    public static boolean hasHoneycombMR1() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1;
+    }
+
+    public static boolean hasJellyBean() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
+    }
 }