diff --git a/samples/training/bitmapfun/AndroidManifest.xml b/samples/training/bitmapfun/AndroidManifest.xml index 4a6f0f541..cabb442d1 100644 --- a/samples/training/bitmapfun/AndroidManifest.xml +++ b/samples/training/bitmapfun/AndroidManifest.xml @@ -22,7 +22,7 @@ + android:targetSdkVersion="16" /> @@ -36,7 +36,10 @@ + + + + + + + + + + + + + + + + + diff --git a/samples/training/bitmapfun/res/layout/image_detail_fragment.xml b/samples/training/bitmapfun/res/layout/image_detail_fragment.xml index 2d9dfcbf8..ff616dacc 100644 --- a/samples/training/bitmapfun/res/layout/image_detail_fragment.xml +++ b/samples/training/bitmapfun/res/layout/image_detail_fragment.xml @@ -16,12 +16,10 @@ --> + - 150dp - 1dp + 148dp + 2dp \ No newline at end of file diff --git a/samples/training/bitmapfun/res/values/colors.xml b/samples/training/bitmapfun/res/values/colors.xml new file mode 100644 index 000000000..7e4a4fecd --- /dev/null +++ b/samples/training/bitmapfun/res/values/colors.xml @@ -0,0 +1,23 @@ + + + + + + #BB7dbcd3 + #777dbcd3 + + diff --git a/samples/training/bitmapfun/res/values/strings.xml b/samples/training/bitmapfun/res/values/strings.xml index b77f768a4..8108c232f 100644 --- a/samples/training/bitmapfun/res/values/strings.xml +++ b/samples/training/bitmapfun/res/values/strings.xml @@ -19,12 +19,13 @@ BitmapFun This is a sample application for the Android Training class - "Displaying Bitmaps Efficiently" + "Displaying Bitmaps Efficiently" (http://developer.android.com/training/displaying-bitmaps/display-bitmap.html). It is not designed to be a full reference application but to demonstrate the concepts discussed in training course. Clear Caches - Caches have been cleared + Caches have been cleared Image Thumbnail + No network connection found \ No newline at end of file diff --git a/samples/training/bitmapfun/res/values/styles.xml b/samples/training/bitmapfun/res/values/styles.xml index 3e72fd335..0f1a018b9 100644 --- a/samples/training/bitmapfun/res/values/styles.xml +++ b/samples/training/bitmapfun/res/values/styles.xml @@ -22,8 +22,8 @@ \ No newline at end of file diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/provider/Images.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/provider/Images.java index 5c9ef5c61..809d73b5d 100644 --- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/provider/Images.java +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/provider/Images.java @@ -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", }; } diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailActivity.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailActivity.java index c7ee8cd4a..acd483fe8 100644 --- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailActivity.java +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailActivity.java @@ -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(); diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailFragment.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailFragment.java index e2fd70385..a0b3855cc 100644 --- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailFragment.java +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailFragment.java @@ -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); + } } } diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridActivity.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridActivity.java index 28d97b345..2e00930d7 100644 --- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridActivity.java +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridActivity.java @@ -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) { diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridFragment.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridFragment.java index 495d405ba..8a8bcf033 100644 --- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridFragment.java +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridFragment.java @@ -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(); } diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/AsyncTask.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/AsyncTask.java new file mode 100644 index 000000000..018ce1a23 --- /dev/null +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/AsyncTask.java @@ -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 + * ************************************* + * + *

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.

+ * + *

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 java.util.concurrent pacakge such as {@link Executor}, + * {@link ThreadPoolExecutor} and {@link FutureTask}.

+ * + *

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 Params, Progress and Result, + * and 4 steps, called onPreExecute, doInBackground, + * onProgressUpdate and onPostExecute.

+ * + *
+ *

Developer Guides

+ *

For more information about using tasks and threads, read the + * Processes and + * Threads developer guide.

+ *
+ * + *

Usage

+ *

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}.)

+ * + *

Here is an example of subclassing:

+ *
+ * private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
+ *     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");
+ *     }
+ * }
+ * 
+ * + *

Once created, a task is executed very simply:

+ *
+ * new DownloadFilesTask().execute(url1, url2, url3);
+ * 
+ * + *

AsyncTask's generic types

+ *

The three types used by an asynchronous task are the following:

+ *
    + *
  1. Params, the type of the parameters sent to the task upon + * execution.
  2. + *
  3. Progress, the type of the progress units published during + * the background computation.
  4. + *
  5. Result, the type of the result of the background + * computation.
  6. + *
+ *

Not all types are always used by an asynchronous task. To mark a type as unused, + * simply use the type {@link Void}:

+ *
+ * private class MyTask extends AsyncTask<Void, Void, Void> { ... }
+ * 
+ * + *

The 4 steps

+ *

When an asynchronous task is executed, the task goes through 4 steps:

+ *
    + *
  1. {@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.
  2. + *
  3. {@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.
  4. + *
  5. {@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.
  6. + *
  7. {@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.
  8. + *
+ * + *

Cancelling a task

+ *

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.)

+ * + *

Threading rules

+ *

There are a few threading rules that must be followed for this class to + * work properly:

+ *
    + *
  • The AsyncTask class must be loaded on the UI thread. This is done + * automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.
  • + *
  • The task instance must be created on the UI thread.
  • + *
  • {@link #execute} must be invoked on the UI thread.
  • + *
  • Do not call {@link #onPreExecute()}, {@link #onPostExecute}, + * {@link #doInBackground}, {@link #onProgressUpdate} manually.
  • + *
  • The task can be executed only once (an exception will be thrown if + * a second execution is attempted.)
  • + *
+ * + *

Memory observability

+ *

AsyncTask guarantees that all callback calls are synchronized in such a way that the following + * operations are safe without explicit synchronizations.

+ *
    + *
  • Set member fields in the constructor or {@link #onPreExecute}, and refer to them + * in {@link #doInBackground}. + *
  • Set member fields in {@link #doInBackground}, and refer to them in + * {@link #onProgressUpdate} and {@link #onPostExecute}. + *
+ * + *

Order of execution

+ *

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.

+ *

If you truly want parallel execution, you can invoke + * {@link #executeOnExecutor(java.util.concurrent.Executor, Object[])} with + * {@link #THREAD_POOL_EXECUTOR}.

+ */ +public abstract class AsyncTask { + 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 sPoolWorkQueue = + new LinkedBlockingQueue(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 mWorker; + private final FutureTask 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 mTasks = new ArrayDeque(); + 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() { + public Result call() throws Exception { + mTaskInvoked.set(true); + + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + //noinspection unchecked + return postResult(doInBackground(mParams)); + } + }; + + mFuture = new FutureTask(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(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() { + } + + /** + *

Runs on the UI thread after {@link #doInBackground}. The + * specified result is the value returned by {@link #doInBackground}.

+ * + *

This method won't be invoked if the task was cancelled.

+ * + * @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) { + } + + /** + *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and + * {@link #doInBackground(Object[])} has finished.

+ * + *

The default implementation simply invokes {@link #onCancelled()} and + * ignores the result. If you write your own implementation, do not call + * super.onCancelled(result).

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

Applications should preferably override {@link #onCancelled(Object)}. + * This method is invoked by the default implementation of + * {@link #onCancelled(Object)}.

+ * + *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and + * {@link #doInBackground(Object[])} has finished.

+ * + * @see #onCancelled(Object) + * @see #cancel(boolean) + * @see #isCancelled() + */ + protected void onCancelled() { + } + + /** + * Returns true 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 true if task was cancelled before it completed + * + * @see #cancel(boolean) + */ + public final boolean isCancelled() { + return mCancelled.get(); + } + + /** + *

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 cancel is called, + * this task should never run. If the task has already started, + * then the mayInterruptIfRunning parameter determines + * whether the thread executing this task should be interrupted in + * an attempt to stop the task.

+ * + *

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.

+ * + * @param mayInterruptIfRunning true if the thread executing this + * task should be interrupted; otherwise, in-progress tasks are allowed + * to complete. + * + * @return false if the task could not be cancelled, + * typically because it has already completed normally; + * true 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. + * + *

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. + * + *

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 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. + * + *

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. + * + *

Warning: Allowing multiple tasks to run in parallel from + * a thread pool is generally not 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}. + * + *

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 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(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 implements Callable { + Params[] mParams; + } + + @SuppressWarnings({"RawUseOfParameterizedType"}) + private static class AsyncTaskResult { + final AsyncTask mTask; + final Data[] mData; + + AsyncTaskResult(AsyncTask task, Data... data) { + mTask = task; + mData = data; + } + } +} \ No newline at end of file diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/DiskLruCache.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/DiskLruCache.java index a9f2166ea..26cdbd781 100644 --- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/DiskLruCache.java +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/DiskLruCache.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012 The Android Open Source Project + * Copyright (C) 2011 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. @@ -16,321 +16,938 @@ package com.example.android.bitmapfun.util; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Bitmap.CompressFormat; -import android.graphics.BitmapFactory; -import android.os.Environment; -import android.util.Log; - -import com.example.android.bitmapfun.BuildConfig; - -import java.io.BufferedOutputStream; +import java.io.BufferedInputStream; +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.EOFException; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; -import java.io.FilenameFilter; +import java.io.FileWriter; +import java.io.FilterOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.Collections; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Array; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Map.Entry; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; /** - * A simple disk LRU bitmap cache to illustrate how a disk cache would be used for bitmap caching. A - * much more robust and efficient disk LRU cache solution can be found in the ICS source code - * (libcore/luni/src/main/java/libcore/io/DiskLruCache.java) and is preferable to this simple - * implementation. + ****************************************************************************** + * Taken from the JB source code, can be found in: + * libcore/luni/src/main/java/libcore/io/DiskLruCache.java + * or direct link: + * https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java + ****************************************************************************** + * + * A cache that uses a bounded amount of space on a filesystem. Each cache + * entry has a string key and a fixed number of values. Values are byte + * sequences, accessible as streams or files. Each value must be between {@code + * 0} and {@code Integer.MAX_VALUE} bytes in length. + * + *

The cache stores its data in a directory on the filesystem. This + * directory must be exclusive to the cache; the cache may delete or overwrite + * files from its directory. It is an error for multiple processes to use the + * same cache directory at the same time. + * + *

This cache limits the number of bytes that it will store on the + * filesystem. When the number of stored bytes exceeds the limit, the cache will + * remove entries in the background until the limit is satisfied. The limit is + * not strict: the cache may temporarily exceed it while waiting for files to be + * deleted. The limit does not include filesystem overhead or the cache + * journal so space-sensitive applications should set a conservative limit. + * + *

Clients call {@link #edit} to create or update the values of an entry. An + * entry may have only one editor at one time; if a value is not available to be + * edited then {@link #edit} will return null. + *

    + *
  • When an entry is being created it is necessary to + * supply a full set of values; the empty value should be used as a + * placeholder if necessary. + *
  • When an entry is being edited, it is not necessary + * to supply data for every value; values default to their previous + * value. + *
+ * Every {@link #edit} call must be matched by a call to {@link Editor#commit} + * or {@link Editor#abort}. Committing is atomic: a read observes the full set + * of values as they were before or after the commit, but never a mix of values. + * + *

Clients call {@link #get} to read a snapshot of an entry. The read will + * observe the value at the time that {@link #get} was called. Updates and + * removals after the call do not impact ongoing reads. + * + *

This class is tolerant of some I/O errors. If files are missing from the + * filesystem, the corresponding entries will be dropped from the cache. If + * an error occurs while writing a cache value, the edit will fail silently. + * Callers should handle other problems by catching {@code IOException} and + * responding appropriately. */ -public class DiskLruCache { - private static final String TAG = "DiskLruCache"; - private static final String CACHE_FILENAME_PREFIX = "cache_"; - private static final int MAX_REMOVALS = 4; - private static final int INITIAL_CAPACITY = 32; - private static final float LOAD_FACTOR = 0.75f; +public final class DiskLruCache implements Closeable { + static final String JOURNAL_FILE = "journal"; + static final String JOURNAL_FILE_TMP = "journal.tmp"; + static final String MAGIC = "libcore.io.DiskLruCache"; + static final String VERSION_1 = "1"; + static final long ANY_SEQUENCE_NUMBER = -1; + private static final String CLEAN = "CLEAN"; + private static final String DIRTY = "DIRTY"; + private static final String REMOVE = "REMOVE"; + private static final String READ = "READ"; - private final File mCacheDir; - private int cacheSize = 0; - private int cacheByteSize = 0; - private final int maxCacheItemSize = 64; // 64 item default - private long maxCacheByteSize = 1024 * 1024 * 5; // 5MB default - private CompressFormat mCompressFormat = CompressFormat.JPEG; - private int mCompressQuality = 70; + private static final Charset UTF_8 = Charset.forName("UTF-8"); + private static final int IO_BUFFER_SIZE = 8 * 1024; - private final Map mLinkedHashMap = - Collections.synchronizedMap(new LinkedHashMap( - INITIAL_CAPACITY, LOAD_FACTOR, true)); - - /** - * A filename filter to use to identify the cache filenames which have CACHE_FILENAME_PREFIX - * prepended. - */ - private static final FilenameFilter cacheFileFilter = new FilenameFilter() { - @Override - public boolean accept(File dir, String filename) { - return filename.startsWith(CACHE_FILENAME_PREFIX); - } - }; - - /** - * Used to fetch an instance of DiskLruCache. + /* + * This cache uses a journal file named "journal". A typical journal file + * looks like this: + * libcore.io.DiskLruCache + * 1 + * 100 + * 2 * - * @param context - * @param cacheDir - * @param maxByteSize - * @return + * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 + * DIRTY 335c4c6028171cfddfbaae1a9c313c52 + * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 + * REMOVE 335c4c6028171cfddfbaae1a9c313c52 + * DIRTY 1ab96a171faeeee38496d8b330771a7a + * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 + * READ 335c4c6028171cfddfbaae1a9c313c52 + * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 + * + * The first five lines of the journal form its header. They are the + * constant string "libcore.io.DiskLruCache", the disk cache's version, + * the application's version, the value count, and a blank line. + * + * Each of the subsequent lines in the file is a record of the state of a + * cache entry. Each line contains space-separated values: a state, a key, + * and optional state-specific values. + * o DIRTY lines track that an entry is actively being created or updated. + * Every successful DIRTY action should be followed by a CLEAN or REMOVE + * action. DIRTY lines without a matching CLEAN or REMOVE indicate that + * temporary files may need to be deleted. + * o CLEAN lines track a cache entry that has been successfully published + * and may be read. A publish line is followed by the lengths of each of + * its values. + * o READ lines track accesses for LRU. + * o REMOVE lines track entries that have been deleted. + * + * The journal file is appended to as cache operations occur. The journal may + * occasionally be compacted by dropping redundant lines. A temporary file named + * "journal.tmp" will be used during compaction; that file should be deleted if + * it exists when the cache is opened. */ - public static DiskLruCache openCache(Context context, File cacheDir, long maxByteSize) { - if (!cacheDir.exists()) { - cacheDir.mkdir(); - } - if (cacheDir.isDirectory() && cacheDir.canWrite() - && Utils.getUsableSpace(cacheDir) > maxByteSize) { - return new DiskLruCache(cacheDir, maxByteSize); - } + private final File directory; + private final File journalFile; + private final File journalFileTmp; + private final int appVersion; + private final long maxSize; + private final int valueCount; + private long size = 0; + private Writer journalWriter; + private final LinkedHashMap lruEntries + = new LinkedHashMap(0, 0.75f, true); + private int redundantOpCount; - return null; + /** + * To differentiate between old and current snapshots, each entry is given + * a sequence number each time an edit is committed. A snapshot is stale if + * its sequence number is not equal to its entry's sequence number. + */ + private long nextSequenceNumber = 0; + + /* From java.util.Arrays */ + @SuppressWarnings("unchecked") + private static T[] copyOfRange(T[] original, int start, int end) { + final int originalLength = original.length; // For exception priority compatibility. + if (start > end) { + throw new IllegalArgumentException(); + } + if (start < 0 || start > originalLength) { + throw new ArrayIndexOutOfBoundsException(); + } + final int resultLength = end - start; + final int copyLength = Math.min(resultLength, originalLength - start); + final T[] result = (T[]) Array + .newInstance(original.getClass().getComponentType(), resultLength); + System.arraycopy(original, start, result, 0, copyLength); + return result; } /** - * Constructor that should not be called directly, instead use - * {@link DiskLruCache#openCache(Context, File, long)} which runs some extra checks before - * creating a DiskLruCache instance. - * - * @param cacheDir - * @param maxByteSize + * Returns the remainder of 'reader' as a string, closing it when done. */ - private DiskLruCache(File cacheDir, long maxByteSize) { - mCacheDir = cacheDir; - maxCacheByteSize = maxByteSize; - } - - /** - * Add a bitmap to the disk cache. - * - * @param key A unique identifier for the bitmap. - * @param data The bitmap to store. - */ - public void put(String key, Bitmap data) { - synchronized (mLinkedHashMap) { - if (mLinkedHashMap.get(key) == null) { - try { - final String file = createFilePath(mCacheDir, key); - if (writeBitmapToFile(data, file)) { - put(key, file); - flushCache(); - } - } catch (final FileNotFoundException e) { - Log.e(TAG, "Error in put: " + e.getMessage()); - } catch (final IOException e) { - Log.e(TAG, "Error in put: " + e.getMessage()); - } + public static String readFully(Reader reader) throws IOException { + try { + StringWriter writer = new StringWriter(); + char[] buffer = new char[1024]; + int count; + while ((count = reader.read(buffer)) != -1) { + writer.write(buffer, 0, count); } + return writer.toString(); + } finally { + reader.close(); } } - private void put(String key, String file) { - mLinkedHashMap.put(key, file); - cacheSize = mLinkedHashMap.size(); - cacheByteSize += new File(file).length(); + /** + * Returns the ASCII characters up to but not including the next "\r\n", or + * "\n". + * + * @throws java.io.EOFException if the stream is exhausted before the next newline + * character. + */ + public static String readAsciiLine(InputStream in) throws IOException { + // TODO: support UTF-8 here instead + + StringBuilder result = new StringBuilder(80); + while (true) { + int c = in.read(); + if (c == -1) { + throw new EOFException(); + } else if (c == '\n') { + break; + } + + result.append((char) c); + } + int length = result.length(); + if (length > 0 && result.charAt(length - 1) == '\r') { + result.setLength(length - 1); + } + return result.toString(); } /** - * Flush the cache, removing oldest entries if the total size is over the specified cache size. - * Note that this isn't keeping track of stale files in the cache directory that aren't in the - * HashMap. If the images and keys in the disk cache change often then they probably won't ever - * be removed. + * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null. */ - private void flushCache() { - Entry eldestEntry; - File eldestFile; - long eldestFileSize; - int count = 0; - - while (count < MAX_REMOVALS && - (cacheSize > maxCacheItemSize || cacheByteSize > maxCacheByteSize)) { - eldestEntry = mLinkedHashMap.entrySet().iterator().next(); - eldestFile = new File(eldestEntry.getValue()); - eldestFileSize = eldestFile.length(); - mLinkedHashMap.remove(eldestEntry.getKey()); - eldestFile.delete(); - cacheSize = mLinkedHashMap.size(); - cacheByteSize -= eldestFileSize; - count++; - if (BuildConfig.DEBUG) { - Log.d(TAG, "flushCache - Removed cache file, " + eldestFile + ", " - + eldestFileSize); + public static void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { } } } /** - * Get an image from the disk cache. - * - * @param key The unique identifier for the bitmap - * @return The bitmap or null if not found + * Recursively delete everything in {@code dir}. */ - public Bitmap get(String key) { - synchronized (mLinkedHashMap) { - final String file = mLinkedHashMap.get(key); - if (file != null) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "Disk cache hit"); + // TODO: this should specify paths as Strings rather than as Files + public static void deleteContents(File dir) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + throw new IllegalArgumentException("not a directory: " + dir); + } + for (File file : files) { + if (file.isDirectory()) { + deleteContents(file); + } + if (!file.delete()) { + throw new IOException("failed to delete file: " + file); + } + } + } + + /** This cache uses a single background thread to evict entries. */ + private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, + 60L, TimeUnit.SECONDS, new LinkedBlockingQueue()); + private final Callable cleanupCallable = new Callable() { + @Override public Void call() throws Exception { + synchronized (DiskLruCache.this) { + if (journalWriter == null) { + return null; // closed } - return BitmapFactory.decodeFile(file); - } else { - final String existingFile = createFilePath(mCacheDir, key); - if (new File(existingFile).exists()) { - put(key, existingFile); - if (BuildConfig.DEBUG) { - Log.d(TAG, "Disk cache hit (existing file)"); - } - return BitmapFactory.decodeFile(existingFile); + trimToSize(); + if (journalRebuildRequired()) { + rebuildJournal(); + redundantOpCount = 0; } } return null; } + }; + + private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { + this.directory = directory; + this.appVersion = appVersion; + this.journalFile = new File(directory, JOURNAL_FILE); + this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP); + this.valueCount = valueCount; + this.maxSize = maxSize; } /** - * Checks if a specific key exist in the cache. + * Opens the cache in {@code directory}, creating a cache if none exists + * there. * - * @param key The unique identifier for the bitmap - * @return true if found, false otherwise + * @param directory a writable directory + * @param appVersion + * @param valueCount the number of values per cache entry. Must be positive. + * @param maxSize the maximum number of bytes this cache should use to store + * @throws IOException if reading or writing the cache directory fails */ - public boolean containsKey(String key) { - // See if the key is in our HashMap - if (mLinkedHashMap.containsKey(key)) { - return true; + public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) + throws IOException { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + if (valueCount <= 0) { + throw new IllegalArgumentException("valueCount <= 0"); } - // Now check if there's an actual file that exists based on the key - final String existingFile = createFilePath(mCacheDir, key); - if (new File(existingFile).exists()) { - // File found, add it to the HashMap for future use - put(key, existingFile); - return true; + // prefer to pick up where we left off + DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + if (cache.journalFile.exists()) { + try { + cache.readJournal(); + cache.processJournal(); + cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true), + IO_BUFFER_SIZE); + return cache; + } catch (IOException journalIsCorrupt) { +// System.logW("DiskLruCache " + directory + " is corrupt: " +// + journalIsCorrupt.getMessage() + ", removing"); + cache.delete(); + } } - return false; + + // create a new empty cache + directory.mkdirs(); + cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + cache.rebuildJournal(); + return cache; } - /** - * Removes all disk cache entries from this instance cache dir - */ - public void clearCache() { - DiskLruCache.clearCache(mCacheDir); - } - - /** - * Removes all disk cache entries from the application cache directory in the uniqueName - * sub-directory. - * - * @param context The context to use - * @param uniqueName A unique cache directory name to append to the app cache directory - */ - public static void clearCache(Context context, String uniqueName) { - File cacheDir = getDiskCacheDir(context, uniqueName); - clearCache(cacheDir); - } - - /** - * Removes all disk cache entries from the given directory. This should not be called directly, - * call {@link DiskLruCache#clearCache(Context, String)} or {@link DiskLruCache#clearCache()} - * instead. - * - * @param cacheDir The directory to remove the cache files from - */ - private static void clearCache(File cacheDir) { - final File[] files = cacheDir.listFiles(cacheFileFilter); - for (int i=0; i i = lruEntries.values().iterator(); i.hasNext(); ) { + Entry entry = i.next(); + if (entry.currentEditor == null) { + for (int t = 0; t < valueCount; t++) { + size += entry.lengths[t]; + } + } else { + entry.currentEditor = null; + for (int t = 0; t < valueCount; t++) { + deleteIfExists(entry.getCleanFile(t)); + deleteIfExists(entry.getDirtyFile(t)); + } + i.remove(); } } } + + /** + * Creates a new journal that omits redundant information. This replaces the + * current journal if it exists. + */ + private synchronized void rebuildJournal() throws IOException { + if (journalWriter != null) { + journalWriter.close(); + } + + Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE); + writer.write(MAGIC); + writer.write("\n"); + writer.write(VERSION_1); + writer.write("\n"); + writer.write(Integer.toString(appVersion)); + writer.write("\n"); + writer.write(Integer.toString(valueCount)); + writer.write("\n"); + writer.write("\n"); + + for (Entry entry : lruEntries.values()) { + if (entry.currentEditor != null) { + writer.write(DIRTY + ' ' + entry.key + '\n'); + } else { + writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + } + } + + writer.close(); + journalFileTmp.renameTo(journalFile); + journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE); + } + + private static void deleteIfExists(File file) throws IOException { +// try { +// Libcore.os.remove(file.getPath()); +// } catch (ErrnoException errnoException) { +// if (errnoException.errno != OsConstants.ENOENT) { +// throw errnoException.rethrowAsIOException(); +// } +// } + if (file.exists() && !file.delete()) { + throw new IOException(); + } + } + + /** + * Returns a snapshot of the entry named {@code key}, or null if it doesn't + * exist is not currently readable. If a value is returned, it is moved to + * the head of the LRU queue. + */ + public synchronized Snapshot get(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null) { + return null; + } + + if (!entry.readable) { + return null; + } + + /* + * Open all streams eagerly to guarantee that we see a single published + * snapshot. If we opened streams lazily then the streams could come + * from different edits. + */ + InputStream[] ins = new InputStream[valueCount]; + try { + for (int i = 0; i < valueCount; i++) { + ins[i] = new FileInputStream(entry.getCleanFile(i)); + } + } catch (FileNotFoundException e) { + // a file must have been deleted manually! + return null; + } + + redundantOpCount++; + journalWriter.append(READ + ' ' + key + '\n'); + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return new Snapshot(key, entry.sequenceNumber, ins); + } + + /** + * Returns an editor for the entry named {@code key}, or null if another + * edit is in progress. + */ + public Editor edit(String key) throws IOException { + return edit(key, ANY_SEQUENCE_NUMBER); + } + + private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER + && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) { + return null; // snapshot is stale + } + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } else if (entry.currentEditor != null) { + return null; // another edit is in progress + } + + Editor editor = new Editor(entry); + entry.currentEditor = editor; + + // flush the journal before creating files to prevent file leaks + journalWriter.write(DIRTY + ' ' + key + '\n'); + journalWriter.flush(); + return editor; + } + + /** + * Returns the directory where this cache stores its data. + */ + public File getDirectory() { + return directory; + } + + /** + * Returns the maximum number of bytes that this cache should use to store + * its data. + */ + public long maxSize() { + return maxSize; + } + + /** + * Returns the number of bytes currently being used to store the values in + * this cache. This may be greater than the max size if a background + * deletion is pending. + */ + public synchronized long size() { + return size; + } + + private synchronized void completeEdit(Editor editor, boolean success) throws IOException { + Entry entry = editor.entry; + if (entry.currentEditor != editor) { + throw new IllegalStateException(); + } + + // if this edit is creating the entry for the first time, every index must have a value + if (success && !entry.readable) { + for (int i = 0; i < valueCount; i++) { + if (!entry.getDirtyFile(i).exists()) { + editor.abort(); + throw new IllegalStateException("edit didn't create file " + i); + } + } + } + + for (int i = 0; i < valueCount; i++) { + File dirty = entry.getDirtyFile(i); + if (success) { + if (dirty.exists()) { + File clean = entry.getCleanFile(i); + dirty.renameTo(clean); + long oldLength = entry.lengths[i]; + long newLength = clean.length(); + entry.lengths[i] = newLength; + size = size - oldLength + newLength; + } + } else { + deleteIfExists(dirty); + } + } + + redundantOpCount++; + entry.currentEditor = null; + if (entry.readable | success) { + entry.readable = true; + journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + if (success) { + entry.sequenceNumber = nextSequenceNumber++; + } + } else { + lruEntries.remove(entry.key); + journalWriter.write(REMOVE + ' ' + entry.key + '\n'); + } + + if (size > maxSize || journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + } + + /** + * We only rebuild the journal when it will halve the size of the journal + * and eliminate at least 2000 ops. + */ + private boolean journalRebuildRequired() { + final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000; + return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD + && redundantOpCount >= lruEntries.size(); + } + + /** + * Drops the entry for {@code key} if it exists and can be removed. Entries + * actively being edited cannot be removed. + * + * @return true if an entry was removed. + */ + public synchronized boolean remove(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null || entry.currentEditor != null) { + return false; + } + + for (int i = 0; i < valueCount; i++) { + File file = entry.getCleanFile(i); + if (!file.delete()) { + throw new IOException("failed to delete " + file); + } + size -= entry.lengths[i]; + entry.lengths[i] = 0; + } + + redundantOpCount++; + journalWriter.append(REMOVE + ' ' + key + '\n'); + lruEntries.remove(key); + + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return true; + } + + /** + * Returns true if this cache has been closed. + */ + public boolean isClosed() { + return journalWriter == null; + } + + private void checkNotClosed() { + if (journalWriter == null) { + throw new IllegalStateException("cache is closed"); + } + } + + /** + * Force buffered operations to the filesystem. + */ + public synchronized void flush() throws IOException { + checkNotClosed(); + trimToSize(); + journalWriter.flush(); + } + + /** + * Closes this cache. Stored values will remain on the filesystem. + */ + public synchronized void close() throws IOException { + if (journalWriter == null) { + return; // already closed + } + for (Entry entry : new ArrayList(lruEntries.values())) { + if (entry.currentEditor != null) { + entry.currentEditor.abort(); + } + } + trimToSize(); + journalWriter.close(); + journalWriter = null; + } + + private void trimToSize() throws IOException { + while (size > maxSize) { +// Map.Entry toEvict = lruEntries.eldest(); + final Map.Entry toEvict = lruEntries.entrySet().iterator().next(); + remove(toEvict.getKey()); + } + } + + /** + * Closes the cache and deletes all of its stored values. This will delete + * all files in the cache directory including files that weren't created by + * the cache. + */ + public void delete() throws IOException { + close(); + deleteContents(directory); + } + + private void validateKey(String key) { + if (key.contains(" ") || key.contains("\n") || key.contains("\r")) { + throw new IllegalArgumentException( + "keys must not contain spaces or newlines: \"" + key + "\""); + } + } + + private static String inputStreamToString(InputStream in) throws IOException { + return readFully(new InputStreamReader(in, UTF_8)); + } + + /** + * A snapshot of the values for an entry. + */ + public final class Snapshot implements Closeable { + private final String key; + private final long sequenceNumber; + private final InputStream[] ins; + + private Snapshot(String key, long sequenceNumber, InputStream[] ins) { + this.key = key; + this.sequenceNumber = sequenceNumber; + this.ins = ins; + } + + /** + * Returns an editor for this snapshot's entry, or null if either the + * entry has changed since this snapshot was created or if another edit + * is in progress. + */ + public Editor edit() throws IOException { + return DiskLruCache.this.edit(key, sequenceNumber); + } + + /** + * Returns the unbuffered stream with the value for {@code index}. + */ + public InputStream getInputStream(int index) { + return ins[index]; + } + + /** + * Returns the string value for {@code index}. + */ + public String getString(int index) throws IOException { + return inputStreamToString(getInputStream(index)); + } + + @Override public void close() { + for (InputStream in : ins) { + closeQuietly(in); + } + } + } + + /** + * Edits the values for an entry. + */ + public final class Editor { + private final Entry entry; + private boolean hasErrors; + + private Editor(Entry entry) { + this.entry = entry; + } + + /** + * Returns an unbuffered input stream to read the last committed value, + * or null if no value has been committed. + */ + public InputStream newInputStream(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + if (!entry.readable) { + return null; + } + return new FileInputStream(entry.getCleanFile(index)); + } + } + + /** + * Returns the last committed value as a string, or null if no value + * has been committed. + */ + public String getString(int index) throws IOException { + InputStream in = newInputStream(index); + return in != null ? inputStreamToString(in) : null; + } + + /** + * Returns a new unbuffered output stream to write the value at + * {@code index}. If the underlying output stream encounters errors + * when writing to the filesystem, this edit will be aborted when + * {@link #commit} is called. The returned output stream does not throw + * IOExceptions. + */ + public OutputStream newOutputStream(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index))); + } + } + + /** + * Sets the value at {@code index} to {@code value}. + */ + public void set(int index, String value) throws IOException { + Writer writer = null; + try { + writer = new OutputStreamWriter(newOutputStream(index), UTF_8); + writer.write(value); + } finally { + closeQuietly(writer); + } + } + + /** + * Commits this edit so it is visible to readers. This releases the + * edit lock so another edit may be started on the same key. + */ + public void commit() throws IOException { + if (hasErrors) { + completeEdit(this, false); + remove(entry.key); // the previous entry is stale + } else { + completeEdit(this, true); + } + } + + /** + * Aborts this edit. This releases the edit lock so another edit may be + * started on the same key. + */ + public void abort() throws IOException { + completeEdit(this, false); + } + + private class FaultHidingOutputStream extends FilterOutputStream { + private FaultHidingOutputStream(OutputStream out) { + super(out); + } + + @Override public void write(int oneByte) { + try { + out.write(oneByte); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override public void write(byte[] buffer, int offset, int length) { + try { + out.write(buffer, offset, length); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override public void close() { + try { + out.close(); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override public void flush() { + try { + out.flush(); + } catch (IOException e) { + hasErrors = true; + } + } + } + } + + private final class Entry { + private final String key; + + /** Lengths of this entry's files. */ + private final long[] lengths; + + /** True if this entry has ever been published */ + private boolean readable; + + /** The ongoing edit or null if this entry is not being edited. */ + private Editor currentEditor; + + /** The sequence number of the most recently committed edit to this entry. */ + private long sequenceNumber; + + private Entry(String key) { + this.key = key; + this.lengths = new long[valueCount]; + } + + public String getLengths() throws IOException { + StringBuilder result = new StringBuilder(); + for (long size : lengths) { + result.append(' ').append(size); + } + return result.toString(); + } + + /** + * Set lengths using decimal numbers like "10123". + */ + private void setLengths(String[] strings) throws IOException { + if (strings.length != valueCount) { + throw invalidLengths(strings); + } + + try { + for (int i = 0; i < strings.length; i++) { + lengths[i] = Long.parseLong(strings[i]); + } + } catch (NumberFormatException e) { + throw invalidLengths(strings); + } + } + + private IOException invalidLengths(String[] strings) throws IOException { + throw new IOException("unexpected journal line: " + Arrays.toString(strings)); + } + + public File getCleanFile(int i) { + return new File(directory, key + "." + i); + } + + public File getDirtyFile(int i) { + return new File(directory, key + "." + i + ".tmp"); + } + } } diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageCache.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageCache.java index 63eaea412..6995eab88 100644 --- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageCache.java +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageCache.java @@ -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 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(cacheParams.memCacheSize) { + if (mCacheParams.memoryCacheEnabled) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Memory cache created (size = " + mCacheParams.memCacheSize + ")"); + } + mMemoryCache = new LruCache(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; + } + } + } diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageFetcher.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageFetcher.java index 8b19dc32b..708484530 100644 --- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageFetcher.java +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageFetcher.java @@ -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"); + } } } diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageResizer.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageResizer.java index 18d1f82ff..f533231c2 100644 --- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageResizer.java +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageResizer.java @@ -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 diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageWorker.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageWorker.java index a0d26930d..32c43b25d 100644 --- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageWorker.java +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageWorker.java @@ -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); } @@ -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 { + + @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); } } diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/RetainFragment.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/RetainFragment.java deleted file mode 100644 index 3ee9cd63f..000000000 --- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/RetainFragment.java +++ /dev/null @@ -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; - } - -} diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/Utils.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/Utils.java index 544df339a..52a99f9f2 100644 --- a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/Utils.java +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/Utils.java @@ -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; + } }