package com.novoda.downloadmanager.lib;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Pair;
import com.novoda.downloadmanager.lib.logger.LLog;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Stores information about an individual download.
*/
class FileDownloadInfo {
private static final String TAR_MIME_TYPE = "application/x-tar";
// TODO: move towards these in-memory objects being sources of truth, and periodically pushing to provider.
/**
* Constants used to indicate network state for a specific download, after
* applying any requested constraints.
*/
public enum NetworkState {
/**
* The network is usable for the given download.
*/
OK,
/**
* There is no network connectivity.
*/
NO_CONNECTION,
/**
* The download exceeds the maximum size for this network.
*/
UNUSABLE_DUE_TO_SIZE,
/**
* The download exceeds the recommended maximum size for this network,
* the user must confirm for this download to proceed without WiFi.
*/
RECOMMENDED_UNUSABLE_DUE_TO_SIZE,
/**
* The current connection is roaming, and the download can't proceed
* over a roaming connection.
*/
CANNOT_USE_ROAMING,
/**
* The app requesting the download specific that it can't use the
* current network connection.
*/
TYPE_DISALLOWED_BY_REQUESTOR,
/**
* Current network is blocked for requesting application.
*/
BLOCKED
}
private long id;
private String uri;
private boolean scannable;
private boolean noIntegrity;
private String hint;
private String fileName;
private String mimeType;
private int destination;
private int control;
private int status;
private int numFailed;
private int retryAfter;
private long lastMod;
private String notificationClassName;
private String extras;
private String cookies;
private String userAgent;
private String referer;
private long totalBytes;
private long currentBytes;
private String eTag;
private int uid;
private int mediaScanned;
private boolean deleted;
private String mediaProviderUri;
private int allowedNetworkTypes;
private boolean allowRoaming;
private boolean allowMetered;
private int bypassRecommendedSizeLimit;
private long batchId;
private boolean alwaysResume;
private boolean allowTarUpdates;
private final List<Pair<String, String>> requestHeaders = new ArrayList<>();
private final SystemFacade systemFacade;
private final RandomNumberGenerator randomNumberGenerator;
private final DownloadsUriProvider downloadsUriProvider;
FileDownloadInfo(SystemFacade systemFacade, RandomNumberGenerator randomNumberGenerator, DownloadsUriProvider downloadsUriProvider) {
this.systemFacade = systemFacade;
this.randomNumberGenerator = randomNumberGenerator;
this.downloadsUriProvider = downloadsUriProvider;
}
public long getId() {
return id;
}
public String getUri() {
return uri;
}
public boolean isNoIntegrity() {
return noIntegrity;
}
public String getHint() {
return hint;
}
public String getFileName() {
return fileName;
}
public String getMimeType() {
return mimeType;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
public int getDestination() {
return destination;
}
public int getControl() {
return control;
}
public int getStatus() {
return status;
}
public int getNumFailed() {
return numFailed;
}
public String getNotificationClassName() {
return notificationClassName;
}
public String getUserAgent() {
return userAgent;
}
public long getTotalBytes() {
return totalBytes;
}
public long getCurrentBytes() {
return currentBytes;
}
public String getETag() {
return eTag;
}
public void setETag(String eTag) {
this.eTag = eTag;
}
public boolean isDeleted() {
return deleted;
}
public String getMediaProviderUri() {
return mediaProviderUri;
}
public long getBatchId() {
return batchId;
}
public boolean allowMetered() {
return allowMetered;
}
public boolean allowRoaming() {
return allowRoaming;
}
public boolean isRecommendedSizeLimitBypassed() {
return bypassRecommendedSizeLimit == 0;
}
public String getExtras() {
return extras;
}
public Collection<Pair<String, String>> getHeaders() {
return Collections.unmodifiableList(requestHeaders);
}
/**
* Returns the time when a download should be restarted.
*/
public long restartTime(long now) {
if (numFailed == 0) {
return now;
}
if (retryAfter > 0) {
return lastMod + retryAfter;
}
return lastMod + Constants.RETRY_FIRST_DELAY * (1000 + randomNumberGenerator.generate()) * (1 << (numFailed - 1));
}
/**
* Translate a ConnectivityManager.TYPE_* constant to the corresponding
* DownloadManager.Request.NETWORK_* bit flag.
*/
private int translateNetworkTypeToApiFlag(int networkType) {
switch (networkType) {
case ConnectivityManager.TYPE_MOBILE:
return Request.NETWORK_MOBILE;
case ConnectivityManager.TYPE_WIFI:
return Request.NETWORK_WIFI;
case ConnectivityManager.TYPE_BLUETOOTH:
return Request.NETWORK_BLUETOOTH;
default:
return 0;
}
}
/**
* Check if the download's size prohibits it from running over the current network.
*
* @return one of the NETWORK_* constants
*/
private NetworkState checkSizeAllowedForNetwork(int networkType) {
if (totalBytes <= 0) {
return NetworkState.OK; // we don't know the size yet
}
if (networkType == ConnectivityManager.TYPE_WIFI) {
return NetworkState.OK; // anything goes over wifi
}
Long maxBytesOverMobile = systemFacade.getMaxBytesOverMobile();
if (maxBytesOverMobile != null && totalBytes > maxBytesOverMobile) {
return NetworkState.UNUSABLE_DUE_TO_SIZE;
}
if (bypassRecommendedSizeLimit == 0) {
Long recommendedMaxBytesOverMobile = systemFacade.getRecommendedMaxBytesOverMobile();
if (recommendedMaxBytesOverMobile != null
&& totalBytes > recommendedMaxBytesOverMobile) {
return NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE;
}
}
return NetworkState.OK;
}
public boolean isSubmittedOrRunning() {
return DownloadStatus.isSubmitted(status) || DownloadStatus.isRunning(status);
}
/**
* If download is ready to be scanned, enqueue it into the given
* {@link DownloadScanner}.
*
* @return If actively scanning.
*/
public boolean startScanIfReady(DownloadScanner scanner) {
synchronized (this) {
final boolean isReady = shouldScanFile();
if (isReady) {
scanner.requestScan(this);
}
return isReady;
}
}
public boolean isOnCache() {
return (destination == DownloadsDestination.DESTINATION_CACHE_PARTITION
|| destination == DownloadsDestination.DESTINATION_SYSTEMCACHE_PARTITION
|| destination == DownloadsDestination.DESTINATION_CACHE_PARTITION_NOROAMING
|| destination == DownloadsDestination.DESTINATION_CACHE_PARTITION_PURGEABLE);
}
public Uri getMyDownloadsUri() {
return ContentUris.withAppendedId(downloadsUriProvider.getContentUri(), id);
}
public Uri getAllDownloadsUri() {
return ContentUris.withAppendedId(downloadsUriProvider.getAllDownloadsUri(), id);
}
/**
* Returns whether a file should be scanned
*/
private boolean shouldScanFile() {
return (mediaScanned == 0)
&& (getDestination() == DownloadsDestination.DESTINATION_EXTERNAL ||
getDestination() == DownloadsDestination.DESTINATION_FILE_URI ||
getDestination() == DownloadsDestination.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
&& DownloadStatus.isSuccess(getStatus())
&& scannable;
}
public boolean hasTotalBytes() {
return totalBytes != Constants.UNKNOWN_BYTE_SIZE;
}
public boolean hasUnknownTotalBytes() {
return !hasTotalBytes();
}
private void addHeader(String header, String value) {
requestHeaders.add(Pair.create(header, value));
}
private void clearHeaders() {
requestHeaders.clear();
}
public boolean isResumable() {
return alwaysResume || (eTag != null && isNoIntegrity());
}
public boolean shouldAllowTarUpdate(String mimeType) {
if (!allowTarUpdates) {
return false;
}
if (TAR_MIME_TYPE.equals(mimeType)) {
return true;
}
LLog.e("Flag allowTarUpdates set but file not matching Tar mimeType, functionality will be disabled.");
return false;
}
public static class Reader {
private final ContentResolver resolver;
private final Cursor cursor;
public Reader(ContentResolver resolver, Cursor cursor) {
this.resolver = resolver;
this.cursor = cursor;
}
public FileDownloadInfo newDownloadInfo(SystemFacade systemFacade, DownloadsUriProvider downloadsUriProvider) {
RandomNumberGenerator randomNumberGenerator = new RandomNumberGenerator();
FileDownloadInfo info = new FileDownloadInfo(systemFacade, randomNumberGenerator, downloadsUriProvider);
updateFromDatabase(info);
readRequestHeaders(info);
return info;
}
public void updateFromDatabase(FileDownloadInfo info) {
info.id = getLong(DownloadContract.Downloads._ID);
info.uri = getString(DownloadContract.Downloads.COLUMN_URI);
info.scannable = getInt(DownloadContract.Downloads.COLUMN_MEDIA_SCANNED) == 1;
info.noIntegrity = getInt(DownloadContract.Downloads.COLUMN_NO_INTEGRITY) == 1;
info.hint = getString(DownloadContract.Downloads.COLUMN_FILE_NAME_HINT);
info.fileName = getString(DownloadContract.Downloads.COLUMN_DATA);
info.mimeType = getString(DownloadContract.Downloads.COLUMN_MIME_TYPE);
info.destination = getInt(DownloadContract.Downloads.COLUMN_DESTINATION);
info.status = getInt(DownloadContract.Downloads.COLUMN_STATUS);
info.numFailed = getInt(DownloadContract.Downloads.COLUMN_FAILED_CONNECTIONS);
int retryRedirect = getInt(Constants.RETRY_AFTER_X_REDIRECT_COUNT);
info.retryAfter = retryRedirect & 0xfffffff;
info.lastMod = getLong(DownloadContract.Downloads.COLUMN_LAST_MODIFICATION);
info.notificationClassName = getString(DownloadContract.Downloads.COLUMN_NOTIFICATION_CLASS);
info.extras = getString(DownloadContract.Downloads.COLUMN_NOTIFICATION_EXTRAS);
info.cookies = getString(DownloadContract.Downloads.COLUMN_COOKIE_DATA);
info.userAgent = getString(DownloadContract.Downloads.COLUMN_USER_AGENT);
info.referer = getString(DownloadContract.Downloads.COLUMN_REFERER);
info.totalBytes = getLong(DownloadContract.Downloads.COLUMN_TOTAL_BYTES);
info.currentBytes = getLong(DownloadContract.Downloads.COLUMN_CURRENT_BYTES);
info.eTag = getString(Constants.ETAG);
info.uid = getInt(Constants.UID);
info.mediaScanned = getInt(Constants.MEDIA_SCANNED);
info.deleted = getInt(DownloadContract.Downloads.COLUMN_DELETED) == 1;
info.mediaProviderUri = getString(DownloadContract.Downloads.COLUMN_MEDIAPROVIDER_URI);
info.allowedNetworkTypes = getInt(DownloadContract.Downloads.COLUMN_ALLOWED_NETWORK_TYPES);
info.allowRoaming = getInt(DownloadContract.Downloads.COLUMN_ALLOW_ROAMING) != 0;
info.allowMetered = getInt(DownloadContract.Downloads.COLUMN_ALLOW_METERED) != 0;
info.bypassRecommendedSizeLimit = getInt(DownloadContract.Downloads.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
info.batchId = getLong(DownloadContract.Downloads.COLUMN_BATCH_ID);
info.alwaysResume = getInt(DownloadContract.Downloads.COLUMN_ALWAYS_RESUME) != 0;
info.allowTarUpdates = getInt(DownloadContract.Downloads.COLUMN_ALLOW_TAR_UPDATES) != 0;
synchronized (this) {
info.control = getInt(DownloadContract.Downloads.COLUMN_CONTROL);
}
}
private void readRequestHeaders(FileDownloadInfo info) {
info.clearHeaders();
Uri headerUri = Uri.withAppendedPath(info.getAllDownloadsUri(), DownloadContract.RequestHeaders.URI_SEGMENT);
Cursor cursor = resolver.query(headerUri, null, null, null, null);
try {
int headerIndex =
cursor.getColumnIndexOrThrow(DownloadContract.RequestHeaders.COLUMN_HEADER);
int valueIndex =
cursor.getColumnIndexOrThrow(DownloadContract.RequestHeaders.COLUMN_VALUE);
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
info.addHeader(cursor.getString(headerIndex), cursor.getString(valueIndex));
}
} finally {
cursor.close();
}
if (info.cookies != null) {
info.addHeader("Cookie", info.cookies);
}
if (info.referer != null) {
info.addHeader("Referer", info.referer);
}
}
private String getString(String column) {
int index = cursor.getColumnIndexOrThrow(column);
String s = cursor.getString(index);
return (TextUtils.isEmpty(s)) ? null : s;
}
private Integer getInt(String column) {
return cursor.getInt(cursor.getColumnIndexOrThrow(column));
}
private Long getLong(String column) {
return cursor.getLong(cursor.getColumnIndexOrThrow(column));
}
}
static final class ControlStatus {
private int control;
private int status;
public ControlStatus(int control, int status) {
this.control = control;
this.status = status;
}
public boolean isPaused() {
return control == DownloadsControl.CONTROL_PAUSED;
}
public boolean isCanceled() {
return status == DownloadStatus.CANCELED;
}
static final class Reader {
private static final String[] PROJECTION = new String[]{
DownloadContract.Downloads.COLUMN_CONTROL, DownloadContract.Downloads.COLUMN_STATUS
};
private final ContentResolver contentResolver;
private final Uri downloadUri;
public Reader(ContentResolver contentResolver, Uri downloadUri) {
this.contentResolver = contentResolver;
this.downloadUri = downloadUri;
}
public ControlStatus newControlStatus() {
Cursor downloadsCursor = contentResolver.query(downloadUri, PROJECTION, null, null, null);
try {
downloadsCursor.moveToFirst();
int control = downloadsCursor.getInt(0);
int status = downloadsCursor.getInt(1);
return new FileDownloadInfo.ControlStatus(control, status);
} finally {
downloadsCursor.close();
}
}
}
}
}