OSDN Git Service

Eleven: Synchronization on a non-final field 'mPauseLock'
[android-x86/packages-apps-Eleven.git] / src / com / cyanogenmod / eleven / cache / ImageCache.java
1 /*
2  * Copyright (C) 2012 Andrew Neal
3  * Copyright (C) 2014 The CyanogenMod Project
4  * Licensed under the Apache License, Version 2.0
5  * (the "License"); you may not use this file except in compliance with the
6  * License. You may obtain a copy of the License at
7  * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
8  * or agreed to in writing, software distributed under the License is
9  * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
10  * KIND, either express or implied. See the License for the specific language
11  * governing permissions and limitations under the License.
12  */
13
14 package com.cyanogenmod.eleven.cache;
15
16 import android.annotation.SuppressLint;
17 import android.app.Activity;
18 import android.app.ActivityManager;
19 import android.app.Fragment;
20 import android.app.FragmentManager;
21 import android.content.ComponentCallbacks2;
22 import android.content.ContentUris;
23 import android.content.Context;
24 import android.content.res.Configuration;
25 import android.graphics.Bitmap;
26 import android.graphics.Bitmap.CompressFormat;
27 import android.graphics.BitmapFactory;
28 import android.net.Uri;
29 import android.os.AsyncTask;
30 import android.os.Bundle;
31 import android.os.Environment;
32 import android.os.Looper;
33 import android.os.ParcelFileDescriptor;
34 import android.util.Log;
35
36 import com.cyanogenmod.eleven.utils.ApolloUtils;
37
38 import java.io.File;
39 import java.io.FileDescriptor;
40 import java.io.FileNotFoundException;
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.io.OutputStream;
44 import java.security.MessageDigest;
45 import java.security.NoSuchAlgorithmException;
46 import java.util.HashSet;
47
48 /**
49  * This class holds the memory and disk bitmap caches.
50  */
51 public final class ImageCache {
52
53     private static final String TAG = ImageCache.class.getSimpleName();
54
55     /**
56      * The {@link Uri} used to retrieve album art
57      */
58     private static final Uri mArtworkUri;
59
60     /**
61      * Default memory cache size as a percent of device memory class
62      */
63     private static final float MEM_CACHE_DIVIDER = 0.25f;
64
65     /**
66      * Default disk cache size 10MB
67      */
68     private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10;
69
70     /**
71      * Compression settings when writing images to disk cache
72      */
73     private static final CompressFormat COMPRESS_FORMAT = CompressFormat.JPEG;
74
75     /**
76      * Disk cache index to read from
77      */
78     private static final int DISK_CACHE_INDEX = 0;
79
80     /**
81      * Image compression quality
82      */
83     private static final int COMPRESS_QUALITY = 98;
84
85     /**
86      * LRU cache
87      */
88     private MemoryCache mLruCache;
89
90     /**
91      * Disk LRU cache
92      */
93     private DiskLruCache mDiskCache;
94
95     /**
96      * listeners to the cache state
97      */
98     private HashSet<ICacheListener> mListeners = new HashSet<ICacheListener>();
99
100     private static ImageCache sInstance;
101
102     /**
103      * Used to temporarily pause the disk cache while scrolling
104      */
105     public boolean mPauseDiskAccess = false;
106     private final Object mPauseLock = new Object();
107
108     static {
109         mArtworkUri = Uri.parse("content://media/external/audio/albumart");
110     }
111
112     /**
113      * Constructor of <code>ImageCache</code>
114      *
115      * @param context The {@link Context} to use
116      */
117     public ImageCache(final Context context) {
118         init(context);
119     }
120
121     /**
122      * Used to create a singleton of {@link ImageCache}
123      *
124      * @param context The {@link Context} to use
125      * @return A new instance of this class.
126      */
127     public final static ImageCache getInstance(final Context context) {
128         if (sInstance == null) {
129             sInstance = new ImageCache(context.getApplicationContext());
130         }
131         return sInstance;
132     }
133
134     /**
135      * Initialize the cache, providing all parameters.
136      *
137      * @param context The {@link Context} to use
138      * @param cacheParams The cache parameters to initialize the cache
139      */
140     private void init(final Context context) {
141         ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
142
143             @Override
144             protected Void doInBackground(final Void... unused) {
145                 // Initialize the disk cahe in a background thread
146                 initDiskCache(context);
147                 return null;
148             }
149         }, (Void[])null);
150         // Set up the memory cache
151         initLruCache(context);
152     }
153
154     /**
155      * Initializes the disk cache. Note that this includes disk access so this
156      * should not be executed on the main/UI thread. By default an ImageCache
157      * does not initialize the disk cache when it is created, instead you should
158      * call initDiskCache() to initialize it on a background thread.
159      *
160      * @param context The {@link Context} to use
161      */
162     private synchronized void initDiskCache(final Context context) {
163         // Set up disk cache
164         if (mDiskCache == null || mDiskCache.isClosed()) {
165             File diskCacheDir = getDiskCacheDir(context, TAG);
166             if (diskCacheDir != null) {
167                 if (!diskCacheDir.exists()) {
168                     diskCacheDir.mkdirs();
169                 }
170                 if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
171                     try {
172                         mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
173                     } catch (final IOException e) {
174                         diskCacheDir = null;
175                     }
176                 }
177             }
178         }
179     }
180
181     /**
182      * Sets up the Lru cache
183      *
184      * @param context The {@link Context} to use
185      */
186     @SuppressLint("NewApi")
187     public void initLruCache(final Context context) {
188         final ActivityManager activityManager = (ActivityManager)context
189                 .getSystemService(Context.ACTIVITY_SERVICE);
190         final int lruCacheSize = Math.round(MEM_CACHE_DIVIDER * activityManager.getMemoryClass()
191                 * 1024 * 1024);
192         mLruCache = new MemoryCache(lruCacheSize);
193
194         // Release some memory as needed
195         context.registerComponentCallbacks(new ComponentCallbacks2() {
196
197             /**
198              * {@inheritDoc}
199              */
200             @Override
201             public void onTrimMemory(final int level) {
202                 if (level >= TRIM_MEMORY_MODERATE) {
203                     evictAll();
204                 } else if (level >= TRIM_MEMORY_BACKGROUND) {
205                     mLruCache.trimToSize(mLruCache.size() / 2);
206                 }
207             }
208
209             /**
210              * {@inheritDoc}
211              */
212             @Override
213             public void onLowMemory() {
214                 // Nothing to do
215             }
216
217             /**
218              * {@inheritDoc}
219              */
220             @Override
221             public void onConfigurationChanged(final Configuration newConfig) {
222                 // Nothing to do
223             }
224         });
225     }
226
227     /**
228      * Find and return an existing ImageCache stored in a {@link RetainFragment}
229      * , if not found a new one is created using the supplied params and saved
230      * to a {@link RetainFragment}
231      *
232      * @param activity The calling {@link FragmentActivity}
233      * @return An existing retained ImageCache object or a new one if one did
234      *         not exist
235      */
236     public static final ImageCache findOrCreateCache(final Activity activity) {
237
238         // Search for, or create an instance of the non-UI RetainFragment
239         final RetainFragment retainFragment = findOrCreateRetainFragment(
240                 activity.getFragmentManager());
241
242         // See if we already have an ImageCache stored in RetainFragment
243         ImageCache cache = (ImageCache)retainFragment.getObject();
244
245         // No existing ImageCache, create one and store it in RetainFragment
246         if (cache == null) {
247             cache = getInstance(activity);
248             retainFragment.setObject(cache);
249         }
250         return cache;
251     }
252
253     /**
254      * Locate an existing instance of this {@link Fragment} or if not found,
255      * create and add it using {@link FragmentManager}
256      *
257      * @param fm The {@link FragmentManager} to use
258      * @return The existing instance of the {@link Fragment} or the new instance
259      *         if just created
260      */
261     public static final RetainFragment findOrCreateRetainFragment(final FragmentManager fm) {
262         // Check to see if we have retained the worker fragment
263         RetainFragment retainFragment = (RetainFragment)fm.findFragmentByTag(TAG);
264
265         // If not retained, we need to create and add it
266         if (retainFragment == null) {
267             retainFragment = new RetainFragment();
268             fm.beginTransaction().add(retainFragment, TAG).commit();
269         }
270         return retainFragment;
271     }
272
273     /**
274      * Adds a new image to the memory and disk caches
275      *
276      * @param data The key used to store the image
277      * @param bitmap The {@link Bitmap} to cache
278      */
279     public void addBitmapToCache(final String data, final Bitmap bitmap) {
280         addBitmapToCache(data, bitmap, false);
281     }
282
283     /**
284      * Adds a new image to the memory and disk caches
285      *
286      * @param data The key used to store the image
287      * @param bitmap The {@link Bitmap} to cache
288      * @param replace force a replace even if the bitmap exists in the cache
289      */
290     public void addBitmapToCache(final String data, final Bitmap bitmap, final boolean replace) {
291         if (data == null || bitmap == null) {
292             return;
293         }
294
295         // Add to memory cache
296         addBitmapToMemCache(data, bitmap, replace);
297
298         // Add to disk cache
299         if (mDiskCache != null && !mDiskCache.isClosed()) {
300             final String key = hashKeyForDisk(data);
301             OutputStream out = null;
302             try {
303                 final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
304                 if (snapshot != null) {
305                     snapshot.getInputStream(DISK_CACHE_INDEX).close();
306                 }
307
308                 if (snapshot == null || replace) {
309                     final DiskLruCache.Editor editor = mDiskCache.edit(key);
310                     if (editor != null) {
311                         out = editor.newOutputStream(DISK_CACHE_INDEX);
312                         bitmap.compress(COMPRESS_FORMAT, COMPRESS_QUALITY, out);
313                         editor.commit();
314                         out.close();
315                         flush();
316                     }
317                 }
318             } catch (final IOException e) {
319                 Log.e(TAG, "addBitmapToCache - " + e);
320             } catch (final IllegalStateException e) {
321                 // if the user clears the cache while we have an async task going we could try
322                 // writing to the disk cache while it isn't ready. Catching here will silently
323                 // fail instead
324                 Log.e(TAG, "addBitmapToCache - " + e);
325             } finally {
326                 try {
327                     if (out != null) {
328                         out.close();
329                         out = null;
330                     }
331                 } catch (final IOException e) {
332                     Log.e(TAG, "addBitmapToCache - " + e);
333                 } catch (final IllegalStateException e) {
334                     Log.e(TAG, "addBitmapToCache - " + e);
335                 }
336             }
337         }
338     }
339
340     /**
341      * Called to add a new image to the memory cache
342      *
343      * @param data The key identifier
344      * @param bitmap The {@link Bitmap} to cache
345      */
346     public void addBitmapToMemCache(final String data, final Bitmap bitmap) {
347         addBitmapToMemCache(data, bitmap, false);
348     }
349
350     /**
351      * Called to add a new image to the memory cache
352      *
353      * @param data The key identifier
354      * @param bitmap The {@link Bitmap} to cache
355      * @param replace whether to force a replace if it already exists
356      */
357     public void addBitmapToMemCache(final String data, final Bitmap bitmap, final boolean replace) {
358         if (data == null || bitmap == null) {
359             return;
360         }
361         // Add to memory cache
362         if (replace || getBitmapFromMemCache(data) == null) {
363             mLruCache.put(data, bitmap);
364         }
365     }
366
367     /**
368      * Fetches a cached image from the memory cache
369      *
370      * @param data Unique identifier for which item to get
371      * @return The {@link Bitmap} if found in cache, null otherwise
372      */
373     public final Bitmap getBitmapFromMemCache(final String data) {
374         if (data == null) {
375             return null;
376         }
377         if (mLruCache != null) {
378             final Bitmap lruBitmap = mLruCache.get(data);
379             if (lruBitmap != null) {
380                 return lruBitmap;
381             }
382         }
383         return null;
384     }
385
386     /**
387      * Fetches a cached image from the disk cache
388      *
389      * @param data Unique identifier for which item to get
390      * @return The {@link Bitmap} if found in cache, null otherwise
391      */
392     public final Bitmap getBitmapFromDiskCache(final String data) {
393         if (data == null) {
394             return null;
395         }
396
397         // Check in the memory cache here to avoid going to the disk cache less
398         // often
399         if (getBitmapFromMemCache(data) != null) {
400             return getBitmapFromMemCache(data);
401         }
402
403         waitUntilUnpaused();
404         final String key = hashKeyForDisk(data);
405         if (mDiskCache != null) {
406             InputStream inputStream = null;
407             try {
408                 final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
409                 if (snapshot != null) {
410                     inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
411                     if (inputStream != null) {
412                         final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
413                         if (bitmap != null) {
414                             return bitmap;
415                         }
416                     }
417                 }
418             } catch (final IOException e) {
419                 Log.e(TAG, "getBitmapFromDiskCache - " + e);
420             } finally {
421                 try {
422                     if (inputStream != null) {
423                         inputStream.close();
424                     }
425                 } catch (final IOException e) {
426                 }
427             }
428         }
429         return null;
430     }
431
432     /**
433      * Tries to return a cached image from memory cache before fetching from the
434      * disk cache
435      *
436      * @param data Unique identifier for which item to get
437      * @return The {@link Bitmap} if found in cache, null otherwise
438      */
439     public final Bitmap getCachedBitmap(final String data) {
440         if (data == null) {
441             return null;
442         }
443         Bitmap cachedImage = getBitmapFromMemCache(data);
444         if (cachedImage == null) {
445             cachedImage = getBitmapFromDiskCache(data);
446         }
447         if (cachedImage != null) {
448             addBitmapToMemCache(data, cachedImage);
449             return cachedImage;
450         }
451         return null;
452     }
453
454     /**
455      * Tries to return the album art from memory cache and disk cache, before
456      * calling {@code #getArtworkFromFile(Context, String)} again
457      *
458      * @param context The {@link Context} to use
459      * @param data The name of the album art
460      * @param id The ID of the album to find artwork for
461      * @return The artwork for an album
462      */
463     public final Bitmap getCachedArtwork(final Context context, final String data, final long id) {
464         if (context == null || data == null) {
465             return null;
466         }
467         Bitmap cachedImage = getCachedBitmap(data);
468         if (cachedImage == null && id >= 0) {
469             cachedImage = getArtworkFromFile(context, id);
470         }
471         if (cachedImage != null) {
472             addBitmapToMemCache(data, cachedImage);
473             return cachedImage;
474         }
475         return null;
476     }
477
478     /**
479      * Used to fetch the artwork for an album locally from the user's device
480      *
481      * @param context The {@link Context} to use
482      * @param albumID The ID of the album to find artwork for
483      * @return The artwork for an album
484      */
485     public final Bitmap getArtworkFromFile(final Context context, final long albumId) {
486         if (albumId < 0) {
487             return null;
488         }
489         Bitmap artwork = null;
490         waitUntilUnpaused();
491         try {
492             final Uri uri = ContentUris.withAppendedId(mArtworkUri, albumId);
493             final ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver()
494                     .openFileDescriptor(uri, "r");
495             if (parcelFileDescriptor != null) {
496                 final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
497                 artwork = BitmapFactory.decodeFileDescriptor(fileDescriptor);
498             }
499         } catch (final IllegalStateException e) {
500             // Log.e(TAG, "IllegalStateExcetpion - getArtworkFromFile - ", e);
501         } catch (final FileNotFoundException e) {
502             // Log.e(TAG, "FileNotFoundException - getArtworkFromFile - ", e);
503         } catch (final OutOfMemoryError evict) {
504             // Log.e(TAG, "OutOfMemoryError - getArtworkFromFile - ", evict);
505             evictAll();
506         }
507         return artwork;
508     }
509
510     /**
511      * flush() is called to synchronize up other methods that are accessing the
512      * cache first
513      */
514     public void flush() {
515         ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
516
517             @Override
518             protected Void doInBackground(final Void... unused) {
519                 if (mDiskCache != null) {
520                     try {
521                         if (!mDiskCache.isClosed()) {
522                             mDiskCache.flush();
523                         }
524                     } catch (final IOException e) {
525                         Log.e(TAG, "flush - " + e);
526                     }
527                 }
528                 return null;
529             }
530         }, (Void[])null);
531     }
532
533     /**
534      * Clears the disk and memory caches
535      */
536     public void clearCaches() {
537         ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
538
539             @Override
540             protected Void doInBackground(final Void... unused) {
541                 // Clear the disk cache
542                 try {
543                     if (mDiskCache != null) {
544                         mDiskCache.delete();
545                         mDiskCache = null;
546                     }
547                 } catch (final IOException e) {
548                     Log.e(TAG, "clearCaches - " + e);
549                 }
550                 // Clear the memory cache
551                 evictAll();
552                 return null;
553             }
554         }, (Void[])null);
555     }
556
557     /**
558      * Closes the disk cache associated with this ImageCache object. Note that
559      * this includes disk access so this should not be executed on the main/UI
560      * thread.
561      */
562     public void close() {
563         ApolloUtils.execute(false, new AsyncTask<Void, Void, Void>() {
564
565             @Override
566             protected Void doInBackground(final Void... unused) {
567                 if (mDiskCache != null) {
568                     try {
569                         if (!mDiskCache.isClosed()) {
570                             mDiskCache.close();
571                             mDiskCache = null;
572                         }
573                     } catch (final IOException e) {
574                         Log.e(TAG, "close - " + e);
575                     }
576                 }
577                 return null;
578             }
579         }, (Void[]) null);
580     }
581
582     /**
583      * Evicts all of the items from the memory cache and lets the system know
584      * now would be a good time to garbage collect
585      */
586     public void evictAll() {
587         if (mLruCache != null) {
588             mLruCache.evictAll();
589         }
590         System.gc();
591     }
592
593     /**
594      * @param key The key used to identify which cache entries to delete.
595      */
596     public void removeFromCache(final String key) {
597         if (key == null) {
598             return;
599         }
600         // Remove the Lru entry
601         if (mLruCache != null) {
602             mLruCache.remove(key);
603         }
604
605         try {
606             // Remove the disk entry
607             if (mDiskCache != null) {
608                 mDiskCache.remove(hashKeyForDisk(key));
609             }
610         } catch (final IOException e) {
611             Log.e(TAG, "remove - " + e);
612         }
613         flush();
614     }
615
616     /**
617      * Used to temporarily pause the disk cache while the user is scrolling to
618      * improve scrolling.
619      *
620      * @param pause True to temporarily pause the disk cache, false otherwise.
621      */
622     public void setPauseDiskCache(final boolean pause) {
623         synchronized (mPauseLock) {
624             if (mPauseDiskAccess != pause) {
625                 mPauseDiskAccess = pause;
626                 if (!pause) {
627                     mPauseLock.notify();
628
629                     for (ICacheListener listener : mListeners) {
630                         listener.onCacheUnpaused();
631                     }
632                 }
633             }
634         }
635     }
636
637     private void waitUntilUnpaused() {
638         synchronized (mPauseLock) {
639             if (Looper.myLooper() != Looper.getMainLooper()) {
640                 while (mPauseDiskAccess) {
641                     try {
642                         mPauseLock.wait();
643                     } catch (InterruptedException e) {
644                         // ignored, we'll start waiting again
645                     }
646                 }
647             }
648         }
649     }
650
651     /**
652      * @return True if the user is scrolling, false otherwise.
653      */
654     public boolean isDiskCachePaused() {
655         return mPauseDiskAccess;
656     }
657
658     public void addCacheListener(ICacheListener listener) {
659         mListeners.add(listener);
660     }
661
662     public void removeCacheListener(ICacheListener listener) {
663         mListeners.remove(listener);
664     }
665
666     /**
667      * Get a usable cache directory (external if available, internal otherwise)
668      *
669      * @param context The {@link Context} to use
670      * @param uniqueName A unique directory name to append to the cache
671      *            directory
672      * @return The cache directory
673      */
674     public static final File getDiskCacheDir(final Context context, final String uniqueName) {
675         // getExternalCacheDir(context) returns null if external storage is not ready
676         final String cachePath = getExternalCacheDir(context) != null
677                                     ? getExternalCacheDir(context).getPath()
678                                     : context.getCacheDir().getPath();
679         return new File(cachePath, uniqueName);
680     }
681
682     /**
683      * Check if external storage is built-in or removable
684      *
685      * @return True if external storage is removable (like an SD card), false
686      *         otherwise
687      */
688     public static final boolean isExternalStorageRemovable() {
689         return Environment.isExternalStorageRemovable();
690     }
691
692     /**
693      * Get the external app cache directory
694      *
695      * @param context The {@link Context} to use
696      * @return The external cache directory
697      */
698     public static final File getExternalCacheDir(final Context context) {
699         return context.getExternalCacheDir();
700     }
701
702     /**
703      * Check how much usable space is available at a given path.
704      *
705      * @param path The path to check
706      * @return The space available in bytes
707      */
708     public static final long getUsableSpace(final File path) {
709         return path.getUsableSpace();
710     }
711
712     /**
713      * A hashing method that changes a string (like a URL) into a hash suitable
714      * for using as a disk filename.
715      *
716      * @param key The key used to store the file
717      */
718     public static final String hashKeyForDisk(final String key) {
719         String cacheKey;
720         try {
721             final MessageDigest digest = MessageDigest.getInstance("MD5");
722             digest.update(key.getBytes());
723             cacheKey = bytesToHexString(digest.digest());
724         } catch (final NoSuchAlgorithmException e) {
725             cacheKey = String.valueOf(key.hashCode());
726         }
727         return cacheKey;
728     }
729
730     /**
731      * http://stackoverflow.com/questions/332079
732      *
733      * @param bytes The bytes to convert.
734      * @return A {@link String} converted from the bytes of a hashable key used
735      *         to store a filename on the disk, to hex digits.
736      */
737     private static final String bytesToHexString(final byte[] bytes) {
738         final StringBuilder builder = new StringBuilder();
739         for (final byte b : bytes) {
740             final String hex = Integer.toHexString(0xFF & b);
741             if (hex.length() == 1) {
742                 builder.append('0');
743             }
744             builder.append(hex);
745         }
746         return builder.toString();
747     }
748
749     /**
750      * A simple non-UI Fragment that stores a single Object and is retained over
751      * configuration changes. In this sample it will be used to retain an
752      * {@link ImageCache} object.
753      */
754     public static final class RetainFragment extends Fragment {
755
756         /**
757          * The object to be stored
758          */
759         private Object mObject;
760
761         /**
762          * Empty constructor as per the {@link Fragment} documentation
763          */
764         public RetainFragment() {
765         }
766
767         /**
768          * {@inheritDoc}
769          */
770         @Override
771         public void onCreate(final Bundle savedInstanceState) {
772             super.onCreate(savedInstanceState);
773             // Make sure this Fragment is retained over a configuration change
774             setRetainInstance(true);
775         }
776
777         /**
778          * Store a single object in this {@link Fragment}
779          *
780          * @param object The object to store
781          */
782         public void setObject(final Object object) {
783             mObject = object;
784         }
785
786         /**
787          * Get the stored object
788          *
789          * @return The stored object
790          */
791         public Object getObject() {
792             return mObject;
793         }
794     }
795
796     /**
797      * Used to cache images via {@link LruCache}.
798      */
799     public static final class MemoryCache extends LruCache<String, Bitmap> {
800
801         /**
802          * Constructor of <code>MemoryCache</code>
803          *
804          * @param maxSize The allowed size of the {@link LruCache}
805          */
806         public MemoryCache(final int maxSize) {
807             super(maxSize);
808         }
809
810         /**
811          * Get the size in bytes of a bitmap.
812          */
813         public static final int getBitmapSize(final Bitmap bitmap) {
814             return bitmap.getByteCount();
815         }
816
817         /**
818          * {@inheritDoc}
819          */
820         @Override
821         protected int sizeOf(final String paramString, final Bitmap paramBitmap) {
822             return getBitmapSize(paramBitmap);
823         }
824
825     }
826
827 }