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 17package com.android.providers.downloads; 18 19import static android.provider.Downloads.Impl.VISIBILITY_VISIBLE; 20import static android.provider.Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED; 21 22import static com.android.providers.downloads.Constants.TAG; 23 24import android.app.DownloadManager; 25import android.app.job.JobInfo; 26import android.content.ContentResolver; 27import android.content.ContentUris; 28import android.content.Context; 29import android.content.Intent; 30import android.database.Cursor; 31import android.net.Uri; 32import android.os.Environment; 33import android.provider.Downloads; 34import android.text.TextUtils; 35import android.text.format.DateUtils; 36import android.util.Log; 37import android.util.Pair; 38 39import com.android.internal.util.IndentingPrintWriter; 40 41import java.io.CharArrayWriter; 42import java.io.File; 43import java.util.ArrayList; 44import java.util.Collection; 45import java.util.Collections; 46import java.util.List; 47 48/** 49 * Details about a specific download. Fields should only be mutated by updating 50 * from database query. 51 */ 52public class DownloadInfo { 53 // TODO: move towards these in-memory objects being sources of truth, and 54 // periodically pushing to provider. 55 56 public static class Reader { 57 private ContentResolver mResolver; 58 private Cursor mCursor; 59 60 public Reader(ContentResolver resolver, Cursor cursor) { 61 mResolver = resolver; 62 mCursor = cursor; 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 = Intent.normalizeMimeType(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(Downloads.Impl.COLUMN_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(Downloads.Impl.COLUMN_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.mFlags = getInt(Downloads.Impl.COLUMN_FLAGS); 97 info.mTitle = getString(Downloads.Impl.COLUMN_TITLE); 98 info.mDescription = getString(Downloads.Impl.COLUMN_DESCRIPTION); 99 info.mBypassRecommendedSizeLimit = 100 getInt(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT); 101 102 synchronized (this) { 103 info.mControl = getInt(Downloads.Impl.COLUMN_CONTROL); 104 } 105 } 106 107 public void readRequestHeaders(DownloadInfo info) { 108 info.mRequestHeaders.clear(); 109 Uri headerUri = Uri.withAppendedPath( 110 info.getAllDownloadsUri(), Downloads.Impl.RequestHeaders.URI_SEGMENT); 111 Cursor cursor = mResolver.query(headerUri, null, null, null, null); 112 try { 113 int headerIndex = 114 cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_HEADER); 115 int valueIndex = 116 cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_VALUE); 117 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 118 addHeader(info, cursor.getString(headerIndex), cursor.getString(valueIndex)); 119 } 120 } finally { 121 cursor.close(); 122 } 123 124 if (info.mCookies != null) { 125 addHeader(info, "Cookie", info.mCookies); 126 } 127 if (info.mReferer != null) { 128 addHeader(info, "Referer", info.mReferer); 129 } 130 } 131 132 private void addHeader(DownloadInfo info, String header, String value) { 133 info.mRequestHeaders.add(Pair.create(header, value)); 134 } 135 136 private String getString(String column) { 137 int index = mCursor.getColumnIndexOrThrow(column); 138 String s = mCursor.getString(index); 139 return (TextUtils.isEmpty(s)) ? null : s; 140 } 141 142 private Integer getInt(String column) { 143 return mCursor.getInt(mCursor.getColumnIndexOrThrow(column)); 144 } 145 146 private Long getLong(String column) { 147 return mCursor.getLong(mCursor.getColumnIndexOrThrow(column)); 148 } 149 } 150 151 public long mId; 152 public String mUri; 153 @Deprecated 154 public boolean mNoIntegrity; 155 public String mHint; 156 public String mFileName; 157 public String mMimeType; 158 public int mDestination; 159 public int mVisibility; 160 public int mControl; 161 public int mStatus; 162 public int mNumFailed; 163 public int mRetryAfter; 164 public long mLastMod; 165 public String mPackage; 166 public String mClass; 167 public String mExtras; 168 public String mCookies; 169 public String mUserAgent; 170 public String mReferer; 171 public long mTotalBytes; 172 public long mCurrentBytes; 173 public String mETag; 174 public int mUid; 175 public int mMediaScanned; 176 public boolean mDeleted; 177 public String mMediaProviderUri; 178 public boolean mIsPublicApi; 179 public int mAllowedNetworkTypes; 180 public boolean mAllowRoaming; 181 public boolean mAllowMetered; 182 public int mFlags; 183 public String mTitle; 184 public String mDescription; 185 public int mBypassRecommendedSizeLimit; 186 187 private List<Pair<String, String>> mRequestHeaders = new ArrayList<Pair<String, String>>(); 188 189 private final Context mContext; 190 private final SystemFacade mSystemFacade; 191 192 public DownloadInfo(Context context) { 193 mContext = context; 194 mSystemFacade = Helpers.getSystemFacade(context); 195 } 196 197 public static DownloadInfo queryDownloadInfo(Context context, long downloadId) { 198 final ContentResolver resolver = context.getContentResolver(); 199 try (Cursor cursor = resolver.query( 200 ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, downloadId), 201 null, null, null, null)) { 202 final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor); 203 final DownloadInfo info = new DownloadInfo(context); 204 if (cursor.moveToFirst()) { 205 reader.updateFromDatabase(info); 206 reader.readRequestHeaders(info); 207 return info; 208 } 209 } 210 return null; 211 } 212 213 public Collection<Pair<String, String>> getHeaders() { 214 return Collections.unmodifiableList(mRequestHeaders); 215 } 216 217 public String getUserAgent() { 218 if (mUserAgent != null) { 219 return mUserAgent; 220 } else { 221 return Constants.DEFAULT_USER_AGENT; 222 } 223 } 224 225 public void sendIntentIfRequested() { 226 if (mPackage == null) { 227 return; 228 } 229 230 Intent intent; 231 if (mIsPublicApi) { 232 intent = new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE); 233 intent.setPackage(mPackage); 234 intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, mId); 235 } else { // legacy behavior 236 if (mClass == null) { 237 return; 238 } 239 intent = new Intent(Downloads.Impl.ACTION_DOWNLOAD_COMPLETED); 240 intent.setClassName(mPackage, mClass); 241 if (mExtras != null) { 242 intent.putExtra(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, mExtras); 243 } 244 // We only send the content: URI, for security reasons. Otherwise, malicious 245 // applications would have an easier time spoofing download results by 246 // sending spoofed intents. 247 intent.setData(getMyDownloadsUri()); 248 } 249 mSystemFacade.sendBroadcast(intent); 250 } 251 252 /** 253 * Return if this download is visible to the user while running. 254 */ 255 public boolean isVisible() { 256 switch (mVisibility) { 257 case VISIBILITY_VISIBLE: 258 case VISIBILITY_VISIBLE_NOTIFY_COMPLETED: 259 return true; 260 default: 261 return false; 262 } 263 } 264 265 /** 266 * Add random fuzz to the given delay so it's anywhere between 1-1.5x the 267 * requested delay. 268 */ 269 private long fuzzDelay(long delay) { 270 return delay + Helpers.sRandom.nextInt((int) (delay / 2)); 271 } 272 273 /** 274 * Return minimum latency in milliseconds required before this download is 275 * allowed to start again. 276 * 277 * @see android.app.job.JobInfo.Builder#setMinimumLatency(long) 278 */ 279 public long getMinimumLatency() { 280 if (mStatus == Downloads.Impl.STATUS_WAITING_TO_RETRY) { 281 final long now = mSystemFacade.currentTimeMillis(); 282 final long startAfter; 283 if (mNumFailed == 0) { 284 startAfter = now; 285 } else if (mRetryAfter > 0) { 286 startAfter = mLastMod + fuzzDelay(mRetryAfter); 287 } else { 288 final long delay = (Constants.RETRY_FIRST_DELAY * DateUtils.SECOND_IN_MILLIS 289 * (1 << (mNumFailed - 1))); 290 startAfter = mLastMod + fuzzDelay(delay); 291 } 292 return Math.max(0, startAfter - now); 293 } else { 294 return 0; 295 } 296 } 297 298 /** 299 * Return the network type constraint required by this download. 300 * 301 * @see android.app.job.JobInfo.Builder#setRequiredNetworkType(int) 302 */ 303 public int getRequiredNetworkType(long totalBytes) { 304 if (!mAllowMetered) { 305 return JobInfo.NETWORK_TYPE_UNMETERED; 306 } 307 if (mAllowedNetworkTypes == DownloadManager.Request.NETWORK_WIFI) { 308 return JobInfo.NETWORK_TYPE_UNMETERED; 309 } 310 if (totalBytes > mSystemFacade.getMaxBytesOverMobile()) { 311 return JobInfo.NETWORK_TYPE_UNMETERED; 312 } 313 if (totalBytes > mSystemFacade.getRecommendedMaxBytesOverMobile() 314 && mBypassRecommendedSizeLimit == 0) { 315 return JobInfo.NETWORK_TYPE_UNMETERED; 316 } 317 if (!mAllowRoaming) { 318 return JobInfo.NETWORK_TYPE_NOT_ROAMING; 319 } 320 return JobInfo.NETWORK_TYPE_ANY; 321 } 322 323 /** 324 * Returns whether this download is ready to be scheduled. 325 */ 326 public boolean isReadyToSchedule() { 327 if (mControl == Downloads.Impl.CONTROL_PAUSED) { 328 // the download is paused, so it's not going to start 329 return false; 330 } 331 switch (mStatus) { 332 case 0: 333 case Downloads.Impl.STATUS_PENDING: 334 case Downloads.Impl.STATUS_RUNNING: 335 case Downloads.Impl.STATUS_WAITING_FOR_NETWORK: 336 case Downloads.Impl.STATUS_WAITING_TO_RETRY: 337 case Downloads.Impl.STATUS_QUEUED_FOR_WIFI: 338 return true; 339 340 case Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR: 341 // is the media mounted? 342 final Uri uri = Uri.parse(mUri); 343 if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { 344 final File file = new File(uri.getPath()); 345 return Environment.MEDIA_MOUNTED 346 .equals(Environment.getExternalStorageState(file)); 347 } else { 348 Log.w(TAG, "Expected file URI on external storage: " + mUri); 349 return false; 350 } 351 352 default: 353 return false; 354 } 355 } 356 357 /** 358 * Returns whether this download has a visible notification after 359 * completion. 360 */ 361 public boolean hasCompletionNotification() { 362 if (!Downloads.Impl.isStatusCompleted(mStatus)) { 363 return false; 364 } 365 if (mVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) { 366 return true; 367 } 368 return false; 369 } 370 371 public boolean isMeteredAllowed(long totalBytes) { 372 return getRequiredNetworkType(totalBytes) != JobInfo.NETWORK_TYPE_UNMETERED; 373 } 374 375 public boolean isRoamingAllowed() { 376 if (mIsPublicApi) { 377 return mAllowRoaming; 378 } else { // legacy behavior 379 return mDestination != Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING; 380 } 381 } 382 383 public Uri getMyDownloadsUri() { 384 return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, mId); 385 } 386 387 public Uri getAllDownloadsUri() { 388 return ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, mId); 389 } 390 391 @Override 392 public String toString() { 393 final CharArrayWriter writer = new CharArrayWriter(); 394 dump(new IndentingPrintWriter(writer, " ")); 395 return writer.toString(); 396 } 397 398 public void dump(IndentingPrintWriter pw) { 399 pw.println("DownloadInfo:"); 400 pw.increaseIndent(); 401 402 pw.printPair("mId", mId); 403 pw.printPair("mLastMod", mLastMod); 404 pw.printPair("mPackage", mPackage); 405 pw.printPair("mUid", mUid); 406 pw.println(); 407 408 pw.printPair("mUri", mUri); 409 pw.println(); 410 411 pw.printPair("mMimeType", mMimeType); 412 pw.printPair("mCookies", (mCookies != null) ? "yes" : "no"); 413 pw.printPair("mReferer", (mReferer != null) ? "yes" : "no"); 414 pw.printPair("mUserAgent", mUserAgent); 415 pw.println(); 416 417 pw.printPair("mFileName", mFileName); 418 pw.printPair("mDestination", mDestination); 419 pw.println(); 420 421 pw.printPair("mStatus", Downloads.Impl.statusToString(mStatus)); 422 pw.printPair("mCurrentBytes", mCurrentBytes); 423 pw.printPair("mTotalBytes", mTotalBytes); 424 pw.println(); 425 426 pw.printPair("mNumFailed", mNumFailed); 427 pw.printPair("mRetryAfter", mRetryAfter); 428 pw.printPair("mETag", mETag); 429 pw.printPair("mIsPublicApi", mIsPublicApi); 430 pw.println(); 431 432 pw.printPair("mAllowedNetworkTypes", mAllowedNetworkTypes); 433 pw.printPair("mAllowRoaming", mAllowRoaming); 434 pw.printPair("mAllowMetered", mAllowMetered); 435 pw.printPair("mFlags", mFlags); 436 pw.println(); 437 438 pw.decreaseIndent(); 439 } 440 441 /** 442 * Returns whether a file should be scanned 443 */ 444 public boolean shouldScanFile(int status) { 445 return (mMediaScanned == 0) 446 && (mDestination == Downloads.Impl.DESTINATION_EXTERNAL || 447 mDestination == Downloads.Impl.DESTINATION_FILE_URI || 448 mDestination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) 449 && Downloads.Impl.isStatusSuccess(status); 450 } 451 452 /** 453 * Query and return status of requested download. 454 */ 455 public int queryDownloadStatus() { 456 return queryDownloadInt(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING); 457 } 458 459 public int queryDownloadControl() { 460 return queryDownloadInt(Downloads.Impl.COLUMN_CONTROL, Downloads.Impl.CONTROL_RUN); 461 } 462 463 public int queryDownloadInt(String columnName, int defaultValue) { 464 try (Cursor cursor = mContext.getContentResolver().query(getAllDownloadsUri(), 465 new String[] { columnName }, null, null, null)) { 466 if (cursor.moveToFirst()) { 467 return cursor.getInt(0); 468 } else { 469 return defaultValue; 470 } 471 } 472 } 473} 474