Android Training: Threads sample app
Change-Id: I6d07871acad31807eb4bf06aa8406b722320680c
This commit is contained in:
66
samples/training/threadsample/AndroidManifest.xml
Normal file
66
samples/training/threadsample/AndroidManifest.xml
Normal file
@@ -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>
|
||||
BIN
samples/training/threadsample/res/drawable-hdpi/icon.png
Normal file
BIN
samples/training/threadsample/res/drawable-hdpi/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
samples/training/threadsample/res/drawable-ldpi/icon.png
Normal file
BIN
samples/training/threadsample/res/drawable-ldpi/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
samples/training/threadsample/res/drawable-mdpi/icon.png
Normal file
BIN
samples/training/threadsample/res/drawable-mdpi/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
samples/training/threadsample/res/drawable-xhdpi/icon.png
Normal file
BIN
samples/training/threadsample/res/drawable-xhdpi/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
24
samples/training/threadsample/res/layout/fragmenthost.xml
Normal file
24
samples/training/threadsample/res/layout/fragmenthost.xml
Normal file
@@ -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" />
|
||||
30
samples/training/threadsample/res/layout/galleryitem.xml
Normal file
30
samples/training/threadsample/res/layout/galleryitem.xml
Normal file
@@ -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>
|
||||
42
samples/training/threadsample/res/layout/gridlist.xml
Normal file
42
samples/training/threadsample/res/layout/gridlist.xml
Normal file
@@ -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>
|
||||
32
samples/training/threadsample/res/layout/photo.xml
Normal file
32
samples/training/threadsample/res/layout/photo.xml
Normal file
@@ -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>
|
||||
37
samples/training/threadsample/res/layout/progress.xml
Normal file
37
samples/training/threadsample/res/layout/progress.xml
Normal file
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="thumbSize">160.0dip</dimen>
|
||||
</resources>
|
||||
6
samples/training/threadsample/res/values-v11/styles.xml
Normal file
6
samples/training/threadsample/res/values-v11/styles.xml
Normal file
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="thumbSize">160.0dip</dimen>
|
||||
</resources>
|
||||
8
samples/training/threadsample/res/values/attrs.xml
Normal file
8
samples/training/threadsample/res/values/attrs.xml
Normal file
@@ -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>
|
||||
9
samples/training/threadsample/res/values/bools.xml
Normal file
9
samples/training/threadsample/res/values/bools.xml
Normal file
@@ -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>
|
||||
4
samples/training/threadsample/res/values/dimens.xml
Normal file
4
samples/training/threadsample/res/values/dimens.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="thumbSize">160.0dip</dimen>
|
||||
</resources>
|
||||
13
samples/training/threadsample/res/values/strings.xml
Normal file
13
samples/training/threadsample/res/values/strings.xml
Normal file
@@ -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>
|
||||
24
samples/training/threadsample/res/values/styles.xml
Normal file
24
samples/training/threadsample/res/values/styles.xml
Normal file
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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 = " ";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user