am 2c063c88: Android Training: Threads sample app

* commit '2c063c889aa816e0de91bf17fdc0c78f48d5e2d0':
  Android Training: Threads sample app
diff --git a/samples/training/threadsample/AndroidManifest.xml b/samples/training/threadsample/AndroidManifest.xml
new file mode 100644
index 0000000..7b39207
--- /dev/null
+++ b/samples/training/threadsample/AndroidManifest.xml
@@ -0,0 +1,66 @@
+<?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.threadsample"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+    <uses-sdk
+        android:minSdkVersion="11"
+        android:targetSdkVersion="17" />
+
+    <!-- Requires this permission to download RSS data from Picasa -->
+    <uses-permission android:name="android.permission.INTERNET" />
+    
+    <!-- Requires this permission to check the network state       -->
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+
+    <!--
+        Defines the application.
+    -->
+    <application
+        android:icon="@drawable/icon"
+        android:label="@string/app_name">
+
+        <activity
+            android:name=".DisplayActivity"
+            android:label="@string/activity_title" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <!--
+            No intent filters are specified, so android:exported defaults to "false". The
+            service is only available to this app.
+        -->
+        <service
+            android:name=".RSSPullService"
+            android:exported="false"/>
+
+        <!--
+            The attribute "android:exported" must be set to "false" to restrict this content
+            provider to its own app. Otherwise, all apps could access it.
+         -->
+        <provider
+            android:name=".DataProvider"
+            android:exported="false"
+            android:authorities="@string/authority"/>
+    </application>
+
+</manifest>
diff --git a/samples/training/threadsample/res/drawable-hdpi/icon.png b/samples/training/threadsample/res/drawable-hdpi/icon.png
new file mode 100644
index 0000000..bf461aa
--- /dev/null
+++ b/samples/training/threadsample/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/samples/training/threadsample/res/drawable-ldpi/icon.png b/samples/training/threadsample/res/drawable-ldpi/icon.png
new file mode 100644
index 0000000..e80fe01
--- /dev/null
+++ b/samples/training/threadsample/res/drawable-ldpi/icon.png
Binary files differ
diff --git a/samples/training/threadsample/res/drawable-mdpi/icon.png b/samples/training/threadsample/res/drawable-mdpi/icon.png
new file mode 100644
index 0000000..ba7a853
--- /dev/null
+++ b/samples/training/threadsample/res/drawable-mdpi/icon.png
Binary files differ
diff --git a/samples/training/threadsample/res/drawable-xhdpi/icon.png b/samples/training/threadsample/res/drawable-xhdpi/icon.png
new file mode 100644
index 0000000..72c2b27
--- /dev/null
+++ b/samples/training/threadsample/res/drawable-xhdpi/icon.png
Binary files differ
diff --git a/samples/training/threadsample/res/drawable/decodedecoding.xml b/samples/training/threadsample/res/drawable/decodedecoding.xml
new file mode 100644
index 0000000..0133903
--- /dev/null
+++ b/samples/training/threadsample/res/drawable/decodedecoding.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape android:shape="rectangle"
+  xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- orange -->
+    <solid android:color="#fff8ef66" />
+</shape>
diff --git a/samples/training/threadsample/res/drawable/decodequeued.xml b/samples/training/threadsample/res/drawable/decodequeued.xml
new file mode 100644
index 0000000..fdbbe1a
--- /dev/null
+++ b/samples/training/threadsample/res/drawable/decodequeued.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape android:shape="rectangle"
+  xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- golden yellow -->
+    <solid android:color="#ffaca865" />
+</shape>
diff --git a/samples/training/threadsample/res/drawable/imagedownloadfailed.xml b/samples/training/threadsample/res/drawable/imagedownloadfailed.xml
new file mode 100644
index 0000000..ca1d72f
--- /dev/null
+++ b/samples/training/threadsample/res/drawable/imagedownloadfailed.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape android:shape="rectangle"
+  xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- dark red -->
+    <solid android:color="#ffbf1111" />
+</shape>
diff --git a/samples/training/threadsample/res/drawable/imagedownloading.xml b/samples/training/threadsample/res/drawable/imagedownloading.xml
new file mode 100644
index 0000000..46a707e
--- /dev/null
+++ b/samples/training/threadsample/res/drawable/imagedownloading.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape android:shape="rectangle"
+  xmlns:android="http://schemas.android.com/apk/res/android">
+    <!--  dark green opaque -->
+    <solid android:color="#ff2a6a22" />
+</shape>
diff --git a/samples/training/threadsample/res/drawable/imagenotqueued.xml b/samples/training/threadsample/res/drawable/imagenotqueued.xml
new file mode 100644
index 0000000..ea2eec5
--- /dev/null
+++ b/samples/training/threadsample/res/drawable/imagenotqueued.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape android:shape="rectangle"
+  xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- opaque near black -->
+    <solid android:color="#ff444444" />
+</shape>
diff --git a/samples/training/threadsample/res/drawable/imagequeued.xml b/samples/training/threadsample/res/drawable/imagequeued.xml
new file mode 100644
index 0000000..f1342b8
--- /dev/null
+++ b/samples/training/threadsample/res/drawable/imagequeued.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape android:shape="rectangle"
+  xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="#ff888888" />
+</shape>
diff --git a/samples/training/threadsample/res/layout/fragmenthost.xml b/samples/training/threadsample/res/layout/fragmenthost.xml
new file mode 100644
index 0000000..39e72f1
--- /dev/null
+++ b/samples/training/threadsample/res/layout/fragmenthost.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.
+ */
+-->
+<LinearLayout
+    android:orientation="horizontal"
+    android:id="@+id/fragmentHost"
+    android:layout_width="fill_parent" android:layout_height="fill_parent"
+    android:animateLayoutChanges="true"
+    xmlns:android="http://schemas.android.com/apk/res/android" />
diff --git a/samples/training/threadsample/res/layout/galleryitem.xml b/samples/training/threadsample/res/layout/galleryitem.xml
new file mode 100644
index 0000000..187ba77
--- /dev/null
+++ b/samples/training/threadsample/res/layout/galleryitem.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.
+ */
+-->
+<!-- Defines a single thumbnail view -->
+<FrameLayout
+        android:layout_width="?android:listPreferredItemHeight"
+        android:layout_height="?android:listPreferredItemHeight"
+        xmlns:android="http://schemas.android.com/apk/res/android">
+    <com.example.android.threadsample.PhotoView
+        android:id="@+id/thumbImage"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:scaleType="centerCrop"
+        android:adjustViewBounds="true" />
+</FrameLayout>
diff --git a/samples/training/threadsample/res/layout/gridlist.xml b/samples/training/threadsample/res/layout/gridlist.xml
new file mode 100644
index 0000000..df07423
--- /dev/null
+++ b/samples/training/threadsample/res/layout/gridlist.xml
@@ -0,0 +1,42 @@
+<?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:layout_gravity="center"
+	android:layout_width="0.0dip" 
+    android:layout_height="fill_parent"
+	android:layout_weight="1.0">
+    <GridView 
+        android:id="@android:id/list"
+		android:alwaysDrawnWithCache="true" 
+        android:layout_width="fill_parent"
+		android:layout_height="wrap_content" 
+        android:cacheColorHint="@android:color/black"/>
+        <LinearLayout 
+            xmlns:android="http://schemas.android.com/apk/res/android"
+            android:id="@+id/progressRoot"
+            android:gravity="center"
+            android:visibility="invisible" 
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent">
+<ProgressBar android:layout_gravity="center_vertical"
+			android:paddingLeft="10.0dip" android:layout_width="wrap_content"
+			android:layout_height="wrap_content" />
+	</LinearLayout>
+</FrameLayout>
diff --git a/samples/training/threadsample/res/layout/photo.xml b/samples/training/threadsample/res/layout/photo.xml
new file mode 100644
index 0000000..93b5ca6
--- /dev/null
+++ b/samples/training/threadsample/res/layout/photo.xml
@@ -0,0 +1,32 @@
+<?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.
+ */
+-->
+<!-- Defines a single full-screen image -->
+<FrameLayout
+    android:layout_width="0.0dip"
+    android:layout_height="fill_parent"
+    android:layout_weight="3.0"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:image="http://schemas.android.com/apk/res/com.example.android.threadsample">
+    <ProgressBar android:layout_gravity="center" android:id="@+id/photoProgress"
+        android:layout_width="wrap_content" android:layout_height="wrap_content" />
+    <com.example.android.threadsample.PhotoView
+        android:id="@+id/photoView" android:layout_width="fill_parent"
+        android:layout_height="fill_parent" android:scaleType="centerInside"
+        image:hideShowSibling="@id/photoProgress" />
+</FrameLayout>
diff --git a/samples/training/threadsample/res/layout/progress.xml b/samples/training/threadsample/res/layout/progress.xml
new file mode 100644
index 0000000..12ff0ed
--- /dev/null
+++ b/samples/training/threadsample/res/layout/progress.xml
@@ -0,0 +1,37 @@
+<?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.
+-->
+<!--
+    Defines the layout that shows the progress of downloading and parsing the RSS fead.
+ -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/progressRoot"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+
+    <!-- Shows an activity indicator -->
+
+    <ProgressBar
+        android:id="@+id/Progress"
+        style="@android:style/Widget.ProgressBar.Large"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:indeterminateOnly="true"
+        android:visibility="visible"/>
+
+</RelativeLayout>
diff --git a/samples/training/threadsample/res/values-sw600dp/bools.xml b/samples/training/threadsample/res/values-sw600dp/bools.xml
new file mode 100644
index 0000000..ccba3a5
--- /dev/null
+++ b/samples/training/threadsample/res/values-sw600dp/bools.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- The reason this is done is that on ICS tablets (>= sw600) , hiding the navigation has no function, but
+causes the side effect of eating a tap that would otherwise be delivered to the framework. -->
+<resources>
+    <bool name="sideBySide">true</bool>
+    <bool name="hideNavigation">false</bool>    
+</resources>
\ No newline at end of file
diff --git a/samples/training/threadsample/res/values-sw600dp/dimens.xml b/samples/training/threadsample/res/values-sw600dp/dimens.xml
new file mode 100644
index 0000000..c3fe548
--- /dev/null
+++ b/samples/training/threadsample/res/values-sw600dp/dimens.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <dimen name="thumbSize">160.0dip</dimen>
+</resources>
diff --git a/samples/training/threadsample/res/values-v11/styles.xml b/samples/training/threadsample/res/values-v11/styles.xml
new file mode 100644
index 0000000..6f91b36
--- /dev/null
+++ b/samples/training/threadsample/res/values-v11/styles.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="Theme.NoBackground" parent="@android:style/Theme.Holo">
+        <item name="android:windowBackground">@android:color/black</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/samples/training/threadsample/res/values-xlarge/bools.xml b/samples/training/threadsample/res/values-xlarge/bools.xml
new file mode 100644
index 0000000..ccba3a5
--- /dev/null
+++ b/samples/training/threadsample/res/values-xlarge/bools.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- The reason this is done is that on ICS tablets (>= sw600) , hiding the navigation has no function, but
+causes the side effect of eating a tap that would otherwise be delivered to the framework. -->
+<resources>
+    <bool name="sideBySide">true</bool>
+    <bool name="hideNavigation">false</bool>    
+</resources>
\ No newline at end of file
diff --git a/samples/training/threadsample/res/values-xlarge/dimens.xml b/samples/training/threadsample/res/values-xlarge/dimens.xml
new file mode 100644
index 0000000..c3fe548
--- /dev/null
+++ b/samples/training/threadsample/res/values-xlarge/dimens.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <dimen name="thumbSize">160.0dip</dimen>
+</resources>
diff --git a/samples/training/threadsample/res/values/attrs.xml b/samples/training/threadsample/res/values/attrs.xml
new file mode 100644
index 0000000..b987298
--- /dev/null
+++ b/samples/training/threadsample/res/values/attrs.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <declare-styleable name="ImageDownloaderView">
+        <!-- The sibling to hide after the image is downloaded 
+             and show when the image is being downloaded -->
+        <attr format="reference" name="hideShowSibling" />
+    </declare-styleable>        
+</resources>
\ No newline at end of file
diff --git a/samples/training/threadsample/res/values/bools.xml b/samples/training/threadsample/res/values/bools.xml
new file mode 100644
index 0000000..6a944d7
--- /dev/null
+++ b/samples/training/threadsample/res/values/bools.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    The reason this is done is that on tablets (>= sw600) , hiding the navigation has no function,
+    but causes the side effect of eating a tap that would otherwise be delivered to the framework.
+-->
+<resources>
+    <bool name="sideBySide">false</bool>
+    <bool name="hideNavigation">true</bool>
+</resources>
diff --git a/samples/training/threadsample/res/values/dimens.xml b/samples/training/threadsample/res/values/dimens.xml
new file mode 100644
index 0000000..c3fe548
--- /dev/null
+++ b/samples/training/threadsample/res/values/dimens.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <dimen name="thumbSize">160.0dip</dimen>
+</resources>
diff --git a/samples/training/threadsample/res/values/strings.xml b/samples/training/threadsample/res/values/strings.xml
new file mode 100644
index 0000000..81d9035
--- /dev/null
+++ b/samples/training/threadsample/res/values/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="authority">com.example.android.threadsample</string>
+    <string name="app_name">Picasa Images</string>
+    <string name="activity_title">Picasa Images</string>
+    <string name="progress_starting_update">Starting Update</string>
+    <string name="progress_connecting">Connecting</string>
+    <string name="progress_parsing">Parsing</string>
+    <string name="progress_writing">Writing to Database</string>
+    <string name="no_connection">In offline mode, and no stored data is available.</string>
+    <string name="alert_description">Alert!</string>
+    <string name="offline_mode">In offline mode. Stored data is shown.</string>
+</resources>
diff --git a/samples/training/threadsample/res/values/styles.xml b/samples/training/threadsample/res/values/styles.xml
new file mode 100644
index 0000000..d293831
--- /dev/null
+++ b/samples/training/threadsample/res/values/styles.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.
+-->
+<!--
+    Defines a style resource for a black background
+ -->
+<resources>
+    <style name="Theme.NoBackground" parent="@android:style/Theme.NoTitleBar">
+        <item name="android:windowBackground">@android:color/black</item>
+    </style>
+</resources>
diff --git a/samples/training/threadsample/src/com/example/android/threadsample/BroadcastNotifier.java b/samples/training/threadsample/src/com/example/android/threadsample/BroadcastNotifier.java
new file mode 100644
index 0000000..120f6e0
--- /dev/null
+++ b/samples/training/threadsample/src/com/example/android/threadsample/BroadcastNotifier.java
@@ -0,0 +1,87 @@
+/*
+ * 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.threadsample;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.v4.content.LocalBroadcastManager;
+
+public class BroadcastNotifier {
+
+    private LocalBroadcastManager mBroadcaster;
+
+    /**
+     * Creates a BroadcastNotifier containing an instance of LocalBroadcastManager.
+     * LocalBroadcastManager is more efficient than BroadcastManager; because it only
+     * broadcasts to components within the app, it doesn't have to do parceling and so forth.
+     *
+     * @param context a Context from which to get the LocalBroadcastManager
+     */
+    public BroadcastNotifier(Context context) {
+
+        // Gets an instance of the support library local broadcastmanager
+        mBroadcaster = LocalBroadcastManager.getInstance(context);
+
+    }
+
+    /**
+     *
+     * Uses LocalBroadcastManager to send an {@link Intent} containing {@code status}. The
+     * {@link Intent} has the action {@code BROADCAST_ACTION} and the category {@code DEFAULT}.
+     *
+     * @param status {@link Integer} denoting a work request status
+     */
+    public void broadcastIntentWithState(int status) {
+
+        Intent localIntent = new Intent();
+
+        // The Intent contains the custom broadcast action for this app
+        localIntent.setAction(Constants.BROADCAST_ACTION);
+
+        // Puts the status into the Intent
+        localIntent.putExtra(Constants.EXTENDED_DATA_STATUS, status);
+        localIntent.addCategory(Intent.CATEGORY_DEFAULT);
+
+        // Broadcasts the Intent
+        mBroadcaster.sendBroadcast(localIntent);
+
+    }
+
+    /**
+     * Uses LocalBroadcastManager to send an {@link String} containing a logcat message.
+     * {@link Intent} has the action {@code BROADCAST_ACTION} and the category {@code DEFAULT}.
+     *
+     * @param logData a {@link String} to insert into the log.
+     */
+    public void notifyProgress(String logData) {
+
+        Intent localIntent = new Intent();
+
+        // The Intent contains the custom broadcast action for this app
+        localIntent.setAction(Constants.BROADCAST_ACTION);
+
+        localIntent.putExtra(Constants.EXTENDED_DATA_STATUS, -1);
+
+        // Puts log data into the Intent
+        localIntent.putExtra(Constants.EXTENDED_STATUS_LOG, logData);
+        localIntent.addCategory(Intent.CATEGORY_DEFAULT);
+
+        // Broadcasts the Intent
+        mBroadcaster.sendBroadcast(localIntent);
+
+    }
+}
diff --git a/samples/training/threadsample/src/com/example/android/threadsample/Constants.java b/samples/training/threadsample/src/com/example/android/threadsample/Constants.java
new file mode 100644
index 0000000..0d8909f
--- /dev/null
+++ b/samples/training/threadsample/src/com/example/android/threadsample/Constants.java
@@ -0,0 +1,91 @@
+/*
+ * 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.threadsample;
+
+import java.util.Locale;
+
+/**
+ *
+ * Constants used by multiple classes in this package
+ */
+public final class Constants {
+
+    // Set to true to turn on verbose logging
+    public static final boolean LOGV = false;
+    
+    // Set to true to turn on debug logging
+    public static final boolean LOGD = true;
+
+    // Custom actions
+    
+    public static final String ACTION_VIEW_IMAGE =
+            "com.example.android.threadsample.ACTION_VIEW_IMAGE";
+
+    public static final String ACTION_ZOOM_IMAGE =
+            "com.example.android.threadsample.ACTION_ZOOM_IMAGE";
+    
+    // Defines a custom Intent action
+    public static final String BROADCAST_ACTION = "com.example.android.threadsample.BROADCAST";
+
+    // Fragment tags
+    public static final String PHOTO_FRAGMENT_TAG =
+            "com.example.android.threadsample.PHOTO_FRAGMENT_TAG";
+    
+    public static final String THUMBNAIL_FRAGMENT_TAG =
+            "com.example.android.threadsample.THUMBNAIL_FRAGMENT_TAG";
+
+    // Defines the key for the status "extra" in an Intent
+    public static final String EXTENDED_DATA_STATUS = "com.example.android.threadsample.STATUS";
+
+    // Defines the key for the log "extra" in an Intent
+    public static final String EXTENDED_STATUS_LOG = "com.example.android.threadsample.LOG";
+    
+    // Defines the key for storing fullscreen state
+    public static final String EXTENDED_FULLSCREEN =
+            "com.example.android.threadsample.EXTENDED_FULLSCREEN";
+
+    /*
+     * A user-agent string that's sent to the HTTP site. It includes information about the device
+     * and the build that the device is running.
+     */
+    public static final String USER_AGENT = "Mozilla/5.0 (Linux; U; Android "
+            + android.os.Build.VERSION.RELEASE + ";"
+            + Locale.getDefault().toString() + "; " + android.os.Build.DEVICE
+            + "/" + android.os.Build.ID + ")";
+
+    // Status values to broadcast to the Activity
+
+    // The download is starting
+    public static final int STATE_ACTION_STARTED = 0;
+
+    // The background thread is connecting to the RSS feed
+    public static final int STATE_ACTION_CONNECTING = 1;
+
+    // The background thread is parsing the RSS feed
+    public static final int STATE_ACTION_PARSING = 2;
+
+    // The background thread is writing data to the content provider
+    public static final int STATE_ACTION_WRITING = 3;
+
+    // The background thread is done
+    public static final int STATE_ACTION_COMPLETE = 4;
+
+    // The background thread is doing logging
+    public static final int STATE_LOG = -1;
+
+    public static final CharSequence BLANK = " ";
+}
diff --git a/samples/training/threadsample/src/com/example/android/threadsample/DataProvider.java b/samples/training/threadsample/src/com/example/android/threadsample/DataProvider.java
new file mode 100644
index 0000000..3741fec
--- /dev/null
+++ b/samples/training/threadsample/src/com/example/android/threadsample/DataProvider.java
@@ -0,0 +1,482 @@
+/*
+ * 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.threadsample;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.util.Log;
+import android.util.SparseArray;
+
+/**
+ *
+ * Defines a ContentProvider that stores URLs of Picasa featured pictures
+ * The provider also has a table that tracks the last time a picture URL was updated.
+ */
+public class DataProvider extends ContentProvider {
+    // Indicates that the incoming query is for a picture URL
+    public static final int IMAGE_URL_QUERY = 1;
+
+    // Indicates that the incoming query is for a URL modification date
+    public static final int URL_DATE_QUERY = 2;
+
+    // Indicates an invalid content URI
+    public static final int INVALID_URI = -1;
+
+    // Constants for building SQLite tables during initialization
+    private static final String TEXT_TYPE = "TEXT";
+    private static final String PRIMARY_KEY_TYPE = "INTEGER PRIMARY KEY";
+    private static final String INTEGER_TYPE = "INTEGER";
+
+    // Defines an SQLite statement that builds the Picasa picture URL table
+    private static final String CREATE_PICTUREURL_TABLE_SQL = "CREATE TABLE" + " " +
+            DataProviderContract.PICTUREURL_TABLE_NAME + " " +
+            "(" + " " +
+            DataProviderContract.ROW_ID + " " + PRIMARY_KEY_TYPE + " ," +
+            DataProviderContract.IMAGE_THUMBURL_COLUMN + " " + TEXT_TYPE + " ," +
+            DataProviderContract.IMAGE_URL_COLUMN + " " + TEXT_TYPE + " ," +
+            DataProviderContract.IMAGE_THUMBNAME_COLUMN + " " + TEXT_TYPE + " ," +
+            DataProviderContract.IMAGE_PICTURENAME_COLUMN + " " + TEXT_TYPE +
+            ")";
+
+    // Defines an SQLite statement that builds the URL modification date table
+    private static final String CREATE_DATE_TABLE_SQL = "CREATE TABLE" + " " +
+            DataProviderContract.DATE_TABLE_NAME + " " +
+            "(" + " " +
+            DataProviderContract.ROW_ID + " " + PRIMARY_KEY_TYPE + " ," +
+            DataProviderContract.DATA_DATE_COLUMN + " " + INTEGER_TYPE +
+            ")";
+
+    // Identifies log statements issued by this component
+    public static final String LOG_TAG = "DataProvider";
+
+    // Defines an helper object for the backing database
+    private SQLiteOpenHelper mHelper;
+
+    // Defines a helper object that matches content URIs to table-specific parameters
+    private static final UriMatcher sUriMatcher;
+
+    // Stores the MIME types served by this provider
+    private static final SparseArray<String> sMimeTypes;
+
+    /*
+     * Initializes meta-data used by the content provider:
+     * - UriMatcher that maps content URIs to codes
+     * - MimeType array that returns the custom MIME type of a table
+     */
+    static {
+        
+        // Creates an object that associates content URIs with numeric codes
+        sUriMatcher = new UriMatcher(0);
+
+        /*
+         * Sets up an array that maps content URIs to MIME types, via a mapping between the
+         * URIs and an integer code. These are custom MIME types that apply to tables and rows
+         * in this particular provider.
+         */
+        sMimeTypes = new SparseArray<String>();
+
+        // Adds a URI "match" entry that maps picture URL content URIs to a numeric code
+        sUriMatcher.addURI(
+                DataProviderContract.AUTHORITY,
+                DataProviderContract.PICTUREURL_TABLE_NAME,
+                IMAGE_URL_QUERY);
+
+        // Adds a URI "match" entry that maps modification date content URIs to a numeric code
+        sUriMatcher.addURI(
+            DataProviderContract.AUTHORITY,
+            DataProviderContract.DATE_TABLE_NAME,
+            URL_DATE_QUERY);
+        
+        // Specifies a custom MIME type for the picture URL table
+        sMimeTypes.put(
+                IMAGE_URL_QUERY,
+                "vnd.android.cursor.dir/vnd." +
+                DataProviderContract.AUTHORITY + "." +
+                DataProviderContract.PICTUREURL_TABLE_NAME);
+
+        // Specifies the custom MIME type for a single modification date row
+        sMimeTypes.put(
+                URL_DATE_QUERY,
+                "vnd.android.cursor.item/vnd."+
+                DataProviderContract.AUTHORITY + "." +
+                DataProviderContract.DATE_TABLE_NAME);
+    }
+
+    // Closes the SQLite database helper class, to avoid memory leaks
+    public void close() {
+        mHelper.close();
+    }
+    
+    /**
+     * Defines a helper class that opens the SQLite database for this provider when a request is
+     * received. If the database doesn't yet exist, the helper creates it.
+     */
+    private class DataProviderHelper extends SQLiteOpenHelper {
+        /**
+         * Instantiates a new SQLite database using the supplied database name and version
+         *
+         * @param context The current context
+         */
+        DataProviderHelper(Context context) {
+            super(context,
+                    DataProviderContract.DATABASE_NAME,
+                    null,
+                    DataProviderContract.DATABASE_VERSION);
+        }
+
+
+        /**
+         * Executes the queries to drop all of the tables from the database.
+         *
+         * @param db A handle to the provider's backing database.
+         */
+        private void dropTables(SQLiteDatabase db) {
+
+            // If the table doesn't exist, don't throw an error
+            db.execSQL("DROP TABLE IF EXISTS " + DataProviderContract.PICTUREURL_TABLE_NAME);
+            db.execSQL("DROP TABLE IF EXISTS " + DataProviderContract.DATE_TABLE_NAME);
+        }
+
+        /**
+         * Does setup of the database. The system automatically invokes this method when
+         * SQLiteDatabase.getWriteableDatabase() or SQLiteDatabase.getReadableDatabase() are
+         * invoked and no db instance is available.
+         *
+         * @param db the database instance in which to create the tables.
+         */
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            // Creates the tables in the backing database for this provider
+            db.execSQL(CREATE_PICTUREURL_TABLE_SQL);
+            db.execSQL(CREATE_DATE_TABLE_SQL);
+
+        }
+
+        /**
+         * Handles upgrading the database from a previous version. Drops the old tables and creates
+         * new ones.
+         *
+         * @param db The database to upgrade
+         * @param version1 The old database version
+         * @param version2 The new database version
+         */
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int version1, int version2) {
+            Log.w(DataProviderHelper.class.getName(),
+                    "Upgrading database from version " + version1 + " to "
+                            + version2 + ", which will destroy all the existing data");
+
+            // Drops all the existing tables in the database
+            dropTables(db);
+
+            // Invokes the onCreate callback to build new tables
+            onCreate(db);
+        }
+        /**
+         * Handles downgrading the database from a new to a previous version. Drops the old tables
+         * and creates new ones.
+         * @param db The database object to downgrade
+         * @param version1 The old database version
+         * @param version2 The new database version
+         */
+        @Override
+        public void onDowngrade(SQLiteDatabase db, int version1, int version2) {
+            Log.w(DataProviderHelper.class.getName(),
+                "Downgrading database from version " + version1 + " to "
+                        + version2 + ", which will destroy all the existing data");
+    
+            // Drops all the existing tables in the database
+            dropTables(db);
+    
+            // Invokes the onCreate callback to build new tables
+            onCreate(db);
+            
+        }
+    }
+    /**
+     * Initializes the content provider. Notice that this method simply creates a
+     * the SQLiteOpenHelper instance and returns. You should do most of the initialization of a
+     * content provider in its static initialization block or in SQLiteDatabase.onCreate().
+     */
+    @Override
+    public boolean onCreate() {
+
+        // Creates a new database helper object
+        mHelper = new DataProviderHelper(getContext());
+
+        return true;
+    }
+    /**
+     * Returns the result of querying the chosen table.
+     * @see android.content.ContentProvider#query(Uri, String[], String, String[], String)
+     * @param uri The content URI of the table
+     * @param projection The names of the columns to return in the cursor
+     * @param selection The selection clause for the query
+     * @param selectionArgs An array of Strings containing search criteria
+     * @param sortOrder A clause defining the order in which the retrieved rows should be sorted
+     * @return The query results, as a {@link android.database.Cursor} of rows and columns
+     */
+    @Override
+    public Cursor query(
+        Uri uri,
+        String[] projection,
+        String selection,
+        String[] selectionArgs,
+        String sortOrder) {
+
+        SQLiteDatabase db = mHelper.getReadableDatabase();
+        // Decodes the content URI and maps it to a code
+        switch (sUriMatcher.match(uri)) {
+
+            // If the query is for a picture URL
+            case IMAGE_URL_QUERY:
+                // Does the query against a read-only version of the database
+                Cursor returnCursor = db.query(
+                    DataProviderContract.PICTUREURL_TABLE_NAME,
+                    projection,
+                    null, null, null, null, null);
+
+                // Sets the ContentResolver to watch this content URI for data changes
+                returnCursor.setNotificationUri(getContext().getContentResolver(), uri);
+                return returnCursor;
+
+            // If the query is for a modification date URL
+            case URL_DATE_QUERY:
+                returnCursor = db.query(
+                    DataProviderContract.DATE_TABLE_NAME,
+                    projection,
+                    selection,
+                    selectionArgs,
+                    null,
+                    null,
+                    sortOrder);
+
+                // No notification Uri is set, because the data doesn't have to be watched.
+                return returnCursor;
+
+            case INVALID_URI:
+
+                throw new IllegalArgumentException("Query -- Invalid URI:" + uri);
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the mimeType associated with the Uri (query).
+     * @see android.content.ContentProvider#getType(Uri)
+     * @param uri the content URI to be checked
+     * @return the corresponding MIMEtype
+     */
+    @Override
+    public String getType(Uri uri) {
+
+        return sMimeTypes.get(sUriMatcher.match(uri));
+    }
+    /**
+     *
+     * Insert a single row into a table
+     * @see android.content.ContentProvider#insert(Uri, ContentValues)
+     * @param uri the content URI of the table
+     * @param values a {@link android.content.ContentValues} object containing the row to insert
+     * @return the content URI of the new row
+     */
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+
+        // Decode the URI to choose which action to take
+        switch (sUriMatcher.match(uri)) {
+
+            // For the modification date table
+            case URL_DATE_QUERY:
+
+                // Creates a writeable database or gets one from cache
+                SQLiteDatabase localSQLiteDatabase = mHelper.getWritableDatabase();
+
+                // Inserts the row into the table and returns the new row's _id value
+                long id = localSQLiteDatabase.insert(
+                        DataProviderContract.DATE_TABLE_NAME,
+                        DataProviderContract.DATA_DATE_COLUMN,
+                        values
+                );
+
+                // If the insert succeeded, notify a change and return the new row's content URI.
+                if (-1 != id) {
+                    getContext().getContentResolver().notifyChange(uri, null);
+                    return Uri.withAppendedPath(uri, Long.toString(id));
+                } else {
+
+                    throw new SQLiteException("Insert error:" + uri);
+                }
+            case IMAGE_URL_QUERY:
+
+                throw new IllegalArgumentException("Insert: Invalid URI" + uri);
+        }
+
+        return null;
+    }
+    /**
+     * Implements bulk row insertion using
+     * {@link SQLiteDatabase#insert(String, String, ContentValues) SQLiteDatabase.insert()}
+     * and SQLite transactions. The method also notifies the current
+     * {@link android.content.ContentResolver} that the {@link android.content.ContentProvider} has
+     * been changed.
+     * @see android.content.ContentProvider#bulkInsert(Uri, ContentValues[])
+     * @param uri The content URI for the insertion
+     * @param insertValuesArray A {@link android.content.ContentValues} array containing the row to
+     * insert
+     * @return The number of rows inserted.
+     */
+    @Override
+    public int bulkInsert(Uri uri, ContentValues[] insertValuesArray) {
+
+        // Decodes the content URI and choose which insert to use
+        switch (sUriMatcher.match(uri)) {
+
+            // picture URLs table
+            case IMAGE_URL_QUERY:
+
+                // Gets a writeable database instance if one is not already cached
+                SQLiteDatabase localSQLiteDatabase = mHelper.getWritableDatabase();
+
+                /*
+                 * Begins a transaction in "exclusive" mode. No other mutations can occur on the
+                 * db until this transaction finishes.
+                 */
+                localSQLiteDatabase.beginTransaction();
+
+                // Deletes all the existing rows in the table
+                localSQLiteDatabase.delete(DataProviderContract.PICTUREURL_TABLE_NAME, null, null);
+
+                // Gets the size of the bulk insert
+                int numImages = insertValuesArray.length;
+
+                // Inserts each ContentValues entry in the array as a row in the database
+                for (int i = 0; i < numImages; i++) {
+
+                    localSQLiteDatabase.insert(DataProviderContract.PICTUREURL_TABLE_NAME,
+                            DataProviderContract.IMAGE_URL_COLUMN, insertValuesArray[i]);
+                }
+
+                // Reports that the transaction was successful and should not be backed out.
+                localSQLiteDatabase.setTransactionSuccessful();
+
+                // Ends the transaction and closes the current db instances
+                localSQLiteDatabase.endTransaction();
+                localSQLiteDatabase.close();
+
+                /*
+                 * Notifies the current ContentResolver that the data associated with "uri" has
+                 * changed.
+                 */
+
+                getContext().getContentResolver().notifyChange(uri, null);
+
+                // The semantics of bulkInsert is to return the number of rows inserted.
+                return numImages;
+
+            // modification date table
+            case URL_DATE_QUERY:
+
+                // Do inserts by calling SQLiteDatabase.insert on each row in insertValuesArray
+                return super.bulkInsert(uri, insertValuesArray);
+
+            case INVALID_URI:
+
+                // An invalid URI was passed. Throw an exception
+                throw new IllegalArgumentException("Bulk insert -- Invalid URI:" + uri);
+
+        }
+
+        return -1;
+
+    }
+    /**
+     * Returns an UnsupportedOperationException if delete is called
+     * @see android.content.ContentProvider#delete(Uri, String, String[])
+     * @param uri The content URI
+     * @param selection The SQL WHERE string. Use "?" to mark places that should be substituted by
+     * values in selectionArgs.
+     * @param selectionArgs An array of values that are mapped to each "?" in selection. If no "?"
+     * are used, set this to NULL.
+     *
+     * @return the number of rows deleted
+     */
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+
+        throw new UnsupportedOperationException("Delete -- unsupported operation " + uri);
+    }
+
+    /**
+     * Updates one or more rows in a table.
+     * @see android.content.ContentProvider#update(Uri, ContentValues, String, String[])
+     * @param uri The content URI for the table
+     * @param values The values to use to update the row or rows. You only need to specify column
+     * names for the columns you want to change. To clear the contents of a column, specify the
+     * column name and NULL for its value.
+     * @param selection An SQL WHERE clause (without the WHERE keyword) specifying the rows to
+     * update. Use "?" to mark places that should be substituted by values in selectionArgs.
+     * @param selectionArgs An array of values that are mapped in order to each "?" in selection.
+     * If no "?" are used, set this to NULL.
+     *
+     * @return int The number of rows updated.
+     */
+    @Override
+    public int update(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+
+        // Decodes the content URI and choose which insert to use
+        switch (sUriMatcher.match(uri)) {
+
+            // A picture URL content URI
+            case URL_DATE_QUERY:
+
+                // Creats a new writeable database or retrieves a cached one
+                SQLiteDatabase localSQLiteDatabase = mHelper.getWritableDatabase();
+
+                // Updates the table
+                int rows = localSQLiteDatabase.update(
+                        DataProviderContract.DATE_TABLE_NAME,
+                        values,
+                        selection,
+                        selectionArgs);
+
+                // If the update succeeded, notify a change and return the number of updated rows.
+                if (0 != rows) {
+                    getContext().getContentResolver().notifyChange(uri, null);
+                    return rows;
+                } else {
+
+                    throw new SQLiteException("Update error:" + uri);
+                }
+
+            case IMAGE_URL_QUERY:
+
+                throw new IllegalArgumentException("Update: Invalid URI: " + uri);
+        }
+
+        return -1;
+    }
+}
diff --git a/samples/training/threadsample/src/com/example/android/threadsample/DataProviderContract.java b/samples/training/threadsample/src/com/example/android/threadsample/DataProviderContract.java
new file mode 100644
index 0000000..e7dd73b
--- /dev/null
+++ b/samples/training/threadsample/src/com/example/android/threadsample/DataProviderContract.java
@@ -0,0 +1,99 @@
+package com.example.android.threadsample;
+
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+/**
+ *
+ * Defines constants for accessing the content provider defined in DataProvider. A content provider
+ * contract assists in accessing the provider's available content URIs, column names, MIME types,
+ * and so forth, without having to know the actual values.
+ */
+public final class DataProviderContract implements BaseColumns {
+
+    private DataProviderContract() { }
+        
+        // The URI scheme used for content URIs
+        public static final String SCHEME = "content";
+
+        // The provider's authority
+        public static final String AUTHORITY = "com.example.android.threadsample";
+
+        /**
+         * The DataProvider content URI
+         */
+        public static final Uri CONTENT_URI = Uri.parse(SCHEME + "://" + AUTHORITY);
+
+        /**
+         *  The MIME type for a content URI that would return multiple rows
+         *  <P>Type: TEXT</P>
+         */
+        public static final String MIME_TYPE_ROWS =
+                "vnd.android.cursor.dir/vnd.com.example.android.threadsample";
+
+        /**
+         * The MIME type for a content URI that would return a single row
+         *  <P>Type: TEXT</P>
+         *
+         */
+        public static final String MIME_TYPE_SINGLE_ROW =
+                "vnd.android.cursor.item/vnd.com.example.android.threadsample";
+
+        /**
+         * Picture table primary key column name
+         */
+        public static final String ROW_ID = BaseColumns._ID;
+        
+        /**
+         * Picture table name
+         */
+        public static final String PICTUREURL_TABLE_NAME = "PictureUrlData";
+
+        /**
+         * Picture table content URI
+         */
+        public static final Uri PICTUREURL_TABLE_CONTENTURI =
+                Uri.withAppendedPath(CONTENT_URI, PICTUREURL_TABLE_NAME);
+
+        /**
+         * Picture table thumbnail URL column name
+         */
+        public static final String IMAGE_THUMBURL_COLUMN = "ThumbUrl";
+        
+        /**
+         * Picture table thumbnail filename column name
+         */
+        public static final String IMAGE_THUMBNAME_COLUMN = "ThumbUrlName";
+        
+        /**
+         * Picture table full picture URL column name
+         */
+        public static final String IMAGE_URL_COLUMN = "ImageUrl";
+        
+        /**
+         * Picture table full picture filename column name
+         */
+        public static final String IMAGE_PICTURENAME_COLUMN = "ImageName";
+        
+        /**
+         * Modification date table name
+         */
+        public static final String DATE_TABLE_NAME = "DateMetadatData";
+
+        /**
+         * Content URI for modification date table
+         */
+        public static final Uri DATE_TABLE_CONTENTURI =
+                Uri.withAppendedPath(CONTENT_URI, DATE_TABLE_NAME);
+
+        /**
+         * Modification date table date column name
+         */
+        public static final String DATA_DATE_COLUMN = "DownloadDate";
+        
+        // The content provider database name
+        public static final String DATABASE_NAME = "PictureDataDB";
+
+        // The starting version of the database
+        public static final int DATABASE_VERSION = 1;
+}
diff --git a/samples/training/threadsample/src/com/example/android/threadsample/DisplayActivity.java b/samples/training/threadsample/src/com/example/android/threadsample/DisplayActivity.java
new file mode 100644
index 0000000..f3abdf2
--- /dev/null
+++ b/samples/training/threadsample/src/com/example/android/threadsample/DisplayActivity.java
@@ -0,0 +1,533 @@
+/*
+ * 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.threadsample;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentManager.OnBackStackChangedListener;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+
+/**
+ * This activity displays Picasa's current featured images. It uses a service running
+ * a background thread to download Picasa's "featured image" RSS feed.
+ * <p>
+ * An IntentHandler is used to communicate between the active Fragment and this
+ * activity. This pattern simulates some of the communication used between
+ * activities, and allows this activity to make choices of how to manage the
+ * fragments.
+ */
+public class DisplayActivity extends FragmentActivity implements OnBackStackChangedListener {
+    
+    // A handle to the main screen view
+    View mMainView;
+    
+    // An instance of the status broadcast receiver
+    DownloadStateReceiver mDownloadStateReceiver;
+    
+    // Tracks whether Fragments are displaying side-by-side
+    boolean mSideBySide;
+    
+    // Tracks whether navigation should be hidden
+    boolean mHideNavigation;
+    
+    // Tracks whether the app is in full-screen mode
+    boolean mFullScreen;
+    
+    // Tracks the number of Fragments on the back stack
+    int mPreviousStackCount;
+    
+    // Instantiates a new broadcast receiver for handling Fragment state
+    private FragmentDisplayer mFragmentDisplayer = new FragmentDisplayer();
+    
+    // Sets a tag to use in logging
+    private static final String CLASS_TAG = "DisplayActivity";
+
+    /**
+     * Sets full screen mode on the device, by setting parameters in the current
+     * window and View
+     * @param fullscreen
+     */
+    public void setFullScreen(boolean fullscreen) {
+        // If full screen is set, sets the fullscreen flag in the Window manager
+        getWindow().setFlags(
+                fullscreen ? WindowManager.LayoutParams.FLAG_FULLSCREEN : 0,
+                WindowManager.LayoutParams.FLAG_FULLSCREEN);
+        
+        // Sets the global fullscreen flag to the current setting
+        mFullScreen = fullscreen;
+
+        // If the platform version is Android 3.0 (Honeycomb) or above
+        if (Build.VERSION.SDK_INT >= 11) {
+            
+            // Sets the View to be "low profile". Status and navigation bar icons will be dimmed
+            int flag = fullscreen ? View.SYSTEM_UI_FLAG_LOW_PROFILE : 0;
+            
+            // If the platform version is Android 4.0 (ICS) or above
+            if (Build.VERSION.SDK_INT >= 14 && fullscreen) {
+                
+                // Hides all of the navigation icons
+                flag |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
+            }
+            
+            // Applies the settings to the screen View
+            mMainView.setSystemUiVisibility(flag);
+
+            // If the user requests a full-screen view, hides the Action Bar.
+            if ( fullscreen ) {
+                this.getActionBar().hide();
+            } else {
+                this.getActionBar().show();
+            }
+        }
+    }
+
+    /*
+     * A callback invoked when the task's back stack changes. This allows the app to
+     * move to the previous state of the Fragment being displayed.
+     *
+     */
+    @Override
+    public void onBackStackChanged() {
+        
+        // Gets the previous global stack count
+        int previousStackCount = mPreviousStackCount;
+        
+        // Gets a FragmentManager instance
+        FragmentManager localFragmentManager = getSupportFragmentManager();
+        
+        // Sets the current back stack count
+        int currentStackCount = localFragmentManager.getBackStackEntryCount();
+        
+        // Re-sets the global stack count to be the current count
+        mPreviousStackCount = currentStackCount;
+        
+        /*
+         * If the current stack count is less than the previous, something was popped off the stack
+         * probably because the user clicked Back.
+         */
+        boolean popping = currentStackCount < previousStackCount;
+        Log.d(CLASS_TAG, "backstackchanged: popping = " + popping);
+        
+        // When going backwards in the back stack, turns off full screen mode.
+        if (popping) {
+            setFullScreen(false);
+        }
+    }
+
+    /*
+     * This callback is invoked by the system when the Activity is being killed
+     * It saves the full screen status, so it can be restored when the Activity is restored
+     *
+     */
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        outState.putBoolean(Constants.EXTENDED_FULLSCREEN, mFullScreen);
+        super.onSaveInstanceState(outState);
+    }
+
+    /*
+     * This callback is invoked when the Activity is first created. It sets up the Activity's
+     * window and initializes the Fragments associated with the Activity
+     */
+    @Override
+    public void onCreate(Bundle stateBundle) {
+        // Sets fullscreen-related flags for the display
+        getWindow().setFlags(
+                WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+                        | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR,
+                WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+                        | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
+        
+        // Calls the super method (required)
+        super.onCreate(stateBundle);
+        
+        // Inflates the main View, which will be the host View for the fragments
+        mMainView = getLayoutInflater().inflate(R.layout.fragmenthost, null);
+        
+        // Sets the content view for the Activity
+        setContentView(mMainView);
+                
+        /*
+         * Creates an intent filter for DownloadStateReceiver that intercepts broadcast Intents
+         */
+        
+        // The filter's action is BROADCAST_ACTION
+        IntentFilter statusIntentFilter = new IntentFilter(
+                Constants.BROADCAST_ACTION);
+        
+        // Sets the filter's category to DEFAULT
+        statusIntentFilter.addCategory(Intent.CATEGORY_DEFAULT);
+        
+        // Instantiates a new DownloadStateReceiver
+        mDownloadStateReceiver = new DownloadStateReceiver();
+        
+        // Registers the DownloadStateReceiver and its intent filters
+        LocalBroadcastManager.getInstance(this).registerReceiver(
+                mDownloadStateReceiver,
+                statusIntentFilter);
+        
+        /*
+         * Creates intent filters for the FragmentDisplayer
+         */
+        
+        // One filter is for the action ACTION_VIEW_IMAGE
+        IntentFilter displayerIntentFilter = new IntentFilter(
+                Constants.ACTION_VIEW_IMAGE);
+        
+        // Adds a data filter for the HTTP scheme
+        displayerIntentFilter.addDataScheme("http");
+        
+        // Registers the receiver
+        LocalBroadcastManager.getInstance(this).registerReceiver(
+                mFragmentDisplayer,
+                displayerIntentFilter);
+       
+        // Creates a second filter for ACTION_ZOOM_IMAGE
+        displayerIntentFilter = new IntentFilter(Constants.ACTION_ZOOM_IMAGE);
+        
+        // Registers the receiver
+        LocalBroadcastManager.getInstance(this).registerReceiver(
+                mFragmentDisplayer,
+                displayerIntentFilter);
+        
+        // Gets an instance of the support library FragmentManager
+        FragmentManager localFragmentManager = getSupportFragmentManager();
+        
+        /*
+         * Detects if side-by-side display should be enabled. It's only available on xlarge and
+         * sw600dp devices (for example, tablets). The setting in res/values/ is "false", but this
+         * is overridden in values-xlarge and values-sw600dp.
+         */
+        mSideBySide = getResources().getBoolean(R.bool.sideBySide);
+        
+        /*
+         * Detects if hiding navigation controls should be enabled. On xlarge andsw600dp, it should
+         * be false, to avoid having the user enter an additional tap.
+         */
+        mHideNavigation = getResources().getBoolean(R.bool.hideNavigation);
+        
+        /*
+         * Adds the back stack change listener defined in this Activity as the listener for the
+         * FragmentManager. See the method onBackStackChanged().
+         */
+        localFragmentManager.addOnBackStackChangedListener(this);
+
+        // If the incoming state of the Activity is null, sets the initial view to be thumbnails
+        if (null == stateBundle) {
+            
+            // Starts a Fragment transaction to track the stack
+            FragmentTransaction localFragmentTransaction = localFragmentManager
+                    .beginTransaction();
+            
+            // Adds the PhotoThumbnailFragment to the host View
+            localFragmentTransaction.add(R.id.fragmentHost,
+                    new PhotoThumbnailFragment(), Constants.THUMBNAIL_FRAGMENT_TAG);
+            
+            // Commits this transaction to display the Fragment
+            localFragmentTransaction.commit();
+            
+        // The incoming state of the Activity isn't null.
+        } else {
+            
+            // Gets the previous state of the fullscreen indicator
+            mFullScreen = stateBundle.getBoolean(Constants.EXTENDED_FULLSCREEN);
+            
+            // Sets the fullscreen flag to its previous state
+            setFullScreen(mFullScreen);
+            
+            // Gets the previous backstack entry count.
+            mPreviousStackCount = localFragmentManager.getBackStackEntryCount();
+        }
+    }
+
+    /*
+     * This callback is invoked when the system is about to destroy the Activity.
+     */
+    @Override
+    public void onDestroy() {
+        
+        // If the DownloadStateReceiver still exists, unregister it and set it to null
+        if (mDownloadStateReceiver != null) {
+            LocalBroadcastManager.getInstance(this).unregisterReceiver(mDownloadStateReceiver);
+            mDownloadStateReceiver = null;
+        }
+        
+        // Unregisters the FragmentDisplayer instance
+        LocalBroadcastManager.getInstance(this).unregisterReceiver(this.mFragmentDisplayer);
+        
+        // Sets the main View to null
+        mMainView = null;
+        
+        // Must always call the super method at the end.
+        super.onDestroy();
+    }
+
+    /*
+     * This callback is invoked when the system is stopping the Activity. It stops
+     * background threads.
+     */
+    @Override
+    protected void onStop() {
+        
+        // Cancel all the running threads managed by the PhotoManager
+        PhotoManager.cancelAll();
+        super.onStop();
+    }
+
+    /**
+     * This class uses the BroadcastReceiver framework to detect and handle status messages from
+     * the service that downloads URLs.
+     */
+    private class DownloadStateReceiver extends BroadcastReceiver {
+        
+        private DownloadStateReceiver() {
+            
+            // prevents instantiation by other packages.
+        }
+        /**
+         *
+         * This method is called by the system when a broadcast Intent is matched by this class'
+         * intent filters
+         *
+         * @param context An Android context
+         * @param intent The incoming broadcast Intent
+         */
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            
+            /*
+             * Gets the status from the Intent's extended data, and chooses the appropriate action
+             */
+            switch (intent.getIntExtra(Constants.EXTENDED_DATA_STATUS,
+                    Constants.STATE_ACTION_COMPLETE)) {
+                
+                // Logs "started" state
+                case Constants.STATE_ACTION_STARTED:
+                    if (Constants.LOGD) {
+                        
+                        Log.d(CLASS_TAG, "State: STARTED");
+                    }
+                    break;
+                // Logs "connecting to network" state
+                case Constants.STATE_ACTION_CONNECTING:
+                    if (Constants.LOGD) {
+                        
+                        Log.d(CLASS_TAG, "State: CONNECTING");
+                    }
+                    break;
+                 // Logs "parsing the RSS feed" state
+                 case Constants.STATE_ACTION_PARSING:
+                    if (Constants.LOGD) {
+                        
+                        Log.d(CLASS_TAG, "State: PARSING");
+                    }
+                    break;
+                // Logs "Writing the parsed data to the content provider" state
+                case Constants.STATE_ACTION_WRITING:
+                    if (Constants.LOGD) {
+                        
+                        Log.d(CLASS_TAG, "State: WRITING");
+                    }
+                    break;
+                // Starts displaying data when the RSS download is complete
+                case Constants.STATE_ACTION_COMPLETE:
+                    // Logs the status
+                    if (Constants.LOGD) {
+                        
+                        Log.d(CLASS_TAG, "State: COMPLETE");
+                    }
+
+                    // Finds the fragment that displays thumbnails
+                    PhotoThumbnailFragment localThumbnailFragment =
+                            (PhotoThumbnailFragment) getSupportFragmentManager().findFragmentByTag(
+                                    Constants.THUMBNAIL_FRAGMENT_TAG);
+                    
+                    // If the thumbnail Fragment is hidden, don't change its display status
+                    if ((localThumbnailFragment == null)
+                            || (!localThumbnailFragment.isVisible()))
+                        return;
+                    
+                    // Indicates that the thumbnail Fragment is visible
+                    localThumbnailFragment.setLoaded(true);
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+    
+    /**
+     * This class uses the broadcast receiver framework to detect incoming broadcast Intents
+     * and change the currently-visible fragment based on the Intent action.
+     * It adds or replaces Fragments as necessary, depending on how much screen real-estate is
+     * available.
+     */
+    private class FragmentDisplayer extends BroadcastReceiver {
+        
+        // Default null constructor
+        public FragmentDisplayer() {
+            
+            // Calls the constructor for BroadcastReceiver
+            super();
+        }
+        /**
+         * Receives broadcast Intents for viewing or zooming pictures, and displays the
+         * appropriate Fragment.
+         *
+         * @param context The current Context of the callback
+         * @param intent The broadcast Intent that triggered the callback
+         */
+        @Override
+        public void onReceive(Context context, Intent intent) {
+
+            // Declares a local FragmentManager instance
+            FragmentManager fragmentManager1;
+            
+            // Declares a local instance of the Fragment that displays photos
+            PhotoFragment photoFragment;
+            
+            // Stores a string representation of the URL in the incoming Intent
+            String urlString;
+            
+            // If the incoming Intent is a request is to view an image
+            if (intent.getAction().equals(Constants.ACTION_VIEW_IMAGE)) {
+                
+                // Gets an instance of the support library fragment manager
+                fragmentManager1 = getSupportFragmentManager();
+                
+                // Gets a handle to the Fragment that displays photos
+                photoFragment =
+                        (PhotoFragment) fragmentManager1.findFragmentByTag(
+                            Constants.PHOTO_FRAGMENT_TAG
+                );
+                
+                // Gets the URL of the picture to display
+                urlString = intent.getDataString();
+                
+                // If the photo Fragment exists from a previous display
+                if (null != photoFragment) {
+                    
+                    // If the incoming URL is not already being displayed
+                    if (!urlString.equals(photoFragment.getURLString())) {
+                        
+                        // Sets the Fragment to use the URL from the Intent for the photo
+                        photoFragment.setPhoto(urlString);
+                        
+                        // Loads the photo into the Fragment
+                        photoFragment.loadPhoto();
+                    }
+                
+                // If the Fragment doesn't already exist
+                } else {
+                    // Instantiates a new Fragment
+                    photoFragment = new PhotoFragment();
+                    
+                    // Sets the Fragment to use the URL from the Intent for the photo
+                    photoFragment.setPhoto(urlString);
+                    
+                    // Starts a new Fragment transaction
+                    FragmentTransaction localFragmentTransaction2 =
+                            fragmentManager1.beginTransaction();
+                    
+                    // If the fragments are side-by-side, adds the photo Fragment to the display
+                    if (mSideBySide) {
+                        localFragmentTransaction2.add(
+                                R.id.fragmentHost,
+                                photoFragment,
+                                Constants.PHOTO_FRAGMENT_TAG
+                        );
+                    /*
+                     * If the Fragments are not side-by-side, replaces the current Fragment with
+                     * the photo Fragment
+                     */
+                    } else {
+                        localFragmentTransaction2.replace(
+                                R.id.fragmentHost,
+                                photoFragment,
+                                Constants.PHOTO_FRAGMENT_TAG);
+                    }
+                    
+                    // Don't remember the transaction (sets the Fragment backstack to null)
+                    localFragmentTransaction2.addToBackStack(null);
+                    
+                    // Commits the transaction
+                    localFragmentTransaction2.commit();
+                }
+                
+                // If not in side-by-side mode, sets "full screen", so that no controls are visible
+                if (!mSideBySide) setFullScreen(true);
+                
+            /*
+             * If the incoming Intent is a request to zoom in on an existing image
+             * (Notice that zooming is only supported on large-screen devices)
+             */
+            } else if (intent.getAction().equals(Constants.ACTION_ZOOM_IMAGE)) {
+                
+                // If the Fragments are being displayed side-by-side
+                if (mSideBySide) {
+                    
+                    // Gets another instance of the FragmentManager
+                    FragmentManager localFragmentManager2 = getSupportFragmentManager();
+                    
+                    // Gets a thumbnail Fragment instance
+                    PhotoThumbnailFragment localThumbnailFragment =
+                            (PhotoThumbnailFragment) localFragmentManager2.findFragmentByTag(
+                                Constants.THUMBNAIL_FRAGMENT_TAG);
+                    
+                    // If the instance exists from a previous display
+                    if (null != localThumbnailFragment) {
+                        
+                        // if the existing instance is visible
+                        if (localThumbnailFragment.isVisible()) {
+                            
+                            // Starts a fragment transaction
+                            FragmentTransaction localFragmentTransaction2 =
+                                    localFragmentManager2.beginTransaction();
+                            
+                            /*
+                             * Hides the current thumbnail, clears the backstack, and commits the
+                             * transaction
+                             */
+                            localFragmentTransaction2.hide(localThumbnailFragment);
+                            localFragmentTransaction2.addToBackStack(null);
+                            localFragmentTransaction2.commit();
+                        
+                        // If the existing instance is not visible, display it by going "Back"
+                        } else {
+                            
+                            // Pops the back stack to show the previous Fragment state
+                            localFragmentManager2.popBackStack();
+                        }
+                    }
+                    
+                    // Removes controls from the screen
+                    setFullScreen(true);
+                }
+            }
+        }
+    }
+    
+}
diff --git a/samples/training/threadsample/src/com/example/android/threadsample/PhotoDecodeRunnable.java b/samples/training/threadsample/src/com/example/android/threadsample/PhotoDecodeRunnable.java
new file mode 100644
index 0000000..1f867e5
--- /dev/null
+++ b/samples/training/threadsample/src/com/example/android/threadsample/PhotoDecodeRunnable.java
@@ -0,0 +1,288 @@
+/*
+ * 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.threadsample;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+
+/**
+ * This runnable decodes a byte array containing an image.
+ *
+ * Objects of this class are instantiated and managed by instances of PhotoTask, which
+ * implements the methods {@link TaskRunnableDecodeMethods}. PhotoTask objects call
+ * {@link #PhotoDecodeRunnable(TaskRunnableDecodeMethods) PhotoDecodeRunnable()} with
+ * themselves as the argument. In effect, an PhotoTask object and a
+ * PhotoDecodeRunnable object communicate through the fields of the PhotoTask.
+ *
+ */
+class PhotoDecodeRunnable implements Runnable {
+    
+    // Limits the number of times the decoder tries to process an image
+    private static final int NUMBER_OF_DECODE_TRIES = 2;
+
+    // Tells the Runnable to pause for a certain number of milliseconds
+    private static final long SLEEP_TIME_MILLISECONDS = 250;
+    
+    // Sets the log tag
+    private static final String LOG_TAG = "PhotoDecodeRunnable";
+    
+    // Constants for indicating the state of the decode
+    static final int DECODE_STATE_FAILED = -1;
+    static final int DECODE_STATE_STARTED = 0;
+    static final int DECODE_STATE_COMPLETED = 1;
+    
+    // Defines a field that contains the calling object of type PhotoTask.
+    final TaskRunnableDecodeMethods mPhotoTask;
+    
+    /**
+    *
+    * An interface that defines methods that PhotoTask implements. An instance of
+    * PhotoTask passes itself to an PhotoDecodeRunnable instance through the
+    * PhotoDecodeRunnable constructor, after which the two instances can access each other's
+    * variables.
+    */
+    interface TaskRunnableDecodeMethods {
+        
+        /**
+         * Sets the Thread that this instance is running on
+         * @param currentThread the current Thread
+         */
+        void setImageDecodeThread(Thread currentThread);
+        
+        /**
+         * Returns the current contents of the download buffer
+         * @return The byte array downloaded from the URL in the last read
+         */
+        byte[] getByteBuffer();
+
+        /**
+         * Sets the actions for each state of the PhotoTask instance.
+         * @param state The state being handled.
+         */
+        void handleDecodeState(int state);
+        
+        /**
+         * Returns the desired width of the image, based on the ImageView being created.
+         * @return The target width
+         */
+        int getTargetWidth();
+        
+        /**
+         * Returns the desired height of the image, based on the ImageView being created.
+         * @return The target height.
+         */
+        int getTargetHeight();
+        
+        /**
+         * Sets the Bitmap for the ImageView being displayed.
+         * @param image
+         */
+        void setImage(Bitmap image);
+    }
+
+    /**
+     * This constructor creates an instance of PhotoDownloadRunnable and stores in it a reference
+     * to the PhotoTask instance that instantiated it.
+     *
+     * @param downloadTask The PhotoTask, which implements ImageDecoderRunnableCallback
+     */
+    PhotoDecodeRunnable(TaskRunnableDecodeMethods downloadTask) {
+        mPhotoTask = downloadTask;
+    }
+    
+    /*
+     * Defines this object's task, which is a set of instructions designed to be run on a Thread.
+     */
+    @Override
+    public void run() {
+
+        /*
+         * Stores the current Thread in the the PhotoTask instance, so that the instance
+         * can interrupt the Thread.
+         */
+        mPhotoTask.setImageDecodeThread(Thread.currentThread());
+        
+        /*
+         * Gets the image cache buffer object from the PhotoTask instance. This makes the
+         * to both PhotoDownloadRunnable and PhotoTask.
+         */
+        byte[] imageBuffer = mPhotoTask.getByteBuffer();
+        
+        // Defines the Bitmap object that this thread will create
+        Bitmap returnBitmap = null;
+
+        /*
+         * A try block that decodes a downloaded image.
+         *
+         */
+        try {
+
+            /*
+             * Calls the PhotoTask implementation of {@link #handleDecodeState} to
+             * set the state of the download
+             */
+            mPhotoTask.handleDecodeState(DECODE_STATE_STARTED);
+    
+            // Sets up options for creating a Bitmap object from the
+            // downloaded image.
+            BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
+    
+            /*
+             * Sets the desired image height and width based on the
+             * ImageView being created.
+             */
+            int targetWidth = mPhotoTask.getTargetWidth();
+            int targetHeight = mPhotoTask.getTargetHeight();
+    
+            // Before continuing, checks to see that the Thread hasn't
+            // been interrupted
+            if (Thread.interrupted()) {
+                
+                return;
+            }
+    
+            /*
+             * Even if the decoder doesn't set a Bitmap, this flag tells
+             * the decoder to return the calculated bounds.
+             */
+            bitmapOptions.inJustDecodeBounds = true;
+    
+            /*
+             * First pass of decoding to get scaling and sampling
+             * parameters from the image
+             */
+            BitmapFactory
+                    .decodeByteArray(imageBuffer, 0, imageBuffer.length, bitmapOptions);
+    
+            /*
+             * Sets horizontal and vertical scaling factors so that the
+             * image is expanded or compressed from its actual size to
+             * the size of the target ImageView
+             */
+            int hScale = bitmapOptions.outHeight / targetHeight;
+            int wScale = bitmapOptions.outWidth / targetWidth;
+    
+            /*
+             * Sets the sample size to be larger of the horizontal or
+             * vertical scale factor
+             */
+            //
+            int sampleSize = Math.max(hScale, wScale);
+    
+            /*
+             * If either of the scaling factors is > 1, the image's
+             * actual dimension is larger that the available dimension.
+             * This means that the BitmapFactory must compress the image
+             * by the larger of the scaling factors. Setting
+             * inSampleSize accomplishes this.
+             */
+            if (sampleSize > 1) {
+                bitmapOptions.inSampleSize = sampleSize;
+            }
+    
+            if (Thread.interrupted()) {
+                return;
+            }
+    
+            // Second pass of decoding. If no bitmap is created, nothing
+            // is set in the object.
+            bitmapOptions.inJustDecodeBounds = false;
+    
+            /*
+             * This does the actual decoding of the buffer. If the
+             * decode encounters an an out-of-memory error, it may throw
+             * an Exception or an Error, both of which need to be
+             * handled. Once the problem is handled, the decode is
+             * re-tried.
+             */
+            for (int i = 0; i < NUMBER_OF_DECODE_TRIES; i++) {
+                try {
+                    // Tries to decode the image buffer
+                    returnBitmap = BitmapFactory.decodeByteArray(
+                            imageBuffer,
+                            0,
+                            imageBuffer.length,
+                            bitmapOptions
+                            );
+                    /*
+                     * If the decode works, no Exception or Error has occurred.
+                    break;
+    
+                    /*
+                     * If the decode fails, this block tries to get more memory.
+                     */
+                } catch (Throwable e) {
+    
+                    // Logs an error
+                    Log.e(LOG_TAG, "Out of memory in decode stage. Throttling.");
+    
+                    /*
+                     * Tells the system that garbage collection is
+                     * necessary. Notice that collection may or may not
+                     * occur.
+                     */
+                    java.lang.System.gc();
+    
+                    if (Thread.interrupted()) {
+                        return;
+    
+                    }
+                    /*
+                     * Tries to pause the thread for 250 milliseconds,
+                     * and catches an Exception if something tries to
+                     * activate the thread before it wakes up.
+                     */
+                    try {
+                        Thread.sleep(SLEEP_TIME_MILLISECONDS);
+                    } catch (java.lang.InterruptedException interruptException) {
+                        return;
+                    }
+                }
+            }
+
+            // Catches exceptions if something tries to activate the
+            // Thread incorrectly.
+        } finally {
+            // If the decode failed, there's no bitmap.
+            if (null == returnBitmap) {
+                
+                // Sends a failure status to the PhotoTask
+                mPhotoTask.handleDecodeState(DECODE_STATE_FAILED);
+
+                // Logs the error
+                Log.e(LOG_TAG, "Download failed in PhotoDecodeRunnable");
+    
+            } else {
+                
+                // Sets the ImageView Bitmap
+                mPhotoTask.setImage(returnBitmap);
+                
+                // Reports a status of "completed"
+                mPhotoTask.handleDecodeState(DECODE_STATE_COMPLETED);
+            }
+    
+            // Sets the current Thread to null, releasing its storage
+            mPhotoTask.setImageDecodeThread(null);
+            
+            // Clears the Thread's interrupt flag
+            Thread.interrupted();
+            
+        }
+
+    }
+}
diff --git a/samples/training/threadsample/src/com/example/android/threadsample/PhotoDownloadRunnable.java b/samples/training/threadsample/src/com/example/android/threadsample/PhotoDownloadRunnable.java
new file mode 100644
index 0000000..47b6007
--- /dev/null
+++ b/samples/training/threadsample/src/com/example/android/threadsample/PhotoDownloadRunnable.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright (C) ${year} 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.threadsample;
+
+import com.example.android.threadsample.PhotoDecodeRunnable.TaskRunnableDecodeMethods;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+/**
+ * This task downloads bytes from a resource addressed by a URL.  When the task
+ * has finished, it calls handleState to report its results.
+ *
+ * Objects of this class are instantiated and managed by instances of PhotoTask, which
+ * implements the methods of {@link TaskRunnableDecodeMethods}. PhotoTask objects call
+ * {@link #PhotoDownloadRunnable(TaskRunnableDownloadMethods) PhotoDownloadRunnable()} with
+ * themselves as the argument. In effect, an PhotoTask object and a
+ * PhotoDownloadRunnable object communicate through the fields of the PhotoTask.
+ */
+class PhotoDownloadRunnable implements Runnable {
+    // Sets the size for each read action (bytes)
+    private static final int READ_SIZE = 1024 * 2;
+
+    // Sets a tag for this class
+    @SuppressWarnings("unused")
+    private static final String LOG_TAG = "PhotoDownloadRunnable";
+    
+    // Constants for indicating the state of the download
+    static final int HTTP_STATE_FAILED = -1;
+    static final int HTTP_STATE_STARTED = 0;
+    static final int HTTP_STATE_COMPLETED = 1;
+    
+    // Defines a field that contains the calling object of type PhotoTask.
+    final TaskRunnableDownloadMethods mPhotoTask;
+    
+    /**
+     *
+     * An interface that defines methods that PhotoTask implements. An instance of
+     * PhotoTask passes itself to an PhotoDownloadRunnable instance through the
+     * PhotoDownloadRunnable constructor, after which the two instances can access each other's
+     * variables.
+     */
+    interface TaskRunnableDownloadMethods {
+        
+        /**
+         * Sets the Thread that this instance is running on
+         * @param currentThread the current Thread
+         */
+        void setDownloadThread(Thread currentThread);
+        
+        /**
+         * Returns the current contents of the download buffer
+         * @return The byte array downloaded from the URL in the last read
+         */
+        byte[] getByteBuffer();
+        
+        /**
+         * Sets the current contents of the download buffer
+         * @param buffer The bytes that were just read
+         */
+        void setByteBuffer(byte[] buffer);
+        
+        /**
+         * Defines the actions for each state of the PhotoTask instance.
+         * @param state The current state of the task
+         */
+        void handleDownloadState(int state);
+        
+        /**
+         * Gets the URL for the image being downloaded
+         * @return The image URL
+         */
+        URL getImageURL();
+    }
+    
+    /**
+     * This constructor creates an instance of PhotoDownloadRunnable and stores in it a reference
+     * to the PhotoTask instance that instantiated it.
+     *
+     * @param photoTask The PhotoTask, which implements TaskRunnableDecodeMethods
+     */
+    PhotoDownloadRunnable(TaskRunnableDownloadMethods photoTask) {
+        mPhotoTask = photoTask;
+    }
+    
+    /*
+     * Defines this object's task, which is a set of instructions designed to be run on a Thread.
+     */
+    @SuppressWarnings("resource")
+    @Override
+    public void run() {
+
+        /*
+         * Stores the current Thread in the the PhotoTask instance, so that the instance
+         * can interrupt the Thread.
+         */
+        mPhotoTask.setDownloadThread(Thread.currentThread());
+        
+        // Moves the current Thread into the background
+        android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
+
+        /*
+         * Gets the image cache buffer object from the PhotoTask instance. This makes the
+         * to both PhotoDownloadRunnable and PhotoTask.
+         */
+        byte[] byteBuffer = mPhotoTask.getByteBuffer();
+
+        /*
+         * A try block that downloads a Picasa image from a URL. The URL value is in the field
+         * PhotoTask.mImageURL
+         */
+        // Tries to download the picture from Picasa
+        try {
+            // Before continuing, checks to see that the Thread hasn't been
+            // interrupted
+            if (Thread.interrupted()) {
+                
+                throw new InterruptedException();
+            }
+            
+            // If there's no cache buffer for this image
+            if (null == byteBuffer) {
+
+                /*
+                 * Calls the PhotoTask implementation of {@link #handleDownloadState} to
+                 * set the state of the download
+                 */
+                mPhotoTask.handleDownloadState(HTTP_STATE_STARTED);
+
+                // Defines a handle for the byte download stream
+                InputStream byteStream = null;
+
+                // Downloads the image and catches IO errors
+                try {
+
+                    // Opens an HTTP connection to the image's URL
+                    HttpURLConnection httpConn =
+                            (HttpURLConnection) mPhotoTask.getImageURL().openConnection();
+
+                    // Sets the user agent to report to the server
+                    httpConn.setRequestProperty("User-Agent", Constants.USER_AGENT);
+
+                    // Before continuing, checks to see that the Thread
+                    // hasn't been interrupted
+                    if (Thread.interrupted()) {
+                     
+                        throw new InterruptedException();
+                    }
+                    // Gets the input stream containing the image
+                    byteStream = httpConn.getInputStream();
+
+                    if (Thread.interrupted()) {
+                     
+                        throw new InterruptedException();
+                    }
+                    /*
+                     * Gets the size of the file being downloaded. This
+                     * may or may not be returned.
+                     */
+                    int contentSize = httpConn.getContentLength();
+
+                    /*
+                     * If the size of the image isn't available
+                     */
+                    if (-1 == contentSize) {
+
+                        // Allocates a temporary buffer
+                        byte[] tempBuffer = new byte[READ_SIZE];
+
+                        // Records the initial amount of available space
+                        int bufferLeft = tempBuffer.length;
+
+                        /*
+                         * Defines the initial offset of the next available
+                         * byte in the buffer, and the initial result of
+                         * reading the binary
+                         */
+                        int bufferOffset = 0;
+                        int readResult = 0;
+
+                        /*
+                         * The "outer" loop continues until all the bytes
+                         * have been downloaded. The inner loop continues
+                         * until the temporary buffer is full, and then
+                         * allocates more buffer space.
+                         */
+                        outer: do {
+                            while (bufferLeft > 0) {
+
+                                /*
+                                 * Reads from the URL location into
+                                 * the temporary buffer, starting at the
+                                 * next available free byte and reading as
+                                 * many bytes as are available in the
+                                 * buffer.
+                                 */
+                                readResult = byteStream.read(tempBuffer, bufferOffset,
+                                        bufferLeft);
+
+                                /*
+                                 * InputStream.read() returns zero when the
+                                 * file has been completely read.
+                                 */
+                                if (readResult < 0) {
+                                    // The read is finished, so this breaks
+                                    // the to "outer" loop
+                                    break outer;
+                                }
+
+                                /*
+                                 * The read isn't finished. This sets the
+                                 * next available open position in the
+                                 * buffer (the buffer index is 0-based).
+                                 */
+                                bufferOffset += readResult;
+
+                                // Subtracts the number of bytes read from
+                                // the amount of buffer left
+                                bufferLeft -= readResult;
+
+                                if (Thread.interrupted()) {
+                                    
+                                    throw new InterruptedException();
+                                }
+                            }
+                            /*
+                             * The temporary buffer is full, so the
+                             * following code creates a new buffer that can
+                             * contain the existing contents plus the next
+                             * read cycle.
+                             */
+
+                            // Resets the amount of buffer left to be the
+                            // max buffer size
+                            bufferLeft = READ_SIZE;
+
+                            /*
+                             * Sets a new size that can contain the existing
+                             * buffer's contents plus space for the next
+                             * read cycle.
+                             */
+                            int newSize = tempBuffer.length + READ_SIZE;
+
+                            /*
+                             * Creates a new temporary buffer, moves the
+                             * contents of the old temporary buffer into it,
+                             * and then points the temporary buffer variable
+                             * to the new buffer.
+                             */
+                            byte[] expandedBuffer = new byte[newSize];
+                            System.arraycopy(tempBuffer, 0, expandedBuffer, 0,
+                                    tempBuffer.length);
+                            tempBuffer = expandedBuffer;
+                        } while (true);
+
+                        /*
+                         * When the entire image has been read, this creates
+                         * a permanent byte buffer with the same size as
+                         * the number of used bytes in the temporary buffer
+                         * (equal to the next open byte, because tempBuffer
+                         * is 0=based).
+                         */
+                        byteBuffer = new byte[bufferOffset];
+
+                        // Copies the temporary buffer to the image buffer
+                        System.arraycopy(tempBuffer, 0, byteBuffer, 0, bufferOffset);
+
+                        /*
+                         * The download size is available, so this creates a
+                         * permanent buffer of that length.
+                         */
+                    } else {
+                        byteBuffer = new byte[contentSize];
+
+                        // How much of the buffer still remains empty
+                        int remainingLength = contentSize;
+
+                        // The next open space in the buffer
+                        int bufferOffset = 0;
+
+                        /*
+                         * Reads into the buffer until the number of bytes
+                         * equal to the length of the buffer (the size of
+                         * the image) have been read.
+                         */
+                        while (remainingLength > 0) {
+                            int readResult = byteStream.read(
+                                    byteBuffer,
+                                    bufferOffset,
+                                    remainingLength);
+                            /*
+                             * EOF should not occur, because the loop should
+                             * read the exact # of bytes in the image
+                             */
+                            if (readResult < 0) {
+
+                                // Throws an EOF Exception
+                                throw new EOFException();
+                            }
+
+                            // Moves the buffer offset to the next open byte
+                            bufferOffset += readResult;
+
+                            // Subtracts the # of bytes read from the
+                            // remaining length
+                            remainingLength -= readResult;
+
+                            if (Thread.interrupted()) {
+                                
+                                throw new InterruptedException();
+                            }
+                        }
+                    }
+
+                    if (Thread.interrupted()) {
+                        
+                        throw new InterruptedException();
+                    }
+
+                    // If an IO error occurs, returns immediately
+                } catch (IOException e) {
+                    e.printStackTrace();
+                    return;
+
+                    /*
+                     * If the input stream is still open, close it
+                     */
+                } finally {
+                    if (null != byteStream) {
+                        try {
+                            byteStream.close();
+                        } catch (Exception e) {
+
+                        }
+                    }
+                }
+            }
+            
+            /*
+             * Stores the downloaded bytes in the byte buffer in the PhotoTask instance.
+             */
+            mPhotoTask.setByteBuffer(byteBuffer);
+
+            /*
+             * Sets the status message in the PhotoTask instance. This sets the
+             * ImageView background to indicate that the image is being
+             * decoded.
+             */
+            mPhotoTask.handleDownloadState(HTTP_STATE_COMPLETED);
+      
+        // Catches exceptions thrown in response to a queued interrupt
+        } catch (InterruptedException e1) {
+            
+            // Does nothing
+        
+        // In all cases, handle the results
+        } finally {
+            
+            // If the byteBuffer is null, reports that the download failed.
+            if (null == byteBuffer) {
+                mPhotoTask.handleDownloadState(HTTP_STATE_FAILED);
+            }
+
+            /*
+             * The implementation of setHTTPDownloadThread() in PhotoTask calls
+             * PhotoTask.setCurrentThread(), which then locks on the static ThreadPool
+             * object and returns the current thread. Locking keeps all references to Thread
+             * objects the same until the reference to the current Thread is deleted.
+             */
+            
+            // Sets the reference to the current Thread to null, releasing its storage
+            mPhotoTask.setDownloadThread(null);
+            
+            // Clears the Thread's interrupt flag
+            Thread.interrupted();
+        }
+    }
+}
+
diff --git a/samples/training/threadsample/src/com/example/android/threadsample/PhotoFragment.java b/samples/training/threadsample/src/com/example/android/threadsample/PhotoFragment.java
new file mode 100644
index 0000000..9b2ce01
--- /dev/null
+++ b/samples/training/threadsample/src/com/example/android/threadsample/PhotoFragment.java
@@ -0,0 +1,187 @@
+/*
+ * 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.threadsample;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.ShareCompat;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class PhotoFragment extends Fragment implements View.OnClickListener {
+    // Constants
+    private static final String LOG_TAG = "ImageDownloaderThread";
+    private static final String PHOTO_URL_KEY = "com.example.android.threadsample.PHOTO_URL_KEY";
+    
+    PhotoView mPhotoView;
+    
+    String mURLString;
+    
+    ShareCompat.IntentBuilder mShareCompatIntentBuilder;
+
+    /**
+     * Converts the stored URL string to a URL, and then tries to download the picture from that
+     * URL.
+     */
+    public void loadPhoto() {
+        // If setPhoto() was called to store a URL, proceed
+        if (mURLString != null) {
+            
+            // Handles invalid URLs
+            try {
+                
+                // Converts the URL string to a valid URL
+                URL localURL = new URL(mURLString);
+                
+                /*
+                 * setImageURL(url,false,null) attempts to download and decode the picture at
+                 * at "url" without caching and without providing a Drawable. The result will be
+                 * a BitMap stored in the PhotoView for this Fragment.
+                 */
+                mPhotoView.setImageURL(localURL, false, null);
+                
+            // Catches an invalid URL format
+            } catch (MalformedURLException localMalformedURLException) {
+                localMalformedURLException.printStackTrace();
+            }
+        }
+    }
+    /**
+     * Returns the stored URL string
+     * @return The URL of the picture being shown by this Fragment, in String format
+     */
+    public String getURLString() {
+        return mURLString;
+    }
+
+    /*
+     * This callback is invoked when users click on a displayed image. The input argument is
+     * a handle to the View object that was clicked
+     */
+    @Override
+    public void onClick(View view) {
+        
+        // Sends a broadcast intent to zoom the image
+        Intent localIntent = new Intent(Constants.ACTION_ZOOM_IMAGE);
+        LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(localIntent);
+    }
+
+    /*
+     * This callback is invoked when the Fragment is created.
+     */
+    @Override
+    public void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+    }
+
+    /*
+     * This callback is invoked as the Fragment's View is being constructed.
+     */
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
+        super.onCreateView(inflater, viewGroup, bundle);
+        
+        /*
+         * Creates a View from the specified layout file. The layout uses the parameters specified
+         * in viewGroup, but is not attached to any parent
+         */
+        View localView = inflater.inflate(R.layout.photo, viewGroup, false);
+        
+        // Gets a handle to the PhotoView View in the layout
+        mPhotoView = ((PhotoView) localView.findViewById(R.id.photoView));
+        
+        /*
+         * The click listener becomes this class (PhotoFragment). The onClick() method in this
+         * class is invoked when users click a photo.
+         */
+        mPhotoView.setOnClickListener(this);
+        
+        // If the bundle argument contains data, uses it as a URL for the picture to display
+        if (bundle != null) {
+            mURLString = bundle.getString(PHOTO_URL_KEY);
+        }
+        
+        if (mURLString != null)
+            loadPhoto();
+        
+        // Returns the resulting View
+        return localView;
+    }
+
+    /*
+     * This callback is invoked as the Fragment's View is being destroyed
+     */
+    @Override
+    public void onDestroyView() {
+        // Logs the destroy operation
+        Log.d(LOG_TAG, "onDestroyView");
+        
+        // If the View object still exists, delete references to avoid memory leaks
+        if (mPhotoView != null) {
+            
+            mPhotoView.setOnClickListener(null);
+            this.mPhotoView = null;
+        }
+        
+        // Always call the super method last
+        super.onDestroyView();
+    }
+
+    /*
+     * This callback is invoked when the Fragment is no longer attached to its Activity.
+     * Sets the URL for the Fragment to null
+     */
+    @Override
+    public void onDetach() {
+        // Logs the detach
+        Log.d(LOG_TAG, "onDetach");
+        
+        // Removes the reference to the URL
+        mURLString = null;
+        
+        // Always call the super method last
+        super.onDetach();
+    }
+
+    /*
+     * This callback is invoked if the system asks the Fragment to save its state. This allows the
+     * the system to restart the Fragment later on.
+     */
+    @Override
+    public void onSaveInstanceState(Bundle bundle) {
+        // Always call the super method first
+        super.onSaveInstanceState(bundle);
+        
+        // Puts the current URL for the picture being shown into the saved state
+        bundle.putString(PHOTO_URL_KEY, mURLString);
+    }
+
+    /**
+     * Sets the photo for this Fragment, by storing a URL that points to a picture
+     * @param urlString A String representation of the URL pointing to the picture
+     */
+    public void setPhoto(String urlString) {
+        mURLString = urlString;
+    }
+}
diff --git a/samples/training/threadsample/src/com/example/android/threadsample/PhotoManager.java b/samples/training/threadsample/src/com/example/android/threadsample/PhotoManager.java
new file mode 100644
index 0000000..783b0a3
--- /dev/null
+++ b/samples/training/threadsample/src/com/example/android/threadsample/PhotoManager.java
@@ -0,0 +1,446 @@
+/*
+ * 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.threadsample;
+
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.support.v4.util.LruCache;
+
+import java.net.URL;
+import java.util.Queue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class creates pools of background threads for downloading
+ * Picasa images from the web, based on URLs retrieved from Picasa's featured images RSS feed.
+ * The class is implemented as a singleton; the only way to get an PhotoManager instance is to
+ * call {@link #getInstance}.
+ * <p>
+ * The class sets the pool size and cache size based on the particular operation it's performing.
+ * The algorithm doesn't apply to all situations, so if you re-use the code to implement a pool
+ * of threads for your own app, you will have to come up with your choices for pool size, cache
+ * size, and so forth. In many cases, you'll have to set some numbers arbitrarily and then
+ * measure the impact on performance.
+ * <p>
+ * This class actually uses two threadpools in order to limit the number of
+ * simultaneous image decoding threads to the number of available processor
+ * cores.
+ * <p>
+ * Finally, this class defines a handler that communicates back to the UI
+ * thread to change the bitmap to reflect the state.
+ */
+@SuppressWarnings("unused")
+public class PhotoManager {
+    /*
+     * Status indicators
+     */
+    static final int DOWNLOAD_FAILED = -1;
+    static final int DOWNLOAD_STARTED = 1;
+    static final int DOWNLOAD_COMPLETE = 2;
+    static final int DECODE_STARTED = 3;
+    static final int TASK_COMPLETE = 4;
+
+    // Sets the size of the storage that's used to cache images
+    private static final int IMAGE_CACHE_SIZE = 1024 * 1024 * 4;
+
+    // Sets the amount of time an idle thread will wait for a task before terminating
+    private static final int KEEP_ALIVE_TIME = 1;
+
+    // Sets the Time Unit to seconds
+    private static final TimeUnit KEEP_ALIVE_TIME_UNIT;
+
+    // Sets the initial threadpool size to 8
+    private static final int CORE_POOL_SIZE = 8;
+
+    // Sets the maximum threadpool size to 8
+    private static final int MAXIMUM_POOL_SIZE = 8;
+
+    /**
+     * NOTE: This is the number of total available cores. On current versions of
+     * Android, with devices that use plug-and-play cores, this will return less
+     * than the total number of cores. The total number of cores is not
+     * available in current Android implementations.
+     */
+    private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
+
+    /*
+     * Creates a cache of byte arrays indexed by image URLs. As new items are added to the
+     * cache, the oldest items are ejected and subject to garbage collection.
+     */
+    private final LruCache<URL, byte[]> mPhotoCache;
+
+    // A queue of Runnables for the image download pool
+    private final BlockingQueue<Runnable> mDownloadWorkQueue;
+
+    // A queue of Runnables for the image decoding pool
+    private final BlockingQueue<Runnable> mDecodeWorkQueue;
+
+    // A queue of PhotoManager tasks. Tasks are handed to a ThreadPool.
+    private final Queue<PhotoTask> mPhotoTaskWorkQueue;
+
+    // A managed pool of background download threads
+    private final ThreadPoolExecutor mDownloadThreadPool;
+
+    // A managed pool of background decoder threads
+    private final ThreadPoolExecutor mDecodeThreadPool;
+
+    // An object that manages Messages in a Thread
+    private Handler mHandler;
+
+    // A single instance of PhotoManager, used to implement the singleton pattern
+    private static PhotoManager sInstance = null;
+
+    // A static block that sets class fields
+    static {
+        
+        // The time unit for "keep alive" is in seconds
+        KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
+        
+        // Creates a single static instance of PhotoManager
+        sInstance = new PhotoManager();
+    }
+    /**
+     * Constructs the work queues and thread pools used to download and decode images.
+     */
+    private PhotoManager() {
+
+        /*
+         * Creates a work queue for the pool of Thread objects used for downloading, using a linked
+         * list queue that blocks when the queue is empty.
+         */
+        mDownloadWorkQueue = new LinkedBlockingQueue<Runnable>();
+
+        /*
+         * Creates a work queue for the pool of Thread objects used for decoding, using a linked
+         * list queue that blocks when the queue is empty.
+         */
+        mDecodeWorkQueue = new LinkedBlockingQueue<Runnable>();
+
+        /*
+         * Creates a work queue for the set of of task objects that control downloading and
+         * decoding, using a linked list queue that blocks when the queue is empty.
+         */
+        mPhotoTaskWorkQueue = new LinkedBlockingQueue<PhotoTask>();
+
+        /*
+         * Creates a new pool of Thread objects for the download work queue
+         */
+        mDownloadThreadPool = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
+                KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, mDownloadWorkQueue);
+
+        /*
+         * Creates a new pool of Thread objects for the decoding work queue
+         */
+        mDecodeThreadPool = new ThreadPoolExecutor(NUMBER_OF_CORES, NUMBER_OF_CORES,
+                KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, mDecodeWorkQueue);
+
+        // Instantiates a new cache based on the cache size estimate
+        mPhotoCache = new LruCache<URL, byte[]>(IMAGE_CACHE_SIZE) {
+
+            /*
+             * This overrides the default sizeOf() implementation to return the
+             * correct size of each cache entry.
+             */
+
+            @Override
+            protected int sizeOf(URL paramURL, byte[] paramArrayOfByte) {
+                return paramArrayOfByte.length;
+            }
+        };
+        /*
+         * Instantiates a new anonymous Handler object and defines its
+         * handleMessage() method. The Handler *must* run on the UI thread, because it moves photo
+         * Bitmaps from the PhotoTask object to the View object.
+         * To force the Handler to run on the UI thread, it's defined as part of the PhotoManager
+         * constructor. The constructor is invoked when the class is first referenced, and that
+         * happens when the View invokes startDownload. Since the View runs on the UI Thread, so
+         * does the constructor and the Handler.
+         */
+        mHandler = new Handler(Looper.getMainLooper()) {
+
+            /*
+             * handleMessage() defines the operations to perform when the
+             * Handler receives a new Message to process.
+             */
+            @Override
+            public void handleMessage(Message inputMessage) {
+
+                // Gets the image task from the incoming Message object.
+                PhotoTask photoTask = (PhotoTask) inputMessage.obj;
+
+                // Sets an PhotoView that's a weak reference to the
+                // input ImageView
+                PhotoView localView = photoTask.getPhotoView();
+
+                // If this input view isn't null
+                if (localView != null) {
+
+                    /*
+                     * Gets the URL of the *weak reference* to the input
+                     * ImageView. The weak reference won't have changed, even if
+                     * the input ImageView has.
+                     */
+                    URL localURL = localView.getLocation();
+
+                    /*
+                     * Compares the URL of the input ImageView to the URL of the
+                     * weak reference. Only updates the bitmap in the ImageView
+                     * if this particular Thread is supposed to be serving the
+                     * ImageView.
+                     */
+                    if (photoTask.getImageURL() == localURL)
+
+                        /*
+                         * Chooses the action to take, based on the incoming message
+                         */
+                        switch (inputMessage.what) {
+
+                            // If the download has started, sets background color to dark green
+                            case DOWNLOAD_STARTED:
+                                localView.setStatusResource(R.drawable.imagedownloading);
+                                break;
+
+                            /*
+                             * If the download is complete, but the decode is waiting, sets the
+                             * background color to golden yellow
+                             */
+                            case DOWNLOAD_COMPLETE:
+                                // Sets background color to golden yellow
+                                localView.setStatusResource(R.drawable.decodequeued);
+                                break;
+                            // If the decode has started, sets background color to orange
+                            case DECODE_STARTED:
+                                localView.setStatusResource(R.drawable.decodedecoding);
+                                break;
+                            /*
+                             * The decoding is done, so this sets the
+                             * ImageView's bitmap to the bitmap in the
+                             * incoming message
+                             */
+                            case TASK_COMPLETE:
+                                localView.setImageBitmap(photoTask.getImage());
+                                recycleTask(photoTask);
+                                break;
+                            // The download failed, sets the background color to dark red
+                            case DOWNLOAD_FAILED:
+                                localView.setStatusResource(R.drawable.imagedownloadfailed);
+                                
+                                // Attempts to re-use the Task object
+                                recycleTask(photoTask);
+                                break;
+                            default:
+                                // Otherwise, calls the super method
+                                super.handleMessage(inputMessage);
+                        }
+                }
+            }
+        };
+    }
+
+    /**
+     * Returns the PhotoManager object
+     * @return The global PhotoManager object
+     */
+    public static PhotoManager getInstance() {
+
+        return sInstance;
+    }
+    
+    /**
+     * Handles state messages for a particular task object
+     * @param photoTask A task object
+     * @param state The state of the task
+     */
+    @SuppressLint("HandlerLeak")
+    public void handleState(PhotoTask photoTask, int state) {
+        switch (state) {
+            
+            // The task finished downloading and decoding the image
+            case TASK_COMPLETE:
+                
+                // Puts the image into cache
+                if (photoTask.isCacheEnabled()) {
+                    // If the task is set to cache the results, put the buffer
+                    // that was
+                    // successfully decoded into the cache
+                    mPhotoCache.put(photoTask.getImageURL(), photoTask.getByteBuffer());
+                }
+                
+                // Gets a Message object, stores the state in it, and sends it to the Handler
+                Message completeMessage = mHandler.obtainMessage(state, photoTask);
+                completeMessage.sendToTarget();
+                break;
+            
+            // The task finished downloading the image
+            case DOWNLOAD_COMPLETE:
+                /*
+                 * Decodes the image, by queuing the decoder object to run in the decoder
+                 * thread pool
+                 */
+                mDecodeThreadPool.execute(photoTask.getPhotoDecodeRunnable());
+            
+            // In all other cases, pass along the message without any other action.
+            default:
+                mHandler.obtainMessage(state, photoTask).sendToTarget();
+                break;
+        }
+
+    }
+
+    /**
+     * Cancels all Threads in the ThreadPool
+     */
+    public static void cancelAll() {
+
+        /*
+         * Creates an array of tasks that's the same size as the task work queue
+         */
+        PhotoTask[] taskArray = new PhotoTask[sInstance.mDownloadWorkQueue.size()];
+
+        // Populates the array with the task objects in the queue
+        sInstance.mDownloadWorkQueue.toArray(taskArray);
+
+        // Stores the array length in order to iterate over the array
+        int taskArraylen = taskArray.length;
+
+        /*
+         * Locks on the singleton to ensure that other processes aren't mutating Threads, then
+         * iterates over the array of tasks and interrupts the task's current Thread.
+         */
+        synchronized (sInstance) {
+            
+            // Iterates over the array of tasks
+            for (int taskArrayIndex = 0; taskArrayIndex < taskArraylen; taskArrayIndex++) {
+                
+                // Gets the task's current thread
+                Thread thread = taskArray[taskArrayIndex].mThreadThis;
+                
+                // if the Thread exists, post an interrupt to it
+                if (null != thread) {
+                    thread.interrupt();
+                }
+            }
+        }
+    }
+
+    /**
+     * Stops a download Thread and removes it from the threadpool
+     *
+     * @param downloaderTask The download task associated with the Thread
+     * @param pictureURL The URL being downloaded
+     */
+    static public void removeDownload(PhotoTask downloaderTask, URL pictureURL) {
+
+        // If the Thread object still exists and the download matches the specified URL
+        if (downloaderTask != null && downloaderTask.getImageURL().equals(pictureURL)) {
+
+            /*
+             * Locks on this class to ensure that other processes aren't mutating Threads.
+             */
+            synchronized (sInstance) {
+                
+                // Gets the Thread that the downloader task is running on
+                Thread thread = downloaderTask.getCurrentThread();
+
+                // If the Thread exists, posts an interrupt to it
+                if (null != thread)
+                    thread.interrupt();
+            }
+            /*
+             * Removes the download Runnable from the ThreadPool. This opens a Thread in the
+             * ThreadPool's work queue, allowing a task in the queue to start.
+             */
+            sInstance.mDownloadThreadPool.remove(downloaderTask.getHTTPDownloadRunnable());
+        }
+    }
+
+    /**
+     * Starts an image download and decode
+     *
+     * @param imageView The ImageView that will get the resulting Bitmap
+     * @param cacheFlag Determines if caching should be used
+     * @return The task instance that will handle the work
+     */
+    static public PhotoTask startDownload(
+            PhotoView imageView,
+            boolean cacheFlag) {
+
+        /*
+         * Gets a task from the pool of tasks, returning null if the pool is empty
+         */
+        PhotoTask downloadTask = sInstance.mPhotoTaskWorkQueue.poll();
+
+        // If the queue was empty, create a new task instead.
+        if (null == downloadTask) {
+            downloadTask = new PhotoTask();
+        }
+
+        // Initializes the task
+        downloadTask.initializeDownloaderTask(PhotoManager.sInstance, imageView, cacheFlag);
+        
+        /*
+         * Provides the download task with the cache buffer corresponding to the URL to be
+         * downloaded.
+         */
+        downloadTask.setByteBuffer(sInstance.mPhotoCache.get(downloadTask.getImageURL()));
+
+        // If the byte buffer was empty, the image wasn't cached
+        if (null == downloadTask.getByteBuffer()) {
+            
+            /*
+             * "Executes" the tasks' download Runnable in order to download the image. If no
+             * Threads are available in the thread pool, the Runnable waits in the queue.
+             */
+            sInstance.mDownloadThreadPool.execute(downloadTask.getHTTPDownloadRunnable());
+
+            // Sets the display to show that the image is queued for downloading and decoding.
+            imageView.setStatusResource(R.drawable.imagequeued);
+        
+        // The image was cached, so no download is required.
+        } else {
+            
+            /*
+             * Signals that the download is "complete", because the byte array already contains the
+             * undecoded image. The decoding starts.
+             */
+            
+            sInstance.handleState(downloadTask, DOWNLOAD_COMPLETE);
+        }
+
+        // Returns a task object, either newly-created or one from the task pool
+        return downloadTask;
+    }
+
+    /**
+     * Recycles tasks by calling their internal recycle() method and then putting them back into
+     * the task queue.
+     * @param downloadTask The task to recycle
+     */
+    void recycleTask(PhotoTask downloadTask) {
+        
+        // Frees up memory in the task
+        downloadTask.recycle();
+        
+        // Puts the task object back into the queue for re-use.
+        mPhotoTaskWorkQueue.offer(downloadTask);
+    }
+}
diff --git a/samples/training/threadsample/src/com/example/android/threadsample/PhotoTask.java b/samples/training/threadsample/src/com/example/android/threadsample/PhotoTask.java
new file mode 100644
index 0000000..b07c497
--- /dev/null
+++ b/samples/training/threadsample/src/com/example/android/threadsample/PhotoTask.java
@@ -0,0 +1,298 @@
+/*
+ * 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.threadsample;
+
+import com.example.android.threadsample.PhotoDecodeRunnable.TaskRunnableDecodeMethods;
+import com.example.android.threadsample.PhotoDownloadRunnable.TaskRunnableDownloadMethods;
+
+import android.graphics.Bitmap;
+
+import java.lang.ref.WeakReference;
+import java.net.URL;
+
+/**
+ * This class manages PhotoDownloadRunnable and PhotoDownloadRunnable objects.  It does't perform
+ * the download or decode; instead, it manages persistent storage for the tasks that do the work.
+ * It does this by implementing the interfaces that the download and decode classes define, and
+ * then passing itself as an argument to the constructor of a download or decode object. In effect,
+ * this allows PhotoTask to start on a Thread, run a download in a delegate object, then
+ * run a decode, and then start over again. This class can be pooled and reused as necessary.
+ */
+public class PhotoTask implements
+        TaskRunnableDownloadMethods, TaskRunnableDecodeMethods {
+
+    /*
+     * Creates a weak reference to the ImageView that this Task will populate.
+     * The weak reference prevents memory leaks and crashes, because it
+     * automatically tracks the "state" of the variable it backs. If the
+     * reference becomes invalid, the weak reference is garbage- collected. This
+     * technique is important for referring to objects that are part of a
+     * component lifecycle. Using a hard reference may cause memory leaks as the
+     * value continues to change; even worse, it can cause crashes if the
+     * underlying component is destroyed. Using a weak reference to a View
+     * ensures that the reference is more transitory in nature.
+     */
+    private WeakReference<PhotoView> mImageWeakRef;
+
+    // The image's URL
+    private URL mImageURL;
+
+    // The width and height of the decoded image
+    private int mTargetHeight;
+    private int mTargetWidth;
+    
+    // Is the cache enabled for this transaction?
+    private boolean mCacheEnabled;
+
+    /*
+     * Field containing the Thread this task is running on.
+     */
+    Thread mThreadThis;
+
+    /*
+     * Fields containing references to the two runnable objects that handle downloading and
+     * decoding of the image.
+     */
+    private Runnable mDownloadRunnable;
+    private Runnable mDecodeRunnable;
+
+    // A buffer for containing the bytes that make up the image
+    byte[] mImageBuffer;
+    
+    // The decoded image
+    private Bitmap mDecodedImage;
+    
+    // The Thread on which this task is currently running.
+    private Thread mCurrentThread;
+    
+    /*
+     * An object that contains the ThreadPool singleton.
+     */
+    private static PhotoManager sPhotoManager;
+
+    /**
+     * Creates an PhotoTask containing a download object and a decoder object.
+     */
+    PhotoTask() {
+        // Create the runnables
+        mDownloadRunnable = new PhotoDownloadRunnable(this);
+        mDecodeRunnable = new PhotoDecodeRunnable(this);
+        sPhotoManager = PhotoManager.getInstance();
+    }
+    
+    /**
+     * Initializes the Task
+     *
+     * @param photoManager A ThreadPool object
+     * @param photoView An ImageView instance that shows the downloaded image
+     * @param cacheFlag Whether caching is enabled
+     */
+    void initializeDownloaderTask(
+            PhotoManager photoManager,
+            PhotoView photoView,
+            boolean cacheFlag)
+    {
+        // Sets this object's ThreadPool field to be the input argument
+        sPhotoManager = photoManager;
+        
+        // Gets the URL for the View
+        mImageURL = photoView.getLocation();
+
+        // Instantiates the weak reference to the incoming view
+        mImageWeakRef = new WeakReference<PhotoView>(photoView);
+
+        // Sets the cache flag to the input argument
+        mCacheEnabled = cacheFlag;
+
+        // Gets the width and height of the provided ImageView
+        mTargetWidth = photoView.getWidth();
+        mTargetHeight = photoView.getHeight();
+        
+    }
+    
+    // Implements HTTPDownloaderRunnable.getByteBuffer
+    @Override
+    public byte[] getByteBuffer() {
+        
+        // Returns the global field
+        return mImageBuffer;
+    }
+    
+    /**
+     * Recycles an PhotoTask object before it's put back into the pool. One reason to do
+     * this is to avoid memory leaks.
+     */
+    void recycle() {
+        
+        // Deletes the weak reference to the imageView
+        if ( null != mImageWeakRef ) {
+            mImageWeakRef.clear();
+            mImageWeakRef = null;
+        }
+        
+        // Releases references to the byte buffer and the BitMap
+        mImageBuffer = null;
+        mDecodedImage = null;
+    }
+
+    // Implements PhotoDownloadRunnable.getTargetWidth. Returns the global target width.
+    @Override
+    public int getTargetWidth() {
+        return mTargetWidth;
+    }
+
+    // Implements PhotoDownloadRunnable.getTargetHeight. Returns the global target height.
+    @Override
+    public int getTargetHeight() {
+        return mTargetHeight;
+    }
+
+    // Detects the state of caching
+    boolean isCacheEnabled() {
+        return mCacheEnabled;
+    }
+
+    // Implements PhotoDownloadRunnable.getImageURL. Returns the global Image URL.
+    @Override
+    public URL getImageURL() {
+        return mImageURL;
+    }
+
+    // Implements PhotoDownloadRunnable.setByteBuffer. Sets the image buffer to a buffer object.
+    @Override
+    public void setByteBuffer(byte[] imageBuffer) {
+        mImageBuffer = imageBuffer;
+    }
+    
+    // Delegates handling the current state of the task to the PhotoManager object
+    void handleState(int state) {
+        sPhotoManager.handleState(this, state);
+    }
+
+    // Returns the image that PhotoDecodeRunnable decoded.
+    Bitmap getImage() {
+        return mDecodedImage;
+    }
+
+    // Returns the instance that downloaded the image
+    Runnable getHTTPDownloadRunnable() {
+        return mDownloadRunnable;
+    }
+    
+    // Returns the instance that decode the image
+    Runnable getPhotoDecodeRunnable() {
+        return mDecodeRunnable;
+    }
+
+    // Returns the ImageView that's being constructed.
+    public PhotoView getPhotoView() {
+        if ( null != mImageWeakRef ) {
+            return mImageWeakRef.get();
+        }
+        return null;
+    }
+
+    /*
+     * Returns the Thread that this Task is running on. The method must first get a lock on a
+     * static field, in this case the ThreadPool singleton. The lock is needed because the
+     * Thread object reference is stored in the Thread object itself, and that object can be
+     * changed by processes outside of this app.
+     */
+    public Thread getCurrentThread() {
+        synchronized(sPhotoManager) {
+            return mCurrentThread;
+        }
+    }
+
+    /*
+     * Sets the identifier for the current Thread. This must be a synchronized operation; see the
+     * notes for getCurrentThread()
+     */
+    public void setCurrentThread(Thread thread) {
+        synchronized(sPhotoManager) {
+            mCurrentThread = thread;
+        }
+    }
+
+    // Implements ImageCoderRunnable.setImage(). Sets the Bitmap for the current image.
+    @Override
+    public void setImage(Bitmap decodedImage) {
+        mDecodedImage = decodedImage;
+    }
+
+    // Implements PhotoDownloadRunnable.setHTTPDownloadThread(). Calls setCurrentThread().
+    @Override
+    public void setDownloadThread(Thread currentThread) {
+        setCurrentThread(currentThread);
+    }
+
+    /*
+     * Implements PhotoDownloadRunnable.handleHTTPState(). Passes the download state to the
+     * ThreadPool object.
+     */
+    
+    @Override
+    public void handleDownloadState(int state) {
+        int outState;
+        
+        // Converts the download state to the overall state
+        switch(state) {
+            case PhotoDownloadRunnable.HTTP_STATE_COMPLETED:
+                outState = PhotoManager.DOWNLOAD_COMPLETE;
+                break;
+            case PhotoDownloadRunnable.HTTP_STATE_FAILED:
+                outState = PhotoManager.DOWNLOAD_FAILED;
+                break;
+            default:
+                outState = PhotoManager.DOWNLOAD_STARTED;
+                break;
+        }
+        // Passes the state to the ThreadPool object.
+        handleState(outState);
+    }
+
+    // Implements PhotoDecodeRunnable.setImageDecodeThread(). Calls setCurrentThread().
+    @Override
+    public void setImageDecodeThread(Thread currentThread) {
+        setCurrentThread(currentThread);
+    }
+
+    /*
+     * Implements PhotoDecodeRunnable.handleDecodeState(). Passes the decoding state to the
+     * ThreadPool object.
+     */
+    @Override
+    public void handleDecodeState(int state) {
+        int outState;
+        
+        // Converts the decode state to the overall state.
+        switch(state) {
+            case PhotoDecodeRunnable.DECODE_STATE_COMPLETED:
+                outState = PhotoManager.TASK_COMPLETE;
+                break;
+            case PhotoDecodeRunnable.DECODE_STATE_FAILED:
+                outState = PhotoManager.DOWNLOAD_FAILED;
+                break;
+            default:
+                outState = PhotoManager.DECODE_STARTED;
+                break;
+        }
+        
+        // Passes the state to the ThreadPool object.
+        handleState(outState);
+    }
+}
diff --git a/samples/training/threadsample/src/com/example/android/threadsample/PhotoThumbnailFragment.java b/samples/training/threadsample/src/com/example/android/threadsample/PhotoThumbnailFragment.java
new file mode 100644
index 0000000..0a89e9e
--- /dev/null
+++ b/samples/training/threadsample/src/com/example/android/threadsample/PhotoThumbnailFragment.java
@@ -0,0 +1,436 @@
+/*
+ * 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.threadsample;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.content.LocalBroadcastManager;
+import android.support.v4.widget.CursorAdapter;
+import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.GridView;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.concurrent.RejectedExecutionException;
+
+/**
+ * PhotoThumbnailFragment displays a GridView of picture thumbnails downloaded from Picasa
+ */
+public class PhotoThumbnailFragment extends Fragment implements
+        LoaderManager.LoaderCallbacks<Cursor>, AdapterView.OnItemClickListener {
+    
+    private static final String STATE_IS_HIDDEN =
+            "com.example.android.threadsample.STATE_IS_HIDDEN";
+    
+    // The width of each column in the grid
+    private int mColumnWidth;
+    
+    // A Drawable for a grid cell that's empty
+    private Drawable mEmptyDrawable;
+    
+    // The GridView for displaying thumbnails
+    private GridView mGridView;
+    
+    // Denotes if the GridView has been loaded
+    private boolean mIsLoaded;
+    
+    // Intent for starting the IntentService that downloads the Picasa featured picture RSS feed
+    private Intent mServiceIntent;
+    
+    // An adapter between a Cursor and the Fragment's GridView
+    private GridViewAdapter mAdapter;
+
+    // The URL of the Picasa featured picture RSS feed, in String format
+    private static final String PICASA_RSS_URL =
+            "http://picasaweb.google.com/data/feed/base/featured?" +
+            "alt=rss&kind=photo&access=public&slabel=featured&hl=en_US&imgmax=1600";
+
+    private static final String[] PROJECTION =
+    {
+        DataProviderContract._ID,
+        DataProviderContract.IMAGE_THUMBURL_COLUMN,
+        DataProviderContract.IMAGE_URL_COLUMN
+    };
+    
+    // Constants that define the order of columns in the returned cursor
+    private static final int IMAGE_THUMBURL_CURSOR_INDEX = 1;
+    private static final int IMAGE_URL_CURSOR_INDEX = 2;
+
+    // Identifies a particular Loader being used in this component
+    private static final int URL_LOADER = 0;
+    
+    /*
+     * This callback is invoked when the framework is starting or re-starting the Loader. It
+     * returns a CursorLoader object containing the desired query
+     */
+    @Override
+    public Loader<Cursor> onCreateLoader(int loaderID, Bundle bundle)
+    {
+        /*
+         * Takes action based on the ID of the Loader that's being created
+         */
+        switch (loaderID) {
+            case URL_LOADER:
+            // Returns a new CursorLoader
+            return new CursorLoader(
+                        getActivity(),                                     // Context
+                        DataProviderContract.PICTUREURL_TABLE_CONTENTURI,  // Table to query
+                        PROJECTION,                                        // Projection to return
+                        null,                                              // No selection clause
+                        null,                                              // No selection arguments
+                        null                                               // Default sort order
+            );
+            default:
+                // An invalid id was passed in
+                return null;
+
+        }
+        
+    }
+
+    /*
+     * This callback is invoked when the the Fragment's View is being loaded. It sets up the View.
+     */
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
+        
+        // Always call the super method first
+        super.onCreateView(inflater, viewGroup, bundle);
+        
+        /*
+         * Inflates the View from the gridlist layout file, using the layout parameters in
+         * "viewGroup"
+         */
+        View localView = inflater.inflate(R.layout.gridlist, viewGroup, false);
+        
+        // Sets the View's data adapter to be a new GridViewAdapter
+        mAdapter = new GridViewAdapter(getActivity());
+        
+        // Gets a handle to the GridView in the layout
+        mGridView = ((GridView) localView.findViewById(android.R.id.list));
+        
+        // Instantiates a DisplayMetrics object
+        DisplayMetrics localDisplayMetrics = new DisplayMetrics();
+        
+        // Gets the current display metrics from the current Window
+        getActivity().getWindowManager().getDefaultDisplay().getMetrics(localDisplayMetrics);
+        
+        /*
+         * Gets the dp value from the thumbSize resource as an integer in dps. The value can
+         * be adjusted for specific display sizes, etc. in the dimens.xml file for a particular
+         * values-<qualifier> directory
+         */
+        int pixelSize = getResources().getDimensionPixelSize(R.dimen.thumbSize);
+        
+        /*
+         * Calculates a width scale factor from the pixel width of the current display and the
+         * desired pixel size
+         */
+        int widthScale = localDisplayMetrics.widthPixels / pixelSize;
+        
+        // Calculates the grid column width
+        mColumnWidth = (localDisplayMetrics.widthPixels / widthScale);
+        
+        // Sets the GridView's column width
+        mGridView.setColumnWidth(mColumnWidth);
+        
+        // Starts by setting the GridView to have no columns
+        mGridView.setNumColumns(-1);
+        
+        // Sets the GridView's data adapter
+        mGridView.setAdapter(mAdapter);
+        
+        /*
+         * Sets the GridView's click listener to be this class. As a result, when users click the
+         * GridView, PhotoThumbnailFragment.onClick() is invoked.
+         */
+        mGridView.setOnItemClickListener(this);
+        
+        /*
+         * Sets the "empty" View for the layout. If there's nothing to show, a ProgressBar
+         * is displayed.
+         */
+        mGridView.setEmptyView(localView.findViewById(R.id.progressRoot));
+        
+        // Sets a dark background to show when no image is queued to be downloaded
+        mEmptyDrawable = getResources().getDrawable(R.drawable.imagenotqueued);
+        
+        // Initializes the CursorLoader
+        getLoaderManager().initLoader(URL_LOADER, null, this);
+        
+        /*
+         * Creates a new Intent to send to the download IntentService. The Intent contains the
+         * URL of the Picasa feature picture RSS feed
+         */
+        mServiceIntent =
+                new Intent(getActivity(), RSSPullService.class)
+                        .setData(Uri.parse(PICASA_RSS_URL));
+        
+        // If there's no pre-existing state for this Fragment
+        if (bundle == null) {
+            // If the data wasn't previously loaded
+            if (!this.mIsLoaded) {
+                // Starts the IntentService to download the RSS feed data
+                getActivity().startService(mServiceIntent);
+            }
+
+        // If this Fragment existed previously, gets its state
+        } else if (bundle.getBoolean(STATE_IS_HIDDEN, false)) {
+            
+            // Begins a transaction
+            FragmentTransaction localFragmentTransaction =
+                    getFragmentManager().beginTransaction();
+            
+            // Hides the Fragment
+            localFragmentTransaction.hide(this);
+            
+            // Commits the transaction
+            localFragmentTransaction.commit();
+        }
+        
+        // Returns the View inflated from the layout
+        return localView;
+    }
+
+    /*
+     * This callback is invoked when the Fragment is being destroyed.
+     */
+    @Override
+    public void onDestroyView() {
+        
+        // Sets variables to null, to avoid memory leaks
+        mGridView = null;
+        
+        // If the EmptyDrawable contains something, sets those members to null
+        if (mEmptyDrawable != null) {
+            this.mEmptyDrawable.setCallback(null);
+            this.mEmptyDrawable = null;
+        }
+        
+        // Always call the super method last
+        super.onDestroyView();
+    }
+
+    /*
+     * This callback is invoked after onDestroyView(). It clears out variables, shuts down the
+     * CursorLoader, and so forth
+     */
+    @Override
+    public void onDetach() {
+        
+        // Destroys variables and references, and catches Exceptions
+        try {
+            getLoaderManager().destroyLoader(0);
+            if (mAdapter != null) {
+                mAdapter.changeCursor(null);
+                mAdapter = null;
+            }
+        } catch (Throwable localThrowable) {
+        }
+        
+        // Always call the super method last
+        super.onDetach();
+        return;
+    }
+
+    /*
+     * This is invoked whenever the visibility state of the Fragment changes
+     */
+    @Override
+    public void onHiddenChanged(boolean viewState) {
+        super.onHiddenChanged(viewState);
+    }
+
+    /*
+     * Implements OnItemClickListener.onItemClick() for this View's listener.
+     * This implementation detects the View that was clicked and retrieves its picture URL.
+     */
+    @Override
+    public void onItemClick(AdapterView<?> adapterView, View view, int viewId, long rowId) {
+        
+        // Returns a one-row cursor for the data that backs the View that was clicked.
+        Cursor cursor = (Cursor) mAdapter.getItem(viewId);
+
+        // Retrieves the urlString from the cursor
+        String urlString = cursor.getString(IMAGE_URL_CURSOR_INDEX);
+
+        /*
+         * Creates a new Intent to get the full picture for the thumbnail that the user clicked.
+         * The full photo is loaded into a separate Fragment
+         */
+        Intent localIntent =
+                new Intent(Constants.ACTION_VIEW_IMAGE)
+                .setData(Uri.parse(urlString));
+        
+        // Broadcasts the Intent to receivers in this app. See DisplayActivity.FragmentDisplayer.
+        LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(localIntent);
+    }
+
+    /*
+     * Invoked when the CursorLoader finishes the query. A reference to the Loader and the
+     * returned Cursor are passed in as arguments
+     */
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor returnCursor) {
+        
+        /*
+         *  Changes the adapter's Cursor to be the results of the load. This forces the View to
+         *  redraw.
+         */
+        
+        mAdapter.changeCursor(returnCursor);
+    }
+
+    /*
+     * Invoked when the CursorLoader is being reset. For example, this is called if the
+     * data in the provider changes and the Cursor becomes stale.
+     */
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+        
+        // Sets the Adapter's backing data to null. This prevents memory leaks.
+        mAdapter.changeCursor(null);
+    }
+
+    /*
+     * This callback is invoked when the system has to destroy the Fragment for some reason. It
+     * allows the Fragment to save its state, so the state can be restored later on.
+     */
+    @Override
+    public void onSaveInstanceState(Bundle bundle) {
+        
+        // Saves the show-hide status of the display
+        bundle.putBoolean(STATE_IS_HIDDEN, isHidden());
+        
+        // Always call the super method last
+        super.onSaveInstanceState(bundle);
+    }
+
+    // Sets the state of the loaded flag
+    public void setLoaded(boolean loadState) {
+        mIsLoaded = loadState;
+    }
+
+    /**
+     * Defines a custom View adapter that extends CursorAdapter. The main reason to do this is to
+     * display images based on the backing Cursor, rather than just displaying the URLs that the
+     * Cursor contains.
+     */
+    private class GridViewAdapter extends CursorAdapter {
+        
+        /**
+         * Simplified constructor that calls the super constructor with the input Context,
+         * a null value for Cursor, and no flags
+         * @param context A Context for this object
+         */
+        public GridViewAdapter(Context context) {
+            super(context, null, false);
+        }
+        
+        /**
+         *
+         * Binds a View and a Cursor
+         *
+         * @param view An existing View object
+         * @param context A Context for the View and Cursor
+         * @param cursor The Cursor to bind to the View, representing one row of the returned query.
+         */
+        @Override
+        public void bindView(View view, Context context, Cursor cursor) {
+            
+            // Gets a handle to the View
+            PhotoView localImageDownloaderView = (PhotoView) view.getTag();
+            
+            // Converts the URL string to a URL and tries to retrieve the picture
+            try {
+                // Gets the URL
+                URL localURL =
+                        new URL(
+                            cursor.getString(IMAGE_THUMBURL_CURSOR_INDEX)
+                        )
+                ;
+                /*
+                 * Invokes setImageURL for the View. If the image isn't already available, this
+                 * will download and decode it.
+                 */
+                localImageDownloaderView.setImageURL(
+                            localURL, true, PhotoThumbnailFragment.this.mEmptyDrawable);
+            
+            // Catches an invalid URL
+            } catch (MalformedURLException localMalformedURLException) {
+                localMalformedURLException.printStackTrace();
+            
+            // Catches errors trying to download and decode the picture in a ThreadPool
+            } catch (RejectedExecutionException localRejectedExecutionException) {
+            }
+        }
+
+        /**
+         * Creates a new View that shows the contents of the Cursor
+         *
+         *
+         * @param context A Context for the View and Cursor
+         * @param cursor The Cursor to display. This is a single row of the returned query
+         * @param viewGroup The viewGroup that's the parent of the new View
+         * @return the newly-created View
+         */
+        @Override
+        public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
+            // Gets a new layout inflater instance
+            LayoutInflater inflater = LayoutInflater.from(context);
+            
+            /*
+             * Creates a new View by inflating the specified layout file. The root ViewGroup is
+             * the root of the layout file. This View is a FrameLayout
+             */
+            View layoutView = inflater.inflate(R.layout.galleryitem, null);
+            
+            /*
+             * Creates a second View to hold the thumbnail image.
+             */
+            View thumbView = layoutView.findViewById(R.id.thumbImage);
+            
+            /*
+             * Sets layout parameters for the layout based on the layout parameters of a virtual
+             * list. In addition, this sets the layoutView's width to be MATCH_PARENT, and its
+             * height to be the column width?
+             */
+            layoutView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT,
+                    PhotoThumbnailFragment.this.mColumnWidth));
+            
+            // Sets the layoutView's tag to be the same as the thumbnail image tag.
+            layoutView.setTag(thumbView);
+            return layoutView;
+        }
+
+    }
+}
diff --git a/samples/training/threadsample/src/com/example/android/threadsample/PhotoView.java b/samples/training/threadsample/src/com/example/android/threadsample/PhotoView.java
new file mode 100644
index 0000000..eaf3940
--- /dev/null
+++ b/samples/training/threadsample/src/com/example/android/threadsample/PhotoView.java
@@ -0,0 +1,349 @@
+/*
+ * 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.threadsample;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+
+
+import java.lang.ref.WeakReference;
+import java.net.URL;
+
+/**
+ * This class extends the standard Android ImageView View class with some features
+ * that are useful for downloading, decoding, and displaying Picasa images.
+ *
+ */
+public class PhotoView extends ImageView {
+    
+    // Indicates if caching should be used
+    private boolean mCacheFlag;
+    
+    // Status flag that indicates if onDraw has completed
+    private boolean mIsDrawn;
+    
+    /*
+     * Creates a weak reference to the ImageView in this object. The weak
+     * reference prevents memory leaks and crashes, because it automatically tracks the "state" of
+     * the variable it backs. If the reference becomes invalid, the weak reference is garbage-
+     * collected.
+     * This technique is important for referring to objects that are part of a component lifecycle.
+     * Using a hard reference may cause memory leaks as the value continues to change; even worse,
+     * it can cause crashes if the underlying component is destroyed. Using a weak reference to
+     * a View ensures that the reference is more transitory in nature.
+     */
+    private WeakReference<View> mThisView;
+    
+    // Contains the ID of the internal View
+    private int mHideShowResId = -1;
+    
+    // The URL that points to the source of the image for this ImageView
+    private URL mImageURL;
+    
+    // The Thread that will be used to download the image for this ImageView
+    private PhotoTask mDownloadThread;
+
+    /**
+     * Creates an ImageDownloadView with no settings
+     * @param context A context for the View
+     */
+    public PhotoView(Context context) {
+        super(context);
+    }
+
+    /**
+     * Creates an ImageDownloadView and gets attribute values
+     * @param context A Context to use with the View
+     * @param attributeSet The entire set of attributes for the View
+     */
+    public PhotoView(Context context, AttributeSet attributeSet) {
+        super(context, attributeSet);
+        
+        // Gets attributes associated with the attribute set
+        getAttributes(attributeSet);
+    }
+
+    /**
+     * Creates an ImageDownloadView, gets attribute values, and applies a default style
+     * @param context A context for the View
+     * @param attributeSet The entire set of attributes for the View
+     * @param defaultStyle The default style to use with the View
+     */
+    public PhotoView(Context context, AttributeSet attributeSet, int defaultStyle) {
+        super(context, attributeSet, defaultStyle);
+        
+        // Gets attributes associated with the attribute set
+        getAttributes(attributeSet);
+    }
+
+    /**
+     * Gets the resource ID for the hideShowSibling resource
+     * @param attributeSet The entire set of attributes for the View
+     */
+    private void getAttributes(AttributeSet attributeSet) {
+        
+        // Gets an array of attributes for the View
+        TypedArray attributes =
+                getContext().obtainStyledAttributes(attributeSet, R.styleable.ImageDownloaderView);
+        
+        // Gets the resource Id of the View to hide or show
+        mHideShowResId =
+                attributes.getResourceId(R.styleable.ImageDownloaderView_hideShowSibling, -1);
+        
+        // Returns the array for re-use
+        attributes.recycle();
+    }
+
+    /**
+     * Sets the visibility of the PhotoView
+     * @param visState The visibility state (see View.setVisibility)
+     */
+    private void showView(int visState) {
+        // If the View contains something
+        if (mThisView != null) {
+            
+            // Gets a local hard reference to the View
+            View localView = mThisView.get();
+            
+            // If the weak reference actually contains something, set the visibility
+            if (localView != null)
+                localView.setVisibility(visState);
+        }
+    }
+    
+    /**
+     * Sets the image in this ImageView to null, and makes the View visible
+     */
+    public void clearImage() {
+        setImageDrawable(null);
+        showView(View.VISIBLE);
+    }
+
+    /**
+     * Returns the URL of the picture associated with this ImageView
+     * @return a URL
+     */
+    final URL getLocation() {
+        return mImageURL;
+    }
+
+    /*
+     * This callback is invoked when the system attaches the ImageView to a Window. The callback
+     * is invoked before onDraw(), but may be invoked after onMeasure()
+     */
+    @Override
+    protected void onAttachedToWindow() {
+        // Always call the supermethod first
+        super.onAttachedToWindow();
+        
+        // If the sibling View is set and the parent of the ImageView is itself a View
+        if ((this.mHideShowResId != -1) && ((getParent() instanceof View))) {
+            
+            // Gets a handle to the sibling View
+            View localView = ((View) getParent()).findViewById(this.mHideShowResId);
+            
+            // If the sibling View contains something, make it the weak reference for this View
+            if (localView != null) {
+                this.mThisView = new WeakReference<View>(localView);
+            }
+        }
+    }
+
+    /*
+     * This callback is invoked when the ImageView is removed from a Window. It "unsets" variables
+     * to prevent memory leaks.
+     */
+    @Override
+    protected void onDetachedFromWindow() {
+        
+        // Clears out the image drawable, turns off the cache, disconnects the view from a URL
+        setImageURL(null, false, null);
+        
+        // Gets the current Drawable, or null if no Drawable is attached
+        Drawable localDrawable = getDrawable();
+        
+        // if the Drawable is null, unbind it from this VIew
+        if (localDrawable != null)
+            localDrawable.setCallback(null);
+        
+        // If this View still exists, clears the weak reference, then sets the reference to null
+        if (mThisView != null) {
+            mThisView.clear();
+            mThisView = null;
+        }
+        
+        // Sets the downloader thread to null
+        this.mDownloadThread = null;
+        
+        // Always call the super method last
+        super.onDetachedFromWindow();
+    }
+
+    /*
+     * This callback is invoked when the system tells the View to draw itself. If the View isn't
+     * already drawn, and its URL isn't null, it invokes a Thread to download the image. Otherwise,
+     * it simply passes the existing Canvas to the super method
+     */
+    @Override
+    protected void onDraw(Canvas canvas) {
+        // If the image isn't already drawn, and the URL is set
+        if ((!mIsDrawn) && (mImageURL != null)) {
+            
+            // Starts downloading this View, using the current cache setting
+            mDownloadThread = PhotoManager.startDownload(this, mCacheFlag);
+            
+            // After successfully downloading the image, this marks that it's available.
+            mIsDrawn = true;
+        }
+        // Always call the super method last
+        super.onDraw(canvas);
+    }
+
+    /**
+     * Sets the current View weak reference to be the incoming View. See the definition of
+     * mThisView
+     * @param view the View to use as the new WeakReference
+     */
+    public void setHideView(View view) {
+        this.mThisView = new WeakReference<View>(view);
+    }
+
+    @Override
+    public void setImageBitmap(Bitmap paramBitmap) {
+        super.setImageBitmap(paramBitmap);
+    }
+
+    @Override
+    public void setImageDrawable(Drawable drawable) {
+        // The visibility of the View
+        int viewState;
+        
+        /*
+         * Sets the View state to visible if the method is called with a null argument (the
+         * image is being cleared). Otherwise, sets the View state to invisible before refreshing
+         * it.
+         */
+        if (drawable == null) {
+            
+            viewState = View.VISIBLE;
+        } else {
+            
+            viewState = View.INVISIBLE;
+        }
+        // Either hides or shows the View, depending on the view state
+        showView(viewState);
+
+        // Invokes the supermethod with the provided drawable
+        super.setImageDrawable(drawable);
+    }
+
+    /*
+     * Displays a drawable in the View
+     */
+    @Override
+    public void setImageResource(int resId) {
+        super.setImageResource(resId);
+    }
+
+    /*
+     * Sets the URI for the Image
+     */
+    @Override
+    public void setImageURI(Uri uri) {
+        super.setImageURI(uri);
+    }
+
+    /**
+     * Attempts to set the picture URL for this ImageView and then download the picture.
+     * <p>
+     * If the picture URL for this view is already set, and the input URL is not the same as the
+     * stored URL, then the picture has moved and any existing downloads are stopped.
+     * <p>
+     * If the input URL is the same as the stored URL, then nothing needs to be done.
+     * <p>
+     * If the stored URL is null, then this method starts a download and decode of the picture
+     * @param pictureURL An incoming URL for a Picasa picture
+     * @param cacheFlag Whether to use caching when doing downloading and decoding
+     * @param imageDrawable The Drawable to use for this ImageView
+     */
+    public void setImageURL(URL pictureURL, boolean cacheFlag, Drawable imageDrawable) {
+        // If the picture URL for this ImageView is already set
+        if (mImageURL != null) {
+            
+            // If the stored URL doesn't match the incoming URL, then the picture has changed.
+            if (!mImageURL.equals(pictureURL)) {
+                
+                // Stops any ongoing downloads for this ImageView
+                PhotoManager.removeDownload(mDownloadThread, mImageURL);
+            } else {
+                
+                // The stored URL matches the incoming URL. Returns without doing any work.
+                return;
+            }
+        }
+        
+        // Sets the Drawable for this ImageView
+        setImageDrawable(imageDrawable);
+        
+        // Stores the picture URL for this ImageView
+        mImageURL = pictureURL;
+        
+        // If the draw operation for this ImageVIew has completed, and the picture URL isn't empty
+        if ((mIsDrawn) && (pictureURL != null)) {
+            
+            // Sets the cache flag
+            mCacheFlag = cacheFlag;
+            
+            /*
+             * Starts a download of the picture file. Notice that if caching is on, the picture
+             * file's contents may be taken from the cache.
+             */
+            mDownloadThread = PhotoManager.startDownload(this, cacheFlag);
+        }
+    }
+
+    /**
+     * Sets the Drawable for this ImageView
+     * @param drawable A Drawable to use for the ImageView
+     */
+    public void setStatusDrawable(Drawable drawable) {
+        
+        // If the View is empty, sets a Drawable as its content
+        if (mThisView == null) {
+            setImageDrawable(drawable);
+        }
+    }
+
+    /**
+     * Sets the content of this ImageView to be a Drawable resource
+     * @param resId
+     */
+    public void setStatusResource(int resId) {
+        
+        // If the View is empty, provides it with a Drawable resource as its content
+        if (mThisView == null) {
+            setImageResource(resId);
+        }
+    }
+}
diff --git a/samples/training/threadsample/src/com/example/android/threadsample/ProgressNotifier.java b/samples/training/threadsample/src/com/example/android/threadsample/ProgressNotifier.java
new file mode 100644
index 0000000..479ba79
--- /dev/null
+++ b/samples/training/threadsample/src/com/example/android/threadsample/ProgressNotifier.java
@@ -0,0 +1,21 @@
+/*
+ * 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.threadsample;
+
+public interface ProgressNotifier {
+    public void notifyProgress(String paramString);
+}
diff --git a/samples/training/threadsample/src/com/example/android/threadsample/RSSPullParser.java b/samples/training/threadsample/src/com/example/android/threadsample/RSSPullParser.java
new file mode 100644
index 0000000..48b2151
--- /dev/null
+++ b/samples/training/threadsample/src/com/example/android/threadsample/RSSPullParser.java
@@ -0,0 +1,204 @@
+/*
+ * 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.threadsample;
+
+import org.xml.sax.helpers.DefaultHandler;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import android.content.ContentValues;
+import android.net.Uri;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Vector;
+
+/**
+ * RSSPullParser reads an RSS feed from the Picasa featured pictures site. It uses
+ * several packages from the widely-known XMLPull API.
+ *
+ */
+public class RSSPullParser extends DefaultHandler {
+    // Global constants
+
+    // An attribute value indicating that the element contains media content
+    private static final String CONTENT = "media:content";
+    
+    // An attribute value indicating that the element contains a thumbnail
+    private static final String THUMBNAIL = "media:thumbnail";
+    
+    // An attribute value indicating that the element contains an item
+    private static final String ITEM = "item";
+
+    // Sets the initial size of the vector that stores data.
+    private static final int VECTOR_INITIAL_SIZE = 500;
+
+    // Storage for a single ContentValues for image data
+    private static ContentValues mImage;
+    
+    // A vector that will contain all of the images
+    private Vector<ContentValues> mImages;
+
+    /**
+     * A getter that returns the image data Vector
+     * @return A Vector containing all of the image data retrieved by the parser
+     */
+    public Vector<ContentValues> getImages() {
+        return mImages;
+    }
+    /**
+     * This method parses XML in an input stream and stores parts of the data in memory
+     *
+     * @param inputStream a stream of data containing XML elements, usually a RSS feed
+     * @param progressNotifier a helper class for sending status and logs
+     * @throws XmlPullParserException defined by XMLPullParser; thrown if the thread is cancelled.
+     * @throws IOException thrown if an IO error occurs during parsing
+     */
+    public void parseXml(InputStream inputStream,
+            BroadcastNotifier progressNotifier)
+            throws XmlPullParserException, IOException {
+
+        // Instantiates a parser factory
+        XmlPullParserFactory localXmlPullParserFactory = XmlPullParserFactory
+                .newInstance();
+
+        // Turns off namespace handling for the XML input
+        localXmlPullParserFactory.setNamespaceAware(false);
+
+        // Instantiates a new pull parser
+        XmlPullParser localXmlPullParser = localXmlPullParserFactory
+                .newPullParser();
+
+        // Sets the parser's input stream
+        localXmlPullParser.setInput(inputStream, null);
+
+        // Gets the first event in the input sream
+        int eventType = localXmlPullParser.getEventType();
+
+        // Sets the number of images read to 1
+        int imageCount = 1;
+
+        // Returns if the current event (state) is not START_DOCUMENT
+        if (eventType != XmlPullParser.START_DOCUMENT) {
+
+            throw new XmlPullParserException("Invalid RSS");
+
+        }
+
+        // Creates a new store for image URL data
+        mImages = new Vector<ContentValues>(VECTOR_INITIAL_SIZE);
+
+        // Loops indefinitely. The exit occurs if there are no more URLs to process
+        while (true) {
+
+            // Gets the next event in the input stream
+            int nextEvent = localXmlPullParser.next();
+
+            // If the current thread is interrupted, throws an exception and returns
+            if (Thread.currentThread().isInterrupted()) {
+
+                throw new XmlPullParserException("Cancelled");
+
+            // At the end of the feed, exits the loop
+            } else if (nextEvent == XmlPullParser.END_DOCUMENT) {
+                break;
+
+            // At the beginning of the feed, skips the event and continues
+            } else if (nextEvent == XmlPullParser.START_DOCUMENT) {
+                continue;
+
+            // At the start of a tag, gets the tag's name
+            } else if (nextEvent == XmlPullParser.START_TAG) {
+                String eventName = localXmlPullParser.getName();
+
+                /*
+                 * If this is the start of an individual item, logs it and creates a new
+                 * ContentValues
+                 */
+                if (eventName.equalsIgnoreCase(ITEM)) {
+
+                    mImage = new ContentValues();
+
+                // If this isn't an item, then checks for other options
+                } else {
+
+                    // Defines keys to store the column names
+                    String imageUrlKey;
+                    String imageNameKey;
+                    
+                    // Defines a place to store the filename of a URL,
+                    String fileName;
+
+                    // If it's CONTENT
+                    if (eventName.equalsIgnoreCase(CONTENT)) {
+
+                        // Stores the image URL and image name column names as keys
+                        imageUrlKey = DataProviderContract.IMAGE_URL_COLUMN;
+                        imageNameKey = DataProviderContract.IMAGE_PICTURENAME_COLUMN;
+
+                    // If it's a THUMBNAIL
+                    } else if (eventName.equalsIgnoreCase(THUMBNAIL)) {
+
+                        // Stores the thumbnail URL and thumbnail name column names as keys
+                        imageUrlKey = DataProviderContract.IMAGE_THUMBURL_COLUMN;
+                        imageNameKey = DataProviderContract.IMAGE_THUMBNAME_COLUMN;
+
+                    // Otherwise it's some other event that isn't important
+                    } else {
+                        continue;
+                    }
+
+                    // It's not an ITEM. Gets the URL attribute from the event
+                    String urlValue = localXmlPullParser.getAttributeValue(null, "url");
+
+                    // If the value is null, exits
+                    if (urlValue == null)
+                        break;
+
+                    // Puts the URL and the key into the ContentValues
+                    mImage.put(imageUrlKey, urlValue);
+                    
+                    // Gets the filename of the URL and puts it into the ContentValues
+                    fileName = Uri.parse(urlValue).getLastPathSegment();
+                    mImage.put(imageNameKey, fileName);
+                }
+            }
+            /*
+             * If it's not an ITEM, and it is an END_TAG, and the current event is an ITEM, and
+             * there is data in the current ContentValues
+             */
+            else if ((nextEvent == XmlPullParser.END_TAG)
+                    && (localXmlPullParser.getName().equalsIgnoreCase(ITEM))
+                    && (mImage != null)) {
+
+                // Adds the current ContentValues to the ContentValues storage
+                mImages.add(mImage);
+
+                // Logs progress
+                progressNotifier.notifyProgress("Parsed Image[" + imageCount + "]:"
+                        + mImage.getAsString(DataProviderContract.IMAGE_URL_COLUMN));
+
+                // Clears out the current ContentValues
+                mImage = null;
+
+                // Increments the count of the number of images stored.
+                imageCount++;
+            }
+        }
+    }
+}
diff --git a/samples/training/threadsample/src/com/example/android/threadsample/RSSPullService.java b/samples/training/threadsample/src/com/example/android/threadsample/RSSPullService.java
new file mode 100644
index 0000000..b8b3e26
--- /dev/null
+++ b/samples/training/threadsample/src/com/example/android/threadsample/RSSPullService.java
@@ -0,0 +1,266 @@
+/*
+ * 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.threadsample;
+
+import android.app.IntentService;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.Cursor;
+
+import org.apache.http.HttpStatus;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Date;
+import java.util.Vector;
+
+/**
+ * This service pulls RSS content from a web site URL contained in the incoming Intent (see
+ * onHandleIntent()). As it runs, it broadcasts its status using LocalBroadcastManager; any
+ * component that wants to see the status should implement a subclass of BroadcastReceiver and
+ * register to receive broadcast Intents with category = CATEGORY_DEFAULT and action
+ * Constants.BROADCAST_ACTION.
+ *
+ */
+public class RSSPullService extends IntentService {
+    // Used to write to the system log from this class.
+    public static final String LOG_TAG = "RSSPullService";
+
+    // Defines and instantiates an object for handling status updates.
+    private BroadcastNotifier mBroadcaster = new BroadcastNotifier(this);
+
+    /**
+     * An IntentService must always have a constructor that calls the super constructor. The
+     * string supplied to the super constructor is used to give a name to the IntentService's
+     * background thread.
+     */
+    public RSSPullService() {
+
+        super("RSSPullService");
+    }
+
+    /**
+     * In an IntentService, onHandleIntent is run on a background thread.  As it
+     * runs, it broadcasts its current status using the LocalBroadcastManager.
+     * @param workIntent The Intent that starts the IntentService. This Intent contains the
+     * URL of the web site from which the RSS parser gets data.
+     */
+    @Override
+    protected void onHandleIntent(Intent workIntent) {
+        // Gets a URL to read from the incoming Intent's "data" value
+        String localUrlString = workIntent.getDataString();
+
+        // Creates a projection to use in querying the modification date table in the provider.
+        final String[] dateProjection = new String[]
+        {
+            DataProviderContract.ROW_ID,
+            DataProviderContract.DATA_DATE_COLUMN
+        };
+
+        // A URL that's local to this method
+        URL localURL;
+
+        // A cursor that's local to this method.
+        Cursor cursor = null;
+
+        /*
+         * A block that tries to connect to the Picasa featured picture URL passed as the "data"
+         * value in the incoming Intent. The block throws exceptions (see the end of the block).
+         */
+        try {
+
+            // Convert the incoming data string to a URL.
+            localURL = new URL(localUrlString);
+
+            /*
+             * Tries to open a connection to the URL. If an IO error occurs, this throws an
+             * IOException
+             */
+            URLConnection localURLConnection = localURL.openConnection();
+
+            // If the connection is an HTTP connection, continue
+            if ((localURLConnection instanceof HttpURLConnection)) {
+
+                // Broadcasts an Intent indicating that processing has started.
+                mBroadcaster.broadcastIntentWithState(Constants.STATE_ACTION_STARTED);
+
+                // Casts the connection to a HTTP connection
+                HttpURLConnection localHttpURLConnection = (HttpURLConnection) localURLConnection;
+
+                // Sets the user agent for this request.
+                localHttpURLConnection.setRequestProperty("User-Agent", Constants.USER_AGENT);
+
+                /*
+                 * Queries the content provider to see if this URL was read previously, and when.
+                 * The content provider throws an exception if the URI is invalid.
+                 */
+                cursor = getContentResolver().query(
+                        DataProviderContract.DATE_TABLE_CONTENTURI,
+                        dateProjection,
+                        null,
+                        null,
+                        null);
+
+                // Flag to indicate that new metadata was retrieved
+                boolean newMetadataRetrieved;
+
+                /*
+                 * Tests to see if the table contains a modification date for the URL
+                 */
+                if (null != cursor && cursor.moveToFirst()) {
+
+                    // Find the URL's last modified date in the content provider
+                    long storedModifiedDate =
+                            cursor.getLong(cursor.getColumnIndex(
+                                    DataProviderContract.DATA_DATE_COLUMN)
+                            )
+                    ;
+
+                    /*
+                     * If the modified date isn't 0, sets another request property to ensure that
+                     * data is only downloaded if it has changed since the last recorded
+                     * modification date. Formats the date according to the RFC1123 format.
+                     */
+                    if (0 != storedModifiedDate) {
+                        localHttpURLConnection.setRequestProperty(
+                                "If-Modified-Since",
+                                org.apache.http.impl.cookie.DateUtils.formatDate(
+                                        new Date(storedModifiedDate),
+                                        org.apache.http.impl.cookie.DateUtils.PATTERN_RFC1123));
+                    }
+
+                    // Marks that new metadata does not need to be retrieved
+                    newMetadataRetrieved = false;
+
+                } else {
+
+                    /*
+                     * No modification date was found for the URL, so newmetadata has to be
+                     * retrieved.
+                     */
+                    newMetadataRetrieved = true;
+
+                }
+
+                // Reports that the service is about to connect to the RSS feed
+                mBroadcaster.broadcastIntentWithState(Constants.STATE_ACTION_CONNECTING);
+
+                // Gets a response code from the RSS server
+                int responseCode = localHttpURLConnection.getResponseCode();
+
+                switch (responseCode) {
+
+                    // If the response is OK
+                    case HttpStatus.SC_OK:
+
+                        // Gets the last modified data for the URL
+                        long lastModifiedDate = localHttpURLConnection.getLastModified();
+
+                        // Reports that the service is parsing
+                        mBroadcaster.broadcastIntentWithState(Constants.STATE_ACTION_PARSING);
+
+                        /*
+                         * Instantiates a pull parser and uses it to parse XML from the RSS feed.
+                         * The mBroadcaster argument send a broadcaster utility object to the
+                         * parser.
+                         */
+                        RSSPullParser localPicasaPullParser = new RSSPullParser();
+
+                        localPicasaPullParser.parseXml(
+                            localURLConnection.getInputStream(),
+                            mBroadcaster);
+
+                        // Reports that the service is now writing data to the content provider.
+                        mBroadcaster.broadcastIntentWithState(Constants.STATE_ACTION_WRITING);
+
+                        // Gets image data from the parser
+                        Vector<ContentValues> imageValues = localPicasaPullParser.getImages();
+
+                        // Stores the number of images
+                        int imageVectorSize = imageValues.size();
+
+                        // Creates one ContentValues for each image
+                        ContentValues[] imageValuesArray = new ContentValues[imageVectorSize];
+
+                        imageValuesArray = imageValues.toArray(imageValuesArray);
+
+                        /*
+                         * Stores the image data in the content provider. The content provider
+                         * throws an exception if the URI is invalid.
+                         */
+                        getContentResolver().bulkInsert(
+                                DataProviderContract.PICTUREURL_TABLE_CONTENTURI, imageValuesArray);
+
+                        // Creates another ContentValues for storing date information
+                        ContentValues dateValues = new ContentValues();
+
+                        // Adds the URL's last modified date to the ContentValues
+                        dateValues.put(DataProviderContract.DATA_DATE_COLUMN, lastModifiedDate);
+
+                        if (newMetadataRetrieved) {
+
+                            // No previous metadata existed, so insert the data
+                            getContentResolver().insert(
+                                DataProviderContract.DATE_TABLE_CONTENTURI,
+                                dateValues
+                            );
+
+                        } else {
+
+                            // Previous metadata existed, so update it.
+                            getContentResolver().update(
+                                    DataProviderContract.DATE_TABLE_CONTENTURI,
+                                    dateValues,
+                                    DataProviderContract.ROW_ID + "=" +
+                                            cursor.getString(cursor.getColumnIndex(
+                                                            DataProviderContract.ROW_ID)), null);
+                        }
+                        break;
+
+                }
+
+                // Reports that the feed retrieval is complete.
+                mBroadcaster.broadcastIntentWithState(Constants.STATE_ACTION_COMPLETE);
+            }
+
+        // Handles possible exceptions
+        } catch (MalformedURLException localMalformedURLException) {
+
+            localMalformedURLException.printStackTrace();
+
+        } catch (IOException localIOException) {
+
+            localIOException.printStackTrace();
+
+        } catch (XmlPullParserException localXmlPullParserException) {
+
+            localXmlPullParserException.printStackTrace();
+
+        } finally {
+
+            // If an exception occurred, close the cursor to prevent memory leaks.
+            if (null != cursor) {
+                cursor.close();
+            }
+        }
+    }
+
+}