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.Pair; 35 36 import com.android.internal.annotations.GuardedBy; 37 import com.android.internal.util.IndentingPrintWriter; 38 39 import java.util.ArrayList; 40 import java.util.Collection; 41 import java.util.Collections; 42 import java.util.List; 43 import java.util.concurrent.Executor; 44 import java.util.concurrent.ExecutorService; 45 import java.util.concurrent.Future; 46 47 /** 48 * Stores information about an individual download. 49 */ 50 public class DownloadInfo { 51 // TODO: move towards these in-memory objects being sources of truth, and 52 // periodically pushing to provider. 53 54 public static class Reader { 55 private ContentResolver mResolver; 56 private Cursor mCursor; 57 Reader(ContentResolver resolver, Cursor cursor)58 public Reader(ContentResolver resolver, Cursor cursor) { 59 mResolver = resolver; 60 mCursor = cursor; 61 } 62 newDownloadInfo(Context context, SystemFacade systemFacade, StorageManager storageManager, DownloadNotifier notifier)63 public DownloadInfo newDownloadInfo(Context context, SystemFacade systemFacade, 64 StorageManager storageManager, DownloadNotifier notifier) { 65 final DownloadInfo info = new DownloadInfo( 66 context, systemFacade, storageManager, notifier); 67 updateFromDatabase(info); 68 readRequestHeaders(info); 69 return info; 70 } 71 updateFromDatabase(DownloadInfo info)72 public void updateFromDatabase(DownloadInfo info) { 73 info.mId = getLong(Downloads.Impl._ID); 74 info.mUri = getString(Downloads.Impl.COLUMN_URI); 75 info.mNoIntegrity = getInt(Downloads.Impl.COLUMN_NO_INTEGRITY) == 1; 76 info.mHint = getString(Downloads.Impl.COLUMN_FILE_NAME_HINT); 77 info.mFileName = getString(Downloads.Impl._DATA); 78 info.mMimeType = getString(Downloads.Impl.COLUMN_MIME_TYPE); 79 info.mDestination = getInt(Downloads.Impl.COLUMN_DESTINATION); 80 info.mVisibility = getInt(Downloads.Impl.COLUMN_VISIBILITY); 81 info.mStatus = getInt(Downloads.Impl.COLUMN_STATUS); 82 info.mNumFailed = getInt(Downloads.Impl.COLUMN_FAILED_CONNECTIONS); 83 int retryRedirect = getInt(Constants.RETRY_AFTER_X_REDIRECT_COUNT); 84 info.mRetryAfter = retryRedirect & 0xfffffff; 85 info.mLastMod = getLong(Downloads.Impl.COLUMN_LAST_MODIFICATION); 86 info.mPackage = getString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); 87 info.mClass = getString(Downloads.Impl.COLUMN_NOTIFICATION_CLASS); 88 info.mExtras = getString(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS); 89 info.mCookies = getString(Downloads.Impl.COLUMN_COOKIE_DATA); 90 info.mUserAgent = getString(Downloads.Impl.COLUMN_USER_AGENT); 91 info.mReferer = getString(Downloads.Impl.COLUMN_REFERER); 92 info.mTotalBytes = getLong(Downloads.Impl.COLUMN_TOTAL_BYTES); 93 info.mCurrentBytes = getLong(Downloads.Impl.COLUMN_CURRENT_BYTES); 94 info.mETag = getString(Constants.ETAG); 95 info.mUid = getInt(Constants.UID); 96 info.mMediaScanned = getInt(Constants.MEDIA_SCANNED); 97 info.mDeleted = getInt(Downloads.Impl.COLUMN_DELETED) == 1; 98 info.mMediaProviderUri = getString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI); 99 info.mIsPublicApi = getInt(Downloads.Impl.COLUMN_IS_PUBLIC_API) != 0; 100 info.mAllowedNetworkTypes = getInt(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES); 101 info.mAllowRoaming = getInt(Downloads.Impl.COLUMN_ALLOW_ROAMING) != 0; 102 info.mAllowMetered = getInt(Downloads.Impl.COLUMN_ALLOW_METERED) != 0; 103 info.mTitle = getString(Downloads.Impl.COLUMN_TITLE); 104 info.mDescription = getString(Downloads.Impl.COLUMN_DESCRIPTION); 105 info.mBypassRecommendedSizeLimit = 106 getInt(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT); 107 108 synchronized (this) { 109 info.mControl = getInt(Downloads.Impl.COLUMN_CONTROL); 110 } 111 } 112 readRequestHeaders(DownloadInfo info)113 private void readRequestHeaders(DownloadInfo info) { 114 info.mRequestHeaders.clear(); 115 Uri headerUri = Uri.withAppendedPath( 116 info.getAllDownloadsUri(), Downloads.Impl.RequestHeaders.URI_SEGMENT); 117 Cursor cursor = mResolver.query(headerUri, null, null, null, null); 118 try { 119 int headerIndex = 120 cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_HEADER); 121 int valueIndex = 122 cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_VALUE); 123 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 124 addHeader(info, cursor.getString(headerIndex), cursor.getString(valueIndex)); 125 } 126 } finally { 127 cursor.close(); 128 } 129 130 if (info.mCookies != null) { 131 addHeader(info, "Cookie", info.mCookies); 132 } 133 if (info.mReferer != null) { 134 addHeader(info, "Referer", info.mReferer); 135 } 136 } 137 addHeader(DownloadInfo info, String header, String value)138 private void addHeader(DownloadInfo info, String header, String value) { 139 info.mRequestHeaders.add(Pair.create(header, value)); 140 } 141 getString(String column)142 private String getString(String column) { 143 int index = mCursor.getColumnIndexOrThrow(column); 144 String s = mCursor.getString(index); 145 return (TextUtils.isEmpty(s)) ? null : s; 146 } 147 getInt(String column)148 private Integer getInt(String column) { 149 return mCursor.getInt(mCursor.getColumnIndexOrThrow(column)); 150 } 151 getLong(String column)152 private Long getLong(String column) { 153 return mCursor.getLong(mCursor.getColumnIndexOrThrow(column)); 154 } 155 } 156 157 /** 158 * Constants used to indicate network state for a specific download, after 159 * applying any requested constraints. 160 */ 161 public enum NetworkState { 162 /** 163 * The network is usable for the given download. 164 */ 165 OK, 166 167 /** 168 * There is no network connectivity. 169 */ 170 NO_CONNECTION, 171 172 /** 173 * The download exceeds the maximum size for this network. 174 */ 175 UNUSABLE_DUE_TO_SIZE, 176 177 /** 178 * The download exceeds the recommended maximum size for this network, 179 * the user must confirm for this download to proceed without WiFi. 180 */ 181 RECOMMENDED_UNUSABLE_DUE_TO_SIZE, 182 183 /** 184 * The current connection is roaming, and the download can't proceed 185 * over a roaming connection. 186 */ 187 CANNOT_USE_ROAMING, 188 189 /** 190 * The app requesting the download specific that it can't use the 191 * current network connection. 192 */ 193 TYPE_DISALLOWED_BY_REQUESTOR, 194 195 /** 196 * Current network is blocked for requesting application. 197 */ 198 BLOCKED; 199 } 200 201 /** 202 * For intents used to notify the user that a download exceeds a size threshold, if this extra 203 * is true, WiFi is required for this download size; otherwise, it is only recommended. 204 */ 205 public static final String EXTRA_IS_WIFI_REQUIRED = "isWifiRequired"; 206 207 public long mId; 208 public String mUri; 209 public boolean mNoIntegrity; 210 public String mHint; 211 public String mFileName; 212 public String mMimeType; 213 public int mDestination; 214 public int mVisibility; 215 public int mControl; 216 public int mStatus; 217 public int mNumFailed; 218 public int mRetryAfter; 219 public long mLastMod; 220 public String mPackage; 221 public String mClass; 222 public String mExtras; 223 public String mCookies; 224 public String mUserAgent; 225 public String mReferer; 226 public long mTotalBytes; 227 public long mCurrentBytes; 228 public String mETag; 229 public int mUid; 230 public int mMediaScanned; 231 public boolean mDeleted; 232 public String mMediaProviderUri; 233 public boolean mIsPublicApi; 234 public int mAllowedNetworkTypes; 235 public boolean mAllowRoaming; 236 public boolean mAllowMetered; 237 public String mTitle; 238 public String mDescription; 239 public int mBypassRecommendedSizeLimit; 240 241 public int mFuzz; 242 243 private List<Pair<String, String>> mRequestHeaders = new ArrayList<Pair<String, String>>(); 244 245 /** 246 * Result of last {@link DownloadThread} started by 247 * {@link #startDownloadIfReady(ExecutorService)}. 248 */ 249 @GuardedBy("this") 250 private Future<?> mSubmittedTask; 251 252 @GuardedBy("this") 253 private DownloadThread mTask; 254 255 private final Context mContext; 256 private final SystemFacade mSystemFacade; 257 private final StorageManager mStorageManager; 258 private final DownloadNotifier mNotifier; 259 DownloadInfo(Context context, SystemFacade systemFacade, StorageManager storageManager, DownloadNotifier notifier)260 private DownloadInfo(Context context, SystemFacade systemFacade, StorageManager storageManager, 261 DownloadNotifier notifier) { 262 mContext = context; 263 mSystemFacade = systemFacade; 264 mStorageManager = storageManager; 265 mNotifier = notifier; 266 mFuzz = Helpers.sRandom.nextInt(1001); 267 } 268 getHeaders()269 public Collection<Pair<String, String>> getHeaders() { 270 return Collections.unmodifiableList(mRequestHeaders); 271 } 272 sendIntentIfRequested()273 public void sendIntentIfRequested() { 274 if (mPackage == null) { 275 return; 276 } 277 278 Intent intent; 279 if (mIsPublicApi) { 280 intent = new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE); 281 intent.setPackage(mPackage); 282 intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, mId); 283 } else { // legacy behavior 284 if (mClass == null) { 285 return; 286 } 287 intent = new Intent(Downloads.Impl.ACTION_DOWNLOAD_COMPLETED); 288 intent.setClassName(mPackage, mClass); 289 if (mExtras != null) { 290 intent.putExtra(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, mExtras); 291 } 292 // We only send the content: URI, for security reasons. Otherwise, malicious 293 // applications would have an easier time spoofing download results by 294 // sending spoofed intents. 295 intent.setData(getMyDownloadsUri()); 296 } 297 mSystemFacade.sendBroadcast(intent); 298 } 299 300 /** 301 * Returns the time when a download should be restarted. 302 */ restartTime(long now)303 public long restartTime(long now) { 304 if (mNumFailed == 0) { 305 return now; 306 } 307 if (mRetryAfter > 0) { 308 return mLastMod + mRetryAfter; 309 } 310 return mLastMod + 311 Constants.RETRY_FIRST_DELAY * 312 (1000 + mFuzz) * (1 << (mNumFailed - 1)); 313 } 314 315 /** 316 * Returns whether this download should be enqueued. 317 */ isReadyToDownload()318 private boolean isReadyToDownload() { 319 if (mControl == Downloads.Impl.CONTROL_PAUSED) { 320 // the download is paused, so it's not going to start 321 return false; 322 } 323 switch (mStatus) { 324 case 0: // status hasn't been initialized yet, this is a new download 325 case Downloads.Impl.STATUS_PENDING: // download is explicit marked as ready to start 326 case Downloads.Impl.STATUS_RUNNING: // download interrupted (process killed etc) while 327 // running, without a chance to update the database 328 return true; 329 330 case Downloads.Impl.STATUS_WAITING_FOR_NETWORK: 331 case Downloads.Impl.STATUS_QUEUED_FOR_WIFI: 332 return checkCanUseNetwork() == NetworkState.OK; 333 334 case Downloads.Impl.STATUS_WAITING_TO_RETRY: 335 // download was waiting for a delayed restart 336 final long now = mSystemFacade.currentTimeMillis(); 337 return restartTime(now) <= now; 338 case Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR: 339 // is the media mounted? 340 return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); 341 case Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR: 342 // avoids repetition of retrying download 343 return false; 344 } 345 return false; 346 } 347 348 /** 349 * Returns whether this download has a visible notification after 350 * completion. 351 */ hasCompletionNotification()352 public boolean hasCompletionNotification() { 353 if (!Downloads.Impl.isStatusCompleted(mStatus)) { 354 return false; 355 } 356 if (mVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) { 357 return true; 358 } 359 return false; 360 } 361 362 /** 363 * Returns whether this download is allowed to use the network. 364 */ checkCanUseNetwork()365 public NetworkState checkCanUseNetwork() { 366 final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mUid); 367 if (info == null || !info.isConnected()) { 368 return NetworkState.NO_CONNECTION; 369 } 370 if (DetailedState.BLOCKED.equals(info.getDetailedState())) { 371 return NetworkState.BLOCKED; 372 } 373 if (mSystemFacade.isNetworkRoaming() && !isRoamingAllowed()) { 374 return NetworkState.CANNOT_USE_ROAMING; 375 } 376 if (mSystemFacade.isActiveNetworkMetered() && !mAllowMetered) { 377 return NetworkState.TYPE_DISALLOWED_BY_REQUESTOR; 378 } 379 return checkIsNetworkTypeAllowed(info.getType()); 380 } 381 isRoamingAllowed()382 private boolean isRoamingAllowed() { 383 if (mIsPublicApi) { 384 return mAllowRoaming; 385 } else { // legacy behavior 386 return mDestination != Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING; 387 } 388 } 389 390 /** 391 * Check if this download can proceed over the given network type. 392 * @param networkType a constant from ConnectivityManager.TYPE_*. 393 * @return one of the NETWORK_* constants 394 */ checkIsNetworkTypeAllowed(int networkType)395 private NetworkState checkIsNetworkTypeAllowed(int networkType) { 396 if (mIsPublicApi) { 397 final int flag = translateNetworkTypeToApiFlag(networkType); 398 final boolean allowAllNetworkTypes = mAllowedNetworkTypes == ~0; 399 if (!allowAllNetworkTypes && (flag & mAllowedNetworkTypes) == 0) { 400 return NetworkState.TYPE_DISALLOWED_BY_REQUESTOR; 401 } 402 } 403 return checkSizeAllowedForNetwork(networkType); 404 } 405 406 /** 407 * Translate a ConnectivityManager.TYPE_* constant to the corresponding 408 * DownloadManager.Request.NETWORK_* bit flag. 409 */ translateNetworkTypeToApiFlag(int networkType)410 private int translateNetworkTypeToApiFlag(int networkType) { 411 switch (networkType) { 412 case ConnectivityManager.TYPE_MOBILE: 413 return DownloadManager.Request.NETWORK_MOBILE; 414 415 case ConnectivityManager.TYPE_WIFI: 416 return DownloadManager.Request.NETWORK_WIFI; 417 418 case ConnectivityManager.TYPE_BLUETOOTH: 419 return DownloadManager.Request.NETWORK_BLUETOOTH; 420 421 default: 422 return 0; 423 } 424 } 425 426 /** 427 * Check if the download's size prohibits it from running over the current network. 428 * @return one of the NETWORK_* constants 429 */ checkSizeAllowedForNetwork(int networkType)430 private NetworkState checkSizeAllowedForNetwork(int networkType) { 431 if (mTotalBytes <= 0) { 432 return NetworkState.OK; // we don't know the size yet 433 } 434 if (networkType == ConnectivityManager.TYPE_WIFI) { 435 return NetworkState.OK; // anything goes over wifi 436 } 437 Long maxBytesOverMobile = mSystemFacade.getMaxBytesOverMobile(); 438 if (maxBytesOverMobile != null && mTotalBytes > maxBytesOverMobile) { 439 return NetworkState.UNUSABLE_DUE_TO_SIZE; 440 } 441 if (mBypassRecommendedSizeLimit == 0) { 442 Long recommendedMaxBytesOverMobile = mSystemFacade.getRecommendedMaxBytesOverMobile(); 443 if (recommendedMaxBytesOverMobile != null 444 && mTotalBytes > recommendedMaxBytesOverMobile) { 445 return NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE; 446 } 447 } 448 return NetworkState.OK; 449 } 450 451 /** 452 * If download is ready to start, and isn't already pending or executing, 453 * create a {@link DownloadThread} and enqueue it into given 454 * {@link Executor}. 455 * 456 * @return If actively downloading. 457 */ startDownloadIfReady(ExecutorService executor)458 public boolean startDownloadIfReady(ExecutorService executor) { 459 synchronized (this) { 460 final boolean isReady = isReadyToDownload(); 461 final boolean isActive = mSubmittedTask != null && !mSubmittedTask.isDone(); 462 if (isReady && !isActive) { 463 if (mStatus != Impl.STATUS_RUNNING) { 464 mStatus = Impl.STATUS_RUNNING; 465 ContentValues values = new ContentValues(); 466 values.put(Impl.COLUMN_STATUS, mStatus); 467 mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null); 468 } 469 470 mTask = new DownloadThread( 471 mContext, mSystemFacade, this, mStorageManager, mNotifier); 472 mSubmittedTask = executor.submit(mTask); 473 } 474 return isReady; 475 } 476 } 477 478 /** 479 * If download is ready to be scanned, enqueue it into the given 480 * {@link DownloadScanner}. 481 * 482 * @return If actively scanning. 483 */ startScanIfReady(DownloadScanner scanner)484 public boolean startScanIfReady(DownloadScanner scanner) { 485 synchronized (this) { 486 final boolean isReady = shouldScanFile(); 487 if (isReady) { 488 scanner.requestScan(this); 489 } 490 return isReady; 491 } 492 } 493 isOnCache()494 public boolean isOnCache() { 495 return (mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION 496 || mDestination == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION 497 || mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING 498 || mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE); 499 } 500 getMyDownloadsUri()501 public Uri getMyDownloadsUri() { 502 return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, mId); 503 } 504 getAllDownloadsUri()505 public Uri getAllDownloadsUri() { 506 return ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, mId); 507 } 508 dump(IndentingPrintWriter pw)509 public void dump(IndentingPrintWriter pw) { 510 pw.println("DownloadInfo:"); 511 pw.increaseIndent(); 512 513 pw.printPair("mId", mId); 514 pw.printPair("mLastMod", mLastMod); 515 pw.printPair("mPackage", mPackage); 516 pw.printPair("mUid", mUid); 517 pw.println(); 518 519 pw.printPair("mUri", mUri); 520 pw.println(); 521 522 pw.printPair("mMimeType", mMimeType); 523 pw.printPair("mCookies", (mCookies != null) ? "yes" : "no"); 524 pw.printPair("mReferer", (mReferer != null) ? "yes" : "no"); 525 pw.printPair("mUserAgent", mUserAgent); 526 pw.println(); 527 528 pw.printPair("mFileName", mFileName); 529 pw.printPair("mDestination", mDestination); 530 pw.println(); 531 532 pw.printPair("mStatus", Downloads.Impl.statusToString(mStatus)); 533 pw.printPair("mCurrentBytes", mCurrentBytes); 534 pw.printPair("mTotalBytes", mTotalBytes); 535 pw.println(); 536 537 pw.printPair("mNumFailed", mNumFailed); 538 pw.printPair("mRetryAfter", mRetryAfter); 539 pw.printPair("mETag", mETag); 540 pw.printPair("mIsPublicApi", mIsPublicApi); 541 pw.println(); 542 543 pw.printPair("mAllowedNetworkTypes", mAllowedNetworkTypes); 544 pw.printPair("mAllowRoaming", mAllowRoaming); 545 pw.printPair("mAllowMetered", mAllowMetered); 546 pw.println(); 547 548 pw.decreaseIndent(); 549 } 550 551 /** 552 * Return time when this download will be ready for its next action, in 553 * milliseconds after given time. 554 * 555 * @return If {@code 0}, download is ready to proceed immediately. If 556 * {@link Long#MAX_VALUE}, then download has no future actions. 557 */ nextActionMillis(long now)558 public long nextActionMillis(long now) { 559 if (Downloads.Impl.isStatusCompleted(mStatus)) { 560 return Long.MAX_VALUE; 561 } 562 if (mStatus != Downloads.Impl.STATUS_WAITING_TO_RETRY) { 563 return 0; 564 } 565 long when = restartTime(now); 566 if (when <= now) { 567 return 0; 568 } 569 return when - now; 570 } 571 572 /** 573 * Returns whether a file should be scanned 574 */ shouldScanFile()575 public boolean shouldScanFile() { 576 return (mMediaScanned == 0) 577 && (mDestination == Downloads.Impl.DESTINATION_EXTERNAL || 578 mDestination == Downloads.Impl.DESTINATION_FILE_URI || 579 mDestination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) 580 && Downloads.Impl.isStatusSuccess(mStatus); 581 } 582 notifyPauseDueToSize(boolean isWifiRequired)583 void notifyPauseDueToSize(boolean isWifiRequired) { 584 Intent intent = new Intent(Intent.ACTION_VIEW); 585 intent.setData(getAllDownloadsUri()); 586 intent.setClassName(SizeLimitActivity.class.getPackage().getName(), 587 SizeLimitActivity.class.getName()); 588 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 589 intent.putExtra(EXTRA_IS_WIFI_REQUIRED, isWifiRequired); 590 mContext.startActivity(intent); 591 } 592 593 /** 594 * Query and return status of requested download. 595 */ queryDownloadStatus(ContentResolver resolver, long id)596 public static int queryDownloadStatus(ContentResolver resolver, long id) { 597 final Cursor cursor = resolver.query( 598 ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id), 599 new String[] { Downloads.Impl.COLUMN_STATUS }, null, null, null); 600 try { 601 if (cursor.moveToFirst()) { 602 return cursor.getInt(0); 603 } else { 604 // TODO: increase strictness of value returned for unknown 605 // downloads; this is safe default for now. 606 return Downloads.Impl.STATUS_PENDING; 607 } 608 } finally { 609 cursor.close(); 610 } 611 } 612 } 613