Updating code sample for "displaying bitmaps efficiently" training class.

Changes:
 -Use updated versions of ImageWorker & ImageCache from I/O 2012 app
 -Use copied DiskLruCache from system (rather than custom)
 -Use copied AsyncTask from system (to keep behavior consistent)
 -Ensure no strict mode violations or lint errors
 -Other misc bug fixes
 -Move single-use static methods in Utils to corresponding class

Change-Id: If21e045db1e1a80391169f3c9c9258d48345ab6b
This commit is contained in:
Adam Koch
2012-08-08 11:21:26 -04:00
parent ef91cce81b
commit 03ceb37f79
23 changed files with 2695 additions and 868 deletions

View File

@@ -16,8 +16,6 @@
package com.example.android.bitmapfun.provider;
import com.example.android.bitmapfun.util.ImageWorker.ImageWorkerAdapter;
/**
* Some simple test data to use for this sample app.
*/
@@ -28,45 +26,45 @@ public class Images {
* used to fetch the URLs.
*/
public final static String[] imageUrls = new String[] {
"https://lh6.googleusercontent.com/-jZgveEqb6pg/T3R4kXScycI/AAAAAAAAAE0/xQ7CvpfXDzc/s1024/sample_image_01.jpg",
"https://lh4.googleusercontent.com/-K2FMuOozxU0/T3R4lRAiBTI/AAAAAAAAAE8/a3Eh9JvnnzI/s1024/sample_image_02.jpg",
"https://lh5.googleusercontent.com/-SCS5C646rxM/T3R4l7QB6xI/AAAAAAAAAFE/xLcuVv3CUyA/s1024/sample_image_03.jpg",
"https://lh6.googleusercontent.com/-f0NJR6-_Thg/T3R4mNex2wI/AAAAAAAAAFI/45oug4VE8MI/s1024/sample_image_04.jpg",
"https://lh3.googleusercontent.com/-n-xcJmiI0pg/T3R4mkSchHI/AAAAAAAAAFU/EoiNNb7kk3A/s1024/sample_image_05.jpg",
"https://lh3.googleusercontent.com/-X43vAudm7f4/T3R4nGSChJI/AAAAAAAAAFk/3bna6D-2EE8/s1024/sample_image_06.jpg",
"https://lh5.googleusercontent.com/-MpZneqIyjXU/T3R4nuGO1aI/AAAAAAAAAFg/r09OPjLx1ZY/s1024/sample_image_07.jpg",
"https://lh6.googleusercontent.com/-ql3YNfdClJo/T3XvW9apmFI/AAAAAAAAAL4/_6HFDzbahc4/s1024/sample_image_08.jpg",
"https://lh5.googleusercontent.com/-Pxa7eqF4cyc/T3R4oasvPEI/AAAAAAAAAF0/-uYDH92h8LA/s1024/sample_image_09.jpg",
"https://lh4.googleusercontent.com/-Li-rjhFEuaI/T3R4o-VUl4I/AAAAAAAAAF8/5E5XdMnP1oE/s1024/sample_image_10.jpg",
"https://lh5.googleusercontent.com/-_HU4fImgFhA/T3R4pPVIwWI/AAAAAAAAAGA/0RfK_Vkgth4/s1024/sample_image_11.jpg",
"https://lh6.googleusercontent.com/-0gnNrVjwa0Y/T3R4peGYJwI/AAAAAAAAAGU/uX_9wvRPM9I/s1024/sample_image_12.jpg",
"https://lh3.googleusercontent.com/-HBxuzALS_Zs/T3R4qERykaI/AAAAAAAAAGQ/_qQ16FaZ1q0/s1024/sample_image_13.jpg",
"https://lh4.googleusercontent.com/-cKojDrARNjQ/T3R4qfWSGPI/AAAAAAAAAGY/MR5dnbNaPyY/s1024/sample_image_14.jpg",
"https://lh3.googleusercontent.com/-WujkdYfcyZ8/T3R4qrIMGUI/AAAAAAAAAGk/277LIdgvnjg/s1024/sample_image_15.jpg",
"https://lh6.googleusercontent.com/-FMHR7Vy3PgI/T3R4rOXlEKI/AAAAAAAAAGs/VeXrDNDBkaw/s1024/sample_image_16.jpg",
"https://lh4.googleusercontent.com/-mrR0AJyNTH0/T3R4rZs6CuI/AAAAAAAAAG0/UE1wQqCOqLA/s1024/sample_image_17.jpg",
"https://lh6.googleusercontent.com/-z77w0eh3cow/T3R4rnLn05I/AAAAAAAAAG4/BaerfWoNucU/s1024/sample_image_18.jpg",
"https://lh5.googleusercontent.com/-aWVwh1OU5Bk/T3R4sAWw0yI/AAAAAAAAAHE/4_KAvJttFwA/s1024/sample_image_19.jpg",
"https://lh6.googleusercontent.com/-q-js52DMnWQ/T3R4tZhY2sI/AAAAAAAAAHM/A8kjp2Ivdqg/s1024/sample_image_20.jpg",
"https://lh5.googleusercontent.com/-_jIzvvzXKn4/T3R4t7xpdVI/AAAAAAAAAHU/7QC6eZ10jgs/s1024/sample_image_21.jpg",
"https://lh3.googleusercontent.com/-lnGi4IMLpwU/T3R4uCMa7vI/AAAAAAAAAHc/1zgzzz6qTpk/s1024/sample_image_22.jpg",
"https://lh5.googleusercontent.com/-fFCzKjFPsPc/T3R4u0SZPFI/AAAAAAAAAHk/sbgjzrktOK0/s1024/sample_image_23.jpg",
"https://lh4.googleusercontent.com/-8TqoW5gBE_Y/T3R4vBS3NPI/AAAAAAAAAHs/EZYvpNsaNXk/s1024/sample_image_24.jpg",
"https://lh6.googleusercontent.com/-gc4eQ3ySdzs/T3R4vafoA7I/AAAAAAAAAH4/yKii5P6tqDE/s1024/sample_image_25.jpg",
"https://lh5.googleusercontent.com/--NYOPCylU7Q/T3R4vjAiWkI/AAAAAAAAAH8/IPNx5q3ptRA/s1024/sample_image_26.jpg",
"https://lh6.googleusercontent.com/-9IJM8so4vCI/T3R4vwJO2yI/AAAAAAAAAIE/ljlr-cwuqZM/s1024/sample_image_27.jpg",
"https://lh4.googleusercontent.com/-KW6QwOHfhBs/T3R4w0RsQiI/AAAAAAAAAIM/uEFLVgHPFCk/s1024/sample_image_28.jpg",
"https://lh4.googleusercontent.com/-z2557Ec1ctY/T3R4x3QA2hI/AAAAAAAAAIk/9-GzPL1lTWE/s1024/sample_image_29.jpg",
"https://lh5.googleusercontent.com/-LaKXAn4Kr1c/T3R4yc5b4lI/AAAAAAAAAIY/fMgcOVQfmD0/s1024/sample_image_30.jpg",
"https://lh4.googleusercontent.com/-F9LRToJoQdo/T3R4yrLtyQI/AAAAAAAAAIg/ri9uUCWuRmo/s1024/sample_image_31.jpg",
"https://lh4.googleusercontent.com/-6X-xBwP-QpI/T3R4zGVboII/AAAAAAAAAIs/zYH4PjjngY0/s1024/sample_image_32.jpg",
"https://lh5.googleusercontent.com/-VdLRjbW4LAs/T3R4zXu3gUI/AAAAAAAAAIw/9aFp9t7mCPg/s1024/sample_image_33.jpg",
"https://lh6.googleusercontent.com/-gL6R17_fDJU/T3R4zpIXGjI/AAAAAAAAAI8/Q2Vjx-L9X20/s1024/sample_image_34.jpg",
"https://lh3.googleusercontent.com/-1fGH4YJXEzo/T3R40Y1B7KI/AAAAAAAAAJE/MnTsa77g-nk/s1024/sample_image_35.jpg",
"https://lh4.googleusercontent.com/-Ql0jHSrea-A/T3R403mUfFI/AAAAAAAAAJM/qzI4SkcH9tY/s1024/sample_image_36.jpg",
"https://lh5.googleusercontent.com/-BL5FIBR_tzI/T3R41DA0AKI/AAAAAAAAAJk/GZfeeb-SLM0/s1024/sample_image_37.jpg",
"https://lh4.googleusercontent.com/-wF2Vc9YDutw/T3R41fR2BCI/AAAAAAAAAJc/JdU1sHdMRAk/s1024/sample_image_38.jpg",
"https://lh6.googleusercontent.com/-ZWHiPehwjTI/T3R41zuaKCI/AAAAAAAAAJg/hR3QJ1v3REg/s1024/sample_image_39.jpg",
"http://lh6.googleusercontent.com/-jZgveEqb6pg/T3R4kXScycI/AAAAAAAAAE0/xQ7CvpfXDzc/s1024/sample_image_01.jpg",
"http://lh4.googleusercontent.com/-K2FMuOozxU0/T3R4lRAiBTI/AAAAAAAAAE8/a3Eh9JvnnzI/s1024/sample_image_02.jpg",
"http://lh5.googleusercontent.com/-SCS5C646rxM/T3R4l7QB6xI/AAAAAAAAAFE/xLcuVv3CUyA/s1024/sample_image_03.jpg",
"http://lh6.googleusercontent.com/-f0NJR6-_Thg/T3R4mNex2wI/AAAAAAAAAFI/45oug4VE8MI/s1024/sample_image_04.jpg",
"http://lh3.googleusercontent.com/-n-xcJmiI0pg/T3R4mkSchHI/AAAAAAAAAFU/EoiNNb7kk3A/s1024/sample_image_05.jpg",
"http://lh3.googleusercontent.com/-X43vAudm7f4/T3R4nGSChJI/AAAAAAAAAFk/3bna6D-2EE8/s1024/sample_image_06.jpg",
"http://lh5.googleusercontent.com/-MpZneqIyjXU/T3R4nuGO1aI/AAAAAAAAAFg/r09OPjLx1ZY/s1024/sample_image_07.jpg",
"http://lh6.googleusercontent.com/-ql3YNfdClJo/T3XvW9apmFI/AAAAAAAAAL4/_6HFDzbahc4/s1024/sample_image_08.jpg",
"http://lh5.googleusercontent.com/-Pxa7eqF4cyc/T3R4oasvPEI/AAAAAAAAAF0/-uYDH92h8LA/s1024/sample_image_09.jpg",
"http://lh4.googleusercontent.com/-Li-rjhFEuaI/T3R4o-VUl4I/AAAAAAAAAF8/5E5XdMnP1oE/s1024/sample_image_10.jpg",
"http://lh5.googleusercontent.com/-_HU4fImgFhA/T3R4pPVIwWI/AAAAAAAAAGA/0RfK_Vkgth4/s1024/sample_image_11.jpg",
"http://lh6.googleusercontent.com/-0gnNrVjwa0Y/T3R4peGYJwI/AAAAAAAAAGU/uX_9wvRPM9I/s1024/sample_image_12.jpg",
"http://lh3.googleusercontent.com/-HBxuzALS_Zs/T3R4qERykaI/AAAAAAAAAGQ/_qQ16FaZ1q0/s1024/sample_image_13.jpg",
"http://lh4.googleusercontent.com/-cKojDrARNjQ/T3R4qfWSGPI/AAAAAAAAAGY/MR5dnbNaPyY/s1024/sample_image_14.jpg",
"http://lh3.googleusercontent.com/-WujkdYfcyZ8/T3R4qrIMGUI/AAAAAAAAAGk/277LIdgvnjg/s1024/sample_image_15.jpg",
"http://lh6.googleusercontent.com/-FMHR7Vy3PgI/T3R4rOXlEKI/AAAAAAAAAGs/VeXrDNDBkaw/s1024/sample_image_16.jpg",
"http://lh4.googleusercontent.com/-mrR0AJyNTH0/T3R4rZs6CuI/AAAAAAAAAG0/UE1wQqCOqLA/s1024/sample_image_17.jpg",
"http://lh6.googleusercontent.com/-z77w0eh3cow/T3R4rnLn05I/AAAAAAAAAG4/BaerfWoNucU/s1024/sample_image_18.jpg",
"http://lh5.googleusercontent.com/-aWVwh1OU5Bk/T3R4sAWw0yI/AAAAAAAAAHE/4_KAvJttFwA/s1024/sample_image_19.jpg",
"http://lh6.googleusercontent.com/-q-js52DMnWQ/T3R4tZhY2sI/AAAAAAAAAHM/A8kjp2Ivdqg/s1024/sample_image_20.jpg",
"http://lh5.googleusercontent.com/-_jIzvvzXKn4/T3R4t7xpdVI/AAAAAAAAAHU/7QC6eZ10jgs/s1024/sample_image_21.jpg",
"http://lh3.googleusercontent.com/-lnGi4IMLpwU/T3R4uCMa7vI/AAAAAAAAAHc/1zgzzz6qTpk/s1024/sample_image_22.jpg",
"http://lh5.googleusercontent.com/-fFCzKjFPsPc/T3R4u0SZPFI/AAAAAAAAAHk/sbgjzrktOK0/s1024/sample_image_23.jpg",
"http://lh4.googleusercontent.com/-8TqoW5gBE_Y/T3R4vBS3NPI/AAAAAAAAAHs/EZYvpNsaNXk/s1024/sample_image_24.jpg",
"http://lh6.googleusercontent.com/-gc4eQ3ySdzs/T3R4vafoA7I/AAAAAAAAAH4/yKii5P6tqDE/s1024/sample_image_25.jpg",
"http://lh5.googleusercontent.com/--NYOPCylU7Q/T3R4vjAiWkI/AAAAAAAAAH8/IPNx5q3ptRA/s1024/sample_image_26.jpg",
"http://lh6.googleusercontent.com/-9IJM8so4vCI/T3R4vwJO2yI/AAAAAAAAAIE/ljlr-cwuqZM/s1024/sample_image_27.jpg",
"http://lh4.googleusercontent.com/-KW6QwOHfhBs/T3R4w0RsQiI/AAAAAAAAAIM/uEFLVgHPFCk/s1024/sample_image_28.jpg",
"http://lh4.googleusercontent.com/-z2557Ec1ctY/T3R4x3QA2hI/AAAAAAAAAIk/9-GzPL1lTWE/s1024/sample_image_29.jpg",
"http://lh5.googleusercontent.com/-LaKXAn4Kr1c/T3R4yc5b4lI/AAAAAAAAAIY/fMgcOVQfmD0/s1024/sample_image_30.jpg",
"http://lh4.googleusercontent.com/-F9LRToJoQdo/T3R4yrLtyQI/AAAAAAAAAIg/ri9uUCWuRmo/s1024/sample_image_31.jpg",
"http://lh4.googleusercontent.com/-6X-xBwP-QpI/T3R4zGVboII/AAAAAAAAAIs/zYH4PjjngY0/s1024/sample_image_32.jpg",
"http://lh5.googleusercontent.com/-VdLRjbW4LAs/T3R4zXu3gUI/AAAAAAAAAIw/9aFp9t7mCPg/s1024/sample_image_33.jpg",
"http://lh6.googleusercontent.com/-gL6R17_fDJU/T3R4zpIXGjI/AAAAAAAAAI8/Q2Vjx-L9X20/s1024/sample_image_34.jpg",
"http://lh3.googleusercontent.com/-1fGH4YJXEzo/T3R40Y1B7KI/AAAAAAAAAJE/MnTsa77g-nk/s1024/sample_image_35.jpg",
"http://lh4.googleusercontent.com/-Ql0jHSrea-A/T3R403mUfFI/AAAAAAAAAJM/qzI4SkcH9tY/s1024/sample_image_36.jpg",
"http://lh5.googleusercontent.com/-BL5FIBR_tzI/T3R41DA0AKI/AAAAAAAAAJk/GZfeeb-SLM0/s1024/sample_image_37.jpg",
"http://lh4.googleusercontent.com/-wF2Vc9YDutw/T3R41fR2BCI/AAAAAAAAAJc/JdU1sHdMRAk/s1024/sample_image_38.jpg",
"http://lh6.googleusercontent.com/-ZWHiPehwjTI/T3R41zuaKCI/AAAAAAAAAJg/hR3QJ1v3REg/s1024/sample_image_39.jpg",
};
/**
@@ -74,74 +72,44 @@ public class Images {
* should be used to fetch the URLs.
*/
public final static String[] imageThumbUrls = new String[] {
"https://lh6.googleusercontent.com/-jZgveEqb6pg/T3R4kXScycI/AAAAAAAAAE0/xQ7CvpfXDzc/s160-c/sample_image_01.jpg",
"https://lh4.googleusercontent.com/-K2FMuOozxU0/T3R4lRAiBTI/AAAAAAAAAE8/a3Eh9JvnnzI/s160-c/sample_image_02.jpg",
"https://lh5.googleusercontent.com/-SCS5C646rxM/T3R4l7QB6xI/AAAAAAAAAFE/xLcuVv3CUyA/s160-c/sample_image_03.jpg",
"https://lh6.googleusercontent.com/-f0NJR6-_Thg/T3R4mNex2wI/AAAAAAAAAFI/45oug4VE8MI/s160-c/sample_image_04.jpg",
"https://lh3.googleusercontent.com/-n-xcJmiI0pg/T3R4mkSchHI/AAAAAAAAAFU/EoiNNb7kk3A/s160-c/sample_image_05.jpg",
"https://lh3.googleusercontent.com/-X43vAudm7f4/T3R4nGSChJI/AAAAAAAAAFk/3bna6D-2EE8/s160-c/sample_image_06.jpg",
"https://lh5.googleusercontent.com/-MpZneqIyjXU/T3R4nuGO1aI/AAAAAAAAAFg/r09OPjLx1ZY/s160-c/sample_image_07.jpg",
"https://lh6.googleusercontent.com/-ql3YNfdClJo/T3XvW9apmFI/AAAAAAAAAL4/_6HFDzbahc4/s160-c/sample_image_08.jpg",
"https://lh5.googleusercontent.com/-Pxa7eqF4cyc/T3R4oasvPEI/AAAAAAAAAF0/-uYDH92h8LA/s160-c/sample_image_09.jpg",
"https://lh4.googleusercontent.com/-Li-rjhFEuaI/T3R4o-VUl4I/AAAAAAAAAF8/5E5XdMnP1oE/s160-c/sample_image_10.jpg",
"https://lh5.googleusercontent.com/-_HU4fImgFhA/T3R4pPVIwWI/AAAAAAAAAGA/0RfK_Vkgth4/s160-c/sample_image_11.jpg",
"https://lh6.googleusercontent.com/-0gnNrVjwa0Y/T3R4peGYJwI/AAAAAAAAAGU/uX_9wvRPM9I/s160-c/sample_image_12.jpg",
"https://lh3.googleusercontent.com/-HBxuzALS_Zs/T3R4qERykaI/AAAAAAAAAGQ/_qQ16FaZ1q0/s160-c/sample_image_13.jpg",
"https://lh4.googleusercontent.com/-cKojDrARNjQ/T3R4qfWSGPI/AAAAAAAAAGY/MR5dnbNaPyY/s160-c/sample_image_14.jpg",
"https://lh3.googleusercontent.com/-WujkdYfcyZ8/T3R4qrIMGUI/AAAAAAAAAGk/277LIdgvnjg/s160-c/sample_image_15.jpg",
"https://lh6.googleusercontent.com/-FMHR7Vy3PgI/T3R4rOXlEKI/AAAAAAAAAGs/VeXrDNDBkaw/s160-c/sample_image_16.jpg",
"https://lh4.googleusercontent.com/-mrR0AJyNTH0/T3R4rZs6CuI/AAAAAAAAAG0/UE1wQqCOqLA/s160-c/sample_image_17.jpg",
"https://lh6.googleusercontent.com/-z77w0eh3cow/T3R4rnLn05I/AAAAAAAAAG4/BaerfWoNucU/s160-c/sample_image_18.jpg",
"https://lh5.googleusercontent.com/-aWVwh1OU5Bk/T3R4sAWw0yI/AAAAAAAAAHE/4_KAvJttFwA/s160-c/sample_image_19.jpg",
"https://lh6.googleusercontent.com/-q-js52DMnWQ/T3R4tZhY2sI/AAAAAAAAAHM/A8kjp2Ivdqg/s160-c/sample_image_20.jpg",
"https://lh5.googleusercontent.com/-_jIzvvzXKn4/T3R4t7xpdVI/AAAAAAAAAHU/7QC6eZ10jgs/s160-c/sample_image_21.jpg",
"https://lh3.googleusercontent.com/-lnGi4IMLpwU/T3R4uCMa7vI/AAAAAAAAAHc/1zgzzz6qTpk/s160-c/sample_image_22.jpg",
"https://lh5.googleusercontent.com/-fFCzKjFPsPc/T3R4u0SZPFI/AAAAAAAAAHk/sbgjzrktOK0/s160-c/sample_image_23.jpg",
"https://lh4.googleusercontent.com/-8TqoW5gBE_Y/T3R4vBS3NPI/AAAAAAAAAHs/EZYvpNsaNXk/s160-c/sample_image_24.jpg",
"https://lh6.googleusercontent.com/-gc4eQ3ySdzs/T3R4vafoA7I/AAAAAAAAAH4/yKii5P6tqDE/s160-c/sample_image_25.jpg",
"https://lh5.googleusercontent.com/--NYOPCylU7Q/T3R4vjAiWkI/AAAAAAAAAH8/IPNx5q3ptRA/s160-c/sample_image_26.jpg",
"https://lh6.googleusercontent.com/-9IJM8so4vCI/T3R4vwJO2yI/AAAAAAAAAIE/ljlr-cwuqZM/s160-c/sample_image_27.jpg",
"https://lh4.googleusercontent.com/-KW6QwOHfhBs/T3R4w0RsQiI/AAAAAAAAAIM/uEFLVgHPFCk/s160-c/sample_image_28.jpg",
"https://lh4.googleusercontent.com/-z2557Ec1ctY/T3R4x3QA2hI/AAAAAAAAAIk/9-GzPL1lTWE/s160-c/sample_image_29.jpg",
"https://lh5.googleusercontent.com/-LaKXAn4Kr1c/T3R4yc5b4lI/AAAAAAAAAIY/fMgcOVQfmD0/s160-c/sample_image_30.jpg",
"https://lh4.googleusercontent.com/-F9LRToJoQdo/T3R4yrLtyQI/AAAAAAAAAIg/ri9uUCWuRmo/s160-c/sample_image_31.jpg",
"https://lh4.googleusercontent.com/-6X-xBwP-QpI/T3R4zGVboII/AAAAAAAAAIs/zYH4PjjngY0/s160-c/sample_image_32.jpg",
"https://lh5.googleusercontent.com/-VdLRjbW4LAs/T3R4zXu3gUI/AAAAAAAAAIw/9aFp9t7mCPg/s160-c/sample_image_33.jpg",
"https://lh6.googleusercontent.com/-gL6R17_fDJU/T3R4zpIXGjI/AAAAAAAAAI8/Q2Vjx-L9X20/s160-c/sample_image_34.jpg",
"https://lh3.googleusercontent.com/-1fGH4YJXEzo/T3R40Y1B7KI/AAAAAAAAAJE/MnTsa77g-nk/s160-c/sample_image_35.jpg",
"https://lh4.googleusercontent.com/-Ql0jHSrea-A/T3R403mUfFI/AAAAAAAAAJM/qzI4SkcH9tY/s160-c/sample_image_36.jpg",
"https://lh5.googleusercontent.com/-BL5FIBR_tzI/T3R41DA0AKI/AAAAAAAAAJk/GZfeeb-SLM0/s160-c/sample_image_37.jpg",
"https://lh4.googleusercontent.com/-wF2Vc9YDutw/T3R41fR2BCI/AAAAAAAAAJc/JdU1sHdMRAk/s160-c/sample_image_38.jpg",
"https://lh6.googleusercontent.com/-ZWHiPehwjTI/T3R41zuaKCI/AAAAAAAAAJg/hR3QJ1v3REg/s160-c/sample_image_39.jpg",
};
/**
* Simple static adapter to use for images.
*/
public final static ImageWorkerAdapter imageWorkerUrlsAdapter = new ImageWorkerAdapter() {
@Override
public Object getItem(int num) {
return Images.imageUrls[num];
}
@Override
public int getSize() {
return Images.imageUrls.length;
}
};
/**
* Simple static adapter to use for image thumbnails.
*/
public final static ImageWorkerAdapter imageThumbWorkerUrlsAdapter = new ImageWorkerAdapter() {
@Override
public Object getItem(int num) {
return Images.imageThumbUrls[num];
}
@Override
public int getSize() {
return Images.imageThumbUrls.length;
}
"http://lh6.googleusercontent.com/-jZgveEqb6pg/T3R4kXScycI/AAAAAAAAAE0/xQ7CvpfXDzc/s160-c/sample_image_01.jpg",
"http://lh4.googleusercontent.com/-K2FMuOozxU0/T3R4lRAiBTI/AAAAAAAAAE8/a3Eh9JvnnzI/s160-c/sample_image_02.jpg",
"http://lh5.googleusercontent.com/-SCS5C646rxM/T3R4l7QB6xI/AAAAAAAAAFE/xLcuVv3CUyA/s160-c/sample_image_03.jpg",
"http://lh6.googleusercontent.com/-f0NJR6-_Thg/T3R4mNex2wI/AAAAAAAAAFI/45oug4VE8MI/s160-c/sample_image_04.jpg",
"http://lh3.googleusercontent.com/-n-xcJmiI0pg/T3R4mkSchHI/AAAAAAAAAFU/EoiNNb7kk3A/s160-c/sample_image_05.jpg",
"http://lh3.googleusercontent.com/-X43vAudm7f4/T3R4nGSChJI/AAAAAAAAAFk/3bna6D-2EE8/s160-c/sample_image_06.jpg",
"http://lh5.googleusercontent.com/-MpZneqIyjXU/T3R4nuGO1aI/AAAAAAAAAFg/r09OPjLx1ZY/s160-c/sample_image_07.jpg",
"http://lh6.googleusercontent.com/-ql3YNfdClJo/T3XvW9apmFI/AAAAAAAAAL4/_6HFDzbahc4/s160-c/sample_image_08.jpg",
"http://lh5.googleusercontent.com/-Pxa7eqF4cyc/T3R4oasvPEI/AAAAAAAAAF0/-uYDH92h8LA/s160-c/sample_image_09.jpg",
"http://lh4.googleusercontent.com/-Li-rjhFEuaI/T3R4o-VUl4I/AAAAAAAAAF8/5E5XdMnP1oE/s160-c/sample_image_10.jpg",
"http://lh5.googleusercontent.com/-_HU4fImgFhA/T3R4pPVIwWI/AAAAAAAAAGA/0RfK_Vkgth4/s160-c/sample_image_11.jpg",
"http://lh6.googleusercontent.com/-0gnNrVjwa0Y/T3R4peGYJwI/AAAAAAAAAGU/uX_9wvRPM9I/s160-c/sample_image_12.jpg",
"http://lh3.googleusercontent.com/-HBxuzALS_Zs/T3R4qERykaI/AAAAAAAAAGQ/_qQ16FaZ1q0/s160-c/sample_image_13.jpg",
"http://lh4.googleusercontent.com/-cKojDrARNjQ/T3R4qfWSGPI/AAAAAAAAAGY/MR5dnbNaPyY/s160-c/sample_image_14.jpg",
"http://lh3.googleusercontent.com/-WujkdYfcyZ8/T3R4qrIMGUI/AAAAAAAAAGk/277LIdgvnjg/s160-c/sample_image_15.jpg",
"http://lh6.googleusercontent.com/-FMHR7Vy3PgI/T3R4rOXlEKI/AAAAAAAAAGs/VeXrDNDBkaw/s160-c/sample_image_16.jpg",
"http://lh4.googleusercontent.com/-mrR0AJyNTH0/T3R4rZs6CuI/AAAAAAAAAG0/UE1wQqCOqLA/s160-c/sample_image_17.jpg",
"http://lh6.googleusercontent.com/-z77w0eh3cow/T3R4rnLn05I/AAAAAAAAAG4/BaerfWoNucU/s160-c/sample_image_18.jpg",
"http://lh5.googleusercontent.com/-aWVwh1OU5Bk/T3R4sAWw0yI/AAAAAAAAAHE/4_KAvJttFwA/s160-c/sample_image_19.jpg",
"http://lh6.googleusercontent.com/-q-js52DMnWQ/T3R4tZhY2sI/AAAAAAAAAHM/A8kjp2Ivdqg/s160-c/sample_image_20.jpg",
"http://lh5.googleusercontent.com/-_jIzvvzXKn4/T3R4t7xpdVI/AAAAAAAAAHU/7QC6eZ10jgs/s160-c/sample_image_21.jpg",
"http://lh3.googleusercontent.com/-lnGi4IMLpwU/T3R4uCMa7vI/AAAAAAAAAHc/1zgzzz6qTpk/s160-c/sample_image_22.jpg",
"http://lh5.googleusercontent.com/-fFCzKjFPsPc/T3R4u0SZPFI/AAAAAAAAAHk/sbgjzrktOK0/s160-c/sample_image_23.jpg",
"http://lh4.googleusercontent.com/-8TqoW5gBE_Y/T3R4vBS3NPI/AAAAAAAAAHs/EZYvpNsaNXk/s160-c/sample_image_24.jpg",
"http://lh6.googleusercontent.com/-gc4eQ3ySdzs/T3R4vafoA7I/AAAAAAAAAH4/yKii5P6tqDE/s160-c/sample_image_25.jpg",
"http://lh5.googleusercontent.com/--NYOPCylU7Q/T3R4vjAiWkI/AAAAAAAAAH8/IPNx5q3ptRA/s160-c/sample_image_26.jpg",
"http://lh6.googleusercontent.com/-9IJM8so4vCI/T3R4vwJO2yI/AAAAAAAAAIE/ljlr-cwuqZM/s160-c/sample_image_27.jpg",
"http://lh4.googleusercontent.com/-KW6QwOHfhBs/T3R4w0RsQiI/AAAAAAAAAIM/uEFLVgHPFCk/s160-c/sample_image_28.jpg",
"http://lh4.googleusercontent.com/-z2557Ec1ctY/T3R4x3QA2hI/AAAAAAAAAIk/9-GzPL1lTWE/s160-c/sample_image_29.jpg",
"http://lh5.googleusercontent.com/-LaKXAn4Kr1c/T3R4yc5b4lI/AAAAAAAAAIY/fMgcOVQfmD0/s160-c/sample_image_30.jpg",
"http://lh4.googleusercontent.com/-F9LRToJoQdo/T3R4yrLtyQI/AAAAAAAAAIg/ri9uUCWuRmo/s160-c/sample_image_31.jpg",
"http://lh4.googleusercontent.com/-6X-xBwP-QpI/T3R4zGVboII/AAAAAAAAAIs/zYH4PjjngY0/s160-c/sample_image_32.jpg",
"http://lh5.googleusercontent.com/-VdLRjbW4LAs/T3R4zXu3gUI/AAAAAAAAAIw/9aFp9t7mCPg/s160-c/sample_image_33.jpg",
"http://lh6.googleusercontent.com/-gL6R17_fDJU/T3R4zpIXGjI/AAAAAAAAAI8/Q2Vjx-L9X20/s160-c/sample_image_34.jpg",
"http://lh3.googleusercontent.com/-1fGH4YJXEzo/T3R40Y1B7KI/AAAAAAAAAJE/MnTsa77g-nk/s160-c/sample_image_35.jpg",
"http://lh4.googleusercontent.com/-Ql0jHSrea-A/T3R403mUfFI/AAAAAAAAAJM/qzI4SkcH9tY/s160-c/sample_image_36.jpg",
"http://lh5.googleusercontent.com/-BL5FIBR_tzI/T3R41DA0AKI/AAAAAAAAAJk/GZfeeb-SLM0/s160-c/sample_image_37.jpg",
"http://lh4.googleusercontent.com/-wF2Vc9YDutw/T3R41fR2BCI/AAAAAAAAAJc/JdU1sHdMRAk/s160-c/sample_image_38.jpg",
"http://lh6.googleusercontent.com/-ZWHiPehwjTI/T3R41zuaKCI/AAAAAAAAAJg/hR3QJ1v3REg/s160-c/sample_image_39.jpg",
};
}

View File

@@ -16,32 +16,28 @@
package com.example.android.bitmapfun.ui;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.ActionBar;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.app.NavUtils;
import android.support.v4.view.ViewPager;
import android.util.DisplayMetrics;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.WindowManager.LayoutParams;
import android.widget.Toast;
import com.example.android.bitmapfun.BuildConfig;
import com.example.android.bitmapfun.R;
import com.example.android.bitmapfun.provider.Images;
import com.example.android.bitmapfun.util.DiskLruCache;
import com.example.android.bitmapfun.util.ImageCache;
import com.example.android.bitmapfun.util.ImageFetcher;
import com.example.android.bitmapfun.util.ImageResizer;
import com.example.android.bitmapfun.util.ImageWorker;
import com.example.android.bitmapfun.util.Utils;
public class ImageDetailActivity extends FragmentActivity implements OnClickListener {
@@ -49,51 +45,59 @@ public class ImageDetailActivity extends FragmentActivity implements OnClickList
public static final String EXTRA_IMAGE = "extra_image";
private ImagePagerAdapter mAdapter;
private ImageResizer mImageWorker;
private ImageFetcher mImageFetcher;
private ViewPager mPager;
@SuppressLint("NewApi")
@TargetApi(11)
@Override
public void onCreate(Bundle savedInstanceState) {
if (BuildConfig.DEBUG) {
Utils.enableStrictMode();
}
super.onCreate(savedInstanceState);
setContentView(R.layout.image_detail_pager);
// Fetch screen height and width, to use as our max size when loading images as this
// activity runs full screen
final DisplayMetrics displaymetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
final int height = displaymetrics.heightPixels;
final int width = displaymetrics.widthPixels;
final int longest = height > width ? height : width;
final DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
final int height = displayMetrics.heightPixels;
final int width = displayMetrics.widthPixels;
// The ImageWorker takes care of loading images into our ImageView children asynchronously
mImageWorker = new ImageFetcher(this, longest);
mImageWorker.setAdapter(Images.imageWorkerUrlsAdapter);
mImageWorker.setImageCache(ImageCache.findOrCreateCache(this, IMAGE_CACHE_DIR));
mImageWorker.setImageFadeIn(false);
// For this sample we'll use half of the longest width to resize our images. As the
// image scaling ensures the image is larger than this, we should be left with a
// resolution that is appropriate for both portrait and landscape. For best image quality
// we shouldn't divide by 2, but this will use more memory and require a larger memory
// cache.
final int longest = (height > width ? height : width) / 2;
ImageCache.ImageCacheParams cacheParams =
new ImageCache.ImageCacheParams(this, IMAGE_CACHE_DIR);
cacheParams.setMemCacheSizePercent(this, 0.25f); // Set memory cache to 25% of mem class
// The ImageFetcher takes care of loading images into our ImageView children asynchronously
mImageFetcher = new ImageFetcher(this, longest);
mImageFetcher.addImageCache(getSupportFragmentManager(), cacheParams);
mImageFetcher.setImageFadeIn(false);
// Set up ViewPager and backing adapter
mAdapter = new ImagePagerAdapter(getSupportFragmentManager(),
mImageWorker.getAdapter().getSize());
mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), Images.imageUrls.length);
mPager = (ViewPager) findViewById(R.id.pager);
mPager.setAdapter(mAdapter);
mPager.setPageMargin((int) getResources().getDimension(R.dimen.image_detail_pager_margin));
mPager.setOffscreenPageLimit(2);
// Set up activity to go full screen
getWindow().addFlags(LayoutParams.FLAG_FULLSCREEN);
// Enable some additional newer visibility and ActionBar features to create a more immersive
// photo viewing experience
if (Utils.hasActionBar()) {
// Enable some additional newer visibility and ActionBar features to create a more
// immersive photo viewing experience
if (Utils.hasHoneycomb()) {
final ActionBar actionBar = getActionBar();
// Enable "up" navigation on ActionBar icon and hide title text
actionBar.setDisplayHomeAsUpEnabled(true);
// Hide title text and set home as up
actionBar.setDisplayShowTitleEnabled(false);
// Start low profile mode and hide ActionBar
mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
actionBar.hide();
actionBar.setDisplayHomeAsUpEnabled(true);
// Hide and show the ActionBar as the visibility changes
mPager.setOnSystemUiVisibilityChangeListener(
@@ -107,6 +111,10 @@ public class ImageDetailActivity extends FragmentActivity implements OnClickList
}
}
});
// Start low profile mode and hide ActionBar
mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
actionBar.hide();
}
// Set the current item based on the extra passed in to this activity
@@ -116,23 +124,35 @@ public class ImageDetailActivity extends FragmentActivity implements OnClickList
}
}
@Override
public void onResume() {
super.onResume();
mImageFetcher.setExitTasksEarly(false);
}
@Override
protected void onPause() {
super.onPause();
mImageFetcher.setExitTasksEarly(true);
mImageFetcher.flushCache();
}
@Override
protected void onDestroy() {
super.onDestroy();
mImageFetcher.closeCache();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// Home or "up" navigation
final Intent intent = new Intent(this, ImageGridActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
NavUtils.navigateUpFromSameTask(this);
return true;
case R.id.clear_cache:
final ImageCache cache = mImageWorker.getImageCache();
if (cache != null) {
mImageWorker.getImageCache().clearCaches();
DiskLruCache.clearCache(this, ImageFetcher.HTTP_CACHE_DIR);
Toast.makeText(this, R.string.clear_cache_complete,
Toast.LENGTH_SHORT).show();
}
mImageFetcher.clearCache();
Toast.makeText(
this, R.string.clear_cache_complete_toast,Toast.LENGTH_SHORT).show();
return true;
}
return super.onOptionsItemSelected(item);
@@ -140,18 +160,15 @@ public class ImageDetailActivity extends FragmentActivity implements OnClickList
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.main_menu, menu);
getMenuInflater().inflate(R.menu.main_menu, menu);
return true;
}
/**
* Called by the ViewPager child fragments to load images via the one ImageWorker
*
* @return
* Called by the ViewPager child fragments to load images via the one ImageFetcher
*/
public ImageWorker getImageWorker() {
return mImageWorker;
public ImageFetcher getImageFetcher() {
return mImageFetcher;
}
/**
@@ -174,15 +191,7 @@ public class ImageDetailActivity extends FragmentActivity implements OnClickList
@Override
public Fragment getItem(int position) {
return ImageDetailFragment.newInstance(position);
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
final ImageDetailFragment fragment = (ImageDetailFragment) object;
// As the item gets destroyed we try and cancel any existing work.
fragment.cancelWork();
super.destroyItem(container, position, object);
return ImageDetailFragment.newInstance(Images.imageUrls[position]);
}
}
@@ -190,7 +199,7 @@ public class ImageDetailActivity extends FragmentActivity implements OnClickList
* Set on the ImageView in the ViewPager children fragments, to enable/disable low profile mode
* when the ImageView is touched.
*/
@SuppressLint("NewApi")
@TargetApi(11)
@Override
public void onClick(View v) {
final int vis = mPager.getSystemUiVisibility();

View File

@@ -18,6 +18,7 @@ package com.example.android.bitmapfun.ui;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
@@ -25,6 +26,7 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import com.example.android.bitmapfun.R;
import com.example.android.bitmapfun.util.ImageFetcher;
import com.example.android.bitmapfun.util.ImageWorker;
import com.example.android.bitmapfun.util.Utils;
@@ -32,22 +34,22 @@ import com.example.android.bitmapfun.util.Utils;
* This fragment will populate the children of the ViewPager from {@link ImageDetailActivity}.
*/
public class ImageDetailFragment extends Fragment {
private static final String IMAGE_DATA_EXTRA = "resId";
private int mImageNum;
private static final String IMAGE_DATA_EXTRA = "extra_image_data";
private String mImageUrl;
private ImageView mImageView;
private ImageWorker mImageWorker;
private ImageFetcher mImageFetcher;
/**
* Factory method to generate a new instance of the fragment given an image number.
*
* @param imageNum The image number within the parent adapter to load
* @param imageUrl The image url to load
* @return A new instance of ImageDetailFragment with imageNum extras
*/
public static ImageDetailFragment newInstance(int imageNum) {
public static ImageDetailFragment newInstance(String imageUrl) {
final ImageDetailFragment f = new ImageDetailFragment();
final Bundle args = new Bundle();
args.putInt(IMAGE_DATA_EXTRA, imageNum);
args.putString(IMAGE_DATA_EXTRA, imageUrl);
f.setArguments(args);
return f;
@@ -59,13 +61,13 @@ public class ImageDetailFragment extends Fragment {
public ImageDetailFragment() {}
/**
* Populate image number from extra, use the convenience factory method
* {@link ImageDetailFragment#newInstance(int)} to create this fragment.
* Populate image using a url from extras, use the convenience factory method
* {@link ImageDetailFragment#newInstance(String)} to create this fragment.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
mImageUrl = getArguments() != null ? getArguments().getString(IMAGE_DATA_EXTRA) : null;
}
@Override
@@ -84,23 +86,23 @@ public class ImageDetailFragment extends Fragment {
// Use the parent activity to load the image asynchronously into the ImageView (so a single
// cache can be used over all pages in the ViewPager
if (ImageDetailActivity.class.isInstance(getActivity())) {
mImageWorker = ((ImageDetailActivity) getActivity()).getImageWorker();
mImageWorker.loadImage(mImageNum, mImageView);
mImageFetcher = ((ImageDetailActivity) getActivity()).getImageFetcher();
mImageFetcher.loadImage(mImageUrl, mImageView);
}
// Pass clicks on the ImageView to the parent activity to handle
if (OnClickListener.class.isInstance(getActivity()) && Utils.hasActionBar()) {
if (OnClickListener.class.isInstance(getActivity()) && Utils.hasHoneycomb()) {
mImageView.setOnClickListener((OnClickListener) getActivity());
}
}
/**
* Cancels the asynchronous work taking place on the ImageView, called by the adapter backing
* the ViewPager when the child is destroyed.
*/
public void cancelWork() {
ImageWorker.cancelWork(mImageView);
mImageView.setImageDrawable(null);
mImageView = null;
@Override
public void onDestroy() {
super.onDestroy();
if (mImageView != null) {
// Cancel any pending image work
ImageWorker.cancelWork(mImageView);
mImageView.setImageDrawable(null);
}
}
}

View File

@@ -16,6 +16,9 @@
package com.example.android.bitmapfun.ui;
import com.example.android.bitmapfun.BuildConfig;
import com.example.android.bitmapfun.util.Utils;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentTransaction;
@@ -24,10 +27,13 @@ import android.support.v4.app.FragmentTransaction;
* Simple FragmentActivity to hold the main {@link ImageGridFragment} and not much else.
*/
public class ImageGridActivity extends FragmentActivity {
private static final String TAG = "ImageGridFragment";
private static final String TAG = "ImageGridActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
if (BuildConfig.DEBUG) {
Utils.enableStrictMode();
}
super.onCreate(savedInstanceState);
if (getSupportFragmentManager().findFragmentByTag(TAG) == null) {

View File

@@ -16,6 +16,8 @@
package com.example.android.bitmapfun.ui;
import android.annotation.TargetApi;
import android.app.ActivityOptions;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
@@ -40,11 +42,8 @@ import android.widget.Toast;
import com.example.android.bitmapfun.BuildConfig;
import com.example.android.bitmapfun.R;
import com.example.android.bitmapfun.provider.Images;
import com.example.android.bitmapfun.util.DiskLruCache;
import com.example.android.bitmapfun.util.ImageCache;
import com.example.android.bitmapfun.util.ImageCache.ImageCacheParams;
import com.example.android.bitmapfun.util.ImageFetcher;
import com.example.android.bitmapfun.util.ImageResizer;
import com.example.android.bitmapfun.util.Utils;
/**
@@ -52,7 +51,7 @@ import com.example.android.bitmapfun.util.Utils;
* implementation with the key addition being the ImageWorker class w/ImageCache to load children
* asynchronously, keeping the UI nice and smooth and caching thumbnails for quick retrieval. The
* cache is retained over configuration changes like orientation change so the images are populated
* quickly as the user rotates the device.
* quickly if, for example, the user rotates the device.
*/
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
private static final String TAG = "ImageGridFragment";
@@ -61,7 +60,7 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
private int mImageThumbSize;
private int mImageThumbSpacing;
private ImageAdapter mAdapter;
private ImageResizer mImageWorker;
private ImageFetcher mImageFetcher;
/**
* Empty constructor as per the Fragment documentation
@@ -78,22 +77,15 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
mAdapter = new ImageAdapter(getActivity());
ImageCacheParams cacheParams = new ImageCacheParams(IMAGE_CACHE_DIR);
ImageCacheParams cacheParams = new ImageCacheParams(getActivity(), IMAGE_CACHE_DIR);
// Allocate a third of the per-app memory limit to the bitmap memory cache. This value
// should be chosen carefully based on a number of factors. Refer to the corresponding
// Android Training class for more discussion:
// http://developer.android.com/training/displaying-bitmaps/
// In this case, we aren't using memory for much else other than this activity and the
// ImageDetailActivity so a third lets us keep all our sample image thumbnails in memory
// at once.
cacheParams.memCacheSize = 1024 * 1024 * Utils.getMemoryClass(getActivity()) / 3;
// Set memory cache to 25% of mem class
cacheParams.setMemCacheSizePercent(getActivity(), 0.25f);
// The ImageWorker takes care of loading images into our ImageView children asynchronously
mImageWorker = new ImageFetcher(getActivity(), mImageThumbSize);
mImageWorker.setAdapter(Images.imageThumbWorkerUrlsAdapter);
mImageWorker.setLoadingImage(R.drawable.empty_photo);
mImageWorker.setImageCache(ImageCache.findOrCreateCache(getActivity(), cacheParams));
// The ImageFetcher takes care of loading images into our ImageView children asynchronously
mImageFetcher = new ImageFetcher(getActivity(), mImageThumbSize);
mImageFetcher.setLoadingImage(R.drawable.empty_photo);
mImageFetcher.addImageCache(getActivity().getSupportFragmentManager(), cacheParams);
}
@Override
@@ -104,6 +96,22 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
mGridView.setAdapter(mAdapter);
mGridView.setOnItemClickListener(this);
mGridView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView absListView, int scrollState) {
// Pause fetcher to ensure smoother scrolling when flinging
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
mImageFetcher.setPauseWork(true);
} else {
mImageFetcher.setPauseWork(false);
}
}
@Override
public void onScroll(AbsListView absListView, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
}
});
// This listener is used to get the final width of the GridView and then calculate the
// number of columns and the width of each column. The width of each column is variable
@@ -135,21 +143,38 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
@Override
public void onResume() {
super.onResume();
mImageWorker.setExitTasksEarly(false);
mImageFetcher.setExitTasksEarly(false);
mAdapter.notifyDataSetChanged();
}
@Override
public void onPause() {
super.onPause();
mImageWorker.setExitTasksEarly(true);
mImageFetcher.setExitTasksEarly(true);
mImageFetcher.flushCache();
}
@Override
public void onDestroy() {
super.onDestroy();
mImageFetcher.closeCache();
}
@TargetApi(16)
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
i.putExtra(ImageDetailActivity.EXTRA_IMAGE, (int) id);
startActivity(i);
if (Utils.hasJellyBean()) {
// makeThumbnailScaleUpAnimation() looks kind of ugly here as the loading spinner may
// show plus the thumbnail image in GridView is cropped. so using
// makeScaleUpAnimation() instead.
ActivityOptions options =
ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.getWidth(), v.getHeight());
getActivity().startActivity(i, options.toBundle());
} else {
startActivity(i);
}
}
@Override
@@ -161,13 +186,9 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.clear_cache:
final ImageCache cache = mImageWorker.getImageCache();
if (cache != null) {
mImageWorker.getImageCache().clearCaches();
DiskLruCache.clearCache(getActivity(), ImageFetcher.HTTP_CACHE_DIR);
Toast.makeText(getActivity(), R.string.clear_cache_complete,
Toast.LENGTH_SHORT).show();
}
mImageFetcher.clearCache();
Toast.makeText(getActivity(), R.string.clear_cache_complete_toast,
Toast.LENGTH_SHORT).show();
return true;
}
return super.onOptionsItemSelected(item);
@@ -183,7 +204,7 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
private final Context mContext;
private int mItemHeight = 0;
private int mNumColumns = 0;
private int mActionBarHeight = -1;
private int mActionBarHeight = 0;
private GridView.LayoutParams mImageViewLayoutParams;
public ImageAdapter(Context context) {
@@ -191,18 +212,25 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
mContext = context;
mImageViewLayoutParams = new GridView.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
// Calculate ActionBar height
TypedValue tv = new TypedValue();
if (context.getTheme().resolveAttribute(
android.R.attr.actionBarSize, tv, true)) {
mActionBarHeight = TypedValue.complexToDimensionPixelSize(
tv.data, context.getResources().getDisplayMetrics());
}
}
@Override
public int getCount() {
// Size of adapter + number of columns for top empty row
return mImageWorker.getAdapter().getSize() + mNumColumns;
// Size + number of columns for top empty row
return Images.imageThumbUrls.length + mNumColumns;
}
@Override
public Object getItem(int position) {
return position < mNumColumns ?
null : mImageWorker.getAdapter().getItem(position - mNumColumns);
null : Images.imageThumbUrls[position - mNumColumns];
}
@Override
@@ -233,18 +261,6 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
if (convertView == null) {
convertView = new View(mContext);
}
// Calculate ActionBar height
if (mActionBarHeight < 0) {
TypedValue tv = new TypedValue();
if (mContext.getTheme().resolveAttribute(
android.R.attr.actionBarSize, tv, true)) {
mActionBarHeight = TypedValue.complexToDimensionPixelSize(
tv.data, mContext.getResources().getDisplayMetrics());
} else {
// No ActionBar style (pre-Honeycomb or ActionBar not in theme)
mActionBarHeight = 0;
}
}
// Set empty view with height of ActionBar
convertView.setLayoutParams(new AbsListView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, mActionBarHeight));
@@ -268,7 +284,7 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
// Finally load the image asynchronously into the ImageView, this also takes care of
// setting a placeholder image while the background thread runs
mImageWorker.loadImage(position - mNumColumns, imageView);
mImageFetcher.loadImage(Images.imageThumbUrls[position - mNumColumns], imageView);
return imageView;
}
@@ -285,7 +301,7 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
mItemHeight = height;
mImageViewLayoutParams =
new GridView.LayoutParams(LayoutParams.MATCH_PARENT, mItemHeight);
mImageWorker.setImageSize(height);
mImageFetcher.setImageSize(height);
notifyDataSetChanged();
}

View File

@@ -0,0 +1,693 @@
/*
* Copyright (C) 2008 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.bitmapfun.util;
import android.annotation.TargetApi;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
import java.util.ArrayDeque;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* *************************************
* Copied from JB release framework:
* https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/os/AsyncTask.java
*
* so that threading behavior on all OS versions is the same and we can tweak behavior by using
* executeOnExecutor() if needed.
*
* There are 3 changes in this copy of AsyncTask:
* -pre-HC a single thread executor is used for serial operation
* (Executors.newSingleThreadExecutor) and is the default
* -the default THREAD_POOL_EXECUTOR was changed to use DiscardOldestPolicy
* -a new fixed thread pool called DUAL_THREAD_EXECUTOR was added
* *************************************
*
* <p>AsyncTask enables proper and easy use of the UI thread. This class allows to
* perform background operations and publish results on the UI thread without
* having to manipulate threads and/or handlers.</p>
*
* <p>AsyncTask is designed to be a helper class around {@link Thread} and {@link Handler}
* and does not constitute a generic threading framework. AsyncTasks should ideally be
* used for short operations (a few seconds at the most.) If you need to keep threads
* running for long periods of time, it is highly recommended you use the various APIs
* provided by the <code>java.util.concurrent</code> pacakge such as {@link Executor},
* {@link ThreadPoolExecutor} and {@link FutureTask}.</p>
*
* <p>An asynchronous task is defined by a computation that runs on a background thread and
* whose result is published on the UI thread. An asynchronous task is defined by 3 generic
* types, called <code>Params</code>, <code>Progress</code> and <code>Result</code>,
* and 4 steps, called <code>onPreExecute</code>, <code>doInBackground</code>,
* <code>onProgressUpdate</code> and <code>onPostExecute</code>.</p>
*
* <div class="special reference">
* <h3>Developer Guides</h3>
* <p>For more information about using tasks and threads, read the
* <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html">Processes and
* Threads</a> developer guide.</p>
* </div>
*
* <h2>Usage</h2>
* <p>AsyncTask must be subclassed to be used. The subclass will override at least
* one method ({@link #doInBackground}), and most often will override a
* second one ({@link #onPostExecute}.)</p>
*
* <p>Here is an example of subclassing:</p>
* <pre class="prettyprint">
* private class DownloadFilesTask extends AsyncTask&lt;URL, Integer, Long&gt; {
* protected Long doInBackground(URL... urls) {
* int count = urls.length;
* long totalSize = 0;
* for (int i = 0; i < count; i++) {
* totalSize += Downloader.downloadFile(urls[i]);
* publishProgress((int) ((i / (float) count) * 100));
* // Escape early if cancel() is called
* if (isCancelled()) break;
* }
* return totalSize;
* }
*
* protected void onProgressUpdate(Integer... progress) {
* setProgressPercent(progress[0]);
* }
*
* protected void onPostExecute(Long result) {
* showDialog("Downloaded " + result + " bytes");
* }
* }
* </pre>
*
* <p>Once created, a task is executed very simply:</p>
* <pre class="prettyprint">
* new DownloadFilesTask().execute(url1, url2, url3);
* </pre>
*
* <h2>AsyncTask's generic types</h2>
* <p>The three types used by an asynchronous task are the following:</p>
* <ol>
* <li><code>Params</code>, the type of the parameters sent to the task upon
* execution.</li>
* <li><code>Progress</code>, the type of the progress units published during
* the background computation.</li>
* <li><code>Result</code>, the type of the result of the background
* computation.</li>
* </ol>
* <p>Not all types are always used by an asynchronous task. To mark a type as unused,
* simply use the type {@link Void}:</p>
* <pre>
* private class MyTask extends AsyncTask&lt;Void, Void, Void&gt; { ... }
* </pre>
*
* <h2>The 4 steps</h2>
* <p>When an asynchronous task is executed, the task goes through 4 steps:</p>
* <ol>
* <li>{@link #onPreExecute()}, invoked on the UI thread immediately after the task
* is executed. This step is normally used to setup the task, for instance by
* showing a progress bar in the user interface.</li>
* <li>{@link #doInBackground}, invoked on the background thread
* immediately after {@link #onPreExecute()} finishes executing. This step is used
* to perform background computation that can take a long time. The parameters
* of the asynchronous task are passed to this step. The result of the computation must
* be returned by this step and will be passed back to the last step. This step
* can also use {@link #publishProgress} to publish one or more units
* of progress. These values are published on the UI thread, in the
* {@link #onProgressUpdate} step.</li>
* <li>{@link #onProgressUpdate}, invoked on the UI thread after a
* call to {@link #publishProgress}. The timing of the execution is
* undefined. This method is used to display any form of progress in the user
* interface while the background computation is still executing. For instance,
* it can be used to animate a progress bar or show logs in a text field.</li>
* <li>{@link #onPostExecute}, invoked on the UI thread after the background
* computation finishes. The result of the background computation is passed to
* this step as a parameter.</li>
* </ol>
*
* <h2>Cancelling a task</h2>
* <p>A task can be cancelled at any time by invoking {@link #cancel(boolean)}. Invoking
* this method will cause subsequent calls to {@link #isCancelled()} to return true.
* After invoking this method, {@link #onCancelled(Object)}, instead of
* {@link #onPostExecute(Object)} will be invoked after {@link #doInBackground(Object[])}
* returns. To ensure that a task is cancelled as quickly as possible, you should always
* check the return value of {@link #isCancelled()} periodically from
* {@link #doInBackground(Object[])}, if possible (inside a loop for instance.)</p>
*
* <h2>Threading rules</h2>
* <p>There are a few threading rules that must be followed for this class to
* work properly:</p>
* <ul>
* <li>The AsyncTask class must be loaded on the UI thread. This is done
* automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.</li>
* <li>The task instance must be created on the UI thread.</li>
* <li>{@link #execute} must be invoked on the UI thread.</li>
* <li>Do not call {@link #onPreExecute()}, {@link #onPostExecute},
* {@link #doInBackground}, {@link #onProgressUpdate} manually.</li>
* <li>The task can be executed only once (an exception will be thrown if
* a second execution is attempted.)</li>
* </ul>
*
* <h2>Memory observability</h2>
* <p>AsyncTask guarantees that all callback calls are synchronized in such a way that the following
* operations are safe without explicit synchronizations.</p>
* <ul>
* <li>Set member fields in the constructor or {@link #onPreExecute}, and refer to them
* in {@link #doInBackground}.
* <li>Set member fields in {@link #doInBackground}, and refer to them in
* {@link #onProgressUpdate} and {@link #onPostExecute}.
* </ul>
*
* <h2>Order of execution</h2>
* <p>When first introduced, AsyncTasks were executed serially on a single background
* thread. Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed
* to a pool of threads allowing multiple tasks to operate in parallel. Starting with
* {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are executed on a single
* thread to avoid common application errors caused by parallel execution.</p>
* <p>If you truly want parallel execution, you can invoke
* {@link #executeOnExecutor(java.util.concurrent.Executor, Object[])} with
* {@link #THREAD_POOL_EXECUTOR}.</p>
*/
public abstract class AsyncTask<Params, Progress, Result> {
private static final String LOG_TAG = "AsyncTask";
private static final int CORE_POOL_SIZE = 5;
private static final int MAXIMUM_POOL_SIZE = 128;
private static final int KEEP_ALIVE = 1;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
}
};
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(10);
/**
* An {@link Executor} that can be used to execute tasks in parallel.
*/
public static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory,
new ThreadPoolExecutor.DiscardOldestPolicy());
/**
* An {@link Executor} that executes tasks one at a time in serial
* order. This serialization is global to a particular process.
*/
public static final Executor SERIAL_EXECUTOR = Utils.hasHoneycomb() ? new SerialExecutor() :
Executors.newSingleThreadExecutor(sThreadFactory);
public static final Executor DUAL_THREAD_EXECUTOR =
Executors.newFixedThreadPool(2, sThreadFactory);
private static final int MESSAGE_POST_RESULT = 0x1;
private static final int MESSAGE_POST_PROGRESS = 0x2;
private static final InternalHandler sHandler = new InternalHandler();
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
private final WorkerRunnable<Params, Result> mWorker;
private final FutureTask<Result> mFuture;
private volatile Status mStatus = Status.PENDING;
private final AtomicBoolean mCancelled = new AtomicBoolean();
private final AtomicBoolean mTaskInvoked = new AtomicBoolean();
@TargetApi(11)
private static class SerialExecutor implements Executor {
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;
public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
scheduleNext();
}
}
protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}
/**
* Indicates the current status of the task. Each status will be set only once
* during the lifetime of a task.
*/
public enum Status {
/**
* Indicates that the task has not been executed yet.
*/
PENDING,
/**
* Indicates that the task is running.
*/
RUNNING,
/**
* Indicates that {@link AsyncTask#onPostExecute} has finished.
*/
FINISHED,
}
/** @hide Used to force static handler to be created. */
public static void init() {
sHandler.getLooper();
}
/** @hide */
public static void setDefaultExecutor(Executor exec) {
sDefaultExecutor = exec;
}
/**
* Creates a new asynchronous task. This constructor must be invoked on the UI thread.
*/
public AsyncTask() {
mWorker = new WorkerRunnable<Params, Result>() {
public Result call() throws Exception {
mTaskInvoked.set(true);
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
return postResult(doInBackground(mParams));
}
};
mFuture = new FutureTask<Result>(mWorker) {
@Override
protected void done() {
try {
postResultIfNotInvoked(get());
} catch (InterruptedException e) {
android.util.Log.w(LOG_TAG, e);
} catch (ExecutionException e) {
throw new RuntimeException("An error occured while executing doInBackground()",
e.getCause());
} catch (CancellationException e) {
postResultIfNotInvoked(null);
}
}
};
}
private void postResultIfNotInvoked(Result result) {
final boolean wasTaskInvoked = mTaskInvoked.get();
if (!wasTaskInvoked) {
postResult(result);
}
}
private Result postResult(Result result) {
@SuppressWarnings("unchecked")
Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
new AsyncTaskResult<Result>(this, result));
message.sendToTarget();
return result;
}
/**
* Returns the current status of this task.
*
* @return The current status.
*/
public final Status getStatus() {
return mStatus;
}
/**
* Override this method to perform a computation on a background thread. The
* specified parameters are the parameters passed to {@link #execute}
* by the caller of this task.
*
* This method can call {@link #publishProgress} to publish updates
* on the UI thread.
*
* @param params The parameters of the task.
*
* @return A result, defined by the subclass of this task.
*
* @see #onPreExecute()
* @see #onPostExecute
* @see #publishProgress
*/
protected abstract Result doInBackground(Params... params);
/**
* Runs on the UI thread before {@link #doInBackground}.
*
* @see #onPostExecute
* @see #doInBackground
*/
protected void onPreExecute() {
}
/**
* <p>Runs on the UI thread after {@link #doInBackground}. The
* specified result is the value returned by {@link #doInBackground}.</p>
*
* <p>This method won't be invoked if the task was cancelled.</p>
*
* @param result The result of the operation computed by {@link #doInBackground}.
*
* @see #onPreExecute
* @see #doInBackground
* @see #onCancelled(Object)
*/
@SuppressWarnings({"UnusedDeclaration"})
protected void onPostExecute(Result result) {
}
/**
* Runs on the UI thread after {@link #publishProgress} is invoked.
* The specified values are the values passed to {@link #publishProgress}.
*
* @param values The values indicating progress.
*
* @see #publishProgress
* @see #doInBackground
*/
@SuppressWarnings({"UnusedDeclaration"})
protected void onProgressUpdate(Progress... values) {
}
/**
* <p>Runs on the UI thread after {@link #cancel(boolean)} is invoked and
* {@link #doInBackground(Object[])} has finished.</p>
*
* <p>The default implementation simply invokes {@link #onCancelled()} and
* ignores the result. If you write your own implementation, do not call
* <code>super.onCancelled(result)</code>.</p>
*
* @param result The result, if any, computed in
* {@link #doInBackground(Object[])}, can be null
*
* @see #cancel(boolean)
* @see #isCancelled()
*/
@SuppressWarnings({"UnusedParameters"})
protected void onCancelled(Result result) {
onCancelled();
}
/**
* <p>Applications should preferably override {@link #onCancelled(Object)}.
* This method is invoked by the default implementation of
* {@link #onCancelled(Object)}.</p>
*
* <p>Runs on the UI thread after {@link #cancel(boolean)} is invoked and
* {@link #doInBackground(Object[])} has finished.</p>
*
* @see #onCancelled(Object)
* @see #cancel(boolean)
* @see #isCancelled()
*/
protected void onCancelled() {
}
/**
* Returns <tt>true</tt> if this task was cancelled before it completed
* normally. If you are calling {@link #cancel(boolean)} on the task,
* the value returned by this method should be checked periodically from
* {@link #doInBackground(Object[])} to end the task as soon as possible.
*
* @return <tt>true</tt> if task was cancelled before it completed
*
* @see #cancel(boolean)
*/
public final boolean isCancelled() {
return mCancelled.get();
}
/**
* <p>Attempts to cancel execution of this task. This attempt will
* fail if the task has already completed, already been cancelled,
* or could not be cancelled for some other reason. If successful,
* and this task has not started when <tt>cancel</tt> is called,
* this task should never run. If the task has already started,
* then the <tt>mayInterruptIfRunning</tt> parameter determines
* whether the thread executing this task should be interrupted in
* an attempt to stop the task.</p>
*
* <p>Calling this method will result in {@link #onCancelled(Object)} being
* invoked on the UI thread after {@link #doInBackground(Object[])}
* returns. Calling this method guarantees that {@link #onPostExecute(Object)}
* is never invoked. After invoking this method, you should check the
* value returned by {@link #isCancelled()} periodically from
* {@link #doInBackground(Object[])} to finish the task as early as
* possible.</p>
*
* @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
* task should be interrupted; otherwise, in-progress tasks are allowed
* to complete.
*
* @return <tt>false</tt> if the task could not be cancelled,
* typically because it has already completed normally;
* <tt>true</tt> otherwise
*
* @see #isCancelled()
* @see #onCancelled(Object)
*/
public final boolean cancel(boolean mayInterruptIfRunning) {
mCancelled.set(true);
return mFuture.cancel(mayInterruptIfRunning);
}
/**
* Waits if necessary for the computation to complete, and then
* retrieves its result.
*
* @return The computed result.
*
* @throws CancellationException If the computation was cancelled.
* @throws ExecutionException If the computation threw an exception.
* @throws InterruptedException If the current thread was interrupted
* while waiting.
*/
public final Result get() throws InterruptedException, ExecutionException {
return mFuture.get();
}
/**
* Waits if necessary for at most the given time for the computation
* to complete, and then retrieves its result.
*
* @param timeout Time to wait before cancelling the operation.
* @param unit The time unit for the timeout.
*
* @return The computed result.
*
* @throws CancellationException If the computation was cancelled.
* @throws ExecutionException If the computation threw an exception.
* @throws InterruptedException If the current thread was interrupted
* while waiting.
* @throws TimeoutException If the wait timed out.
*/
public final Result get(long timeout, TimeUnit unit) throws InterruptedException,
ExecutionException, TimeoutException {
return mFuture.get(timeout, unit);
}
/**
* Executes the task with the specified parameters. The task returns
* itself (this) so that the caller can keep a reference to it.
*
* <p>Note: this function schedules the task on a queue for a single background
* thread or pool of threads depending on the platform version. When first
* introduced, AsyncTasks were executed serially on a single background thread.
* Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed
* to a pool of threads allowing multiple tasks to operate in parallel. Starting
* {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are back to being
* executed on a single thread to avoid common application errors caused
* by parallel execution. If you truly want parallel execution, you can use
* the {@link #executeOnExecutor} version of this method
* with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings
* on its use.
*
* <p>This method must be invoked on the UI thread.
*
* @param params The parameters of the task.
*
* @return This instance of AsyncTask.
*
* @throws IllegalStateException If {@link #getStatus()} returns either
* {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
*
* @see #executeOnExecutor(java.util.concurrent.Executor, Object[])
* @see #execute(Runnable)
*/
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}
/**
* Executes the task with the specified parameters. The task returns
* itself (this) so that the caller can keep a reference to it.
*
* <p>This method is typically used with {@link #THREAD_POOL_EXECUTOR} to
* allow multiple tasks to run in parallel on a pool of threads managed by
* AsyncTask, however you can also use your own {@link Executor} for custom
* behavior.
*
* <p><em>Warning:</em> Allowing multiple tasks to run in parallel from
* a thread pool is generally <em>not</em> what one wants, because the order
* of their operation is not defined. For example, if these tasks are used
* to modify any state in common (such as writing a file due to a button click),
* there are no guarantees on the order of the modifications.
* Without careful work it is possible in rare cases for the newer version
* of the data to be over-written by an older one, leading to obscure data
* loss and stability issues. Such changes are best
* executed in serial; to guarantee such work is serialized regardless of
* platform version you can use this function with {@link #SERIAL_EXECUTOR}.
*
* <p>This method must be invoked on the UI thread.
*
* @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a
* convenient process-wide thread pool for tasks that are loosely coupled.
* @param params The parameters of the task.
*
* @return This instance of AsyncTask.
*
* @throws IllegalStateException If {@link #getStatus()} returns either
* {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
*
* @see #execute(Object[])
*/
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
Params... params) {
if (mStatus != Status.PENDING) {
switch (mStatus) {
case RUNNING:
throw new IllegalStateException("Cannot execute task:"
+ " the task is already running.");
case FINISHED:
throw new IllegalStateException("Cannot execute task:"
+ " the task has already been executed "
+ "(a task can be executed only once)");
}
}
mStatus = Status.RUNNING;
onPreExecute();
mWorker.mParams = params;
exec.execute(mFuture);
return this;
}
/**
* Convenience version of {@link #execute(Object...)} for use with
* a simple Runnable object. See {@link #execute(Object[])} for more
* information on the order of execution.
*
* @see #execute(Object[])
* @see #executeOnExecutor(java.util.concurrent.Executor, Object[])
*/
public static void execute(Runnable runnable) {
sDefaultExecutor.execute(runnable);
}
/**
* This method can be invoked from {@link #doInBackground} to
* publish updates on the UI thread while the background computation is
* still running. Each call to this method will trigger the execution of
* {@link #onProgressUpdate} on the UI thread.
*
* {@link #onProgressUpdate} will note be called if the task has been
* canceled.
*
* @param values The progress values to update the UI with.
*
* @see #onProgressUpdate
* @see #doInBackground
*/
protected final void publishProgress(Progress... values) {
if (!isCancelled()) {
sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
new AsyncTaskResult<Progress>(this, values)).sendToTarget();
}
}
private void finish(Result result) {
if (isCancelled()) {
onCancelled(result);
} else {
onPostExecute(result);
}
mStatus = Status.FINISHED;
}
private static class InternalHandler extends Handler {
@SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
@Override
public void handleMessage(Message msg) {
AsyncTaskResult result = (AsyncTaskResult) msg.obj;
switch (msg.what) {
case MESSAGE_POST_RESULT:
// There is only one result
result.mTask.finish(result.mData[0]);
break;
case MESSAGE_POST_PROGRESS:
result.mTask.onProgressUpdate(result.mData);
break;
}
}
}
private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
Params[] mParams;
}
@SuppressWarnings({"RawUseOfParameterizedType"})
private static class AsyncTaskResult<Data> {
final AsyncTask mTask;
final Data[] mData;
AsyncTaskResult(AsyncTask task, Data... data) {
mTask = task;
mData = data;
}
}
}

View File

@@ -16,16 +16,28 @@
package com.example.android.bitmapfun.util;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.support.v4.app.FragmentActivity;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Environment;
import android.os.StatFs;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.util.LruCache;
import android.util.Log;
import com.example.android.bitmapfun.BuildConfig;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* This class holds our bitmap caches (memory and disk).
@@ -42,23 +54,27 @@ public class ImageCache {
// Compression settings when writing images to disk cache
private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG;
private static final int DEFAULT_COMPRESS_QUALITY = 70;
private static final int DISK_CACHE_INDEX = 0;
// Constants to easily toggle various caches
private static final boolean DEFAULT_MEM_CACHE_ENABLED = true;
private static final boolean DEFAULT_DISK_CACHE_ENABLED = true;
private static final boolean DEFAULT_CLEAR_DISK_CACHE_ON_START = false;
private static final boolean DEFAULT_INIT_DISK_CACHE_ON_CREATE = false;
private DiskLruCache mDiskCache;
private DiskLruCache mDiskLruCache;
private LruCache<String, Bitmap> mMemoryCache;
private ImageCacheParams mCacheParams;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
/**
* Creating a new ImageCache object using the specified parameters.
*
* @param context The context to use
* @param cacheParams The cache parameters to use to initialize the cache
*/
public ImageCache(Context context, ImageCacheParams cacheParams) {
init(context, cacheParams);
public ImageCache(ImageCacheParams cacheParams) {
init(cacheParams);
}
/**
@@ -68,43 +84,29 @@ public class ImageCache {
* @param uniqueName A unique name that will be appended to the cache directory
*/
public ImageCache(Context context, String uniqueName) {
init(context, new ImageCacheParams(uniqueName));
}
/**
* Find and return an existing ImageCache stored in a {@link RetainFragment}, if not found a new
* one is created with defaults and saved to a {@link RetainFragment}.
*
* @param activity The calling {@link FragmentActivity}
* @param uniqueName A unique name to append to the cache directory
* @return An existing retained ImageCache object or a new one if one did not exist.
*/
public static ImageCache findOrCreateCache(
final FragmentActivity activity, final String uniqueName) {
return findOrCreateCache(activity, new ImageCacheParams(uniqueName));
init(new ImageCacheParams(context, uniqueName));
}
/**
* Find and return an existing ImageCache stored in a {@link RetainFragment}, if not found a new
* one is created using the supplied params and saved to a {@link RetainFragment}.
*
* @param activity The calling {@link FragmentActivity}
* @param fragmentManager The fragment manager to use when dealing with the retained fragment.
* @param cacheParams The cache parameters to use if creating the ImageCache
* @return An existing retained ImageCache object or a new one if one did not exist
*/
public static ImageCache findOrCreateCache(
final FragmentActivity activity, ImageCacheParams cacheParams) {
FragmentManager fragmentManager, ImageCacheParams cacheParams) {
// Search for, or create an instance of the non-UI RetainFragment
final RetainFragment mRetainFragment = RetainFragment.findOrCreateRetainFragment(
activity.getSupportFragmentManager());
final RetainFragment mRetainFragment = findOrCreateRetainFragment(fragmentManager);
// See if we already have an ImageCache stored in RetainFragment
ImageCache imageCache = (ImageCache) mRetainFragment.getObject();
// No existing ImageCache, create one and store it in RetainFragment
if (imageCache == null) {
imageCache = new ImageCache(activity, cacheParams);
imageCache = new ImageCache(cacheParams);
mRetainFragment.setObject(imageCache);
}
@@ -114,36 +116,75 @@ public class ImageCache {
/**
* Initialize the cache, providing all parameters.
*
* @param context The context to use
* @param cacheParams The cache parameters to initialize the cache
*/
private void init(Context context, ImageCacheParams cacheParams) {
final File diskCacheDir = DiskLruCache.getDiskCacheDir(context, cacheParams.uniqueName);
// Set up disk cache
if (cacheParams.diskCacheEnabled) {
mDiskCache = DiskLruCache.openCache(context, diskCacheDir, cacheParams.diskCacheSize);
mDiskCache.setCompressParams(cacheParams.compressFormat, cacheParams.compressQuality);
if (cacheParams.clearDiskCacheOnStart) {
mDiskCache.clearCache();
}
}
private void init(ImageCacheParams cacheParams) {
mCacheParams = cacheParams;
// Set up memory cache
if (cacheParams.memoryCacheEnabled) {
mMemoryCache = new LruCache<String, Bitmap>(cacheParams.memCacheSize) {
if (mCacheParams.memoryCacheEnabled) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Memory cache created (size = " + mCacheParams.memCacheSize + ")");
}
mMemoryCache = new LruCache<String, Bitmap>(mCacheParams.memCacheSize) {
/**
* Measure item size in bytes rather than units which is more practical for a bitmap
* cache
* Measure item size in bytes rather than units which is more practical
* for a bitmap cache
*/
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return Utils.getBitmapSize(bitmap);
return getBitmapSize(bitmap);
}
};
}
// By default the disk cache is not initialized here as it should be initialized
// on a separate thread due to disk access.
if (cacheParams.initDiskCacheOnCreate) {
// Set up disk cache
initDiskCache();
}
}
/**
* Initializes the disk cache. Note that this includes disk access so this should not be
* executed on the main/UI thread. By default an ImageCache does not initialize the disk
* cache when it is created, instead you should call initDiskCache() to initialize it on a
* background thread.
*/
public void initDiskCache() {
// Set up disk cache
synchronized (mDiskCacheLock) {
if (mDiskLruCache == null || mDiskLruCache.isClosed()) {
File diskCacheDir = mCacheParams.diskCacheDir;
if (mCacheParams.diskCacheEnabled && diskCacheDir != null) {
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > mCacheParams.diskCacheSize) {
try {
mDiskLruCache = DiskLruCache.open(
diskCacheDir, 1, 1, mCacheParams.diskCacheSize);
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache initialized");
}
} catch (final IOException e) {
mCacheParams.diskCacheDir = null;
Log.e(TAG, "initDiskCache - " + e);
}
}
}
}
mDiskCacheStarting = false;
mDiskCacheLock.notifyAll();
}
}
/**
* Adds a bitmap to both memory and disk cache.
* @param data Unique identifier for the bitmap to store
* @param bitmap The bitmap to store
*/
public void addBitmapToCache(String data, Bitmap bitmap) {
if (data == null || bitmap == null) {
return;
@@ -154,9 +195,37 @@ public class ImageCache {
mMemoryCache.put(data, bitmap);
}
// Add to disk cache
if (mDiskCache != null && !mDiskCache.containsKey(data)) {
mDiskCache.put(data, bitmap);
synchronized (mDiskCacheLock) {
// Add to disk cache
if (mDiskLruCache != null) {
final String key = hashKeyForDisk(data);
OutputStream out = null;
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot == null) {
final DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
out = editor.newOutputStream(DISK_CACHE_INDEX);
bitmap.compress(
mCacheParams.compressFormat, mCacheParams.compressQuality, out);
editor.commit();
out.close();
}
} else {
snapshot.getInputStream(DISK_CACHE_INDEX).close();
}
} catch (final IOException e) {
Log.e(TAG, "addBitmapToCache - " + e);
} catch (Exception e) {
Log.e(TAG, "addBitmapToCache - " + e);
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {}
}
}
}
}
@@ -186,32 +255,324 @@ public class ImageCache {
* @return The bitmap if found in cache, null otherwise
*/
public Bitmap getBitmapFromDiskCache(String data) {
if (mDiskCache != null) {
return mDiskCache.get(data);
final String key = hashKeyForDisk(data);
synchronized (mDiskCacheLock) {
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
InputStream inputStream = null;
try {
final DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache hit");
}
inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
if (inputStream != null) {
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
return bitmap;
}
}
} catch (final IOException e) {
Log.e(TAG, "getBitmapFromDiskCache - " + e);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {}
}
}
return null;
}
return null;
}
public void clearCaches() {
mDiskCache.clearCache();
mMemoryCache.evictAll();
/**
* Clears both the memory and disk cache associated with this ImageCache object. Note that
* this includes disk access so this should not be executed on the main/UI thread.
*/
public void clearCache() {
if (mMemoryCache != null) {
mMemoryCache.evictAll();
if (BuildConfig.DEBUG) {
Log.d(TAG, "Memory cache cleared");
}
}
synchronized (mDiskCacheLock) {
mDiskCacheStarting = true;
if (mDiskLruCache != null && !mDiskLruCache.isClosed()) {
try {
mDiskLruCache.delete();
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache cleared");
}
} catch (IOException e) {
Log.e(TAG, "clearCache - " + e);
}
mDiskLruCache = null;
initDiskCache();
}
}
}
/**
* Flushes the disk cache associated with this ImageCache object. Note that this includes
* disk access so this should not be executed on the main/UI thread.
*/
public void flush() {
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null) {
try {
mDiskLruCache.flush();
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache flushed");
}
} catch (IOException e) {
Log.e(TAG, "flush - " + e);
}
}
}
}
/**
* Closes the disk cache associated with this ImageCache object. Note that this includes
* disk access so this should not be executed on the main/UI thread.
*/
public void close() {
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null) {
try {
if (!mDiskLruCache.isClosed()) {
mDiskLruCache.close();
mDiskLruCache = null;
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache closed");
}
}
} catch (IOException e) {
Log.e(TAG, "close - " + e);
}
}
}
}
/**
* A holder class that contains cache parameters.
*/
public static class ImageCacheParams {
public String uniqueName;
public int memCacheSize = DEFAULT_MEM_CACHE_SIZE;
public int diskCacheSize = DEFAULT_DISK_CACHE_SIZE;
public File diskCacheDir;
public CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT;
public int compressQuality = DEFAULT_COMPRESS_QUALITY;
public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED;
public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED;
public boolean clearDiskCacheOnStart = DEFAULT_CLEAR_DISK_CACHE_ON_START;
public boolean initDiskCacheOnCreate = DEFAULT_INIT_DISK_CACHE_ON_CREATE;
public ImageCacheParams(String uniqueName) {
this.uniqueName = uniqueName;
public ImageCacheParams(Context context, String uniqueName) {
diskCacheDir = getDiskCacheDir(context, uniqueName);
}
public ImageCacheParams(File diskCacheDir) {
this.diskCacheDir = diskCacheDir;
}
/**
* Sets the memory cache size based on a percentage of the device memory class.
* Eg. setting percent to 0.2 would set the memory cache to one fifth of the device memory
* class. Throws {@link IllegalArgumentException} if percent is < 0.05 or > .8.
*
* This value should be chosen carefully based on a number of factors
* Refer to the corresponding Android Training class for more discussion:
* http://developer.android.com/training/displaying-bitmaps/
*
* @param context Context to use to fetch memory class
* @param percent Percent of memory class to use to size memory cache
*/
public void setMemCacheSizePercent(Context context, float percent) {
if (percent < 0.05f || percent > 0.8f) {
throw new IllegalArgumentException("setMemCacheSizePercent - percent must be "
+ "between 0.05 and 0.8 (inclusive)");
}
memCacheSize = Math.round(percent * getMemoryClass(context) * 1024 * 1024);
}
private static int getMemoryClass(Context context) {
return ((ActivityManager) context.getSystemService(
Context.ACTIVITY_SERVICE)).getMemoryClass();
}
}
/**
* Get a usable cache directory (external if available, internal otherwise).
*
* @param context The context to use
* @param uniqueName A unique directory name to append to the cache dir
* @return The cache dir
*/
public static File getDiskCacheDir(Context context, String uniqueName) {
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
// otherwise use internal cache dir
final String cachePath =
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}
/**
* A hashing method that changes a string (like a URL) into a hash suitable for using as a
* disk filename.
*/
public static String hashKeyForDisk(String key) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
private static String bytesToHexString(byte[] bytes) {
// http://stackoverflow.com/questions/332079
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
/**
* Get the size in bytes of a bitmap.
* @param bitmap
* @return size in bytes
*/
@TargetApi(12)
public static int getBitmapSize(Bitmap bitmap) {
if (Utils.hasHoneycombMR1()) {
return bitmap.getByteCount();
}
// Pre HC-MR1
return bitmap.getRowBytes() * bitmap.getHeight();
}
/**
* Check if external storage is built-in or removable.
*
* @return True if external storage is removable (like an SD card), false
* otherwise.
*/
@TargetApi(9)
public static boolean isExternalStorageRemovable() {
if (Utils.hasGingerbread()) {
return Environment.isExternalStorageRemovable();
}
return true;
}
/**
* Get the external app cache directory.
*
* @param context The context to use
* @return The external cache dir
*/
@TargetApi(8)
public static File getExternalCacheDir(Context context) {
if (Utils.hasFroyo()) {
return context.getExternalCacheDir();
}
// Before Froyo we need to construct the external cache dir ourselves
final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/";
return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir);
}
/**
* Check how much usable space is available at a given path.
*
* @param path The path to check
* @return The space available in bytes
*/
@TargetApi(9)
public static long getUsableSpace(File path) {
if (Utils.hasGingerbread()) {
return path.getUsableSpace();
}
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}
/**
* Locate an existing instance of this Fragment or if not found, create and
* add it using FragmentManager.
*
* @param fm The FragmentManager manager to use.
* @return The existing instance of the Fragment or the new instance if just
* created.
*/
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
// Check to see if we have retained the worker fragment.
RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG);
// If not retained (or first time running), we need to create and add it.
if (mRetainFragment == null) {
mRetainFragment = new RetainFragment();
fm.beginTransaction().add(mRetainFragment, TAG).commitAllowingStateLoss();
}
return mRetainFragment;
}
/**
* A simple non-UI Fragment that stores a single Object and is retained over configuration
* changes. It will be used to retain the ImageCache object.
*/
public static class RetainFragment extends Fragment {
private Object mObject;
/**
* Empty constructor as per the Fragment documentation
*/
public RetainFragment() {}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Make sure this Fragment is retained over a configuration change
setRetainInstance(true);
}
/**
* Store a single object in this Fragment.
*
* @param object The object to store
*/
public void setObject(Object object) {
mObject = object;
}
/**
* Get the stored object.
*
* @return The stored object
*/
public Object getObject() {
return mObject;
}
}
}

View File

@@ -20,17 +20,20 @@ import android.content.Context;
import android.graphics.Bitmap;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;
import com.example.android.bitmapfun.BuildConfig;
import com.example.android.bitmapfun.R;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
@@ -40,7 +43,14 @@ import java.net.URL;
public class ImageFetcher extends ImageResizer {
private static final String TAG = "ImageFetcher";
private static final int HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10MB
public static final String HTTP_CACHE_DIR = "http";
private static final String HTTP_CACHE_DIR = "http";
private static final int IO_BUFFER_SIZE = 8 * 1024;
private DiskLruCache mHttpDiskCache;
private File mHttpCacheDir;
private boolean mHttpDiskCacheStarting = true;
private final Object mHttpDiskCacheLock = new Object();
private static final int DISK_CACHE_INDEX = 0;
/**
* Initialize providing a target image width and height for the processing images.
@@ -67,19 +77,103 @@ public class ImageFetcher extends ImageResizer {
private void init(Context context) {
checkConnection(context);
mHttpCacheDir = ImageCache.getDiskCacheDir(context, HTTP_CACHE_DIR);
}
@Override
protected void initDiskCacheInternal() {
super.initDiskCacheInternal();
initHttpDiskCache();
}
private void initHttpDiskCache() {
if (!mHttpCacheDir.exists()) {
mHttpCacheDir.mkdirs();
}
synchronized (mHttpDiskCacheLock) {
if (ImageCache.getUsableSpace(mHttpCacheDir) > HTTP_CACHE_SIZE) {
try {
mHttpDiskCache = DiskLruCache.open(mHttpCacheDir, 1, 1, HTTP_CACHE_SIZE);
if (BuildConfig.DEBUG) {
Log.d(TAG, "HTTP cache initialized");
}
} catch (IOException e) {
mHttpDiskCache = null;
}
}
mHttpDiskCacheStarting = false;
mHttpDiskCacheLock.notifyAll();
}
}
@Override
protected void clearCacheInternal() {
super.clearCacheInternal();
synchronized (mHttpDiskCacheLock) {
if (mHttpDiskCache != null && !mHttpDiskCache.isClosed()) {
try {
mHttpDiskCache.delete();
if (BuildConfig.DEBUG) {
Log.d(TAG, "HTTP cache cleared");
}
} catch (IOException e) {
Log.e(TAG, "clearCacheInternal - " + e);
}
mHttpDiskCache = null;
mHttpDiskCacheStarting = true;
initHttpDiskCache();
}
}
}
@Override
protected void flushCacheInternal() {
super.flushCacheInternal();
synchronized (mHttpDiskCacheLock) {
if (mHttpDiskCache != null) {
try {
mHttpDiskCache.flush();
if (BuildConfig.DEBUG) {
Log.d(TAG, "HTTP cache flushed");
}
} catch (IOException e) {
Log.e(TAG, "flush - " + e);
}
}
}
}
@Override
protected void closeCacheInternal() {
super.closeCacheInternal();
synchronized (mHttpDiskCacheLock) {
if (mHttpDiskCache != null) {
try {
if (!mHttpDiskCache.isClosed()) {
mHttpDiskCache.close();
mHttpDiskCache = null;
if (BuildConfig.DEBUG) {
Log.d(TAG, "HTTP cache closed");
}
}
} catch (IOException e) {
Log.e(TAG, "closeCacheInternal - " + e);
}
}
}
}
/**
* Simple network connection check.
*
* @param context
*/
* Simple network connection check.
*
* @param context
*/
private void checkConnection(Context context) {
final ConnectivityManager cm =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
if (networkInfo == null || !networkInfo.isConnectedOrConnecting()) {
Toast.makeText(context, "No network connection found.", Toast.LENGTH_LONG).show();
Toast.makeText(context, R.string.no_network_connection_toast, Toast.LENGTH_LONG).show();
Log.e(TAG, "checkConnection - no connection found");
}
}
@@ -96,15 +190,65 @@ public class ImageFetcher extends ImageResizer {
Log.d(TAG, "processBitmap - " + data);
}
// Download a bitmap, write it to a file
final File f = downloadBitmap(mContext, data);
final String key = ImageCache.hashKeyForDisk(data);
FileDescriptor fileDescriptor = null;
FileInputStream fileInputStream = null;
DiskLruCache.Snapshot snapshot;
synchronized (mHttpDiskCacheLock) {
// Wait for disk cache to initialize
while (mHttpDiskCacheStarting) {
try {
mHttpDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (f != null) {
// Return a sampled down version
return decodeSampledBitmapFromFile(f.toString(), mImageWidth, mImageHeight);
if (mHttpDiskCache != null) {
try {
snapshot = mHttpDiskCache.get(key);
if (snapshot == null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "processBitmap, not found in http cache, downloading...");
}
DiskLruCache.Editor editor = mHttpDiskCache.edit(key);
if (editor != null) {
if (downloadUrlToStream(data,
editor.newOutputStream(DISK_CACHE_INDEX))) {
editor.commit();
} else {
editor.abort();
}
}
snapshot = mHttpDiskCache.get(key);
}
if (snapshot != null) {
fileInputStream =
(FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
fileDescriptor = fileInputStream.getFD();
}
} catch (IOException e) {
Log.e(TAG, "processBitmap - " + e);
} catch (IllegalStateException e) {
Log.e(TAG, "processBitmap - " + e);
} finally {
if (fileDescriptor == null && fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {}
}
}
}
}
return null;
Bitmap bitmap = null;
if (fileDescriptor != null) {
bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth, mImageHeight);
}
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {}
}
return bitmap;
}
@Override
@@ -113,65 +257,54 @@ public class ImageFetcher extends ImageResizer {
}
/**
* Download a bitmap from a URL, write it to a disk and return the File pointer. This
* implementation uses a simple disk cache.
* Download a bitmap from a URL and write the content to an output stream.
*
* @param context The context to use
* @param urlString The URL to fetch
* @return A File pointing to the fetched bitmap
* @return true if successful, false otherwise
*/
public static File downloadBitmap(Context context, String urlString) {
final File cacheDir = DiskLruCache.getDiskCacheDir(context, HTTP_CACHE_DIR);
final DiskLruCache cache =
DiskLruCache.openCache(context, cacheDir, HTTP_CACHE_SIZE);
final File cacheFile = new File(cache.createFilePath(urlString));
if (cache.containsKey(urlString)) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "downloadBitmap - found in http cache - " + urlString);
}
return cacheFile;
}
if (BuildConfig.DEBUG) {
Log.d(TAG, "downloadBitmap - downloading - " + urlString);
}
Utils.disableConnectionReuseIfNecessary();
public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
disableConnectionReuseIfNecessary();
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
final InputStream in =
new BufferedInputStream(urlConnection.getInputStream(), Utils.IO_BUFFER_SIZE);
out = new BufferedOutputStream(new FileOutputStream(cacheFile), Utils.IO_BUFFER_SIZE);
in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return cacheFile;
return true;
} catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmap - " + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
if (out != null) {
try {
try {
if (out != null) {
out.close();
} catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmap - " + e);
}
}
if (in != null) {
in.close();
}
} catch (final IOException e) {}
}
return false;
}
return null;
/**
* Workaround for bug pre-Froyo, see here for more info:
* http://android-developers.blogspot.com/2011/09/androids-http-clients.html
*/
public static void disableConnectionReuseIfNecessary() {
// HTTP connection reuse which was buggy pre-froyo
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) {
System.setProperty("http.keepAlive", "false");
}
}
}

View File

@@ -24,13 +24,15 @@ import android.util.Log;
import com.example.android.bitmapfun.BuildConfig;
import java.io.FileDescriptor;
/**
* A simple subclass of {@link ImageWorker} that resizes images from resources given a target width
* and height. Useful for when the input images might be too large to simply load directly into
* memory.
*/
public class ImageResizer extends ImageWorker {
private static final String TAG = "ImageWorker";
private static final String TAG = "ImageResizer";
protected int mImageWidth;
protected int mImageHeight;
@@ -88,8 +90,7 @@ public class ImageResizer extends ImageWorker {
if (BuildConfig.DEBUG) {
Log.d(TAG, "processBitmap - " + resId);
}
return decodeSampledBitmapFromResource(
mContext.getResources(), resId, mImageWidth, mImageHeight);
return decodeSampledBitmapFromResource(mResources, resId, mImageWidth, mImageHeight);
}
@Override
@@ -132,7 +133,7 @@ public class ImageResizer extends ImageWorker {
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions
* that are equal to or greater than the requested width and height
*/
public static synchronized Bitmap decodeSampledBitmapFromFile(String filename,
public static Bitmap decodeSampledBitmapFromFile(String filename,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
@@ -148,6 +149,31 @@ public class ImageResizer extends ImageWorker {
return BitmapFactory.decodeFile(filename, options);
}
/**
* Decode and sample down a bitmap from a file input stream to the requested width and height.
*
* @param fileDescriptor The file descriptor to read from
* @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions
* that are equal to or greater than the requested width and height
*/
public static Bitmap decodeSampledBitmapFromDescriptor(
FileDescriptor fileDescriptor, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
}
/**
* Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding
* bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates

View File

@@ -24,7 +24,7 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.os.AsyncTask;
import android.support.v4.app.FragmentManager;
import android.util.Log;
import android.widget.ImageView;
@@ -42,15 +42,22 @@ public abstract class ImageWorker {
private static final int FADE_IN_TIME = 200;
private ImageCache mImageCache;
private ImageCache.ImageCacheParams mImageCacheParams;
private Bitmap mLoadingBitmap;
private boolean mFadeInBitmap = true;
private boolean mExitTasksEarly = false;
protected boolean mPauseWork = false;
private final Object mPauseWorkLock = new Object();
protected Context mContext;
protected ImageWorkerAdapter mImageWorkerAdapter;
protected Resources mResources;
private static final int MESSAGE_CLEAR = 0;
private static final int MESSAGE_INIT_DISK_CACHE = 1;
private static final int MESSAGE_FLUSH = 2;
private static final int MESSAGE_CLOSE = 3;
protected ImageWorker(Context context) {
mContext = context;
mResources = context.getResources();
}
/**
@@ -65,6 +72,10 @@ public abstract class ImageWorker {
* @param imageView The ImageView to bind the downloaded image to.
*/
public void loadImage(Object data, ImageView imageView) {
if (data == null) {
return;
}
Bitmap bitmap = null;
if (mImageCache != null) {
@@ -77,29 +88,13 @@ public abstract class ImageWorker {
} else if (cancelPotentialWork(data, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(mContext.getResources(), mLoadingBitmap, task);
new AsyncDrawable(mResources, mLoadingBitmap, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(data);
}
}
/**
* Load an image specified from a set adapter into an ImageView (override
* {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and disk
* cache will be used if an {@link ImageCache} has been set using
* {@link ImageWorker#setImageCache(ImageCache)}. If the image is found in the memory cache, it
* is set immediately, otherwise an {@link AsyncTask} will be created to asynchronously load the
* bitmap. {@link ImageWorker#setAdapter(ImageWorkerAdapter)} must be called before using this
* method.
*
* @param data The URL of the image to download.
* @param imageView The ImageView to bind the downloaded image to.
*/
public void loadImage(int num, ImageView imageView) {
if (mImageWorkerAdapter != null) {
loadImage(mImageWorkerAdapter.getItem(num), imageView);
} else {
throw new NullPointerException("Data not set, must call setAdapter() first.");
// NOTE: This uses a custom version of AsyncTask that has been pulled from the
// framework and slightly modified. Refer to the docs at the top of the class
// for more info on what was changed.
task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR, data);
}
}
@@ -118,26 +113,36 @@ public abstract class ImageWorker {
* @param resId
*/
public void setLoadingImage(int resId) {
mLoadingBitmap = BitmapFactory.decodeResource(mContext.getResources(), resId);
mLoadingBitmap = BitmapFactory.decodeResource(mResources, resId);
}
/**
* Set the {@link ImageCache} object to use with this ImageWorker.
*
* @param cacheCallback
* Adds an {@link ImageCache} to this worker in the background (to prevent disk access on UI
* thread).
* @param fragmentManager
* @param cacheParams
*/
public void setImageCache(ImageCache cacheCallback) {
mImageCache = cacheCallback;
public void addImageCache(FragmentManager fragmentManager,
ImageCache.ImageCacheParams cacheParams) {
mImageCacheParams = cacheParams;
setImageCache(ImageCache.findOrCreateCache(fragmentManager, mImageCacheParams));
new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE);
}
public ImageCache getImageCache() {
return mImageCache;
/**
* Sets the {@link ImageCache} object to use with this ImageWorker. Usually you will not need
* to call this directly, instead use {@link ImageWorker#addImageCache} which will create and
* add the {@link ImageCache} object in a background thread (to ensure no disk access on the
* main/UI thread).
*
* @param imageCache
*/
public void setImageCache(ImageCache imageCache) {
mImageCache = imageCache;
}
/**
* If set to true, the image will fade-in once it has been loaded by the background thread.
*
* @param fadeIn
*/
public void setImageFadeIn(boolean fadeIn) {
mFadeInBitmap = fadeIn;
@@ -158,6 +163,10 @@ public abstract class ImageWorker {
*/
protected abstract Bitmap processBitmap(Object data);
/**
* Cancels any pending work attached to the provided ImageView.
* @param imageView
*/
public static void cancelWork(ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
@@ -225,10 +234,23 @@ public abstract class ImageWorker {
*/
@Override
protected Bitmap doInBackground(Object... params) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "doInBackground - starting work");
}
data = params[0];
final String dataString = String.valueOf(data);
Bitmap bitmap = null;
// Wait here if work is paused and the task is not cancelled
synchronized (mPauseWorkLock) {
while (mPauseWork && !isCancelled()) {
try {
mPauseWorkLock.wait();
} catch (InterruptedException e) {}
}
}
// If the image cache is available and this task has not been cancelled by another
// thread and the ImageView that was originally bound to this task is still bound back
// to this task and our "exit early" flag is not set then try and fetch the bitmap from
@@ -255,6 +277,10 @@ public abstract class ImageWorker {
mImageCache.addBitmapToCache(dataString, bitmap);
}
if (BuildConfig.DEBUG) {
Log.d(TAG, "doInBackground - finished work");
}
return bitmap;
}
@@ -270,10 +296,21 @@ public abstract class ImageWorker {
final ImageView imageView = getAttachedImageView();
if (bitmap != null && imageView != null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "onPostExecute - setting bitmap");
}
setImageBitmap(imageView, bitmap);
}
}
@Override
protected void onCancelled(Bitmap bitmap) {
super.onCancelled(bitmap);
synchronized (mPauseWorkLock) {
mPauseWorkLock.notifyAll();
}
}
/**
* Returns the ImageView associated with this task as long as the ImageView's task still
* points to this task as well. Returns null otherwise.
@@ -301,7 +338,6 @@ public abstract class ImageWorker {
public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
}
@@ -323,11 +359,11 @@ public abstract class ImageWorker {
final TransitionDrawable td =
new TransitionDrawable(new Drawable[] {
new ColorDrawable(android.R.color.transparent),
new BitmapDrawable(mContext.getResources(), bitmap)
new BitmapDrawable(mResources, bitmap)
});
// Set background to loading bitmap
imageView.setBackgroundDrawable(
new BitmapDrawable(mContext.getResources(), mLoadingBitmap));
new BitmapDrawable(mResources, mLoadingBitmap));
imageView.setImageDrawable(td);
td.startTransition(FADE_IN_TIME);
@@ -336,29 +372,71 @@ public abstract class ImageWorker {
}
}
/**
* Set the simple adapter which holds the backing data.
*
* @param adapter
*/
public void setAdapter(ImageWorkerAdapter adapter) {
mImageWorkerAdapter = adapter;
public void setPauseWork(boolean pauseWork) {
synchronized (mPauseWorkLock) {
mPauseWork = pauseWork;
if (!mPauseWork) {
mPauseWorkLock.notifyAll();
}
}
}
/**
* Get the current adapter.
*
* @return
*/
public ImageWorkerAdapter getAdapter() {
return mImageWorkerAdapter;
protected class CacheAsyncTask extends AsyncTask<Object, Void, Void> {
@Override
protected Void doInBackground(Object... params) {
switch ((Integer)params[0]) {
case MESSAGE_CLEAR:
clearCacheInternal();
break;
case MESSAGE_INIT_DISK_CACHE:
initDiskCacheInternal();
break;
case MESSAGE_FLUSH:
flushCacheInternal();
break;
case MESSAGE_CLOSE:
closeCacheInternal();
break;
}
return null;
}
}
/**
* A very simple adapter for use with ImageWorker class and subclasses.
*/
public static abstract class ImageWorkerAdapter {
public abstract Object getItem(int num);
public abstract int getSize();
protected void initDiskCacheInternal() {
if (mImageCache != null) {
mImageCache.initDiskCache();
}
}
protected void clearCacheInternal() {
if (mImageCache != null) {
mImageCache.clearCache();
}
}
protected void flushCacheInternal() {
if (mImageCache != null) {
mImageCache.flush();
}
}
protected void closeCacheInternal() {
if (mImageCache != null) {
mImageCache.close();
mImageCache = null;
}
}
public void clearCache() {
new CacheAsyncTask().execute(MESSAGE_CLEAR);
}
public void flushCache() {
new CacheAsyncTask().execute(MESSAGE_FLUSH);
}
public void closeCache() {
new CacheAsyncTask().execute(MESSAGE_CLOSE);
}
}

View File

@@ -1,83 +0,0 @@
/*
* 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.bitmapfun.util;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
/**
* A simple non-UI Fragment that stores a single Object and is retained over configuration changes.
* In this sample it will be used to retain the ImageCache object.
*/
public class RetainFragment extends Fragment {
private static final String TAG = "RetainFragment";
private Object mObject;
/**
* Empty constructor as per the Fragment documentation
*/
public RetainFragment() {}
/**
* Locate an existing instance of this Fragment or if not found, create and
* add it using FragmentManager.
*
* @param fm The FragmentManager manager to use.
* @return The existing instance of the Fragment or the new instance if just
* created.
*/
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
// Check to see if we have retained the worker fragment.
RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG);
// If not retained (or first time running), we need to create and add it.
if (mRetainFragment == null) {
mRetainFragment = new RetainFragment();
fm.beginTransaction().add(mRetainFragment, TAG).commit();
}
return mRetainFragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Make sure this Fragment is retained over a configuration change
setRetainInstance(true);
}
/**
* Store a single object in this Fragment.
*
* @param object The object to store
*/
public void setObject(Object object) {
mObject = object;
}
/**
* Get the stored object.
*
* @return The stored object
*/
public Object getObject() {
return mObject;
}
}

View File

@@ -16,131 +16,61 @@
package com.example.android.bitmapfun.util;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Environment;
import android.os.StatFs;
import com.example.android.bitmapfun.ui.ImageDetailActivity;
import com.example.android.bitmapfun.ui.ImageGridActivity;
import java.io.File;
import android.annotation.TargetApi;
import android.os.Build;
import android.os.StrictMode;
/**
* Class containing some static utility methods.
*/
public class Utils {
public static final int IO_BUFFER_SIZE = 8 * 1024;
private Utils() {};
/**
* Workaround for bug pre-Froyo, see here for more info:
* http://android-developers.blogspot.com/2011/09/androids-http-clients.html
*/
public static void disableConnectionReuseIfNecessary() {
// HTTP connection reuse which was buggy pre-froyo
if (hasHttpConnectionBug()) {
System.setProperty("http.keepAlive", "false");
@TargetApi(11)
public static void enableStrictMode() {
if (Utils.hasGingerbread()) {
StrictMode.ThreadPolicy.Builder threadPolicyBuilder =
new StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog();
StrictMode.VmPolicy.Builder vmPolicyBuilder =
new StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog();
if (Utils.hasHoneycomb()) {
threadPolicyBuilder.penaltyFlashScreen();
vmPolicyBuilder
.setClassInstanceLimit(ImageGridActivity.class, 1)
.setClassInstanceLimit(ImageDetailActivity.class, 1);
}
StrictMode.setThreadPolicy(threadPolicyBuilder.build());
StrictMode.setVmPolicy(vmPolicyBuilder.build());
}
}
/**
* Get the size in bytes of a bitmap.
* @param bitmap
* @return size in bytes
*/
@SuppressLint("NewApi")
public static int getBitmapSize(Bitmap bitmap) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
return bitmap.getByteCount();
}
// Pre HC-MR1
return bitmap.getRowBytes() * bitmap.getHeight();
}
/**
* Check if external storage is built-in or removable.
*
* @return True if external storage is removable (like an SD card), false
* otherwise.
*/
@SuppressLint("NewApi")
public static boolean isExternalStorageRemovable() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
return Environment.isExternalStorageRemovable();
}
return true;
}
/**
* Get the external app cache directory.
*
* @param context The context to use
* @return The external cache dir
*/
@SuppressLint("NewApi")
public static File getExternalCacheDir(Context context) {
if (hasExternalCacheDir()) {
return context.getExternalCacheDir();
}
// Before Froyo we need to construct the external cache dir ourselves
final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/";
return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir);
}
/**
* Check how much usable space is available at a given path.
*
* @param path The path to check
* @return The space available in bytes
*/
@SuppressLint("NewApi")
public static long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
return path.getUsableSpace();
}
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}
/**
* Get the memory class of this device (approx. per-app memory limit)
*
* @param context
* @return
*/
public static int getMemoryClass(Context context) {
return ((ActivityManager) context.getSystemService(
Context.ACTIVITY_SERVICE)).getMemoryClass();
}
/**
* Check if OS version has a http URLConnection bug. See here for more information:
* http://android-developers.blogspot.com/2011/09/androids-http-clients.html
*
* @return
*/
public static boolean hasHttpConnectionBug() {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO;
}
/**
* Check if OS version has built-in external cache dir method.
*
* @return
*/
public static boolean hasExternalCacheDir() {
public static boolean hasFroyo() {
// Can use static final constants like FROYO, declared in later versions
// of the OS since they are inlined at compile time. This is guaranteed behavior.
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO;
}
/**
* Check if ActionBar is available.
*
* @return
*/
public static boolean hasActionBar() {
public static boolean hasGingerbread() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD;
}
public static boolean hasHoneycomb() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
}
public static boolean hasHoneycombMR1() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1;
}
public static boolean hasJellyBean() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
}
}