summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoe Malin <jmalin@google.com>2012-12-13 10:57:14 -0800
committerAndroid Git Automerger <android-git-automerger@android.com>2012-12-13 10:57:14 -0800
commit1e083591aed6b77408f6fce363bd48e63db92ed4 (patch)
tree003a2aa9dd21c57682a7143a5f3c50d613e3e106
parent80e0ac6abc4a58d7442cc1d0707e77f4636ea0bd (diff)
parent2c063c889aa816e0de91bf17fdc0c78f48d5e2d0 (diff)
downloaddevelopment-jb-dev.tar.gz
am 2c063c88: Android Training: Threads sample appandroid-cts-4.1_r4android-cts-4.1_r2jb-dev
* commit '2c063c889aa816e0de91bf17fdc0c78f48d5e2d0': Android Training: Threads sample app
-rw-r--r--samples/training/threadsample/AndroidManifest.xml66
-rw-r--r--samples/training/threadsample/res/drawable-hdpi/icon.pngbin0 -> 2985 bytes
-rw-r--r--samples/training/threadsample/res/drawable-ldpi/icon.pngbin0 -> 1360 bytes
-rw-r--r--samples/training/threadsample/res/drawable-mdpi/icon.pngbin0 -> 1769 bytes
-rw-r--r--samples/training/threadsample/res/drawable-xhdpi/icon.pngbin0 -> 3670 bytes
-rw-r--r--samples/training/threadsample/res/drawable/decodedecoding.xml6
-rw-r--r--samples/training/threadsample/res/drawable/decodequeued.xml6
-rw-r--r--samples/training/threadsample/res/drawable/imagedownloadfailed.xml6
-rw-r--r--samples/training/threadsample/res/drawable/imagedownloading.xml6
-rw-r--r--samples/training/threadsample/res/drawable/imagenotqueued.xml6
-rw-r--r--samples/training/threadsample/res/drawable/imagequeued.xml5
-rw-r--r--samples/training/threadsample/res/layout/fragmenthost.xml24
-rw-r--r--samples/training/threadsample/res/layout/galleryitem.xml30
-rw-r--r--samples/training/threadsample/res/layout/gridlist.xml42
-rw-r--r--samples/training/threadsample/res/layout/photo.xml32
-rw-r--r--samples/training/threadsample/res/layout/progress.xml37
-rw-r--r--samples/training/threadsample/res/values-sw600dp/bools.xml7
-rw-r--r--samples/training/threadsample/res/values-sw600dp/dimens.xml4
-rw-r--r--samples/training/threadsample/res/values-v11/styles.xml6
-rw-r--r--samples/training/threadsample/res/values-xlarge/bools.xml7
-rw-r--r--samples/training/threadsample/res/values-xlarge/dimens.xml4
-rw-r--r--samples/training/threadsample/res/values/attrs.xml8
-rw-r--r--samples/training/threadsample/res/values/bools.xml9
-rw-r--r--samples/training/threadsample/res/values/dimens.xml4
-rw-r--r--samples/training/threadsample/res/values/strings.xml13
-rw-r--r--samples/training/threadsample/res/values/styles.xml24
-rw-r--r--samples/training/threadsample/src/com/example/android/threadsample/BroadcastNotifier.java87
-rw-r--r--samples/training/threadsample/src/com/example/android/threadsample/Constants.java91
-rw-r--r--samples/training/threadsample/src/com/example/android/threadsample/DataProvider.java482
-rw-r--r--samples/training/threadsample/src/com/example/android/threadsample/DataProviderContract.java99
-rw-r--r--samples/training/threadsample/src/com/example/android/threadsample/DisplayActivity.java533
-rw-r--r--samples/training/threadsample/src/com/example/android/threadsample/PhotoDecodeRunnable.java288
-rw-r--r--samples/training/threadsample/src/com/example/android/threadsample/PhotoDownloadRunnable.java396
-rw-r--r--samples/training/threadsample/src/com/example/android/threadsample/PhotoFragment.java187
-rw-r--r--samples/training/threadsample/src/com/example/android/threadsample/PhotoManager.java446
-rw-r--r--samples/training/threadsample/src/com/example/android/threadsample/PhotoTask.java298
-rw-r--r--samples/training/threadsample/src/com/example/android/threadsample/PhotoThumbnailFragment.java436
-rw-r--r--samples/training/threadsample/src/com/example/android/threadsample/PhotoView.java349
-rw-r--r--samples/training/threadsample/src/com/example/android/threadsample/ProgressNotifier.java21
-rw-r--r--samples/training/threadsample/src/com/example/android/threadsample/RSSPullParser.java204
-rw-r--r--samples/training/threadsample/src/com/example/android/threadsample/RSSPullService.java266
41 files changed, 4535 insertions, 0 deletions
diff --git a/samples/training/threadsample/AndroidManifest.xml b/samples/training/threadsample/AndroidManifest.xml
new file mode 100644
index 000000000..7b3920726
--- /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 000000000..bf461aac3
--- /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 000000000..e80fe015d
--- /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 000000000..ba7a8532d
--- /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 000000000..72c2b2769
--- /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 000000000..0133903f4
--- /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 000000000..fdbbe1a7e
--- /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 000000000..ca1d72fa3
--- /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 000000000..46a707e09
--- /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 000000000..ea2eec58a
--- /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 000000000..f1342b879
--- /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 000000000..39e72f1b2
--- /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 000000000..187ba771e
--- /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 000000000..df074239f
--- /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 000000000..93b5ca63b
--- /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 000000000..12ff0ed75
--- /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 000000000..ccba3a5af
--- /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 000000000..c3fe548cc
--- /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 000000000..6f91b36ec
--- /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 000000000..ccba3a5af
--- /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 000000000..c3fe548cc
--- /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 000000000..b98729831
--- /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 000000000..6a944d788
--- /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 000000000..c3fe548cc
--- /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 000000000..81d9035c0
--- /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 000000000..d293831c3
--- /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 000000000..120f6e026
--- /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 000000000..0d8909f5d
--- /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 000000000..3741fec0c
--- /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 000000000..e7dd73b2f
--- /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 000000000..f3abdf248
--- /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 000000000..1f867e52b
--- /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 000000000..47b6007f0
--- /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 000000000..9b2ce014a
--- /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 000000000..783b0a3b7
--- /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 000000000..b07c49745
--- /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 000000000..0a89e9eba
--- /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 000000000..eaf394004
--- /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 000000000..479ba79c5
--- /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 000000000..48b2151f1
--- /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 000000000..b8b3e2681
--- /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();
+ }
+ }
+ }
+
+}