diff options
author | Joe Malin <jmalin@google.com> | 2012-12-13 10:57:14 -0800 |
---|---|---|
committer | Android Git Automerger <android-git-automerger@android.com> | 2012-12-13 10:57:14 -0800 |
commit | 1e083591aed6b77408f6fce363bd48e63db92ed4 (patch) | |
tree | 003a2aa9dd21c57682a7143a5f3c50d613e3e106 | |
parent | 80e0ac6abc4a58d7442cc1d0707e77f4636ea0bd (diff) | |
parent | 2c063c889aa816e0de91bf17fdc0c78f48d5e2d0 (diff) | |
download | development-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
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 Binary files differnew file mode 100644 index 000000000..bf461aac3 --- /dev/null +++ b/samples/training/threadsample/res/drawable-hdpi/icon.png diff --git a/samples/training/threadsample/res/drawable-ldpi/icon.png b/samples/training/threadsample/res/drawable-ldpi/icon.png Binary files differnew file mode 100644 index 000000000..e80fe015d --- /dev/null +++ b/samples/training/threadsample/res/drawable-ldpi/icon.png diff --git a/samples/training/threadsample/res/drawable-mdpi/icon.png b/samples/training/threadsample/res/drawable-mdpi/icon.png Binary files differnew file mode 100644 index 000000000..ba7a8532d --- /dev/null +++ b/samples/training/threadsample/res/drawable-mdpi/icon.png diff --git a/samples/training/threadsample/res/drawable-xhdpi/icon.png b/samples/training/threadsample/res/drawable-xhdpi/icon.png Binary files differnew file mode 100644 index 000000000..72c2b2769 --- /dev/null +++ b/samples/training/threadsample/res/drawable-xhdpi/icon.png 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(); + } + } + } + +} |