Android Training: Threads sample app

Change-Id: I6d07871acad31807eb4bf06aa8406b722320680c
This commit is contained in:
Joe Malin
2012-11-26 09:35:08 -08:00
parent 70db2b6bce
commit 2c063c889a
41 changed files with 4535 additions and 0 deletions

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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" />

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="thumbSize">160.0dip</dimen>
</resources>

View 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>

View File

@@ -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>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="thumbSize">160.0dip</dimen>
</resources>

View 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>

View 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>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="thumbSize">160.0dip</dimen>
</resources>

View 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>

View 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>

View File

@@ -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);
}
}

View File

@@ -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 = " ";
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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++;
}
}
}
}

View File

@@ -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();
}
}
}
}