├── META-INF └── MANIFEST.MF ├── examples └── WebImageList │ ├── res │ ├── drawable │ │ ├── icon.png │ │ ├── loader_frame_00.png │ │ ├── loader_frame_01.png │ │ ├── loader_frame_02.png │ │ ├── loader_frame_03.png │ │ ├── loader_frame_04.png │ │ ├── loader_frame_05.png │ │ ├── loader_frame_06.png │ │ ├── loader_frame_07.png │ │ ├── loader_frame_08.png │ │ ├── loader_frame_09.png │ │ ├── loader_frame_10.png │ │ ├── loader_frame_11.png │ │ ├── person_placeholder.png │ │ ├── person_placeholder_error.png │ │ └── loading_animation.xml │ ├── drawable-hdpi │ │ ├── icon.png │ │ ├── person_placeholder.png │ │ └── person_placeholder_error.png │ ├── drawable-ldpi │ │ └── icon.png │ ├── layout │ │ ├── web_image_activity.xml │ │ └── web_image_container_view.xml │ ├── values │ │ └── strings.xml │ └── menu │ │ └── main_menu.xml │ ├── proguard.cfg │ ├── default.properties │ ├── build.properties │ ├── AndroidManifest.xml │ ├── WebImageList.iml │ ├── src │ └── com │ │ └── wrapp │ │ └── android │ │ └── webimagelist │ │ ├── WebImageContainerView.java │ │ ├── WebImageListAdapter.java │ │ └── WebImageListActivity.java │ └── build.xml ├── .gitignore ├── extras ├── generate-number-images.sh └── rainbow-colors.txt ├── README.md ├── LICENSE.txt ├── README-MIGRATING.md ├── version.properties ├── proguard.cfg ├── default.properties ├── src └── com │ └── wrapp │ └── android │ └── webimage │ ├── RequestResponse.java │ ├── DownloadThread.java │ ├── ImageRequest.java │ ├── RequestRouterThread.java │ ├── LogWrapper.java │ ├── CheckTimestampThread.java │ ├── FileLoaderThread.java │ ├── ImageLoader.java │ ├── TaskQueueThread.java │ ├── DownloadThreadPool.java │ ├── WebImage.java │ ├── ImageCache.java │ ├── WebImageView.java │ └── ImageDownloader.java ├── AndroidManifest.xml ├── WebImage.iml ├── README-OLD.md └── WebImage.ipr /META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | 3 | -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable/icon.png -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable-hdpi/icon.png -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable-ldpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable-ldpi/icon.png -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable/loader_frame_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable/loader_frame_00.png -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable/loader_frame_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable/loader_frame_01.png -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable/loader_frame_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable/loader_frame_02.png -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable/loader_frame_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable/loader_frame_03.png -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable/loader_frame_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable/loader_frame_04.png -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable/loader_frame_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable/loader_frame_05.png -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable/loader_frame_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable/loader_frame_06.png -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable/loader_frame_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable/loader_frame_07.png -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable/loader_frame_08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable/loader_frame_08.png -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable/loader_frame_09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable/loader_frame_09.png -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable/loader_frame_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable/loader_frame_10.png -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable/loader_frame_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable/loader_frame_11.png -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable/person_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable/person_placeholder.png -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable-hdpi/person_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable-hdpi/person_placeholder.png -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable/person_placeholder_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable/person_placeholder_error.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.apk 2 | *.iws 3 | bin/* 4 | gen/* 5 | out/* 6 | local.properties 7 | examples/*/bin/* 8 | examples/*/gen/* 9 | examples/*/out/* 10 | examples/*/local.properties 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable-hdpi/person_placeholder_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wrapp-archive/WebImage-Android/HEAD/examples/WebImageList/res/drawable-hdpi/person_placeholder_error.png -------------------------------------------------------------------------------- /extras/generate-number-images.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | printf "Generating images:" 3 | for x in {0..99} ; do 4 | printf " %s" "$x" 5 | line=$(tail -n $((100 - $x)) rainbow-colors.txt | head -1) 6 | convert -background "$line" -fill black -size 250x250 -gravity center label:"$x" $x.png 7 | done 8 | printf "\n" 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WebImage-Android has been deprecated 2 | ==================================== 3 | 4 | WebImage-Android is no longer under active development, and has been 5 | deprecated in favor of [webimageloader][1], which was inspired by WebImage's 6 | architecture but has a nicer calling API and many more features which WebImage 7 | lacks. 8 | 9 | We've decided to leave the code for WebImage online for people who may need 10 | it, but advise those who end up here to try [webimageloader][1] instead. 11 | 12 | 13 | [1]: https://github.com/lexs/webimageloader 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Bohemian Wrappsody AB 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README-MIGRATING.md: -------------------------------------------------------------------------------- 1 | Migrating From WebImage-1.x to 2.x 2 | ================================== 3 | 4 | The API in WebImage-2.x has been significantly changed, and if you were using 5 | a 1.x version of the library, you will most likely have to make some changes 6 | in your code. Sorry! 7 | 8 | Major changes since 1.x include: 9 | 10 | - Elimination of the in-memory cache. Although this was very convenient, it is 11 | also the source of many OutOfMemory exceptions, and is not significantly 12 | faster than loading images from disk. After much careful consideration, I 13 | decided to remove this feature to prevent potential accidental misuse. 14 | - Refactored and simplified calling API, particularly to the `WebImageView` 15 | class. This is probably the most noticeable change which will break things 16 | in your code. 17 | - You must pass a context into most operations. Rather than setting the cache 18 | directory during initalization, your app's package name is automatically 19 | used. 20 | - Now `Bitmap` is fetched instead of `Drawable`. This allows for a number of 21 | memory optimizations, and also for rescaling/downsampling images on the fly. 22 | 23 | -------------------------------------------------------------------------------- /version.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2011 Bohemian Wrappsody AB 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | # this software and associated documentation files (the "Software"), to deal in 6 | # the Software without restriction, including without limitation the rights to 7 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | # the Software, and to permit persons to whom the Software is furnished to do so, 9 | # subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | # 21 | 22 | version.num=1.1.1 23 | -------------------------------------------------------------------------------- /extras/rainbow-colors.txt: -------------------------------------------------------------------------------- 1 | #C91F16 2 | #CC3615 3 | #D9580E 4 | #DD680B 5 | #E68601 6 | #EEAD00 7 | #FCCF03 8 | #EDE400 9 | #CED500 10 | #B0C50F 11 | #8FBB0C 12 | #77B312 13 | #69B011 14 | #47A41E 15 | #2D9C1F 16 | #189425 17 | #088343 18 | #0E8C62 19 | #05949D 20 | #0894B6 21 | #1F9AD7 22 | #0D84C4 23 | #0363A3 24 | #094A91 25 | #113279 26 | #182369 27 | #1D1259 28 | #310C5A 29 | #5B0B5A 30 | #820060 31 | #AE0964 32 | #C50059 33 | #C50A2A 34 | #C4232B 35 | #C50A2A 36 | #C50059 37 | #AE0964 38 | #820060 39 | #5B0B5A 40 | #310C5A 41 | #1D1259 42 | #182369 43 | #113279 44 | #094A91 45 | #0363A3 46 | #0D84C4 47 | #1F9AD7 48 | #0894B6 49 | #05949D 50 | #0E8C62 51 | #088343 52 | #189425 53 | #2D9C1F 54 | #47A41E 55 | #69B011 56 | #77B312 57 | #8FBB0C 58 | #B0C50F 59 | #CED500 60 | #EDE400 61 | #FCCF03 62 | #EEAD00 63 | #E68601 64 | #DD680B 65 | #D9580E 66 | #CC3615 67 | #C91F16 68 | #CC3615 69 | #D9580E 70 | #DD680B 71 | #E68601 72 | #EEAD00 73 | #FCCF03 74 | #EDE400 75 | #CED500 76 | #B0C50F 77 | #8FBB0C 78 | #77B312 79 | #69B011 80 | #47A41E 81 | #2D9C1F 82 | #189425 83 | #088343 84 | #0E8C62 85 | #05949D 86 | #0894B6 87 | #1F9AD7 88 | #0D84C4 89 | #0363A3 90 | #094A91 91 | #113279 92 | #182369 93 | #1D1259 94 | #310C5A 95 | #5B0B5A 96 | #820060 97 | #AE0964 98 | #C50059 99 | #C50A2A 100 | #C4232B 101 | -------------------------------------------------------------------------------- /proguard.cfg: -------------------------------------------------------------------------------- 1 | -optimizationpasses 5 2 | -dontusemixedcaseclassnames 3 | -dontskipnonpubliclibraryclasses 4 | -dontpreverify 5 | -verbose 6 | -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* 7 | 8 | -keep public class * extends android.app.Activity 9 | -keep public class * extends android.app.Application 10 | -keep public class * extends android.app.Service 11 | -keep public class * extends android.content.BroadcastReceiver 12 | -keep public class * extends android.content.ContentProvider 13 | -keep public class * extends android.app.backup.BackupAgentHelper 14 | -keep public class * extends android.preference.Preference 15 | -keep public class com.android.vending.licensing.ILicensingService 16 | 17 | -keepclasseswithmembernames class * { 18 | native ; 19 | } 20 | 21 | -keepclasseswithmembers class * { 22 | public (android.content.Context, android.util.AttributeSet); 23 | } 24 | 25 | -keepclasseswithmembers class * { 26 | public (android.content.Context, android.util.AttributeSet, int); 27 | } 28 | 29 | -keepclassmembers class * extends android.app.Activity { 30 | public void *(android.view.View); 31 | } 32 | 33 | -keepclassmembers enum * { 34 | public static **[] values(); 35 | public static ** valueOf(java.lang.String); 36 | } 37 | 38 | -keep class * implements android.os.Parcelable { 39 | public static final android.os.Parcelable$Creator *; 40 | } 41 | -------------------------------------------------------------------------------- /examples/WebImageList/proguard.cfg: -------------------------------------------------------------------------------- 1 | -optimizationpasses 5 2 | -dontusemixedcaseclassnames 3 | -dontskipnonpubliclibraryclasses 4 | -dontpreverify 5 | -verbose 6 | -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* 7 | 8 | -keep public class * extends android.app.Activity 9 | -keep public class * extends android.app.Application 10 | -keep public class * extends android.app.Service 11 | -keep public class * extends android.content.BroadcastReceiver 12 | -keep public class * extends android.content.ContentProvider 13 | -keep public class * extends android.app.backup.BackupAgentHelper 14 | -keep public class * extends android.preference.Preference 15 | -keep public class com.android.vending.licensing.ILicensingService 16 | 17 | -keepclasseswithmembernames class * { 18 | native ; 19 | } 20 | 21 | -keepclasseswithmembers class * { 22 | public (android.content.Context, android.util.AttributeSet); 23 | } 24 | 25 | -keepclasseswithmembers class * { 26 | public (android.content.Context, android.util.AttributeSet, int); 27 | } 28 | 29 | -keepclassmembers class * extends android.app.Activity { 30 | public void *(android.view.View); 31 | } 32 | 33 | -keepclassmembers enum * { 34 | public static **[] values(); 35 | public static ** valueOf(java.lang.String); 36 | } 37 | 38 | -keep class * implements android.os.Parcelable { 39 | public static final android.os.Parcelable$Creator *; 40 | } 41 | -------------------------------------------------------------------------------- /examples/WebImageList/res/layout/web_image_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 29 | -------------------------------------------------------------------------------- /examples/WebImageList/default.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2011 Bohemian Wrappsody AB 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | # this software and associated documentation files (the "Software"), to deal in 6 | # the Software without restriction, including without limitation the rights to 7 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | # the Software, and to permit persons to whom the Software is furnished to do so, 9 | # subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | # 21 | 22 | # This file is automatically generated by Android Tools. 23 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 24 | # 25 | # This file must be checked in Version Control Systems. 26 | # 27 | # To customize properties used by the Ant build system use, 28 | # "build.properties", and override values to adapt the script to your 29 | # project structure. 30 | 31 | # Project target. 32 | target=android-8 33 | -------------------------------------------------------------------------------- /default.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2011 Bohemian Wrappsody AB 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | # this software and associated documentation files (the "Software"), to deal in 6 | # the Software without restriction, including without limitation the rights to 7 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | # the Software, and to permit persons to whom the Software is furnished to do so, 9 | # subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | # 21 | 22 | # This file is automatically generated by Android Tools. 23 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 24 | # 25 | # This file must be checked in Version Control Systems. 26 | # 27 | # To customize properties used by the Ant build system use, 28 | # "build.properties", and override values to adapt the script to your 29 | # project structure. 30 | 31 | android.library=true 32 | # Project target. 33 | target=android-7 34 | -------------------------------------------------------------------------------- /src/com/wrapp/android/webimage/RequestResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Bohemian Wrappsody AB 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package com.wrapp.android.webimage; 23 | 24 | import android.graphics.Bitmap; 25 | 26 | import java.lang.ref.WeakReference; 27 | 28 | public class RequestResponse { 29 | public WeakReference bitmapReference; 30 | public ImageRequest originalRequest; 31 | 32 | public RequestResponse(Bitmap bitmap, ImageRequest originalRequest) { 33 | this.bitmapReference = new WeakReference(bitmap); 34 | this.originalRequest = originalRequest; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/WebImageList/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 24 | WebImageList 25 | Refresh 26 | Clear Caches 27 | Show Memory Use 28 | Restrict Memory Use 29 | Force Timestamp Checks 30 | 31 | -------------------------------------------------------------------------------- /examples/WebImageList/res/menu/main_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 23 | 24 | 25 | 27 | 29 | 31 | 33 | 35 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 27 | 28 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/WebImageList/build.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2011 Bohemian Wrappsody AB 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | # this software and associated documentation files (the "Software"), to deal in 6 | # the Software without restriction, including without limitation the rights to 7 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | # the Software, and to permit persons to whom the Software is furnished to do so, 9 | # subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | # 21 | 22 | # This file is used to override default values used by the Ant build system. 23 | # 24 | # This file must be checked in Version Control Systems, as it is 25 | # integral to the build system of your project. 26 | 27 | # This file is only used by the Ant script. 28 | 29 | # You can use this to override default values such as 30 | # 'source.dir' for the location of your java source folder and 31 | # 'out.dir' for the location of your output folder. 32 | 33 | # You can also use it define how the release builds are signed by declaring 34 | # the following properties: 35 | # 'key.store' for the location of your keystore and 36 | # 'key.alias' for the name of the key to use. 37 | # The password will be asked during the build when you use the 'release' target. 38 | 39 | -------------------------------------------------------------------------------- /examples/WebImageList/res/layout/web_image_container_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 23 | 24 | 27 | 32 | 41 | -------------------------------------------------------------------------------- /src/com/wrapp/android/webimage/DownloadThread.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Bohemian Wrappsody AB 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package com.wrapp.android.webimage; 23 | 24 | import android.graphics.Bitmap; 25 | 26 | public class DownloadThread extends TaskQueueThread { 27 | public DownloadThread() { 28 | super("Download"); 29 | setPriority(Thread.MIN_PRIORITY); 30 | } 31 | 32 | @Override 33 | protected Bitmap processRequest(ImageRequest request) { 34 | if(ImageCache.isImageCached(request.context, request.imageKey) && !request.forceDownload) { 35 | FileLoaderThread.getInstance().addTask(request); 36 | } 37 | else if(ImageDownloader.loadImage(request.context, request.imageKey, request.imageUrl)) { 38 | FileLoaderThread.getInstance().addTask(request); 39 | } 40 | return null; 41 | } 42 | 43 | @Override 44 | protected void onRequestComplete(RequestResponse response) { 45 | // Never reached 46 | } 47 | 48 | @Override 49 | protected void onRequestCancelled(ImageRequest request) { 50 | // Nothing to do 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /WebImage.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/com/wrapp/android/webimage/ImageRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Bohemian Wrappsody AB 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package com.wrapp.android.webimage; 23 | 24 | import android.content.Context; 25 | import android.graphics.BitmapFactory; 26 | 27 | import java.net.URL; 28 | 29 | public final class ImageRequest { 30 | public Context context; 31 | public String imageKey; 32 | public URL imageUrl; 33 | public Listener listener; 34 | public BitmapFactory.Options loadOptions; 35 | public boolean forceDownload = false; 36 | 37 | public interface Listener { 38 | public void onBitmapLoaded(final RequestResponse requestResponse); 39 | public void onBitmapLoadError(String message); 40 | public void onBitmapLoadCancelled(); 41 | } 42 | 43 | public ImageRequest(final Context context, URL imageUrl, Listener listener, BitmapFactory.Options options) { 44 | this.context = context; 45 | this.imageKey = ImageCache.getCacheKeyForUrl(imageUrl); 46 | this.imageUrl = imageUrl; 47 | this.listener = listener; 48 | this.loadOptions = options; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/WebImageList/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /examples/WebImageList/WebImageList.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/com/wrapp/android/webimage/RequestRouterThread.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Bohemian Wrappsody AB 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package com.wrapp.android.webimage; 23 | 24 | import android.graphics.Bitmap; 25 | 26 | public class RequestRouterThread extends TaskQueueThread { 27 | protected static RequestRouterThread staticInstance; 28 | 29 | public static RequestRouterThread getInstance() { 30 | if(staticInstance == null) { 31 | staticInstance = new RequestRouterThread(); 32 | } 33 | return staticInstance; 34 | } 35 | 36 | private RequestRouterThread() { 37 | super("RequestRouter"); 38 | setPriority(Thread.NORM_PRIORITY - 1); 39 | } 40 | 41 | @Override 42 | protected Bitmap processRequest(ImageRequest request) { 43 | if(ImageCache.isImageCached(request.context, request.imageKey)) { 44 | FileLoaderThread.getInstance().addTask(request); 45 | } 46 | else { 47 | DownloadThreadPool.getInstance().addTask(request); 48 | } 49 | 50 | return null; 51 | } 52 | 53 | @Override 54 | protected void onRequestComplete(RequestResponse response) { 55 | // Never reached 56 | } 57 | 58 | @Override 59 | protected void onRequestCancelled(ImageRequest request) { 60 | // Nothing to do 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/WebImageList/res/drawable/loading_animation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/com/wrapp/android/webimage/LogWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Bohemian Wrappsody AB 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package com.wrapp.android.webimage; 23 | 24 | import android.util.Log; 25 | 26 | public class LogWrapper { 27 | public static int level; 28 | public static String tag; 29 | 30 | public static void enableLogging(String tag, int level) { 31 | LogWrapper.tag = tag; 32 | LogWrapper.level = level; 33 | } 34 | 35 | public static final void logMessage(String message) { 36 | // Log.println() will throw if the message is null or empty, so better to do the check here 37 | if(tag != null && message != null && message.length() > 0) { 38 | Log.println(level, tag, message); 39 | } 40 | } 41 | 42 | public static final void logException(Exception exception) { 43 | if(tag != null) { 44 | StackTraceElement[] stackTraceElements = exception.getStackTrace(); 45 | for(int i = 0; i < stackTraceElements.length; i++) { 46 | StackTraceElement element = stackTraceElements[i]; 47 | if(element.getClassName().contains(ImageLoader.class.getSimpleName())) { 48 | String message = exception.getClass().getSimpleName() + " at " + element.getFileName() + ":" + 49 | element.getLineNumber() + ": " + exception.getMessage(); 50 | Log.println(Log.ERROR, tag, message); 51 | break; 52 | } 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /examples/WebImageList/src/com/wrapp/android/webimagelist/WebImageContainerView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Bohemian Wrappsody AB 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package com.wrapp.android.webimagelist; 23 | 24 | import android.content.Context; 25 | import android.graphics.BitmapFactory; 26 | import android.util.AttributeSet; 27 | import android.view.LayoutInflater; 28 | import android.widget.RelativeLayout; 29 | import android.widget.TextView; 30 | import com.wrapp.android.webimage.WebImageView; 31 | 32 | import java.net.MalformedURLException; 33 | import java.net.URL; 34 | 35 | /** Small container class for a WebImageView and a corresponding TextView */ 36 | @SuppressWarnings({"UnusedDeclaration"}) 37 | public class WebImageContainerView extends RelativeLayout { 38 | private WebImageView webImageView; 39 | private TextView imageText; 40 | 41 | public WebImageContainerView(Context context) { 42 | super(context); 43 | initialize(context); 44 | } 45 | 46 | public WebImageContainerView(Context context, AttributeSet attrs) { 47 | super(context, attrs); 48 | initialize(context); 49 | } 50 | 51 | public WebImageContainerView(Context context, AttributeSet attrs, int defStyle) { 52 | super(context, attrs, defStyle); 53 | initialize(context); 54 | } 55 | 56 | private void initialize(Context context) { 57 | LayoutInflater layoutInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 58 | layoutInflater.inflate(R.layout.web_image_container_view, this, true); 59 | webImageView = (WebImageView)findViewById(R.id.WebImageView); 60 | imageText = (TextView)findViewById(R.id.WebImageViewText); 61 | } 62 | 63 | public void setImageUrl(String imageUrlString, WebImageView.Listener listener, BitmapFactory.Options options) { 64 | try { 65 | URL imageUrl = new URL(imageUrlString); 66 | webImageView.setListener(listener); 67 | webImageView.setImageUrl(imageUrl, options, R.drawable.person_placeholder_error, R.drawable.person_placeholder); 68 | } 69 | catch(MalformedURLException e) { 70 | e.printStackTrace(); 71 | } 72 | } 73 | 74 | public void setImageText(String text) { 75 | imageText.setText(text); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/com/wrapp/android/webimage/CheckTimestampThread.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Bohemian Wrappsody AB 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package com.wrapp.android.webimage; 23 | 24 | import android.graphics.Bitmap; 25 | 26 | import java.io.File; 27 | import java.util.Date; 28 | 29 | public class CheckTimestampThread extends TaskQueueThread { 30 | static CheckTimestampThread staticInstance; 31 | 32 | public static CheckTimestampThread getInstance() { 33 | if(staticInstance == null) { 34 | staticInstance = new CheckTimestampThread(); 35 | } 36 | return staticInstance; 37 | } 38 | 39 | private CheckTimestampThread() { 40 | super("CheckTimestamp"); 41 | setPriority(Thread.MIN_PRIORITY); 42 | } 43 | 44 | @Override 45 | protected Bitmap processRequest(ImageRequest request) { 46 | LogWrapper.logMessage("Requesting timestamp for " + request.imageUrl); 47 | File cacheFile = new File(ImageCache.getCacheDirectory(request.context), request.imageKey); 48 | Date expirationDate = ImageDownloader.getServerTimestamp(request.imageUrl); 49 | Date now = new Date(); 50 | if(expirationDate.after(now)) { 51 | LogWrapper.logMessage("Cached version of " + request.imageUrl.toString() + " is still current, updating timestamp"); 52 | if(!cacheFile.setLastModified(now.getTime())) { 53 | LogWrapper.logMessage("Can't update timestamp!"); 54 | // TODO: It seems that in some cases this call will always return false and refuse to update the timestamp 55 | // For more info, see: http://code.google.com/p/android/issues/detail?id=18624 56 | // This occurs on other devices, including my Galaxy Nexus. Not sure how many others have this bug. 57 | } 58 | } 59 | else { 60 | LogWrapper.logMessage("Cached version of " + request.imageUrl.toString() + " found, but has expired."); 61 | cacheFile.delete(); 62 | request.forceDownload = true; 63 | DownloadThreadPool.getInstance().addTask(request); 64 | } 65 | 66 | return null; 67 | } 68 | 69 | @Override 70 | protected void onRequestComplete(RequestResponse response) { 71 | // Never reached 72 | } 73 | 74 | @Override 75 | protected void onRequestCancelled(ImageRequest request) { 76 | // Nothing to do 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /examples/WebImageList/src/com/wrapp/android/webimagelist/WebImageListAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Bohemian Wrappsody AB 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package com.wrapp.android.webimagelist; 23 | 24 | import android.graphics.BitmapFactory; 25 | import android.view.View; 26 | import android.view.ViewGroup; 27 | import android.widget.BaseAdapter; 28 | import com.wrapp.android.webimage.WebImageView; 29 | 30 | public class WebImageListAdapter extends BaseAdapter { 31 | private static final int NUM_IMAGES = 100; 32 | private static final int IMAGE_SIZE = 100; 33 | 34 | private WebImageView.Listener listener; 35 | private boolean shouldRestrictMemoryUsage = false; 36 | private static BitmapFactory.Options options = new BitmapFactory.Options(); 37 | 38 | public WebImageListAdapter(WebImageView.Listener listener) { 39 | this.listener = listener; 40 | } 41 | 42 | public int getCount() { 43 | return NUM_IMAGES; 44 | } 45 | 46 | private String getImageUrl(int i) { 47 | // Numbers with random backgrounds. More useful for testing correct ListView behavior. 48 | return "http://static.nikreiman.com/numbers/" + i + ".png"; 49 | // Unicorns! 50 | // return "http://unicornify.appspot.com/avatar/" + i + "?s=" + IMAGE_SIZE; 51 | // Generic Gravatar identicons 52 | // return "http://www.gravatar.com/avatar/" + i + "?s=" + IMAGE_SIZE + "&d=identicon"; 53 | } 54 | 55 | public Object getItem(int i) { 56 | return getImageUrl(i); 57 | } 58 | 59 | public long getItemId(int i) { 60 | return i; 61 | } 62 | 63 | public View getView(int i, View convertView, ViewGroup parentViewGroup) { 64 | WebImageContainerView containerView; 65 | if(convertView != null) { 66 | containerView = (WebImageContainerView)convertView; 67 | } 68 | else { 69 | containerView = new WebImageContainerView(parentViewGroup.getContext()); 70 | } 71 | 72 | containerView.setImageUrl(getImageUrl(i), listener, options); 73 | containerView.setImageText("Image #" + i); 74 | return containerView; 75 | } 76 | 77 | public boolean getShouldRestrictMemoryUsage() { 78 | return shouldRestrictMemoryUsage; 79 | } 80 | 81 | public void setShouldRestrictMemoryUsage(boolean shouldRestrictMemoryUsage) { 82 | this.shouldRestrictMemoryUsage = shouldRestrictMemoryUsage; 83 | options.inInputShareable = shouldRestrictMemoryUsage; 84 | options.inPurgeable = shouldRestrictMemoryUsage; 85 | options.inSampleSize = shouldRestrictMemoryUsage ? 2 : 1; 86 | options.inDither = shouldRestrictMemoryUsage; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/com/wrapp/android/webimage/FileLoaderThread.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Bohemian Wrappsody AB 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package com.wrapp.android.webimage; 23 | 24 | import android.graphics.Bitmap; 25 | import android.graphics.BitmapFactory; 26 | 27 | import java.io.File; 28 | import java.io.FileInputStream; 29 | import java.io.IOException; 30 | import java.util.Date; 31 | 32 | public class FileLoaderThread extends TaskQueueThread { 33 | static FileLoaderThread staticInstance; 34 | 35 | public static FileLoaderThread getInstance() { 36 | if(staticInstance == null) { 37 | staticInstance = new FileLoaderThread(); 38 | } 39 | return staticInstance; 40 | } 41 | 42 | private FileLoaderThread() { 43 | super("FileLoader"); 44 | // Set to be slightly below normal priority so that the GUI thread runs a bit snappier 45 | setPriority(Thread.NORM_PRIORITY - 1); 46 | } 47 | 48 | @Override 49 | protected Bitmap processRequest(ImageRequest request) { 50 | Bitmap bitmap = null; 51 | 52 | FileInputStream inputStream = null; 53 | File cacheFile = new File(ImageCache.getCacheDirectory(request.context), request.imageKey); 54 | if(cacheFile.exists()) { 55 | try { 56 | Date now = new Date(); 57 | long fileAgeInMs = now.getTime() - cacheFile.lastModified(); 58 | if(fileAgeInMs > ImageCache.getCacheRecheckAgeInMs()) { 59 | CheckTimestampThread.getInstance().addTask(request); 60 | } 61 | 62 | LogWrapper.logMessage("Loading image " + request.imageUrl + " from file cache"); 63 | inputStream = new FileInputStream(cacheFile); 64 | bitmap = BitmapFactory.decodeFileDescriptor(inputStream.getFD(), null, request.loadOptions); 65 | if(bitmap == null) { 66 | throw new Exception("Could not create bitmap from image " + request.imageUrl.toString()); 67 | } 68 | } 69 | catch(Exception e) { 70 | LogWrapper.logException(e); 71 | } 72 | finally { 73 | if(inputStream != null) { 74 | try { 75 | inputStream.close(); 76 | } 77 | catch(IOException e) { 78 | LogWrapper.logException(e); 79 | } 80 | } 81 | } 82 | } 83 | 84 | return bitmap; 85 | } 86 | 87 | @Override 88 | protected void onRequestComplete(RequestResponse response) { 89 | response.originalRequest.listener.onBitmapLoaded(response); 90 | } 91 | 92 | @Override 93 | protected void onRequestCancelled(ImageRequest request) { 94 | request.listener.onBitmapLoadCancelled(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/com/wrapp/android/webimage/ImageLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Bohemian Wrappsody AB 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package com.wrapp.android.webimage; 23 | 24 | import android.content.Context; 25 | import android.graphics.BitmapFactory; 26 | 27 | import java.net.URL; 28 | 29 | public class ImageLoader { 30 | // Static singleton instance 31 | private static ImageLoader staticInstance; 32 | 33 | // Worker threads for different tasks, ordered from fast -> slow 34 | private RequestRouterThread requestRouterThread; 35 | private FileLoaderThread fileLoaderThread; 36 | private CheckTimestampThread checkTimestampThread; 37 | private DownloadThreadPool downloadThreadPool; 38 | 39 | public static ImageLoader getInstance(Context context) { 40 | if(staticInstance == null) { 41 | staticInstance = new ImageLoader(context); 42 | } 43 | return staticInstance; 44 | } 45 | 46 | private ImageLoader(final Context context) { 47 | fileLoaderThread = FileLoaderThread.getInstance(); 48 | fileLoaderThread.start(); 49 | checkTimestampThread = CheckTimestampThread.getInstance(); 50 | checkTimestampThread.start(); 51 | downloadThreadPool = DownloadThreadPool.getInstance(); 52 | downloadThreadPool.start(context); 53 | requestRouterThread = RequestRouterThread.getInstance(); 54 | requestRouterThread.start(); 55 | } 56 | 57 | public static void load(final Context context, URL imageUrl, ImageRequest.Listener listener, BitmapFactory.Options options) { 58 | final ImageLoader instance = getInstance(context); 59 | instance.requestRouterThread.addTask(new ImageRequest(context, imageUrl, listener, options)); 60 | } 61 | 62 | public static void cancelAllRequests() { 63 | final ImageLoader imageLoader = getInstance(null); 64 | imageLoader.requestRouterThread.cancelAllRequests(); 65 | imageLoader.fileLoaderThread.cancelAllRequests(); 66 | imageLoader.checkTimestampThread.cancelAllRequests(); 67 | imageLoader.downloadThreadPool.cancelAllRequests(); 68 | } 69 | 70 | public static void shutdown() { 71 | LogWrapper.logMessage("Shutting down"); 72 | final ImageLoader imageLoader = getInstance(null); 73 | imageLoader.requestRouterThread.shutdown(); 74 | RequestRouterThread.staticInstance = null; 75 | imageLoader.fileLoaderThread.shutdown(); 76 | FileLoaderThread.staticInstance = null; 77 | imageLoader.checkTimestampThread.shutdown(); 78 | CheckTimestampThread.staticInstance = null; 79 | imageLoader.downloadThreadPool.shutdown(); 80 | DownloadThreadPool.staticInstance = null; 81 | staticInstance = null; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /README-OLD.md: -------------------------------------------------------------------------------- 1 | WebImage for Android 2 | ==================== 3 | 4 | WebImage is an Android library which asynchronously downloads images from the 5 | internet and caches them for quick access. It has the following features: 6 | 7 | - Images are downloaded in a background thread pool, keeping the GUI thread 8 | responsive. The size of the thread pool is based on the device's network 9 | speed, with more threads for faster connection types. 10 | - Separate background thread for loading cached images from disk, which 11 | reduces I/O bottlenecks on most Android phones. 12 | - Images are cached to SD card, or if unavailable, to the app's internal 13 | storage cache. 14 | - Images saved in the cache are periodically checked to see if they have 15 | expired and must be re-downloaded. This query only fetches HTTP headers and 16 | is done in a separate low-priority thread. 17 | - Good performance, even on older Android 2.1 devices. 18 | - Compatible with API level 7 and up. 19 | - No additional library or framework dependencies! 20 | - Builds quickly and easily with ant, or simply drop the tiny jarfile to your 21 | project's libs directory. 22 | 23 | 24 | Project History 25 | --------------- 26 | 27 | WebImage was written for [Wrapp](http://www.wrapp.com) for use in our [Android 28 | client](http://market.android.com/details?id=com.wrapp.android) in the summer 29 | of 2011 by Nik Reiman. We needed a library capable of handling a large number 30 | of asynchronous image downloads from the web without effecting the GUI, and 31 | with the exception of [droid-fu](https://github.com/kaeppler/droid-fu) the 32 | only code to do this exists in the form of StackOverflow answers. 33 | 34 | This seemed to be a common wheel which many Android developers continue to 35 | re-invent, so we broke off some of our image handling code into a separate, 36 | modular library. Hopefully other developers will find it useful in their 37 | projects. 38 | 39 | 40 | Installation 41 | ------------ 42 | 43 | To use WebImage in your Android application, simply download the latest 44 | [WebImage.jar](https://github.com/wrapp/WebImage-Android/downloads) file and 45 | copy it to your project's "libs" directory. You may need to configure your IDE 46 | to include jarfiles from this directory if you have not already done so. 47 | 48 | Your project will need to declare the following permissions in the 49 | AndroidManifest file: 50 | 51 | 52 | 53 | 54 | 55 | 56 | To actually download images, use the included `WebImageView` class, which is a 57 | subclass of `ImageView` and has a method named `setImageUrl()`. This class can 58 | be used either from XML layout files or in your classes. Subclassing this 59 | class is a good way to provide extra functionality, such as rescaling the 60 | download images or showing progress spinners while the image is loading. 61 | 62 | Although you can subclass `WebImageView` to provide post-processing on the 63 | image (which is done outside of the GUI thread), you can also fetch raw 64 | bitmaps asynchronously via `WebImage.load()`. This approach is not usually 65 | recommended, but may be preferable in some cases. 66 | 67 | 68 | Configuration 69 | ------------- 70 | 71 | During your application's initialization, you can configure WebImage's 72 | caching, threading, and logging behavior. WebImage tries to use sane defaults, 73 | so in most cases you should not need to change these behaviors, but if you do, 74 | you must do so before attempting to fetch any images. 75 | 76 | By default, WebImage is designed to work best in an environment where a large 77 | number of images are being requested from the internet, like in a ListView 78 | where each item is an image from the web. If your application only needs to 79 | fetch a single image every now and then, you should probably set the maximum 80 | number of worker threads to 1. 81 | 82 | For more details, check out the source code, which is also bundled in the 83 | distribution jarfile. WebImage also has includes [a demo application] 84 | (https://github.com/wrapp/WebImage-Android/tree/master/examples) 85 | which shows how the library can be used in your project. 86 | 87 | 88 | Licensing 89 | --------- 90 | 91 | WebImage is licensed under the MIT license. See the file LICENSE.txt for more 92 | details. 93 | 94 | -------------------------------------------------------------------------------- /examples/WebImageList/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 24 | 25 | 29 | 30 | 31 | 48 | 49 | 50 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 63 | 75 | 76 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/com/wrapp/android/webimage/TaskQueueThread.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Bohemian Wrappsody AB 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package com.wrapp.android.webimage; 23 | 24 | import android.graphics.Bitmap; 25 | 26 | import java.util.*; 27 | 28 | public abstract class TaskQueueThread extends Thread { 29 | private static final long SHUTDOWN_TIMEOUT_IN_MS = 100; 30 | private final Queue pendingRequests; 31 | private boolean isRunning; 32 | 33 | protected abstract Bitmap processRequest(ImageRequest request); 34 | protected abstract void onRequestComplete(RequestResponse response); 35 | protected abstract void onRequestCancelled(ImageRequest request); 36 | 37 | public TaskQueueThread(final String taskName) { 38 | super(taskName); 39 | pendingRequests = new LinkedList(); 40 | } 41 | 42 | @Override 43 | public void run() { 44 | LogWrapper.logMessage("Starting up task " + getName()); 45 | ImageRequest request; 46 | isRunning = true; 47 | while(isRunning) { 48 | synchronized(pendingRequests) { 49 | while(pendingRequests.isEmpty() && isRunning) { 50 | try { 51 | pendingRequests.wait(); 52 | } 53 | catch(InterruptedException e) { 54 | isRunning = false; 55 | break; 56 | } 57 | } 58 | 59 | try { 60 | request = getNextRequest(pendingRequests); 61 | } 62 | catch(Exception e) { 63 | continue; 64 | } 65 | } 66 | 67 | try { 68 | if(request != null && request.listener != null) { 69 | try { 70 | Bitmap bitmap = processRequest(request); 71 | synchronized(pendingRequests) { 72 | if(isRequestStillValid(request, pendingRequests)) { 73 | if(bitmap != null) { 74 | onRequestComplete(new RequestResponse(bitmap, request)); 75 | } 76 | } 77 | else { 78 | LogWrapper.logMessage("Bitmap request is no longer valid: " + request.imageUrl); 79 | onRequestCancelled(request); 80 | } 81 | } 82 | } 83 | catch(Exception e) { 84 | request.listener.onBitmapLoadError(e.getMessage()); 85 | } 86 | } 87 | } 88 | catch(Exception e) { 89 | LogWrapper.logException(e); 90 | } 91 | } 92 | 93 | LogWrapper.logMessage("Shutting down task " + getName()); 94 | } 95 | 96 | @Override 97 | public void interrupt() { 98 | super.interrupt(); 99 | isRunning = false; 100 | } 101 | 102 | public void addTask(ImageRequest request) { 103 | synchronized(pendingRequests) { 104 | pendingRequests.add(request); 105 | pendingRequests.notifyAll(); 106 | } 107 | } 108 | 109 | private ImageRequest getNextRequest(Queue requestQueue) { 110 | // Pop the first element from the pending request queue 111 | ImageRequest request = requestQueue.poll(); 112 | if(request.listener == null) { 113 | return request; 114 | } 115 | 116 | // Go through the list of pending requests, pruning duplicate requests and using the latest URL 117 | // requested by a particular listener. It is quite common that a listener will request multiple 118 | // URL's, especially when a ListView is scrolling quickly. 119 | Iterator requestIterator = requestQueue.iterator(); 120 | while(requestIterator.hasNext()) { 121 | ImageRequest checkRequest = (ImageRequest)requestIterator.next(); 122 | if(request.listener.equals(checkRequest.listener)) { 123 | if(request.imageUrl.equals(checkRequest.imageUrl)) { 124 | // Ignore duplicate requests. This is common when doing view recycling in list adapters. 125 | request.listener.onBitmapLoadCancelled(); 126 | requestIterator.remove(); 127 | } 128 | else { 129 | // If this request in the queue was made by the same listener but is for a new URL, 130 | // then use that request instead and remove it from the queue. 131 | request.listener.onBitmapLoadCancelled(); 132 | request = checkRequest; 133 | requestIterator.remove(); 134 | } 135 | } 136 | } 137 | 138 | return request; 139 | } 140 | 141 | private boolean isRequestStillValid(ImageRequest finishedRequest, Queue requestQueue) { 142 | for(ImageRequest checkRequest : requestQueue) { 143 | if(finishedRequest.listener.equals(checkRequest.listener) && 144 | !finishedRequest.imageUrl.equals(checkRequest.imageUrl)) { 145 | return false; 146 | } 147 | } 148 | return true; 149 | } 150 | 151 | public void cancelAllRequests() { 152 | synchronized(pendingRequests) { 153 | for(ImageRequest request : pendingRequests) { 154 | request.listener.onBitmapLoadCancelled(); 155 | request.listener = null; 156 | } 157 | pendingRequests.clear(); 158 | } 159 | } 160 | 161 | public void shutdown() { 162 | try { 163 | interrupt(); 164 | join(SHUTDOWN_TIMEOUT_IN_MS); 165 | } 166 | catch(InterruptedException e) { 167 | LogWrapper.logException(e); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/com/wrapp/android/webimage/DownloadThreadPool.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Bohemian Wrappsody AB 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package com.wrapp.android.webimage; 23 | 24 | import android.content.BroadcastReceiver; 25 | import android.content.Context; 26 | import android.content.Intent; 27 | import android.content.IntentFilter; 28 | import android.net.ConnectivityManager; 29 | import android.net.NetworkInfo; 30 | import android.os.Build; 31 | 32 | public class DownloadThreadPool { 33 | // These don't seem to be declared in the Android SDK. Or did I just not look hard enough? 34 | private static final int CONNECTION_TYPE_MOBILE = 0; 35 | private static final int CONNECTION_TYPE_WIFI = 1; 36 | private static final int CONNECTION_TYPE_ETHERNET = 9; 37 | private static final int DEFAULT_MAX_THREADS = 4; 38 | 39 | static DownloadThreadPool staticInstance; 40 | private static int maxThreads = DEFAULT_MAX_THREADS; 41 | private DownloadThread[] downloadThreads; 42 | private int numActiveThreads = 0; 43 | private int currentThread = 0; 44 | 45 | public static class ConnectivityChangeReceiver extends BroadcastReceiver { 46 | @Override 47 | public void onReceive(Context context, Intent intent) { 48 | DownloadThreadPool.resizeThreadPool(context); 49 | } 50 | 51 | public static IntentFilter getIntentFilter() { 52 | return new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); 53 | } 54 | } 55 | 56 | public static DownloadThreadPool getInstance() { 57 | if(staticInstance == null) { 58 | staticInstance = new DownloadThreadPool(); 59 | } 60 | return staticInstance; 61 | } 62 | 63 | public static void setMaxThreads(int value) { 64 | maxThreads = value; 65 | } 66 | 67 | private DownloadThreadPool() { 68 | downloadThreads = new DownloadThread[maxThreads]; 69 | for(int i = 0; i < maxThreads; i++) { 70 | downloadThreads[i] = new DownloadThread(); 71 | } 72 | } 73 | 74 | public void addTask(ImageRequest request) { 75 | DownloadThreadPool downloadThreadPool = getInstance(); 76 | // If there is only one thread in the worker pool, issue the request right away 77 | if(downloadThreadPool.numActiveThreads == 1) { 78 | downloadThreads[0].addTask(request); 79 | } 80 | else { 81 | // Rotate to the next thread 82 | downloadThreadPool.currentThread++; 83 | if(downloadThreadPool.currentThread >= downloadThreadPool.numActiveThreads) { 84 | downloadThreadPool.currentThread = 0; 85 | } 86 | downloadThreads[downloadThreadPool.currentThread].addTask(request); 87 | } 88 | } 89 | 90 | public void start(Context context) { 91 | for(int i = 0; i < maxThreads; i++) { 92 | downloadThreads[i].start(); 93 | } 94 | numActiveThreads = getBestThreadPoolSize(context); 95 | } 96 | 97 | public static void resizeThreadPool(Context context) { 98 | final DownloadThreadPool downloadThreadPool = getInstance(); 99 | // Check if the thread pool has not been initialized 100 | if(downloadThreadPool.numActiveThreads == 0) { 101 | return; 102 | } 103 | 104 | downloadThreadPool.currentThread = 0; 105 | downloadThreadPool.numActiveThreads = downloadThreadPool.getBestThreadPoolSize(context); 106 | } 107 | 108 | private int getBestThreadPoolSize(final Context context) { 109 | // Android 2.1 devices are never going to be that fast even in the very best case, so only use 110 | // a single downloader thread for them 111 | if(Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) { 112 | return 1; 113 | } 114 | 115 | try { 116 | // Try to see network information to determine the ideal thread pool size. 117 | ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); 118 | NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); 119 | if(networkInfo != null) { 120 | final int type = networkInfo.getType(); 121 | switch(type) { 122 | case CONNECTION_TYPE_MOBILE: 123 | // Connection subtype will return integer respective for 1G, 2G, 3G 124 | // For 3G connections and better, we should use up to half the max pool size. 125 | if(networkInfo.getSubtype() >= 3) { 126 | return maxThreads / 2; 127 | } 128 | // For all other cases, just use one thread. EDGE/2G is slow pretty much everywhere. 129 | else { 130 | return 1; 131 | } 132 | // For WIFI, use the entire available thread pool 133 | case CONNECTION_TYPE_WIFI: 134 | return maxThreads; 135 | // Yeah, this looks weird, but there are Android devices which support this (like Android-x86). 136 | case CONNECTION_TYPE_ETHERNET: 137 | return maxThreads; 138 | } 139 | } 140 | } 141 | catch(SecurityException e) { 142 | LogWrapper.logMessage("Could not determine network type, need permission for ACCESS_NETWORK_STATE in app's manifest"); 143 | } 144 | catch(Exception e) { 145 | LogWrapper.logMessage("Could not determine network connection type"); 146 | } 147 | 148 | // If we couldn't figure out a good pool size, then just use one thread to be safe. 149 | return 1; 150 | } 151 | 152 | public void cancelAllRequests() { 153 | for(int i = 0; i < maxThreads; i++) { 154 | downloadThreads[i].cancelAllRequests(); 155 | } 156 | } 157 | 158 | public void shutdown() { 159 | for(int i = 0; i < maxThreads; i++) { 160 | downloadThreads[i].shutdown(); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /WebImage.ipr: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 35 | 36 | 37 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 51 | 52 | 53 | 54 | 55 | 56 | 58 | 59 | 60 | 61 | 69 | 70 | 74 | 75 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | http://www.w3.org/1999/xhtml 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /src/com/wrapp/android/webimage/WebImage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Bohemian Wrappsody AB 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package com.wrapp.android.webimage; 23 | 24 | import android.content.Context; 25 | import android.graphics.BitmapFactory; 26 | 27 | import java.net.URL; 28 | 29 | /** Endpoint class for all main library tasks. */ 30 | @SuppressWarnings({"UnusedDeclaration"}) 31 | public class WebImage { 32 | // Loading Images //////////////////////////////////////////////////////////////////////////////////////////////////// 33 | 34 | /** 35 | * Load an image from URL to the given listener. This is a non-blocking call which is run in 36 | * a background thread. It is safe to call this method multiple times; duplicate requests will 37 | * be ignored. 38 | * @param context Context used for getting app's package name 39 | * @param imageUrl URL to load the image from 40 | * @param listener Object which will be notified when the request is complete 41 | */ 42 | public static void load(final Context context, URL imageUrl, ImageRequest.Listener listener) { 43 | ImageLoader.load(context, imageUrl, listener, null); 44 | } 45 | 46 | /** 47 | * Load an image from URL to the given listener. This is a non-blocking call which is run in 48 | * a background thread. It is safe to call this method multiple times; duplicate requests will 49 | * be ignored. 50 | * @param context Context used for getting app's package name 51 | * @param imageUrl URL to load the image from 52 | * @param listener Object which will be notified when the request is complete 53 | * @param options Options to use when loading the image. See the documentation for {@link BitmapFactory.Options} 54 | * for more details. Can be null. 55 | */ 56 | public static void load(final Context context, URL imageUrl, ImageRequest.Listener listener, BitmapFactory.Options options) { 57 | ImageLoader.load(context, imageUrl, listener, options); 58 | } 59 | 60 | // Image Cache Operations //////////////////////////////////////////////////////////////////////////////////////////// 61 | 62 | /** 63 | * Check to see if an image has already been saved in the file cache. This can be useful when 64 | * you want to display an animation or other GUI notification in case the image has to be 65 | * fetched from the net. 66 | * @param context Context used for getting app's package name 67 | * @param imageUrl URL to check 68 | * @return True if the image is in the file cache, false otherwise 69 | */ 70 | public static boolean isImageCached(final Context context, URL imageUrl) { 71 | return ImageCache.isImageCached(context, ImageCache.getCacheKeyForUrl(imageUrl)); 72 | } 73 | 74 | /** 75 | * Remove old files from the file cache. The parent application should call this method once during 76 | * initialization to prevent the file cache from growing too large. 77 | * @param context Context used for getting app's package name 78 | */ 79 | public static void clearOldCacheFiles(final Context context) { 80 | ImageCache.clearOldCacheFiles(context); 81 | } 82 | 83 | /** 84 | * Remove cached files older than this many seconds from the file cache. Call with 0 to remove all 85 | * files in the cache. 86 | * @param context Context used for getting app's package name 87 | * @param cacheAgeInSec Maximum age of file, in seconds 88 | */ 89 | public static void clearOldCacheFiles(final Context context, long cacheAgeInSec) { 90 | ImageCache.clearOldCacheFiles(context, cacheAgeInSec); 91 | } 92 | 93 | /** 94 | * Remove a single image from the disk cache 95 | * @param context Context used for getting app's package name 96 | * @param imageUrl Image URL to remove 97 | */ 98 | public static void clearImageFromCaches(final Context context, final URL imageUrl) { 99 | ImageCache.clearImageFromCaches(context, imageUrl); 100 | } 101 | 102 | // Configuration ///////////////////////////////////////////////////////////////////////////////////////////////////// 103 | 104 | /** 105 | * By default, the WebImage library is silent and will not produce any output to the console. During 106 | * debugging you may wish to call this method in your app's initialization method to see debugging 107 | * output to the logcat. 108 | * @param tag Android logging tag to use 109 | * @param level Android log level to use 110 | */ 111 | public static void enableLogging(String tag, int level) { 112 | LogWrapper.enableLogging(tag, level); 113 | } 114 | 115 | /** 116 | * Set the maximum number of threads to be used for downloading images. The actual number of download 117 | * threads varies depending on the phone's network connection. Smaller apps which only load a few 118 | * images may want to set this value to 1. 119 | * Note that this does not effect the total number of threads started by WebImage; there will still 120 | * be other background threads for reading cached images, checking timestamps, etc. 121 | * @param value Number of threads 122 | */ 123 | public static void setMaxDownloadThreads(int value) { 124 | DownloadThreadPool.setMaxThreads(value); 125 | } 126 | 127 | // Thread Control Operations ///////////////////////////////////////////////////////////////////////////////////////// 128 | 129 | /** 130 | * Cancel all pending requests. The parent activity should call this method when it is about 131 | * to be stopped or paused, or else you will waste resources by running in the background. 132 | */ 133 | public static void cancelAllRequests() { 134 | ImageLoader.cancelAllRequests(); 135 | } 136 | 137 | /** 138 | * Call this method to manually force a resize check for the download thread pool size. Normally 139 | * the preferred way of doing this is to instead use the BroadcastReceiver provided by the 140 | * DownloadThreadPool class (see the example app for a demonstration of this). 141 | * However, you may also wish to manually call this, for instance when your application is resumed 142 | * and any network changes may not have been caught by your app. 143 | * @param context Activity's context 144 | */ 145 | public static void onNetworkStatusChanged(Context context) { 146 | DownloadThreadPool.resizeThreadPool(context); 147 | } 148 | 149 | /** 150 | * Stop all background threads. Call this when your application quits. Can also be called when 151 | * the app is paused to free up additional resources. Note that the next request to load an 152 | * image will re-inialize the thread pool. 153 | */ 154 | public static void shutdown() { 155 | ImageLoader.shutdown(); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /examples/WebImageList/src/com/wrapp/android/webimagelist/WebImageListActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Bohemian Wrappsody AB 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package com.wrapp.android.webimagelist; 23 | 24 | import android.app.ActivityManager; 25 | import android.app.ListActivity; 26 | import android.content.BroadcastReceiver; 27 | import android.content.Context; 28 | import android.os.Bundle; 29 | import android.os.Debug; 30 | import android.os.Handler; 31 | import android.util.Log; 32 | import android.view.Menu; 33 | import android.view.MenuInflater; 34 | import android.view.MenuItem; 35 | import android.view.Window; 36 | import android.widget.Toast; 37 | import com.wrapp.android.webimage.*; 38 | 39 | public class WebImageListActivity extends ListActivity implements WebImageView.Listener { 40 | // Don't show the progress spinner right away, because when scrolling rapidly through the list 41 | // of images, there will get a bunch of callbacks which may cause the progress spinner to flicker 42 | // as it is rapidly shown and hidden. Imposing a small delay here will show the spinner only when 43 | // images take more than a few milliseconds to load, ie, from the network and not from disk/memory. 44 | private static final long SHOW_PROGRESS_DELAY_IN_MS = 100; 45 | 46 | // Handler used to post to the GUI thread. This is important, because WebImageView.Listener 47 | // callbacks are posted to the background thread, so if we want to update the GUI it must 48 | // be with a handler. If no handler is used, there will be a bunch of "Only the original thread 49 | // that created a view hierarchy can touch its views" exceptions. 50 | private Handler uiHandler; 51 | 52 | // Runnable which is called in case of error, image cancelled, or image loaded 53 | private Runnable stopTaskRunnable; 54 | 55 | // Counter to keep track of number of running image tasks. Note that this must be an Integer 56 | // rather than an int so that it can be synchronized and thus more threadsafe. 57 | private Integer numTasks = 0; 58 | 59 | // Used when toggling cache recheck time from menu 60 | private long defaultCacheRecheckAgeInMs = ImageCache.getCacheRecheckAgeInMs(); 61 | private BroadcastReceiver connectivityChangeReceiver; 62 | 63 | @Override 64 | public void onCreate(Bundle savedInstanceState) { 65 | super.onCreate(savedInstanceState); 66 | requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 67 | setContentView(R.layout.web_image_activity); 68 | 69 | // Remove all images from the cache when starting up 70 | WebImage.clearOldCacheFiles(this); 71 | // Calling the above method with an extra argument of 0 will force all images to be removed from the cache 72 | // WebImage.clearOldCacheFiles(this, 0); 73 | 74 | // Turn on logging so we can see what is going on. 75 | WebImage.enableLogging("WebImageList", Log.DEBUG); 76 | 77 | // Create handler, runnable used when stopping tasks. 78 | uiHandler = new Handler(); 79 | stopTaskRunnable = new Runnable() { 80 | public void run() { 81 | onTaskStopped(); 82 | } 83 | }; 84 | 85 | // Create a list adapter and attach it to the ListView 86 | WebImageListAdapter listAdapter = new WebImageListAdapter(this); 87 | setListAdapter(listAdapter); 88 | } 89 | 90 | @Override 91 | protected void onResume() { 92 | super.onResume(); 93 | connectivityChangeReceiver = new DownloadThreadPool.ConnectivityChangeReceiver(); 94 | registerReceiver(connectivityChangeReceiver, DownloadThreadPool.ConnectivityChangeReceiver.getIntentFilter()); 95 | } 96 | 97 | /** 98 | * If your activity plans on loading a lot of images, you should call WebImage.cancelAllRequests() 99 | * before going into the background. Otherwise, you risk wasting CPU time and bandwidth. 100 | */ 101 | @Override 102 | protected void onPause() { 103 | super.onPause(); 104 | WebImage.cancelAllRequests(); 105 | unregisterReceiver(connectivityChangeReceiver); 106 | } 107 | 108 | @Override 109 | protected void onStop() { 110 | super.onStop(); 111 | LogWrapper.logMessage("Stopping activity"); 112 | WebImage.shutdown(); 113 | } 114 | 115 | @Override 116 | public boolean onCreateOptionsMenu(Menu menu) { 117 | MenuInflater menuInflater = new MenuInflater(this); 118 | menuInflater.inflate(R.menu.main_menu, menu); 119 | return true; 120 | } 121 | 122 | @Override 123 | public boolean onOptionsItemSelected(MenuItem item) { 124 | switch(item.getItemId()) { 125 | case R.id.MainMenuRefreshItem: 126 | refresh(); 127 | break; 128 | case R.id.MainMenuClearCachesItem: 129 | WebImage.cancelAllRequests(); 130 | WebImage.clearOldCacheFiles(this, 0); 131 | Toast toast = Toast.makeText(this, "Caches cleared", Toast.LENGTH_SHORT); 132 | toast.show(); 133 | refresh(); 134 | break; 135 | case R.id.MainMenuForceTimestampChecks: 136 | toggleForceTimestampChecks(); 137 | break; 138 | case R.id.MainMenuShowMemoryUse: 139 | showMemoryUsageToast(); 140 | break; 141 | case R.id.MainMenuRestrictMemoryUse: 142 | toggleRestrictMemoryUsage(); 143 | break; 144 | default: 145 | refresh(); 146 | break; 147 | } 148 | 149 | return true; 150 | } 151 | 152 | private void refresh() { 153 | final WebImageListAdapter listAdapter = (WebImageListAdapter)getListAdapter(); 154 | listAdapter.notifyDataSetChanged(); 155 | } 156 | 157 | public void onImageLoadStarted() { 158 | uiHandler.postDelayed(new Runnable() { 159 | public void run() { 160 | onTaskStarted(); 161 | } 162 | }, SHOW_PROGRESS_DELAY_IN_MS); 163 | } 164 | 165 | // Start and stop the progress spinner in the activity's top bar 166 | 167 | public void onImageLoadComplete() { 168 | uiHandler.post(stopTaskRunnable); 169 | } 170 | 171 | public void onImageLoadError() { 172 | uiHandler.post(stopTaskRunnable); 173 | } 174 | 175 | public void onImageLoadCancelled() { 176 | uiHandler.post(stopTaskRunnable); 177 | } 178 | 179 | private void onTaskStarted() { 180 | synchronized(numTasks) { 181 | if(numTasks == 0) { 182 | setProgressBarIndeterminateVisibility(true); 183 | } 184 | numTasks++; 185 | } 186 | } 187 | 188 | private void onTaskStopped() { 189 | synchronized(numTasks) { 190 | numTasks--; 191 | if(numTasks == 0) { 192 | setProgressBarIndeterminateVisibility(false); 193 | } 194 | } 195 | } 196 | 197 | private void toggleRestrictMemoryUsage() { 198 | final WebImageListAdapter webImageListAdapter = (WebImageListAdapter)getListAdapter(); 199 | final boolean shouldRestrictMemoryUsage = !webImageListAdapter.getShouldRestrictMemoryUsage(); 200 | webImageListAdapter.setShouldRestrictMemoryUsage(shouldRestrictMemoryUsage); 201 | WebImage.cancelAllRequests(); 202 | WebImage.clearOldCacheFiles(this, 0); 203 | final String toastMessage = "Restrict memory usage: " + (shouldRestrictMemoryUsage ? "enabled" : "disabled"); 204 | Toast toast = Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT); 205 | toast.show(); 206 | refresh(); 207 | } 208 | 209 | private void toggleForceTimestampChecks() { 210 | String toastMessage = "Force timestamp checks: "; 211 | if(ImageCache.getCacheRecheckAgeInMs() == 1) { 212 | ImageCache.setCacheRecheckAgeInMs(defaultCacheRecheckAgeInMs); 213 | toastMessage += "disabled"; 214 | } 215 | else { 216 | ImageCache.setCacheRecheckAgeInMs(1); 217 | toastMessage += "enabled"; 218 | } 219 | Toast toast = Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT); 220 | toast.show(); 221 | refresh(); 222 | } 223 | 224 | public void showMemoryUsageToast() { 225 | final int heapKbAllocated = (int)(Debug.getNativeHeapAllocatedSize() / 1024); 226 | final int heapKbTotal = (int)(Debug.getNativeHeapSize() / 1024); 227 | final int heapPercent = (int)(100.0f * heapKbAllocated / heapKbTotal); 228 | ActivityManager activityManager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE); 229 | final int memoryClass = activityManager.getMemoryClass(); 230 | final String toastMessage = "Heap: " + heapKbAllocated + "K used (" + heapPercent + "%)\nMemory class: " + memoryClass; 231 | Toast toast = Toast.makeText(this, toastMessage, Toast.LENGTH_LONG); 232 | toast.show(); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/com/wrapp/android/webimage/ImageCache.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Bohemian Wrappsody AB 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package com.wrapp.android.webimage; 23 | 24 | import android.content.Context; 25 | import android.os.Environment; 26 | 27 | import java.io.File; 28 | import java.net.URL; 29 | import java.security.MessageDigest; 30 | import java.security.NoSuchAlgorithmException; 31 | import java.util.Date; 32 | 33 | public class ImageCache { 34 | private static final long ONE_DAY_IN_SEC = 24 * 60 * 60; 35 | private static final long CACHE_RECHECK_AGE_IN_SEC = ONE_DAY_IN_SEC; 36 | private static final long CACHE_RECHECK_AGE_IN_MS = CACHE_RECHECK_AGE_IN_SEC * 1000; 37 | private static final long CACHE_EXPIRATION_AGE_IN_SEC = ONE_DAY_IN_SEC * 30; 38 | private static final String DEFAULT_CACHE_SUBDIRECTORY_NAME = "images"; 39 | 40 | private static File cacheDirectory; 41 | private static long cacheRecheckAgeInMs = CACHE_RECHECK_AGE_IN_MS; 42 | 43 | public static boolean isImageCached(Context context, String imageKey) { 44 | final File cacheFile = new File(getCacheDirectory(context), imageKey); 45 | return cacheFile.exists(); 46 | } 47 | 48 | public static long getCacheRecheckAgeInMs() { 49 | return cacheRecheckAgeInMs; 50 | } 51 | 52 | public static void setCacheRecheckAgeInMs(long cacheRecheckAgeInMs) { 53 | ImageCache.cacheRecheckAgeInMs = cacheRecheckAgeInMs; 54 | } 55 | 56 | public static File getCacheDirectory(final Context context) { 57 | boolean canWriteToExternalStorage = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); 58 | if(!canWriteToExternalStorage) { 59 | LogWrapper.logMessage("Can't write to external storage, using app's internal cache"); 60 | cacheDirectory = null; 61 | return getInternalCacheDirectory(context); 62 | } 63 | 64 | if(cacheDirectory == null) { 65 | setCacheDirectory(context, DEFAULT_CACHE_SUBDIRECTORY_NAME); 66 | } 67 | return cacheDirectory; 68 | } 69 | 70 | public static void setCacheDirectory(Context context, String subdirectoryName) { 71 | // Final destination is Android/data/com.packagename/cache/subdirectory 72 | final File androidDirectory = new File(android.os.Environment.getExternalStorageDirectory(), "Android"); 73 | if(!androidDirectory.exists()) { 74 | androidDirectory.mkdir(); 75 | } 76 | 77 | final File dataDirectory = new File(androidDirectory, "data"); 78 | if(!dataDirectory.exists()) { 79 | dataDirectory.mkdir(); 80 | } 81 | 82 | final File packageDirectory = new File(dataDirectory, context.getPackageName()); 83 | if(!packageDirectory.exists()) { 84 | packageDirectory.mkdir(); 85 | } 86 | 87 | final File packageCacheDirectory = new File(packageDirectory, "cache"); 88 | if(!packageCacheDirectory.exists()) { 89 | packageCacheDirectory.mkdir(); 90 | } 91 | 92 | cacheDirectory = new File(packageCacheDirectory, subdirectoryName); 93 | if(!cacheDirectory.exists()) { 94 | cacheDirectory.mkdir(); 95 | } 96 | 97 | LogWrapper.logMessage("Cache directory is " + cacheDirectory.toString()); 98 | 99 | // WebImage versions prior to 1.2.2 stored images in /mnt/sdcard/data/packageName. If images are found 100 | // there, we should migrate them to the correct location. Unfortunately, WebImage 1.1.2 and below also 101 | // used the location /mnt/sdcard/data/images if no packageName was supplied. Since this isn't very 102 | // specific, we don't bother to remove those images, as they may belong to other applications. 103 | final File oldDataDirectory = new File(android.os.Environment.getExternalStorageDirectory(), "data"); 104 | final File oldPackageDirectory = new File(oldDataDirectory, context.getPackageName()); 105 | final File oldCacheDirectory = new File(oldPackageDirectory, subdirectoryName); 106 | if(oldCacheDirectory.exists()) { 107 | if(cacheDirectory.delete()) { 108 | if(!oldCacheDirectory.renameTo(cacheDirectory)) { 109 | LogWrapper.logMessage("Could not migrate old cache directory from " + oldCacheDirectory.toString()); 110 | cacheDirectory.mkdir(); 111 | } 112 | else { 113 | LogWrapper.logMessage("Finished migrating <1.2.2 cache directory"); 114 | } 115 | } 116 | } 117 | else { 118 | // WebImage versions prior to 1.6.0 stored the subdirectory directly under the package name, avoiding 119 | // the intermediate cache directory. Migrate these images if this is the case. 120 | if(!subdirectoryName.equals("cache")) { 121 | final File oldSubdirectory = new File(packageDirectory, subdirectoryName); 122 | if(oldSubdirectory.exists()) { 123 | if(cacheDirectory.delete()) { 124 | if(!oldSubdirectory.renameTo(cacheDirectory)) { 125 | LogWrapper.logMessage("Could not migrate old cache directory from " + oldSubdirectory.toString()); 126 | cacheDirectory.mkdir(); 127 | } 128 | else { 129 | LogWrapper.logMessage("Finished migrating <1.6.0 cache directory"); 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | 137 | public static File getInternalCacheDirectory(Context context) { 138 | File internalCacheDirectory = new File(context.getCacheDir(), "temp-images"); 139 | if(!internalCacheDirectory.exists()) { 140 | if(!internalCacheDirectory.mkdir()) { 141 | LogWrapper.logMessage("Failed creating temporary storage directory, this is probably not good"); 142 | } 143 | } 144 | return internalCacheDirectory; 145 | } 146 | 147 | public static void clearImageFromCaches(final Context context, final URL imageUrl) { 148 | String imageKey = getCacheKeyForUrl(imageUrl); 149 | final File cacheFile = new File(getCacheDirectory(context), imageKey); 150 | if(cacheFile.exists()) { 151 | if(!cacheFile.delete()) { 152 | LogWrapper.logMessage("Could not remove cached version of image " + imageUrl); 153 | } 154 | } 155 | } 156 | 157 | /** 158 | * Clear expired images in the file cache to save disk space. This method will remove all 159 | * images older than {@link #CACHE_EXPIRATION_AGE_IN_SEC} seconds. 160 | * @param context Context used for getting app's package name 161 | */ 162 | public static void clearOldCacheFiles(final Context context) { 163 | clearOldCacheFiles(context, CACHE_EXPIRATION_AGE_IN_SEC); 164 | } 165 | 166 | /** 167 | * Clear all images older than a given amount of seconds. 168 | * @param context Context used for getting app's package name 169 | * @param cacheAgeInSec Image expiration limit, in seconds 170 | */ 171 | public static void clearOldCacheFiles(final Context context, long cacheAgeInSec) { 172 | // Clear all files from the temporary cache if external storage is available 173 | // TODO: This could technically be moved to external storage, but whatever 174 | final File internalCacheDirectory = getInternalCacheDirectory(context); 175 | String[] cacheFiles = internalCacheDirectory.list(); 176 | if(cacheFiles != null) { 177 | for(String child : cacheFiles) { 178 | File childFile = new File(internalCacheDirectory, child); 179 | LogWrapper.logMessage("Deleting image '" + child + "' from internal cache"); 180 | childFile.delete(); 181 | } 182 | } 183 | 184 | final long cacheAgeInMs = cacheAgeInSec * 1000; 185 | Date now = new Date(); 186 | final File externalCacheDirectory = getCacheDirectory(context); 187 | cacheFiles = externalCacheDirectory.list(); 188 | if(cacheFiles != null) { 189 | for(String child : cacheFiles) { 190 | File childFile = new File(externalCacheDirectory, child); 191 | if(childFile.isFile()) { 192 | long fileAgeInMs = now.getTime() - childFile.lastModified(); 193 | if(fileAgeInMs > cacheAgeInMs) { 194 | LogWrapper.logMessage("Deleting image '" + child + "' from external cache"); 195 | childFile.delete(); 196 | } 197 | } 198 | } 199 | } 200 | } 201 | 202 | private static final char[] HEX_CHARACTERS = { 203 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' 204 | }; 205 | 206 | /** 207 | * Calculate a hash key for the given URL, which is used to create safe filenames and 208 | * key strings. Internally, this method uses MD5, as that is available on Android 2.1 209 | * devices (unlike base64, for example). 210 | * @param url Image URL 211 | * @return Hash for image URL 212 | */ 213 | public static String getCacheKeyForUrl(URL url) { 214 | String result = ""; 215 | 216 | try { 217 | String urlString = url.toString(); 218 | MessageDigest digest = MessageDigest.getInstance("MD5"); 219 | digest.update(urlString.getBytes(), 0, urlString.length()); 220 | byte[] resultBytes = digest.digest(); 221 | StringBuilder hexStringBuilder = new StringBuilder(2 * resultBytes.length); 222 | for(final byte b : resultBytes) { 223 | hexStringBuilder.append(HEX_CHARACTERS[(b & 0xf0) >> 4]).append(HEX_CHARACTERS[b & 0x0f]); 224 | } 225 | result = hexStringBuilder.toString(); 226 | } 227 | catch(NoSuchAlgorithmException e) { 228 | LogWrapper.logException(e); 229 | } 230 | 231 | return result; 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/com/wrapp/android/webimage/WebImageView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Bohemian Wrappsody AB 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package com.wrapp.android.webimage; 23 | 24 | import android.content.Context; 25 | import android.graphics.Bitmap; 26 | import android.graphics.BitmapFactory; 27 | import android.graphics.drawable.Drawable; 28 | import android.os.Handler; 29 | import android.util.AttributeSet; 30 | import android.widget.ImageView; 31 | 32 | import java.net.MalformedURLException; 33 | import java.net.URL; 34 | 35 | /** 36 | * ImageView successor class which can load images asynchronously from the web. This class 37 | * is safe to use in ListAdapters or views which may trigger many simultaneous requests. 38 | */ 39 | public class WebImageView extends ImageView implements ImageRequest.Listener { 40 | Handler uiHandler; 41 | private Listener listener; 42 | // Save both a Drawable and int here. If the user wants to pass a resource ID, we can load 43 | // this lazily and save a bit of memory. 44 | private Drawable errorImage; 45 | private int errorImageResId; 46 | private Drawable placeholderImage; 47 | private int placeholderImageResId; 48 | private URL loadedImageUrl; 49 | private URL pendingImageUrl; 50 | 51 | private enum States { 52 | EMPTY, 53 | LOADING, 54 | LOADED, 55 | RELOADING, 56 | CANCELLED, 57 | ERROR, 58 | } 59 | private States currentState = States.EMPTY; 60 | 61 | public interface Listener { 62 | public void onImageLoadStarted(); 63 | public void onImageLoadComplete(); 64 | public void onImageLoadError(); 65 | public void onImageLoadCancelled(); 66 | } 67 | 68 | @SuppressWarnings({"UnusedDeclaration"}) 69 | public WebImageView(Context context) { 70 | super(context); 71 | initialize(); 72 | } 73 | 74 | @SuppressWarnings({"UnusedDeclaration"}) 75 | public WebImageView(Context context, AttributeSet attrs) { 76 | super(context, attrs); 77 | initialize(); 78 | } 79 | 80 | @SuppressWarnings({"UnusedDeclaration"}) 81 | public WebImageView(Context context, AttributeSet attrs, int defStyle) { 82 | super(context, attrs, defStyle); 83 | initialize(); 84 | } 85 | 86 | private void initialize() { 87 | uiHandler = new Handler(); 88 | } 89 | 90 | /** 91 | * Set a listener to be informed about events from this view. If this is not set, then no events are sent. 92 | * @param listener Listener 93 | */ 94 | public void setListener(Listener listener) { 95 | this.listener = listener; 96 | } 97 | 98 | /** 99 | * Load an image asynchronously from the web 100 | * @param imageUrlString Image URL to download image from 101 | */ 102 | @SuppressWarnings("UnusedDeclaration") 103 | public void setImageUrl(String imageUrlString) { 104 | try { 105 | setImageUrl(new URL(imageUrlString)); 106 | } 107 | catch(MalformedURLException e) { 108 | LogWrapper.logException(e); 109 | } 110 | } 111 | 112 | /** 113 | * Load an image asynchronously from the web 114 | * @param imageUrl Image URL to download image from 115 | */ 116 | @SuppressWarnings("UnusedDeclaration") 117 | public void setImageUrl(URL imageUrl) { 118 | //noinspection NullableProblems 119 | setImageUrl(imageUrl, null, 0, 0); 120 | } 121 | 122 | /** 123 | * Load an image asynchronously from the web 124 | * @param imageUrl Image URL to download image from 125 | * @param options Options to use when loading the image. See the documentation for {@link BitmapFactory.Options} 126 | * for more details. Can be null. 127 | * @param errorImageResId Resource ID to be displayed in case the image could not be loaded. If 0, no new image 128 | * will be displayed on error. 129 | * @param placeholderImageResId Resource ID to set for placeholder image while image is loading. 130 | */ 131 | public void setImageUrl(URL imageUrl, BitmapFactory.Options options, int errorImageResId, int placeholderImageResId) { 132 | if(imageUrl == null) { 133 | return; 134 | } 135 | else if(currentState == States.LOADED && imageUrl.equals(loadedImageUrl)) { 136 | return; 137 | } 138 | else if(currentState == States.LOADING && imageUrl.equals(pendingImageUrl)) { 139 | return; 140 | } 141 | 142 | currentState = States.LOADING; 143 | this.errorImageResId = errorImageResId; 144 | if(this.placeholderImageResId > 0) { 145 | setImageResource(this.placeholderImageResId); 146 | } 147 | else if(this.placeholderImage != null) { 148 | setImageDrawable(this.placeholderImage); 149 | } 150 | else if(placeholderImageResId > 0) { 151 | setImageResource(placeholderImageResId); 152 | } 153 | if(this.listener != null) { 154 | listener.onImageLoadStarted(); 155 | } 156 | pendingImageUrl = imageUrl; 157 | ImageLoader.load(getContext(), imageUrl, this, options); 158 | } 159 | 160 | /** 161 | * This method is called when the drawable has been downloaded (or retreived from cache) and is 162 | * ready to be displayed. If you override this class, then you should not call this method via 163 | * super.onBitmapLoaded(). Instead, handle the drawable as necessary (ie, resizing or other 164 | * transformations), and then call postToGuiThread() to display the image from the correct thread. 165 | * 166 | * If you only need a callback to be notified about the drawable being loaded to update other 167 | * GUI elements and whatnot, then you should override onImageLoaded() instead. 168 | * 169 | * @param response Request response 170 | */ 171 | public void onBitmapLoaded(final RequestResponse response) { 172 | if(response.originalRequest.imageUrl.equals(pendingImageUrl)) { 173 | postToGuiThread(new Runnable() { 174 | public void run() { 175 | final Bitmap bitmap = response.bitmapReference.get(); 176 | if(bitmap != null) { 177 | setImageBitmap(bitmap); 178 | currentState = States.LOADED; 179 | loadedImageUrl = response.originalRequest.imageUrl; 180 | pendingImageUrl = null; 181 | if(listener != null) { 182 | listener.onImageLoadComplete(); 183 | } 184 | } 185 | else { 186 | // The garbage collecter has cleaned up this bitmap by now (yes, that does happen), so re-issue the request 187 | ImageLoader.load(getContext(), response.originalRequest.imageUrl, response.originalRequest.listener, response.originalRequest.loadOptions); 188 | currentState = States.RELOADING; 189 | } 190 | } 191 | }); 192 | } 193 | else { 194 | if(listener != null) { 195 | listener.onImageLoadCancelled(); 196 | } 197 | currentState = States.CANCELLED; 198 | } 199 | } 200 | 201 | /** 202 | * This method is called if the drawable could not be loaded for any reason. If you need a callback 203 | * to react to these events, you should override onImageError() instead. 204 | * @param message Error message (non-localized) 205 | */ 206 | public void onBitmapLoadError(String message) { 207 | currentState = States.ERROR; 208 | LogWrapper.logMessage(message); 209 | postToGuiThread(new Runnable() { 210 | public void run() { 211 | // In case of error, lazily load the drawable here 212 | if(errorImageResId > 0) { 213 | errorImage = getResources().getDrawable(errorImageResId); 214 | } 215 | if(errorImage != null) { 216 | setImageDrawable(errorImage); 217 | } 218 | } 219 | }); 220 | if(listener != null) { 221 | listener.onImageLoadError(); 222 | } 223 | } 224 | 225 | @Override 226 | protected void onWindowVisibilityChanged(int visibility) { 227 | super.onWindowVisibilityChanged(visibility); 228 | if(visibility == VISIBLE && currentState == States.LOADING) { 229 | setImageUrl(pendingImageUrl); 230 | } 231 | } 232 | 233 | /** 234 | * Called when the URL which the caller asked to load was cancelled. This can happen for a number 235 | * of reasons, including the activity being closed or scrolling rapidly in a ListView. For this 236 | * reason it is recommended not to do so much work in this method. 237 | */ 238 | public void onBitmapLoadCancelled() { 239 | currentState = States.CANCELLED; 240 | if(listener != null) { 241 | listener.onImageLoadCancelled(); 242 | } 243 | } 244 | 245 | /** 246 | * Post a message to the GUI thread. This should be used for updating the component from 247 | * background tasks. 248 | * @param runnable Runnable task 249 | */ 250 | public final void postToGuiThread(Runnable runnable) { 251 | uiHandler.post(runnable); 252 | } 253 | 254 | @SuppressWarnings("UnusedDeclaration") 255 | public void setErrorImageResId(int errorImageResId) { 256 | this.errorImageResId = errorImageResId; 257 | } 258 | 259 | @SuppressWarnings("UnusedDeclaration") 260 | public void setErrorImage(Drawable errorImage) { 261 | this.errorImage = errorImage; 262 | } 263 | 264 | @SuppressWarnings("UnusedDeclaration") 265 | public void setPlaceholderImage(Drawable placeholderImage) { 266 | this.placeholderImage = placeholderImage; 267 | } 268 | 269 | @SuppressWarnings("UnusedDeclaration") 270 | public void setPlaceholderImageResId(int placeholderImageResId) { 271 | this.placeholderImageResId = placeholderImageResId; 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/com/wrapp/android/webimage/ImageDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Bohemian Wrappsody AB 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package com.wrapp.android.webimage; 23 | 24 | import android.content.Context; 25 | import android.net.http.AndroidHttpClient; 26 | import android.os.Build; 27 | import org.apache.http.*; 28 | import org.apache.http.client.HttpClient; 29 | import org.apache.http.client.methods.HttpGet; 30 | import org.apache.http.client.methods.HttpHead; 31 | import org.apache.http.impl.client.DefaultHttpClient; 32 | import org.apache.http.params.CoreConnectionPNames; 33 | import org.apache.http.params.HttpParams; 34 | 35 | import java.io.*; 36 | import java.net.URL; 37 | import java.text.ParseException; 38 | import java.text.SimpleDateFormat; 39 | import java.util.Date; 40 | import java.util.Locale; 41 | 42 | public class ImageDownloader { 43 | private static final int CONNECTION_TIMEOUT_IN_MS = 10 * 1000; 44 | private static final int DEFAULT_BUFFER_SIZE = 8192; 45 | private static final int MAX_REDIRECT_COUNT = 4; 46 | private static String userAgent = null; 47 | 48 | public static boolean loadImage(final Context context, final String imageKey, final URL imageUrl) { 49 | return loadImage(context, imageKey, imageUrl, 0); 50 | } 51 | 52 | private static boolean loadImage(final Context context, final String imageKey, final URL imageUrl, int redirectCount) { 53 | if(redirectCount > MAX_REDIRECT_COUNT) { 54 | LogWrapper.logMessage("Too many redirects!"); 55 | return false; 56 | } 57 | 58 | HttpClient httpClient = null; 59 | HttpEntity responseEntity = null; 60 | BufferedInputStream bufferedInputStream = null; 61 | BufferedOutputStream bufferedOutputStream = null; 62 | 63 | try { 64 | final String imageUrlString = imageUrl.toString(); 65 | if(imageUrlString == null || imageUrlString.length() == 0) { 66 | throw new Exception("Passed empty URL"); 67 | } 68 | LogWrapper.logMessage("Requesting image " + imageUrlString); 69 | httpClient = getHttpClient(); 70 | final HttpParams httpParams = httpClient.getParams(); 71 | httpParams.setParameter(CoreConnectionPNames.SO_TIMEOUT, CONNECTION_TIMEOUT_IN_MS); 72 | httpParams.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, CONNECTION_TIMEOUT_IN_MS); 73 | httpParams.setParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, DEFAULT_BUFFER_SIZE); 74 | final HttpGet httpGet = new HttpGet(imageUrlString); 75 | final HttpResponse response = httpClient.execute(httpGet); 76 | 77 | responseEntity = response.getEntity(); 78 | if(responseEntity == null) { 79 | throw new Exception("No response entity for image " + imageUrl.toString()); 80 | } 81 | final StatusLine statusLine = response.getStatusLine(); 82 | final int statusCode = statusLine.getStatusCode(); 83 | switch(statusCode) { 84 | case HttpStatus.SC_OK: 85 | break; 86 | case HttpStatus.SC_MOVED_TEMPORARILY: 87 | case HttpStatus.SC_MOVED_PERMANENTLY: 88 | case HttpStatus.SC_SEE_OTHER: 89 | final String location = response.getFirstHeader("Location").getValue(); 90 | LogWrapper.logMessage("Image redirected to " + location); 91 | // Force close the connection now, otherwise we risk leaking too many open HTTP connections 92 | responseEntity.consumeContent(); 93 | responseEntity = null; 94 | if(httpClient instanceof AndroidHttpClient) { 95 | ((AndroidHttpClient)httpClient).close(); 96 | } 97 | httpClient = null; 98 | return loadImage(context, imageKey, new URL(location), redirectCount + 1); 99 | default: 100 | LogWrapper.logMessage("Could not download image, got status code " + statusCode); 101 | return false; 102 | } 103 | 104 | bufferedInputStream = new BufferedInputStream(responseEntity.getContent()); 105 | File cacheFile = File.createTempFile("image-", "tmp", ImageCache.getCacheDirectory(context)); 106 | bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(cacheFile)); 107 | 108 | long contentSize = responseEntity.getContentLength(); 109 | long totalBytesRead = 0; 110 | try { 111 | int bytesRead; 112 | final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; 113 | do { 114 | bytesRead = bufferedInputStream.read(buffer, 0, DEFAULT_BUFFER_SIZE); 115 | if(bytesRead > 0) { 116 | bufferedOutputStream.write(buffer, 0, bytesRead); 117 | totalBytesRead += bytesRead; 118 | } 119 | } while(bytesRead > 0); 120 | } 121 | catch(IOException e) { 122 | LogWrapper.logException(e); 123 | return false; 124 | } 125 | 126 | if(totalBytesRead != contentSize) { 127 | LogWrapper.logMessage("Short read! Expected " + contentSize + "b, got " + totalBytesRead); 128 | return false; 129 | } 130 | else { 131 | LogWrapper.logMessage("Downloaded image " + imageUrlString + " to file cache"); 132 | File outputFile = new File(ImageCache.getCacheDirectory(context), imageKey); 133 | cacheFile.renameTo(outputFile); 134 | } 135 | } 136 | catch(IOException e) { 137 | LogWrapper.logException(e); 138 | return false; 139 | } 140 | catch(Exception e) { 141 | LogWrapper.logException(e); 142 | return false; 143 | } 144 | finally { 145 | try { 146 | if(bufferedInputStream != null) { 147 | bufferedInputStream.close(); 148 | } 149 | if(bufferedOutputStream != null) { 150 | bufferedOutputStream.flush(); 151 | bufferedOutputStream.close(); 152 | } 153 | if(responseEntity != null) { 154 | responseEntity.consumeContent(); 155 | } 156 | } 157 | catch(IOException e) { 158 | LogWrapper.logException(e); 159 | } 160 | if(httpClient != null) { 161 | try { 162 | if(httpClient instanceof AndroidHttpClient) { 163 | ((AndroidHttpClient)httpClient).close(); 164 | } 165 | } 166 | catch(Exception e) { 167 | // Ignore 168 | } 169 | } 170 | } 171 | 172 | return true; 173 | } 174 | 175 | public static Date getServerTimestamp(final URL imageUrl) { 176 | Date expirationDate = new Date(); 177 | HttpClient httpClient = null; 178 | 179 | try { 180 | final String imageUrlString = imageUrl.toString(); 181 | if(imageUrlString == null || imageUrlString.length() == 0) { 182 | throw new Exception("Passed empty URL"); 183 | } 184 | LogWrapper.logMessage("Requesting image " + imageUrlString); 185 | httpClient = getHttpClient(); 186 | final HttpParams httpParams = httpClient.getParams(); 187 | httpParams.setParameter(CoreConnectionPNames.SO_TIMEOUT, CONNECTION_TIMEOUT_IN_MS); 188 | httpParams.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, CONNECTION_TIMEOUT_IN_MS); 189 | httpParams.setParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, DEFAULT_BUFFER_SIZE); 190 | final HttpHead httpHead = new HttpHead(imageUrlString); 191 | final HttpResponse response = httpClient.execute(httpHead); 192 | 193 | Header[] header = response.getHeaders("Expires"); 194 | if(header != null && header.length > 0) { 195 | expirationDate = parseServerDateHeader(header[0]); 196 | LogWrapper.logMessage("Image at " + imageUrl.toString() + " expires on " + expirationDate.toString()); 197 | } 198 | } 199 | catch(Exception e) { 200 | LogWrapper.logException(e); 201 | } 202 | finally { 203 | if(httpClient != null) { 204 | try { 205 | if(httpClient instanceof AndroidHttpClient) { 206 | ((AndroidHttpClient)httpClient).close(); 207 | } 208 | } 209 | catch(Exception e) { 210 | // Ignore 211 | } 212 | } 213 | } 214 | 215 | return expirationDate; 216 | } 217 | 218 | // AndroidHttpClient was introduced in API Level 8, but many 2.1 phones actually have it. Why this is, 219 | // I have no idea. However, on these devices it is much safer to use the default HTTP client instead 220 | // rather than risk throwing an exception when the class (or one of its methods) is not found. 221 | private static HttpClient getHttpClient() { 222 | if(Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) { 223 | return new DefaultHttpClient(); 224 | } 225 | else { 226 | return AndroidHttpClient.newInstance(getUserAgent()); 227 | } 228 | } 229 | 230 | private static Date parseServerDateHeader(Header serverDateHeader) { 231 | if(Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) { 232 | SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.US); 233 | try { 234 | return dateFormat.parse(serverDateHeader.getValue()); 235 | } 236 | catch(ParseException e) { 237 | return new Date(); 238 | } 239 | } 240 | else { 241 | return new Date(AndroidHttpClient.parseDate(serverDateHeader.getValue())); 242 | } 243 | } 244 | 245 | private static String getUserAgent() { 246 | if(userAgent == null) { 247 | final String USER_AGENT_TEMPLATE = "WebImage-Android (%s %s; %s API%s; %s)"; 248 | userAgent = String.format(USER_AGENT_TEMPLATE, Build.MANUFACTURER, Build.MODEL, Build.VERSION.RELEASE, 249 | Build.VERSION.SDK_INT, Locale.getDefault().toString()); 250 | } 251 | return userAgent; 252 | } 253 | } 254 | --------------------------------------------------------------------------------