├── .gitignore ├── AndroidManifest.xml ├── LICENSE_LGPL_V2 ├── Makefile ├── README.md ├── libs ├── CWAC-AdapterWrapper.jar └── android-support-v4.jar ├── project.properties ├── res ├── layout │ └── list_loading.xml └── values │ └── ids.xml ├── src └── edu │ └── mit │ └── mobile │ └── android │ └── imagecache │ ├── DiskCache.java │ ├── DrawableMemCache.java │ ├── ImageCache.java │ ├── ImageCacheException.java │ ├── ImageLoaderAdapter.java │ ├── KeyedLock.java │ ├── SimpleThumbnailAdapter.java │ └── SimpleThumbnailCursorAdapter.java └── test ├── .gitignore ├── AndroidManifest.xml ├── assets └── logo_locast.png ├── lint.xml ├── project.properties ├── res ├── drawable-hdpi │ └── ic_launcher.png ├── drawable-ldpi │ └── ic_launcher.png ├── drawable-mdpi │ └── ic_launcher.png ├── drawable-xhdpi │ └── ic_launcher.png ├── layout │ ├── activity_concurrency_test.xml │ ├── main.xml │ ├── small_thumbnail_item.xml │ ├── square_thumbnail_item.xml │ └── thumbnail_item.xml ├── menu │ └── main_menu.xml └── values │ └── strings.xml └── src └── edu └── mit └── mobile └── android └── imagecache └── test ├── ConcurrencyTest.java ├── ImageCacheJunitTest.java ├── InteractiveDemo.java ├── KeyedLockTest.java └── TestData.java /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | gen/ 3 | .classpath 4 | .project 5 | test/.project 6 | local.properties 7 | build.xml 8 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE_LGPL_V2: -------------------------------------------------------------------------------- 1 | GNU LIBRARY GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1991 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor 6 | Boston, MA 02110-1301, USA. 7 | Everyone is permitted to copy and distribute verbatim copies 8 | of this license document, but changing it is not allowed. 9 | 10 | [This is the first released version of the library GPL. It is 11 | numbered 2 because it goes with version 2 of the ordinary GPL.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Library General Public License, applies to some 21 | specially designated Free Software Foundation software, and to any 22 | other libraries whose authors decide to use it. You can use it for 23 | your libraries, too. 24 | 25 | When we speak of free software, we are referring to freedom, not 26 | price. Our General Public Licenses are designed to make sure that you 27 | have the freedom to distribute copies of free software (and charge for 28 | this service if you wish), that you receive source code or can get it 29 | if you want it, that you can change the software or use pieces of it 30 | in new free programs; and that you know you can do these things. 31 | 32 | To protect your rights, we need to make restrictions that forbid 33 | anyone to deny you these rights or to ask you to surrender the rights. 34 | These restrictions translate to certain responsibilities for you if 35 | you distribute copies of the library, or if you modify it. 36 | 37 | For example, if you distribute copies of the library, whether gratis 38 | or for a fee, you must give the recipients all the rights that we gave 39 | you. You must make sure that they, too, receive or can get the source 40 | code. If you link a program with the library, you must provide 41 | complete object files to the recipients so that they can relink them 42 | with the library, after making changes to the library and recompiling 43 | it. And you must show them these terms so they know their rights. 44 | 45 | Our method of protecting your rights has two steps: (1) copyright 46 | the library, and (2) offer you this license which gives you legal 47 | permission to copy, distribute and/or modify the library. 48 | 49 | Also, for each distributor's protection, we want to make certain 50 | that everyone understands that there is no warranty for this free 51 | library. If the library is modified by someone else and passed on, we 52 | want its recipients to know that what they have is not the original 53 | version, so that any problems introduced by others will not reflect on 54 | the original authors' reputations. 55 | 56 | Finally, any free program is threatened constantly by software 57 | patents. We wish to avoid the danger that companies distributing free 58 | software will individually obtain patent licenses, thus in effect 59 | transforming the program into proprietary software. To prevent this, 60 | we have made it clear that any patent must be licensed for everyone's 61 | free use or not licensed at all. 62 | 63 | Most GNU software, including some libraries, is covered by the ordinary 64 | GNU General Public License, which was designed for utility programs. This 65 | license, the GNU Library General Public License, applies to certain 66 | designated libraries. This license is quite different from the ordinary 67 | one; be sure to read it in full, and don't assume that anything in it is 68 | the same as in the ordinary license. 69 | 70 | The reason we have a separate public license for some libraries is that 71 | they blur the distinction we usually make between modifying or adding to a 72 | program and simply using it. Linking a program with a library, without 73 | changing the library, is in some sense simply using the library, and is 74 | analogous to running a utility program or application program. However, in 75 | a textual and legal sense, the linked executable is a combined work, a 76 | derivative of the original library, and the ordinary General Public License 77 | treats it as such. 78 | 79 | Because of this blurred distinction, using the ordinary General 80 | Public License for libraries did not effectively promote software 81 | sharing, because most developers did not use the libraries. We 82 | concluded that weaker conditions might promote sharing better. 83 | 84 | However, unrestricted linking of non-free programs would deprive the 85 | users of those programs of all benefit from the free status of the 86 | libraries themselves. This Library General Public License is intended to 87 | permit developers of non-free programs to use free libraries, while 88 | preserving your freedom as a user of such programs to change the free 89 | libraries that are incorporated in them. (We have not seen how to achieve 90 | this as regards changes in header files, but we have achieved it as regards 91 | changes in the actual functions of the Library.) The hope is that this 92 | will lead to faster development of free libraries. 93 | 94 | The precise terms and conditions for copying, distribution and 95 | modification follow. Pay close attention to the difference between a 96 | "work based on the library" and a "work that uses the library". The 97 | former contains code derived from the library, while the latter only 98 | works together with the library. 99 | 100 | Note that it is possible for a library to be covered by the ordinary 101 | General Public License rather than by this special one. 102 | 103 | GNU LIBRARY GENERAL PUBLIC LICENSE 104 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 105 | 106 | 0. This License Agreement applies to any software library which 107 | contains a notice placed by the copyright holder or other authorized 108 | party saying it may be distributed under the terms of this Library 109 | General Public License (also called "this License"). Each licensee is 110 | addressed as "you". 111 | 112 | A "library" means a collection of software functions and/or data 113 | prepared so as to be conveniently linked with application programs 114 | (which use some of those functions and data) to form executables. 115 | 116 | The "Library", below, refers to any such software library or work 117 | which has been distributed under these terms. A "work based on the 118 | Library" means either the Library or any derivative work under 119 | copyright law: that is to say, a work containing the Library or a 120 | portion of it, either verbatim or with modifications and/or translated 121 | straightforwardly into another language. (Hereinafter, translation is 122 | included without limitation in the term "modification".) 123 | 124 | "Source code" for a work means the preferred form of the work for 125 | making modifications to it. For a library, complete source code means 126 | all the source code for all modules it contains, plus any associated 127 | interface definition files, plus the scripts used to control compilation 128 | and installation of the library. 129 | 130 | Activities other than copying, distribution and modification are not 131 | covered by this License; they are outside its scope. The act of 132 | running a program using the Library is not restricted, and output from 133 | such a program is covered only if its contents constitute a work based 134 | on the Library (independent of the use of the Library in a tool for 135 | writing it). Whether that is true depends on what the Library does 136 | and what the program that uses the Library does. 137 | 138 | 1. You may copy and distribute verbatim copies of the Library's 139 | complete source code as you receive it, in any medium, provided that 140 | you conspicuously and appropriately publish on each copy an 141 | appropriate copyright notice and disclaimer of warranty; keep intact 142 | all the notices that refer to this License and to the absence of any 143 | warranty; and distribute a copy of this License along with the 144 | Library. 145 | 146 | You may charge a fee for the physical act of transferring a copy, 147 | and you may at your option offer warranty protection in exchange for a 148 | fee. 149 | 150 | 2. You may modify your copy or copies of the Library or any portion 151 | of it, thus forming a work based on the Library, and copy and 152 | distribute such modifications or work under the terms of Section 1 153 | above, provided that you also meet all of these conditions: 154 | 155 | a) The modified work must itself be a software library. 156 | 157 | b) You must cause the files modified to carry prominent notices 158 | stating that you changed the files and the date of any change. 159 | 160 | c) You must cause the whole of the work to be licensed at no 161 | charge to all third parties under the terms of this License. 162 | 163 | d) If a facility in the modified Library refers to a function or a 164 | table of data to be supplied by an application program that uses 165 | the facility, other than as an argument passed when the facility 166 | is invoked, then you must make a good faith effort to ensure that, 167 | in the event an application does not supply such function or 168 | table, the facility still operates, and performs whatever part of 169 | its purpose remains meaningful. 170 | 171 | (For example, a function in a library to compute square roots has 172 | a purpose that is entirely well-defined independent of the 173 | application. Therefore, Subsection 2d requires that any 174 | application-supplied function or table used by this function must 175 | be optional: if the application does not supply it, the square 176 | root function must still compute square roots.) 177 | 178 | These requirements apply to the modified work as a whole. If 179 | identifiable sections of that work are not derived from the Library, 180 | and can be reasonably considered independent and separate works in 181 | themselves, then this License, and its terms, do not apply to those 182 | sections when you distribute them as separate works. But when you 183 | distribute the same sections as part of a whole which is a work based 184 | on the Library, the distribution of the whole must be on the terms of 185 | this License, whose permissions for other licensees extend to the 186 | entire whole, and thus to each and every part regardless of who wrote 187 | it. 188 | 189 | Thus, it is not the intent of this section to claim rights or contest 190 | your rights to work written entirely by you; rather, the intent is to 191 | exercise the right to control the distribution of derivative or 192 | collective works based on the Library. 193 | 194 | In addition, mere aggregation of another work not based on the Library 195 | with the Library (or with a work based on the Library) on a volume of 196 | a storage or distribution medium does not bring the other work under 197 | the scope of this License. 198 | 199 | 3. You may opt to apply the terms of the ordinary GNU General Public 200 | License instead of this License to a given copy of the Library. To do 201 | this, you must alter all the notices that refer to this License, so 202 | that they refer to the ordinary GNU General Public License, version 2, 203 | instead of to this License. (If a newer version than version 2 of the 204 | ordinary GNU General Public License has appeared, then you can specify 205 | that version instead if you wish.) Do not make any other change in 206 | these notices. 207 | 208 | Once this change is made in a given copy, it is irreversible for 209 | that copy, so the ordinary GNU General Public License applies to all 210 | subsequent copies and derivative works made from that copy. 211 | 212 | This option is useful when you wish to copy part of the code of 213 | the Library into a program that is not a library. 214 | 215 | 4. You may copy and distribute the Library (or a portion or 216 | derivative of it, under Section 2) in object code or executable form 217 | under the terms of Sections 1 and 2 above provided that you accompany 218 | it with the complete corresponding machine-readable source code, which 219 | must be distributed under the terms of Sections 1 and 2 above on a 220 | medium customarily used for software interchange. 221 | 222 | If distribution of object code is made by offering access to copy 223 | from a designated place, then offering equivalent access to copy the 224 | source code from the same place satisfies the requirement to 225 | distribute the source code, even though third parties are not 226 | compelled to copy the source along with the object code. 227 | 228 | 5. A program that contains no derivative of any portion of the 229 | Library, but is designed to work with the Library by being compiled or 230 | linked with it, is called a "work that uses the Library". Such a 231 | work, in isolation, is not a derivative work of the Library, and 232 | therefore falls outside the scope of this License. 233 | 234 | However, linking a "work that uses the Library" with the Library 235 | creates an executable that is a derivative of the Library (because it 236 | contains portions of the Library), rather than a "work that uses the 237 | library". The executable is therefore covered by this License. 238 | Section 6 states terms for distribution of such executables. 239 | 240 | When a "work that uses the Library" uses material from a header file 241 | that is part of the Library, the object code for the work may be a 242 | derivative work of the Library even though the source code is not. 243 | Whether this is true is especially significant if the work can be 244 | linked without the Library, or if the work is itself a library. The 245 | threshold for this to be true is not precisely defined by law. 246 | 247 | If such an object file uses only numerical parameters, data 248 | structure layouts and accessors, and small macros and small inline 249 | functions (ten lines or less in length), then the use of the object 250 | file is unrestricted, regardless of whether it is legally a derivative 251 | work. (Executables containing this object code plus portions of the 252 | Library will still fall under Section 6.) 253 | 254 | Otherwise, if the work is a derivative of the Library, you may 255 | distribute the object code for the work under the terms of Section 6. 256 | Any executables containing that work also fall under Section 6, 257 | whether or not they are linked directly with the Library itself. 258 | 259 | 6. As an exception to the Sections above, you may also compile or 260 | link a "work that uses the Library" with the Library to produce a 261 | work containing portions of the Library, and distribute that work 262 | under terms of your choice, provided that the terms permit 263 | modification of the work for the customer's own use and reverse 264 | engineering for debugging such modifications. 265 | 266 | You must give prominent notice with each copy of the work that the 267 | Library is used in it and that the Library and its use are covered by 268 | this License. You must supply a copy of this License. If the work 269 | during execution displays copyright notices, you must include the 270 | copyright notice for the Library among them, as well as a reference 271 | directing the user to the copy of this License. Also, you must do one 272 | of these things: 273 | 274 | a) Accompany the work with the complete corresponding 275 | machine-readable source code for the Library including whatever 276 | changes were used in the work (which must be distributed under 277 | Sections 1 and 2 above); and, if the work is an executable linked 278 | with the Library, with the complete machine-readable "work that 279 | uses the Library", as object code and/or source code, so that the 280 | user can modify the Library and then relink to produce a modified 281 | executable containing the modified Library. (It is understood 282 | that the user who changes the contents of definitions files in the 283 | Library will not necessarily be able to recompile the application 284 | to use the modified definitions.) 285 | 286 | b) Accompany the work with a written offer, valid for at 287 | least three years, to give the same user the materials 288 | specified in Subsection 6a, above, for a charge no more 289 | than the cost of performing this distribution. 290 | 291 | c) If distribution of the work is made by offering access to copy 292 | from a designated place, offer equivalent access to copy the above 293 | specified materials from the same place. 294 | 295 | d) Verify that the user has already received a copy of these 296 | materials or that you have already sent this user a copy. 297 | 298 | For an executable, the required form of the "work that uses the 299 | Library" must include any data and utility programs needed for 300 | reproducing the executable from it. However, as a special exception, 301 | the source code distributed need not include anything that is normally 302 | distributed (in either source or binary form) with the major 303 | components (compiler, kernel, and so on) of the operating system on 304 | which the executable runs, unless that component itself accompanies 305 | the executable. 306 | 307 | It may happen that this requirement contradicts the license 308 | restrictions of other proprietary libraries that do not normally 309 | accompany the operating system. Such a contradiction means you cannot 310 | use both them and the Library together in an executable that you 311 | distribute. 312 | 313 | 7. You may place library facilities that are a work based on the 314 | Library side-by-side in a single library together with other library 315 | facilities not covered by this License, and distribute such a combined 316 | library, provided that the separate distribution of the work based on 317 | the Library and of the other library facilities is otherwise 318 | permitted, and provided that you do these two things: 319 | 320 | a) Accompany the combined library with a copy of the same work 321 | based on the Library, uncombined with any other library 322 | facilities. This must be distributed under the terms of the 323 | Sections above. 324 | 325 | b) Give prominent notice with the combined library of the fact 326 | that part of it is a work based on the Library, and explaining 327 | where to find the accompanying uncombined form of the same work. 328 | 329 | 8. You may not copy, modify, sublicense, link with, or distribute 330 | the Library except as expressly provided under this License. Any 331 | attempt otherwise to copy, modify, sublicense, link with, or 332 | distribute the Library is void, and will automatically terminate your 333 | rights under this License. However, parties who have received copies, 334 | or rights, from you under this License will not have their licenses 335 | terminated so long as such parties remain in full compliance. 336 | 337 | 9. You are not required to accept this License, since you have not 338 | signed it. However, nothing else grants you permission to modify or 339 | distribute the Library or its derivative works. These actions are 340 | prohibited by law if you do not accept this License. Therefore, by 341 | modifying or distributing the Library (or any work based on the 342 | Library), you indicate your acceptance of this License to do so, and 343 | all its terms and conditions for copying, distributing or modifying 344 | the Library or works based on it. 345 | 346 | 10. Each time you redistribute the Library (or any work based on the 347 | Library), the recipient automatically receives a license from the 348 | original licensor to copy, distribute, link with or modify the Library 349 | subject to these terms and conditions. You may not impose any further 350 | restrictions on the recipients' exercise of the rights granted herein. 351 | You are not responsible for enforcing compliance by third parties to 352 | this License. 353 | 354 | 11. If, as a consequence of a court judgment or allegation of patent 355 | infringement or for any other reason (not limited to patent issues), 356 | conditions are imposed on you (whether by court order, agreement or 357 | otherwise) that contradict the conditions of this License, they do not 358 | excuse you from the conditions of this License. If you cannot 359 | distribute so as to satisfy simultaneously your obligations under this 360 | License and any other pertinent obligations, then as a consequence you 361 | may not distribute the Library at all. For example, if a patent 362 | license would not permit royalty-free redistribution of the Library by 363 | all those who receive copies directly or indirectly through you, then 364 | the only way you could satisfy both it and this License would be to 365 | refrain entirely from distribution of the Library. 366 | 367 | If any portion of this section is held invalid or unenforceable under any 368 | particular circumstance, the balance of the section is intended to apply, 369 | and the section as a whole is intended to apply in other circumstances. 370 | 371 | It is not the purpose of this section to induce you to infringe any 372 | patents or other property right claims or to contest validity of any 373 | such claims; this section has the sole purpose of protecting the 374 | integrity of the free software distribution system which is 375 | implemented by public license practices. Many people have made 376 | generous contributions to the wide range of software distributed 377 | through that system in reliance on consistent application of that 378 | system; it is up to the author/donor to decide if he or she is willing 379 | to distribute software through any other system and a licensee cannot 380 | impose that choice. 381 | 382 | This section is intended to make thoroughly clear what is believed to 383 | be a consequence of the rest of this License. 384 | 385 | 12. If the distribution and/or use of the Library is restricted in 386 | certain countries either by patents or by copyrighted interfaces, the 387 | original copyright holder who places the Library under this License may add 388 | an explicit geographical distribution limitation excluding those countries, 389 | so that distribution is permitted only in or among countries not thus 390 | excluded. In such case, this License incorporates the limitation as if 391 | written in the body of this License. 392 | 393 | 13. The Free Software Foundation may publish revised and/or new 394 | versions of the Library General Public License from time to time. 395 | Such new versions will be similar in spirit to the present version, 396 | but may differ in detail to address new problems or concerns. 397 | 398 | Each version is given a distinguishing version number. If the Library 399 | specifies a version number of this License which applies to it and 400 | "any later version", you have the option of following the terms and 401 | conditions either of that version or of any later version published by 402 | the Free Software Foundation. If the Library does not specify a 403 | license version number, you may choose any version ever published by 404 | the Free Software Foundation. 405 | 406 | 14. If you wish to incorporate parts of the Library into other free 407 | programs whose distribution conditions are incompatible with these, 408 | write to the author to ask for permission. For software which is 409 | copyrighted by the Free Software Foundation, write to the Free 410 | Software Foundation; we sometimes make exceptions for this. Our 411 | decision will be guided by the two goals of preserving the free status 412 | of all derivatives of our free software and of promoting the sharing 413 | and reuse of software generally. 414 | 415 | NO WARRANTY 416 | 417 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 418 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 419 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 420 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 421 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 422 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 423 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 424 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 425 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 426 | 427 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 428 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 429 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 430 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 431 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 432 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 433 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 434 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 435 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 436 | DAMAGES. 437 | 438 | END OF TERMS AND CONDITIONS 439 | 440 | How to Apply These Terms to Your New Libraries 441 | 442 | If you develop a new library, and you want it to be of the greatest 443 | possible use to the public, we recommend making it free software that 444 | everyone can redistribute and change. You can do so by permitting 445 | redistribution under these terms (or, alternatively, under the terms of the 446 | ordinary General Public License). 447 | 448 | To apply these terms, attach the following notices to the library. It is 449 | safest to attach them to the start of each source file to most effectively 450 | convey the exclusion of warranty; and each file should have at least the 451 | "copyright" line and a pointer to where the full notice is found. 452 | 453 | 454 | Copyright (C) 455 | 456 | This library is free software; you can redistribute it and/or 457 | modify it under the terms of the GNU Lesser General Public 458 | License as published by the Free Software Foundation; either 459 | version 2 of the License, or (at your option) any later version. 460 | 461 | This library is distributed in the hope that it will be useful, 462 | but WITHOUT ANY WARRANTY; without even the implied warranty of 463 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 464 | Lesser General Public License for more details. 465 | 466 | You should have received a copy of the GNU Lesser General Public 467 | License along with this library; if not, write to the Free Software 468 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 469 | 470 | Also add information on how to contact you by electronic and paper mail. 471 | 472 | You should also get your employer (if you work as a programmer) or your 473 | school, if any, to sign a "copyright disclaimer" for the library, if 474 | necessary. Here is a sample; alter the names: 475 | 476 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 477 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 478 | 479 | , 1 April 1990 480 | Ty Coon, President of Vice 481 | 482 | That's all there is to it! 483 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | srcdir := ImageCache 2 | version := $(shell sed '/versionName/ { s/.*versionName="\([^"]*\)".*/\1/; s/ /_/g; p }; d' AndroidManifest.xml) 3 | out_package := ../$(srcdir)_$(version).tar.gz 4 | 5 | package: $(out_package) 6 | 7 | $(out_package): 8 | tar -zcv --exclude .git --exclude-vcs --exclude \*\~ --exclude-backups -X .gitignore -f $@ -C ../ $(srcdir) 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Image Cache 2 | =========== 3 | 4 | An image download-and-cacher that also knows how to efficiently generate 5 | and retrieve thumbnails of various sizes. 6 | 7 | Features 8 | -------- 9 | 10 | * easily integrates into content-provider backed applications, providing an 11 | adapter that can read local and web URLs from a cursor 12 | * automatic generation and caching of multiple sizes of images based on one 13 | downloaded asset 14 | * provides a disk cache as well as a memory cache 15 | * automatic disk cache management; no setup necessary, but parameters can be 16 | fine-tuned if desired 17 | * designed to work with your existing setup: no extending a custom application 18 | or activity needed 19 | * cursor adapter supports multiple image fields for each ImageView; skips 20 | fields that are null or empty 21 | * cursor adapter has an automatic progress bar when loading the cursor 22 | 23 | Using 24 | ----- 25 | 26 | Please see the `test/` directory for both a simple example of using it as well as 27 | some unit tests. When running the application in `test/` make sure to run it as 28 | an Android activity if you want to see the demo. 29 | 30 | Both the unit tests and the interactive test load some images from our lab's servers. 31 | 32 | License 33 | ======= 34 | 35 | MEL Android Image Cache 36 | Copyright (C) 2011-2013 [MIT Mobile Experience Lab][mel] 37 | 38 | This library is free software; you can redistribute it and/or 39 | modify it under the terms of the GNU Lesser General Public 40 | License version 2.1 as published by the Free Software Foundation. 41 | 42 | This library is distributed in the hope that it will be useful, 43 | but WITHOUT ANY WARRANTY; without even the implied warranty of 44 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 45 | Lesser General Public License for more details. 46 | 47 | You should have received a copy of the GNU Lesser General Public 48 | License along with this library; if not, write to the Free Software 49 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 50 | 51 | [mel]: http://mobile.mit.edu/ 52 | -------------------------------------------------------------------------------- /libs/CWAC-AdapterWrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmel/Android-Image-Cache/b3b093d945b7ddfa3f68078fe553d6f634d29676/libs/CWAC-AdapterWrapper.jar -------------------------------------------------------------------------------- /libs/android-support-v4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmel/Android-Image-Cache/b3b093d945b7ddfa3f68078fe553d6f634d29676/libs/android-support-v4.jar -------------------------------------------------------------------------------- /project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system use, 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | 10 | android.library=true 11 | # Project target. 12 | target=android-17 13 | -------------------------------------------------------------------------------- /res/layout/list_loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | 15 | -------------------------------------------------------------------------------- /res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/edu/mit/mobile/android/imagecache/DiskCache.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.imagecache; 2 | 3 | /* 4 | * Copyright (C) 2011-2013 MIT Mobile Experience Lab 5 | * 6 | * This library is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 2.1 of the License, or (at your option) any later version. 10 | * 11 | * This library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this library; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 | */ 20 | import java.io.File; 21 | import java.io.FileFilter; 22 | import java.io.FileInputStream; 23 | import java.io.FileNotFoundException; 24 | import java.io.FileOutputStream; 25 | import java.io.IOException; 26 | import java.io.InputStream; 27 | import java.io.OutputStream; 28 | import java.math.BigInteger; 29 | import java.security.MessageDigest; 30 | import java.security.NoSuchAlgorithmException; 31 | import java.util.Arrays; 32 | import java.util.Collections; 33 | import java.util.Comparator; 34 | import java.util.List; 35 | import java.util.concurrent.ConcurrentLinkedQueue; 36 | import java.util.concurrent.LinkedBlockingQueue; 37 | import java.util.concurrent.ThreadPoolExecutor; 38 | import java.util.concurrent.TimeUnit; 39 | 40 | import android.os.Build; 41 | import android.os.StatFs; 42 | import android.util.Log; 43 | 44 | /** 45 | *

46 | * A simple disk cache. 47 | *

48 | * 49 | *

50 | * By default, the maximum size of the cache is automatically set based on the amount of free space 51 | * available to the cache. Alternatively, a fixed size can be specified using 52 | * {@link #setCacheMaxSize(long)}. 53 | *

54 | * 55 | *

56 | * By default, the cache will automatically maintain its size by periodically checking to see if it 57 | * estimates that a trim is needed and if it is, proceeding to running {@link #trim()} on a worker 58 | * thread. This feature can be controlled by {@link #setAutoTrimFrequency(int)}. 59 | *

60 | * 61 | * @author Steve Pomeroy 62 | * 63 | * @param 64 | * the key to store/retrieve the value 65 | * @param 66 | * the value that will be stored to disk 67 | */ 68 | public abstract class DiskCache { 69 | private static final String TAG = "DiskCache"; 70 | 71 | /** 72 | * Automatically determines the maximum size of the cache based on available free space. 73 | */ 74 | public static final int AUTO_MAX_CACHE_SIZE = 0; 75 | 76 | /** 77 | * The default number of cache hits before {@link #trim()} is automatically triggered. See 78 | * {@link #setAutoTrimFrequency(int)}. 79 | */ 80 | public static final int DEFAULT_AUTO_TRIM_FREQUENCY = 10; 81 | 82 | /** 83 | * Pass to {@link #setAutoTrimFrequency(int)} to disable automatic trimming. See {@link #trim()} 84 | * . 85 | */ 86 | public static final int AUTO_TRIM_DISABLED = 0; 87 | 88 | // ///////////////////////////////////////////// 89 | 90 | private long mMaxDiskUsage = AUTO_MAX_CACHE_SIZE; 91 | 92 | private MessageDigest hash; 93 | 94 | private final File mCacheBase; 95 | private final String mCachePrefix, mCacheSuffix; 96 | 97 | private final ConcurrentLinkedQueue mQueue = new ConcurrentLinkedQueue(); 98 | 99 | /** 100 | * In auto max cache mode, the maximum is set to the total free space divided by this amount. 101 | */ 102 | private static final int AUTO_MAX_CACHE_SIZE_DIVISOR = 10; 103 | 104 | private int mAutoTrimFrequency = DEFAULT_AUTO_TRIM_FREQUENCY; 105 | 106 | private final ThreadPoolExecutor mExecutor = new ThreadPoolExecutor(1, 5, 60, TimeUnit.SECONDS, 107 | new LinkedBlockingQueue()); 108 | 109 | private int mAutoTrimHitCount = 1; 110 | 111 | private long mEstimatedDiskUsage; 112 | 113 | private long mEstimatedFreeSpace; 114 | 115 | /** 116 | * Creates a new disk cache with no cachePrefix or cacheSuffix 117 | * 118 | * @param cacheBase 119 | */ 120 | public DiskCache(File cacheBase) { 121 | this(cacheBase, null, null); 122 | } 123 | 124 | /** 125 | * Creates a new disk cache. 126 | * 127 | * @param cacheBase 128 | * The base directory within which all the cache files will be stored. 129 | * @param cachePrefix 130 | * If you want a prefix to the filenames, place one here. Otherwise, pass null. 131 | * @param cacheSuffix 132 | * A suffix to the cache filename. Null is also ok here. 133 | */ 134 | public DiskCache(File cacheBase, String cachePrefix, String cacheSuffix) { 135 | mCacheBase = cacheBase; 136 | mCachePrefix = cachePrefix; 137 | mCacheSuffix = cacheSuffix; 138 | 139 | try { 140 | hash = MessageDigest.getInstance("SHA-1"); 141 | 142 | } catch (final NoSuchAlgorithmException e) { 143 | try { 144 | hash = MessageDigest.getInstance("MD5"); 145 | } catch (final NoSuchAlgorithmException e2) { 146 | final RuntimeException re = new RuntimeException("No available hashing algorithm"); 147 | re.initCause(e2); 148 | throw re; 149 | } 150 | } 151 | 152 | updateDiskUsageInBg(); 153 | } 154 | 155 | /** 156 | * Sets the maximum size of the cache, in bytes. The default is to automatically manage the max 157 | * size based on the available disk space. This can be explicitly set by passing this 158 | * {@link #AUTO_MAX_CACHE_SIZE}. 159 | * 160 | * @param maxSize 161 | * maximum size of the cache, in bytes. 162 | */ 163 | public void setCacheMaxSize(long maxSize) { 164 | mMaxDiskUsage = maxSize; 165 | } 166 | 167 | /** 168 | * After this many puts, if it looks like there's a low space condition, {@link #trim()} will 169 | * automatically be called. 170 | * 171 | * @param autoTrimFrequency 172 | * Set to {@link #AUTO_TRIM_DISABLED} to turn off auto trim. The default is 173 | * {@link #DEFAULT_AUTO_TRIM_FREQUENCY}. 174 | */ 175 | public void setAutoTrimFrequency(int autoTrimFrequency) { 176 | mAutoTrimFrequency = autoTrimFrequency; 177 | } 178 | 179 | /** 180 | * Updates cached estimates on the 181 | */ 182 | private void updateDiskUsageEstimates() { 183 | final long diskUsage = getCacheDiskUsage(); 184 | 185 | final long availableSpace = getFreeSpace(); 186 | 187 | synchronized (this) { 188 | mEstimatedDiskUsage = diskUsage; 189 | mEstimatedFreeSpace = availableSpace; 190 | } 191 | } 192 | 193 | private void updateDiskUsageInBg() { 194 | mExecutor.execute(new Runnable() { 195 | 196 | @Override 197 | public void run() { 198 | updateDiskUsageEstimates(); 199 | } 200 | }); 201 | } 202 | 203 | /** 204 | * Gets the amount of space free on the cache volume. 205 | * 206 | * @return free space in bytes. 207 | */ 208 | private long getFreeSpace() { 209 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { 210 | return mCacheBase.getUsableSpace(); 211 | } else { 212 | // maybe make singleton 213 | final StatFs stat = new StatFs(mCacheBase.getAbsolutePath()); 214 | return (long) stat.getAvailableBlocks() * (long) stat.getBlockSize(); 215 | } 216 | } 217 | 218 | /** 219 | * Gets the cache filename for the given key. 220 | * 221 | * @param key 222 | * @return 223 | */ 224 | protected File getFile(K key) { 225 | return new File(mCacheBase, (mCachePrefix != null ? mCachePrefix : "") + hash(key) 226 | + (mCacheSuffix != null ? mCacheSuffix : "")); 227 | } 228 | 229 | /** 230 | * Writes the value stored in the cache to disk by calling 231 | * {@link #toDisk(Object, Object, OutputStream)}. 232 | * 233 | * @param key 234 | * The key to find the value. 235 | * @param value 236 | * the data to be written to disk. 237 | */ 238 | public final synchronized void put(K key, V value) throws IOException, FileNotFoundException { 239 | final File saveHere = getFile(key); 240 | 241 | final OutputStream os = new FileOutputStream(saveHere); 242 | toDisk(key, value, os); 243 | os.close(); 244 | 245 | mEstimatedDiskUsage += saveHere.length(); 246 | 247 | touchEntry(saveHere); 248 | 249 | autotrim(); 250 | } 251 | 252 | /** 253 | * Writes the contents of the InputStream straight to disk. It is the caller's responsibility to 254 | * ensure it's the same type as what would be written with 255 | * {@link #toDisk(Object, Object, OutputStream)} 256 | * 257 | * @param key 258 | * @param value 259 | * @throws IOException 260 | * @throws FileNotFoundException 261 | */ 262 | public final void putRaw(K key, InputStream value) throws IOException, FileNotFoundException { 263 | 264 | final File saveHere = getFile(key); 265 | 266 | final File tempFile = new File(saveHere.getAbsolutePath() + ".temp"); 267 | 268 | boolean allGood = false; 269 | try { 270 | final OutputStream os = new FileOutputStream(tempFile); 271 | 272 | inputStreamToOutputStream(value, os); 273 | os.close(); 274 | 275 | synchronized (this) { 276 | // overwrite 277 | saveHere.delete(); 278 | tempFile.renameTo(saveHere); 279 | } 280 | allGood = true; 281 | } finally { 282 | // clean up on any exception 283 | if (!allGood) { 284 | saveHere.delete(); 285 | tempFile.delete(); 286 | } 287 | } 288 | if (allGood) { 289 | mEstimatedDiskUsage += saveHere.length(); 290 | 291 | touchEntry(saveHere); 292 | 293 | autotrim(); 294 | } 295 | } 296 | 297 | /** 298 | * Puts the key at the end of the queue, removing it if it's already present. This will cause it 299 | * to be removed last when {@link #trim()} is called. 300 | * 301 | * @param cacheFile 302 | */ 303 | private void touchEntry(File cacheFile) { 304 | if (mQueue.contains(cacheFile)) { 305 | mQueue.remove(cacheFile); 306 | } 307 | mQueue.add(cacheFile); 308 | } 309 | 310 | /** 311 | * Marks the given key as accessed recently. This will deprioritize it from automatically being 312 | * purged upon {@link #trim()}. 313 | * 314 | * @param key 315 | */ 316 | protected void touchKey(K key) { 317 | touchEntry(getFile(key)); 318 | } 319 | 320 | /** 321 | * Call this every time you may be able to start a trim in the background. This implicitly runs 322 | * {@link #updateDiskUsageInBg()} each time it's called. 323 | */ 324 | private void autotrim() { 325 | if (mAutoTrimFrequency == 0) { 326 | return; 327 | } 328 | 329 | mAutoTrimHitCount = (mAutoTrimHitCount + 1) % mAutoTrimFrequency; 330 | 331 | if (mAutoTrimHitCount == 0 332 | && mEstimatedDiskUsage > Math.min(mEstimatedFreeSpace, mMaxDiskUsage)) { 333 | 334 | mExecutor.execute(new Runnable() { 335 | @Override 336 | public void run() { 337 | trim(); 338 | } 339 | }); 340 | } 341 | 342 | updateDiskUsageInBg(); 343 | } 344 | 345 | /** 346 | * Reads from an inputstream, dumps to an outputstream 347 | * 348 | * @param is 349 | * @param os 350 | * @throws IOException 351 | */ 352 | static public void inputStreamToOutputStream(InputStream is, OutputStream os) 353 | throws IOException { 354 | final int bufsize = 8196 * 10; 355 | final byte[] cbuf = new byte[bufsize]; 356 | 357 | for (int readBytes = is.read(cbuf, 0, bufsize); readBytes > 0; readBytes = is.read(cbuf, 0, 358 | bufsize)) { 359 | os.write(cbuf, 0, readBytes); 360 | } 361 | } 362 | 363 | /** 364 | * Reads the value from disk using {@link #fromDisk(Object, InputStream)}. 365 | * 366 | * @param key 367 | * @return The value for key or null if the key doesn't map to any existing entries. 368 | */ 369 | public final synchronized V get(K key) throws IOException { 370 | final File readFrom = getFile(key); 371 | 372 | if (!readFrom.exists()) { 373 | return null; 374 | } 375 | 376 | final InputStream is = new FileInputStream(readFrom); 377 | final V out = fromDisk(key, is); 378 | is.close(); 379 | 380 | touchEntry(readFrom); 381 | 382 | return out; 383 | } 384 | 385 | /** 386 | * Checks the disk cache for a given key. 387 | * 388 | * @param key 389 | * @return true if the disk cache contains the given key 390 | */ 391 | public final synchronized boolean contains(K key) { 392 | final File readFrom = getFile(key); 393 | 394 | return readFrom.exists(); 395 | } 396 | 397 | /** 398 | * Removes the item from the disk cache. 399 | * 400 | * @param key 401 | * @return true if the cached item has been removed or was already removed, false if it was not 402 | * able to be removed. 403 | */ 404 | public synchronized boolean clear(K key) { 405 | final File readFrom = getFile(key); 406 | 407 | if (!readFrom.exists()) { 408 | return true; 409 | } 410 | final long size = readFrom.length(); 411 | 412 | final boolean success = readFrom.delete(); 413 | 414 | if (success) { 415 | mEstimatedDiskUsage -= size; 416 | } 417 | 418 | return success; 419 | } 420 | 421 | /** 422 | * Removes the item from the disk cache. 423 | * 424 | * @param cacheFile 425 | * @return true if the cached item has been removed or was already removed, false if it was not 426 | * able to be removed. 427 | */ 428 | private synchronized boolean clear(File cacheFile) { 429 | 430 | if (!cacheFile.exists()) { 431 | return true; 432 | } 433 | final long size = cacheFile.length(); 434 | 435 | final boolean success = cacheFile.delete(); 436 | 437 | if (success) { 438 | mEstimatedDiskUsage -= size; 439 | } 440 | 441 | return success; 442 | } 443 | 444 | /** 445 | * Clears the cache files from disk. 446 | * 447 | * Note: this only clears files that match the given prefix/suffix. 448 | * 449 | * @return true if the operation succeeded without error. It is possible that it will fail and 450 | * the cache ends up being partially cleared. 451 | */ 452 | public synchronized boolean clear() { 453 | boolean success = true; 454 | 455 | for (final File cacheFile : mCacheBase.listFiles(mCacheFileFilter)) { 456 | if (!cacheFile.delete()) { 457 | Log.e(TAG, "error deleting " + cacheFile); 458 | success = false; 459 | } 460 | } 461 | return success; 462 | } 463 | 464 | /** 465 | * @return the number of files in the cache 466 | * @deprecated please use {@link #getCacheEntryCount()} or {@link #getCacheDiskUsage()} instead. 467 | */ 468 | @Deprecated 469 | public int getCacheSize() { 470 | return getCacheEntryCount(); 471 | } 472 | 473 | /** 474 | * @return the number of files in the cache as it is on disk. 475 | */ 476 | public int getCacheEntryCount() { 477 | return mCacheBase.listFiles(mCacheFileFilter).length; 478 | } 479 | 480 | /** 481 | * @return the size of the cache in bytes, as it is on disk. 482 | */ 483 | public long getCacheDiskUsage() { 484 | long usage = 0; 485 | for (final File cacheFile : mCacheBase.listFiles(mCacheFileFilter)) { 486 | usage += cacheFile.length(); 487 | } 488 | return usage; 489 | } 490 | 491 | private final CacheFileFilter mCacheFileFilter = new CacheFileFilter(); 492 | 493 | private class CacheFileFilter implements FileFilter { 494 | @Override 495 | public boolean accept(File pathname) { 496 | final String path = pathname.getName(); 497 | return (mCachePrefix != null ? path.startsWith(mCachePrefix) : true) 498 | && (mCacheSuffix != null ? path.endsWith(mCacheSuffix) : true); 499 | } 500 | }; 501 | 502 | private final Comparator mLastModifiedOldestFirstComparator = new Comparator() { 503 | 504 | @Override 505 | public int compare(File lhs, File rhs) { 506 | return Long.valueOf(lhs.lastModified()).compareTo(rhs.lastModified()); 507 | } 508 | }; 509 | 510 | /** 511 | * Clears out cache entries in order to reduce the on-disk usage to the desired maximum size. 512 | * This is a somewhat expensive operation, so it should be done on a background thread. 513 | * 514 | * @return the number of bytes worth of files that were trimmed. 515 | * @see #setCacheMaxSize(long) 516 | */ 517 | public synchronized long trim() { 518 | 519 | long desiredSize; 520 | final long freeSpace = getFreeSpace(); 521 | 522 | if (mMaxDiskUsage > 0) { 523 | desiredSize = mMaxDiskUsage; 524 | } else { 525 | desiredSize = getFreeSpace() / AUTO_MAX_CACHE_SIZE_DIVISOR; 526 | } 527 | 528 | desiredSize = Math.min(freeSpace, desiredSize); 529 | 530 | final long sizeToTrim = Math.max(0, getCacheDiskUsage() - desiredSize); 531 | 532 | if (sizeToTrim == 0) { 533 | return 0; 534 | } 535 | 536 | long trimmed = 0; 537 | 538 | final List sorted = Arrays.asList(mCacheBase.listFiles(mCacheFileFilter)); 539 | Collections.sort(sorted, mLastModifiedOldestFirstComparator); 540 | 541 | // first clear out any files that aren't in the queue 542 | for (final File cacheFile : sorted) { 543 | if (mQueue.contains(cacheFile)) { 544 | continue; 545 | } 546 | 547 | final long size = cacheFile.length(); 548 | if (clear(cacheFile)) { 549 | trimmed += size; 550 | if (BuildConfig.DEBUG) { 551 | Log.d(TAG, "trimmed unqueued " + cacheFile.getName() + " from cache."); 552 | } 553 | } 554 | 555 | if (trimmed >= sizeToTrim) { 556 | break; 557 | } 558 | } 559 | 560 | while (trimmed < sizeToTrim && !mQueue.isEmpty()) { 561 | final File cacheFile = mQueue.poll(); 562 | 563 | // shouldn't happen due to the check above, but just in case... 564 | if (cacheFile == null) { 565 | break; 566 | } 567 | 568 | final long size = cacheFile.length(); 569 | 570 | if (clear(cacheFile)) { 571 | trimmed += size; 572 | if (BuildConfig.DEBUG) { 573 | Log.d(TAG, "trimmed " + cacheFile.getName() + " from cache."); 574 | } 575 | } else { 576 | Log.e(TAG, "error deleting " + cacheFile); 577 | } 578 | } 579 | 580 | if (BuildConfig.DEBUG) { 581 | Log.d(TAG, "trimmed a total of " + trimmed + " bytes from cache."); 582 | } 583 | return trimmed; 584 | } 585 | 586 | /** 587 | * Implement this to do the actual disk writing. Do not close the OutputStream; it will be 588 | * closed for you. 589 | * 590 | * @param key 591 | * @param in 592 | * @param out 593 | */ 594 | protected abstract void toDisk(K key, V in, OutputStream out); 595 | 596 | /** 597 | * Implement this to do the actual disk reading. 598 | * 599 | * @param key 600 | * @param in 601 | * @return a new instance of {@link V} containing the contents of in. 602 | */ 603 | protected abstract V fromDisk(K key, InputStream in); 604 | 605 | /** 606 | * Using the key's {@link Object#toString() toString()} method, generates a string suitable for 607 | * using as a filename. 608 | * 609 | * @param key 610 | * @return a string uniquely representing the the key. 611 | */ 612 | public String hash(K key) { 613 | final byte[] ba; 614 | 615 | // MessageDigest isn't threadsafe, so we need to ensure it doesn't tread on itself. 616 | synchronized (hash) { 617 | hash.update(key.toString().getBytes()); 618 | ba = hash.digest(); 619 | } 620 | final BigInteger bi = new BigInteger(1, ba); 621 | final String result = bi.toString(16); 622 | if (result.length() % 2 != 0) { 623 | return "0" + result; 624 | } 625 | return result; 626 | 627 | } 628 | } 629 | -------------------------------------------------------------------------------- /src/edu/mit/mobile/android/imagecache/DrawableMemCache.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.imagecache; 2 | 3 | /* 4 | * Copyright (C) 2012 MIT Mobile Experience Lab 5 | * 6 | * This library is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 2.1 of the License, or (at your option) any later version. 10 | * 11 | * This library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this library; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 | */ 20 | import android.graphics.Bitmap; 21 | import android.graphics.drawable.BitmapDrawable; 22 | import android.graphics.drawable.Drawable; 23 | import android.support.v4.util.LruCache; 24 | 25 | public class DrawableMemCache extends LruCache { 26 | 27 | @SuppressWarnings("unused") 28 | private static final String TAG = DrawableMemCache.class.getSimpleName(); 29 | 30 | public DrawableMemCache(int maxSize) { 31 | super(maxSize); 32 | } 33 | 34 | @Override 35 | protected int sizeOf(T key, Drawable value) { 36 | int size = 0; 37 | if (value instanceof BitmapDrawable) { 38 | final Bitmap b = ((BitmapDrawable) value).getBitmap(); 39 | if (b != null) { 40 | size = b.getRowBytes() * b.getHeight(); 41 | } 42 | } 43 | return size; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/edu/mit/mobile/android/imagecache/ImageCache.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.imagecache; 2 | 3 | /* 4 | * Copyright (C) 2011-2012 MIT Mobile Experience Lab 5 | * 6 | * This library is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 2.1 of the License, or (at your option) any later version. 10 | * 11 | * This library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this library; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 | */ 20 | import java.io.File; 21 | import java.io.FileInputStream; 22 | import java.io.IOException; 23 | import java.io.InputStream; 24 | import java.io.OutputStream; 25 | import java.lang.reflect.InvocationTargetException; 26 | import java.lang.reflect.Method; 27 | import java.net.URL; 28 | import java.net.URLConnection; 29 | import java.util.Collections; 30 | import java.util.HashMap; 31 | import java.util.HashSet; 32 | import java.util.Map; 33 | import java.util.concurrent.PriorityBlockingQueue; 34 | import java.util.concurrent.ThreadPoolExecutor; 35 | import java.util.concurrent.TimeUnit; 36 | 37 | import org.apache.http.HttpEntity; 38 | import org.apache.http.HttpResponse; 39 | import org.apache.http.StatusLine; 40 | import org.apache.http.client.ClientProtocolException; 41 | import org.apache.http.client.HttpClient; 42 | import org.apache.http.client.HttpResponseException; 43 | import org.apache.http.client.methods.HttpGet; 44 | import org.apache.http.client.params.ClientPNames; 45 | import org.apache.http.conn.scheme.PlainSocketFactory; 46 | import org.apache.http.conn.scheme.Scheme; 47 | import org.apache.http.conn.scheme.SchemeRegistry; 48 | import org.apache.http.conn.ssl.SSLSocketFactory; 49 | import org.apache.http.impl.client.DefaultHttpClient; 50 | import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; 51 | import org.apache.http.params.CoreConnectionPNames; 52 | import org.apache.http.params.HttpParams; 53 | 54 | import android.annotation.SuppressLint; 55 | import android.app.Activity; 56 | import android.content.Context; 57 | import android.content.res.Resources; 58 | import android.graphics.Bitmap; 59 | import android.graphics.Bitmap.CompressFormat; 60 | import android.graphics.BitmapFactory; 61 | import android.graphics.drawable.BitmapDrawable; 62 | import android.graphics.drawable.Drawable; 63 | import android.net.Uri; 64 | import android.os.Handler; 65 | import android.os.Message; 66 | import android.util.Log; 67 | import android.util.SparseArray; 68 | import android.widget.ImageView; 69 | 70 | /** 71 | *

72 | * An image download-and-cacher that also knows how to efficiently generate thumbnails of various 73 | * sizes. 74 | *

75 | * 76 | *

77 | * The cache is shared with the entire process, so make sure you 78 | * {@link #registerOnImageLoadListener(OnImageLoadListener)} and 79 | * {@link #unregisterOnImageLoadListener(OnImageLoadListener)} any load listeners in your 80 | * activities. 81 | *

82 | * 83 | * @author Steve Pomeroy 84 | * 85 | */ 86 | public class ImageCache extends DiskCache { 87 | private static final String TAG = ImageCache.class.getSimpleName(); 88 | 89 | static final boolean DEBUG = false; 90 | 91 | // whether to use Apache HttpClient or URL.openConnection() 92 | private static final boolean USE_APACHE_NC = true; 93 | 94 | // the below settings are copied from AsyncTask.java 95 | private static final int CORE_POOL_SIZE = 5; // thread 96 | private static final int MAXIMUM_POOL_SIZE = 128; // thread 97 | private static final int KEEP_ALIVE_TIME = 1; // second 98 | 99 | private final HashSet mImageLoadListeners = new HashSet(); 100 | 101 | public static final int DEFAULT_CACHE_SIZE = (24 /* MiB */* 1024 * 1024); // in bytes 102 | 103 | private DrawableMemCache mMemCache = new DrawableMemCache(DEFAULT_CACHE_SIZE); 104 | 105 | private Integer mIDCounter = 0; 106 | 107 | private static ImageCache mInstance; 108 | 109 | // this is a custom Executor, as we want to have the tasks loaded in FILO order. FILO works 110 | // particularly well when scrolling with a ListView. 111 | private final ThreadPoolExecutor mExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, 112 | MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, 113 | new PriorityBlockingQueue()); 114 | 115 | // ignored as SparseArray isn't thread-safe 116 | @SuppressLint("UseSparseArrays") 117 | private final Map jobs = Collections 118 | .synchronizedMap(new HashMap()); 119 | 120 | private final HttpClient hc; 121 | 122 | private CompressFormat mCompressFormat; 123 | private int mQuality; 124 | 125 | private final Resources mRes; 126 | 127 | private static final int MSG_IMAGE_LOADED = 100; 128 | 129 | private final KeyedLock mDownloading = new KeyedLock(); 130 | 131 | private static class ImageLoadHandler extends Handler { 132 | private final ImageCache mCache; 133 | 134 | public ImageLoadHandler(ImageCache cache) { 135 | super(); 136 | mCache = cache; 137 | } 138 | 139 | @Override 140 | public void handleMessage(Message msg) { 141 | switch (msg.what) { 142 | case MSG_IMAGE_LOADED: 143 | mCache.notifyListeners((LoadResult) msg.obj); 144 | break; 145 | } 146 | }; 147 | } 148 | 149 | private final ImageLoadHandler mHandler = new ImageLoadHandler(this); 150 | 151 | // TODO make it so this is customizable on the instance level. 152 | /** 153 | * Gets an instance of the cache. 154 | * 155 | * @param context 156 | * @return an instance of the cache 157 | */ 158 | public static ImageCache getInstance(Context context) { 159 | if (mInstance == null) { 160 | mInstance = new ImageCache(context, CompressFormat.JPEG, 85); 161 | } 162 | return mInstance; 163 | } 164 | 165 | /** 166 | * Generally, it's best to use the shared image cache using {@link #getInstance(Context)}. Use 167 | * this if you want to customize a cache or keep it separate. 168 | * 169 | * @param context 170 | * @param format 171 | * @param quality 172 | */ 173 | public ImageCache(Context context, CompressFormat format, int quality) { 174 | super(context.getCacheDir(), null, getExtension(format)); 175 | if (USE_APACHE_NC) { 176 | hc = getHttpClient(); 177 | } else { 178 | hc = null; 179 | } 180 | 181 | mRes = context.getResources(); 182 | 183 | mCompressFormat = format; 184 | mQuality = quality; 185 | } 186 | 187 | /** 188 | * Sets the compression format for resized images. 189 | * 190 | * @param format 191 | */ 192 | public void setCompressFormat(CompressFormat format) { 193 | mCompressFormat = format; 194 | } 195 | 196 | /** 197 | * Set the image quality. Hint to the compressor, 0-100. 0 meaning compress for small size, 100 198 | * meaning compress for max quality. Some formats, like PNG which is lossless, will ignore the 199 | * quality setting 200 | * 201 | * @param quality 202 | */ 203 | public void setQuality(int quality) { 204 | mQuality = quality; 205 | } 206 | 207 | /** 208 | * Sets the maximum size of the memory cache. Note, this will clear the memory cache. 209 | * 210 | * @param maxSize 211 | * the maximum size of the memory cache in bytes. 212 | */ 213 | public void setMemCacheMaxSize(int maxSize) { 214 | mMemCache = new DrawableMemCache(maxSize); 215 | } 216 | 217 | private static String getExtension(CompressFormat format) { 218 | String extension; 219 | switch (format) { 220 | case JPEG: 221 | extension = ".jpg"; 222 | break; 223 | 224 | case PNG: 225 | extension = ".png"; 226 | break; 227 | 228 | default: 229 | throw new IllegalArgumentException(); 230 | } 231 | 232 | return extension; 233 | } 234 | 235 | /** 236 | * If loading a number of images where you don't have a unique ID to represent the individual 237 | * load, this can be used to generate a sequential ID. 238 | * 239 | * @return a new unique ID 240 | */ 241 | public int getNewID() { 242 | synchronized (mIDCounter) { 243 | return mIDCounter++; 244 | } 245 | } 246 | 247 | @Override 248 | protected Bitmap fromDisk(String key, InputStream in) { 249 | 250 | if (DEBUG) { 251 | Log.d(TAG, "disk cache hit for key " + key); 252 | } 253 | try { 254 | final Bitmap image = BitmapFactory.decodeStream(in); 255 | return image; 256 | 257 | } catch (final OutOfMemoryError oom) { 258 | oomClear(); 259 | return null; 260 | } 261 | } 262 | 263 | @Override 264 | protected void toDisk(String key, Bitmap image, OutputStream out) { 265 | if (DEBUG) { 266 | Log.d(TAG, "disk cache write for key " + key); 267 | } 268 | if (image != null) { 269 | if (!image.compress(mCompressFormat, mQuality, out)) { 270 | Log.e(TAG, "error writing compressed image to disk for key " + key); 271 | } 272 | } else { 273 | Log.e(TAG, "Ignoring attempt to write null image to disk cache"); 274 | } 275 | } 276 | 277 | /** 278 | * Gets an instance of AndroidHttpClient if the devices has it (it was introduced in 2.2), or 279 | * falls back on a http client that should work reasonably well. 280 | * 281 | * @return a working instance of an HttpClient 282 | */ 283 | private HttpClient getHttpClient() { 284 | HttpClient ahc; 285 | try { 286 | final Class ahcClass = Class.forName("android.net.http.AndroidHttpClient"); 287 | final Method newInstance = ahcClass.getMethod("newInstance", String.class); 288 | ahc = (HttpClient) newInstance.invoke(null, "ImageCache"); 289 | 290 | } catch (final ClassNotFoundException e) { 291 | DefaultHttpClient dhc = new DefaultHttpClient(); 292 | final HttpParams params = dhc.getParams(); 293 | dhc = null; 294 | 295 | params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 20 * 1000); 296 | 297 | final SchemeRegistry registry = new SchemeRegistry(); 298 | registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); 299 | registry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443)); 300 | 301 | final ThreadSafeClientConnManager manager = new ThreadSafeClientConnManager(params, 302 | registry); 303 | ahc = new DefaultHttpClient(manager, params); 304 | 305 | } catch (final NoSuchMethodException e) { 306 | 307 | final RuntimeException re = new RuntimeException("Programming error"); 308 | re.initCause(e); 309 | throw re; 310 | 311 | } catch (final IllegalAccessException e) { 312 | final RuntimeException re = new RuntimeException("Programming error"); 313 | re.initCause(e); 314 | throw re; 315 | 316 | } catch (final InvocationTargetException e) { 317 | final RuntimeException re = new RuntimeException("Programming error"); 318 | re.initCause(e); 319 | throw re; 320 | } 321 | return ahc; 322 | } 323 | 324 | /** 325 | *

326 | * Registers an {@link OnImageLoadListener} with the cache. When an image is loaded 327 | * asynchronously either directly by way of {@link #scheduleLoadImage(int, Uri, int, int)} or 328 | * indirectly by {@link #loadImage(int, Uri, int, int)}, any registered listeners will get 329 | * called. 330 | *

331 | * 332 | *

333 | * This should probably be called from {@link Activity#onResume()}. 334 | *

335 | * 336 | * @param onImageLoadListener 337 | */ 338 | public void registerOnImageLoadListener(OnImageLoadListener onImageLoadListener) { 339 | mImageLoadListeners.add(onImageLoadListener); 340 | } 341 | 342 | /** 343 | *

344 | * Unregisters the listener with the cache. This will not cancel any pending load requests. 345 | *

346 | * 347 | *

348 | * This should probably be called from {@link Activity#onPause()}. 349 | *

350 | * 351 | * @param onImageLoadListener 352 | */ 353 | public void unregisterOnImageLoadListener(OnImageLoadListener onImageLoadListener) { 354 | mImageLoadListeners.remove(onImageLoadListener); 355 | } 356 | 357 | private class LoadResult { 358 | public LoadResult(int id, Uri image, Drawable drawable) { 359 | this.id = id; 360 | this.drawable = drawable; 361 | this.image = image; 362 | } 363 | 364 | final Uri image; 365 | final int id; 366 | final Drawable drawable; 367 | } 368 | 369 | /** 370 | * @param uri 371 | * the image uri 372 | * @return a key unique to the given uri 373 | */ 374 | public String getKey(Uri uri) { 375 | return uri.toString(); 376 | } 377 | 378 | /** 379 | * Gets the given key as a drawable, retrieving it from memory cache if it's present. 380 | * 381 | * @param key 382 | * a key generated by {@link #getKey(Uri)} or {@link #getKey(Uri, int, int)} 383 | * @return the drawable if it's in the memory cache or null. 384 | */ 385 | public Drawable getDrawable(String key) { 386 | final Drawable img = mMemCache.get(key); 387 | if (img != null) { 388 | if (DEBUG) { 389 | Log.d(TAG, "mem cache hit for key " + key); 390 | } 391 | touchKey(key); 392 | return img; 393 | } 394 | 395 | return null; 396 | } 397 | 398 | /** 399 | * Puts a drawable into memory cache. 400 | * 401 | * @param key 402 | * a key generated by {@link #getKey(Uri)} or {@link #getKey(Uri, int, int)} 403 | * @param drawable 404 | */ 405 | public void putDrawable(String key, Drawable drawable) { 406 | mMemCache.put(key, drawable); 407 | } 408 | 409 | /** 410 | * A blocking call to get an image. If it's in the cache, it'll return the drawable immediately. 411 | * Otherwise it will download, scale, and cache the image before returning it. For non-blocking 412 | * use, see {@link #loadImage(int, Uri, int, int)} 413 | * 414 | * @param uri 415 | * @param width 416 | * @param height 417 | * @return 418 | * @throws ClientProtocolException 419 | * @throws IOException 420 | * @throws ImageCacheException 421 | */ 422 | public Drawable getImage(Uri uri, int width, int height) throws ClientProtocolException, 423 | IOException, ImageCacheException { 424 | 425 | final String scaledKey = getKey(uri, width, height); 426 | 427 | mDownloading.lock(scaledKey); 428 | 429 | try { 430 | Drawable d = getDrawable(scaledKey); 431 | if (d != null) { 432 | return d; 433 | } 434 | 435 | Bitmap bmp = get(scaledKey); 436 | 437 | if (bmp == null) { 438 | if ("file".equals(uri.getScheme())) { 439 | bmp = scaleLocalImage(new File(uri.getPath()), width, height); 440 | } else { 441 | final String sourceKey = getKey(uri); 442 | 443 | mDownloading.lock(sourceKey); 444 | 445 | try { 446 | if (!contains(sourceKey)) { 447 | downloadImage(sourceKey, uri); 448 | } 449 | } finally { 450 | mDownloading.unlock(sourceKey); 451 | } 452 | 453 | bmp = scaleLocalImage(getFile(sourceKey), width, height); 454 | if (bmp == null) { 455 | clear(sourceKey); 456 | } 457 | } 458 | put(scaledKey, bmp); 459 | 460 | } 461 | if (bmp == null) { 462 | throw new ImageCacheException("got null bitmap from request to scale"); 463 | 464 | } 465 | d = new BitmapDrawable(mRes, bmp); 466 | putDrawable(scaledKey, d); 467 | 468 | return d; 469 | 470 | } finally { 471 | mDownloading.unlock(scaledKey); 472 | } 473 | } 474 | 475 | private final SparseArray mKeyCache = new SparseArray(); 476 | 477 | /** 478 | * Returns an opaque cache key representing the given uri, width and height. 479 | * 480 | * @param uri 481 | * an image uri 482 | * @param width 483 | * the desired image max width 484 | * @param height 485 | * the desired image max height 486 | * @return a cache key unique to the given parameters 487 | */ 488 | public String getKey(Uri uri, int width, int height) { 489 | // collisions are possible, but unlikely. 490 | final int hashId = uri.hashCode() + width + height * 10000; 491 | 492 | String key = mKeyCache.get(hashId); 493 | if (key == null) { 494 | key = uri.buildUpon().appendQueryParameter("width", String.valueOf(width)) 495 | .appendQueryParameter("height", String.valueOf(height)).build().toString(); 496 | mKeyCache.put(hashId, key); 497 | } 498 | return key; 499 | } 500 | 501 | @Override 502 | public synchronized boolean clear() { 503 | final boolean success = super.clear(); 504 | 505 | mMemCache.evictAll(); 506 | 507 | mKeyCache.clear(); 508 | 509 | return success; 510 | } 511 | 512 | @Override 513 | public synchronized boolean clear(String key) { 514 | final boolean success = super.clear(key); 515 | 516 | mMemCache.remove(key); 517 | 518 | return success; 519 | } 520 | 521 | private class ImageLoadTask implements Runnable, Comparable { 522 | private final int id; 523 | private final Uri uri; 524 | private final int width; 525 | private final int height; 526 | private final long when = System.nanoTime(); 527 | 528 | public ImageLoadTask(int id, Uri image, int width, int height) { 529 | this.id = id; 530 | this.uri = image; 531 | this.width = width; 532 | this.height = height; 533 | } 534 | 535 | @Override 536 | public void run() { 537 | 538 | if (DEBUG) { 539 | Log.d(TAG, "ImageLoadTask.doInBackground(" + id + ", " + uri + ", " + width + ", " 540 | + height + ")"); 541 | } 542 | 543 | try { 544 | final LoadResult result = new LoadResult(id, uri, getImage(uri, width, height)); 545 | synchronized (jobs) { 546 | if (jobs.containsKey(id)) { 547 | // Job still valid. 548 | jobs.remove(id); 549 | mHandler.obtainMessage(MSG_IMAGE_LOADED, result).sendToTarget(); 550 | } 551 | } 552 | 553 | // TODO this exception came about, no idea why: 554 | // java.lang.IllegalArgumentException: Parser may not be null 555 | } catch (final IllegalArgumentException e) { 556 | Log.e(TAG, e.getLocalizedMessage(), e); 557 | } catch (final OutOfMemoryError oom) { 558 | oomClear(); 559 | } catch (final ClientProtocolException e) { 560 | Log.e(TAG, e.getLocalizedMessage(), e); 561 | } catch (final IOException e) { 562 | Log.e(TAG, e.getLocalizedMessage(), e); 563 | } catch (final ImageCacheException e) { 564 | Log.e(TAG, e.getLocalizedMessage(), e); 565 | } 566 | } 567 | 568 | @Override 569 | public int compareTo(ImageLoadTask another) { 570 | return Long.valueOf(another.when).compareTo(when); 571 | }; 572 | } 573 | 574 | private void oomClear() { 575 | Log.w(TAG, "out of memory, clearing mem cache"); 576 | mMemCache.evictAll(); 577 | } 578 | 579 | /** 580 | * Checks the cache for an image matching the given criteria and returns it. If it isn't 581 | * immediately available, calls {@link #scheduleLoadImage}. 582 | * 583 | * @param id 584 | * An ID to keep track of image load requests. For one-off loads, this can just be 585 | * the ID of the {@link ImageView}. Otherwise, an unique ID can be acquired using 586 | * {@link #getNewID()}. 587 | * 588 | * @param image 589 | * the image to be loaded. Can be a local file or a network resource. 590 | * @param width 591 | * the maximum width of the resulting image 592 | * @param height 593 | * the maximum height of the resulting image 594 | * @return the cached bitmap if it's available immediately or null if it needs to be loaded 595 | * asynchronously. 596 | */ 597 | public Drawable loadImage(int id, Uri image, int width, int height) throws IOException { 598 | if (DEBUG) { 599 | Log.d(TAG, "loadImage(" + id + ", " + image + ", " + width + ", " + height + ")"); 600 | } 601 | final Drawable res = getDrawable(getKey(image, width, height)); 602 | if (res == null) { 603 | if (DEBUG) { 604 | Log.d(TAG, 605 | "Image not found in memory cache. Scheduling load from network / disk..."); 606 | } 607 | scheduleLoadImage(id, image, width, height); 608 | } 609 | return res; 610 | } 611 | 612 | /** 613 | * Deprecated to make IDs ints instead of longs. See {@link #loadImage(int, Uri, int, int)}. 614 | * 615 | * @param id 616 | * @param image 617 | * @param width 618 | * @param height 619 | * @return 620 | * @throws IOException 621 | */ 622 | @Deprecated 623 | public Drawable loadImage(long id, Uri image, int width, int height) throws IOException { 624 | return loadImage(id, image, width, height); 625 | } 626 | 627 | /** 628 | * Schedules a load of the given image. When the image has finished loading and scaling, all 629 | * registered {@link OnImageLoadListener}s will be called. 630 | * 631 | * @param id 632 | * An ID to keep track of image load requests. For one-off loads, this can just be 633 | * the ID of the {@link ImageView}. Otherwise, an unique ID can be acquired using 634 | * {@link #getNewID()}. 635 | * 636 | * @param image 637 | * the image to be loaded. Can be a local file or a network resource. 638 | * @param width 639 | * the maximum width of the resulting image 640 | * @param height 641 | * the maximum height of the resulting image 642 | */ 643 | public void scheduleLoadImage(int id, Uri image, int width, int height) { 644 | if (DEBUG) { 645 | Log.d(TAG, "executing new ImageLoadTask in background..."); 646 | } 647 | final ImageLoadTask imt = new ImageLoadTask(id, image, width, height); 648 | 649 | jobs.put(id, imt); 650 | mExecutor.execute(imt); 651 | } 652 | 653 | /** 654 | * Deprecated in favour of {@link #scheduleLoadImage(int, Uri, int, int)}. 655 | * 656 | * @param id 657 | * @param image 658 | * @param width 659 | * @param height 660 | */ 661 | @Deprecated 662 | public void scheduleLoadImage(long id, Uri image, int width, int height) { 663 | scheduleLoadImage(id, image, width, height); 664 | } 665 | 666 | /** 667 | * Cancels all the asynchronous image loads. Note: currently does not function properly. 668 | * 669 | */ 670 | public void cancelLoads() { 671 | jobs.clear(); 672 | mExecutor.getQueue().clear(); 673 | } 674 | 675 | public void cancel(int id) { 676 | synchronized (jobs) { 677 | final Runnable job = jobs.get(id); 678 | if (job != null) { 679 | jobs.remove(id); 680 | mExecutor.remove(job); 681 | if (DEBUG) { 682 | Log.d(TAG, "removed load id " + id); 683 | } 684 | } 685 | } 686 | } 687 | 688 | /** 689 | * Deprecated in favour of {@link #cancel(int)}. 690 | * 691 | * @param id 692 | */ 693 | @Deprecated 694 | public void cancel(long id) { 695 | cancel(id); 696 | } 697 | 698 | /** 699 | * Blocking call to scale a local file. Scales using preserving aspect ratio 700 | * 701 | * @param localFile 702 | * local image file to be scaled 703 | * @param width 704 | * maximum width 705 | * @param height 706 | * maximum height 707 | * @return the scaled image 708 | * @throws ClientProtocolException 709 | * @throws IOException 710 | */ 711 | private static Bitmap scaleLocalImage(File localFile, int width, int height) 712 | throws ClientProtocolException, IOException { 713 | 714 | if (DEBUG) { 715 | Log.d(TAG, "scaleLocalImage(" + localFile + ", " + width + ", " + height + ")"); 716 | } 717 | 718 | if (!localFile.exists()) { 719 | throw new IOException("local file does not exist: " + localFile); 720 | } 721 | if (!localFile.canRead()) { 722 | throw new IOException("cannot read from local file: " + localFile); 723 | } 724 | 725 | // the below borrowed from: 726 | // https://github.com/thest1/LazyList/blob/master/src/com/fedorvlasov/lazylist/ImageLoader.java 727 | 728 | // decode image size 729 | final BitmapFactory.Options o = new BitmapFactory.Options(); 730 | o.inJustDecodeBounds = true; 731 | 732 | BitmapFactory.decodeStream(new FileInputStream(localFile), null, o); 733 | 734 | // Find the correct scale value. It should be the power of 2. 735 | //final int REQUIRED_WIDTH = width, REQUIRED_HEIGHT = height; 736 | int width_tmp = o.outWidth, height_tmp = o.outHeight; 737 | int scale = 1; 738 | while (true) { 739 | if (width_tmp / 2 <= width || height_tmp / 2 <= height) { 740 | break; 741 | } 742 | width_tmp /= 2; 743 | height_tmp /= 2; 744 | scale *= 2; 745 | } 746 | 747 | // decode with inSampleSize 748 | final BitmapFactory.Options o2 = new BitmapFactory.Options(); 749 | o2.inSampleSize = scale; 750 | final Bitmap prescale = BitmapFactory 751 | .decodeStream(new FileInputStream(localFile), null, o2); 752 | 753 | if (prescale == null) { 754 | Log.e(TAG, localFile + " could not be decoded"); 755 | } else if (DEBUG) { 756 | Log.d(TAG, "Successfully completed scaling of " + localFile + " to " + width + "x" 757 | + height); 758 | } 759 | 760 | return prescale; 761 | } 762 | 763 | /** 764 | * Blocking call to download an image. The image is placed directly into the disk cache at the 765 | * given key. 766 | * 767 | * @param uri 768 | * the location of the image 769 | * @return a decoded bitmap 770 | * @throws ClientProtocolException 771 | * if the HTTP response code wasn't 200 or any other HTTP errors 772 | * @throws IOException 773 | */ 774 | protected void downloadImage(String key, Uri uri) throws ClientProtocolException, IOException { 775 | if (DEBUG) { 776 | Log.d(TAG, "downloadImage(" + key + ", " + uri + ")"); 777 | } 778 | if (USE_APACHE_NC) { 779 | final HttpGet get = new HttpGet(uri.toString()); 780 | final HttpParams params = get.getParams(); 781 | params.setParameter(ClientPNames.HANDLE_REDIRECTS, true); 782 | 783 | final HttpResponse hr = hc.execute(get); 784 | final StatusLine hs = hr.getStatusLine(); 785 | if (hs.getStatusCode() != 200) { 786 | throw new HttpResponseException(hs.getStatusCode(), hs.getReasonPhrase()); 787 | } 788 | 789 | final HttpEntity ent = hr.getEntity(); 790 | 791 | // TODO I think this means that the source file must be a jpeg. fix this. 792 | try { 793 | 794 | putRaw(key, ent.getContent()); 795 | if (DEBUG) { 796 | Log.d(TAG, "source file of " + uri + " saved to disk cache at location " 797 | + getFile(key).getAbsolutePath()); 798 | } 799 | } finally { 800 | ent.consumeContent(); 801 | } 802 | } else { 803 | final URLConnection con = new URL(uri.toString()).openConnection(); 804 | putRaw(key, con.getInputStream()); 805 | if (DEBUG) { 806 | Log.d(TAG, 807 | "source file of " + uri + " saved to disk cache at location " 808 | + getFile(key).getAbsolutePath()); 809 | } 810 | } 811 | 812 | } 813 | 814 | private void notifyListeners(LoadResult result) { 815 | for (final OnImageLoadListener listener : mImageLoadListeners) { 816 | listener.onImageLoaded(result.id, result.image, result.drawable); 817 | } 818 | } 819 | 820 | /** 821 | * Implement this and register it using 822 | * {@link ImageCache#registerOnImageLoadListener(OnImageLoadListener)} to be notified when 823 | * asynchronous image loads have completed. 824 | * 825 | * @author Steve Pomeroy 826 | * 827 | */ 828 | public interface OnImageLoadListener { 829 | /** 830 | * Called when the image has been loaded and scaled. 831 | * 832 | * @param id 833 | * the ID provided by {@link ImageCache#loadImage(int, Uri, int, int)} or 834 | * {@link ImageCache#scheduleLoadImage(int, Uri, int, int)} 835 | * @param imageUri 836 | * the uri of the image that was originally requested 837 | * @param image 838 | * the loaded and scaled image 839 | */ 840 | public void onImageLoaded(int id, Uri imageUri, Drawable image); 841 | } 842 | } 843 | -------------------------------------------------------------------------------- /src/edu/mit/mobile/android/imagecache/ImageCacheException.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.imagecache; 2 | 3 | /* 4 | * Copyright (C) 2011-2012 MIT Mobile Experience Lab 5 | * 6 | * This library is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 2.1 of the License, or (at your option) any later version. 10 | * 11 | * This library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this library; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 | */ 20 | public class ImageCacheException extends Exception { 21 | 22 | /** 23 | * 24 | */ 25 | private static final long serialVersionUID = 997874306474290980L; 26 | 27 | public ImageCacheException(String message) { 28 | super(message); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/edu/mit/mobile/android/imagecache/ImageLoaderAdapter.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.imagecache; 2 | 3 | /* 4 | * Copyright (C) 2011-2013 MIT Mobile Experience Lab 5 | * 6 | * This library is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 2.1 of the License, or (at your option) any later version. 10 | * 11 | * This library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this library; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 | */ 20 | import java.io.IOException; 21 | import java.lang.ref.SoftReference; 22 | 23 | import android.app.Activity; 24 | import android.content.Context; 25 | import android.graphics.drawable.Drawable; 26 | import android.net.Uri; 27 | import android.util.Log; 28 | import android.util.SparseArray; 29 | import android.view.View; 30 | import android.view.ViewGroup; 31 | import android.widget.ImageView; 32 | import android.widget.ListAdapter; 33 | 34 | import com.commonsware.cwac.adapter.AdapterWrapper; 35 | 36 | /** 37 | *

38 | * An adapter that wraps another adapter, loading images into ImageViews asynchronously. 39 | *

40 | * 41 | *

42 | * To use, pass in a {@link ListAdapter} that generates {@link ImageView}s in the layout hierarchy 43 | * of getView(). ImageViews are searched for using the IDs specified in {@code imageViewIDs}. When 44 | * found, {@link ImageView#getTag(R.id.ic__uri)} is called and should return a {@link Uri} 45 | * referencing a local or remote image. See {@link ImageCache#loadImage(int, Uri, int, int)} for 46 | * details on the types of URIs and images supported. 47 | *

48 | * 49 | * @author Steve Pomeroy 50 | * 51 | */ 52 | public class ImageLoaderAdapter extends AdapterWrapper implements ImageCache.OnImageLoadListener { 53 | private static final String TAG = ImageLoaderAdapter.class.getSimpleName(); 54 | 55 | /** 56 | * The unit specified is in pixels 57 | */ 58 | public static final int UNIT_PX = 0; 59 | 60 | /** 61 | * The unit specified is in density-independent pixels (DIP) 62 | */ 63 | public static final int UNIT_DIP = 1; 64 | 65 | // ////////////////////////////////////////////// 66 | // / private 67 | // ////////////////////////////////////////////// 68 | 69 | private final SparseArray> mImageViewsToLoad = new SparseArray>(); 70 | 71 | private final int[] mImageViewIDs; 72 | private final ImageCache mCache; 73 | 74 | private final int mDefaultWidth, mDefaultHeight; 75 | 76 | private final boolean mAutosize; 77 | 78 | private final SparseArray mViewDimensionCache; 79 | 80 | // /////////////////////////////////////////////// 81 | 82 | /** 83 | * Like the 84 | * {@link #ImageLoaderAdapter(Context, ListAdapter, ImageCache, int[], int, int, int, boolean)} 85 | * constructor with a default of {@code true} for autosize. 86 | * 87 | * @param context 88 | * a context for getting the display density. You don't need to worry about this 89 | * class holding on to a reference to this: it's only used in the constructor. 90 | * @param wrapped 91 | * the adapter that's wrapped. See {@link ImageLoaderAdapter} for the requirements of 92 | * using this adapter wrapper. 93 | * @param cache 94 | * an instance of your image cache. This can be shared with the process. 95 | * @param imageViewIDs 96 | * a list of resource IDs matching the ImageViews that should be scanned and loaded. 97 | * @param defaultWidth 98 | * the default maximum width, in the specified unit. This size will be used if the 99 | * size cannot be obtained from the view. 100 | * @param defaultHeight 101 | * the default maximum height, in the specified unit. This size will be used if the 102 | * size cannot be obtained from the view. 103 | * @param unit 104 | * one of {@link #UNIT_PX} or {@link #UNIT_DIP} 105 | */ 106 | public ImageLoaderAdapter(Context context, ListAdapter wrapped, ImageCache cache, 107 | int[] imageViewIDs, int defaultWidth, int defaultHeight, int unit) { 108 | this(context, wrapped, cache, imageViewIDs, defaultWidth, defaultHeight, unit, true); 109 | } 110 | 111 | /** 112 | * @param context 113 | * a context for getting the display density. You don't need to worry about this 114 | * class holding on to a reference to this: it's only used in the constructor. 115 | * @param wrapped 116 | * the adapter that's wrapped. See {@link ImageLoaderAdapter} for the requirements of 117 | * using this adapter wrapper. 118 | * @param cache 119 | * an instance of your image cache. This can be shared with the process. 120 | * @param imageViewIDs 121 | * a list of resource IDs matching the ImageViews that should be scanned and loaded. 122 | * @param defaultWidth 123 | * the default maximum width, in the specified unit. This size will be used if the 124 | * size cannot be obtained from the view. 125 | * @param defaultHeight 126 | * the default maximum height, in the specified unit. This size will be used if the 127 | * size cannot be obtained from the view. 128 | * @param unit 129 | * one of {@link #UNIT_PX} or {@link #UNIT_DIP} 130 | * @param autosize 131 | * if true, the view's dimensions will be cached the first time it's loaded and an 132 | * image of the appropriate size will be requested the next time an image is loaded. 133 | * False uses defaultWidth and defaultHeight only. 134 | */ 135 | public ImageLoaderAdapter(Context context, ListAdapter wrapped, ImageCache cache, 136 | int[] imageViewIDs, int defaultWidth, int defaultHeight, int unit, boolean autosize) { 137 | super(wrapped); 138 | 139 | mImageViewIDs = imageViewIDs; 140 | mCache = cache; 141 | mCache.registerOnImageLoadListener(this); 142 | 143 | mAutosize = autosize; 144 | 145 | if (autosize) { 146 | mViewDimensionCache = new SparseArray(); 147 | } else { 148 | mViewDimensionCache = null; 149 | } 150 | 151 | switch (unit) { 152 | case UNIT_PX: 153 | mDefaultHeight = defaultHeight; 154 | mDefaultWidth = defaultWidth; 155 | break; 156 | 157 | case UNIT_DIP: { 158 | final float scale = context.getResources().getDisplayMetrics().density; 159 | mDefaultHeight = (int) (defaultHeight * scale); 160 | mDefaultWidth = (int) (defaultWidth * scale); 161 | } 162 | break; 163 | 164 | default: 165 | throw new IllegalArgumentException("invalid unit type"); 166 | 167 | } 168 | } 169 | 170 | /** 171 | * Constructs a new adapter with a default unit of pixels. 172 | * 173 | * @param wrapped 174 | * the adapter that's wrapped. See {@link ImageLoaderAdapter} for the requirements of 175 | * using this adapter wrapper. 176 | * @param cache 177 | * an instance of your image cache. This can be shared with the process. 178 | * @param imageViewIDs 179 | * a list of resource IDs matching the ImageViews that should be scan 180 | * @param width 181 | * the maximum width, in pixels 182 | * @param height 183 | * the maximum height, in pixels 184 | */ 185 | public ImageLoaderAdapter(ListAdapter wrapped, ImageCache cache, int[] imageViewIDs, int width, 186 | int height) { 187 | this(null, wrapped, cache, imageViewIDs, width, height, UNIT_PX); 188 | } 189 | 190 | @Override 191 | protected void finalize() throws Throwable { 192 | unregisterOnImageLoadListener(); 193 | super.finalize(); 194 | } 195 | 196 | /** 197 | * This can be called from your {@link Activity#onResume()} method. 198 | */ 199 | public void registerOnImageLoadListener() { 200 | mCache.registerOnImageLoadListener(this); 201 | } 202 | 203 | /** 204 | * This can be called from your {@link Activity#onPause()} method. 205 | */ 206 | public void unregisterOnImageLoadListener() { 207 | mCache.unregisterOnImageLoadListener(this); 208 | } 209 | 210 | @Override 211 | public View getView(int position, View convertView, ViewGroup parent) { 212 | final View v = super.getView(position, convertView, parent); 213 | 214 | for (final int id : mImageViewIDs) { 215 | if (convertView != null) { 216 | final ImageView iv = (ImageView) convertView.findViewById(id); 217 | if (iv != null) { 218 | final Integer tagId = (Integer) iv.getTag(R.id.ic__load_id); 219 | if (tagId != null) { 220 | mCache.cancel(tagId); 221 | } 222 | } 223 | } 224 | 225 | final ImageView iv = (ImageView) v.findViewById(id); 226 | if (iv == null) { 227 | continue; 228 | } 229 | 230 | final Uri tag = (Uri) iv.getTag(R.id.ic__uri); 231 | // short circuit if there's no tag 232 | if (tag == null) { 233 | continue; 234 | } 235 | 236 | ViewDimensionCache viewDimension = null; 237 | 238 | if (mAutosize) { 239 | viewDimension = mViewDimensionCache.get(id); 240 | if (viewDimension == null) { 241 | final int w = iv.getMeasuredWidth(); 242 | final int h = iv.getMeasuredHeight(); 243 | if (w > 0 && h > 0) { 244 | viewDimension = new ViewDimensionCache(); 245 | viewDimension.width = w; 246 | viewDimension.height = h; 247 | mViewDimensionCache.put(id, viewDimension); 248 | } 249 | } 250 | } 251 | 252 | final int imageID = mCache.getNewID(); 253 | 254 | // ic__load_id is used to keep track of what load ID is associated with what 255 | // particular ImageView 256 | 257 | iv.setTag(R.id.ic__load_id, imageID); 258 | // attempt to bypass all the loading machinery to get the image loaded as quickly 259 | // as possible 260 | Drawable d = null; 261 | try { 262 | if (viewDimension != null && viewDimension.width > 0 && viewDimension.height > 0) { 263 | d = mCache.loadImage(imageID, tag, viewDimension.width, viewDimension.height); 264 | } else { 265 | d = mCache.loadImage(imageID, tag, mDefaultWidth, mDefaultHeight); 266 | } 267 | } catch (final IOException e) { 268 | e.printStackTrace(); 269 | } 270 | if (d != null) { 271 | iv.setImageDrawable(d); 272 | } else { 273 | if (ImageCache.DEBUG) { 274 | Log.d(TAG, "scheduling load with ID: " + imageID + "; URI;" + tag); 275 | } 276 | mImageViewsToLoad.put(imageID, new SoftReference(iv)); 277 | } 278 | 279 | } 280 | return v; 281 | } 282 | 283 | @Override 284 | public void onImageLoaded(int id, Uri imageUri, Drawable image) { 285 | final SoftReference ivRef = mImageViewsToLoad.get(id); 286 | if (ivRef == null) { 287 | return; 288 | } 289 | final ImageView iv = ivRef.get(); 290 | if (iv == null) { 291 | mImageViewsToLoad.remove(id); 292 | return; 293 | } 294 | if (ImageCache.DEBUG) { 295 | Log.d(TAG, "loading ID " + id + " with an image"); 296 | } 297 | if (imageUri.equals(iv.getTag(R.id.ic__uri))) { 298 | iv.setImageDrawable(image); 299 | } 300 | mImageViewsToLoad.remove(id); 301 | } 302 | 303 | private static class ViewDimensionCache { 304 | int width; 305 | int height; 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/edu/mit/mobile/android/imagecache/KeyedLock.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.imagecache; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.concurrent.locks.ReentrantLock; 6 | 7 | import android.util.Log; 8 | 9 | /** 10 | * A synchronization lock that creates a separate lock for each key. 11 | * 12 | * @author Steve Pomeroy 13 | * 14 | * @param 15 | */ 16 | public class KeyedLock { 17 | private static final String TAG = KeyedLock.class.getSimpleName(); 18 | 19 | private final Map mLocks = new HashMap(); 20 | 21 | private static boolean DEBUG = false; 22 | 23 | /** 24 | * @param key 25 | */ 26 | public void lock(K key) { 27 | if (DEBUG) { 28 | log("acquiring lock for key " + key); 29 | } 30 | 31 | ReentrantLock lock; 32 | synchronized (mLocks) { 33 | lock = mLocks.get(key); 34 | if (lock == null) { 35 | lock = new ReentrantLock(); 36 | mLocks.put(key, lock); 37 | if (DEBUG) { 38 | log(lock + " created new lock and added it to map"); 39 | } 40 | 41 | } 42 | } 43 | 44 | lock.lock(); 45 | } 46 | 47 | /** 48 | * @param key 49 | */ 50 | public void unlock(K key) { 51 | if (DEBUG) { 52 | log("unlocking lock for key " + key); 53 | } 54 | ReentrantLock lock; 55 | 56 | synchronized (mLocks) { 57 | lock = mLocks.get(key); 58 | if (lock == null) { 59 | Log.e(TAG, "Attempting to unlock lock for key " + key + " which has no entry"); 60 | return; 61 | } 62 | if (DEBUG) { 63 | log(lock + " has queued threads " + lock.hasQueuedThreads() + " for key " + key); 64 | } 65 | // maybe entries should be removed when there are no queued threads. This would 66 | // occasionally fail... 67 | // final boolean queued = lock.hasQueuedThreads(); 68 | 69 | lock.unlock(); 70 | } 71 | } 72 | 73 | private void log(String message) { 74 | 75 | Log.d(TAG, Thread.currentThread().getId() + "\t" + message); 76 | 77 | } 78 | } -------------------------------------------------------------------------------- /src/edu/mit/mobile/android/imagecache/SimpleThumbnailAdapter.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.imagecache; 2 | 3 | /* 4 | * Copyright (C) 2011-2012 MIT Mobile Experience Lab 5 | * 6 | * This library is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 2.1 of the License, or (at your option) any later version. 10 | * 11 | * This library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this library; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 | */ 20 | import java.util.List; 21 | import java.util.Map; 22 | 23 | import android.content.Context; 24 | import android.graphics.drawable.Drawable; 25 | import android.net.Uri; 26 | import android.view.LayoutInflater; 27 | import android.view.View; 28 | import android.widget.ImageView; 29 | import android.widget.SimpleAdapter; 30 | 31 | public class SimpleThumbnailAdapter extends SimpleAdapter { 32 | private final Drawable defaultImages[]; 33 | private final int[] mImageIDs; 34 | 35 | public SimpleThumbnailAdapter(Context context, 36 | List> data, int layout, String[] from, 37 | int[] to, int[]imageIDs) { 38 | super(context, data, layout, from, to); 39 | 40 | final View v = LayoutInflater.from(context) 41 | .inflate(layout, null, false); 42 | defaultImages = new Drawable[imageIDs.length]; 43 | 44 | mImageIDs = imageIDs; 45 | 46 | for (int i = 0; i < mImageIDs.length; i++) { 47 | final ImageView thumb = (ImageView) v.findViewById(imageIDs[i]); 48 | defaultImages[i] = thumb.getDrawable(); 49 | } 50 | 51 | } 52 | 53 | @Override 54 | public void setViewImage(ImageView v, String value) { 55 | final int id = v.getId(); 56 | for (int i = 0; i < mImageIDs.length; i++) { 57 | if (id == mImageIDs[i]) { 58 | setViewImageAndTag(v, value, defaultImages[i]); 59 | } 60 | } 61 | } 62 | 63 | private void setViewImageAndTag(ImageView v, String value, 64 | Drawable defaultImage) { 65 | v.setImageDrawable(defaultImage); 66 | if (value != null && value.length() > 0) { 67 | v.setTag(R.id.ic__uri, Uri.parse(value)); 68 | } else { 69 | v.setTag(R.id.ic__uri, null); 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/edu/mit/mobile/android/imagecache/SimpleThumbnailCursorAdapter.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.imagecache; 2 | 3 | /* 4 | * Copyright (C) 2011-2012 MIT Mobile Experience Lab 5 | * 6 | * This library is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 2.1 of the License, or (at your option) any later version. 10 | * 11 | * This library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this library; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 | */ 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | import android.content.Context; 24 | import android.database.Cursor; 25 | import android.graphics.drawable.Drawable; 26 | import android.net.Uri; 27 | import android.support.v4.widget.SimpleCursorAdapter; 28 | import android.util.SparseArray; 29 | import android.view.LayoutInflater; 30 | import android.view.View; 31 | import android.view.ViewGroup; 32 | import android.widget.ImageView; 33 | 34 | /** 35 | * Like the SimpleCursorAdapter, but has support for setting thumbnails that can 36 | * be loaded using a wrapped {@link ImageLoaderAdapter}. 37 | * 38 | * Multiple image fields can be mapped to the same image view and the first one 39 | * with a valid image will be loaded. 40 | * 41 | * Additionally, it shows an indeterminate progress bar when the adapter is 42 | * null. This progress bar will go away once the adapter has been set. 43 | * 44 | * @author steve 45 | * 46 | */ 47 | public class SimpleThumbnailCursorAdapter extends SimpleCursorAdapter { 48 | 49 | private final Drawable defaultImages[]; 50 | private final int[] mImageIDs; 51 | // XXX HACK the alternate images system is sorta a hack. 52 | private final SparseArray> mAlternateImages = new SparseArray>(); 53 | 54 | private final Context mContext; 55 | 56 | private boolean mShowIndeterminate = false; 57 | private int mExpectedCount = -1; 58 | private View mIndeterminate; 59 | 60 | /** 61 | * All parameters are passed directly to {@link SimpleCursorAdapter} 62 | * {@link #SimpleThumbnailCursorAdapter(Context, int, Cursor, String[], int[], int[], int)} 63 | * 64 | * @param context 65 | * @param layout 66 | * @param c 67 | * @param from 68 | * You can load alternate images by specifying multiple from IDs 69 | * mapping to the same TO ID. This only works for images, as 70 | * listed below. 71 | * @param to 72 | * @param imageIDs 73 | * a list of ImageView IDs whose images will be loaded by this 74 | * adapter. 75 | * @param flags 76 | */ 77 | public SimpleThumbnailCursorAdapter(Context context, int layout, Cursor c, 78 | String[] from, int[] to, int[] imageIDs, int flags) { 79 | super(context, layout, c, from, to, flags); 80 | 81 | mContext = context; 82 | 83 | for (final int imageID : imageIDs) { 84 | final List alternates = new ArrayList(); 85 | mAlternateImages.put(imageID, alternates); 86 | for (int i = 0; i < to.length; i++) { 87 | if (to[i] == imageID) { 88 | alternates.add(from[i]); 89 | } 90 | } 91 | 92 | setIndeterminateLoading(c == null || isNotShowingExpectedCount(c)); 93 | } 94 | 95 | final View v = LayoutInflater.from(context) 96 | .inflate(layout, null, false); 97 | defaultImages = new Drawable[imageIDs.length]; 98 | 99 | mImageIDs = imageIDs; 100 | 101 | for (int i = 0; i < mImageIDs.length; i++) { 102 | final ImageView thumb = (ImageView) v.findViewById(imageIDs[i]); 103 | defaultImages[i] = thumb.getDrawable(); 104 | } 105 | } 106 | 107 | @Override 108 | public void setViewImage(ImageView v, String value) { 109 | final int id = v.getId(); 110 | for (int i = 0; i < mImageIDs.length; i++) { 111 | if (id == mImageIDs[i]) { 112 | final List alternates = mAlternateImages.get(id); 113 | if (alternates != null && alternates.size() > 1) { 114 | final Cursor c = getCursor(); 115 | for (final String alternate : alternates) { 116 | final int idx = c.getColumnIndex(alternate); 117 | if (c.isNull(idx)) { 118 | continue; 119 | } else { 120 | // only set the first one that isn't null 121 | setViewImageAndTag(v, c.getString(idx), 122 | defaultImages[i]); 123 | break; 124 | } 125 | } 126 | } else { 127 | setViewImageAndTag(v, value, defaultImages[i]); 128 | } 129 | } 130 | } 131 | } 132 | 133 | private void setViewImageAndTag(ImageView v, String value, 134 | Drawable defaultImage) { 135 | v.setImageDrawable(defaultImage); 136 | if (value != null && value.length() > 0) { 137 | v.setTag(R.id.ic__uri, Uri.parse(value)); 138 | } else { 139 | v.setTag(R.id.ic__uri, null); 140 | } 141 | } 142 | 143 | private void setIndeterminateLoading(boolean isLoading) { 144 | if (isLoading != mShowIndeterminate) { 145 | mShowIndeterminate = isLoading; 146 | notifyDataSetInvalidated(); 147 | notifyDataSetChanged(); 148 | } 149 | } 150 | 151 | @Override 152 | public int getCount() { 153 | if (mShowIndeterminate) { 154 | return 1; 155 | } else { 156 | return super.getCount(); 157 | } 158 | } 159 | 160 | @Override 161 | public View getView(int position, View convertView, ViewGroup parent) { 162 | final boolean convertViewIsProgressBar = convertView != null 163 | && convertView.getId() == R.id.progress; 164 | 165 | if (mShowIndeterminate) { 166 | if (convertViewIsProgressBar) { 167 | return convertView; 168 | } 169 | 170 | if (mIndeterminate == null) { 171 | mIndeterminate = LayoutInflater.from(mContext).inflate(R.layout.list_loading, 172 | parent, false); 173 | } 174 | 175 | return mIndeterminate; 176 | 177 | } else { 178 | // ensure that we don't reuse the indeterminate progress bar as a 179 | // real view 180 | if (convertViewIsProgressBar) { 181 | convertView = null; 182 | } 183 | return super.getView(position, convertView, parent); 184 | } 185 | } 186 | 187 | @Override 188 | public Cursor swapCursor(Cursor c) { 189 | setIndeterminateLoading(c == null || isNotShowingExpectedCount(c)); 190 | return super.swapCursor(c); 191 | } 192 | 193 | @Override 194 | public void changeCursor(Cursor cursor) { 195 | super.changeCursor(cursor); 196 | setIndeterminateLoading(cursor == null || isNotShowingExpectedCount(cursor)); 197 | } 198 | 199 | private boolean isNotShowingExpectedCount(Cursor c){ 200 | return c != null && (mExpectedCount > -1 && (c.getCount() != mExpectedCount)); 201 | } 202 | 203 | public void setExpectedCount(int expectedCount){ 204 | mExpectedCount = expectedCount; 205 | setIndeterminateLoading(mCursor == null || isNotShowingExpectedCount(mCursor)); 206 | } 207 | 208 | @Override 209 | public boolean areAllItemsEnabled() { 210 | return false; 211 | } 212 | 213 | @Override 214 | public boolean isEnabled(int position) { 215 | 216 | if (mShowIndeterminate){ 217 | return false; 218 | }else{ 219 | return super.isEnabled(position); 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | gen/ 3 | -------------------------------------------------------------------------------- /test/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 17 | 18 | 21 | 22 | 25 | 26 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/assets/logo_locast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmel/Android-Image-Cache/b3b093d945b7ddfa3f68078fe553d6f634d29676/test/assets/logo_locast.png -------------------------------------------------------------------------------- /test/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system use, 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | 10 | android.library.reference.1=.. 11 | # Project target. 12 | target=android-16 13 | -------------------------------------------------------------------------------- /test/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmel/Android-Image-Cache/b3b093d945b7ddfa3f68078fe553d6f634d29676/test/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /test/res/drawable-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmel/Android-Image-Cache/b3b093d945b7ddfa3f68078fe553d6f634d29676/test/res/drawable-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /test/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmel/Android-Image-Cache/b3b093d945b7ddfa3f68078fe553d6f634d29676/test/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /test/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmel/Android-Image-Cache/b3b093d945b7ddfa3f68078fe553d6f634d29676/test/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /test/res/layout/activity_concurrency_test.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /test/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /test/res/layout/small_thumbnail_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | -------------------------------------------------------------------------------- /test/res/layout/square_thumbnail_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | -------------------------------------------------------------------------------- /test/res/layout/thumbnail_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | -------------------------------------------------------------------------------- /test/res/menu/main_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 14 | 19 | 20 | -------------------------------------------------------------------------------- /test/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ImageCache Test 4 | 5 | -------------------------------------------------------------------------------- /test/src/edu/mit/mobile/android/imagecache/test/ConcurrencyTest.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.imagecache.test; 2 | 3 | import java.io.IOException; 4 | import java.util.Random; 5 | 6 | import org.apache.http.client.ClientProtocolException; 7 | 8 | import android.app.Activity; 9 | import android.content.Context; 10 | import android.graphics.Bitmap.CompressFormat; 11 | import android.net.Uri; 12 | import android.os.Bundle; 13 | import android.view.Menu; 14 | import android.view.MenuItem; 15 | import android.widget.GridView; 16 | import android.widget.Toast; 17 | import edu.mit.mobile.android.imagecache.ImageCache; 18 | 19 | public class ConcurrencyTest extends Activity { 20 | 21 | private GridView mGrid; 22 | private ImageCache mCache; 23 | private TestData mData; 24 | 25 | @Override 26 | protected void onCreate(Bundle savedInstanceState) { 27 | super.onCreate(savedInstanceState); 28 | 29 | setContentView(R.layout.activity_concurrency_test); 30 | 31 | mCache = new SlowImageCache(this); 32 | 33 | mGrid = (GridView) findViewById(R.id.grid); 34 | 35 | mData = new TestData(); 36 | 37 | initData(); 38 | 39 | mGrid.setAdapter(TestData.generateAdapter(this, mData, R.layout.square_thumbnail_item, 40 | mCache, 64, 64)); 41 | } 42 | 43 | private void initData() { 44 | mData.addItem( 45 | "Federico", 46 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/person_profile/sites/mel-drudev.mit.edu/files/pic_64px_boss.jpg"); 47 | mData.addItem( 48 | "Leo", 49 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/person_profile/sites/mel-drudev.mit.edu/files/leonardo_0.jpg"); 50 | 51 | mData.addItem( 52 | "Nick", 53 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/person_profile/nwallen_pic.jpg"); 54 | 55 | mData.addItem( 56 | "Steve", 57 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/person_profile/sites/mel-drudev.mit.edu/files/pic_64px_steve.jpg"); 58 | 59 | mData.addItem( 60 | "Amar", 61 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/person_profile/me-icon_0.png"); 62 | 63 | for (int i = 0; i < 10; i++) { 64 | mData.addAll(mData); 65 | } 66 | } 67 | 68 | private void trim() { 69 | final long trimmed = mCache.trim(); 70 | Toast.makeText(this, trimmed + " byte(s) trimmed.", Toast.LENGTH_LONG).show(); 71 | } 72 | 73 | private void clear() { 74 | mCache.clear(); 75 | Toast.makeText(this, "Cache cleared.", Toast.LENGTH_LONG).show(); 76 | } 77 | 78 | @Override 79 | public boolean onOptionsItemSelected(MenuItem item) { 80 | switch (item.getItemId()) { 81 | case R.id.clear: 82 | clear(); 83 | return true; 84 | 85 | case R.id.trim: 86 | 87 | trim(); 88 | return true; 89 | 90 | default: 91 | return super.onOptionsItemSelected(item); 92 | } 93 | } 94 | 95 | @Override 96 | public boolean onCreateOptionsMenu(Menu menu) { 97 | super.onCreateOptionsMenu(menu); 98 | getMenuInflater().inflate(R.menu.main_menu, menu); 99 | menu.findItem(R.id.grid).setVisible(false); 100 | return true; 101 | } 102 | 103 | private static class SlowImageCache extends ImageCache { 104 | 105 | private final Random r = new Random(); 106 | 107 | protected SlowImageCache(Context context) { 108 | super(context, CompressFormat.JPEG, 85); 109 | } 110 | 111 | @Override 112 | protected void downloadImage(String key, Uri uri) throws ClientProtocolException, 113 | IOException { 114 | try { 115 | Thread.sleep(r.nextInt(3000) + 500); 116 | } catch (final InterruptedException e) { 117 | 118 | } 119 | super.downloadImage(key, uri); 120 | 121 | } 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /test/src/edu/mit/mobile/android/imagecache/test/ImageCacheJunitTest.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.imagecache.test; 2 | 3 | /* 4 | * Copyright (C) 2011-2013 MIT Mobile Experience Lab 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU General Public License 8 | * as published by the Free Software Foundation; either version 2 9 | * of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | 21 | import java.io.File; 22 | import java.io.FileOutputStream; 23 | import java.io.IOException; 24 | import java.io.InputStream; 25 | 26 | import org.apache.http.client.ClientProtocolException; 27 | 28 | import android.content.Context; 29 | import android.graphics.Bitmap; 30 | import android.graphics.BitmapFactory; 31 | import android.graphics.drawable.Drawable; 32 | import android.net.Uri; 33 | import android.test.InstrumentationTestCase; 34 | import android.test.suitebuilder.annotation.LargeTest; 35 | import edu.mit.mobile.android.imagecache.ImageCache; 36 | import edu.mit.mobile.android.imagecache.ImageCacheException; 37 | 38 | public class ImageCacheJunitTest extends InstrumentationTestCase { 39 | @SuppressWarnings("unused") 40 | private static final String TAG = ImageCacheJunitTest.class.getSimpleName(); 41 | 42 | private ImageCache imc; 43 | 44 | @Override 45 | protected void setUp() throws Exception { 46 | super.setUp(); 47 | 48 | imc = ImageCache.getInstance(getInstrumentation().getTargetContext()); 49 | } 50 | 51 | public void testPreconditions() { 52 | assertNotNull(imc); 53 | } 54 | 55 | public void testClear() { 56 | assertTrue(imc.clear()); 57 | assertEquals(0, imc.getCacheEntryCount()); 58 | } 59 | 60 | public void testGetPut() throws IOException { 61 | final Context contextInst = getInstrumentation().getContext(); 62 | testClear(); 63 | 64 | final String key01 = "foo"; 65 | final String key02 = "bar"; 66 | Bitmap bmp = BitmapFactory.decodeResource(contextInst.getResources(), 67 | R.drawable.ic_launcher); 68 | assertTrue(bmp.getHeight() > 0); 69 | assertTrue(bmp.getWidth() > 0); 70 | 71 | imc.put(key01, bmp); 72 | 73 | assertEquals(1, imc.getCacheEntryCount()); 74 | 75 | Bitmap bmpResult = imc.get(key01); 76 | assertNotNull(bmpResult); 77 | 78 | // check dimensions 79 | assertBitmapEqual(bmp, bmpResult); 80 | 81 | // check contents to ensure it's the same 82 | // TODO 83 | 84 | bmp = BitmapFactory.decodeResource(contextInst.getResources(), 85 | android.R.drawable.ic_dialog_alert); 86 | 87 | assertTrue(bmp.getHeight() > 0); 88 | assertTrue(bmp.getWidth() > 0); 89 | 90 | // call it again, ensure we overwrite 91 | imc.put(key01, bmp); 92 | assertEquals(1, imc.getCacheEntryCount()); 93 | 94 | bmpResult = imc.get(key01); 95 | assertNotNull(bmpResult); 96 | 97 | // check dimensions 98 | assertBitmapEqual(bmp, bmpResult); 99 | 100 | // test to make sure an empty result returns null 101 | assertNull(imc.get(key02)); 102 | 103 | testClear(); 104 | } 105 | 106 | private void assertBitmapMaxSize(int maxExpectedWidth, int maxExpectedHeight, Drawable actual) { 107 | assertTrue(maxExpectedWidth >= actual.getIntrinsicWidth()); 108 | assertTrue(maxExpectedHeight >= actual.getIntrinsicHeight()); 109 | 110 | } 111 | 112 | private void assertBitmapMinSize(int minExpectedWidth, int minExpectedHeight, Drawable actual) { 113 | assertTrue(minExpectedWidth <= actual.getIntrinsicWidth()); 114 | assertTrue(minExpectedHeight <= actual.getIntrinsicHeight()); 115 | 116 | } 117 | 118 | private void assertBitmapEqual(Bitmap expected, Bitmap actual) { 119 | assertEquals(expected.getHeight(), actual.getHeight()); 120 | assertEquals(expected.getWidth(), actual.getWidth()); 121 | } 122 | 123 | static final int LOCAL_SCALE_SIZE = 100; 124 | 125 | /** 126 | * Loads a file from the assets and saves it to a public location. 127 | * 128 | * @return 129 | * @throws IOException 130 | */ 131 | private Uri loadLocalFile() throws IOException { 132 | 133 | final String testfile = "logo_locast.png"; 134 | final Context contextInst = getInstrumentation().getContext(); 135 | final Context context = getInstrumentation().getTargetContext(); 136 | final InputStream is = contextInst.getAssets().open(testfile); 137 | 138 | assertNotNull(is); 139 | 140 | final FileOutputStream fos = context.openFileOutput(testfile, Context.MODE_PRIVATE); 141 | 142 | assertNotNull(fos); 143 | 144 | int read = 0; 145 | final byte[] bytes = new byte[1024]; 146 | 147 | while ((read = is.read(bytes)) != -1) { 148 | fos.write(bytes, 0, read); 149 | } 150 | 151 | is.close(); 152 | fos.close(); 153 | 154 | final File outFile = context.getFileStreamPath(testfile); 155 | 156 | final Uri fileUri = Uri.fromFile(outFile); 157 | 158 | assertNotNull(fileUri); 159 | return fileUri; 160 | } 161 | 162 | public void testLocalFileLoad() throws IOException, ImageCacheException { 163 | testClear(); 164 | 165 | final Uri fileUri = loadLocalFile(); 166 | 167 | final Drawable img = imc.getImage(fileUri, LOCAL_SCALE_SIZE, LOCAL_SCALE_SIZE); 168 | 169 | assertNotNull(img); 170 | 171 | // the thumbnails produced by this aren't precisely the size we request, due to efficiencies 172 | // in decoding the image. 173 | assertBitmapMaxSize(LOCAL_SCALE_SIZE * 2, LOCAL_SCALE_SIZE * 2, img); 174 | 175 | assertBitmapMinSize(LOCAL_SCALE_SIZE / 2, LOCAL_SCALE_SIZE / 2, img); 176 | 177 | } 178 | 179 | @LargeTest 180 | public void testTrim() throws IOException, ImageCacheException { 181 | testClear(); 182 | 183 | final Uri localFile = loadLocalFile(); 184 | 185 | imc.setAutoTrimFrequency(0); 186 | 187 | final int maxSize = 150; 188 | final int minSize = 50; 189 | final int entryCount = maxSize - minSize + 1 /* includes max size */; 190 | 191 | for (int i = minSize; i <= maxSize; i++) { 192 | final Drawable img = imc.getImage(localFile, i, i); 193 | 194 | assertNotNull(img); 195 | } 196 | 197 | assertEquals(entryCount, imc.getCacheEntryCount()); 198 | 199 | // cause a cache hit on the first item. 200 | imc.get(imc.getKey(localFile, minSize, minSize)); 201 | 202 | final long diskUsage = imc.getCacheDiskUsage(); 203 | 204 | assertTrue("Disk usage isn't reasonable", diskUsage > 1000 && diskUsage < 10 * 1024 * 1024); 205 | 206 | // actual disk usage should be around 479100 207 | 208 | final long cacheSize = 300 * 1024 /* kilo */; 209 | imc.setCacheMaxSize(cacheSize); 210 | 211 | final long trimmed = imc.trim(); 212 | 213 | assertTrue("no bytes were trimmed", trimmed > 0); 214 | 215 | assertTrue("disk usage hasn't changed", diskUsage != imc.getCacheDiskUsage()); 216 | 217 | assertTrue("disk usage is larger than desired max size", 218 | imc.getCacheDiskUsage() < cacheSize); 219 | 220 | assertTrue("entry count wasn't reduced", imc.getCacheEntryCount() < entryCount); 221 | 222 | // this should have the earliest access time, so it should be trimmed first 223 | assertFalse("second entry wasn't trimmed", 224 | imc.contains(imc.getKey(localFile, minSize + 1, minSize + 1))); 225 | 226 | // this has the most recent creation date, so it should be trimmed last 227 | assertTrue("last entry was trimmed", imc.contains(imc.getKey(localFile, maxSize, maxSize))); 228 | 229 | 230 | } 231 | 232 | private final int NET_SCALE_SIZE = 100; 233 | 234 | private void testNetworkLoad(Uri uri) throws IOException, ImageCacheException { 235 | 236 | // ensure we don't have it in the cache 237 | final String origKey = imc.getKey(uri); 238 | assertNull(imc.getDrawable(origKey)); 239 | 240 | final String scaledKey = imc.getKey(uri, NET_SCALE_SIZE, NET_SCALE_SIZE); 241 | assertNull(imc.getDrawable(scaledKey)); 242 | 243 | final Drawable img = imc.getImage(uri, NET_SCALE_SIZE, NET_SCALE_SIZE); 244 | 245 | assertNotNull(img); 246 | 247 | assertBitmapMaxSize(NET_SCALE_SIZE * 2, NET_SCALE_SIZE * 2, img); 248 | 249 | assertBitmapMinSize(NET_SCALE_SIZE / 2, NET_SCALE_SIZE / 2, img); 250 | 251 | // ensure that it's stored in the disk cache 252 | assertNotNull(imc.get(origKey)); 253 | assertNotNull(imc.get(scaledKey)); 254 | 255 | } 256 | 257 | public void testNetworkLoad() throws ClientProtocolException, IOException, ImageCacheException { 258 | testClear(); 259 | 260 | testNetworkLoad(Uri.parse("http://mobile-server.mit.edu/~stevep/logo_start_locast1.png")); 261 | } 262 | 263 | public void testNetworkLoadLarge() throws ClientProtocolException, IOException, 264 | ImageCacheException { 265 | testClear(); 266 | 267 | testNetworkLoad(Uri.parse("http://mobile-server.mit.edu/~stevep/large_logo.png")); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /test/src/edu/mit/mobile/android/imagecache/test/InteractiveDemo.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.imagecache.test; 2 | 3 | /* 4 | * Copyright (C) 2011-2013 MIT Mobile Experience Lab 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU General Public License 8 | * as published by the Free Software Foundation; either version 2 9 | * of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | import android.app.ListActivity; 21 | import android.content.Intent; 22 | import android.os.Bundle; 23 | import android.view.Menu; 24 | import android.view.MenuItem; 25 | import android.widget.Gallery; 26 | import android.widget.Toast; 27 | import edu.mit.mobile.android.imagecache.ImageCache; 28 | 29 | @SuppressWarnings("deprecation") 30 | public class InteractiveDemo extends ListActivity { 31 | /** Called when the activity is first created. */ 32 | private ImageCache mCache; 33 | 34 | private final TestData mTestData = new TestData(); 35 | 36 | @Override 37 | public void onCreate(Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | setContentView(R.layout.main); 40 | 41 | final Gallery gallery = (Gallery) findViewById(R.id.gallery); 42 | 43 | mCache = ImageCache.getInstance(this); 44 | mCache.setCacheMaxSize(1 * 1024 /* mega */* 1024 /* kilo */); 45 | 46 | initData(); 47 | 48 | setListAdapter(TestData.generateAdapter(this, mTestData, R.layout.thumbnail_item, mCache, 49 | 320, 200)); 50 | 51 | gallery.setAdapter(TestData.generateAdapter(this, mTestData, R.layout.small_thumbnail_item, 52 | mCache, 160, 100)); 53 | } 54 | 55 | private void initData() { 56 | 57 | mTestData 58 | .addItem( 59 | "locast tourism", 60 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/implementation_big/locast_tourism.jpg"); 61 | mTestData 62 | .addItem( 63 | "green home", 64 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/implementation_big/gha_01.jpg"); 65 | mTestData 66 | .addItem( 67 | "green home 2", 68 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/implementation_big/gha_05.jpg"); 69 | mTestData 70 | .addItem( 71 | "Locast healthcare", 72 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/implementation_big/locast%20healthcare.jpg"); 73 | mTestData 74 | .addItem( 75 | "locast h2flow 1", 76 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/implementation_big/water%20project%20IMAGE-72dpi.jpg"); 77 | mTestData 78 | .addItem( 79 | "locast h2flow 2", 80 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/implementation_big/H2flOw_image1.jpg"); 81 | mTestData 82 | .addItem( 83 | "locast h2flow 3", 84 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/implementation_big/H2flOw_image2.jpg"); 85 | mTestData 86 | .addItem( 87 | "locast unicef 1", 88 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/implementation_big/Screen%20shot%202011-08-16%20at%204.05.57%20PM.png"); 89 | mTestData 90 | .addItem( 91 | "locast unicef 2", 92 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/implementation_big/Screen%20shot%202011-08-16%20at%204.14.00%20PM.png"); 93 | mTestData 94 | .addItem( 95 | "locast unicef 3", 96 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/implementation_big/DSC01492.JPG"); 97 | mTestData 98 | .addItem( 99 | "memory traces", 100 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/implementation_big/North%20end%20memory.jpg"); 101 | mTestData 102 | .addItem( 103 | "uv tracking", 104 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/implementation_big/MEL_LocastProjectsandNextTV2%2031_1.jpg"); 105 | mTestData 106 | .addItem( 107 | "civic media 1", 108 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/implementation_big/sites/mel-drudev.mit.edu/files/locast_civic_00.jpg"); 109 | mTestData 110 | .addItem( 111 | "civic media 2", 112 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/implementation_big/locastPA2.jpg"); 113 | mTestData 114 | .addItem( 115 | "civic media 3", 116 | "http://mobile.mit.edu/sites/mel-dru.mit.edu.mainsite/files/imagecache/implementation_big/locastPA3.jpg"); 117 | 118 | // fill it up! 119 | mTestData.addAll(mTestData); 120 | mTestData.addAll(mTestData); 121 | 122 | } 123 | 124 | private void trim() { 125 | final long trimmed = mCache.trim(); 126 | Toast.makeText(this, trimmed + " byte(s) trimmed.", Toast.LENGTH_LONG).show(); 127 | } 128 | 129 | private void clear() { 130 | mCache.clear(); 131 | Toast.makeText(this, "Cache cleared.", Toast.LENGTH_LONG).show(); 132 | } 133 | 134 | @Override 135 | public boolean onOptionsItemSelected(MenuItem item) { 136 | switch (item.getItemId()) { 137 | case R.id.clear: 138 | clear(); 139 | return true; 140 | 141 | case R.id.trim: 142 | 143 | trim(); 144 | return true; 145 | 146 | case R.id.grid: 147 | startActivity(new Intent(this, ConcurrencyTest.class)); 148 | return true; 149 | 150 | default: 151 | return super.onOptionsItemSelected(item); 152 | } 153 | } 154 | 155 | @Override 156 | public boolean onCreateOptionsMenu(Menu menu) { 157 | super.onCreateOptionsMenu(menu); 158 | getMenuInflater().inflate(R.menu.main_menu, menu); 159 | return true; 160 | } 161 | 162 | } -------------------------------------------------------------------------------- /test/src/edu/mit/mobile/android/imagecache/test/KeyedLockTest.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.imagecache.test; 2 | 3 | import java.util.Collections; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | 7 | import android.test.AndroidTestCase; 8 | import android.util.Log; 9 | import edu.mit.mobile.android.imagecache.KeyedLock; 10 | 11 | public class KeyedLockTest extends AndroidTestCase { 12 | 13 | protected static final String TAG = "KeyedLockTest"; 14 | final KeyedLock mLock = new KeyedLock(); 15 | 16 | private final Set mGotFromStore = Collections.synchronizedSet(new HashSet()); 17 | 18 | public void testKeyedLock() throws InterruptedException { 19 | 20 | final Thread t1 = new Thread(new Sandwich()); 21 | final Thread t2 = new Thread(new Sandwich()); 22 | final Thread t3 = new Thread(new Sandwich()); 23 | 24 | t1.start(); 25 | t2.start(); 26 | t3.start(); 27 | 28 | t1.join(); 29 | t2.join(); 30 | t3.join(); 31 | } 32 | 33 | public void getIngredient(String ingredient) { 34 | mLock.lock(ingredient); 35 | 36 | log("Getting " + ingredient); 37 | 38 | try { 39 | if (mGotFromStore.contains(ingredient)) { 40 | log("Already have " + ingredient); 41 | return; 42 | } 43 | 44 | log("Going to the store to get " + ingredient); 45 | 46 | try { 47 | Thread.sleep(1000); 48 | } catch (final InterruptedException e) { 49 | log("interrupted going to the store"); 50 | } 51 | mGotFromStore.add(ingredient); 52 | 53 | log("Returned from the store with " + ingredient); 54 | } finally { 55 | mLock.unlock(ingredient); 56 | } 57 | } 58 | 59 | private static int mId = 0; 60 | 61 | private static long startTime = System.nanoTime(); 62 | 63 | private long getTime() { 64 | return (System.nanoTime() - startTime) / 1000000; 65 | } 66 | 67 | private void log(String msg) { 68 | Log.d(TAG, "(" + getTime() + "\t" + Thread.currentThread().getId() + "): " + msg); 69 | } 70 | 71 | private class Sandwich implements Runnable { 72 | 73 | int id = mId++; 74 | 75 | @Override 76 | public void run() { 77 | log("Going to make a sandwich! I need bread..."); 78 | 79 | getIngredient("bread"); 80 | log("got bread"); 81 | 82 | getIngredient("cheese"); 83 | log("got cheese"); 84 | 85 | } 86 | 87 | private void log(String msg) { 88 | KeyedLockTest.this.log("#" + id + ": " + msg); 89 | } 90 | 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /test/src/edu/mit/mobile/android/imagecache/test/TestData.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.imagecache.test; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | 6 | import android.content.Context; 7 | import edu.mit.mobile.android.imagecache.ImageCache; 8 | import edu.mit.mobile.android.imagecache.ImageLoaderAdapter; 9 | import edu.mit.mobile.android.imagecache.SimpleThumbnailAdapter; 10 | 11 | public class TestData extends ArrayList> { 12 | 13 | /** 14 | * 15 | */ 16 | private static final long serialVersionUID = -6862061777028855417L; 17 | 18 | public void addItem(String title, String image) { 19 | final HashMap m = new HashMap(); 20 | 21 | m.put("title", title); 22 | m.put("thumb", image); 23 | 24 | add(m); 25 | } 26 | 27 | public static ImageLoaderAdapter generateAdapter(Context context, TestData data, int layout, 28 | ImageCache cache, int width, int height) { 29 | final SimpleThumbnailAdapter bigAdapter = new SimpleThumbnailAdapter(context, data, 30 | layout, 31 | new String[] { "thumb" }, new int[] { R.id.thumb }, 32 | new int[] { R.id.thumb }); 33 | 34 | return new ImageLoaderAdapter(context, bigAdapter, cache, new int[] { R.id.thumb }, width, 35 | height, ImageLoaderAdapter.UNIT_DIP); 36 | } 37 | } 38 | --------------------------------------------------------------------------------