OSDN Git Service

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