Home | History | Annotate | Download | only in downloads
      1 /*
      2  * Copyright (C) 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.providers.downloads;
     18 
     19 import android.app.DownloadManager;
     20 import android.content.ContentResolver;
     21 import android.content.ContentUris;
     22 import android.content.ContentValues;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.database.Cursor;
     26 import android.net.ConnectivityManager;
     27 import android.net.NetworkInfo;
     28 import android.net.NetworkInfo.DetailedState;
     29 import android.net.Uri;
     30 import android.os.Environment;
     31 import android.provider.Downloads;
     32 import android.provider.Downloads.Impl;
     33 import android.text.TextUtils;
     34 import android.util.Log;
     35 import android.util.Pair;
     36 
     37 import com.android.internal.util.IndentingPrintWriter;
     38 
     39 import java.io.PrintWriter;
     40 import java.util.ArrayList;
     41 import java.util.Collection;
     42 import java.util.Collections;
     43 import java.util.List;
     44 
     45 /**
     46  * Stores information about an individual download.
     47  */
     48 public class DownloadInfo {
     49     public static class Reader {
     50         private ContentResolver mResolver;
     51         private Cursor mCursor;
     52 
     53         public Reader(ContentResolver resolver, Cursor cursor) {
     54             mResolver = resolver;
     55             mCursor = cursor;
     56         }
     57 
     58         public DownloadInfo newDownloadInfo(Context context, SystemFacade systemFacade) {
     59             DownloadInfo info = new DownloadInfo(context, systemFacade);
     60             updateFromDatabase(info);
     61             readRequestHeaders(info);
     62             return info;
     63         }
     64 
     65         public void updateFromDatabase(DownloadInfo info) {
     66             info.mId = getLong(Downloads.Impl._ID);
     67             info.mUri = getString(Downloads.Impl.COLUMN_URI);
     68             info.mNoIntegrity = getInt(Downloads.Impl.COLUMN_NO_INTEGRITY) == 1;
     69             info.mHint = getString(Downloads.Impl.COLUMN_FILE_NAME_HINT);
     70             info.mFileName = getString(Downloads.Impl._DATA);
     71             info.mMimeType = getString(Downloads.Impl.COLUMN_MIME_TYPE);
     72             info.mDestination = getInt(Downloads.Impl.COLUMN_DESTINATION);
     73             info.mVisibility = getInt(Downloads.Impl.COLUMN_VISIBILITY);
     74             info.mStatus = getInt(Downloads.Impl.COLUMN_STATUS);
     75             info.mNumFailed = getInt(Constants.FAILED_CONNECTIONS);
     76             int retryRedirect = getInt(Constants.RETRY_AFTER_X_REDIRECT_COUNT);
     77             info.mRetryAfter = retryRedirect & 0xfffffff;
     78             info.mLastMod = getLong(Downloads.Impl.COLUMN_LAST_MODIFICATION);
     79             info.mPackage = getString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
     80             info.mClass = getString(Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
     81             info.mExtras = getString(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS);
     82             info.mCookies = getString(Downloads.Impl.COLUMN_COOKIE_DATA);
     83             info.mUserAgent = getString(Downloads.Impl.COLUMN_USER_AGENT);
     84             info.mReferer = getString(Downloads.Impl.COLUMN_REFERER);
     85             info.mTotalBytes = getLong(Downloads.Impl.COLUMN_TOTAL_BYTES);
     86             info.mCurrentBytes = getLong(Downloads.Impl.COLUMN_CURRENT_BYTES);
     87             info.mETag = getString(Constants.ETAG);
     88             info.mUid = getInt(Constants.UID);
     89             info.mMediaScanned = getInt(Constants.MEDIA_SCANNED);
     90             info.mDeleted = getInt(Downloads.Impl.COLUMN_DELETED) == 1;
     91             info.mMediaProviderUri = getString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI);
     92             info.mIsPublicApi = getInt(Downloads.Impl.COLUMN_IS_PUBLIC_API) != 0;
     93             info.mAllowedNetworkTypes = getInt(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
     94             info.mAllowRoaming = getInt(Downloads.Impl.COLUMN_ALLOW_ROAMING) != 0;
     95             info.mAllowMetered = getInt(Downloads.Impl.COLUMN_ALLOW_METERED) != 0;
     96             info.mTitle = getString(Downloads.Impl.COLUMN_TITLE);
     97             info.mDescription = getString(Downloads.Impl.COLUMN_DESCRIPTION);
     98             info.mBypassRecommendedSizeLimit =
     99                     getInt(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
    100 
    101             synchronized (this) {
    102                 info.mControl = getInt(Downloads.Impl.COLUMN_CONTROL);
    103             }
    104         }
    105 
    106         private void readRequestHeaders(DownloadInfo info) {
    107             info.mRequestHeaders.clear();
    108             Uri headerUri = Uri.withAppendedPath(
    109                     info.getAllDownloadsUri(), Downloads.Impl.RequestHeaders.URI_SEGMENT);
    110             Cursor cursor = mResolver.query(headerUri, null, null, null, null);
    111             try {
    112                 int headerIndex =
    113                         cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_HEADER);
    114                 int valueIndex =
    115                         cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_VALUE);
    116                 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
    117                     addHeader(info, cursor.getString(headerIndex), cursor.getString(valueIndex));
    118                 }
    119             } finally {
    120                 cursor.close();
    121             }
    122 
    123             if (info.mCookies != null) {
    124                 addHeader(info, "Cookie", info.mCookies);
    125             }
    126             if (info.mReferer != null) {
    127                 addHeader(info, "Referer", info.mReferer);
    128             }
    129         }
    130 
    131         private void addHeader(DownloadInfo info, String header, String value) {
    132             info.mRequestHeaders.add(Pair.create(header, value));
    133         }
    134 
    135         private String getString(String column) {
    136             int index = mCursor.getColumnIndexOrThrow(column);
    137             String s = mCursor.getString(index);
    138             return (TextUtils.isEmpty(s)) ? null : s;
    139         }
    140 
    141         private Integer getInt(String column) {
    142             return mCursor.getInt(mCursor.getColumnIndexOrThrow(column));
    143         }
    144 
    145         private Long getLong(String column) {
    146             return mCursor.getLong(mCursor.getColumnIndexOrThrow(column));
    147         }
    148     }
    149 
    150     // the following NETWORK_* constants are used to indicates specfic reasons for disallowing a
    151     // download from using a network, since specific causes can require special handling
    152 
    153     /**
    154      * The network is usable for the given download.
    155      */
    156     public static final int NETWORK_OK = 1;
    157 
    158     /**
    159      * There is no network connectivity.
    160      */
    161     public static final int NETWORK_NO_CONNECTION = 2;
    162 
    163     /**
    164      * The download exceeds the maximum size for this network.
    165      */
    166     public static final int NETWORK_UNUSABLE_DUE_TO_SIZE = 3;
    167 
    168     /**
    169      * The download exceeds the recommended maximum size for this network, the user must confirm for
    170      * this download to proceed without WiFi.
    171      */
    172     public static final int NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE = 4;
    173 
    174     /**
    175      * The current connection is roaming, and the download can't proceed over a roaming connection.
    176      */
    177     public static final int NETWORK_CANNOT_USE_ROAMING = 5;
    178 
    179     /**
    180      * The app requesting the download specific that it can't use the current network connection.
    181      */
    182     public static final int NETWORK_TYPE_DISALLOWED_BY_REQUESTOR = 6;
    183 
    184     /**
    185      * Current network is blocked for requesting application.
    186      */
    187     public static final int NETWORK_BLOCKED = 7;
    188 
    189     /**
    190      * For intents used to notify the user that a download exceeds a size threshold, if this extra
    191      * is true, WiFi is required for this download size; otherwise, it is only recommended.
    192      */
    193     public static final String EXTRA_IS_WIFI_REQUIRED = "isWifiRequired";
    194 
    195 
    196     public long mId;
    197     public String mUri;
    198     public boolean mNoIntegrity;
    199     public String mHint;
    200     public String mFileName;
    201     public String mMimeType;
    202     public int mDestination;
    203     public int mVisibility;
    204     public int mControl;
    205     public int mStatus;
    206     public int mNumFailed;
    207     public int mRetryAfter;
    208     public long mLastMod;
    209     public String mPackage;
    210     public String mClass;
    211     public String mExtras;
    212     public String mCookies;
    213     public String mUserAgent;
    214     public String mReferer;
    215     public long mTotalBytes;
    216     public long mCurrentBytes;
    217     public String mETag;
    218     public int mUid;
    219     public int mMediaScanned;
    220     public boolean mDeleted;
    221     public String mMediaProviderUri;
    222     public boolean mIsPublicApi;
    223     public int mAllowedNetworkTypes;
    224     public boolean mAllowRoaming;
    225     public boolean mAllowMetered;
    226     public String mTitle;
    227     public String mDescription;
    228     public int mBypassRecommendedSizeLimit;
    229 
    230     public int mFuzz;
    231 
    232     private List<Pair<String, String>> mRequestHeaders = new ArrayList<Pair<String, String>>();
    233     private SystemFacade mSystemFacade;
    234     private Context mContext;
    235 
    236     private DownloadInfo(Context context, SystemFacade systemFacade) {
    237         mContext = context;
    238         mSystemFacade = systemFacade;
    239         mFuzz = Helpers.sRandom.nextInt(1001);
    240     }
    241 
    242     public Collection<Pair<String, String>> getHeaders() {
    243         return Collections.unmodifiableList(mRequestHeaders);
    244     }
    245 
    246     public void sendIntentIfRequested() {
    247         if (mPackage == null) {
    248             return;
    249         }
    250 
    251         Intent intent;
    252         if (mIsPublicApi) {
    253             intent = new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
    254             intent.setPackage(mPackage);
    255             intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, mId);
    256         } else { // legacy behavior
    257             if (mClass == null) {
    258                 return;
    259             }
    260             intent = new Intent(Downloads.Impl.ACTION_DOWNLOAD_COMPLETED);
    261             intent.setClassName(mPackage, mClass);
    262             if (mExtras != null) {
    263                 intent.putExtra(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, mExtras);
    264             }
    265             // We only send the content: URI, for security reasons. Otherwise, malicious
    266             //     applications would have an easier time spoofing download results by
    267             //     sending spoofed intents.
    268             intent.setData(getMyDownloadsUri());
    269         }
    270         mSystemFacade.sendBroadcast(intent);
    271     }
    272 
    273     /**
    274      * Returns the time when a download should be restarted.
    275      */
    276     public long restartTime(long now) {
    277         if (mNumFailed == 0) {
    278             return now;
    279         }
    280         if (mRetryAfter > 0) {
    281             return mLastMod + mRetryAfter;
    282         }
    283         return mLastMod +
    284                 Constants.RETRY_FIRST_DELAY *
    285                     (1000 + mFuzz) * (1 << (mNumFailed - 1));
    286     }
    287 
    288     /**
    289      * Returns whether this download (which the download manager hasn't seen yet)
    290      * should be started.
    291      */
    292     private boolean isReadyToStart(long now) {
    293         if (DownloadHandler.getInstance().hasDownloadInQueue(mId)) {
    294             // already running
    295             return false;
    296         }
    297         if (mControl == Downloads.Impl.CONTROL_PAUSED) {
    298             // the download is paused, so it's not going to start
    299             return false;
    300         }
    301         switch (mStatus) {
    302             case 0: // status hasn't been initialized yet, this is a new download
    303             case Downloads.Impl.STATUS_PENDING: // download is explicit marked as ready to start
    304             case Downloads.Impl.STATUS_RUNNING: // download interrupted (process killed etc) while
    305                                                 // running, without a chance to update the database
    306                 return true;
    307 
    308             case Downloads.Impl.STATUS_WAITING_FOR_NETWORK:
    309             case Downloads.Impl.STATUS_QUEUED_FOR_WIFI:
    310                 return checkCanUseNetwork() == NETWORK_OK;
    311 
    312             case Downloads.Impl.STATUS_WAITING_TO_RETRY:
    313                 // download was waiting for a delayed restart
    314                 return restartTime(now) <= now;
    315             case Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR:
    316                 // is the media mounted?
    317                 return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
    318             case Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR:
    319                 // should check space to make sure it is worth retrying the download.
    320                 // but thats the first thing done by the thread when it retries to download
    321                 // it will fail pretty quickly if there is no space.
    322                 // so, it is not that bad to skip checking space availability here.
    323                 return true;
    324         }
    325         return false;
    326     }
    327 
    328     /**
    329      * Returns whether this download has a visible notification after
    330      * completion.
    331      */
    332     public boolean hasCompletionNotification() {
    333         if (!Downloads.Impl.isStatusCompleted(mStatus)) {
    334             return false;
    335         }
    336         if (mVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) {
    337             return true;
    338         }
    339         return false;
    340     }
    341 
    342     /**
    343      * Returns whether this download is allowed to use the network.
    344      * @return one of the NETWORK_* constants
    345      */
    346     public int checkCanUseNetwork() {
    347         final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mUid);
    348         if (info == null) {
    349             return NETWORK_NO_CONNECTION;
    350         }
    351         if (DetailedState.BLOCKED.equals(info.getDetailedState())) {
    352             return NETWORK_BLOCKED;
    353         }
    354         if (!isRoamingAllowed() && mSystemFacade.isNetworkRoaming()) {
    355             return NETWORK_CANNOT_USE_ROAMING;
    356         }
    357         if (!mAllowMetered && mSystemFacade.isActiveNetworkMetered()) {
    358             return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR;
    359         }
    360         return checkIsNetworkTypeAllowed(info.getType());
    361     }
    362 
    363     private boolean isRoamingAllowed() {
    364         if (mIsPublicApi) {
    365             return mAllowRoaming;
    366         } else { // legacy behavior
    367             return mDestination != Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING;
    368         }
    369     }
    370 
    371     /**
    372      * @return a non-localized string appropriate for logging corresponding to one of the
    373      * NETWORK_* constants.
    374      */
    375     public String getLogMessageForNetworkError(int networkError) {
    376         switch (networkError) {
    377             case NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE:
    378                 return "download size exceeds recommended limit for mobile network";
    379 
    380             case NETWORK_UNUSABLE_DUE_TO_SIZE:
    381                 return "download size exceeds limit for mobile network";
    382 
    383             case NETWORK_NO_CONNECTION:
    384                 return "no network connection available";
    385 
    386             case NETWORK_CANNOT_USE_ROAMING:
    387                 return "download cannot use the current network connection because it is roaming";
    388 
    389             case NETWORK_TYPE_DISALLOWED_BY_REQUESTOR:
    390                 return "download was requested to not use the current network type";
    391 
    392             case NETWORK_BLOCKED:
    393                 return "network is blocked for requesting application";
    394 
    395             default:
    396                 return "unknown error with network connectivity";
    397         }
    398     }
    399 
    400     /**
    401      * Check if this download can proceed over the given network type.
    402      * @param networkType a constant from ConnectivityManager.TYPE_*.
    403      * @return one of the NETWORK_* constants
    404      */
    405     private int checkIsNetworkTypeAllowed(int networkType) {
    406         if (mIsPublicApi) {
    407             final int flag = translateNetworkTypeToApiFlag(networkType);
    408             final boolean allowAllNetworkTypes = mAllowedNetworkTypes == ~0;
    409             if (!allowAllNetworkTypes && (flag & mAllowedNetworkTypes) == 0) {
    410                 return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR;
    411             }
    412         }
    413         return checkSizeAllowedForNetwork(networkType);
    414     }
    415 
    416     /**
    417      * Translate a ConnectivityManager.TYPE_* constant to the corresponding
    418      * DownloadManager.Request.NETWORK_* bit flag.
    419      */
    420     private int translateNetworkTypeToApiFlag(int networkType) {
    421         switch (networkType) {
    422             case ConnectivityManager.TYPE_MOBILE:
    423                 return DownloadManager.Request.NETWORK_MOBILE;
    424 
    425             case ConnectivityManager.TYPE_WIFI:
    426                 return DownloadManager.Request.NETWORK_WIFI;
    427 
    428             default:
    429                 return 0;
    430         }
    431     }
    432 
    433     /**
    434      * Check if the download's size prohibits it from running over the current network.
    435      * @return one of the NETWORK_* constants
    436      */
    437     private int checkSizeAllowedForNetwork(int networkType) {
    438         if (mTotalBytes <= 0) {
    439             return NETWORK_OK; // we don't know the size yet
    440         }
    441         if (networkType == ConnectivityManager.TYPE_WIFI) {
    442             return NETWORK_OK; // anything goes over wifi
    443         }
    444         Long maxBytesOverMobile = mSystemFacade.getMaxBytesOverMobile();
    445         if (maxBytesOverMobile != null && mTotalBytes > maxBytesOverMobile) {
    446             return NETWORK_UNUSABLE_DUE_TO_SIZE;
    447         }
    448         if (mBypassRecommendedSizeLimit == 0) {
    449             Long recommendedMaxBytesOverMobile = mSystemFacade.getRecommendedMaxBytesOverMobile();
    450             if (recommendedMaxBytesOverMobile != null
    451                     && mTotalBytes > recommendedMaxBytesOverMobile) {
    452                 return NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE;
    453             }
    454         }
    455         return NETWORK_OK;
    456     }
    457 
    458     void startIfReady(long now, StorageManager storageManager) {
    459         if (!isReadyToStart(now)) {
    460             return;
    461         }
    462 
    463         if (Constants.LOGV) {
    464             Log.v(Constants.TAG, "Service spawning thread to handle download " + mId);
    465         }
    466         if (mStatus != Impl.STATUS_RUNNING) {
    467             mStatus = Impl.STATUS_RUNNING;
    468             ContentValues values = new ContentValues();
    469             values.put(Impl.COLUMN_STATUS, mStatus);
    470             mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null);
    471         }
    472         DownloadHandler.getInstance().enqueueDownload(this);
    473     }
    474 
    475     public boolean isOnCache() {
    476         return (mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION
    477                 || mDestination == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION
    478                 || mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING
    479                 || mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE);
    480     }
    481 
    482     public Uri getMyDownloadsUri() {
    483         return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, mId);
    484     }
    485 
    486     public Uri getAllDownloadsUri() {
    487         return ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, mId);
    488     }
    489 
    490     public void dump(IndentingPrintWriter pw) {
    491         pw.println("DownloadInfo:");
    492         pw.increaseIndent();
    493 
    494         pw.printPair("mId", mId);
    495         pw.printPair("mLastMod", mLastMod);
    496         pw.printPair("mPackage", mPackage);
    497         pw.printPair("mUid", mUid);
    498         pw.println();
    499 
    500         pw.printPair("mUri", mUri);
    501         pw.println();
    502 
    503         pw.printPair("mMimeType", mMimeType);
    504         pw.printPair("mCookies", (mCookies != null) ? "yes" : "no");
    505         pw.printPair("mReferer", (mReferer != null) ? "yes" : "no");
    506         pw.printPair("mUserAgent", mUserAgent);
    507         pw.println();
    508 
    509         pw.printPair("mFileName", mFileName);
    510         pw.printPair("mDestination", mDestination);
    511         pw.println();
    512 
    513         pw.printPair("mStatus", Downloads.Impl.statusToString(mStatus));
    514         pw.printPair("mCurrentBytes", mCurrentBytes);
    515         pw.printPair("mTotalBytes", mTotalBytes);
    516         pw.println();
    517 
    518         pw.printPair("mNumFailed", mNumFailed);
    519         pw.printPair("mRetryAfter", mRetryAfter);
    520         pw.printPair("mETag", mETag);
    521         pw.printPair("mIsPublicApi", mIsPublicApi);
    522         pw.println();
    523 
    524         pw.printPair("mAllowedNetworkTypes", mAllowedNetworkTypes);
    525         pw.printPair("mAllowRoaming", mAllowRoaming);
    526         pw.printPair("mAllowMetered", mAllowMetered);
    527         pw.println();
    528 
    529         pw.decreaseIndent();
    530     }
    531 
    532     /**
    533      * Returns the amount of time (as measured from the "now" parameter)
    534      * at which a download will be active.
    535      * 0 = immediately - service should stick around to handle this download.
    536      * -1 = never - service can go away without ever waking up.
    537      * positive value - service must wake up in the future, as specified in ms from "now"
    538      */
    539     long nextAction(long now) {
    540         if (Downloads.Impl.isStatusCompleted(mStatus)) {
    541             return -1;
    542         }
    543         if (mStatus != Downloads.Impl.STATUS_WAITING_TO_RETRY) {
    544             return 0;
    545         }
    546         long when = restartTime(now);
    547         if (when <= now) {
    548             return 0;
    549         }
    550         return when - now;
    551     }
    552 
    553     /**
    554      * Returns whether a file should be scanned
    555      */
    556     boolean shouldScanFile() {
    557         return (mMediaScanned == 0)
    558                 && (mDestination == Downloads.Impl.DESTINATION_EXTERNAL ||
    559                         mDestination == Downloads.Impl.DESTINATION_FILE_URI ||
    560                         mDestination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
    561                 && Downloads.Impl.isStatusSuccess(mStatus);
    562     }
    563 
    564     void notifyPauseDueToSize(boolean isWifiRequired) {
    565         Intent intent = new Intent(Intent.ACTION_VIEW);
    566         intent.setData(getAllDownloadsUri());
    567         intent.setClassName(SizeLimitActivity.class.getPackage().getName(),
    568                 SizeLimitActivity.class.getName());
    569         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    570         intent.putExtra(EXTRA_IS_WIFI_REQUIRED, isWifiRequired);
    571         mContext.startActivity(intent);
    572     }
    573 
    574     void startDownloadThread() {
    575         DownloadThread downloader = new DownloadThread(mContext, mSystemFacade, this,
    576                 StorageManager.getInstance(mContext));
    577         mSystemFacade.startThread(downloader);
    578     }
    579 }
    580