Advertisement
bit

ImageLoader.cs

bit
Jun 25th, 2012
128
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 16.88 KB | None | 0 0
  1. // Copyright 2010-2011 Miguel de Icaza
  2. //
  3. // Based on the TweetStation specific ImageStore
  4. //
  5. // Permission is hereby granted, free of charge, to any person obtaining a copy
  6. // of this software and associated documentation files (the "Software"), to deal
  7. // in the Software without restriction, including without limitation the rights
  8. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. // copies of the Software, and to permit persons to whom the Software is
  10. // furnished to do so, subject to the following conditions:
  11. //
  12. // The above copyright notice and this permission notice shall be included in
  13. // all copies or substantial portions of the Software.
  14. //
  15. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  21. // THE SOFTWARE.
  22. //
  23.  
  24. //
  25. // Minor changes (UIImage -> Drawable) required to get this running on Mono-for-Android
  26. //
  27. using System;
  28. using System.Collections.Generic;
  29. using System.IO;
  30. using System.Linq;
  31. using System.Net;
  32. using System.Text;
  33. using System.Threading;
  34.  
  35. using System.Security.Cryptography;
  36. using Android.Graphics.Drawables;
  37. using Android.Util;
  38.  
  39. namespace Android.Dialog
  40. {
  41. /// <summary>
  42. /// This interface needs to be implemented to be notified when an image
  43. /// has been downloaded. The notification will happen on the UI thread.
  44. /// Upon notification, the code should call RequestImage again, this time
  45. /// the image will be loaded from the on-disk cache or the in-memory cache.
  46. /// </summary>
  47. public interface IImageUpdated
  48. {
  49. /// <summary>
  50. /// On Android, you MUST do the operations in your implementation on the UiThread.
  51. /// Be sure to use RunOnUiThread()!!!
  52. /// </summary>
  53. void UpdatedImage(Uri uri);
  54. }
  55.  
  56. /// <summary>
  57. /// Network image loader, with local file system cache and in-memory cache
  58. /// </summary>
  59. /// <remarks>
  60. /// By default, using the static public methods will use an in-memory cache
  61. /// for 50 images and 4 megs total. The behavior of the static methods
  62. /// can be modified by setting the public DefaultLoader property to a value
  63. /// that the user configured.
  64. ///
  65. /// The instance methods can be used to create different imageloader with
  66. /// different properties.
  67. ///
  68. /// Keep in mind that the phone does not have a lot of memory, and using
  69. /// the cache with the unlimited value (0) even with a number of items in
  70. /// the cache can consume memory very quickly.
  71. ///
  72. /// Use the Purge method to release all the memory kept in the caches on
  73. /// low memory conditions, or when the application is sent to the background.
  74. /// </remarks>
  75.  
  76. public class ImageLoader
  77. {
  78. public readonly static string BaseDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "..");
  79. const int MaxRequests = 6;
  80. static string PicDir;
  81.  
  82. // Cache of recently used images
  83. LRUCache<Uri, Drawable /*UIImage*/> cache;
  84.  
  85. // A list of requests that have been issues, with a list of objects to notify.
  86. static Dictionary<Uri, List<IImageUpdated>> pendingRequests;
  87.  
  88. // A list of updates that have completed, we must notify the main thread about them.
  89. static HashSet<Uri> queuedUpdates;
  90.  
  91. // A queue used to avoid flooding the network stack with HTTP requests
  92. static Stack<Uri> requestQueue;
  93.  
  94. /*static NSString nsDispatcher = "x"; */
  95.  
  96. static MD5CryptoServiceProvider checksum = new MD5CryptoServiceProvider();
  97.  
  98. /// <summary>
  99. /// This contains the default loader which is configured to be 50 images
  100. /// up to 4 megs of memory. Assigning to this property a new value will
  101. /// change the behavior. This property is lazyly computed, the first time
  102. /// an image is requested.
  103. /// </summary>
  104. public static ImageLoader DefaultLoader;
  105.  
  106. static ImageLoader()
  107. {
  108. PicDir = Path.Combine(BaseDir, "Library/Caches/Pictures.MonoTouch.Dialog/");
  109.  
  110. if (!Directory.Exists(PicDir))
  111. Directory.CreateDirectory(PicDir);
  112.  
  113. pendingRequests = new Dictionary<Uri, List<IImageUpdated>>();
  114. queuedUpdates = new HashSet<Uri>();
  115. requestQueue = new Stack<Uri>();
  116. }
  117.  
  118. /// <summary>
  119. /// Creates a new instance of the image loader
  120. /// </summary>
  121. /// <param name="cacheSize">
  122. /// The maximum number of entries in the LRU cache
  123. /// </param>
  124. /// <param name="memoryLimit">
  125. /// The maximum number of bytes to consume by the image loader cache.
  126. /// </param>
  127. public ImageLoader(int cacheSize, int memoryLimit)
  128. {
  129. cache = new LRUCache<Uri, Drawable /*UIImage*/>(cacheSize, memoryLimit, sizer);
  130. }
  131.  
  132. static int sizer(Drawable /*UIImage*/ img)
  133. {
  134. /*var cg = img.CGImage;
  135. return cg.BytesPerRow * cg.Height;*/
  136. var pixels = img.IntrinsicHeight * img.IntrinsicWidth;
  137. return pixels * 3; //HACK: assume 3 bytes per pixel (24bit)
  138. }
  139.  
  140. /// <summary>
  141. /// Purges the contents of the DefaultLoader
  142. /// </summary>
  143. public static void Purge()
  144. {
  145. if (DefaultLoader != null)
  146. DefaultLoader.PurgeCache();
  147. }
  148.  
  149. /// <summary>
  150. /// Purges the cache of this instance of the ImageLoader, releasing
  151. /// all the memory used by the images in the caches.
  152. /// </summary>
  153. public void PurgeCache()
  154. {
  155. cache.Purge();
  156. }
  157.  
  158. static int hex(int v)
  159. {
  160. if (v < 10)
  161. return '0' + v;
  162. return 'a' + v - 10;
  163. }
  164.  
  165. static string md5(string input)
  166. {
  167. var bytes = checksum.ComputeHash(Encoding.UTF8.GetBytes(input));
  168. var ret = new char[32];
  169. for (int i = 0; i < 16; i++)
  170. {
  171. ret[i * 2] = (char)hex(bytes[i] >> 4);
  172. ret[i * 2 + 1] = (char)hex(bytes[i] & 0xf);
  173. }
  174. return new string(ret);
  175. }
  176.  
  177. /// <summary>
  178. /// Requests an image to be loaded using the default image loader
  179. /// </summary>
  180. /// <param name="uri">
  181. /// The URI for the image to load
  182. /// </param>
  183. /// <param name="notify">
  184. /// A class implementing the IImageUpdated interface that will be invoked when the image has been loaded
  185. /// </param>
  186. /// <returns>
  187. /// If the image has already been downloaded, or is in the cache, this will return the image as a Drawable.
  188. /// </returns>
  189. public static Drawable /*UIImage*/ DefaultRequestImage(Uri uri, IImageUpdated notify)
  190. {
  191. if (DefaultLoader == null)
  192. DefaultLoader = new ImageLoader(50, 4 * 1024 * 1024);
  193. return DefaultLoader.RequestImage(uri, notify);
  194. }
  195.  
  196. /// <summary>
  197. /// Requests an image to be loaded from the network
  198. /// </summary>
  199. /// <param name="uri">
  200. /// The URI for the image to load
  201. /// </param>
  202. /// <param name="notify">
  203. /// A class implementing the IImageUpdated interface that will be invoked when the image has been loaded
  204. /// </param>
  205. /// <returns>
  206. /// If the image has already been downloaded, or is in the cache, this will return the image as a Drawable.
  207. /// </returns>
  208. public Drawable /*UIImage*/ RequestImage(Uri uri, IImageUpdated notify)
  209. {
  210.  
  211. LogDebug("..requ " + ImageName(uri.AbsoluteUri));
  212. Drawable /*UIImage*/ ret;
  213.  
  214. lock (cache)
  215. {
  216. ret = cache[uri];
  217. if (ret != null)
  218. return ret;
  219. }
  220.  
  221. lock (requestQueue)
  222. {
  223. if (pendingRequests.ContainsKey(uri))
  224. return null;
  225. }
  226.  
  227. string picfile = uri.IsFile ? uri.LocalPath : PicDir + md5(uri.AbsoluteUri);
  228. if (File.Exists(picfile))
  229. {
  230. ret = Drawable.CreateFromPath(picfile); /* UIImage.FromFileUncached(picfile);*/
  231. if (ret != null)
  232. {
  233. lock (cache)
  234. cache[uri] = ret;
  235. return ret;
  236. }
  237. }
  238. if (uri.IsFile) // so there is no point queueing a request for it :)
  239. return null;
  240.  
  241. if (notify != null) // if notify is null, we won't bother retrieving again... assume this is a hit-and-hope query
  242. QueueRequest(uri, picfile, notify);
  243. return null;
  244. }
  245.  
  246. static void QueueRequest(Uri uri, string target, IImageUpdated notify)
  247. {
  248. if (notify == null)
  249. throw new ArgumentNullException("notify");
  250.  
  251. lock (requestQueue)
  252. {
  253. if (pendingRequests.ContainsKey(uri))
  254. {
  255. LogDebug("-------- pendingRequest: added new listener for " + ImageName(uri.AbsoluteUri));
  256. pendingRequests[uri].Add(notify);
  257. return;
  258. }
  259. var slot = new List<IImageUpdated>(4);
  260. slot.Add(notify);
  261. pendingRequests[uri] = slot;
  262.  
  263. if (picDownloaders >= MaxRequests) {
  264. requestQueue.Push(uri);
  265. LogDebug("----push " + ImageName(uri.AbsoluteUri));
  266. } else {
  267.  
  268. ThreadPool.QueueUserWorkItem(delegate {
  269. try {
  270. LogDebug("----dwnl " + ImageName(uri.AbsoluteUri));
  271. StartPicDownload(uri, target); //RWC: Where is the callback? When this thread is done, how does it call back?
  272. } catch (Exception e) {
  273. LogDebug("ThreadPool.QueueUserWorkItem: " + e.Message);
  274. }
  275. });
  276. }
  277. }
  278. }
  279. //TODO: remove; this is just here for logging
  280. public static string ImageName(string uri)
  281. {
  282. var start = uri.LastIndexOf("/") + 1;
  283. var end = uri.LastIndexOf(".");
  284. return uri.Substring(start, end-start);
  285. }
  286. static bool Download(Uri uri, string target)
  287. {
  288. var buffer = new byte[4 * 1024];
  289.  
  290. try
  291. { // use Java.Net.URL instead of WebClient...
  292. var tmpfile = target + ".tmp";
  293. var imageUrl = new Java.Net.URL(uri.AbsoluteUri);
  294. var stream = imageUrl.OpenStream();
  295. LogDebug("====== open " + ImageName(uri.AbsoluteUri));
  296. using (var o = File.Open(tmpfile, FileMode.OpenOrCreate)) {
  297. byte[] buf = new byte[1024];
  298. int r;
  299. while ((r = stream.Read(buf, 0, buf.Length)) > 0) {
  300. o.Write(buf, 0, r);
  301. }
  302. }
  303.  
  304. //using (var file = new FileStream(tmpfile, FileMode.Create, FileAccess.Write, FileShare.Read))
  305. //{
  306. //var req = WebRequest.Create(uri) as HttpWebRequest;
  307.  
  308. //using (var resp = req.GetResponse())
  309. //{
  310. // using (var s = resp.GetResponseStream())
  311. // {
  312. // int n;
  313. // while ((n = s.Read(buffer, 0, buffer.Length)) > 0)
  314. // {
  315. // file.Write(buffer, 0, n);
  316. // }
  317. // }
  318. //}
  319. //}
  320. if (!File.Exists(target)) // we're never updating images if they change, to reduce Exceptions and speed up
  321. File.Move(tmpfile, target);
  322. return true;
  323. }
  324. catch (Exception e)
  325. {
  326. LogDebug(String.Format("Problem with {0} {1}", uri, e.Message));
  327. return false;
  328. }
  329. }
  330.  
  331. static long picDownloaders;
  332.  
  333. static void StartPicDownload(Uri uri, string target)
  334. {
  335. LogDebug("________star " + picDownloaders);
  336. Interlocked.Increment(ref picDownloaders);
  337. try
  338. {
  339. _StartPicDownload(uri, target);
  340. }
  341. catch (Exception e)
  342. {
  343. Console.Error.WriteLine("CRITICAL: should have never happened {0}", e);
  344. }
  345. //Util.Log ("Leaving StartPicDownload {0}", picDownloaders);
  346. LogDebug("________end " + picDownloaders);
  347. Interlocked.Decrement(ref picDownloaders);
  348. }
  349.  
  350. static void _StartPicDownload(Uri uri, string target)
  351. {
  352. do
  353. {
  354. bool downloaded = false;
  355.  
  356. //System.Threading.Thread.Sleep (5000);
  357. downloaded = Download(uri, target);
  358. if (!downloaded)
  359. LogDebug((String.Format("Error fetching picture for {0} to {1}", uri, target)));
  360.  
  361. // Cluster all updates together
  362. bool doInvoke = false;
  363.  
  364. lock (requestQueue)
  365. {
  366. if (downloaded)
  367. {
  368. queuedUpdates.Add(uri);
  369.  
  370. // If this is the first queued update, must notify
  371. if (queuedUpdates.Count == 1)
  372. doInvoke = true;
  373. }
  374. else
  375. pendingRequests.Remove(uri);
  376.  
  377. // Try to get more jobs.
  378. if (requestQueue.Count > 0)
  379. {
  380. uri = requestQueue.Pop();
  381. if (uri == null)
  382. {
  383. Console.Error.WriteLine("Dropping request {0} because url is null", uri);
  384. pendingRequests.Remove(uri);
  385. uri = null;
  386. }
  387. }
  388. else
  389. {
  390. //Util.Log ("Leaving because requestQueue.Count = {0} NOTE: {1}", requestQueue.Count, pendingRequests.Count);
  391. uri = null;
  392. }
  393. }
  394. if (doInvoke)
  395. {
  396. /*nsDispatcher.BeginInvokeOnMainThread(NotifyImageListeners);*/
  397. // HACK: need a context to do RunOnUiThread on...
  398. //RunOnUiThread(() =>
  399. //{
  400. NotifyImageListeners();
  401. //});
  402. }
  403. } while (uri != null);
  404. }
  405.  
  406. /// <summary>
  407. /// NEEDS TO run on the main thread. The iOS version does, but in Android
  408. /// we need access to a Context to get to the main thread, and I haven't
  409. /// figured out a non-hacky way to do that yet.
  410. /// </summary>
  411. static void NotifyImageListeners()
  412. {
  413. lock (requestQueue)
  414. {
  415. foreach (var quri in queuedUpdates)
  416. {
  417. var list = pendingRequests[quri];
  418. pendingRequests.Remove(quri);
  419. foreach (var pr in list)
  420. {
  421. try
  422. {
  423. pr.UpdatedImage(quri); // this is the bit that should be on the UiThread
  424. }
  425. catch (Exception e)
  426. {
  427. LogDebug("NotifyImageListeners: " + e.Message);
  428. }
  429. }
  430. }
  431.  
  432. queuedUpdates.Clear();
  433. }
  434. }
  435.  
  436. /*
  437. Use this to help with ADB watching in CMD
  438. "c:\Program Files (x86)\Android\android-sdk\platform-tools\adb" logcat -s MonoDroid:* mono:* MWC:* ActivityManager:*
  439. */
  440. public static void LogDebug(string message)
  441. {
  442. Console.WriteLine(message);
  443. // Log.Debug("MWC", message);
  444. }
  445. }
  446. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement