diff --git a/build/sdk.atree b/build/sdk.atree index f6ba3004c..0f9cb949e 100644 --- a/build/sdk.atree +++ b/build/sdk.atree @@ -109,6 +109,7 @@ development/samples/CubeLiveWallpaper samples/${PLATFORM_NAME}/CubeLiveWa development/samples/VoiceRecognitionService samples/${PLATFORM_NAME}/VoiceRecognitionService development/samples/TicTacToeLib samples/${PLATFORM_NAME}/TicTacToeLib development/samples/TicTacToeMain samples/${PLATFORM_NAME}/TicTacToeMain +development/samples/XmlAdapters samples/${PLATFORM_NAME}/XmlAdapters # dx bin/dx platforms/${PLATFORM_NAME}/tools/dx diff --git a/samples/XmlAdapters/AndroidManifest.xml b/samples/XmlAdapters/AndroidManifest.xml index af040adff..e4ac4d89d 100644 --- a/samples/XmlAdapters/AndroidManifest.xml +++ b/samples/XmlAdapters/AndroidManifest.xml @@ -17,15 +17,40 @@ + + + - + + + + + + + + + + + + + + + + + + + diff --git a/samples/XmlAdapters/_index.html b/samples/XmlAdapters/_index.html new file mode 100644 index 000000000..2ca36085c --- /dev/null +++ b/samples/XmlAdapters/_index.html @@ -0,0 +1,31 @@ +

This sample demonstrates the use of XML adapters.

+ +

An XML Adapter is an XML file which defines the bindings between the data +retrieved from a +ContentProvider +and the different views of a layout. They are provided by the +Adapters +class.

+ +Three list activities are provided which illustrate this: + + +XmlPhotosAdapter +XmlRssReader diff --git a/samples/XmlAdapters/res/layout/photo_item.xml b/samples/XmlAdapters/res/layout/photo_item.xml new file mode 100644 index 000000000..f24b14395 --- /dev/null +++ b/samples/XmlAdapters/res/layout/photo_item.xml @@ -0,0 +1,41 @@ + + + + + + + + + + diff --git a/samples/XmlAdapters/res/layout/photos_list.xml b/samples/XmlAdapters/res/layout/photos_list.xml new file mode 100644 index 000000000..5756f378e --- /dev/null +++ b/samples/XmlAdapters/res/layout/photos_list.xml @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/samples/XmlAdapters/res/layout/rss_feed_item.xml b/samples/XmlAdapters/res/layout/rss_feed_item.xml new file mode 100644 index 000000000..3975aecd2 --- /dev/null +++ b/samples/XmlAdapters/res/layout/rss_feed_item.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + diff --git a/samples/XmlAdapters/res/layout/rss_feeds_list.xml b/samples/XmlAdapters/res/layout/rss_feeds_list.xml new file mode 100644 index 000000000..b761dcf62 --- /dev/null +++ b/samples/XmlAdapters/res/layout/rss_feeds_list.xml @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/samples/XmlAdapters/res/values/strings.xml b/samples/XmlAdapters/res/values/strings.xml index 3a0b5fed9..1ae158e95 100644 --- a/samples/XmlAdapters/res/values/strings.xml +++ b/samples/XmlAdapters/res/values/strings.xml @@ -15,6 +15,12 @@ --> - Xml Contacts Adapter + Xml Adapters + Xml Contacts Adapter + Xml Photos Adapter + Xml RSS Reader No contacts available + Loading photos... + Loading RSS feed... + diff --git a/samples/XmlAdapters/res/xml/contacts.xml b/samples/XmlAdapters/res/xml/contacts.xml index b33d948b5..02cfbca96 100644 --- a/samples/XmlAdapters/res/xml/contacts.xml +++ b/samples/XmlAdapters/res/xml/contacts.xml @@ -15,11 +15,11 @@ --> + android:uri="content://com.android.contacts/contacts" + android:selection="has_phone_number=1" + android:layout="@layout/contact_item"> - + diff --git a/samples/XmlAdapters/res/xml/photos.xml b/samples/XmlAdapters/res/xml/photos.xml new file mode 100644 index 000000000..a62e62e72 --- /dev/null +++ b/samples/XmlAdapters/res/xml/photos.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/samples/XmlAdapters/res/xml/rss_feed.xml b/samples/XmlAdapters/res/xml/rss_feed.xml new file mode 100644 index 000000000..a5f09f115 --- /dev/null +++ b/samples/XmlAdapters/res/xml/rss_feed.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + diff --git a/samples/XmlAdapters/src/com/example/android/xmladapters/ContactPhotoBinder.java b/samples/XmlAdapters/src/com/example/android/xmladapters/ContactPhotoBinder.java index 947eb2ab3..a5d556f65 100644 --- a/samples/XmlAdapters/src/com/example/android/xmladapters/ContactPhotoBinder.java +++ b/samples/XmlAdapters/src/com/example/android/xmladapters/ContactPhotoBinder.java @@ -35,7 +35,7 @@ import java.util.HashMap; /** * This custom cursor binder is used by the adapter defined in res/xml to - * bin contacts photos to their respective list item. This binder simply + * bind contacts photos to their respective list item. This binder simply * queries a contact's photo based on the contact's id and sets the * photo as a compound drawable on the TextView used to display the contact's * name. @@ -54,7 +54,7 @@ public class ContactPhotoBinder extends Adapters.CursorBinder { mResources = mContext.getResources(); // Default picture used when a contact does not provide one mDefault = mResources.getDrawable(R.drawable.ic_contact_picture); - // Cache used to avoid requerying contacts photos every time + // Cache used to avoid re-querying contacts photos every time mCache = new HashMap(); // Compute the size of the photo based on the display's density mPhotoSize = (int) (PHOTO_SIZE_DIP * mResources.getDisplayMetrics().density + 0.5f); @@ -76,6 +76,7 @@ public class ContactPhotoBinder extends Adapters.CursorBinder { // Creates the drawable for the contact's photo or use our fallback drawable if (stream != null) { + // decoding the bitmap could be done in a worker thread too. Bitmap bitmap = BitmapFactory.decodeStream(stream); d = new BitmapDrawable(mResources, bitmap); } else { diff --git a/samples/XmlAdapters/src/com/example/android/xmladapters/ImageDownloader.java b/samples/XmlAdapters/src/com/example/android/xmladapters/ImageDownloader.java new file mode 100644 index 000000000..08e144c7f --- /dev/null +++ b/samples/XmlAdapters/src/com/example/android/xmladapters/ImageDownloader.java @@ -0,0 +1,346 @@ +/* + * Copyright (C) 2010 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.xmladapters; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.HttpGet; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.net.http.AndroidHttpClient; +import android.os.AsyncTask; +import android.os.Handler; +import android.util.Log; +import android.widget.ImageView; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.concurrent.ConcurrentHashMap; + +/** + * This helper class download images from the Internet and binds those with the provided ImageView. + * + *

It requires the INTERNET permission, which should be added to your application's manifest + * file.

+ * + * A local cache of downloaded images is maintained internally to improve performance. + */ +public class ImageDownloader { + private static final String LOG_TAG = "ImageDownloader"; + + private static final int HARD_CACHE_CAPACITY = 40; + private static final int DELAY_BEFORE_PURGE = 30 * 1000; // in milliseconds + + // Hard cache, with a fixed maximum capacity and a life duration + private final HashMap sHardBitmapCache = + new LinkedHashMap(HARD_CACHE_CAPACITY / 2, 0.75f, true) { + @Override + protected boolean removeEldestEntry(LinkedHashMap.Entry eldest) { + if (size() > HARD_CACHE_CAPACITY) { + // Entries push-out of hard reference cache are transferred to soft reference cache + sSoftBitmapCache.put(eldest.getKey(), new SoftReference(eldest.getValue())); + return true; + } else + return false; + } + }; + + // Soft cache for bitmap kicked out of hard cache + private final static ConcurrentHashMap> sSoftBitmapCache = + new ConcurrentHashMap>(HARD_CACHE_CAPACITY / 2); + + private final Handler purgeHandler = new Handler(); + + private final Runnable purger = new Runnable() { + public void run() { + clearCache(); + } + }; + + /** + * Download the specified image from the Internet and binds it to the provided ImageView. The + * binding is immediate if the image is found in the cache and will be done asynchronously + * otherwise. A null bitmap will be associated to the ImageView if an error occurs. + * + * @param url The URL of the image to download. + * @param imageView The ImageView to bind the downloaded image to. + */ + public void download(String url, ImageView imageView) { + download(url, imageView, null); + } + + /** + * Same as {@link #download(String, ImageView)}, with the possibility to provide an additional + * cookie that will be used when the image will be retrieved. + * + * @param url The URL of the image to download. + * @param imageView The ImageView to bind the downloaded image to. + * @param cookie A cookie String that will be used by the http connection. + */ + public void download(String url, ImageView imageView, String cookie) { + resetPurgeTimer(); + Bitmap bitmap = getBitmapFromCache(url); + + if (bitmap == null) { + forceDownload(url, imageView, cookie); + } else { + cancelPotentialDownload(url, imageView); + imageView.setImageBitmap(bitmap); + } + } + + /* + * Same as download but the image is always downloaded and the cache is not used. + * Kept private at the moment as its interest is not clear. + private void forceDownload(String url, ImageView view) { + forceDownload(url, view, null); + } + */ + + /** + * Same as download but the image is always downloaded and the cache is not used. + * Kept private at the moment as its interest is not clear. + */ + private void forceDownload(String url, ImageView imageView, String cookie) { + // State sanity: url is guaranteed to never be null in DownloadedDrawable and cache keys. + if (url == null) { + imageView.setImageDrawable(null); + return; + } + + if (cancelPotentialDownload(url, imageView)) { + BitmapDownloaderTask task = new BitmapDownloaderTask(imageView); + DownloadedDrawable downloadedDrawable = new DownloadedDrawable(task); + imageView.setImageDrawable(downloadedDrawable); + task.execute(url, cookie); + } + } + + /** + * Clears the image cache used internally to improve performance. Note that for memory + * efficiency reasons, the cache will automatically be cleared after a certain inactivity delay. + */ + public void clearCache() { + sHardBitmapCache.clear(); + sSoftBitmapCache.clear(); + } + + private void resetPurgeTimer() { + purgeHandler.removeCallbacks(purger); + purgeHandler.postDelayed(purger, DELAY_BEFORE_PURGE); + } + + /** + * Returns true if the current download has been canceled or if there was no download in + * progress on this image view. + * Returns false if the download in progress deals with the same url. The download is not + * stopped in that case. + */ + private static boolean cancelPotentialDownload(String url, ImageView imageView) { + BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView); + + if (bitmapDownloaderTask != null) { + String bitmapUrl = bitmapDownloaderTask.url; + if ((bitmapUrl == null) || (!bitmapUrl.equals(url))) { + bitmapDownloaderTask.cancel(true); + } else { + // The same URL is already being downloaded. + return false; + } + } + return true; + } + + /** + * @param imageView Any imageView + * @return Retrieve the currently active download task (if any) associated with this imageView. + * null if there is no such task. + */ + private static BitmapDownloaderTask getBitmapDownloaderTask(ImageView imageView) { + if (imageView != null) { + Drawable drawable = imageView.getDrawable(); + if (drawable instanceof DownloadedDrawable) { + DownloadedDrawable downloadedDrawable = (DownloadedDrawable)drawable; + return downloadedDrawable.getBitmapDownloaderTask(); + } + } + return null; + } + + /** + * @param url The URL of the image that will be retrieved from the cache. + * @return The cached bitmap or null if it was not found. + */ + private Bitmap getBitmapFromCache(String url) { + // First try the hard reference cache + synchronized (sHardBitmapCache) { + final Bitmap bitmap = sHardBitmapCache.get(url); + if (bitmap != null) { + // Bitmap found in hard cache + // Move element to first position, so that it is removed last + sHardBitmapCache.remove(url); + sHardBitmapCache.put(url, bitmap); + return bitmap; + } + } + + // Then try the soft reference cache + SoftReference bitmapReference = sSoftBitmapCache.get(url); + if (bitmapReference != null) { + final Bitmap bitmap = bitmapReference.get(); + if (bitmap != null) { + // Bitmap found in soft cache + return bitmap; + } else { + // Soft reference has been Garbage Collected + sSoftBitmapCache.remove(url); + } + } + + return null; + } + + /** + * The actual AsyncTask that will asynchronously download the image. + */ + class BitmapDownloaderTask extends AsyncTask { + private static final int IO_BUFFER_SIZE = 4 * 1024; + private String url; + private final WeakReference imageViewReference; + + public BitmapDownloaderTask(ImageView imageView) { + imageViewReference = new WeakReference(imageView); + } + + /** + * Actual download method. + */ + @Override + protected Bitmap doInBackground(String... params) { + final AndroidHttpClient client = AndroidHttpClient.newInstance("Android"); + url = params[0]; + final HttpGet getRequest = new HttpGet(url); + String cookie = params[1]; + if (cookie != null) { + getRequest.setHeader("cookie", cookie); + } + + try { + HttpResponse response = client.execute(getRequest); + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + final HttpEntity entity = response.getEntity(); + if (entity != null) { + final InputStream inputStream = entity.getContent(); + final ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); + final OutputStream out = + new BufferedOutputStream(dataStream, IO_BUFFER_SIZE); + copy(inputStream, out); + out.flush(); + out.close(); + inputStream.close(); + + final byte[] data = dataStream.toByteArray(); + final Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); + + // FIXME : Should use BitmapFactory.decodeStream(inputStream) instead. + //final Bitmap bitmap = BitmapFactory.decodeStream(inputStream); + + return bitmap; + } + } + } catch (IOException e) { + getRequest.abort(); + Log.w(LOG_TAG, "I/O error while retrieving bitmap from " + url, e); + } catch (IllegalStateException e) { + getRequest.abort(); + Log.w(LOG_TAG, "Incorrect URL: " + url); + } catch (Exception e) { + getRequest.abort(); + Log.w(LOG_TAG, "Error while retrieving bitmap from " + url, e); + } finally { + if (client != null) { + client.close(); + } + } + return null; + } + + /** + * Once the image is downloaded, associates it to the imageView + */ + @Override + protected void onPostExecute(Bitmap bitmap) { + if (isCancelled()) { + bitmap = null; + } + + // Add bitmap to cache + if (bitmap != null) { + synchronized (sHardBitmapCache) { + sHardBitmapCache.put(url, bitmap); + } + } + + if (imageViewReference != null) { + ImageView imageView = imageViewReference.get(); + BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView); + // Change bitmap only if this process is still associated with it + if (this == bitmapDownloaderTask) { + imageView.setImageBitmap(bitmap); + } + } + } + + public void copy(InputStream in, OutputStream out) throws IOException { + byte[] b = new byte[IO_BUFFER_SIZE]; + int read; + while ((read = in.read(b)) != -1) { + out.write(b, 0, read); + } + } + } + + /** + * A fake Drawable that will be attached to the imageView while the download is in progress. + * + *

Contains a reference to the actual download task, so that a download task can be stopped + * if a new binding is required, and makes sure that only the last started download process can + * bind its result, independently of the download finish order.

+ */ + static class DownloadedDrawable extends ColorDrawable { + private final WeakReference bitmapDownloaderTaskReference; + + public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) { + super(0); + bitmapDownloaderTaskReference = + new WeakReference(bitmapDownloaderTask); + } + + public BitmapDownloaderTask getBitmapDownloaderTask() { + return bitmapDownloaderTaskReference.get(); + } + } +} diff --git a/samples/XmlAdapters/src/com/example/android/xmladapters/PhotosListActivity.java b/samples/XmlAdapters/src/com/example/android/xmladapters/PhotosListActivity.java new file mode 100644 index 000000000..1da151dbb --- /dev/null +++ b/samples/XmlAdapters/src/com/example/android/xmladapters/PhotosListActivity.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2010 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.xmladapters; + +import android.app.ListActivity; +import android.net.Uri; +import android.os.Bundle; +import android.widget.Adapters; + +/** + * This activity uses a custom cursor adapter which fetches a XML photo feed and parses the XML to + * extract the images' URL and their title. + */ +public class PhotosListActivity extends ListActivity { + private static final String PICASA_FEED_URL = + "http://picasaweb.google.com/data/feed/api/featured?max-results=50&thumbsize=144c"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.photos_list); + setListAdapter(Adapters.loadCursorAdapter(this, R.xml.photos, + "content://xmldocument/?url=" + Uri.encode(PICASA_FEED_URL))); + } +} diff --git a/samples/XmlAdapters/src/com/example/android/xmladapters/RssReaderActivity.java b/samples/XmlAdapters/src/com/example/android/xmladapters/RssReaderActivity.java new file mode 100644 index 000000000..fb3e4c17d --- /dev/null +++ b/samples/XmlAdapters/src/com/example/android/xmladapters/RssReaderActivity.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2010 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.xmladapters; + +import android.app.ListActivity; +import android.content.XmlDocumentProvider; +import android.net.Uri; +import android.os.Bundle; +import android.widget.Adapters; +import android.widget.AdapterView.OnItemClickListener; + +/** + * This example demonstrate the creation of a simple RSS feed reader using the XML adapter syntax. + * The different elements of the feed are extracted using an {@link XmlDocumentProvider} and are + * binded to the different views. An {@link OnItemClickListener} is also added, which will open a + * browser on the associated news item page. + */ +public class RssReaderActivity extends ListActivity { + private static final String FEED_URI = "http://feeds.nytimes.com/nyt/rss/HomePage"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.rss_feeds_list); + setListAdapter(Adapters.loadCursorAdapter(this, R.xml.rss_feed, + "content://xmldocument/?url=" + Uri.encode(FEED_URI))); + getListView().setOnItemClickListener(new UrlIntentListener()); + } +} diff --git a/samples/XmlAdapters/src/com/example/android/xmladapters/UrlImageBinder.java b/samples/XmlAdapters/src/com/example/android/xmladapters/UrlImageBinder.java new file mode 100644 index 000000000..33b1e8eb9 --- /dev/null +++ b/samples/XmlAdapters/src/com/example/android/xmladapters/UrlImageBinder.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2010 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.xmladapters; + +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.widget.Adapters; +import android.widget.ImageView; + +/** + * This CursorBinder binds the provided image URL to an ImageView by downloading the image from the + * Internet. + */ +public class UrlImageBinder extends Adapters.CursorBinder { + + private final ImageDownloader imageDownloader; + + public UrlImageBinder(Context context, Adapters.CursorTransformation transformation) { + super(context, transformation); + imageDownloader = new ImageDownloader(); + } + + @Override + public boolean bind(View view, Cursor cursor, int columnIndex) { + if (view instanceof ImageView) { + final String url = mTransformation.transform(cursor, columnIndex); + imageDownloader.download(url, (ImageView) view); + return true; + } + + return false; + } +} diff --git a/samples/XmlAdapters/src/com/example/android/xmladapters/UrlIntentListener.java b/samples/XmlAdapters/src/com/example/android/xmladapters/UrlIntentListener.java new file mode 100644 index 000000000..af814d13b --- /dev/null +++ b/samples/XmlAdapters/src/com/example/android/xmladapters/UrlIntentListener.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2010 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.xmladapters; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; + +/** + * A listener which expects a URL as a tag of the view it is associated with. It then opens the URL + * in the browser application. + */ +public class UrlIntentListener implements OnItemClickListener { + + public void onItemClick(AdapterView parent, View view, int position, long id) { + final String url = view.getTag().toString(); + final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final Context context = parent.getContext(); + context.startActivity(intent); + } + +}