Home | History | Annotate | Download | only in server
      1 /*
      2  * Copyright (C) 2007-2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.server;
     18 
     19 import java.io.FileDescriptor;
     20 import java.io.PrintWriter;
     21 
     22 import android.app.Notification;
     23 import android.app.NotificationManager;
     24 import android.app.PendingIntent;
     25 import android.content.ContentResolver;
     26 import android.content.Context;
     27 import android.content.Intent;
     28 import android.content.pm.IPackageDataObserver;
     29 import android.content.pm.IPackageManager;
     30 import android.content.pm.PackageManager;
     31 import android.os.Binder;
     32 import android.os.Environment;
     33 import android.os.FileObserver;
     34 import android.os.Handler;
     35 import android.os.Message;
     36 import android.os.Process;
     37 import android.os.RemoteException;
     38 import android.os.ServiceManager;
     39 import android.os.StatFs;
     40 import android.os.SystemClock;
     41 import android.os.SystemProperties;
     42 import android.os.UserHandle;
     43 import android.provider.Settings;
     44 import android.text.format.Formatter;
     45 import android.util.EventLog;
     46 import android.util.Slog;
     47 import android.util.TimeUtils;
     48 
     49 /**
     50  * This class implements a service to monitor the amount of disk
     51  * storage space on the device.  If the free storage on device is less
     52  * than a tunable threshold value (a secure settings parameter;
     53  * default 10%) a low memory notification is displayed to alert the
     54  * user. If the user clicks on the low memory notification the
     55  * Application Manager application gets launched to let the user free
     56  * storage space.
     57  *
     58  * Event log events: A low memory event with the free storage on
     59  * device in bytes is logged to the event log when the device goes low
     60  * on storage space.  The amount of free storage on the device is
     61  * periodically logged to the event log. The log interval is a secure
     62  * settings parameter with a default value of 12 hours.  When the free
     63  * storage differential goes below a threshold (again a secure
     64  * settings parameter with a default value of 2MB), the free memory is
     65  * logged to the event log.
     66  */
     67 public class DeviceStorageMonitorService extends Binder {
     68     private static final String TAG = "DeviceStorageMonitorService";
     69     private static final boolean DEBUG = false;
     70     private static final boolean localLOGV = false;
     71     private static final int DEVICE_MEMORY_WHAT = 1;
     72     private static final int MONITOR_INTERVAL = 1; //in minutes
     73     private static final int LOW_MEMORY_NOTIFICATION_ID = 1;
     74     private static final int DEFAULT_THRESHOLD_PERCENTAGE = 10;
     75     private static final int DEFAULT_THRESHOLD_MAX_BYTES = 500*1024*1024; // 500MB
     76     private static final int DEFAULT_FREE_STORAGE_LOG_INTERVAL_IN_MINUTES = 12*60; //in minutes
     77     private static final long DEFAULT_DISK_FREE_CHANGE_REPORTING_THRESHOLD = 2 * 1024 * 1024; // 2MB
     78     private static final long DEFAULT_CHECK_INTERVAL = MONITOR_INTERVAL*60*1000;
     79     private static final int DEFAULT_FULL_THRESHOLD_BYTES = 1024*1024; // 1MB
     80     private long mFreeMem;  // on /data
     81     private long mFreeMemAfterLastCacheClear;  // on /data
     82     private long mLastReportedFreeMem;
     83     private long mLastReportedFreeMemTime;
     84     private boolean mLowMemFlag=false;
     85     private boolean mMemFullFlag=false;
     86     private Context mContext;
     87     private ContentResolver mContentResolver;
     88     private long mTotalMemory;  // on /data
     89     private StatFs mDataFileStats;
     90     private StatFs mSystemFileStats;
     91     private StatFs mCacheFileStats;
     92     private static final String DATA_PATH = "/data";
     93     private static final String SYSTEM_PATH = "/system";
     94     private static final String CACHE_PATH = "/cache";
     95     private long mThreadStartTime = -1;
     96     private boolean mClearSucceeded = false;
     97     private boolean mClearingCache;
     98     private Intent mStorageLowIntent;
     99     private Intent mStorageOkIntent;
    100     private Intent mStorageFullIntent;
    101     private Intent mStorageNotFullIntent;
    102     private CachePackageDataObserver mClearCacheObserver;
    103     private final CacheFileDeletedObserver mCacheFileDeletedObserver;
    104     private static final int _TRUE = 1;
    105     private static final int _FALSE = 0;
    106     // This is the raw threshold that has been set at which we consider
    107     // storage to be low.
    108     private long mMemLowThreshold;
    109     // This is the threshold at which we start trying to flush caches
    110     // to get below the low threshold limit.  It is less than the low
    111     // threshold; we will allow storage to get a bit beyond the limit
    112     // before flushing and checking if we are actually low.
    113     private long mMemCacheStartTrimThreshold;
    114     // This is the threshold that we try to get to when deleting cache
    115     // files.  This is greater than the low threshold so that we will flush
    116     // more files than absolutely needed, to reduce the frequency that
    117     // flushing takes place.
    118     private long mMemCacheTrimToThreshold;
    119     private int mMemFullThreshold;
    120 
    121     /**
    122      * This string is used for ServiceManager access to this class.
    123      */
    124     public static final String SERVICE = "devicestoragemonitor";
    125 
    126     /**
    127     * Handler that checks the amount of disk space on the device and sends a
    128     * notification if the device runs low on disk space
    129     */
    130     Handler mHandler = new Handler() {
    131         @Override
    132         public void handleMessage(Message msg) {
    133             //don't handle an invalid message
    134             if (msg.what != DEVICE_MEMORY_WHAT) {
    135                 Slog.e(TAG, "Will not process invalid message");
    136                 return;
    137             }
    138             checkMemory(msg.arg1 == _TRUE);
    139         }
    140     };
    141 
    142     class CachePackageDataObserver extends IPackageDataObserver.Stub {
    143         public void onRemoveCompleted(String packageName, boolean succeeded) {
    144             mClearSucceeded = succeeded;
    145             mClearingCache = false;
    146             if(localLOGV) Slog.i(TAG, " Clear succeeded:"+mClearSucceeded
    147                     +", mClearingCache:"+mClearingCache+" Forcing memory check");
    148             postCheckMemoryMsg(false, 0);
    149         }
    150     }
    151 
    152     private final void restatDataDir() {
    153         try {
    154             mDataFileStats.restat(DATA_PATH);
    155             mFreeMem = (long) mDataFileStats.getAvailableBlocks() *
    156                 mDataFileStats.getBlockSize();
    157         } catch (IllegalArgumentException e) {
    158             // use the old value of mFreeMem
    159         }
    160         // Allow freemem to be overridden by debug.freemem for testing
    161         String debugFreeMem = SystemProperties.get("debug.freemem");
    162         if (!"".equals(debugFreeMem)) {
    163             mFreeMem = Long.parseLong(debugFreeMem);
    164         }
    165         // Read the log interval from secure settings
    166         long freeMemLogInterval = Settings.Global.getLong(mContentResolver,
    167                 Settings.Global.SYS_FREE_STORAGE_LOG_INTERVAL,
    168                 DEFAULT_FREE_STORAGE_LOG_INTERVAL_IN_MINUTES)*60*1000;
    169         //log the amount of free memory in event log
    170         long currTime = SystemClock.elapsedRealtime();
    171         if((mLastReportedFreeMemTime == 0) ||
    172            (currTime-mLastReportedFreeMemTime) >= freeMemLogInterval) {
    173             mLastReportedFreeMemTime = currTime;
    174             long mFreeSystem = -1, mFreeCache = -1;
    175             try {
    176                 mSystemFileStats.restat(SYSTEM_PATH);
    177                 mFreeSystem = (long) mSystemFileStats.getAvailableBlocks() *
    178                     mSystemFileStats.getBlockSize();
    179             } catch (IllegalArgumentException e) {
    180                 // ignore; report -1
    181             }
    182             try {
    183                 mCacheFileStats.restat(CACHE_PATH);
    184                 mFreeCache = (long) mCacheFileStats.getAvailableBlocks() *
    185                     mCacheFileStats.getBlockSize();
    186             } catch (IllegalArgumentException e) {
    187                 // ignore; report -1
    188             }
    189             EventLog.writeEvent(EventLogTags.FREE_STORAGE_LEFT,
    190                                 mFreeMem, mFreeSystem, mFreeCache);
    191         }
    192         // Read the reporting threshold from secure settings
    193         long threshold = Settings.Global.getLong(mContentResolver,
    194                 Settings.Global.DISK_FREE_CHANGE_REPORTING_THRESHOLD,
    195                 DEFAULT_DISK_FREE_CHANGE_REPORTING_THRESHOLD);
    196         // If mFree changed significantly log the new value
    197         long delta = mFreeMem - mLastReportedFreeMem;
    198         if (delta > threshold || delta < -threshold) {
    199             mLastReportedFreeMem = mFreeMem;
    200             EventLog.writeEvent(EventLogTags.FREE_STORAGE_CHANGED, mFreeMem);
    201         }
    202     }
    203 
    204     private final void clearCache() {
    205         if (mClearCacheObserver == null) {
    206             // Lazy instantiation
    207             mClearCacheObserver = new CachePackageDataObserver();
    208         }
    209         mClearingCache = true;
    210         try {
    211             if (localLOGV) Slog.i(TAG, "Clearing cache");
    212             IPackageManager.Stub.asInterface(ServiceManager.getService("package")).
    213                     freeStorageAndNotify(mMemCacheTrimToThreshold, mClearCacheObserver);
    214         } catch (RemoteException e) {
    215             Slog.w(TAG, "Failed to get handle for PackageManger Exception: "+e);
    216             mClearingCache = false;
    217             mClearSucceeded = false;
    218         }
    219     }
    220 
    221     private final void checkMemory(boolean checkCache) {
    222         //if the thread that was started to clear cache is still running do nothing till its
    223         //finished clearing cache. Ideally this flag could be modified by clearCache
    224         // and should be accessed via a lock but even if it does this test will fail now and
    225         //hopefully the next time this flag will be set to the correct value.
    226         if(mClearingCache) {
    227             if(localLOGV) Slog.i(TAG, "Thread already running just skip");
    228             //make sure the thread is not hung for too long
    229             long diffTime = System.currentTimeMillis() - mThreadStartTime;
    230             if(diffTime > (10*60*1000)) {
    231                 Slog.w(TAG, "Thread that clears cache file seems to run for ever");
    232             }
    233         } else {
    234             restatDataDir();
    235             if (localLOGV)  Slog.v(TAG, "freeMemory="+mFreeMem);
    236 
    237             //post intent to NotificationManager to display icon if necessary
    238             if (mFreeMem < mMemLowThreshold) {
    239                 if (checkCache) {
    240                     // We are allowed to clear cache files at this point to
    241                     // try to get down below the limit, because this is not
    242                     // the initial call after a cache clear has been attempted.
    243                     // In this case we will try a cache clear if our free
    244                     // space has gone below the cache clear limit.
    245                     if (mFreeMem < mMemCacheStartTrimThreshold) {
    246                         // We only clear the cache if the free storage has changed
    247                         // a significant amount since the last time.
    248                         if ((mFreeMemAfterLastCacheClear-mFreeMem)
    249                                 >= ((mMemLowThreshold-mMemCacheStartTrimThreshold)/4)) {
    250                             // See if clearing cache helps
    251                             // Note that clearing cache is asynchronous and so we do a
    252                             // memory check again once the cache has been cleared.
    253                             mThreadStartTime = System.currentTimeMillis();
    254                             mClearSucceeded = false;
    255                             clearCache();
    256                         }
    257                     }
    258                 } else {
    259                     // This is a call from after clearing the cache.  Note
    260                     // the amount of free storage at this point.
    261                     mFreeMemAfterLastCacheClear = mFreeMem;
    262                     if (!mLowMemFlag) {
    263                         // We tried to clear the cache, but that didn't get us
    264                         // below the low storage limit.  Tell the user.
    265                         Slog.i(TAG, "Running low on memory. Sending notification");
    266                         sendNotification();
    267                         mLowMemFlag = true;
    268                     } else {
    269                         if (localLOGV) Slog.v(TAG, "Running low on memory " +
    270                                 "notification already sent. do nothing");
    271                     }
    272                 }
    273             } else {
    274                 mFreeMemAfterLastCacheClear = mFreeMem;
    275                 if (mLowMemFlag) {
    276                     Slog.i(TAG, "Memory available. Cancelling notification");
    277                     cancelNotification();
    278                     mLowMemFlag = false;
    279                 }
    280             }
    281             if (mFreeMem < mMemFullThreshold) {
    282                 if (!mMemFullFlag) {
    283                     sendFullNotification();
    284                     mMemFullFlag = true;
    285                 }
    286             } else {
    287                 if (mMemFullFlag) {
    288                     cancelFullNotification();
    289                     mMemFullFlag = false;
    290                 }
    291             }
    292         }
    293         if(localLOGV) Slog.i(TAG, "Posting Message again");
    294         //keep posting messages to itself periodically
    295         postCheckMemoryMsg(true, DEFAULT_CHECK_INTERVAL);
    296     }
    297 
    298     private void postCheckMemoryMsg(boolean clearCache, long delay) {
    299         // Remove queued messages
    300         mHandler.removeMessages(DEVICE_MEMORY_WHAT);
    301         mHandler.sendMessageDelayed(mHandler.obtainMessage(DEVICE_MEMORY_WHAT,
    302                 clearCache ?_TRUE : _FALSE, 0),
    303                 delay);
    304     }
    305 
    306     /*
    307      * just query settings to retrieve the memory threshold.
    308      * Preferred this over using a ContentObserver since Settings.Secure caches the value
    309      * any way
    310      */
    311     private long getMemThreshold() {
    312         long value = Settings.Global.getInt(
    313                               mContentResolver,
    314                               Settings.Global.SYS_STORAGE_THRESHOLD_PERCENTAGE,
    315                               DEFAULT_THRESHOLD_PERCENTAGE);
    316         if(localLOGV) Slog.v(TAG, "Threshold Percentage="+value);
    317         value = (value*mTotalMemory)/100;
    318         long maxValue = Settings.Global.getInt(
    319                 mContentResolver,
    320                 Settings.Global.SYS_STORAGE_THRESHOLD_MAX_BYTES,
    321                 DEFAULT_THRESHOLD_MAX_BYTES);
    322         //evaluate threshold value
    323         return value < maxValue ? value : maxValue;
    324     }
    325 
    326     /*
    327      * just query settings to retrieve the memory full threshold.
    328      * Preferred this over using a ContentObserver since Settings.Secure caches the value
    329      * any way
    330      */
    331     private int getMemFullThreshold() {
    332         int value = Settings.Global.getInt(
    333                               mContentResolver,
    334                               Settings.Global.SYS_STORAGE_FULL_THRESHOLD_BYTES,
    335                               DEFAULT_FULL_THRESHOLD_BYTES);
    336         if(localLOGV) Slog.v(TAG, "Full Threshold Bytes="+value);
    337         return value;
    338     }
    339 
    340     /**
    341     * Constructor to run service. initializes the disk space threshold value
    342     * and posts an empty message to kickstart the process.
    343     */
    344     public DeviceStorageMonitorService(Context context) {
    345         mLastReportedFreeMemTime = 0;
    346         mContext = context;
    347         mContentResolver = mContext.getContentResolver();
    348         //create StatFs object
    349         mDataFileStats = new StatFs(DATA_PATH);
    350         mSystemFileStats = new StatFs(SYSTEM_PATH);
    351         mCacheFileStats = new StatFs(CACHE_PATH);
    352         //initialize total storage on device
    353         mTotalMemory = (long)mDataFileStats.getBlockCount() *
    354                         mDataFileStats.getBlockSize();
    355         mStorageLowIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_LOW);
    356         mStorageLowIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
    357         mStorageOkIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_OK);
    358         mStorageOkIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
    359         mStorageFullIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_FULL);
    360         mStorageFullIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
    361         mStorageNotFullIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_NOT_FULL);
    362         mStorageNotFullIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
    363         // cache storage thresholds
    364         mMemLowThreshold = getMemThreshold();
    365         mMemFullThreshold = getMemFullThreshold();
    366         mMemCacheStartTrimThreshold = ((mMemLowThreshold*3)+mMemFullThreshold)/4;
    367         mMemCacheTrimToThreshold = mMemLowThreshold
    368                 + ((mMemLowThreshold-mMemCacheStartTrimThreshold)*2);
    369         mFreeMemAfterLastCacheClear = mTotalMemory;
    370         checkMemory(true);
    371 
    372         mCacheFileDeletedObserver = new CacheFileDeletedObserver();
    373         mCacheFileDeletedObserver.startWatching();
    374     }
    375 
    376 
    377     /**
    378     * This method sends a notification to NotificationManager to display
    379     * an error dialog indicating low disk space and launch the Installer
    380     * application
    381     */
    382     private final void sendNotification() {
    383         if(localLOGV) Slog.i(TAG, "Sending low memory notification");
    384         //log the event to event log with the amount of free storage(in bytes) left on the device
    385         EventLog.writeEvent(EventLogTags.LOW_STORAGE, mFreeMem);
    386         //  Pack up the values and broadcast them to everyone
    387         Intent lowMemIntent = new Intent(Environment.isExternalStorageEmulated()
    388                 ? Settings.ACTION_INTERNAL_STORAGE_SETTINGS
    389                 : Intent.ACTION_MANAGE_PACKAGE_STORAGE);
    390         lowMemIntent.putExtra("memory", mFreeMem);
    391         lowMemIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    392         NotificationManager mNotificationMgr =
    393                 (NotificationManager)mContext.getSystemService(
    394                         Context.NOTIFICATION_SERVICE);
    395         CharSequence title = mContext.getText(
    396                 com.android.internal.R.string.low_internal_storage_view_title);
    397         CharSequence details = mContext.getText(
    398                 com.android.internal.R.string.low_internal_storage_view_text);
    399         PendingIntent intent = PendingIntent.getActivityAsUser(mContext, 0,  lowMemIntent, 0,
    400                 null, UserHandle.CURRENT);
    401         Notification notification = new Notification();
    402         notification.icon = com.android.internal.R.drawable.stat_notify_disk_full;
    403         notification.tickerText = title;
    404         notification.flags |= Notification.FLAG_NO_CLEAR;
    405         notification.setLatestEventInfo(mContext, title, details, intent);
    406         mNotificationMgr.notifyAsUser(null, LOW_MEMORY_NOTIFICATION_ID, notification,
    407                 UserHandle.ALL);
    408         mContext.sendStickyBroadcastAsUser(mStorageLowIntent, UserHandle.ALL);
    409     }
    410 
    411     /**
    412      * Cancels low storage notification and sends OK intent.
    413      */
    414     private final void cancelNotification() {
    415         if(localLOGV) Slog.i(TAG, "Canceling low memory notification");
    416         NotificationManager mNotificationMgr =
    417                 (NotificationManager)mContext.getSystemService(
    418                         Context.NOTIFICATION_SERVICE);
    419         //cancel notification since memory has been freed
    420         mNotificationMgr.cancelAsUser(null, LOW_MEMORY_NOTIFICATION_ID, UserHandle.ALL);
    421 
    422         mContext.removeStickyBroadcastAsUser(mStorageLowIntent, UserHandle.ALL);
    423         mContext.sendBroadcastAsUser(mStorageOkIntent, UserHandle.ALL);
    424     }
    425 
    426     /**
    427      * Send a notification when storage is full.
    428      */
    429     private final void sendFullNotification() {
    430         if(localLOGV) Slog.i(TAG, "Sending memory full notification");
    431         mContext.sendStickyBroadcastAsUser(mStorageFullIntent, UserHandle.ALL);
    432     }
    433 
    434     /**
    435      * Cancels memory full notification and sends "not full" intent.
    436      */
    437     private final void cancelFullNotification() {
    438         if(localLOGV) Slog.i(TAG, "Canceling memory full notification");
    439         mContext.removeStickyBroadcastAsUser(mStorageFullIntent, UserHandle.ALL);
    440         mContext.sendBroadcastAsUser(mStorageNotFullIntent, UserHandle.ALL);
    441     }
    442 
    443     public void updateMemory() {
    444         int callingUid = getCallingUid();
    445         if(callingUid != Process.SYSTEM_UID) {
    446             return;
    447         }
    448         // force an early check
    449         postCheckMemoryMsg(true, 0);
    450     }
    451 
    452     /**
    453      * Callable from other things in the system service to obtain the low memory
    454      * threshold.
    455      *
    456      * @return low memory threshold in bytes
    457      */
    458     public long getMemoryLowThreshold() {
    459         return mMemLowThreshold;
    460     }
    461 
    462     /**
    463      * Callable from other things in the system process to check whether memory
    464      * is low.
    465      *
    466      * @return true is memory is low
    467      */
    468     public boolean isMemoryLow() {
    469         return mLowMemFlag;
    470     }
    471 
    472     public static class CacheFileDeletedObserver extends FileObserver {
    473         public CacheFileDeletedObserver() {
    474             super(Environment.getDownloadCacheDirectory().getAbsolutePath(), FileObserver.DELETE);
    475         }
    476 
    477         @Override
    478         public void onEvent(int event, String path) {
    479             EventLogTags.writeCacheFileDeleted(path);
    480         }
    481     }
    482 
    483     @Override
    484     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    485         if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
    486                 != PackageManager.PERMISSION_GRANTED) {
    487 
    488             pw.println("Permission Denial: can't dump " + SERVICE + " from from pid="
    489                     + Binder.getCallingPid()
    490                     + ", uid=" + Binder.getCallingUid());
    491             return;
    492         }
    493 
    494         pw.println("Current DeviceStorageMonitor state:");
    495         pw.print("  mFreeMem="); pw.print(Formatter.formatFileSize(mContext, mFreeMem));
    496                 pw.print(" mTotalMemory=");
    497                 pw.println(Formatter.formatFileSize(mContext, mTotalMemory));
    498         pw.print("  mFreeMemAfterLastCacheClear=");
    499                 pw.println(Formatter.formatFileSize(mContext, mFreeMemAfterLastCacheClear));
    500         pw.print("  mLastReportedFreeMem=");
    501                 pw.print(Formatter.formatFileSize(mContext, mLastReportedFreeMem));
    502                 pw.print(" mLastReportedFreeMemTime=");
    503                 TimeUtils.formatDuration(mLastReportedFreeMemTime, SystemClock.elapsedRealtime(), pw);
    504                 pw.println();
    505         pw.print("  mLowMemFlag="); pw.print(mLowMemFlag);
    506                 pw.print(" mMemFullFlag="); pw.println(mMemFullFlag);
    507         pw.print("  mClearSucceeded="); pw.print(mClearSucceeded);
    508                 pw.print(" mClearingCache="); pw.println(mClearingCache);
    509         pw.print("  mMemLowThreshold=");
    510                 pw.print(Formatter.formatFileSize(mContext, mMemLowThreshold));
    511                 pw.print(" mMemFullThreshold=");
    512                 pw.println(Formatter.formatFileSize(mContext, mMemFullThreshold));
    513         pw.print("  mMemCacheStartTrimThreshold=");
    514                 pw.print(Formatter.formatFileSize(mContext, mMemCacheStartTrimThreshold));
    515                 pw.print(" mMemCacheTrimToThreshold=");
    516                 pw.println(Formatter.formatFileSize(mContext, mMemCacheTrimToThreshold));
    517     }
    518 }
    519