Sample app for Android U class: Displaying Bitmaps Efficiently

Change-Id: I21da4e60f719b7159ada83894cca8d108bedd3d5
diff --git a/samples/training/bitmapfun/AndroidManifest.xml b/samples/training/bitmapfun/AndroidManifest.xml
new file mode 100644
index 0000000..4a6f0f5
--- /dev/null
+++ b/samples/training/bitmapfun/AndroidManifest.xml
@@ -0,0 +1,51 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.android.bitmapfun"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+    <uses-sdk
+        android:minSdkVersion="7"
+        android:targetSdkVersion="15" />
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+
+    <application
+        android:description="@string/app_description"
+        android:hardwareAccelerated="true"
+        android:icon="@drawable/ic_launcher"
+        android:label="@string/app_name" >
+        <activity
+            android:name=".ui.ImageDetailActivity"
+            android:label="@string/app_name"
+            android:theme="@style/AppTheme.FullScreen" >
+        </activity>
+        <activity
+            android:name=".ui.ImageGridActivity"
+            android:label="@string/app_name"
+            android:theme="@style/AppTheme" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/samples/training/bitmapfun/libs/android-support-v4.jar b/samples/training/bitmapfun/libs/android-support-v4.jar
new file mode 100644
index 0000000..99e063b
--- /dev/null
+++ 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
new file mode 100644
index 0000000..0840b4a
--- /dev/null
+++ b/samples/training/bitmapfun/project.properties
@@ -0,0 +1,14 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-15
diff --git a/samples/training/bitmapfun/res/drawable-hdpi/ic_launcher.png b/samples/training/bitmapfun/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..96a442e
--- /dev/null
+++ b/samples/training/bitmapfun/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/samples/training/bitmapfun/res/drawable-ldpi/ic_launcher.png b/samples/training/bitmapfun/res/drawable-ldpi/ic_launcher.png
new file mode 100644
index 0000000..9923872
--- /dev/null
+++ b/samples/training/bitmapfun/res/drawable-ldpi/ic_launcher.png
Binary files differ
diff --git a/samples/training/bitmapfun/res/drawable-mdpi/ic_launcher.png b/samples/training/bitmapfun/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..359047d
--- /dev/null
+++ b/samples/training/bitmapfun/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/samples/training/bitmapfun/res/drawable-nodpi/empty_photo.png b/samples/training/bitmapfun/res/drawable-nodpi/empty_photo.png
new file mode 100644
index 0000000..da1478a
--- /dev/null
+++ b/samples/training/bitmapfun/res/drawable-nodpi/empty_photo.png
Binary files differ
diff --git a/samples/training/bitmapfun/res/drawable-xhdpi/ic_launcher.png b/samples/training/bitmapfun/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..71c6d76
--- /dev/null
+++ b/samples/training/bitmapfun/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/training/bitmapfun/res/layout/image_detail_fragment.xml b/samples/training/bitmapfun/res/layout/image_detail_fragment.xml
new file mode 100644
index 0000000..2d9dfcb
--- /dev/null
+++ b/samples/training/bitmapfun/res/layout/image_detail_fragment.xml
@@ -0,0 +1,36 @@
+<?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.
+-->
+
+<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"
+        android:layout_gravity="center" />
+
+    <ImageView
+        android:id="@+id/imageView"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:contentDescription="@string/imageview_description" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/samples/training/bitmapfun/res/layout/image_detail_pager.xml b/samples/training/bitmapfun/res/layout/image_detail_pager.xml
new file mode 100644
index 0000000..877a26b
--- /dev/null
+++ b/samples/training/bitmapfun/res/layout/image_detail_pager.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.
+-->
+
+<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/pager"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent" >
+
+</android.support.v4.view.ViewPager>
\ No newline at end of file
diff --git a/samples/training/bitmapfun/res/layout/image_grid_fragment.xml b/samples/training/bitmapfun/res/layout/image_grid_fragment.xml
new file mode 100644
index 0000000..e2034de
--- /dev/null
+++ b/samples/training/bitmapfun/res/layout/image_grid_fragment.xml
@@ -0,0 +1,29 @@
+<?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.
+-->
+
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/gridView"
+    style="@style/PhotoGridLayout"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:columnWidth="@dimen/image_thumbnail_size"
+    android:horizontalSpacing="@dimen/image_thumbnail_spacing"
+    android:numColumns="auto_fit"
+    android:stretchMode="columnWidth"
+    android:verticalSpacing="@dimen/image_thumbnail_spacing" >
+
+</GridView>
\ No newline at end of file
diff --git a/samples/training/bitmapfun/res/menu/main_menu.xml b/samples/training/bitmapfun/res/menu/main_menu.xml
new file mode 100644
index 0000000..0e727d0
--- /dev/null
+++ b/samples/training/bitmapfun/res/menu/main_menu.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+    <item
+        android:id="@+id/clear_cache"
+        android:icon="@android:drawable/ic_menu_delete"
+        android:showAsAction="never"
+        android:title="@string/clear_cache_menu"/>
+
+</menu>
\ No newline at end of file
diff --git a/samples/training/bitmapfun/res/values-large/dimens.xml b/samples/training/bitmapfun/res/values-large/dimens.xml
new file mode 100644
index 0000000..503f267
--- /dev/null
+++ b/samples/training/bitmapfun/res/values-large/dimens.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>
+
+    <dimen name="image_thumbnail_size">150dp</dimen>
+    <dimen name="image_thumbnail_spacing">1dp</dimen>
+
+</resources>
\ No newline at end of file
diff --git a/samples/training/bitmapfun/res/values-v11/styles.xml b/samples/training/bitmapfun/res/values-v11/styles.xml
new file mode 100644
index 0000000..0c64526
--- /dev/null
+++ b/samples/training/bitmapfun/res/values-v11/styles.xml
@@ -0,0 +1,36 @@
+<?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>
+
+    <style name="AppTheme" parent="@android:style/Theme.Holo">
+        <item name="android:windowActionBarOverlay">true</item>
+        <item name="android:windowBackground">@android:color/black</item>
+        <item name="android:actionBarStyle">@style/TranslucentDarkActionBar</item>
+    </style>
+
+    <style name="AppTheme.FullScreen" />
+
+    <style name="TranslucentDarkActionBar" parent="@android:style/Widget.Holo.ActionBar">
+        <item name="android:background">#99000000</item>
+    </style>
+
+    <style name="PhotoGridLayout">
+        <item name="android:drawSelectorOnTop">true</item>
+    </style>
+
+</resources>
\ No newline at end of file
diff --git a/samples/training/bitmapfun/res/values-xlarge/dimens.xml b/samples/training/bitmapfun/res/values-xlarge/dimens.xml
new file mode 100644
index 0000000..0f43977
--- /dev/null
+++ b/samples/training/bitmapfun/res/values-xlarge/dimens.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>
+
+    <dimen name="image_thumbnail_size">198dp</dimen>
+    <dimen name="image_thumbnail_spacing">2dp</dimen>
+
+</resources>
\ No newline at end of file
diff --git a/samples/training/bitmapfun/res/values/dimens.xml b/samples/training/bitmapfun/res/values/dimens.xml
new file mode 100644
index 0000000..60d540f
--- /dev/null
+++ b/samples/training/bitmapfun/res/values/dimens.xml
@@ -0,0 +1,24 @@
+<?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>
+
+    <dimen name="image_thumbnail_size">100dp</dimen>
+    <dimen name="image_thumbnail_spacing">1dp</dimen>
+    <dimen name="image_detail_pager_margin">80dp</dimen>
+
+</resources>
\ No newline at end of file
diff --git a/samples/training/bitmapfun/res/values/strings.xml b/samples/training/bitmapfun/res/values/strings.xml
new file mode 100644
index 0000000..b77f768
--- /dev/null
+++ b/samples/training/bitmapfun/res/values/strings.xml
@@ -0,0 +1,30 @@
+<?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>
+
+    <string name="app_name">BitmapFun</string>
+    <string name="app_description">This is a sample application for the Android Training class
+        "Displaying Bitmaps Efficiently"
+        (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="imageview_description">Image Thumbnail</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
new file mode 100644
index 0000000..3e72fd3
--- /dev/null
+++ b/samples/training/bitmapfun/res/values/styles.xml
@@ -0,0 +1,29 @@
+<?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>
+
+    <style name="AppTheme" parent="android:Theme" />
+
+    <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>
+    </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
new file mode 100644
index 0000000..5c9ef5c
--- /dev/null
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/provider/Images.java
@@ -0,0 +1,147 @@
+/*
+ * 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.provider;
+
+import com.example.android.bitmapfun.util.ImageWorker.ImageWorkerAdapter;
+
+/**
+ * Some simple test data to use for this sample app.
+ */
+public class Images {
+
+    /**
+     * This are PicasaWeb URLs and could potentially change. Ideally the PicasaWeb API should be
+     * 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",
+    };
+
+    /**
+     * This are PicasaWeb thumbnail URLs and could potentially change. Ideally the PicasaWeb API
+     * 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;
+        }
+    };
+}
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
new file mode 100644
index 0000000..c7ee8cd
--- /dev/null
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailActivity.java
@@ -0,0 +1,203 @@
+/*
+ * 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.ui;
+
+import android.annotation.SuppressLint;
+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.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.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 {
+    private static final String IMAGE_CACHE_DIR = "images";
+    public static final String EXTRA_IMAGE = "extra_image";
+
+    private ImagePagerAdapter mAdapter;
+    private ImageResizer mImageWorker;
+    private ViewPager mPager;
+
+    @SuppressLint("NewApi")
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        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;
+
+        // 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);
+
+        // Set up ViewPager and backing adapter
+        mAdapter = new ImagePagerAdapter(getSupportFragmentManager(),
+                mImageWorker.getAdapter().getSize());
+        mPager = (ViewPager) findViewById(R.id.pager);
+        mPager.setAdapter(mAdapter);
+        mPager.setPageMargin((int) getResources().getDimension(R.dimen.image_detail_pager_margin));
+
+        // 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()) {
+            final ActionBar actionBar = getActionBar();
+
+            // Enable "up" navigation on ActionBar icon and hide title text
+            actionBar.setDisplayHomeAsUpEnabled(true);
+            actionBar.setDisplayShowTitleEnabled(false);
+
+            // Start low profile mode and hide ActionBar
+            mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
+            actionBar.hide();
+
+            // Hide and show the ActionBar as the visibility changes
+            mPager.setOnSystemUiVisibilityChangeListener(
+                    new View.OnSystemUiVisibilityChangeListener() {
+                        @Override
+                        public void onSystemUiVisibilityChange(int vis) {
+                            if ((vis & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0) {
+                                actionBar.hide();
+                            } else {
+                                actionBar.show();
+                            }
+                        }
+                    });
+        }
+
+        // Set the current item based on the extra passed in to this activity
+        final int extraCurrentItem = getIntent().getIntExtra(EXTRA_IMAGE, -1);
+        if (extraCurrentItem != -1) {
+            mPager.setCurrentItem(extraCurrentItem);
+        }
+    }
+
+    @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);
+                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();
+                }
+                return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        MenuInflater inflater = getMenuInflater();
+        inflater.inflate(R.menu.main_menu, menu);
+        return true;
+    }
+
+    /**
+     * Called by the ViewPager child fragments to load images via the one ImageWorker
+     *
+     * @return
+     */
+    public ImageWorker getImageWorker() {
+        return mImageWorker;
+    }
+
+    /**
+     * The main adapter that backs the ViewPager. A subclass of FragmentStatePagerAdapter as there
+     * could be a large number of items in the ViewPager and we don't want to retain them all in
+     * memory at once but create/destroy them on the fly.
+     */
+    private class ImagePagerAdapter extends FragmentStatePagerAdapter {
+        private final int mSize;
+
+        public ImagePagerAdapter(FragmentManager fm, int size) {
+            super(fm);
+            mSize = size;
+        }
+
+        @Override
+        public int getCount() {
+            return mSize;
+        }
+
+        @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);
+        }
+    }
+
+    /**
+     * Set on the ImageView in the ViewPager children fragments, to enable/disable low profile mode
+     * when the ImageView is touched.
+     */
+    @SuppressLint("NewApi")
+    @Override
+    public void onClick(View v) {
+        final int vis = mPager.getSystemUiVisibility();
+        if ((vis & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0) {
+            mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
+        } else {
+            mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
+        }
+    }
+}
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
new file mode 100644
index 0000000..e2fd703
--- /dev/null
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailFragment.java
@@ -0,0 +1,106 @@
+/*
+ * 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.ui;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import com.example.android.bitmapfun.R;
+import com.example.android.bitmapfun.util.ImageWorker;
+import com.example.android.bitmapfun.util.Utils;
+
+/**
+ * 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 ImageView mImageView;
+    private ImageWorker mImageWorker;
+
+    /**
+     * 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
+     * @return A new instance of ImageDetailFragment with imageNum extras
+     */
+    public static ImageDetailFragment newInstance(int imageNum) {
+        final ImageDetailFragment f = new ImageDetailFragment();
+
+        final Bundle args = new Bundle();
+        args.putInt(IMAGE_DATA_EXTRA, imageNum);
+        f.setArguments(args);
+
+        return f;
+    }
+
+    /**
+     * Empty constructor as per the Fragment documentation
+     */
+    public ImageDetailFragment() {}
+
+    /**
+     * Populate image number from extra, use the convenience factory method
+     * {@link ImageDetailFragment#newInstance(int)} to create this fragment.
+     */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        // Inflate and locate the main ImageView
+        final View v = inflater.inflate(R.layout.image_detail_fragment, container, false);
+        mImageView = (ImageView) v.findViewById(R.id.imageView);
+        return v;
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        // 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);
+        }
+
+        // Pass clicks on the ImageView to the parent activity to handle
+        if (OnClickListener.class.isInstance(getActivity()) && Utils.hasActionBar()) {
+            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;
+    }
+}
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
new file mode 100644
index 0000000..28d97b3
--- /dev/null
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridActivity.java
@@ -0,0 +1,39 @@
+/*
+ * 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.ui;
+
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentTransaction;
+
+/**
+ * Simple FragmentActivity to hold the main {@link ImageGridFragment} and not much else.
+ */
+public class ImageGridActivity extends FragmentActivity {
+    private static final String TAG = "ImageGridFragment";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        if (getSupportFragmentManager().findFragmentByTag(TAG) == null) {
+            final FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+            ft.add(android.R.id.content, new ImageGridFragment(), TAG);
+            ft.commit();
+        }
+    }
+}
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
new file mode 100644
index 0000000..495d405
--- /dev/null
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridFragment.java
@@ -0,0 +1,300 @@
+/*
+ * 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.ui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewTreeObserver;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.GridView;
+import android.widget.ImageView;
+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.ImageCache.ImageCacheParams;
+import com.example.android.bitmapfun.util.ImageFetcher;
+import com.example.android.bitmapfun.util.ImageResizer;
+import com.example.android.bitmapfun.util.Utils;
+
+/**
+ * The main fragment that powers the ImageGridActivity screen. Fairly straight forward GridView
+ * 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.
+ */
+public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
+    private static final String TAG = "ImageGridFragment";
+    private static final String IMAGE_CACHE_DIR = "thumbs";
+
+    private int mImageThumbSize;
+    private int mImageThumbSpacing;
+    private ImageAdapter mAdapter;
+    private ImageResizer mImageWorker;
+
+    /**
+     * Empty constructor as per the Fragment documentation
+     */
+    public ImageGridFragment() {}
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setHasOptionsMenu(true);
+
+        mImageThumbSize = getResources().getDimensionPixelSize(R.dimen.image_thumbnail_size);
+        mImageThumbSpacing = getResources().getDimensionPixelSize(R.dimen.image_thumbnail_spacing);
+
+        mAdapter = new ImageAdapter(getActivity());
+
+        ImageCacheParams cacheParams = new ImageCacheParams(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;
+
+        // 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));
+    }
+
+    @Override
+    public View onCreateView(
+            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+        final View v = inflater.inflate(R.layout.image_grid_fragment, container, false);
+        final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
+        mGridView.setAdapter(mAdapter);
+        mGridView.setOnItemClickListener(this);
+
+        // 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
+        // as the GridView has stretchMode=columnWidth. The column width is used to set the height
+        // of each view so we get nice square thumbnails.
+        mGridView.getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        if (mAdapter.getNumColumns() == 0) {
+                            final int numColumns = (int) Math.floor(
+                                    mGridView.getWidth() / (mImageThumbSize + mImageThumbSpacing));
+                            if (numColumns > 0) {
+                                final int columnWidth =
+                                        (mGridView.getWidth() / numColumns) - mImageThumbSpacing;
+                                mAdapter.setNumColumns(numColumns);
+                                mAdapter.setItemHeight(columnWidth);
+                                if (BuildConfig.DEBUG) {
+                                    Log.d(TAG, "onCreateView - numColumns set to " + numColumns);
+                                }
+                            }
+                        }
+                    }
+                });
+
+        return v;
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        mImageWorker.setExitTasksEarly(false);
+        mAdapter.notifyDataSetChanged();
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        mImageWorker.setExitTasksEarly(true);
+    }
+
+    @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);
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        inflater.inflate(R.menu.main_menu, menu);
+    }
+
+    @Override
+    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();
+                }
+                return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    /**
+     * The main adapter that backs the GridView. This is fairly standard except the number of
+     * columns in the GridView is used to create a fake top row of empty views as we use a
+     * transparent ActionBar and don't want the real top row of images to start off covered by it.
+     */
+    private class ImageAdapter extends BaseAdapter {
+
+        private final Context mContext;
+        private int mItemHeight = 0;
+        private int mNumColumns = 0;
+        private int mActionBarHeight = -1;
+        private GridView.LayoutParams mImageViewLayoutParams;
+
+        public ImageAdapter(Context context) {
+            super();
+            mContext = context;
+            mImageViewLayoutParams = new GridView.LayoutParams(
+                    LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+        }
+
+        @Override
+        public int getCount() {
+            // Size of adapter + number of columns for top empty row
+            return mImageWorker.getAdapter().getSize() + mNumColumns;
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return position < mNumColumns ?
+                    null : mImageWorker.getAdapter().getItem(position - mNumColumns);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position < mNumColumns ? 0 : position - mNumColumns;
+        }
+
+        @Override
+        public int getViewTypeCount() {
+            // Two types of views, the normal ImageView and the top row of empty views
+            return 2;
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            return (position < mNumColumns) ? 1 : 0;
+        }
+
+        @Override
+        public boolean hasStableIds() {
+            return true;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup container) {
+            // First check if this is the top row
+            if (position < mNumColumns) {
+                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));
+                return convertView;
+            }
+
+            // Now handle the main ImageView thumbnails
+            ImageView imageView;
+            if (convertView == null) { // if it's not recycled, instantiate and initialize
+                imageView = new ImageView(mContext);
+                imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
+                imageView.setLayoutParams(mImageViewLayoutParams);
+            } else { // Otherwise re-use the converted view
+                imageView = (ImageView) convertView;
+            }
+
+            // Check the height matches our calculated column width
+            if (imageView.getLayoutParams().height != mItemHeight) {
+                imageView.setLayoutParams(mImageViewLayoutParams);
+            }
+
+            // 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);
+            return imageView;
+        }
+
+        /**
+         * Sets the item height. Useful for when we know the column width so the height can be set
+         * to match.
+         *
+         * @param height
+         */
+        public void setItemHeight(int height) {
+            if (height == mItemHeight) {
+                return;
+            }
+            mItemHeight = height;
+            mImageViewLayoutParams =
+                    new GridView.LayoutParams(LayoutParams.MATCH_PARENT, mItemHeight);
+            mImageWorker.setImageSize(height);
+            notifyDataSetChanged();
+        }
+
+        public void setNumColumns(int numColumns) {
+            mNumColumns = numColumns;
+        }
+
+        public int getNumColumns() {
+            return mNumColumns;
+        }
+    }
+}
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
new file mode 100644
index 0000000..a9f2166
--- /dev/null
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/DiskLruCache.java
@@ -0,0 +1,336 @@
+/*
+ * 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.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.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * 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.
+ */
+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;
+
+    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 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.
+     *
+     * @param context
+     * @param cacheDir
+     * @param maxByteSize
+     * @return
+     */
+    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);
+        }
+
+        return null;
+    }
+
+    /**
+     * 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
+     */
+    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());
+                }
+            }
+        }
+    }
+
+    private void put(String key, String file) {
+        mLinkedHashMap.put(key, file);
+        cacheSize = mLinkedHashMap.size();
+        cacheByteSize += new File(file).length();
+    }
+
+    /**
+     * 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.
+     */
+    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);
+            }
+        }
+    }
+
+    /**
+     * Get an image from the disk cache.
+     *
+     * @param key The unique identifier for the bitmap
+     * @return The bitmap or null if not found
+     */
+    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");
+                }
+                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);
+                }
+            }
+            return null;
+        }
+    }
+
+    /**
+     * Checks if a specific key exist in the cache.
+     *
+     * @param key The unique identifier for the bitmap
+     * @return true if found, false otherwise
+     */
+    public boolean containsKey(String key) {
+        // See if the key is in our HashMap
+        if (mLinkedHashMap.containsKey(key)) {
+            return true;
+        }
+
+        // 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();
+            }
+        }
+    }
+}
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
new file mode 100644
index 0000000..63eaea4
--- /dev/null
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageCache.java
@@ -0,0 +1,217 @@
+/*
+ * 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.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.util.LruCache;
+import android.util.Log;
+
+import com.example.android.bitmapfun.BuildConfig;
+
+import java.io.File;
+
+/**
+ * This class holds our bitmap caches (memory and disk).
+ */
+public class ImageCache {
+    private static final String TAG = "ImageCache";
+
+    // Default memory cache size
+    private static final int DEFAULT_MEM_CACHE_SIZE = 1024 * 1024 * 5; // 5MB
+
+    // Default disk cache size
+    private static final int DEFAULT_DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
+
+    // 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;
+
+    // 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 DiskLruCache mDiskCache;
+    private LruCache<String, Bitmap> mMemoryCache;
+
+    /**
+     * 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);
+    }
+
+    /**
+     * Creating a new ImageCache object using the default parameters.
+     *
+     * @param context The context to use
+     * @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));
+    }
+
+    /**
+     * 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 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) {
+
+        // Search for, or create an instance of the non-UI RetainFragment
+        final RetainFragment mRetainFragment = RetainFragment.findOrCreateRetainFragment(
+                activity.getSupportFragmentManager());
+
+        // 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);
+            mRetainFragment.setObject(imageCache);
+        }
+
+        return imageCache;
+    }
+
+    /**
+     * 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();
+            }
+        }
+
+        // Set up memory cache
+        if (cacheParams.memoryCacheEnabled) {
+            mMemoryCache = new LruCache<String, Bitmap>(cacheParams.memCacheSize) {
+                /**
+                 * 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);
+                }
+            };
+        }
+    }
+
+    public void addBitmapToCache(String data, Bitmap bitmap) {
+        if (data == null || bitmap == null) {
+            return;
+        }
+
+        // Add to memory cache
+        if (mMemoryCache != null && mMemoryCache.get(data) == null) {
+            mMemoryCache.put(data, bitmap);
+        }
+
+        // Add to disk cache
+        if (mDiskCache != null && !mDiskCache.containsKey(data)) {
+            mDiskCache.put(data, bitmap);
+        }
+    }
+
+    /**
+     * Get from memory cache.
+     *
+     * @param data Unique identifier for which item to get
+     * @return The bitmap if found in cache, null otherwise
+     */
+    public Bitmap getBitmapFromMemCache(String data) {
+        if (mMemoryCache != null) {
+            final Bitmap memBitmap = mMemoryCache.get(data);
+            if (memBitmap != null) {
+                if (BuildConfig.DEBUG) {
+                    Log.d(TAG, "Memory cache hit");
+                }
+                return memBitmap;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get from disk cache.
+     *
+     * @param data Unique identifier for which item to get
+     * @return The bitmap if found in cache, null otherwise
+     */
+    public Bitmap getBitmapFromDiskCache(String data) {
+        if (mDiskCache != null) {
+            return mDiskCache.get(data);
+        }
+        return null;
+    }
+
+    public void clearCaches() {
+        mDiskCache.clearCache();
+        mMemoryCache.evictAll();
+    }
+
+    /**
+     * 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 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 ImageCacheParams(String uniqueName) {
+            this.uniqueName = uniqueName;
+        }
+    }
+}
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
new file mode 100644
index 0000000..8b19dc3
--- /dev/null
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageFetcher.java
@@ -0,0 +1,177 @@
+/*
+ * 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.content.Context;
+import android.graphics.Bitmap;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.example.android.bitmapfun.BuildConfig;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+/**
+ * A simple subclass of {@link ImageResizer} that fetches and resizes images fetched from a URL.
+ */
+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";
+
+    /**
+     * Initialize providing a target image width and height for the processing images.
+     *
+     * @param context
+     * @param imageWidth
+     * @param imageHeight
+     */
+    public ImageFetcher(Context context, int imageWidth, int imageHeight) {
+        super(context, imageWidth, imageHeight);
+        init(context);
+    }
+
+    /**
+     * Initialize providing a single target image size (used for both width and height);
+     *
+     * @param context
+     * @param imageSize
+     */
+    public ImageFetcher(Context context, int imageSize) {
+        super(context, imageSize);
+        init(context);
+    }
+
+    private void init(Context context) {
+        checkConnection(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();
+            Log.e(TAG, "checkConnection - no connection found");
+        }
+    }
+
+    /**
+     * The main process method, which will be called by the ImageWorker in the AsyncTask background
+     * thread.
+     *
+     * @param data The data to load the bitmap, in this case, a regular http URL
+     * @return The downloaded and resized bitmap
+     */
+    private Bitmap processBitmap(String data) {
+        if (BuildConfig.DEBUG) {
+            Log.d(TAG, "processBitmap - " + data);
+        }
+
+        // Download a bitmap, write it to a file
+        final File f = downloadBitmap(mContext, data);
+
+        if (f != null) {
+            // Return a sampled down version
+            return decodeSampledBitmapFromFile(f.toString(), mImageWidth, mImageHeight);
+        }
+
+        return null;
+    }
+
+    @Override
+    protected Bitmap processBitmap(Object data) {
+        return processBitmap(String.valueOf(data));
+    }
+
+    /**
+     * Download a bitmap from a URL, write it to a disk and return the File pointer. This
+     * implementation uses a simple disk cache.
+     *
+     * @param context The context to use
+     * @param urlString The URL to fetch
+     * @return A File pointing to the fetched bitmap
+     */
+    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();
+        HttpURLConnection urlConnection = null;
+        BufferedOutputStream out = 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);
+
+            int b;
+            while ((b = in.read()) != -1) {
+                out.write(b);
+            }
+
+            return cacheFile;
+
+        } catch (final IOException e) {
+            Log.e(TAG, "Error in downloadBitmap - " + e);
+        } finally {
+            if (urlConnection != null) {
+                urlConnection.disconnect();
+            }
+            if (out != null) {
+                try {
+                    out.close();
+                } catch (final IOException e) {
+                    Log.e(TAG, "Error in downloadBitmap - " + e);
+                }
+            }
+        }
+
+        return null;
+    }
+}
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
new file mode 100644
index 0000000..18d1f82
--- /dev/null
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageResizer.java
@@ -0,0 +1,198 @@
+/*
+ * 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.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+
+import com.example.android.bitmapfun.BuildConfig;
+
+/**
+ * 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";
+    protected int mImageWidth;
+    protected int mImageHeight;
+
+    /**
+     * Initialize providing a single target image size (used for both width and height);
+     *
+     * @param context
+     * @param imageWidth
+     * @param imageHeight
+     */
+    public ImageResizer(Context context, int imageWidth, int imageHeight) {
+        super(context);
+        setImageSize(imageWidth, imageHeight);
+    }
+
+    /**
+     * Initialize providing a single target image size (used for both width and height);
+     *
+     * @param context
+     * @param imageSize
+     */
+    public ImageResizer(Context context, int imageSize) {
+        super(context);
+        setImageSize(imageSize);
+    }
+
+    /**
+     * Set the target image width and height.
+     *
+     * @param width
+     * @param height
+     */
+    public void setImageSize(int width, int height) {
+        mImageWidth = width;
+        mImageHeight = height;
+    }
+
+    /**
+     * Set the target image size (width and height will be the same).
+     *
+     * @param size
+     */
+    public void setImageSize(int size) {
+        setImageSize(size, size);
+    }
+
+    /**
+     * The main processing method. This happens in a background task. In this case we are just
+     * sampling down the bitmap and returning it from a resource.
+     *
+     * @param resId
+     * @return
+     */
+    private Bitmap processBitmap(int resId) {
+        if (BuildConfig.DEBUG) {
+            Log.d(TAG, "processBitmap - " + resId);
+        }
+        return decodeSampledBitmapFromResource(
+                mContext.getResources(), resId, mImageWidth, mImageHeight);
+    }
+
+    @Override
+    protected Bitmap processBitmap(Object data) {
+        return processBitmap(Integer.parseInt(String.valueOf(data)));
+    }
+
+    /**
+     * Decode and sample down a bitmap from resources to the requested width and height.
+     *
+     * @param res The resources object containing the image data
+     * @param resId The resource id of the image data
+     * @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 decodeSampledBitmapFromResource(Resources res, int resId,
+            int reqWidth, int reqHeight) {
+
+        // First decode with inJustDecodeBounds=true to check dimensions
+        final BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inJustDecodeBounds = true;
+        BitmapFactory.decodeResource(res, resId, options);
+
+        // Calculate inSampleSize
+        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
+
+        // Decode bitmap with inSampleSize set
+        options.inJustDecodeBounds = false;
+        return BitmapFactory.decodeResource(res, resId, options);
+    }
+
+    /**
+     * Decode and sample down a bitmap from a file to the requested width and height.
+     *
+     * @param filename The full path of the file to decode
+     * @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 synchronized Bitmap decodeSampledBitmapFromFile(String filename,
+            int reqWidth, int reqHeight) {
+
+        // First decode with inJustDecodeBounds=true to check dimensions
+        final BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inJustDecodeBounds = true;
+        BitmapFactory.decodeFile(filename, options);
+
+        // Calculate inSampleSize
+        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
+
+        // Decode bitmap with inSampleSize set
+        options.inJustDecodeBounds = false;
+        return BitmapFactory.decodeFile(filename, 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
+     * height equal to or larger than the requested width and height. This implementation does not
+     * ensure a power of 2 is returned for inSampleSize which can be faster when decoding but
+     * results in a larger bitmap which isn't as useful for caching purposes.
+     *
+     * @param options An options object with out* params already populated (run through a decode*
+     *            method with inJustDecodeBounds==true
+     * @param reqWidth The requested width of the resulting bitmap
+     * @param reqHeight The requested height of the resulting bitmap
+     * @return The value to be used for inSampleSize
+     */
+    public static int calculateInSampleSize(BitmapFactory.Options options,
+            int reqWidth, int reqHeight) {
+        // Raw height and width of image
+        final int height = options.outHeight;
+        final int width = options.outWidth;
+        int inSampleSize = 1;
+
+        if (height > reqHeight || width > reqWidth) {
+            if (width > height) {
+                inSampleSize = Math.round((float) height / (float) reqHeight);
+            } else {
+                inSampleSize = Math.round((float) width / (float) reqWidth);
+            }
+
+            // This offers some additional logic in case the image has a strange
+            // aspect ratio. For example, a panorama may have a much larger
+            // width than height. In these cases the total pixels might still
+            // end up being too large to fit comfortably in memory, so we should
+            // be more aggressive with sample down the image (=larger
+            // inSampleSize).
+
+            final float totalPixels = width * height;
+
+            // Anything more than 2x the requested pixels we'll sample down
+            // further.
+            final float totalReqPixelsCap = reqWidth * reqHeight * 2;
+
+            while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
+                inSampleSize++;
+            }
+        }
+        return inSampleSize;
+    }
+}
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
new file mode 100644
index 0000000..a0d2693
--- /dev/null
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageWorker.java
@@ -0,0 +1,364 @@
+/*
+ * 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.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.os.AsyncTask;
+import android.util.Log;
+import android.widget.ImageView;
+
+import com.example.android.bitmapfun.BuildConfig;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * This class wraps up completing some arbitrary long running work when loading a bitmap to an
+ * ImageView. It handles things like using a memory and disk cache, running the work in a background
+ * thread and setting a placeholder image.
+ */
+public abstract class ImageWorker {
+    private static final String TAG = "ImageWorker";
+    private static final int FADE_IN_TIME = 200;
+
+    private ImageCache mImageCache;
+    private Bitmap mLoadingBitmap;
+    private boolean mFadeInBitmap = true;
+    private boolean mExitTasksEarly = false;
+
+    protected Context mContext;
+    protected ImageWorkerAdapter mImageWorkerAdapter;
+
+    protected ImageWorker(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Load an image specified by the data parameter 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.
+     *
+     * @param data The URL of the image to download.
+     * @param imageView The ImageView to bind the downloaded image to.
+     */
+    public void loadImage(Object data, ImageView imageView) {
+        Bitmap bitmap = null;
+
+        if (mImageCache != null) {
+            bitmap = mImageCache.getBitmapFromMemCache(String.valueOf(data));
+        }
+
+        if (bitmap != null) {
+            // Bitmap found in memory cache
+            imageView.setImageBitmap(bitmap);
+        } else if (cancelPotentialWork(data, imageView)) {
+            final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
+            final AsyncDrawable asyncDrawable =
+                    new AsyncDrawable(mContext.getResources(), 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.");
+        }
+    }
+
+    /**
+     * Set placeholder bitmap that shows when the the background thread is running.
+     *
+     * @param bitmap
+     */
+    public void setLoadingImage(Bitmap bitmap) {
+        mLoadingBitmap = bitmap;
+    }
+
+    /**
+     * Set placeholder bitmap that shows when the the background thread is running.
+     *
+     * @param resId
+     */
+    public void setLoadingImage(int resId) {
+        mLoadingBitmap = BitmapFactory.decodeResource(mContext.getResources(), resId);
+    }
+
+    /**
+     * Set the {@link ImageCache} object to use with this ImageWorker.
+     *
+     * @param cacheCallback
+     */
+    public void setImageCache(ImageCache cacheCallback) {
+        mImageCache = cacheCallback;
+    }
+
+    public ImageCache getImageCache() {
+        return mImageCache;
+    }
+
+    /**
+     * 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;
+    }
+
+    public void setExitTasksEarly(boolean exitTasksEarly) {
+        mExitTasksEarly = exitTasksEarly;
+    }
+
+    /**
+     * Subclasses should override this to define any processing or work that must happen to produce
+     * the final bitmap. This will be executed in a background thread and be long running. For
+     * example, you could resize a large bitmap here, or pull down an image from the network.
+     *
+     * @param data The data to identify which image to process, as provided by
+     *            {@link ImageWorker#loadImage(Object, ImageView)}
+     * @return The processed bitmap
+     */
+    protected abstract Bitmap processBitmap(Object data);
+
+    public static void cancelWork(ImageView imageView) {
+        final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+        if (bitmapWorkerTask != null) {
+            bitmapWorkerTask.cancel(true);
+            if (BuildConfig.DEBUG) {
+                final Object bitmapData = bitmapWorkerTask.data;
+                Log.d(TAG, "cancelWork - cancelled work for " + bitmapData);
+            }
+        }
+    }
+
+    /**
+     * Returns true if the current work has been canceled or if there was no work in
+     * progress on this image view.
+     * Returns false if the work in progress deals with the same data. The work is not
+     * stopped in that case.
+     */
+    public static boolean cancelPotentialWork(Object data, ImageView imageView) {
+        final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+
+        if (bitmapWorkerTask != null) {
+            final Object bitmapData = bitmapWorkerTask.data;
+            if (bitmapData == null || !bitmapData.equals(data)) {
+                bitmapWorkerTask.cancel(true);
+                if (BuildConfig.DEBUG) {
+                    Log.d(TAG, "cancelPotentialWork - cancelled work for " + data);
+                }
+            } else {
+                // The same work is already in progress.
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * @param imageView Any imageView
+     * @return Retrieve the currently active work task (if any) associated with this imageView.
+     * null if there is no such task.
+     */
+    private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
+        if (imageView != null) {
+            final Drawable drawable = imageView.getDrawable();
+            if (drawable instanceof AsyncDrawable) {
+                final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+                return asyncDrawable.getBitmapWorkerTask();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * The actual AsyncTask that will asynchronously process the image.
+     */
+    private class BitmapWorkerTask extends AsyncTask<Object, Void, Bitmap> {
+        private Object data;
+        private final WeakReference<ImageView> imageViewReference;
+
+        public BitmapWorkerTask(ImageView imageView) {
+            imageViewReference = new WeakReference<ImageView>(imageView);
+        }
+
+        /**
+         * Background processing.
+         */
+        @Override
+        protected Bitmap doInBackground(Object... params) {
+            data = params[0];
+            final String dataString = String.valueOf(data);
+            Bitmap bitmap = null;
+
+            // 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
+            // the cache
+            if (mImageCache != null && !isCancelled() && getAttachedImageView() != null
+                    && !mExitTasksEarly) {
+                bitmap = mImageCache.getBitmapFromDiskCache(dataString);
+            }
+
+            // If the bitmap was not found in the cache 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 call the main
+            // process method (as implemented by a subclass)
+            if (bitmap == null && !isCancelled() && getAttachedImageView() != null
+                    && !mExitTasksEarly) {
+                bitmap = processBitmap(params[0]);
+            }
+
+            // If the bitmap was processed and the image cache is available, then add the processed
+            // bitmap to the cache for future use. Note we don't check if the task was cancelled
+            // here, if it was, and the thread is still running, we may as well add the processed
+            // bitmap to our cache as it might be used again in the future
+            if (bitmap != null && mImageCache != null) {
+                mImageCache.addBitmapToCache(dataString, bitmap);
+            }
+
+            return bitmap;
+        }
+
+        /**
+         * Once the image is processed, associates it to the imageView
+         */
+        @Override
+        protected void onPostExecute(Bitmap bitmap) {
+            // if cancel was called on this task or the "exit early" flag is set then we're done
+            if (isCancelled() || mExitTasksEarly) {
+                bitmap = null;
+            }
+
+            final ImageView imageView = getAttachedImageView();
+            if (bitmap != null && imageView != null) {
+                setImageBitmap(imageView, bitmap);
+            }
+        }
+
+        /**
+         * Returns the ImageView associated with this task as long as the ImageView's task still
+         * points to this task as well. Returns null otherwise.
+         */
+        private ImageView getAttachedImageView() {
+            final ImageView imageView = imageViewReference.get();
+            final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+
+            if (this == bitmapWorkerTask) {
+                return imageView;
+            }
+
+            return null;
+        }
+    }
+
+    /**
+     * A custom Drawable that will be attached to the imageView while the work is in progress.
+     * Contains a reference to the actual worker task, so that it can be stopped if a new binding is
+     * required, and makes sure that only the last started worker process can bind its result,
+     * independently of the finish order.
+     */
+    private static class AsyncDrawable extends BitmapDrawable {
+        private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
+
+        public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
+            super(res, bitmap);
+
+            bitmapWorkerTaskReference =
+                new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
+        }
+
+        public BitmapWorkerTask getBitmapWorkerTask() {
+            return bitmapWorkerTaskReference.get();
+        }
+    }
+
+    /**
+     * Called when the processing is complete and the final bitmap should be set on the ImageView.
+     *
+     * @param imageView
+     * @param bitmap
+     */
+    private void setImageBitmap(ImageView imageView, Bitmap bitmap) {
+        if (mFadeInBitmap) {
+            // Transition drawable with a transparent drwabale and the final bitmap
+            final TransitionDrawable td =
+                    new TransitionDrawable(new Drawable[] {
+                            new ColorDrawable(android.R.color.transparent),
+                            new BitmapDrawable(mContext.getResources(), bitmap)
+                    });
+            // Set background to loading bitmap
+            imageView.setBackgroundDrawable(
+                    new BitmapDrawable(mContext.getResources(), mLoadingBitmap));
+
+            imageView.setImageDrawable(td);
+            td.startTransition(FADE_IN_TIME);
+        } else {
+            imageView.setImageBitmap(bitmap);
+        }
+    }
+
+    /**
+     * Set the simple adapter which holds the backing data.
+     *
+     * @param adapter
+     */
+    public void setAdapter(ImageWorkerAdapter adapter) {
+        mImageWorkerAdapter = adapter;
+    }
+
+    /**
+     * Get the current adapter.
+     *
+     * @return
+     */
+    public ImageWorkerAdapter getAdapter() {
+        return mImageWorkerAdapter;
+    }
+
+    /**
+     * 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();
+    }
+}
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
new file mode 100644
index 0000000..3ee9cd6
--- /dev/null
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/RetainFragment.java
@@ -0,0 +1,83 @@
+/*
+ * 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
new file mode 100644
index 0000000..544df33
--- /dev/null
+++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/Utils.java
@@ -0,0 +1,146 @@
+/*
+ * 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.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 java.io.File;
+
+/**
+ * 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");
+        }
+    }
+
+    /**
+     * 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() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO;
+    }
+
+    /**
+     * Check if ActionBar is available.
+     *
+     * @return
+     */
+    public static boolean hasActionBar() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
+    }
+}