├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── DownloadManager ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── limpoxe │ │ └── downloads │ │ ├── Constants.java │ │ ├── DownloadInfo.java │ │ ├── DownloadManager.java │ │ ├── DownloadNotifier.java │ │ ├── DownloadProvider.java │ │ ├── DownloadReceiver.java │ │ ├── DownloadService.java │ │ ├── DownloadThread.java │ │ ├── Downloads.java │ │ ├── Helpers.java │ │ ├── PermissionChecker.java │ │ ├── StopRequestException.java │ │ ├── StorageUtils.java │ │ └── utils │ │ ├── ConnectManager.java │ │ ├── GuardedBy.java │ │ └── IoUtils.java │ └── res │ └── values │ └── strings.xml ├── LICENSE ├── LICENSE.txt ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── limpoxe │ │ └── example │ │ └── MainActivity.java │ └── res │ ├── layout │ └── activity_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ ├── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── providers_paths.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # Intellij 36 | *.iml 37 | .idea/workspace.xml 38 | 39 | # Keystore files 40 | *.jks 41 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 26 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 53 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /DownloadManager/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /DownloadManager/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.2" 6 | 7 | defaultConfig { 8 | minSdkVersion 9 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | } 24 | -------------------------------------------------------------------------------- /DownloadManager/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/cailiming/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /DownloadManager/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /DownloadManager/src/main/java/com/limpoxe/downloads/Constants.java: -------------------------------------------------------------------------------- 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.limpoxe.downloads; 18 | 19 | import android.os.Build; 20 | import android.text.TextUtils; 21 | import android.util.Log; 22 | 23 | /** 24 | * Contains the internal constants that are used in the download manager. 25 | * As a general rule, modifying these constants should be done with care. 26 | */ 27 | public class Constants { 28 | 29 | /** Tag used for debugging/logging */ 30 | public static final String TAG = "DownloadManager"; 31 | 32 | /** The column that used to be used for the HTTP method of the request */ 33 | public static final String RETRY_AFTER_X_REDIRECT_COUNT = "method"; 34 | 35 | /** The column that used to be used for the magic OTA update filename */ 36 | public static final String OTA_UPDATE = "otaupdate"; 37 | 38 | /** The column that used to be used to reject system filetypes */ 39 | public static final String NO_SYSTEM_FILES = "no_system"; 40 | 41 | /** The column that is used for the downloads's ETag */ 42 | public static final String ETAG = "etag"; 43 | 44 | /** The column that is used for the initiating app's UID */ 45 | public static final String UID = "uid"; 46 | 47 | /** The intent that gets sent when the service must wake up for a retry */ 48 | public static final String ACTION_RETRY = "com.limpoxe.downloads.action.DOWNLOAD_WAKEUP"; 49 | 50 | /** the intent that gets sent when clicking a successful download */ 51 | public static final String ACTION_OPEN = "com.limpoxe.downloads.action.DOWNLOAD_OPEN"; 52 | 53 | /** the intent that gets sent when clicking an incomplete/failed download */ 54 | public static final String ACTION_LIST = "com.limpoxe.downloads.action.DOWNLOAD_LIST"; 55 | 56 | /** the intent that gets sent when deleting the notification of a completed download */ 57 | public static final String ACTION_HIDE = "com.limpoxe.downloads.action.DOWNLOAD_HIDE"; 58 | 59 | /** The default base name for downloaded files if we can't get one at the HTTP level */ 60 | public static final String DEFAULT_DL_FILENAME = "downloadfile"; 61 | 62 | /** The default extension for html files if we can't get one at the HTTP level */ 63 | public static final String DEFAULT_DL_HTML_EXTENSION = ".html"; 64 | 65 | /** The default extension for text files if we can't get one at the HTTP level */ 66 | public static final String DEFAULT_DL_TEXT_EXTENSION = ".txt"; 67 | 68 | /** The default extension for binary files if we can't get one at the HTTP level */ 69 | public static final String DEFAULT_DL_BINARY_EXTENSION = ".bin"; 70 | 71 | /** 72 | * When a number has to be appended to the filename, this string is used to separate the 73 | * base filename from the sequence number 74 | */ 75 | public static final String FILENAME_SEQUENCE_SEPARATOR = "-"; 76 | 77 | /** A magic filename that is allowed to exist within the system cache */ 78 | public static final String RECOVERY_DIRECTORY = "recovery"; 79 | 80 | /** The default user agent used for downloads */ 81 | public static final String DEFAULT_USER_AGENT; 82 | 83 | static { 84 | final StringBuilder builder = new StringBuilder(); 85 | 86 | final boolean validRelease = !TextUtils.isEmpty(Build.VERSION.RELEASE); 87 | final boolean validId = !TextUtils.isEmpty(Build.ID); 88 | final boolean includeModel = "REL".equals(Build.VERSION.CODENAME) 89 | && !TextUtils.isEmpty(Build.MODEL); 90 | 91 | builder.append("AndroidDownloadManager"); 92 | if (validRelease) { 93 | builder.append("/").append(Build.VERSION.RELEASE); 94 | } 95 | builder.append(" (Linux; U; Android"); 96 | if (validRelease) { 97 | builder.append(" ").append(Build.VERSION.RELEASE); 98 | } 99 | if (includeModel || validId) { 100 | builder.append(";"); 101 | if (includeModel) { 102 | builder.append(" ").append(Build.MODEL); 103 | } 104 | if (validId) { 105 | builder.append(" Build/").append(Build.ID); 106 | } 107 | } 108 | builder.append(")"); 109 | 110 | DEFAULT_USER_AGENT = builder.toString(); 111 | } 112 | 113 | /** The MIME type of APKs */ 114 | public static final String MIMETYPE_APK = "application/vnd.android.package"; 115 | 116 | /** The buffer size used to stream the data */ 117 | public static final int BUFFER_SIZE = 8192; 118 | 119 | /** The minimum amount of progress that has to be done before the progress bar gets updated */ 120 | public static final int MIN_PROGRESS_STEP = 65536; 121 | 122 | /** The minimum amount of time that has to elapse before the progress bar gets updated, in ms */ 123 | public static final long MIN_PROGRESS_TIME = 2000; 124 | 125 | /** 126 | * The number of times that the download manager will retry its network 127 | * operations when no progress is happening before it gives up. 128 | */ 129 | public static final int MAX_RETRIES = 5; 130 | 131 | /** 132 | * The minimum amount of time that the download manager accepts for 133 | * a Retry-After response header with a parameter in delta-seconds. 134 | */ 135 | public static final int MIN_RETRY_AFTER = 30; // 30s 136 | 137 | /** 138 | * The maximum amount of time that the download manager accepts for 139 | * a Retry-After response header with a parameter in delta-seconds. 140 | */ 141 | public static final int MAX_RETRY_AFTER = 24 * 60 * 60; // 24h 142 | 143 | /** 144 | * The maximum number of redirects. 145 | */ 146 | public static final int MAX_REDIRECTS = 5; // can't be more than 7. 147 | 148 | /** 149 | * The time between a failure and the first retry after an IOException. 150 | * Each subsequent retry grows exponentially, doubling each time. 151 | * The time is in seconds. 152 | */ 153 | public static final int RETRY_FIRST_DELAY = 30; 154 | 155 | /** Enable separate connectivity logging */ 156 | static final boolean LOGX = true; 157 | 158 | /** Enable verbose logging - use with "setprop log.tag.DownloadManager VERBOSE" */ 159 | private static final boolean LOCAL_LOGV = true; 160 | public static final boolean LOGV = LOCAL_LOGV && Log.isLoggable(TAG, Log.VERBOSE); 161 | 162 | /** Enable super-verbose logging */ 163 | private static final boolean LOCAL_LOGVV = true; 164 | public static final boolean LOGVV = LOCAL_LOGVV && LOGV; 165 | 166 | /** 167 | * Name of directory on cache partition containing in-progress downloads. 168 | */ 169 | public static final String DIRECTORY_CACHE_RUNNING = "partial_downloads"; 170 | } 171 | -------------------------------------------------------------------------------- /DownloadManager/src/main/java/com/limpoxe/downloads/DownloadInfo.java: -------------------------------------------------------------------------------- 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.limpoxe.downloads; 18 | 19 | import android.content.ContentResolver; 20 | import android.content.ContentUris; 21 | import android.content.ContentValues; 22 | import android.content.Context; 23 | import android.content.Intent; 24 | import android.database.Cursor; 25 | import android.net.ConnectivityManager; 26 | import android.net.NetworkInfo; 27 | import android.net.Uri; 28 | import android.text.TextUtils; 29 | import android.util.Log; 30 | import android.util.Pair; 31 | 32 | import com.limpoxe.downloads.utils.ConnectManager; 33 | 34 | import java.io.CharArrayWriter; 35 | import java.io.File; 36 | import java.util.ArrayList; 37 | import java.util.Collection; 38 | import java.util.Collections; 39 | import java.util.List; 40 | import java.util.concurrent.Executor; 41 | import java.util.concurrent.ExecutorService; 42 | import java.util.concurrent.Future; 43 | 44 | import static com.limpoxe.downloads.Constants.TAG; 45 | 46 | /** 47 | * Details about a specific download. Fields should only be mutated by updating 48 | * from database query. 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 | 58 | public Reader(ContentResolver resolver, Cursor cursor) { 59 | mResolver = resolver; 60 | mCursor = cursor; 61 | } 62 | 63 | public DownloadInfo newDownloadInfo( 64 | Context context, DownloadNotifier notifier) { 65 | final DownloadInfo info = new DownloadInfo(context, notifier); 66 | updateFromDatabase(info); 67 | readRequestHeaders(info); 68 | return info; 69 | } 70 | 71 | public void updateFromDatabase(DownloadInfo info) { 72 | info.mId = getLong(Downloads.Impl._ID); 73 | info.mUri = getString(Downloads.Impl.COLUMN_URI); 74 | info.mNoIntegrity = getInt(Downloads.Impl.COLUMN_NO_INTEGRITY) == 1; 75 | info.mHint = getString(Downloads.Impl.COLUMN_FILE_NAME_HINT); 76 | info.mFileName = getString(Downloads.Impl._DATA); 77 | info.mMimeType = StorageUtils.normalizeMimeType(getString(Downloads.Impl.COLUMN_MIME_TYPE)); 78 | info.mDestination = getInt(Downloads.Impl.COLUMN_DESTINATION); 79 | info.mVisibility = getInt(Downloads.Impl.COLUMN_VISIBILITY); 80 | info.mStatus = getInt(Downloads.Impl.COLUMN_STATUS); 81 | info.mNumFailed = getInt(Downloads.Impl.COLUMN_FAILED_CONNECTIONS); 82 | int retryRedirect = getInt(Constants.RETRY_AFTER_X_REDIRECT_COUNT); 83 | info.mRetryAfter = retryRedirect & 0xfffffff; 84 | info.mLastMod = getLong(Downloads.Impl.COLUMN_LAST_MODIFICATION); 85 | info.mPackage = getString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); 86 | info.mClass = getString(Downloads.Impl.COLUMN_NOTIFICATION_CLASS); 87 | info.mExtras = getString(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS); 88 | info.mCookies = getString(Downloads.Impl.COLUMN_COOKIE_DATA); 89 | info.mUserAgent = getString(Downloads.Impl.COLUMN_USER_AGENT); 90 | info.mReferer = getString(Downloads.Impl.COLUMN_REFERER); 91 | info.mTotalBytes = getLong(Downloads.Impl.COLUMN_TOTAL_BYTES); 92 | info.mCurrentBytes = getLong(Downloads.Impl.COLUMN_CURRENT_BYTES); 93 | info.mETag = getString(Constants.ETAG); 94 | info.mUid = getInt(Constants.UID); 95 | info.mMediaScanned = getInt(Downloads.Impl.COLUMN_MEDIA_SCANNED); 96 | info.mDeleted = getInt(Downloads.Impl.COLUMN_DELETED) == 1; 97 | info.mMediaProviderUri = getString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI); 98 | info.mIsPublicApi = getInt(Downloads.Impl.COLUMN_IS_PUBLIC_API) != 0; 99 | info.mAllowedNetworkTypes = getInt(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES); 100 | info.mAllowRoaming = getInt(Downloads.Impl.COLUMN_ALLOW_ROAMING) != 0; 101 | info.mAllowMetered = getInt(Downloads.Impl.COLUMN_ALLOW_METERED) != 0; 102 | info.mTitle = getString(Downloads.Impl.COLUMN_TITLE); 103 | info.mDescription = getString(Downloads.Impl.COLUMN_DESCRIPTION); 104 | info.mBypassRecommendedSizeLimit = 105 | getInt(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT); 106 | 107 | synchronized (this) { 108 | info.mControl = getInt(Downloads.Impl.COLUMN_CONTROL); 109 | } 110 | } 111 | 112 | private void readRequestHeaders(DownloadInfo info) { 113 | info.mRequestHeaders.clear(); 114 | Uri headerUri = Uri.withAppendedPath( 115 | info.getAllDownloadsUri(), Downloads.Impl.RequestHeaders.URI_SEGMENT); 116 | Cursor cursor = mResolver.query(headerUri, null, null, null, null); 117 | try { 118 | int headerIndex = 119 | cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_HEADER); 120 | int valueIndex = 121 | cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_VALUE); 122 | for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 123 | addHeader(info, cursor.getString(headerIndex), cursor.getString(valueIndex)); 124 | } 125 | } finally { 126 | cursor.close(); 127 | } 128 | 129 | if (info.mCookies != null) { 130 | addHeader(info, "Cookie", info.mCookies); 131 | } 132 | if (info.mReferer != null) { 133 | addHeader(info, "Referer", info.mReferer); 134 | } 135 | } 136 | 137 | private void addHeader(DownloadInfo info, String header, String value) { 138 | info.mRequestHeaders.add(Pair.create(header, value)); 139 | } 140 | 141 | private String getString(String column) { 142 | int index = mCursor.getColumnIndexOrThrow(column); 143 | String s = mCursor.getString(index); 144 | return (TextUtils.isEmpty(s)) ? null : s; 145 | } 146 | 147 | private Integer getInt(String column) { 148 | return mCursor.getInt(mCursor.getColumnIndexOrThrow(column)); 149 | } 150 | 151 | private Long getLong(String column) { 152 | return mCursor.getLong(mCursor.getColumnIndexOrThrow(column)); 153 | } 154 | } 155 | 156 | /** 157 | * Constants used to indicate network state for a specific download, after 158 | * applying any requested constraints. 159 | */ 160 | public enum NetworkState { 161 | /** 162 | * The network is usable for the given download. 163 | */ 164 | OK, 165 | 166 | /** 167 | * There is no network connectivity. 168 | */ 169 | NO_CONNECTION, 170 | 171 | /** 172 | * The download exceeds the maximum size for this network. 173 | */ 174 | UNUSABLE_DUE_TO_SIZE, 175 | 176 | /** 177 | * The download exceeds the recommended maximum size for this network, 178 | * the user must confirm for this download to proceed without WiFi. 179 | */ 180 | RECOMMENDED_UNUSABLE_DUE_TO_SIZE, 181 | 182 | /** 183 | * The current connection is roaming, and the download can't proceed 184 | * over a roaming connection. 185 | */ 186 | CANNOT_USE_ROAMING, 187 | 188 | /** 189 | * The app requesting the download specific that it can't use the 190 | * current network connection. 191 | */ 192 | TYPE_DISALLOWED_BY_REQUESTOR, 193 | 194 | /** 195 | * Current network is blocked for requesting application. 196 | */ 197 | BLOCKED; 198 | } 199 | 200 | /** 201 | * For intents used to notify the user that a download exceeds a size threshold, if this extra 202 | * is true, WiFi is required for this download size; otherwise, it is only recommended. 203 | */ 204 | public static final String EXTRA_IS_WIFI_REQUIRED = "isWifiRequired"; 205 | 206 | public long mId; 207 | public String mUri; 208 | @Deprecated 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> mRequestHeaders = new ArrayList>(); 244 | 245 | /** 246 | * Result of last {@link DownloadThread} started by 247 | * {@link #startDownloadIfReady(ExecutorService)}. 248 | */ 249 | private Future mSubmittedTask; 250 | 251 | private DownloadThread mTask; 252 | 253 | private final Context mContext; 254 | private final DownloadNotifier mNotifier; 255 | 256 | private DownloadInfo(Context context, DownloadNotifier notifier) { 257 | mContext = context; 258 | mNotifier = notifier; 259 | mFuzz = Helpers.sRandom.nextInt(1001); 260 | } 261 | 262 | public Collection> getHeaders() { 263 | return Collections.unmodifiableList(mRequestHeaders); 264 | } 265 | 266 | public String getUserAgent() { 267 | if (mUserAgent != null) { 268 | return mUserAgent; 269 | } else { 270 | return Constants.DEFAULT_USER_AGENT; 271 | } 272 | } 273 | 274 | public void sendIntentIfRequested() { 275 | 276 | Log.e("DownloadInfo", "sendIntentIfRequested download complete"); 277 | 278 | if (mPackage == null) { 279 | return; 280 | } 281 | 282 | Intent intent; 283 | if (mIsPublicApi) { 284 | intent = new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE); 285 | intent.setPackage(mPackage); 286 | intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, mId); 287 | } else { // legacy behavior 288 | if (mClass == null) { 289 | return; 290 | } 291 | intent = new Intent(Downloads.Impl.ACTION_DOWNLOAD_COMPLETED); 292 | intent.setClassName(mPackage, mClass); 293 | if (mExtras != null) { 294 | intent.putExtra(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, mExtras); 295 | } 296 | // We only send the content: URI, for security reasons. Otherwise, malicious 297 | // applications would have an easier time spoofing download results by 298 | // sending spoofed intents. 299 | intent.setData(getMyDownloadsUri()); 300 | } 301 | mContext.sendBroadcast(intent); 302 | } 303 | 304 | /** 305 | * Returns the time when a download should be restarted. 306 | */ 307 | public long restartTime(long now) { 308 | if (mNumFailed == 0) { 309 | return now; 310 | } 311 | if (mRetryAfter > 0) { 312 | return mLastMod + mRetryAfter; 313 | } 314 | return mLastMod + 315 | Constants.RETRY_FIRST_DELAY * 316 | (1000 + mFuzz) * (1 << (mNumFailed - 1)); 317 | } 318 | 319 | /** 320 | * Returns whether this download should be enqueued. 321 | */ 322 | private boolean isReadyToDownload() { 323 | if (mControl == Downloads.Impl.CONTROL_PAUSED) { 324 | // the download is paused, so it's not going to start 325 | return false; 326 | } 327 | switch (mStatus) { 328 | case 0: // status hasn't been initialized yet, this is a new download 329 | case Downloads.Impl.STATUS_PENDING: // download is explicit marked as ready to start 330 | case Downloads.Impl.STATUS_RUNNING: // download interrupted (process killed etc) while 331 | // running, without a chance to update the database 332 | return true; 333 | 334 | case Downloads.Impl.STATUS_WAITING_FOR_NETWORK: 335 | case Downloads.Impl.STATUS_QUEUED_FOR_WIFI: 336 | return checkCanUseNetwork(mTotalBytes) == NetworkState.OK; 337 | 338 | case Downloads.Impl.STATUS_WAITING_TO_RETRY: 339 | // download was waiting for a delayed restart 340 | final long now = System.currentTimeMillis(); 341 | return restartTime(now) <= now; 342 | case Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR: 343 | // is the media mounted? 344 | final Uri uri = Uri.parse(mUri); 345 | if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { 346 | final File file = new File(uri.getPath()); 347 | Log.w(TAG, "Expected file URI on external storage: " + mUri + ", " + file.getAbsolutePath()); 348 | return true; 349 | } else { 350 | Log.w(TAG, "Expected file URI on external storage: " + mUri); 351 | return false; 352 | } 353 | case Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR: 354 | // avoids repetition of retrying download 355 | return false; 356 | } 357 | return false; 358 | } 359 | 360 | /** 361 | * Returns whether this download has a visible notification after 362 | * completion. 363 | */ 364 | public boolean hasCompletionNotification() { 365 | if (!Downloads.Impl.isStatusCompleted(mStatus)) { 366 | return false; 367 | } 368 | if (mVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) { 369 | return true; 370 | } 371 | return false; 372 | } 373 | 374 | /** 375 | * Returns whether this download is allowed to use the network. 376 | */ 377 | public NetworkState checkCanUseNetwork(long totalBytes) { 378 | final NetworkInfo info = ConnectManager.getActiveNetworkInfo(mContext, mUid); 379 | if (info == null || !info.isConnected()) { 380 | return NetworkState.NO_CONNECTION; 381 | } 382 | return checkIsNetworkTypeAllowed(info.getType(), totalBytes); 383 | } 384 | 385 | /** 386 | * Check if this download can proceed over the given network type. 387 | * @param networkType a constant from ConnectivityManager.TYPE_*. 388 | * @return one of the NETWORK_* constants 389 | */ 390 | private NetworkState checkIsNetworkTypeAllowed(int networkType, long totalBytes) { 391 | if (mIsPublicApi) { 392 | final int flag = translateNetworkTypeToApiFlag(networkType); 393 | final boolean allowAllNetworkTypes = mAllowedNetworkTypes == ~0; 394 | if (!allowAllNetworkTypes && (flag & mAllowedNetworkTypes) == 0) { 395 | return NetworkState.TYPE_DISALLOWED_BY_REQUESTOR; 396 | } 397 | } 398 | return checkSizeAllowedForNetwork(networkType, totalBytes); 399 | } 400 | 401 | /** 402 | * Translate a ConnectivityManager.TYPE_* constant to the corresponding 403 | * DownloadManager.Request.NETWORK_* bit flag. 404 | */ 405 | private int translateNetworkTypeToApiFlag(int networkType) { 406 | switch (networkType) { 407 | case ConnectivityManager.TYPE_MOBILE: 408 | return DownloadManager.Request.NETWORK_MOBILE; 409 | 410 | case ConnectivityManager.TYPE_WIFI: 411 | return DownloadManager.Request.NETWORK_WIFI; 412 | 413 | default: 414 | return 0; 415 | } 416 | } 417 | 418 | /** 419 | * Check if the download's size prohibits it from running over the current network. 420 | * @return one of the NETWORK_* constants 421 | */ 422 | private NetworkState checkSizeAllowedForNetwork(int networkType, long totalBytes) { 423 | if (totalBytes <= 0) { 424 | // we don't know the size yet 425 | return NetworkState.OK; 426 | } 427 | 428 | if (ConnectManager.isNetworkTypeMobile(networkType)) { 429 | 430 | if (mBypassRecommendedSizeLimit == 0) { 431 | Log.e("DownloadInfo", "size over due with mobile net"); 432 | } 433 | } 434 | 435 | return NetworkState.OK; 436 | } 437 | 438 | /** 439 | * If download is ready to start, and isn't already pending or executing, 440 | * create a {@link DownloadThread} and enqueue it into given 441 | * {@link Executor}. 442 | * 443 | * @return If actively downloading. 444 | */ 445 | public boolean startDownloadIfReady(ExecutorService executor) { 446 | synchronized (this) { 447 | final boolean isReady = isReadyToDownload(); 448 | final boolean isActive = mSubmittedTask != null && !mSubmittedTask.isDone(); 449 | if (isReady && !isActive) { 450 | if (mStatus != Downloads.Impl.STATUS_RUNNING) { 451 | mStatus = Downloads.Impl.STATUS_RUNNING; 452 | ContentValues values = new ContentValues(); 453 | values.put(Downloads.Impl.COLUMN_STATUS, mStatus); 454 | mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null); 455 | } 456 | 457 | mTask = new DownloadThread(mContext, mNotifier, this); 458 | mSubmittedTask = executor.submit(mTask); 459 | } 460 | return isReady; 461 | } 462 | } 463 | 464 | public Uri getMyDownloadsUri() { 465 | return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, mId); 466 | } 467 | 468 | public Uri getAllDownloadsUri() { 469 | return ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, mId); 470 | } 471 | 472 | @Override 473 | public String toString() { 474 | final CharArrayWriter writer = new CharArrayWriter(); 475 | dump(); 476 | return writer.toString(); 477 | } 478 | 479 | public void dump() { 480 | 481 | Log.d("mId", String.valueOf(mId)); 482 | Log.d("mLastMod", String.valueOf(mLastMod)); 483 | Log.d("mPackage", mPackage); 484 | Log.d("mUid", String.valueOf(mUid)); 485 | 486 | 487 | Log.d("mUri", mUri); 488 | 489 | 490 | Log.d("mMimeType", mMimeType); 491 | Log.d("mCookies", (mCookies != null) ? "yes" : "no"); 492 | Log.d("mReferer", (mReferer != null) ? "yes" : "no"); 493 | Log.d("mUserAgent", mUserAgent); 494 | 495 | Log.d("mFileName", mFileName); 496 | Log.d("mDestination", String.valueOf(mDestination)); 497 | 498 | 499 | Log.d("mStatus", Downloads.Impl.statusToString(mStatus)); 500 | Log.d("mCurrentBytes", String.valueOf(mCurrentBytes)); 501 | Log.d("mTotalBytes", String.valueOf(mTotalBytes)); 502 | 503 | Log.d("mNumFailed", String.valueOf(mNumFailed)); 504 | Log.d("mRetryAfter", String.valueOf(mRetryAfter)); 505 | Log.d("mETag", mETag); 506 | Log.d("mIsPublicApi", String.valueOf(mIsPublicApi)); 507 | 508 | Log.d("mAllowedNetworkTypes", String.valueOf(mAllowedNetworkTypes)); 509 | Log.d("mAllowRoaming", String.valueOf(mAllowRoaming)); 510 | Log.d("mAllowMetered", String.valueOf(mAllowMetered)); 511 | 512 | } 513 | 514 | /** 515 | * Return time when this download will be ready for its next action, in 516 | * milliseconds after given time. 517 | * 518 | * @return If {@code 0}, download is ready to proceed immediately. If 519 | * {@link Long#MAX_VALUE}, then download has no future actions. 520 | */ 521 | public long nextActionMillis(long now) { 522 | if (Downloads.Impl.isStatusCompleted(mStatus)) { 523 | return Long.MAX_VALUE; 524 | } 525 | if (mStatus != Downloads.Impl.STATUS_WAITING_TO_RETRY) { 526 | return 0; 527 | } 528 | long when = restartTime(now); 529 | if (when <= now) { 530 | return 0; 531 | } 532 | return when - now; 533 | } 534 | 535 | void notifyPauseDueToSize(boolean isWifiRequired) { 536 | Log.e("notifyPauseDueToSize", getAllDownloadsUri() + " isWifiRequired = " + isWifiRequired); 537 | } 538 | 539 | /** 540 | * Query and return status of requested download. 541 | */ 542 | public static int queryDownloadStatus(ContentResolver resolver, long id) { 543 | final Cursor cursor = resolver.query( 544 | ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id), 545 | new String[] { Downloads.Impl.COLUMN_STATUS }, null, null, null); 546 | try { 547 | if (cursor.moveToFirst()) { 548 | return cursor.getInt(0); 549 | } else { 550 | // TODO: increase strictness of value returned for unknown 551 | // downloads; this is safe default for now. 552 | return Downloads.Impl.STATUS_PENDING; 553 | } 554 | } finally { 555 | cursor.close(); 556 | } 557 | } 558 | } 559 | -------------------------------------------------------------------------------- /DownloadManager/src/main/java/com/limpoxe/downloads/DownloadNotifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 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.limpoxe.downloads; 18 | 19 | import android.content.Context; 20 | import android.util.Log; 21 | 22 | import java.util.Collection; 23 | 24 | public class DownloadNotifier { 25 | 26 | private final Object mLock1 = new Object(); 27 | private final Object mLock2 = new Object(); 28 | private final Object mLock3 = new Object(); 29 | 30 | public DownloadNotifier(Context context) { 31 | } 32 | 33 | public void cancelAll() { 34 | Log.v("DownloadNotifier", "cancelAll Notification"); 35 | } 36 | 37 | public void notifyDownloadSpeed(long id, long bytesPerSecond) { 38 | synchronized (mLock1) { 39 | if (bytesPerSecond != 0) { 40 | Log.v("DownloadNotifier", "notifyDownloadSpeed " + id + " " + bytesPerSecond); 41 | } else { 42 | Log.v("DownloadNotifier", "notifyDownloadSpeed " + id + " " + bytesPerSecond); 43 | } 44 | } 45 | } 46 | 47 | public void updateWith(Collection downloads) { 48 | synchronized (mLock2) { 49 | Log.v("DownloadNotifier", "updateWith "); 50 | } 51 | } 52 | 53 | public void dumpSpeeds() { 54 | synchronized (mLock3) { 55 | Log.v("DownloadNotifier", "dumpSpeed"); 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /DownloadManager/src/main/java/com/limpoxe/downloads/DownloadReceiver.java: -------------------------------------------------------------------------------- 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.limpoxe.downloads; 18 | 19 | import android.content.BroadcastReceiver; 20 | import android.content.ContentUris; 21 | import android.content.ContentValues; 22 | import android.content.Context; 23 | import android.content.Intent; 24 | import android.database.Cursor; 25 | import android.net.ConnectivityManager; 26 | import android.net.NetworkInfo; 27 | import android.net.Uri; 28 | import android.os.Handler; 29 | import android.os.HandlerThread; 30 | import android.text.TextUtils; 31 | import android.util.Log; 32 | 33 | import static com.limpoxe.downloads.Constants.TAG; 34 | import static com.limpoxe.downloads.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED; 35 | import static com.limpoxe.downloads.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION; 36 | 37 | 38 | /** 39 | * Receives system broadcasts (boot, network connectivity) 40 | */ 41 | public class DownloadReceiver extends BroadcastReceiver { 42 | private static Handler sAsyncHandler; 43 | 44 | static { 45 | final HandlerThread thread = new HandlerThread("DownloadReceiver"); 46 | thread.start(); 47 | sAsyncHandler = new Handler(thread.getLooper()); 48 | } 49 | 50 | @Override 51 | public void onReceive(final Context context, final Intent intent) { 52 | 53 | final String action = intent.getAction(); 54 | if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { 55 | startService(context); 56 | 57 | } else if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) { 58 | startService(context); 59 | 60 | } else if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) { 61 | final ConnectivityManager connManager = (ConnectivityManager) context 62 | .getSystemService(Context.CONNECTIVITY_SERVICE); 63 | final NetworkInfo info = connManager.getActiveNetworkInfo(); 64 | if (info != null && info.isConnected()) { 65 | startService(context); 66 | } 67 | 68 | } else if (Intent.ACTION_UID_REMOVED.equals(action)) { 69 | //app delete? 70 | 71 | } else if (Constants.ACTION_RETRY.equals(action)) { 72 | startService(context); 73 | 74 | } else if (Constants.ACTION_OPEN.equals(action) 75 | || Constants.ACTION_LIST.equals(action) 76 | || Constants.ACTION_HIDE.equals(action)) { 77 | //ignore 78 | Log.d("DownloadReceiver", "action = " + action); 79 | } 80 | } 81 | 82 | /** 83 | * Handle any broadcast related to a system notification. 84 | */ 85 | private void handleNotificationBroadcast(Context context, Intent intent) { 86 | final String action = intent.getAction(); 87 | if (Constants.ACTION_LIST.equals(action)) { 88 | final long[] ids = intent.getLongArrayExtra( 89 | DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS); 90 | sendNotificationClickedIntent(context, ids); 91 | 92 | } else if (Constants.ACTION_OPEN.equals(action)) { 93 | final long id = ContentUris.parseId(intent.getData()); 94 | hideNotification(context, id); 95 | 96 | } else if (Constants.ACTION_HIDE.equals(action)) { 97 | final long id = ContentUris.parseId(intent.getData()); 98 | hideNotification(context, id); 99 | } 100 | } 101 | 102 | /** 103 | * Mark the given {@link DownloadManager#COLUMN_ID} as being acknowledged by 104 | * user so it's not renewed later. 105 | */ 106 | private void hideNotification(Context context, long id) { 107 | final int status; 108 | final int visibility; 109 | 110 | final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id); 111 | final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); 112 | try { 113 | if (cursor.moveToFirst()) { 114 | status = getInt(cursor, Downloads.Impl.COLUMN_STATUS); 115 | visibility = getInt(cursor, Downloads.Impl.COLUMN_VISIBILITY); 116 | } else { 117 | Log.w(TAG, "Missing details for download " + id); 118 | return; 119 | } 120 | } finally { 121 | cursor.close(); 122 | } 123 | 124 | if (Downloads.Impl.isStatusCompleted(status) && 125 | (visibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED 126 | || visibility == VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)) { 127 | final ContentValues values = new ContentValues(); 128 | values.put(Downloads.Impl.COLUMN_VISIBILITY, 129 | Downloads.Impl.VISIBILITY_VISIBLE); 130 | context.getContentResolver().update(uri, values, null, null); 131 | } 132 | } 133 | 134 | /** 135 | * Notify the owner of a running download that its notification was clicked. 136 | */ 137 | private void sendNotificationClickedIntent(Context context, long[] ids) { 138 | Log.d(TAG, "sendNotificationClickedIntent"); 139 | 140 | final String packageName; 141 | final String clazz; 142 | final boolean isPublicApi; 143 | 144 | final Uri uri = ContentUris.withAppendedId( 145 | Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, ids[0]); 146 | final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); 147 | try { 148 | if (cursor.moveToFirst()) { 149 | packageName = getString(cursor, Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); 150 | clazz = getString(cursor, Downloads.Impl.COLUMN_NOTIFICATION_CLASS); 151 | isPublicApi = getInt(cursor, Downloads.Impl.COLUMN_IS_PUBLIC_API) != 0; 152 | } else { 153 | Log.w(TAG, "Missing details for download " + ids[0]); 154 | return; 155 | } 156 | } finally { 157 | cursor.close(); 158 | } 159 | 160 | if (TextUtils.isEmpty(packageName)) { 161 | Log.w(TAG, "Missing package; skipping broadcast"); 162 | return; 163 | } 164 | 165 | Intent appIntent = null; 166 | if (isPublicApi) { 167 | appIntent = new Intent(DownloadManager.ACTION_NOTIFICATION_CLICKED); 168 | appIntent.setPackage(packageName); 169 | appIntent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS, ids); 170 | 171 | } else { // legacy behavior 172 | if (TextUtils.isEmpty(clazz)) { 173 | Log.w(TAG, "Missing class; skipping broadcast"); 174 | return; 175 | } 176 | 177 | appIntent = new Intent(DownloadManager.ACTION_NOTIFICATION_CLICKED); 178 | appIntent.setClassName(packageName, clazz); 179 | appIntent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS, ids); 180 | 181 | if (ids.length == 1) { 182 | appIntent.setData(uri); 183 | } else { 184 | appIntent.setData(Downloads.Impl.CONTENT_URI); 185 | } 186 | } 187 | 188 | context.sendBroadcast(appIntent); 189 | } 190 | 191 | private static String getString(Cursor cursor, String col) { 192 | return cursor.getString(cursor.getColumnIndexOrThrow(col)); 193 | } 194 | 195 | private static int getInt(Cursor cursor, String col) { 196 | return cursor.getInt(cursor.getColumnIndexOrThrow(col)); 197 | } 198 | 199 | private void startService(Context context) { 200 | context.startService(new Intent(context, DownloadService.class)); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /DownloadManager/src/main/java/com/limpoxe/downloads/DownloadService.java: -------------------------------------------------------------------------------- 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.limpoxe.downloads; 18 | 19 | import android.app.AlarmManager; 20 | import android.app.PendingIntent; 21 | import android.app.Service; 22 | import android.content.ContentResolver; 23 | import android.content.Context; 24 | import android.content.Intent; 25 | import android.database.ContentObserver; 26 | import android.database.Cursor; 27 | import android.net.Uri; 28 | import android.os.Handler; 29 | import android.os.HandlerThread; 30 | import android.os.IBinder; 31 | import android.os.Message; 32 | import android.os.Process; 33 | import android.text.TextUtils; 34 | import android.util.Log; 35 | 36 | import com.limpoxe.downloads.utils.GuardedBy; 37 | 38 | import java.io.File; 39 | import java.io.FileDescriptor; 40 | import java.io.PrintWriter; 41 | import java.util.ArrayList; 42 | import java.util.Arrays; 43 | import java.util.Collections; 44 | import java.util.HashMap; 45 | import java.util.HashSet; 46 | import java.util.List; 47 | import java.util.Map; 48 | import java.util.Set; 49 | import java.util.concurrent.CancellationException; 50 | import java.util.concurrent.ExecutionException; 51 | import java.util.concurrent.ExecutorService; 52 | import java.util.concurrent.Future; 53 | import java.util.concurrent.LinkedBlockingQueue; 54 | import java.util.concurrent.ThreadPoolExecutor; 55 | import java.util.concurrent.TimeUnit; 56 | 57 | import static android.text.format.DateUtils.MINUTE_IN_MILLIS; 58 | import static com.limpoxe.downloads.Constants.TAG; 59 | 60 | /** 61 | * Performs background downloads as requested by applications that use 62 | * {@link DownloadManager}. Multiple start commands can be issued at this 63 | * service, and it will continue running until no downloads are being actively 64 | * processed. It may schedule alarms to resume downloads in future. 65 | *

66 | * Any database updates important enough to initiate tasks should always be 67 | * delivered through {@link Context#startService(Intent)}. 68 | */ 69 | public class DownloadService extends Service { 70 | // TODO: migrate WakeLock from individual DownloadThreads out into 71 | // DownloadReceiver to protect our entire workflow. 72 | 73 | private static final boolean DEBUG_LIFECYCLE = true; 74 | 75 | private AlarmManager mAlarmManager; 76 | 77 | /** Observer to get notified when the content observer's data changes */ 78 | private DownloadManagerContentObserver mObserver; 79 | 80 | /** Class to handle Notification Manager updates */ 81 | private DownloadNotifier mNotifier; 82 | 83 | private static final int CLEANUP_JOB_ID = 1; 84 | private static final long CLEANUP_JOB_PERIOD = 1000 * 60 * 60 * 24; // one day 85 | 86 | /** 87 | * The Service's view of the list of downloads, mapping download IDs to the corresponding info 88 | * object. This is kept independently from the content provider, and the Service only initiates 89 | * downloads based on this data, so that it can deal with situation where the data in the 90 | * content provider changes or disappears. 91 | */ 92 | @GuardedBy("mDownloads") 93 | private final Map mDownloads = new HashMap(); 94 | 95 | private final ExecutorService mExecutor = buildDownloadExecutor(); 96 | 97 | private static ExecutorService buildDownloadExecutor() { 98 | final int maxConcurrent = 5; 99 | 100 | // Create a bounded thread pool for executing downloads; it creates 101 | // threads as needed (up to maximum) and reclaims them when finished. 102 | final ThreadPoolExecutor executor = new ThreadPoolExecutor( 103 | maxConcurrent, maxConcurrent, 10, TimeUnit.SECONDS, 104 | new LinkedBlockingQueue()) { 105 | @Override 106 | protected void afterExecute(Runnable r, Throwable t) { 107 | super.afterExecute(r, t); 108 | 109 | if (t == null && r instanceof Future) { 110 | try { 111 | ((Future) r).get(); 112 | } catch (CancellationException ce) { 113 | t = ce; 114 | } catch (ExecutionException ee) { 115 | t = ee.getCause(); 116 | } catch (InterruptedException ie) { 117 | Thread.currentThread().interrupt(); 118 | } 119 | } 120 | 121 | if (t != null) { 122 | Log.w(TAG, "Uncaught exception", t); 123 | } 124 | } 125 | }; 126 | executor.allowCoreThreadTimeOut(true); 127 | return executor; 128 | } 129 | 130 | private HandlerThread mUpdateThread; 131 | private Handler mUpdateHandler; 132 | 133 | private volatile int mLastStartId; 134 | 135 | /** 136 | * Receives notifications when the data in the content provider changes 137 | */ 138 | private class DownloadManagerContentObserver extends ContentObserver { 139 | public DownloadManagerContentObserver() { 140 | super(new Handler()); 141 | } 142 | 143 | @Override 144 | public void onChange(final boolean selfChange) { 145 | enqueueUpdate(); 146 | } 147 | } 148 | 149 | /** 150 | * Returns an IBinder instance when someone wants to connect to this 151 | * service. Binding to this service is not allowed. 152 | * 153 | * @throws UnsupportedOperationException 154 | */ 155 | @Override 156 | public IBinder onBind(Intent i) { 157 | throw new UnsupportedOperationException("Cannot bind to Download Manager Service"); 158 | } 159 | 160 | /** 161 | * Initializes the service when it is first created 162 | */ 163 | @Override 164 | public void onCreate() { 165 | super.onCreate(); 166 | if (Constants.LOGVV) { 167 | Log.v(Constants.TAG, "Service onCreate"); 168 | } 169 | 170 | mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); 171 | 172 | mUpdateThread = new HandlerThread(TAG + "-UpdateThread"); 173 | mUpdateThread.start(); 174 | mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback); 175 | 176 | mNotifier = new DownloadNotifier(this); 177 | mNotifier.cancelAll(); 178 | 179 | mObserver = new DownloadManagerContentObserver(); 180 | getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 181 | true, mObserver); 182 | 183 | } 184 | 185 | @Override 186 | public int onStartCommand(Intent intent, int flags, int startId) { 187 | int returnValue = super.onStartCommand(intent, flags, startId); 188 | if (Constants.LOGVV) { 189 | Log.v(Constants.TAG, "Service onStart"); 190 | } 191 | mLastStartId = startId; 192 | enqueueUpdate(); 193 | return returnValue; 194 | } 195 | 196 | @Override 197 | public void onDestroy() { 198 | getContentResolver().unregisterContentObserver(mObserver); 199 | mUpdateThread.quit(); 200 | if (Constants.LOGVV) { 201 | Log.v(Constants.TAG, "Service onDestroy"); 202 | } 203 | super.onDestroy(); 204 | } 205 | 206 | /** 207 | * Enqueue an {@link #updateLocked()} pass to occur in future. 208 | */ 209 | public void enqueueUpdate() { 210 | if (mUpdateHandler != null) { 211 | mUpdateHandler.removeMessages(MSG_UPDATE); 212 | mUpdateHandler.obtainMessage(MSG_UPDATE, mLastStartId, -1).sendToTarget(); 213 | } 214 | } 215 | 216 | /** 217 | * Enqueue an {@link #updateLocked()} pass to occur after delay, usually to 218 | * catch any finished operations that didn't trigger an update pass. 219 | */ 220 | private void enqueueFinalUpdate() { 221 | mUpdateHandler.removeMessages(MSG_FINAL_UPDATE); 222 | mUpdateHandler.sendMessageDelayed( 223 | mUpdateHandler.obtainMessage(MSG_FINAL_UPDATE, mLastStartId, -1), 224 | 5 * MINUTE_IN_MILLIS); 225 | } 226 | 227 | private static final int MSG_UPDATE = 1; 228 | private static final int MSG_FINAL_UPDATE = 2; 229 | 230 | private Handler.Callback mUpdateCallback = new Handler.Callback() { 231 | @Override 232 | public boolean handleMessage(Message msg) { 233 | Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 234 | 235 | final int startId = msg.arg1; 236 | if (DEBUG_LIFECYCLE) Log.v(TAG, "Updating for startId " + startId); 237 | 238 | // Since database is current source of truth, our "active" status 239 | // depends on database state. We always get one final update pass 240 | // once the real actions have finished and persisted their state. 241 | 242 | // TODO: switch to asking real tasks to derive active state 243 | // TODO: handle media scanner timeouts 244 | 245 | final boolean isActive; 246 | synchronized (mDownloads) { 247 | isActive = updateLocked(); 248 | } 249 | 250 | if (msg.what == MSG_FINAL_UPDATE) { 251 | // Dump thread stacks belonging to pool 252 | for (Map.Entry entry : 253 | Thread.getAllStackTraces().entrySet()) { 254 | if (entry.getKey().getName().startsWith("pool")) { 255 | Log.d(TAG, entry.getKey() + ": " + Arrays.toString(entry.getValue())); 256 | } 257 | } 258 | 259 | // Dump speed and update details 260 | mNotifier.dumpSpeeds(); 261 | 262 | Log.wtf(TAG, "Final update pass triggered, isActive=" + isActive 263 | + "; someone didn't update correctly."); 264 | } 265 | 266 | if (isActive) { 267 | // Still doing useful work, keep service alive. These active 268 | // tasks will trigger another update pass when they're finished. 269 | 270 | // Enqueue delayed update pass to catch finished operations that 271 | // didn't trigger an update pass; these are bugs. 272 | enqueueFinalUpdate(); 273 | 274 | } else { 275 | // No active tasks, and any pending update messages can be 276 | // ignored, since any updates important enough to initiate tasks 277 | // will always be delivered with a new startId. 278 | 279 | if (stopSelfResult(startId)) { 280 | if (DEBUG_LIFECYCLE) Log.v(TAG, "Nothing left; stopped"); 281 | getContentResolver().unregisterContentObserver(mObserver); 282 | mUpdateThread.quit(); 283 | } 284 | } 285 | 286 | return true; 287 | } 288 | }; 289 | 290 | private boolean updateLocked() { 291 | final long now = System.currentTimeMillis(); 292 | 293 | boolean isActive = false; 294 | long nextActionMillis = Long.MAX_VALUE; 295 | 296 | final Set staleIds = new HashSet(mDownloads.keySet()); 297 | 298 | final ContentResolver resolver = getContentResolver(); 299 | final Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, 300 | null, null, null, null); 301 | try { 302 | final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor); 303 | final int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID); 304 | while (cursor.moveToNext()) { 305 | final long id = cursor.getLong(idColumn); 306 | staleIds.remove(id); 307 | 308 | DownloadInfo info = mDownloads.get(id); 309 | if (info != null) { 310 | updateDownload(reader, info, now); 311 | } else { 312 | info = insertDownloadLocked(reader, now); 313 | } 314 | 315 | if (info.mDeleted) { 316 | // Delete download if requested, but only after cleaning up 317 | if (!TextUtils.isEmpty(info.mMediaProviderUri)) { 318 | resolver.delete(Uri.parse(info.mMediaProviderUri), null, null); 319 | } 320 | 321 | deleteFileIfExists(info.mFileName); 322 | resolver.delete(info.getAllDownloadsUri(), null, null); 323 | 324 | } else { 325 | // Kick off download task if ready 326 | final boolean activeDownload = info.startDownloadIfReady(mExecutor); 327 | 328 | if (DEBUG_LIFECYCLE && (activeDownload)) { 329 | Log.v(TAG, "Download " + info.mId + ": activeDownload=" + activeDownload); 330 | } 331 | 332 | isActive |= activeDownload; 333 | } 334 | 335 | // Keep track of nearest next action 336 | nextActionMillis = Math.min(info.nextActionMillis(now), nextActionMillis); 337 | } 338 | } finally { 339 | cursor.close(); 340 | } 341 | 342 | // Clean up stale downloads that disappeared 343 | for (Long id : staleIds) { 344 | deleteDownloadLocked(id); 345 | } 346 | 347 | // Update notifications visible to user 348 | mNotifier.updateWith(mDownloads.values()); 349 | 350 | // Set alarm when next action is in future. It's okay if the service 351 | // continues to run in meantime, since it will kick off an update pass. 352 | if (nextActionMillis > 0 && nextActionMillis < Long.MAX_VALUE) { 353 | if (Constants.LOGV) { 354 | Log.v(TAG, "scheduling start in " + nextActionMillis + "ms"); 355 | } 356 | 357 | final Intent intent = new Intent(Constants.ACTION_RETRY); 358 | intent.setClass(this, DownloadReceiver.class); 359 | mAlarmManager.set(AlarmManager.RTC_WAKEUP, now + nextActionMillis, 360 | PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_ONE_SHOT)); 361 | } 362 | 363 | return isActive; 364 | } 365 | 366 | /** 367 | * Keeps a local copy of the info about a download, and initiates the 368 | * download if appropriate. 369 | */ 370 | private DownloadInfo insertDownloadLocked(DownloadInfo.Reader reader, long now) { 371 | final DownloadInfo info = reader.newDownloadInfo(this, mNotifier); 372 | mDownloads.put(info.mId, info); 373 | 374 | if (Constants.LOGVV) { 375 | Log.v(Constants.TAG, "processing inserted download " + info.mId); 376 | } 377 | 378 | return info; 379 | } 380 | 381 | /** 382 | * Updates the local copy of the info about a download. 383 | */ 384 | private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) { 385 | reader.updateFromDatabase(info); 386 | if (Constants.LOGVV) { 387 | Log.v(Constants.TAG, "processing updated download " + info.mId + 388 | ", status: " + info.mStatus); 389 | } 390 | } 391 | 392 | /** 393 | * Removes the local copy of the info about a download. 394 | */ 395 | private void deleteDownloadLocked(long id) { 396 | DownloadInfo info = mDownloads.get(id); 397 | if (info.mStatus == Downloads.Impl.STATUS_RUNNING) { 398 | info.mStatus = Downloads.Impl.STATUS_CANCELED; 399 | } 400 | if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) { 401 | if (Constants.LOGVV) { 402 | Log.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName); 403 | } 404 | deleteFileIfExists(info.mFileName); 405 | } 406 | mDownloads.remove(info.mId); 407 | } 408 | 409 | private void deleteFileIfExists(String path) { 410 | if (!TextUtils.isEmpty(path)) { 411 | if (Constants.LOGVV) { 412 | Log.d(TAG, "deleteFileIfExists() deleting " + path); 413 | } 414 | final File file = new File(path); 415 | if (file.exists() && !file.delete()) { 416 | Log.w(TAG, "file: '" + path + "' couldn't be deleted"); 417 | } 418 | } 419 | } 420 | 421 | @Override 422 | protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 423 | synchronized (mDownloads) { 424 | final List ids = new ArrayList(mDownloads.keySet()); 425 | Collections.sort(ids); 426 | for (Long id : ids) { 427 | final DownloadInfo info = mDownloads.get(id); 428 | info.dump(); 429 | } 430 | } 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /DownloadManager/src/main/java/com/limpoxe/downloads/DownloadThread.java: -------------------------------------------------------------------------------- 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.limpoxe.downloads; 18 | 19 | import android.content.ContentValues; 20 | import android.content.Context; 21 | import android.net.NetworkInfo; 22 | import android.net.Uri; 23 | import android.os.Build; 24 | import android.os.ParcelFileDescriptor; 25 | import android.os.PowerManager; 26 | import android.os.Process; 27 | import android.os.SystemClock; 28 | import android.system.Os; 29 | import android.util.Log; 30 | import android.util.Pair; 31 | 32 | import com.limpoxe.downloads.DownloadInfo.NetworkState; 33 | 34 | import com.limpoxe.downloads.utils.ConnectManager; 35 | import com.limpoxe.downloads.utils.IoUtils; 36 | 37 | import java.io.File; 38 | import java.io.FileDescriptor; 39 | import java.io.FileNotFoundException; 40 | import java.io.IOException; 41 | import java.io.InputStream; 42 | import java.io.OutputStream; 43 | import java.net.HttpURLConnection; 44 | import java.net.MalformedURLException; 45 | import java.net.ProtocolException; 46 | import java.net.URL; 47 | import java.net.URLConnection; 48 | 49 | import static android.text.format.DateUtils.SECOND_IN_MILLIS; 50 | import static com.limpoxe.downloads.Constants.TAG; 51 | import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; 52 | import static java.net.HttpURLConnection.HTTP_MOVED_PERM; 53 | import static java.net.HttpURLConnection.HTTP_MOVED_TEMP; 54 | import static java.net.HttpURLConnection.HTTP_OK; 55 | import static java.net.HttpURLConnection.HTTP_PARTIAL; 56 | import static java.net.HttpURLConnection.HTTP_PRECON_FAILED; 57 | import static java.net.HttpURLConnection.HTTP_SEE_OTHER; 58 | import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; 59 | import static com.limpoxe.downloads.Downloads.Impl.STATUS_BAD_REQUEST; 60 | import static com.limpoxe.downloads.Downloads.Impl.STATUS_CANCELED; 61 | import static com.limpoxe.downloads.Downloads.Impl.STATUS_CANNOT_RESUME; 62 | import static com.limpoxe.downloads.Downloads.Impl.STATUS_FILE_ERROR; 63 | import static com.limpoxe.downloads.Downloads.Impl.STATUS_HTTP_DATA_ERROR; 64 | import static com.limpoxe.downloads.Downloads.Impl.STATUS_SUCCESS; 65 | import static com.limpoxe.downloads.Downloads.Impl.STATUS_TOO_MANY_REDIRECTS; 66 | import static com.limpoxe.downloads.Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE; 67 | import static com.limpoxe.downloads.Downloads.Impl.STATUS_UNKNOWN_ERROR; 68 | import static com.limpoxe.downloads.Downloads.Impl.STATUS_WAITING_FOR_NETWORK; 69 | import static com.limpoxe.downloads.Downloads.Impl.STATUS_WAITING_TO_RETRY; 70 | 71 | /** 72 | * Task which executes a given {@link DownloadInfo}: making network requests, 73 | * persisting data to disk, and updating {@link DownloadProvider}. 74 | *

75 | * To know if a download is successful, we need to know either the final content 76 | * length to expect, or the transfer to be chunked. To resume an interrupted 77 | * download, we need an ETag. 78 | *

79 | * Failed network requests are retried several times before giving up. Local 80 | * disk errors fail immediately and are not retried. 81 | */ 82 | public class DownloadThread implements Runnable { 83 | 84 | // TODO: bind each download to a specific network interface to avoid state 85 | // checking races once we have ConnectivityManager API 86 | 87 | // TODO: add support for saving to content:// 88 | 89 | private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416; 90 | private static final int HTTP_TEMP_REDIRECT = 307; 91 | 92 | private static final int DEFAULT_TIMEOUT = (int) (20 * SECOND_IN_MILLIS); 93 | 94 | private final Context mContext; 95 | private final DownloadNotifier mNotifier; 96 | 97 | private final long mId; 98 | 99 | /** 100 | * Info object that should be treated as read-only. Any potentially mutated 101 | * fields are tracked in {@link #mInfoDelta}. If a field exists in 102 | * {@link #mInfoDelta}, it must not be read from {@link #mInfo}. 103 | */ 104 | private final DownloadInfo mInfo; 105 | private final DownloadInfoDelta mInfoDelta; 106 | 107 | private volatile boolean mPolicyDirty; 108 | 109 | /** 110 | * Local changes to {@link DownloadInfo}. These are kept local to avoid 111 | * racing with the thread that updates based on change notifications. 112 | */ 113 | private class DownloadInfoDelta { 114 | public String mUri; 115 | public String mFileName; 116 | public String mMimeType; 117 | public int mStatus; 118 | public int mNumFailed; 119 | public int mRetryAfter; 120 | public long mTotalBytes; 121 | public long mCurrentBytes; 122 | public String mETag; 123 | 124 | public String mErrorMsg; 125 | 126 | public DownloadInfoDelta(DownloadInfo info) { 127 | mUri = info.mUri; 128 | mFileName = info.mFileName; 129 | mMimeType = info.mMimeType; 130 | mStatus = info.mStatus; 131 | mNumFailed = info.mNumFailed; 132 | mRetryAfter = info.mRetryAfter; 133 | mTotalBytes = info.mTotalBytes; 134 | mCurrentBytes = info.mCurrentBytes; 135 | mETag = info.mETag; 136 | } 137 | 138 | private ContentValues buildContentValues() { 139 | final ContentValues values = new ContentValues(); 140 | 141 | values.put(Downloads.Impl.COLUMN_URI, mUri); 142 | values.put(Downloads.Impl._DATA, mFileName); 143 | values.put(Downloads.Impl.COLUMN_MIME_TYPE, mMimeType); 144 | values.put(Downloads.Impl.COLUMN_STATUS, mStatus); 145 | values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, mNumFailed); 146 | values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, mRetryAfter); 147 | values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mTotalBytes); 148 | values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, mCurrentBytes); 149 | values.put(Constants.ETAG, mETag); 150 | 151 | values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, System.currentTimeMillis()); 152 | values.put(Downloads.Impl.COLUMN_ERROR_MSG, mErrorMsg); 153 | 154 | return values; 155 | } 156 | 157 | /** 158 | * Blindly push update of current delta values to provider. 159 | */ 160 | public void writeToDatabase() { 161 | mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), buildContentValues(), 162 | null, null); 163 | } 164 | 165 | /** 166 | * Push update of current delta values to provider, asserting strongly 167 | * that we haven't been paused or deleted. 168 | */ 169 | public void writeToDatabaseOrThrow() throws StopRequestException { 170 | if (mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), 171 | buildContentValues(), Downloads.Impl.COLUMN_DELETED + " == '0'", null) == 0) { 172 | throw new StopRequestException(STATUS_CANCELED, "Download deleted or missing!"); 173 | } 174 | } 175 | } 176 | 177 | /** 178 | * Flag indicating if we've made forward progress transferring file data 179 | * from a remote server. 180 | */ 181 | private boolean mMadeProgress = false; 182 | 183 | /** 184 | * Details from the last time we pushed a database update. 185 | */ 186 | private long mLastUpdateBytes = 0; 187 | private long mLastUpdateTime = 0; 188 | 189 | private int mNetworkType = ConnectManager.TYPE_NONE; 190 | 191 | /** Historical bytes/second speed of this download. */ 192 | private long mSpeed; 193 | /** Time when current sample started. */ 194 | private long mSpeedSampleStart; 195 | /** Bytes transferred since current sample started. */ 196 | private long mSpeedSampleBytes; 197 | 198 | public DownloadThread(Context context, DownloadNotifier notifier, 199 | DownloadInfo info) { 200 | mContext = context; 201 | mNotifier = notifier; 202 | 203 | mId = info.mId; 204 | mInfo = info; 205 | mInfoDelta = new DownloadInfoDelta(info); 206 | } 207 | 208 | @Override 209 | public void run() { 210 | Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 211 | 212 | // Skip when download already marked as finished; this download was 213 | // probably started again while racing with UpdateThread. 214 | if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mId) 215 | == STATUS_SUCCESS) { 216 | logDebug("Already finished; skipping"); 217 | return; 218 | } 219 | 220 | PowerManager.WakeLock wakeLock = null; 221 | final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); 222 | 223 | try { 224 | wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG); 225 | wakeLock.acquire(); 226 | 227 | // while performing download, register for rules updates 228 | 229 | logDebug("Starting"); 230 | 231 | // Remember which network this download started on; used to 232 | // determine if errors were due to network changes. 233 | final NetworkInfo info = ConnectManager.getActiveNetworkInfo(mContext, mInfo.mUid); 234 | if (info != null) { 235 | mNetworkType = info.getType(); 236 | } 237 | 238 | executeDownload(); 239 | 240 | mInfoDelta.mStatus = STATUS_SUCCESS; 241 | 242 | // If we just finished a chunked file, record total size 243 | if (mInfoDelta.mTotalBytes == -1) { 244 | mInfoDelta.mTotalBytes = mInfoDelta.mCurrentBytes; 245 | } 246 | 247 | } catch (StopRequestException e) { 248 | mInfoDelta.mStatus = e.getFinalStatus(); 249 | mInfoDelta.mErrorMsg = e.getMessage(); 250 | 251 | logWarning("Stop requested with status " 252 | + Downloads.Impl.statusToString(mInfoDelta.mStatus) + ": " 253 | + mInfoDelta.mErrorMsg); 254 | 255 | // Nobody below our level should request retries, since we handle 256 | // failure counts at this level. 257 | if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY) { 258 | throw new IllegalStateException("Execution should always throw final error codes"); 259 | } 260 | 261 | // Some errors should be retryable, unless we fail too many times. 262 | if (isStatusRetryable(mInfoDelta.mStatus)) { 263 | if (mMadeProgress) { 264 | mInfoDelta.mNumFailed = 1; 265 | } else { 266 | mInfoDelta.mNumFailed += 1; 267 | } 268 | 269 | if (mInfoDelta.mNumFailed < Constants.MAX_RETRIES) { 270 | final NetworkInfo info = ConnectManager.getActiveNetworkInfo(mContext, mInfo.mUid); 271 | if (info != null && info.getType() == mNetworkType && info.isConnected()) { 272 | // Underlying network is still intact, use normal backoff 273 | mInfoDelta.mStatus = STATUS_WAITING_TO_RETRY; 274 | } else { 275 | // Network changed, retry on any next available 276 | mInfoDelta.mStatus = STATUS_WAITING_FOR_NETWORK; 277 | } 278 | 279 | if ((mInfoDelta.mETag == null && mMadeProgress)) { 280 | // However, if we wrote data and have no ETag to verify 281 | // contents against later, we can't actually resume. 282 | mInfoDelta.mStatus = STATUS_CANNOT_RESUME; 283 | } 284 | } 285 | } 286 | 287 | } catch (Throwable t) { 288 | mInfoDelta.mStatus = STATUS_UNKNOWN_ERROR; 289 | mInfoDelta.mErrorMsg = t.toString(); 290 | 291 | logError("Failed: " + mInfoDelta.mErrorMsg, t); 292 | 293 | } finally { 294 | logDebug("Finished with status " + Downloads.Impl.statusToString(mInfoDelta.mStatus)); 295 | 296 | mNotifier.notifyDownloadSpeed(mId, 0); 297 | 298 | finalizeDestination(); 299 | 300 | mInfoDelta.writeToDatabase(); 301 | 302 | if (Downloads.Impl.isStatusCompleted(mInfoDelta.mStatus)) { 303 | mInfo.sendIntentIfRequested(); 304 | } 305 | 306 | if (wakeLock != null) { 307 | wakeLock.release(); 308 | wakeLock = null; 309 | } 310 | } 311 | } 312 | 313 | /** 314 | * Fully execute a single download request. Setup and send the request, 315 | * handle the response, and transfer the data to the destination file. 316 | */ 317 | private void executeDownload() throws StopRequestException { 318 | final boolean resuming = mInfoDelta.mCurrentBytes != 0; 319 | 320 | logDebug("resuming; mCurrentBytes is " + mInfoDelta.mCurrentBytes); 321 | 322 | URL url; 323 | try { 324 | // TODO: migrate URL sanity checking into client side of API 325 | url = new URL(mInfoDelta.mUri); 326 | } catch (MalformedURLException e) { 327 | throw new StopRequestException(STATUS_BAD_REQUEST, e); 328 | } 329 | 330 | int redirectionCount = 0; 331 | while (redirectionCount++ < Constants.MAX_REDIRECTS) { 332 | 333 | // Open connection and follow any redirects until we have a useful 334 | // response with body. 335 | HttpURLConnection conn = null; 336 | try { 337 | checkConnectivity(); 338 | conn = (HttpURLConnection) url.openConnection(); 339 | conn.setInstanceFollowRedirects(false); 340 | conn.setConnectTimeout(DEFAULT_TIMEOUT); 341 | conn.setReadTimeout(DEFAULT_TIMEOUT); 342 | 343 | addRequestHeaders(conn, resuming); 344 | 345 | final int responseCode = conn.getResponseCode(); 346 | switch (responseCode) { 347 | case HTTP_OK: 348 | if (resuming) { 349 | throw new StopRequestException( 350 | STATUS_CANNOT_RESUME, "Expected partial, but received OK"); 351 | } 352 | parseOkHeaders(conn); 353 | transferData(conn); 354 | return; 355 | 356 | case HTTP_PARTIAL: 357 | if (!resuming) { 358 | throw new StopRequestException( 359 | STATUS_CANNOT_RESUME, "Expected OK, but received partial"); 360 | } 361 | 362 | logDebug("resuming; received partial "); 363 | 364 | transferData(conn); 365 | return; 366 | 367 | case HTTP_MOVED_PERM: 368 | case HTTP_MOVED_TEMP: 369 | case HTTP_SEE_OTHER: 370 | case HTTP_TEMP_REDIRECT: 371 | final String location = conn.getHeaderField("Location"); 372 | url = new URL(url, location); 373 | if (responseCode == HTTP_MOVED_PERM) { 374 | // Push updated URL back to database 375 | mInfoDelta.mUri = url.toString(); 376 | } 377 | continue; 378 | 379 | case HTTP_PRECON_FAILED: 380 | throw new StopRequestException( 381 | STATUS_CANNOT_RESUME, "Precondition failed"); 382 | 383 | case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE: 384 | throw new StopRequestException( 385 | STATUS_CANNOT_RESUME, "Requested range not satisfiable"); 386 | 387 | case HTTP_UNAVAILABLE: 388 | parseUnavailableHeaders(conn); 389 | throw new StopRequestException( 390 | HTTP_UNAVAILABLE, conn.getResponseMessage()); 391 | 392 | case HTTP_INTERNAL_ERROR: 393 | throw new StopRequestException( 394 | HTTP_INTERNAL_ERROR, conn.getResponseMessage()); 395 | 396 | default: 397 | StopRequestException.throwUnhandledHttpError( 398 | responseCode, conn.getResponseMessage()); 399 | } 400 | 401 | } catch (IOException e) { 402 | if (e instanceof ProtocolException 403 | && e.getMessage().startsWith("Unexpected status line")) { 404 | throw new StopRequestException(STATUS_UNHANDLED_HTTP_CODE, e); 405 | } else { 406 | // Trouble with low-level sockets 407 | throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e); 408 | } 409 | 410 | } finally { 411 | if (conn != null) conn.disconnect(); 412 | } 413 | } 414 | 415 | throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects"); 416 | } 417 | 418 | /** 419 | * Transfer data from the given connection to the destination file. 420 | */ 421 | private void transferData(HttpURLConnection conn) throws StopRequestException { 422 | 423 | // To detect when we're really finished, we either need a length, closed 424 | // connection, or chunked encoding. 425 | final boolean hasLength = mInfoDelta.mTotalBytes != -1; 426 | final boolean isConnectionClose = "close".equalsIgnoreCase( 427 | conn.getHeaderField("Connection")); 428 | final boolean isEncodingChunked = "chunked".equalsIgnoreCase( 429 | conn.getHeaderField("Transfer-Encoding")); 430 | 431 | final boolean finishKnown = hasLength || isConnectionClose || isEncodingChunked; 432 | if (!finishKnown) { 433 | throw new StopRequestException( 434 | STATUS_CANNOT_RESUME, "can't know size of download, giving up"); 435 | } 436 | 437 | ParcelFileDescriptor outPfd = null; 438 | FileDescriptor outFd = null; 439 | InputStream in = null; 440 | OutputStream out = null; 441 | try { 442 | try { 443 | in = conn.getInputStream(); 444 | } catch (IOException e) { 445 | throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e); 446 | } 447 | 448 | try { 449 | Uri uri = mInfo.getAllDownloadsUri(); 450 | 451 | logDebug("openFileDescriptor " + uri.toString()); 452 | 453 | outPfd = mContext.getContentResolver() 454 | .openFileDescriptor(uri, "rw+"); 455 | outFd = outPfd.getFileDescriptor(); 456 | out = new ParcelFileDescriptor.AutoCloseOutputStream(outPfd); 457 | } catch (Exception e) { 458 | throw new StopRequestException(STATUS_FILE_ERROR, e); 459 | } 460 | 461 | // Start streaming data, periodically watch for pause/cancel 462 | // commands and checking disk space as needed. 463 | transferData(in, out, outFd); 464 | 465 | } finally { 466 | 467 | IoUtils.closeQuietly(in); 468 | 469 | try { 470 | if (out != null) out.flush(); 471 | if (outFd != null) outFd.sync(); 472 | } catch (IOException e) { 473 | } finally { 474 | IoUtils.closeQuietly(out); 475 | } 476 | } 477 | } 478 | 479 | /** 480 | * Transfer as much data as possible from the HTTP response to the 481 | * destination file. 482 | */ 483 | private void transferData(InputStream in, OutputStream out, FileDescriptor outFd) 484 | throws StopRequestException { 485 | final byte buffer[] = new byte[Constants.BUFFER_SIZE]; 486 | while (true) { 487 | checkPausedOrCanceled(); 488 | 489 | int len = -1; 490 | try { 491 | len = in.read(buffer); 492 | } catch (IOException e) { 493 | throw new StopRequestException( 494 | STATUS_HTTP_DATA_ERROR, "Failed reading response: " + e, e); 495 | } 496 | 497 | if (len == -1) { 498 | break; 499 | } 500 | 501 | try { 502 | // When streaming, ensure space before each write 503 | if (mInfoDelta.mTotalBytes == -1) { 504 | 505 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { 506 | final long curSize = Os.fstat(outFd).st_size; 507 | final long newBytes = (mInfoDelta.mCurrentBytes + len) - curSize; 508 | } 509 | StorageUtils.ensureAvailableSpace(mContext, outFd/*,newBytes*/); 510 | } 511 | 512 | out.write(buffer, 0, len); 513 | 514 | mMadeProgress = true; 515 | mInfoDelta.mCurrentBytes += len; 516 | 517 | updateProgress(outFd); 518 | 519 | } catch (Exception e) { 520 | throw new StopRequestException(STATUS_FILE_ERROR, e); 521 | } 522 | } 523 | 524 | // Finished without error; verify length if known 525 | if (mInfoDelta.mTotalBytes != -1 && mInfoDelta.mCurrentBytes != mInfoDelta.mTotalBytes) { 526 | throw new StopRequestException(STATUS_HTTP_DATA_ERROR, "Content length mismatch"); 527 | } 528 | } 529 | 530 | /** 531 | * Called just before the thread finishes, regardless of status, to take any 532 | * necessary action on the downloaded file. 533 | */ 534 | private void finalizeDestination() { 535 | if (Downloads.Impl.isStatusError(mInfoDelta.mStatus)) { 536 | // When error, free up any disk space 537 | try { 538 | final ParcelFileDescriptor target = mContext.getContentResolver() 539 | .openFileDescriptor(mInfo.getAllDownloadsUri(), "rw"); 540 | try { 541 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 542 | Os.ftruncate(target.getFileDescriptor(), 0); 543 | } 544 | } catch (Exception ignored) { 545 | } finally { 546 | IoUtils.closeQuietly(target); 547 | } 548 | } catch (FileNotFoundException ignored) { 549 | } 550 | 551 | // Delete if local file 552 | if (mInfoDelta.mFileName != null) { 553 | new File(mInfoDelta.mFileName).delete(); 554 | mInfoDelta.mFileName = null; 555 | } 556 | 557 | } else if (Downloads.Impl.isStatusSuccess(mInfoDelta.mStatus)) { 558 | // When success, open access if local file 559 | if (mInfoDelta.mFileName != null) { 560 | //chmod 644 561 | if (mInfo.mDestination != Downloads.Impl.DESTINATION_FILE_URI) { 562 | try { 563 | // Move into final resting place, if needed 564 | final File before = new File(mInfoDelta.mFileName); 565 | final File beforeDir = Helpers.getRunningDestinationDirectory( 566 | mContext, mInfo.mDestination); 567 | final File afterDir = Helpers.getSuccessDestinationDirectory( 568 | mContext, mInfo.mDestination); 569 | if (!beforeDir.equals(afterDir) 570 | && before.getParentFile().equals(beforeDir)) { 571 | final File after = new File(afterDir, before.getName()); 572 | if (before.renameTo(after)) { 573 | mInfoDelta.mFileName = after.getAbsolutePath(); 574 | } 575 | } 576 | } catch (IOException ignored) { 577 | } 578 | } 579 | } 580 | } 581 | } 582 | 583 | /** 584 | * Check if current connectivity is valid for this request. 585 | */ 586 | private void checkConnectivity() throws StopRequestException { 587 | // checking connectivity will apply current policy 588 | mPolicyDirty = false; 589 | 590 | final NetworkState networkUsable = mInfo.checkCanUseNetwork(mInfoDelta.mTotalBytes); 591 | if (networkUsable != NetworkState.OK) { 592 | int status = STATUS_WAITING_FOR_NETWORK; 593 | if (networkUsable == NetworkState.UNUSABLE_DUE_TO_SIZE) { 594 | status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI; 595 | mInfo.notifyPauseDueToSize(true); 596 | } else if (networkUsable == NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE) { 597 | status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI; 598 | mInfo.notifyPauseDueToSize(false); 599 | } 600 | throw new StopRequestException(status, networkUsable.name()); 601 | } 602 | } 603 | 604 | /** 605 | * Check if the download has been paused or canceled, stopping the request 606 | * appropriately if it has been. 607 | */ 608 | private void checkPausedOrCanceled() throws StopRequestException { 609 | synchronized (mInfo) { 610 | if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) { 611 | throw new StopRequestException( 612 | Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner"); 613 | } 614 | if (mInfo.mStatus == STATUS_CANCELED || mInfo.mDeleted) { 615 | throw new StopRequestException(STATUS_CANCELED, "download canceled"); 616 | } 617 | } 618 | 619 | // if policy has been changed, trigger connectivity check 620 | if (mPolicyDirty) { 621 | checkConnectivity(); 622 | } 623 | } 624 | 625 | /** 626 | * Report download progress through the database if necessary. 627 | */ 628 | private void updateProgress(FileDescriptor outFd) throws IOException, StopRequestException { 629 | final long now = SystemClock.elapsedRealtime(); 630 | final long currentBytes = mInfoDelta.mCurrentBytes; 631 | 632 | final long sampleDelta = now - mSpeedSampleStart; 633 | if (sampleDelta > 500) { 634 | final long sampleSpeed = ((currentBytes - mSpeedSampleBytes) * 1000) 635 | / sampleDelta; 636 | 637 | if (mSpeed == 0) { 638 | mSpeed = sampleSpeed; 639 | } else { 640 | mSpeed = ((mSpeed * 3) + sampleSpeed) / 4; 641 | } 642 | 643 | // Only notify once we have a full sample window 644 | if (mSpeedSampleStart != 0) { 645 | mNotifier.notifyDownloadSpeed(mId, mSpeed); 646 | } 647 | 648 | mSpeedSampleStart = now; 649 | mSpeedSampleBytes = currentBytes; 650 | } 651 | 652 | final long bytesDelta = currentBytes - mLastUpdateBytes; 653 | final long timeDelta = now - mLastUpdateTime; 654 | if (bytesDelta > Constants.MIN_PROGRESS_STEP && timeDelta > Constants.MIN_PROGRESS_TIME) { 655 | // fsync() to ensure that current progress has been flushed to disk, 656 | // so we can always resume based on latest database information. 657 | outFd.sync(); 658 | 659 | mInfoDelta.writeToDatabaseOrThrow(); 660 | 661 | mLastUpdateBytes = currentBytes; 662 | mLastUpdateTime = now; 663 | } 664 | } 665 | 666 | /** 667 | * Process response headers from first server response. This derives its 668 | * filename, size, and ETag. 669 | */ 670 | private void parseOkHeaders(HttpURLConnection conn) throws StopRequestException { 671 | if (mInfoDelta.mFileName == null) { 672 | final String contentDisposition = conn.getHeaderField("Content-Disposition"); 673 | final String contentLocation = conn.getHeaderField("Content-Location"); 674 | 675 | try { 676 | mInfoDelta.mFileName = Helpers.generateSaveFile(mContext, mInfoDelta.mUri, 677 | mInfo.mHint, contentDisposition, contentLocation, mInfoDelta.mMimeType, 678 | mInfo.mDestination); 679 | } catch (IOException e) { 680 | throw new StopRequestException( 681 | STATUS_FILE_ERROR, "Failed to generate filename: " + e); 682 | } 683 | } 684 | 685 | if (mInfoDelta.mMimeType == null) { 686 | mInfoDelta.mMimeType = StorageUtils.normalizeMimeType(conn.getContentType()); 687 | } 688 | 689 | final String transferEncoding = conn.getHeaderField("Transfer-Encoding"); 690 | if (transferEncoding == null) { 691 | mInfoDelta.mTotalBytes = getHeaderFieldLong(conn, "Content-Length", -1); 692 | } else { 693 | mInfoDelta.mTotalBytes = -1; 694 | } 695 | 696 | mInfoDelta.mETag = conn.getHeaderField("ETag"); 697 | 698 | mInfoDelta.writeToDatabaseOrThrow(); 699 | 700 | // Check connectivity again now that we know the total size 701 | checkConnectivity(); 702 | } 703 | 704 | private void parseUnavailableHeaders(HttpURLConnection conn) { 705 | long retryAfter = conn.getHeaderFieldInt("Retry-After", -1); 706 | if (retryAfter < 0) { 707 | retryAfter = 0; 708 | } else { 709 | if (retryAfter < Constants.MIN_RETRY_AFTER) { 710 | retryAfter = Constants.MIN_RETRY_AFTER; 711 | } else if (retryAfter > Constants.MAX_RETRY_AFTER) { 712 | retryAfter = Constants.MAX_RETRY_AFTER; 713 | } 714 | retryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1); 715 | } 716 | 717 | mInfoDelta.mRetryAfter = (int) (retryAfter * SECOND_IN_MILLIS); 718 | } 719 | 720 | /** 721 | * Add custom headers for this download to the HTTP request. 722 | */ 723 | private void addRequestHeaders(HttpURLConnection conn, boolean resuming) { 724 | for (Pair header : mInfo.getHeaders()) { 725 | conn.addRequestProperty(header.first, header.second); 726 | } 727 | 728 | // Only splice in user agent when not already defined 729 | if (conn.getRequestProperty("User-Agent") == null) { 730 | conn.addRequestProperty("User-Agent", mInfo.getUserAgent()); 731 | } 732 | 733 | // Defeat transparent gzip compression, since it doesn't allow us to 734 | // easily resume partial downloads. 735 | conn.setRequestProperty("Accept-Encoding", "identity"); 736 | 737 | // Defeat connection reuse, since otherwise servers may continue 738 | // streaming large downloads after cancelled. 739 | conn.setRequestProperty("Connection", "close"); 740 | 741 | if (resuming) { 742 | if (mInfoDelta.mETag != null) { 743 | conn.addRequestProperty("If-Match", mInfoDelta.mETag); 744 | } 745 | conn.addRequestProperty("Range", "bytes=" + mInfoDelta.mCurrentBytes + "-"); 746 | } 747 | } 748 | 749 | private void logDebug(String msg) { 750 | Log.d(TAG, "[" + mId + "] " + msg); 751 | } 752 | 753 | private void logWarning(String msg) { 754 | Log.w(TAG, "[" + mId + "] " + msg); 755 | } 756 | 757 | private void logError(String msg, Throwable t) { 758 | Log.e(TAG, "[" + mId + "] " + msg, t); 759 | } 760 | 761 | private static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) { 762 | try { 763 | return Long.parseLong(conn.getHeaderField(field)); 764 | } catch (NumberFormatException e) { 765 | return defaultValue; 766 | } 767 | } 768 | 769 | /** 770 | * Return if given status is eligible to be treated as 771 | */ 772 | public static boolean isStatusRetryable(int status) { 773 | switch (status) { 774 | case STATUS_HTTP_DATA_ERROR: 775 | case HTTP_UNAVAILABLE: 776 | case HTTP_INTERNAL_ERROR: 777 | case STATUS_FILE_ERROR: 778 | return true; 779 | default: 780 | return false; 781 | } 782 | } 783 | } 784 | -------------------------------------------------------------------------------- /DownloadManager/src/main/java/com/limpoxe/downloads/Downloads.java: -------------------------------------------------------------------------------- 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.limpoxe.downloads; 18 | 19 | import android.content.Context; 20 | import android.net.Uri; 21 | import android.provider.BaseColumns; 22 | 23 | public final class Downloads { 24 | 25 | private Downloads() {} 26 | 27 | public static final class Impl implements BaseColumns { 28 | private Impl() {} 29 | 30 | public static final String AUTHORITIES = "limpoxe_downloads"; 31 | 32 | /** 33 | * The permission to access the download manager 34 | */ 35 | public static final String PERMISSION_ACCESS = "android.permission.ACCESS_DOWNLOAD_MANAGER"; 36 | 37 | /** 38 | * The content:// URI to access downloads owned by the caller's UID. 39 | */ 40 | public static final Uri CONTENT_URI = 41 | Uri.parse("content://" + AUTHORITIES + "/my_downloads"); 42 | 43 | /** 44 | * The content URI for accessing all downloads across all UIDs (requires the 45 | * ACCESS_ALL_DOWNLOADS permission). 46 | */ 47 | public static final Uri ALL_DOWNLOADS_CONTENT_URI = 48 | Uri.parse("content://" + AUTHORITIES + "/all_downloads"); 49 | 50 | /** URI segment to access a publicly accessible downloaded file */ 51 | public static final String PUBLICLY_ACCESSIBLE_DOWNLOADS_URI_SEGMENT = "public_downloads"; 52 | 53 | /** 54 | * Broadcast Action: this is sent by the download manager to the app 55 | * that had initiated a download when that download completes. The 56 | * download's content: uri is specified in the intent's data. 57 | */ 58 | public static final String ACTION_DOWNLOAD_COMPLETED = 59 | "com.limpoxe.downloads.action.DOWNLOAD_COMPLETED"; 60 | 61 | /** 62 | * Broadcast Action: this is sent by the download manager to the app 63 | * that had initiated a download when the user selects the notification 64 | * associated with that download. The download's content: uri is specified 65 | * in the intent's data if the click is associated with a single download, 66 | * or Downloads.CONTENT_URI if the notification is associated with 67 | * multiple downloads. 68 | * Note: this is not currently sent for downloads that have completed 69 | * successfully. 70 | */ 71 | public static final String ACTION_NOTIFICATION_CLICKED = 72 | "com.limpoxe.downloads.action.DOWNLOAD_NOTIFICATION_CLICKED"; 73 | 74 | /** 75 | * The name of the column containing the URI of the data being downloaded. 76 | *

Type: TEXT

77 | *

Owner can Init/Read

78 | */ 79 | public static final String COLUMN_URI = "uri"; 80 | 81 | /** 82 | * The name of the column containing application-specific data. 83 | *

Type: TEXT

84 | *

Owner can Init/Read/Write

85 | */ 86 | public static final String COLUMN_APP_DATA = "entity"; 87 | 88 | /** 89 | * The name of the column containing the flags that indicates whether 90 | * the initiating application is capable of verifying the integrity of 91 | * the downloaded file. When this flag is set, the download manager 92 | * performs downloads and reports success even in some situations where 93 | * it can't guarantee that the download has completed (e.g. when doing 94 | * a byte-range request without an ETag, or when it can't determine 95 | * whether a download fully completed). 96 | *

Type: BOOLEAN

97 | *

Owner can Init

98 | */ 99 | public static final String COLUMN_NO_INTEGRITY = "no_integrity"; 100 | 101 | /** 102 | * The name of the column containing the filename that the initiating 103 | * application recommends. When possible, the download manager will attempt 104 | * to use this filename, or a variation, as the actual name for the file. 105 | *

Type: TEXT

106 | *

Owner can Init

107 | */ 108 | public static final String COLUMN_FILE_NAME_HINT = "hint"; 109 | 110 | /** 111 | * The name of the column containing the filename where the downloaded data 112 | * was actually stored. 113 | *

Type: TEXT

114 | *

Owner can Read

115 | */ 116 | public static final String _DATA = "_data"; 117 | 118 | /** 119 | * The name of the column containing the MIME type of the downloaded data. 120 | *

Type: TEXT

121 | *

Owner can Init/Read

122 | */ 123 | public static final String COLUMN_MIME_TYPE = "mimetype"; 124 | 125 | /** 126 | * The name of the column containing the flag that controls the destination 127 | * of the download. See the DESTINATION_* constants for a list of legal values. 128 | *

Type: INTEGER

129 | *

Owner can Init

130 | */ 131 | public static final String COLUMN_DESTINATION = "destination"; 132 | 133 | /** 134 | * The name of the column containing the flags that controls whether the 135 | * download is displayed by the UI. See the VISIBILITY_* constants for 136 | * a list of legal values. 137 | *

Type: INTEGER

138 | *

Owner can Init/Read/Write

139 | */ 140 | public static final String COLUMN_VISIBILITY = "visibility"; 141 | 142 | /** 143 | * The name of the column containing the current control state of the download. 144 | * Applications can write to this to control (pause/resume) the download. 145 | * the CONTROL_* constants for a list of legal values. 146 | *

Type: INTEGER

147 | *

Owner can Read

148 | */ 149 | public static final String COLUMN_CONTROL = "control"; 150 | 151 | /** 152 | * The name of the column containing the current status of the download. 153 | * Applications can read this to follow the progress of each download. See 154 | * the STATUS_* constants for a list of legal values. 155 | *

Type: INTEGER

156 | *

Owner can Read

157 | */ 158 | public static final String COLUMN_STATUS = "status"; 159 | 160 | /** 161 | * The name of the column containing the date at which some interesting 162 | * status changed in the download. Stored as a System.currentTimeMillis() 163 | * value. 164 | *

Type: BIGINT

165 | *

Owner can Read

166 | */ 167 | public static final String COLUMN_LAST_MODIFICATION = "lastmod"; 168 | 169 | /** 170 | * The name of the column containing the package name of the application 171 | * that initiating the download. The download manager will send 172 | * notifications to a component in this package when the download completes. 173 | *

Type: TEXT

174 | *

Owner can Init/Read

175 | */ 176 | public static final String COLUMN_NOTIFICATION_PACKAGE = "notificationpackage"; 177 | 178 | /** 179 | * The name of the column containing the component name of the class that 180 | * will receive notifications associated with the download. The 181 | * package/class combination is passed to 182 | * Intent.setClassName(String,String). 183 | *

Type: TEXT

184 | *

Owner can Init/Read

185 | */ 186 | public static final String COLUMN_NOTIFICATION_CLASS = "notificationclass"; 187 | 188 | /** 189 | * If extras are specified when requesting a download they will be provided in the intent that 190 | * is sent to the specified class and package when a download has finished. 191 | *

Type: TEXT

192 | *

Owner can Init

193 | */ 194 | public static final String COLUMN_NOTIFICATION_EXTRAS = "notificationextras"; 195 | 196 | /** 197 | * The name of the column contain the values of the cookie to be used for 198 | * the download. This is used directly as the value for the Cookie: HTTP 199 | * header that gets sent with the request. 200 | *

Type: TEXT

201 | *

Owner can Init

202 | */ 203 | public static final String COLUMN_COOKIE_DATA = "cookiedata"; 204 | 205 | /** 206 | * The name of the column containing the user agent that the initiating 207 | * application wants the download manager to use for this download. 208 | *

Type: TEXT

209 | *

Owner can Init

210 | */ 211 | public static final String COLUMN_USER_AGENT = "useragent"; 212 | 213 | /** 214 | * The name of the column containing the referer (sic) that the initiating 215 | * application wants the download manager to use for this download. 216 | *

Type: TEXT

217 | *

Owner can Init

218 | */ 219 | public static final String COLUMN_REFERER = "referer"; 220 | 221 | /** 222 | * The name of the column containing the total size of the file being 223 | * downloaded. 224 | *

Type: INTEGER

225 | *

Owner can Read

226 | */ 227 | public static final String COLUMN_TOTAL_BYTES = "total_bytes"; 228 | 229 | /** 230 | * The name of the column containing the size of the part of the file that 231 | * has been downloaded so far. 232 | *

Type: INTEGER

233 | *

Owner can Read

234 | */ 235 | public static final String COLUMN_CURRENT_BYTES = "current_bytes"; 236 | 237 | /** 238 | * The name of the column where the initiating application can provide the 239 | * UID of another application that is allowed to access this download. If 240 | * multiple applications share the same UID, all those applications will be 241 | * allowed to access this download. This column can be updated after the 242 | * download is initiated. This requires the permission 243 | * android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED. 244 | *

Type: INTEGER

245 | *

Owner can Init

246 | */ 247 | public static final String COLUMN_OTHER_UID = "otheruid"; 248 | 249 | /** 250 | * The name of the column where the initiating application can provided the 251 | * title of this download. The title will be displayed ito the user in the 252 | * list of downloads. 253 | *

Type: TEXT

254 | *

Owner can Init/Read/Write

255 | */ 256 | public static final String COLUMN_TITLE = "title"; 257 | 258 | /** 259 | * The name of the column where the initiating application can provide the 260 | * description of this download. The description will be displayed to the 261 | * user in the list of downloads. 262 | *

Type: TEXT

263 | *

Owner can Init/Read/Write

264 | */ 265 | public static final String COLUMN_DESCRIPTION = "description"; 266 | 267 | /** 268 | * The name of the column indicating whether the download was requesting through the public 269 | * API. This controls some differences in behavior. 270 | *

Type: BOOLEAN

271 | *

Owner can Init/Read

272 | */ 273 | public static final String COLUMN_IS_PUBLIC_API = "is_public_api"; 274 | 275 | /** 276 | * The name of the column holding a bitmask of allowed network types. This is only used for 277 | * public API downloads. 278 | *

Type: INTEGER

279 | *

Owner can Init/Read

280 | */ 281 | public static final String COLUMN_ALLOWED_NETWORK_TYPES = "allowed_network_types"; 282 | 283 | /** 284 | * The name of the column indicating whether roaming connections can be used. This is only 285 | * used for public API downloads. 286 | *

Type: BOOLEAN

287 | *

Owner can Init/Read

288 | */ 289 | public static final String COLUMN_ALLOW_ROAMING = "allow_roaming"; 290 | 291 | /** 292 | * The name of the column indicating whether metered connections can be used. This is only 293 | * used for public API downloads. 294 | *

Type: BOOLEAN

295 | *

Owner can Init/Read

296 | */ 297 | public static final String COLUMN_ALLOW_METERED = "allow_metered"; 298 | 299 | /** 300 | * Whether or not this download should be displayed in the system's Downloads UI. Defaults 301 | * to true. 302 | *

Type: INTEGER

303 | *

Owner can Init/Read

304 | */ 305 | public static final String COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI = "is_visible_in_downloads_ui"; 306 | 307 | /** 308 | * If true, the user has confirmed that this download can proceed over the mobile network 309 | * even though it exceeds the recommended maximum size. 310 | *

Type: BOOLEAN

311 | */ 312 | public static final String COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT = 313 | "bypass_recommended_size_limit"; 314 | 315 | /** 316 | * Set to true if this download is deleted. It is completely removed from the database 317 | * when MediaProvider database also deletes the metadata asociated with this downloaded file. 318 | *

Type: BOOLEAN

319 | *

Owner can Read

320 | */ 321 | public static final String COLUMN_DELETED = "deleted"; 322 | 323 | /** 324 | * The URI to the corresponding entry in MediaProvider for this downloaded entry. It is 325 | * used to delete the entries from MediaProvider database when it is deleted from the 326 | * downloaded list. 327 | *

Type: TEXT

328 | *

Owner can Read

329 | */ 330 | public static final String COLUMN_MEDIAPROVIDER_URI = "mediaprovider_uri"; 331 | 332 | /** 333 | * The column that is used to remember whether the media scanner was invoked. 334 | * It can take the values: null or 0(not scanned), 1(scanned), 2 (not scannable). 335 | *

Type: TEXT

336 | */ 337 | public static final String COLUMN_MEDIA_SCANNED = "scanned"; 338 | 339 | /** 340 | * The column with errorMsg for a failed downloaded. 341 | * Used only for debugging purposes. 342 | *

Type: TEXT

343 | */ 344 | public static final String COLUMN_ERROR_MSG = "errorMsg"; 345 | 346 | /** 347 | * This column stores the source of the last update to this row. 348 | * This column is only for internal use. 349 | * Valid values are indicated by LAST_UPDATESRC_* constants. 350 | *

Type: INT

351 | */ 352 | public static final String COLUMN_LAST_UPDATESRC = "lastUpdateSrc"; 353 | 354 | /** The column that is used to count retries */ 355 | public static final String COLUMN_FAILED_CONNECTIONS = "numfailed"; 356 | 357 | public static final String COLUMN_ALLOW_WRITE = "allow_write"; 358 | 359 | /** 360 | * default value for {@link #COLUMN_LAST_UPDATESRC}. 361 | * This value is used when this column's value is not relevant. 362 | */ 363 | public static final int LAST_UPDATESRC_NOT_RELEVANT = 0; 364 | 365 | /** 366 | * One of the values taken by {@link #COLUMN_LAST_UPDATESRC}. 367 | * This value is used when the update is NOT to be relayed to the DownloadService 368 | * (and thus spare DownloadService from scanning the database when this change occurs) 369 | */ 370 | public static final int LAST_UPDATESRC_DONT_NOTIFY_DOWNLOADSVC = 1; 371 | 372 | /* 373 | * Lists the destinations that an application can specify for a download. 374 | */ 375 | 376 | /** 377 | * This download will be saved to the external storage. This is the 378 | * default behavior, and should be used for any file that the user 379 | * can freely access, copy, delete. Even with that destination, 380 | * unencrypted DRM files are saved in secure internal storage. 381 | * Downloads to the external destination only write files for which 382 | * there is a registered handler. The resulting files are accessible 383 | * by filename to all applications. 384 | */ 385 | public static final int DESTINATION_EXTERNAL = 0; 386 | 387 | /** 388 | * This download will be saved to the download manager's private 389 | * partition. This is the behavior used by applications that want to 390 | * download private files that are used and deleted soon after they 391 | * get downloaded. All file types are allowed, and only the initiating 392 | * application can access the file (indirectly through a content 393 | * provider). This requires the 394 | * android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED permission. 395 | */ 396 | public static final int DESTINATION_CACHE_PARTITION = 1; 397 | 398 | /** 399 | * This download will be saved to the download manager's private 400 | * partition and will be purged as necessary to make space. This is 401 | * for private files (similar to CACHE_PARTITION) that aren't deleted 402 | * immediately after they are used, and are kept around by the download 403 | * manager as long as space is available. 404 | */ 405 | public static final int DESTINATION_CACHE_PARTITION_PURGEABLE = 2; 406 | 407 | /** 408 | * This download will be saved to the download manager's private 409 | * partition, as with DESTINATION_CACHE_PARTITION, but the download 410 | * will not proceed if the user is on a roaming data connection. 411 | */ 412 | public static final int DESTINATION_CACHE_PARTITION_NOROAMING = 3; 413 | 414 | /** 415 | * This download will be saved to the location given by the file URI in 416 | * {@link #COLUMN_FILE_NAME_HINT}. 417 | */ 418 | public static final int DESTINATION_FILE_URI = 4; 419 | 420 | /** 421 | * This download was completed by the caller (i.e., NOT downloadmanager) 422 | * and caller wants to have this download displayed in Downloads App. 423 | */ 424 | public static final int DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD = 6; 425 | 426 | /** 427 | * This download is allowed to run. 428 | */ 429 | public static final int CONTROL_RUN = 0; 430 | 431 | /** 432 | * This download must pause at the first opportunity. 433 | */ 434 | public static final int CONTROL_PAUSED = 1; 435 | 436 | /* 437 | * Lists the states that the download manager can set on a download 438 | * to notify applications of the download progress. 439 | * The codes follow the HTTP families:
440 | * 1xx: informational
441 | * 2xx: success
442 | * 3xx: redirects (not used by the download manager)
443 | * 4xx: client errors
444 | * 5xx: server errors 445 | */ 446 | 447 | /** 448 | * Returns whether the status is informational (i.e. 1xx). 449 | */ 450 | public static boolean isStatusInformational(int status) { 451 | return (status >= 100 && status < 200); 452 | } 453 | 454 | /** 455 | * Returns whether the status is a success (i.e. 2xx). 456 | */ 457 | public static boolean isStatusSuccess(int status) { 458 | return (status >= 200 && status < 300); 459 | } 460 | 461 | /** 462 | * Returns whether the status is an error (i.e. 4xx or 5xx). 463 | */ 464 | public static boolean isStatusError(int status) { 465 | return (status >= 400 && status < 600); 466 | } 467 | 468 | /** 469 | * Returns whether the status is a client error (i.e. 4xx). 470 | */ 471 | public static boolean isStatusClientError(int status) { 472 | return (status >= 400 && status < 500); 473 | } 474 | 475 | /** 476 | * Returns whether the status is a server error (i.e. 5xx). 477 | */ 478 | public static boolean isStatusServerError(int status) { 479 | return (status >= 500 && status < 600); 480 | } 481 | 482 | /** 483 | * this method determines if a notification should be displayed for a 484 | * given {@link #COLUMN_VISIBILITY} value 485 | * @param visibility the value of {@link #COLUMN_VISIBILITY}. 486 | * @return true if the notification should be displayed. false otherwise. 487 | */ 488 | public static boolean isNotificationToBeDisplayed(int visibility) { 489 | return visibility == DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED || 490 | visibility == DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION; 491 | } 492 | 493 | /** 494 | * Returns whether the download has completed (either with success or 495 | * error). 496 | */ 497 | public static boolean isStatusCompleted(int status) { 498 | return (status >= 200 && status < 300) || (status >= 400 && status < 600); 499 | } 500 | 501 | /** 502 | * This download hasn't stated yet 503 | */ 504 | public static final int STATUS_PENDING = 190; 505 | 506 | /** 507 | * This download has started 508 | */ 509 | public static final int STATUS_RUNNING = 192; 510 | 511 | /** 512 | * This download has been paused by the owning app. 513 | */ 514 | public static final int STATUS_PAUSED_BY_APP = 193; 515 | 516 | /** 517 | * This download encountered some network error and is waiting before retrying the request. 518 | */ 519 | public static final int STATUS_WAITING_TO_RETRY = 194; 520 | 521 | /** 522 | * This download is waiting for network connectivity to proceed. 523 | */ 524 | public static final int STATUS_WAITING_FOR_NETWORK = 195; 525 | 526 | /** 527 | * This download exceeded a size limit for mobile networks and is waiting for a Wi-Fi 528 | * connection to proceed. 529 | */ 530 | public static final int STATUS_QUEUED_FOR_WIFI = 196; 531 | 532 | /** 533 | * This download couldn't be completed due to insufficient storage 534 | * space. Typically, this is because the SD card is full. 535 | */ 536 | public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 198; 537 | 538 | /** 539 | * This download couldn't be completed because no external storage 540 | * device was found. Typically, this is because the SD card is not 541 | * mounted. 542 | */ 543 | public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 199; 544 | 545 | /** 546 | * This download has successfully completed. 547 | * Warning: there might be other status values that indicate success 548 | * in the future. 549 | * Use isSucccess() to capture the entire category. 550 | */ 551 | public static final int STATUS_SUCCESS = 200; 552 | 553 | /** 554 | * This request couldn't be parsed. This is also used when processing 555 | * requests with unknown/unsupported URI schemes. 556 | */ 557 | public static final int STATUS_BAD_REQUEST = 400; 558 | 559 | /** 560 | * This download can't be performed because the content type cannot be 561 | * handled. 562 | */ 563 | public static final int STATUS_NOT_ACCEPTABLE = 406; 564 | 565 | /** 566 | * This download cannot be performed because the length cannot be 567 | * determined accurately. This is the code for the HTTP error "Length 568 | * Required", which is typically used when making requests that require 569 | * a content length but don't have one, and it is also used in the 570 | * client when a response is received whose length cannot be determined 571 | * accurately (therefore making it impossible to know when a download 572 | * completes). 573 | */ 574 | public static final int STATUS_LENGTH_REQUIRED = 411; 575 | 576 | /** 577 | * This download was interrupted and cannot be resumed. 578 | * This is the code for the HTTP error "Precondition Failed", and it is 579 | * also used in situations where the client doesn't have an ETag at all. 580 | */ 581 | public static final int STATUS_PRECONDITION_FAILED = 412; 582 | 583 | /** 584 | * The lowest-valued error status that is not an actual HTTP status code. 585 | */ 586 | public static final int MIN_ARTIFICIAL_ERROR_STATUS = 488; 587 | 588 | /** 589 | * The requested destination file already exists. 590 | */ 591 | public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488; 592 | 593 | /** 594 | * Some possibly transient error occurred, but we can't resume the download. 595 | */ 596 | public static final int STATUS_CANNOT_RESUME = 489; 597 | 598 | /** 599 | * This download was canceled 600 | */ 601 | public static final int STATUS_CANCELED = 490; 602 | 603 | /** 604 | * This download has completed with an error. 605 | * Warning: there will be other status values that indicate errors in 606 | * the future. Use isStatusError() to capture the entire category. 607 | */ 608 | public static final int STATUS_UNKNOWN_ERROR = 491; 609 | 610 | /** 611 | * This download couldn't be completed because of a storage issue. 612 | * Typically, that's because the filesystem is missing or full. 613 | * Use the more specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR} 614 | * and {@link #STATUS_DEVICE_NOT_FOUND_ERROR} when appropriate. 615 | */ 616 | public static final int STATUS_FILE_ERROR = 492; 617 | 618 | /** 619 | * This download couldn't be completed because of an HTTP 620 | * redirect response that the download manager couldn't 621 | * handle. 622 | */ 623 | public static final int STATUS_UNHANDLED_REDIRECT = 493; 624 | 625 | /** 626 | * This download couldn't be completed because of an 627 | * unspecified unhandled HTTP code. 628 | */ 629 | public static final int STATUS_UNHANDLED_HTTP_CODE = 494; 630 | 631 | /** 632 | * This download couldn't be completed because of an 633 | * error receiving or processing data at the HTTP level. 634 | */ 635 | public static final int STATUS_HTTP_DATA_ERROR = 495; 636 | 637 | /** 638 | * This download couldn't be completed because of an 639 | * HttpException while setting up the request. 640 | */ 641 | public static final int STATUS_HTTP_EXCEPTION = 496; 642 | 643 | /** 644 | * This download couldn't be completed because there were 645 | * too many redirects. 646 | */ 647 | public static final int STATUS_TOO_MANY_REDIRECTS = 497; 648 | 649 | /** 650 | * This download has failed because requesting application has been 651 | * blocked by NetworkPolicyManager. 652 | * 653 | * @hide 654 | * @deprecated since behavior now uses 655 | * {@link #STATUS_WAITING_FOR_NETWORK} 656 | */ 657 | @Deprecated 658 | public static final int STATUS_BLOCKED = 498; 659 | 660 | /** {@hide} */ 661 | public static String statusToString(int status) { 662 | switch (status) { 663 | case STATUS_PENDING: return "PENDING"; 664 | case STATUS_RUNNING: return "RUNNING"; 665 | case STATUS_PAUSED_BY_APP: return "PAUSED_BY_APP"; 666 | case STATUS_WAITING_TO_RETRY: return "WAITING_TO_RETRY"; 667 | case STATUS_WAITING_FOR_NETWORK: return "WAITING_FOR_NETWORK"; 668 | case STATUS_QUEUED_FOR_WIFI: return "QUEUED_FOR_WIFI"; 669 | case STATUS_INSUFFICIENT_SPACE_ERROR: return "INSUFFICIENT_SPACE_ERROR"; 670 | case STATUS_DEVICE_NOT_FOUND_ERROR: return "DEVICE_NOT_FOUND_ERROR"; 671 | case STATUS_SUCCESS: return "SUCCESS"; 672 | case STATUS_BAD_REQUEST: return "BAD_REQUEST"; 673 | case STATUS_NOT_ACCEPTABLE: return "NOT_ACCEPTABLE"; 674 | case STATUS_LENGTH_REQUIRED: return "LENGTH_REQUIRED"; 675 | case STATUS_PRECONDITION_FAILED: return "PRECONDITION_FAILED"; 676 | case STATUS_FILE_ALREADY_EXISTS_ERROR: return "FILE_ALREADY_EXISTS_ERROR"; 677 | case STATUS_CANNOT_RESUME: return "CANNOT_RESUME"; 678 | case STATUS_CANCELED: return "CANCELED"; 679 | case STATUS_UNKNOWN_ERROR: return "UNKNOWN_ERROR"; 680 | case STATUS_FILE_ERROR: return "FILE_ERROR"; 681 | case STATUS_UNHANDLED_REDIRECT: return "UNHANDLED_REDIRECT"; 682 | case STATUS_UNHANDLED_HTTP_CODE: return "UNHANDLED_HTTP_CODE"; 683 | case STATUS_HTTP_DATA_ERROR: return "HTTP_DATA_ERROR"; 684 | case STATUS_HTTP_EXCEPTION: return "HTTP_EXCEPTION"; 685 | case STATUS_TOO_MANY_REDIRECTS: return "TOO_MANY_REDIRECTS"; 686 | case STATUS_BLOCKED: return "BLOCKED"; 687 | default: return Integer.toString(status); 688 | } 689 | } 690 | 691 | /** 692 | * This download is visible but only shows in the notifications 693 | * while it's in progress. 694 | */ 695 | public static final int VISIBILITY_VISIBLE = DownloadManager.Request.VISIBILITY_VISIBLE; 696 | 697 | /** 698 | * This download is visible and shows in the notifications while 699 | * in progress and after completion. 700 | */ 701 | public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 702 | DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED; 703 | 704 | /** 705 | * This download doesn't show in the UI or in the notifications. 706 | */ 707 | public static final int VISIBILITY_HIDDEN = DownloadManager.Request.VISIBILITY_HIDDEN; 708 | 709 | /** 710 | * Constants related to HTTP request headers associated with each download. 711 | */ 712 | public static class RequestHeaders { 713 | public static final String HEADERS_DB_TABLE = "request_headers"; 714 | public static final String COLUMN_DOWNLOAD_ID = "download_id"; 715 | public static final String COLUMN_HEADER = "header"; 716 | public static final String COLUMN_VALUE = "value"; 717 | 718 | /** 719 | * Path segment to add to a download URI to retrieve request headers 720 | */ 721 | public static final String URI_SEGMENT = "headers"; 722 | 723 | /** 724 | * Prefix for ContentValues keys that contain HTTP header lines, to be passed to 725 | * DownloadProvider.insert(). 726 | */ 727 | public static final String INSERT_KEY_PREFIX = "http_header_"; 728 | } 729 | } 730 | 731 | /** 732 | * Query where clause for general querying. 733 | */ 734 | private static final String QUERY_WHERE_CLAUSE = Impl.COLUMN_NOTIFICATION_PACKAGE + "=? AND " 735 | + Impl.COLUMN_NOTIFICATION_CLASS + "=?"; 736 | 737 | /** 738 | * Delete all the downloads for a package/class pair. 739 | */ 740 | public static final void removeAllDownloadsByPackage( 741 | Context context, String notification_package, String notification_class) { 742 | context.getContentResolver().delete(Impl.CONTENT_URI, QUERY_WHERE_CLAUSE, 743 | new String[] { notification_package, notification_class }); 744 | } 745 | } 746 | -------------------------------------------------------------------------------- /DownloadManager/src/main/java/com/limpoxe/downloads/Helpers.java: -------------------------------------------------------------------------------- 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.limpoxe.downloads; 18 | 19 | import android.content.Context; 20 | import android.net.Uri; 21 | import android.os.Environment; 22 | import android.os.SystemClock; 23 | import android.util.Log; 24 | import android.webkit.MimeTypeMap; 25 | 26 | import java.io.File; 27 | import java.io.IOException; 28 | import java.util.Random; 29 | import java.util.Set; 30 | import java.util.regex.Matcher; 31 | import java.util.regex.Pattern; 32 | 33 | /** 34 | * Some helper functions for the download manager 35 | */ 36 | public class Helpers { 37 | public static Random sRandom = new Random(SystemClock.uptimeMillis()); 38 | 39 | /** Regex used to parse content-disposition headers */ 40 | private static final Pattern CONTENT_DISPOSITION_PATTERN = 41 | Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); 42 | 43 | private static final Object sUniqueLock = new Object(); 44 | 45 | private Helpers() { 46 | } 47 | 48 | /* 49 | * Parse the Content-Disposition HTTP Header. The format of the header 50 | * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html 51 | * This header provides a filename for content that is going to be 52 | * downloaded to the file system. We only support the attachment type. 53 | */ 54 | private static String parseContentDisposition(String contentDisposition) { 55 | try { 56 | Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); 57 | if (m.find()) { 58 | return m.group(1); 59 | } 60 | } catch (IllegalStateException ex) { 61 | // This function is defined as returning null when it can't parse the header 62 | } 63 | return null; 64 | } 65 | 66 | /** 67 | * Creates a filename (where the file should be saved) from info about a download. 68 | * This file will be touched to reserve it. 69 | */ 70 | static String generateSaveFile(Context context, String url, String hint, 71 | String contentDisposition, String contentLocation, String mimeType, int destination) 72 | throws IOException { 73 | 74 | final File parent; 75 | final File[] parentTest; 76 | String name = null; 77 | 78 | if (destination == Downloads.Impl.DESTINATION_FILE_URI) { 79 | final File file = new File(Uri.parse(hint).getPath()); 80 | parent = file.getParentFile().getAbsoluteFile(); 81 | parentTest = new File[] { parent }; 82 | name = file.getName(); 83 | } else { 84 | parent = getRunningDestinationDirectory(context, destination); 85 | parentTest = new File[] { 86 | parent, 87 | getSuccessDestinationDirectory(context, destination) 88 | }; 89 | name = chooseFilename(url, hint, contentDisposition, contentLocation); 90 | } 91 | 92 | // Ensure target directories are ready 93 | for (File test : parentTest) { 94 | if (!(test.isDirectory() || test.mkdirs())) { 95 | throw new IOException("Failed to create parent for " + test); 96 | } 97 | } 98 | 99 | final String prefix; 100 | final String suffix; 101 | final int dotIndex = name.lastIndexOf('.'); 102 | final boolean missingExtension = dotIndex < 0; 103 | if (destination == Downloads.Impl.DESTINATION_FILE_URI) { 104 | // Destination is explicitly set - do not change the extension 105 | if (missingExtension) { 106 | prefix = name; 107 | suffix = ""; 108 | } else { 109 | prefix = name.substring(0, dotIndex); 110 | suffix = name.substring(dotIndex); 111 | } 112 | } else { 113 | // Split filename between base and extension 114 | // Add an extension if filename does not have one 115 | if (missingExtension) { 116 | prefix = name; 117 | suffix = chooseExtensionFromMimeType(mimeType, true); 118 | } else { 119 | prefix = name.substring(0, dotIndex); 120 | suffix = chooseExtensionFromFilename(mimeType, destination, name, dotIndex); 121 | } 122 | } 123 | 124 | synchronized (sUniqueLock) { 125 | name = generateAvailableFilenameLocked(parentTest, prefix, suffix); 126 | 127 | // Claim this filename inside lock to prevent other threads from 128 | // clobbering us. We're not paranoid enough to use O_EXCL. 129 | final File file = new File(parent, name); 130 | file.createNewFile(); 131 | return file.getAbsolutePath(); 132 | } 133 | } 134 | 135 | private static String chooseFilename(String url, String hint, String contentDisposition, 136 | String contentLocation) { 137 | String filename = null; 138 | 139 | // First, try to use the hint from the application, if there's one 140 | if (filename == null && hint != null && !hint.endsWith("/")) { 141 | if (Constants.LOGVV) { 142 | Log.v(Constants.TAG, "getting filename from hint"); 143 | } 144 | int index = hint.lastIndexOf('/') + 1; 145 | if (index > 0) { 146 | filename = hint.substring(index); 147 | } else { 148 | filename = hint; 149 | } 150 | } 151 | 152 | // If we couldn't do anything with the hint, move toward the content disposition 153 | if (filename == null && contentDisposition != null) { 154 | filename = parseContentDisposition(contentDisposition); 155 | if (filename != null) { 156 | if (Constants.LOGVV) { 157 | Log.v(Constants.TAG, "getting filename from content-disposition"); 158 | } 159 | int index = filename.lastIndexOf('/') + 1; 160 | if (index > 0) { 161 | filename = filename.substring(index); 162 | } 163 | } 164 | } 165 | 166 | // If we still have nothing at this point, try the content location 167 | if (filename == null && contentLocation != null) { 168 | String decodedContentLocation = Uri.decode(contentLocation); 169 | if (decodedContentLocation != null 170 | && !decodedContentLocation.endsWith("/") 171 | && decodedContentLocation.indexOf('?') < 0) { 172 | if (Constants.LOGVV) { 173 | Log.v(Constants.TAG, "getting filename from content-location"); 174 | } 175 | int index = decodedContentLocation.lastIndexOf('/') + 1; 176 | if (index > 0) { 177 | filename = decodedContentLocation.substring(index); 178 | } else { 179 | filename = decodedContentLocation; 180 | } 181 | } 182 | } 183 | 184 | // If all the other http-related approaches failed, use the plain uri 185 | if (filename == null) { 186 | String decodedUrl = Uri.decode(url); 187 | if (decodedUrl != null 188 | && !decodedUrl.endsWith("/") && decodedUrl.indexOf('?') < 0) { 189 | int index = decodedUrl.lastIndexOf('/') + 1; 190 | if (index > 0) { 191 | if (Constants.LOGVV) { 192 | Log.v(Constants.TAG, "getting filename from uri"); 193 | } 194 | filename = decodedUrl.substring(index); 195 | } 196 | } 197 | } 198 | 199 | // Finally, if couldn't get filename from URI, get a generic filename 200 | if (filename == null) { 201 | if (Constants.LOGVV) { 202 | Log.v(Constants.TAG, "using default filename"); 203 | } 204 | filename = Constants.DEFAULT_DL_FILENAME; 205 | } 206 | 207 | // The VFAT file system is assumed as target for downloads. 208 | // Replace invalid characters according to the specifications of VFAT. 209 | filename = StorageUtils.buildValidFatFilename(filename); 210 | 211 | return filename; 212 | } 213 | 214 | private static String chooseExtensionFromMimeType(String mimeType, boolean useDefaults) { 215 | String extension = null; 216 | if (mimeType != null) { 217 | extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); 218 | if (extension != null) { 219 | if (Constants.LOGVV) { 220 | Log.v(Constants.TAG, "adding extension from type"); 221 | } 222 | extension = "." + extension; 223 | } else { 224 | if (Constants.LOGVV) { 225 | Log.v(Constants.TAG, "couldn't find extension for " + mimeType); 226 | } 227 | } 228 | } 229 | if (extension == null) { 230 | if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) { 231 | if (mimeType.equalsIgnoreCase("text/html")) { 232 | if (Constants.LOGVV) { 233 | Log.v(Constants.TAG, "adding default html extension"); 234 | } 235 | extension = Constants.DEFAULT_DL_HTML_EXTENSION; 236 | } else if (useDefaults) { 237 | if (Constants.LOGVV) { 238 | Log.v(Constants.TAG, "adding default text extension"); 239 | } 240 | extension = Constants.DEFAULT_DL_TEXT_EXTENSION; 241 | } 242 | } else if (useDefaults) { 243 | if (Constants.LOGVV) { 244 | Log.v(Constants.TAG, "adding default binary extension"); 245 | } 246 | extension = Constants.DEFAULT_DL_BINARY_EXTENSION; 247 | } 248 | } 249 | return extension; 250 | } 251 | 252 | private static String chooseExtensionFromFilename(String mimeType, int destination, 253 | String filename, int lastDotIndex) { 254 | String extension = null; 255 | if (mimeType != null) { 256 | // Compare the last segment of the extension against the mime type. 257 | // If there's a mismatch, discard the entire extension. 258 | String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension( 259 | filename.substring(lastDotIndex + 1)); 260 | if (typeFromExt == null || !typeFromExt.equalsIgnoreCase(mimeType)) { 261 | extension = chooseExtensionFromMimeType(mimeType, false); 262 | if (extension != null) { 263 | if (Constants.LOGVV) { 264 | Log.v(Constants.TAG, "substituting extension from type"); 265 | } 266 | } else { 267 | if (Constants.LOGVV) { 268 | Log.v(Constants.TAG, "couldn't find extension for " + mimeType); 269 | } 270 | } 271 | } 272 | } 273 | if (extension == null) { 274 | if (Constants.LOGVV) { 275 | Log.v(Constants.TAG, "keeping extension"); 276 | } 277 | extension = filename.substring(lastDotIndex); 278 | } 279 | return extension; 280 | } 281 | 282 | private static boolean isFilenameAvailableLocked(File[] parents, String name) { 283 | if (Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(name)) return false; 284 | 285 | for (File parent : parents) { 286 | if (new File(parent, name).exists()) { 287 | return false; 288 | } 289 | } 290 | 291 | return true; 292 | } 293 | 294 | private static String generateAvailableFilenameLocked( 295 | File[] parents, String prefix, String suffix) throws IOException { 296 | String name = prefix + suffix; 297 | if (isFilenameAvailableLocked(parents, name)) { 298 | return name; 299 | } 300 | 301 | /* 302 | * This number is used to generate partially randomized filenames to avoid 303 | * collisions. 304 | * It starts at 1. 305 | * The next 9 iterations increment it by 1 at a time (up to 10). 306 | * The next 9 iterations increment it by 1 to 10 (random) at a time. 307 | * The next 9 iterations increment it by 1 to 100 (random) at a time. 308 | * ... Up to the point where it increases by 100000000 at a time. 309 | * (the maximum value that can be reached is 1000000000) 310 | * As soon as a number is reached that generates a filename that doesn't exist, 311 | * that filename is used. 312 | * If the filename coming in is [base].[ext], the generated filenames are 313 | * [base]-[sequence].[ext]. 314 | */ 315 | int sequence = 1; 316 | for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) { 317 | for (int iteration = 0; iteration < 9; ++iteration) { 318 | name = prefix + Constants.FILENAME_SEQUENCE_SEPARATOR + sequence + suffix; 319 | if (isFilenameAvailableLocked(parents, name)) { 320 | return name; 321 | } 322 | sequence += sRandom.nextInt(magnitude) + 1; 323 | } 324 | } 325 | 326 | throw new IOException("Failed to generate an available filename"); 327 | } 328 | 329 | public static File getRunningDestinationDirectory(Context context, int destination) 330 | throws IOException { 331 | return getDestinationDirectory(context, destination, true); 332 | } 333 | 334 | public static File getSuccessDestinationDirectory(Context context, int destination) 335 | throws IOException { 336 | return getDestinationDirectory(context, destination, false); 337 | } 338 | 339 | private static File getDestinationDirectory(Context context, int destination, boolean running) 340 | throws IOException { 341 | switch (destination) { 342 | case Downloads.Impl.DESTINATION_CACHE_PARTITION: 343 | case Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE: 344 | case Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING: 345 | if (running) { 346 | return context.getFilesDir(); 347 | } else { 348 | return context.getCacheDir(); 349 | } 350 | case Downloads.Impl.DESTINATION_EXTERNAL: 351 | final File target = new File( 352 | Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS); 353 | if (!target.isDirectory() && target.mkdirs()) { 354 | throw new IOException("unable to create external downloads directory"); 355 | } 356 | return target; 357 | 358 | default: 359 | throw new IllegalStateException("unexpected destination: " + destination); 360 | } 361 | } 362 | 363 | /** 364 | * Checks whether this looks like a legitimate selection parameter 365 | */ 366 | public static void validateSelection(String selection, Set allowedColumns) { 367 | try { 368 | if (selection == null || selection.isEmpty()) { 369 | return; 370 | } 371 | Lexer lexer = new Lexer(selection, allowedColumns); 372 | parseExpression(lexer); 373 | if (lexer.currentToken() != Lexer.TOKEN_END) { 374 | throw new IllegalArgumentException("syntax error"); 375 | } 376 | } catch (RuntimeException ex) { 377 | if (Constants.LOGV) { 378 | Log.d(Constants.TAG, "invalid selection [" + selection + "] triggered " + ex); 379 | } else if (false) { 380 | Log.d(Constants.TAG, "invalid selection triggered " + ex); 381 | } 382 | throw ex; 383 | } 384 | 385 | } 386 | 387 | // expression <- ( expression ) | statement [AND_OR ( expression ) | statement] * 388 | // | statement [AND_OR expression]* 389 | private static void parseExpression(Lexer lexer) { 390 | for (;;) { 391 | // ( expression ) 392 | if (lexer.currentToken() == Lexer.TOKEN_OPEN_PAREN) { 393 | lexer.advance(); 394 | parseExpression(lexer); 395 | if (lexer.currentToken() != Lexer.TOKEN_CLOSE_PAREN) { 396 | throw new IllegalArgumentException("syntax error, unmatched parenthese"); 397 | } 398 | lexer.advance(); 399 | } else { 400 | // statement 401 | parseStatement(lexer); 402 | } 403 | if (lexer.currentToken() != Lexer.TOKEN_AND_OR) { 404 | break; 405 | } 406 | lexer.advance(); 407 | } 408 | } 409 | 410 | // statement <- COLUMN COMPARE VALUE 411 | // | COLUMN IS NULL 412 | private static void parseStatement(Lexer lexer) { 413 | // both possibilities start with COLUMN 414 | if (lexer.currentToken() != Lexer.TOKEN_COLUMN) { 415 | throw new IllegalArgumentException("syntax error, expected column name"); 416 | } 417 | lexer.advance(); 418 | 419 | // statement <- COLUMN COMPARE VALUE 420 | if (lexer.currentToken() == Lexer.TOKEN_COMPARE) { 421 | lexer.advance(); 422 | if (lexer.currentToken() != Lexer.TOKEN_VALUE) { 423 | throw new IllegalArgumentException("syntax error, expected quoted string"); 424 | } 425 | lexer.advance(); 426 | return; 427 | } 428 | 429 | // statement <- COLUMN IS NULL 430 | if (lexer.currentToken() == Lexer.TOKEN_IS) { 431 | lexer.advance(); 432 | if (lexer.currentToken() != Lexer.TOKEN_NULL) { 433 | throw new IllegalArgumentException("syntax error, expected NULL"); 434 | } 435 | lexer.advance(); 436 | return; 437 | } 438 | 439 | // didn't get anything good after COLUMN 440 | throw new IllegalArgumentException("syntax error after column name"); 441 | } 442 | 443 | /** 444 | * A simple lexer that recognizes the words of our restricted subset of SQL where clauses 445 | */ 446 | private static class Lexer { 447 | public static final int TOKEN_START = 0; 448 | public static final int TOKEN_OPEN_PAREN = 1; 449 | public static final int TOKEN_CLOSE_PAREN = 2; 450 | public static final int TOKEN_AND_OR = 3; 451 | public static final int TOKEN_COLUMN = 4; 452 | public static final int TOKEN_COMPARE = 5; 453 | public static final int TOKEN_VALUE = 6; 454 | public static final int TOKEN_IS = 7; 455 | public static final int TOKEN_NULL = 8; 456 | public static final int TOKEN_END = 9; 457 | 458 | private final String mSelection; 459 | private final Set mAllowedColumns; 460 | private int mOffset = 0; 461 | private int mCurrentToken = TOKEN_START; 462 | private final char[] mChars; 463 | 464 | public Lexer(String selection, Set allowedColumns) { 465 | mSelection = selection; 466 | mAllowedColumns = allowedColumns; 467 | mChars = new char[mSelection.length()]; 468 | mSelection.getChars(0, mChars.length, mChars, 0); 469 | advance(); 470 | } 471 | 472 | public int currentToken() { 473 | return mCurrentToken; 474 | } 475 | 476 | public void advance() { 477 | char[] chars = mChars; 478 | 479 | // consume whitespace 480 | while (mOffset < chars.length && chars[mOffset] == ' ') { 481 | ++mOffset; 482 | } 483 | 484 | // end of input 485 | if (mOffset == chars.length) { 486 | mCurrentToken = TOKEN_END; 487 | return; 488 | } 489 | 490 | // "(" 491 | if (chars[mOffset] == '(') { 492 | ++mOffset; 493 | mCurrentToken = TOKEN_OPEN_PAREN; 494 | return; 495 | } 496 | 497 | // ")" 498 | if (chars[mOffset] == ')') { 499 | ++mOffset; 500 | mCurrentToken = TOKEN_CLOSE_PAREN; 501 | return; 502 | } 503 | 504 | // "?" 505 | if (chars[mOffset] == '?') { 506 | ++mOffset; 507 | mCurrentToken = TOKEN_VALUE; 508 | return; 509 | } 510 | 511 | // "=" and "==" 512 | if (chars[mOffset] == '=') { 513 | ++mOffset; 514 | mCurrentToken = TOKEN_COMPARE; 515 | if (mOffset < chars.length && chars[mOffset] == '=') { 516 | ++mOffset; 517 | } 518 | return; 519 | } 520 | 521 | // ">" and ">=" 522 | if (chars[mOffset] == '>') { 523 | ++mOffset; 524 | mCurrentToken = TOKEN_COMPARE; 525 | if (mOffset < chars.length && chars[mOffset] == '=') { 526 | ++mOffset; 527 | } 528 | return; 529 | } 530 | 531 | // "<", "<=" and "<>" 532 | if (chars[mOffset] == '<') { 533 | ++mOffset; 534 | mCurrentToken = TOKEN_COMPARE; 535 | if (mOffset < chars.length && (chars[mOffset] == '=' || chars[mOffset] == '>')) { 536 | ++mOffset; 537 | } 538 | return; 539 | } 540 | 541 | // "!=" 542 | if (chars[mOffset] == '!') { 543 | ++mOffset; 544 | mCurrentToken = TOKEN_COMPARE; 545 | if (mOffset < chars.length && chars[mOffset] == '=') { 546 | ++mOffset; 547 | return; 548 | } 549 | throw new IllegalArgumentException("Unexpected character after !"); 550 | } 551 | 552 | // columns and keywords 553 | // first look for anything that looks like an identifier or a keyword 554 | // and then recognize the individual words. 555 | // no attempt is made at discarding sequences of underscores with no alphanumeric 556 | // characters, even though it's not clear that they'd be legal column names. 557 | if (isIdentifierStart(chars[mOffset])) { 558 | int startOffset = mOffset; 559 | ++mOffset; 560 | while (mOffset < chars.length && isIdentifierChar(chars[mOffset])) { 561 | ++mOffset; 562 | } 563 | String word = mSelection.substring(startOffset, mOffset); 564 | if (mOffset - startOffset <= 4) { 565 | if (word.equals("IS")) { 566 | mCurrentToken = TOKEN_IS; 567 | return; 568 | } 569 | if (word.equals("OR") || word.equals("AND")) { 570 | mCurrentToken = TOKEN_AND_OR; 571 | return; 572 | } 573 | if (word.equals("NULL")) { 574 | mCurrentToken = TOKEN_NULL; 575 | return; 576 | } 577 | } 578 | if (mAllowedColumns.contains(word)) { 579 | mCurrentToken = TOKEN_COLUMN; 580 | return; 581 | } 582 | throw new IllegalArgumentException("unrecognized column or keyword"); 583 | } 584 | 585 | // quoted strings 586 | if (chars[mOffset] == '\'') { 587 | ++mOffset; 588 | while (mOffset < chars.length) { 589 | if (chars[mOffset] == '\'') { 590 | if (mOffset + 1 < chars.length && chars[mOffset + 1] == '\'') { 591 | ++mOffset; 592 | } else { 593 | break; 594 | } 595 | } 596 | ++mOffset; 597 | } 598 | if (mOffset == chars.length) { 599 | throw new IllegalArgumentException("unterminated string"); 600 | } 601 | ++mOffset; 602 | mCurrentToken = TOKEN_VALUE; 603 | return; 604 | } 605 | 606 | // anything we don't recognize 607 | throw new IllegalArgumentException("illegal character: " + chars[mOffset]); 608 | } 609 | 610 | private static final boolean isIdentifierStart(char c) { 611 | return c == '_' || 612 | (c >= 'A' && c <= 'Z') || 613 | (c >= 'a' && c <= 'z'); 614 | } 615 | 616 | private static final boolean isIdentifierChar(char c) { 617 | return c == '_' || 618 | (c >= 'A' && c <= 'Z') || 619 | (c >= 'a' && c <= 'z') || 620 | (c >= '0' && c <= '9'); 621 | } 622 | } 623 | } 624 | -------------------------------------------------------------------------------- /DownloadManager/src/main/java/com/limpoxe/downloads/PermissionChecker.java: -------------------------------------------------------------------------------- 1 | package com.limpoxe.downloads; 2 | 3 | import android.Manifest; 4 | import android.content.Context; 5 | import android.content.pm.PackageManager; 6 | import android.util.Log; 7 | 8 | import java.io.File; 9 | 10 | /** 11 | * Created by cailiming on 16/12/14. 12 | */ 13 | 14 | public class PermissionChecker { 15 | public static boolean writeExternalStoragePermission(Context context) { 16 | Log.w("PermissionChecker", "check WRITE_EXTERNAL_STORAGE"); 17 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { 18 | int permissionState = context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); 19 | if (permissionState == PackageManager.PERMISSION_GRANTED) { 20 | return true; 21 | } 22 | } 23 | return false; 24 | } 25 | 26 | public static boolean isFileCanDelate(Context context, File file) { 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /DownloadManager/src/main/java/com/limpoxe/downloads/StopRequestException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2010 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 | package com.limpoxe.downloads; 17 | 18 | 19 | import static com.limpoxe.downloads.Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE; 20 | import static com.limpoxe.downloads.Downloads.Impl.STATUS_UNHANDLED_REDIRECT; 21 | 22 | /** 23 | * Raised to indicate that the current request should be stopped immediately. 24 | * 25 | * Note the message passed to this exception will be logged and therefore must be guaranteed 26 | * not to contain any PII, meaning it generally can't include any information about the request 27 | * URI, headers, or destination filename. 28 | */ 29 | class StopRequestException extends Exception { 30 | private final int mFinalStatus; 31 | 32 | public StopRequestException(int finalStatus, String message) { 33 | super(message); 34 | mFinalStatus = finalStatus; 35 | } 36 | 37 | public StopRequestException(int finalStatus, Throwable t) { 38 | this(finalStatus, t.getMessage()); 39 | initCause(t); 40 | } 41 | 42 | public StopRequestException(int finalStatus, String message, Throwable t) { 43 | this(finalStatus, message); 44 | initCause(t); 45 | } 46 | 47 | public int getFinalStatus() { 48 | return mFinalStatus; 49 | } 50 | 51 | public static StopRequestException throwUnhandledHttpError(int code, String message) 52 | throws StopRequestException { 53 | final String error = "Unhandled HTTP response: " + code + " " + message; 54 | if (code >= 400 && code < 600) { 55 | throw new StopRequestException(code, error); 56 | } else if (code >= 300 && code < 400) { 57 | throw new StopRequestException(STATUS_UNHANDLED_REDIRECT, error); 58 | } else { 59 | throw new StopRequestException(STATUS_UNHANDLED_HTTP_CODE, error); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /DownloadManager/src/main/java/com/limpoxe/downloads/StorageUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 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.limpoxe.downloads; 18 | 19 | import android.content.Context; 20 | import android.content.pm.PackageManager; 21 | import android.os.Environment; 22 | import android.os.StatFs; 23 | import android.text.TextUtils; 24 | import android.util.Log; 25 | 26 | import java.io.FileDescriptor; 27 | import java.io.IOException; 28 | import java.io.UnsupportedEncodingException; 29 | import java.util.Locale; 30 | 31 | import static android.text.format.DateUtils.DAY_IN_MILLIS; 32 | 33 | /** 34 | * Utility methods for managing storage space related to 35 | * {@link DownloadManager}. 36 | */ 37 | public class StorageUtils { 38 | 39 | /** 40 | * Minimum age for a file to be considered for deletion. 41 | */ 42 | static final long MIN_DELETE_AGE = DAY_IN_MILLIS; 43 | 44 | /** 45 | * Reserved disk space to avoid filling disk. 46 | */ 47 | static final long RESERVED_BYTES = 32 * 1024 * 1024;//32MB 48 | 49 | static boolean sForceFullEviction = false; 50 | 51 | /** 52 | * Ensure that requested free space exists on the partition backing the 53 | * given {@link FileDescriptor}. If not enough space is available, it tries 54 | * freeing up space as follows: 55 | *
    56 | *
  • If backed by the data partition (including emulated external 57 | * storage), then ask {@link PackageManager} to free space from cache 58 | * directories. 59 | *
  • If backed by the cache partition, then try deleting older downloads 60 | * to free space. 61 | *
62 | */ 63 | public static void ensureAvailableSpace(Context context, FileDescriptor fd) 64 | throws IOException, StopRequestException { 65 | //TODO 66 | Log.w("StorageUtil", "todo should check space here"); 67 | } 68 | 69 | /** 70 | * Return number of available bytes on the filesystem backing the given 71 | * {@link FileDescriptor}, minus any {@link #RESERVED_BYTES} buffer. 72 | */ 73 | private static long getAvailableBytes(FileDescriptor fd) throws IOException { 74 | try { 75 | //TODO only Sdcard check?? 76 | String sdcardDir = Environment.getExternalStorageDirectory().getPath(); 77 | StatFs stat = new StatFs(sdcardDir); 78 | long bytesAvailable = 0; 79 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) { 80 | bytesAvailable = (long)stat.getBlockSizeLong() * (long)stat.getAvailableBlocksLong(); 81 | } else { 82 | bytesAvailable = (long)stat.getBlockSize() * (long)stat.getAvailableBlocks(); 83 | } 84 | return bytesAvailable - RESERVED_BYTES; 85 | } catch (Exception e) { 86 | throw new IOException("getAvailableBytes IOException"); 87 | } 88 | } 89 | 90 | public static String normalizeMimeType(String type) { 91 | if (type == null) { 92 | return null; 93 | } 94 | 95 | type = type.trim().toLowerCase(Locale.ROOT); 96 | 97 | final int semicolonIndex = type.indexOf(';'); 98 | if (semicolonIndex != -1) { 99 | type = type.substring(0, semicolonIndex); 100 | } 101 | return type; 102 | } 103 | 104 | public static String buildValidExtFilename(String name) { 105 | if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { 106 | return "(invalid)"; 107 | } 108 | final StringBuilder res = new StringBuilder(name.length()); 109 | for (int i = 0; i < name.length(); i++) { 110 | final char c = name.charAt(i); 111 | if (isValidExtFilenameChar(c)) { 112 | res.append(c); 113 | } else { 114 | res.append('_'); 115 | } 116 | } 117 | return res.toString(); 118 | } 119 | 120 | private static boolean isValidExtFilenameChar(char c) { 121 | switch (c) { 122 | case '\0': 123 | case '/': 124 | return false; 125 | default: 126 | return true; 127 | } 128 | } 129 | 130 | public static String buildValidFatFilename(String name) { 131 | if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { 132 | return "(invalid)"; 133 | } 134 | final StringBuilder res = new StringBuilder(name.length()); 135 | for (int i = 0; i < name.length(); i++) { 136 | final char c = name.charAt(i); 137 | if (isValidFatFilenameChar(c)) { 138 | res.append(c); 139 | } else { 140 | res.append('_'); 141 | } 142 | } 143 | return res.toString(); 144 | } 145 | 146 | private static boolean isValidFatFilenameChar(char c) { 147 | if ((0x00 <= c && c <= 0x1f)) { 148 | return false; 149 | } 150 | switch (c) { 151 | case '"': 152 | case '*': 153 | case '/': 154 | case ':': 155 | case '<': 156 | case '>': 157 | case '?': 158 | case '\\': 159 | case '|': 160 | case 0x7F: 161 | return false; 162 | default: 163 | return true; 164 | } 165 | } 166 | 167 | /** 168 | * Check if given filename is valid for a FAT filesystem. 169 | */ 170 | public static boolean isValidFatFilename(String name) { 171 | return (name != null) && name.equals(buildValidFatFilename(name)); 172 | } 173 | 174 | public static String trimFilename(String str, int maxBytes) throws UnsupportedEncodingException { 175 | final StringBuilder res = new StringBuilder(str); 176 | trimFilename(res, maxBytes); 177 | return res.toString(); 178 | } 179 | 180 | private static void trimFilename(StringBuilder res, int maxBytes) throws UnsupportedEncodingException { 181 | byte[] raw = res.toString().getBytes("UTF-8"); 182 | if (raw.length > maxBytes) { 183 | maxBytes -= 3; 184 | while (raw.length > maxBytes) { 185 | res.deleteCharAt(res.length() / 2); 186 | raw = res.toString().getBytes("UTF-8"); 187 | } 188 | res.insert(res.length() / 2, "..."); 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /DownloadManager/src/main/java/com/limpoxe/downloads/utils/ConnectManager.java: -------------------------------------------------------------------------------- 1 | package com.limpoxe.downloads.utils; 2 | 3 | import android.content.Context; 4 | import android.net.ConnectivityManager; 5 | import android.net.NetworkInfo; 6 | import android.util.Log; 7 | 8 | import com.limpoxe.downloads.Constants; 9 | 10 | /** 11 | * Created by cailiming on 16/12/14. 12 | */ 13 | 14 | public class ConnectManager { 15 | public static final int TYPE_NONE = -1; 16 | public static final int TYPE_MOBILE = 0; 17 | public static final int TYPE_WIFI = 1; 18 | public static final int TYPE_MOBILE_MMS = 2; 19 | public static final int TYPE_MOBILE_SUPL = 3; 20 | public static final int TYPE_MOBILE_DUN = 4; 21 | public static final int TYPE_MOBILE_HIPRI = 5; 22 | public static final int TYPE_WIMAX = 6; 23 | public static final int TYPE_BLUETOOTH = 7; 24 | public static final int TYPE_DUMMY = 8; 25 | public static final int TYPE_ETHERNET = 9; 26 | public static final int TYPE_MOBILE_FOTA = 10; 27 | public static final int TYPE_MOBILE_IMS = 11; 28 | public static final int TYPE_MOBILE_CBS = 12; 29 | public static final int TYPE_WIFI_P2P = 13; 30 | public static final int TYPE_MOBILE_IA = 14; 31 | public static final int TYPE_MOBILE_EMERGENCY = 15; 32 | public static final int TYPE_PROXY = 16; 33 | public static final int TYPE_VPN = 17; 34 | 35 | public static final int MAX_RADIO_TYPE = TYPE_VPN; 36 | public static final int MAX_NETWORK_TYPE = TYPE_VPN; 37 | 38 | public static boolean isNetworkTypeValid(int networkType) { 39 | return networkType >= 0 && networkType <= MAX_NETWORK_TYPE; 40 | } 41 | 42 | public static String getNetworkTypeName(int type) { 43 | switch (type) { 44 | case TYPE_MOBILE: 45 | return "MOBILE"; 46 | case TYPE_WIFI: 47 | return "WIFI"; 48 | case TYPE_MOBILE_MMS: 49 | return "MOBILE_MMS"; 50 | case TYPE_MOBILE_SUPL: 51 | return "MOBILE_SUPL"; 52 | case TYPE_MOBILE_DUN: 53 | return "MOBILE_DUN"; 54 | case TYPE_MOBILE_HIPRI: 55 | return "MOBILE_HIPRI"; 56 | case TYPE_WIMAX: 57 | return "WIMAX"; 58 | case TYPE_BLUETOOTH: 59 | return "BLUETOOTH"; 60 | case TYPE_DUMMY: 61 | return "DUMMY"; 62 | case TYPE_ETHERNET: 63 | return "ETHERNET"; 64 | case TYPE_MOBILE_FOTA: 65 | return "MOBILE_FOTA"; 66 | case TYPE_MOBILE_IMS: 67 | return "MOBILE_IMS"; 68 | case TYPE_MOBILE_CBS: 69 | return "MOBILE_CBS"; 70 | case TYPE_WIFI_P2P: 71 | return "WIFI_P2P"; 72 | case TYPE_MOBILE_IA: 73 | return "MOBILE_IA"; 74 | case TYPE_MOBILE_EMERGENCY: 75 | return "MOBILE_EMERGENCY"; 76 | case TYPE_PROXY: 77 | return "PROXY"; 78 | case TYPE_VPN: 79 | return "VPN"; 80 | default: 81 | return Integer.toString(type); 82 | } 83 | } 84 | 85 | public static boolean isNetworkTypeMobile(int networkType) { 86 | switch (networkType) { 87 | case TYPE_MOBILE: 88 | case TYPE_MOBILE_MMS: 89 | case TYPE_MOBILE_SUPL: 90 | case TYPE_MOBILE_DUN: 91 | case TYPE_MOBILE_HIPRI: 92 | case TYPE_MOBILE_FOTA: 93 | case TYPE_MOBILE_IMS: 94 | case TYPE_MOBILE_CBS: 95 | case TYPE_MOBILE_IA: 96 | case TYPE_MOBILE_EMERGENCY: 97 | return true; 98 | default: 99 | return false; 100 | } 101 | } 102 | 103 | public static boolean isNetworkTypeWifi(int networkType) { 104 | switch (networkType) { 105 | case TYPE_WIFI: 106 | case TYPE_WIFI_P2P: 107 | return true; 108 | default: 109 | return false; 110 | } 111 | } 112 | 113 | public static NetworkInfo getActiveNetworkInfo(Context context, int uid) { 114 | ConnectivityManager connectivity = 115 | (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 116 | if (connectivity == null) { 117 | Log.w(Constants.TAG, "couldn't get connectivity manager"); 118 | return null; 119 | } 120 | 121 | final NetworkInfo activeInfo = connectivity.getActiveNetworkInfo(); 122 | if (activeInfo == null && Constants.LOGVV) { 123 | Log.v(Constants.TAG, "network is not available"); 124 | } 125 | return activeInfo; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /DownloadManager/src/main/java/com/limpoxe/downloads/utils/GuardedBy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 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.limpoxe.downloads.utils; 18 | 19 | import java.lang.annotation.ElementType; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.RetentionPolicy; 22 | import java.lang.annotation.Target; 23 | 24 | /** 25 | * Annotation type used to mark a method or field that can only be accessed when 26 | * holding the referenced lock. 27 | */ 28 | @Target({ ElementType.FIELD, ElementType.METHOD }) 29 | @Retention(RetentionPolicy.CLASS) 30 | public @interface GuardedBy { 31 | String value(); 32 | } 33 | -------------------------------------------------------------------------------- /DownloadManager/src/main/java/com/limpoxe/downloads/utils/IoUtils.java: -------------------------------------------------------------------------------- 1 | package com.limpoxe.downloads.utils; 2 | 3 | public final class IoUtils { 4 | private IoUtils() { 5 | } 6 | 7 | /** 8 | * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null. 9 | */ 10 | public static void closeQuietly(AutoCloseable closeable) { 11 | if (closeable != null) { 12 | try { 13 | closeable.close(); 14 | } catch (RuntimeException rethrown) { 15 | throw rethrown; 16 | } catch (Exception ignored) { 17 | } 18 | } 19 | } 20 | 21 | 22 | } 23 | -------------------------------------------------------------------------------- /DownloadManager/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | DownloadManager 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Cai Liming 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android-DownloadManager 2 | Android下载管理器,从Android6.0源码中分离出来可独立使用的下载管理器, 用法和特性基本与官方API相同 3 | 4 | 移除了权限、通知栏、UI相关部分 5 | 6 | ## 如何使用 7 | 用法和特性基本与官方API相同, 尽可参考系统下载器用法 8 | 9 | ### TODO 10 | 添加更多API测试Demo -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.2" 6 | defaultConfig { 7 | applicationId "com.limpoxe.example" 8 | minSdkVersion 9 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | compile 'com.android.support:appcompat-v7:25.1.0' 24 | 25 | compile project(":DownloadManager") 26 | } 27 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/cailiming/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/limpoxe/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.limpoxe.example; 2 | 3 | import android.Manifest; 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.IntentFilter; 8 | import android.content.pm.PackageManager; 9 | import android.database.ContentObserver; 10 | import android.database.Cursor; 11 | import android.net.Uri; 12 | import android.os.Bundle; 13 | import android.os.Environment; 14 | import android.os.Handler; 15 | import android.support.annotation.NonNull; 16 | import android.support.v4.content.FileProvider; 17 | import android.support.v7.app.AppCompatActivity; 18 | import android.util.Log; 19 | import android.view.View; 20 | import android.widget.ProgressBar; 21 | import android.widget.TextView; 22 | import android.widget.Toast; 23 | 24 | import com.limpoxe.downloads.DownloadManager; 25 | 26 | import java.io.File; 27 | 28 | /** 29 | * 没用通知栏和下载列表页面、权限检查 30 | */ 31 | public class MainActivity extends AppCompatActivity implements View.OnClickListener { 32 | 33 | Handler handler; 34 | 35 | DownloadManager dw; 36 | 37 | IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE); 38 | 39 | BroadcastReceiver downloadComplte = new BroadcastReceiver() { 40 | @Override 41 | public void onReceive(Context context, Intent intent) { 42 | 43 | Log.d("MainActivity", "downloadComplte"); 44 | 45 | long did = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); 46 | 47 | //如果有多个任务在下载,只关心需要关心的任务 48 | if (did == downloadId) { 49 | //下载完成后打开图片 50 | Intent install = new Intent(Intent.ACTION_VIEW); 51 | Uri downloadFileUri = dw.getUriForDownloadedFile(downloadId); 52 | if (downloadFileUri != null) { 53 | DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloadId); 54 | Cursor c = null; 55 | try { 56 | c = dw.query(query); 57 | if (c != null && c.moveToFirst()) { 58 | String localUri = c.getString(c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI)); 59 | Log.d("MainActivity", downloadFileUri.toString()); 60 | //7.0系统不能在intent中包含file:///协议 61 | File file = new File(localUri.replace("file://", "")); 62 | Intent openIntent = new Intent(Intent.ACTION_VIEW, 63 | FileProvider.getUriForFile(MainActivity.this, 64 | context.getApplicationContext().getPackageName() + ".provider", file)); 65 | openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 66 | startActivity(openIntent); 67 | } 68 | } catch (Exception e) { 69 | e.printStackTrace(); 70 | } finally { 71 | if (c != null) { 72 | c.close(); 73 | } 74 | } 75 | } else { 76 | Log.e("MainActivity", "download error"); 77 | } 78 | } 79 | 80 | } 81 | }; 82 | 83 | long downloadId = 0; 84 | DownloadStatusObserver observer; 85 | 86 | private boolean isDownloading = false; 87 | 88 | class DownloadStatusObserver extends ContentObserver { 89 | public DownloadStatusObserver() { 90 | super(handler); 91 | } 92 | 93 | @Override 94 | public void onChange(boolean selfChange) { 95 | 96 | Log.w("MainActivity", "onChange " + downloadId); 97 | 98 | if (downloadId != 0) { 99 | int[] bytesAndStatus = getBytesAndStatus(downloadId); 100 | int currentSize = bytesAndStatus[0];//当前大小 101 | int totalSize = bytesAndStatus[1];//总大小 102 | int status = bytesAndStatus[2];//下载状态 103 | 104 | progressBar.setMax(totalSize); 105 | progressBar.setProgress(currentSize); 106 | 107 | if (status == DownloadManager.STATUS_SUCCESSFUL) { 108 | isDownloading = false; 109 | Toast.makeText(MainActivity.this, "id=" + downloadId + "下载完成", Toast.LENGTH_SHORT).show(); 110 | } 111 | textView.setText("id=" + downloadId + "下载进度:" + currentSize + "/" + totalSize); 112 | } 113 | } 114 | 115 | } 116 | 117 | private ProgressBar progressBar; 118 | private TextView textView; 119 | 120 | @Override 121 | protected void onCreate(Bundle savedInstanceState) { 122 | super.onCreate(savedInstanceState); 123 | setContentView(R.layout.activity_main); 124 | 125 | checkPermiss(); 126 | 127 | handler = new Handler(); 128 | dw = DownloadManager.getInstance(this); 129 | 130 | progressBar = (ProgressBar)findViewById(R.id.firstBar); 131 | textView = (TextView) findViewById(R.id.text); 132 | 133 | findViewById(R.id.download).setOnClickListener(this); 134 | findViewById(R.id.pause).setOnClickListener(this); 135 | findViewById(R.id.resume).setOnClickListener(this); 136 | 137 | registerReceiver(downloadComplte, filter); 138 | } 139 | 140 | private void checkPermiss() { 141 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { 142 | int permissionState = checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); 143 | if (permissionState != PackageManager.PERMISSION_GRANTED) { 144 | if (!shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { 145 | requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 10086); 146 | } else { 147 | Toast.makeText(MainActivity.this, "6.+系统, 请在设置中授权存储卡读写权限, 才能使用下载功能", Toast.LENGTH_SHORT).show(); 148 | } 149 | } 150 | } 151 | } 152 | 153 | @Override 154 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 155 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 156 | if(requestCode == 10086) { 157 | if (permissions != null && permissions.length > 0 158 | && grantResults != null && grantResults.length > 0) { 159 | if(permissions[0].equals(Manifest.permission.WRITE_EXTERNAL_STORAGE) && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 160 | Toast.makeText(MainActivity.this, "ok, let's go", Toast.LENGTH_SHORT).show(); 161 | } 162 | } 163 | } 164 | } 165 | 166 | @Override 167 | public void onClick(View v) { 168 | 169 | if (v.getId() == R.id.download) { 170 | 171 | checkPermiss(); 172 | 173 | if (isDownloading) { 174 | Toast.makeText(MainActivity.this, "心急吃不了豆腐", Toast.LENGTH_SHORT).show(); 175 | return; 176 | } 177 | 178 | DownloadManager.Request request = new DownloadManager.Request(Uri.parse("http://download.taobaocdn.com/wireless/taobao4android/latest/701483.apk")); 179 | request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE); 180 | request.setTitle("下载jpg"); 181 | request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI); 182 | request.setMimeType("image/jpeg"); 183 | 184 | //实际下载后存放的路径并不一定是这个名字,如果有重名的,自动向名字中追加数字编号 185 | File apkFile = new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS + "/701483.apk"); 186 | request.setDestinationUri(Uri.fromFile(apkFile)); 187 | 188 | downloadId = dw.enqueue(request); 189 | isDownloading = true; 190 | 191 | Uri uri = dw.getDownloadUri(downloadId); 192 | 193 | if (uri != null) { 194 | progressBar.setMax(1000); 195 | progressBar.setProgress(0); 196 | 197 | if (observer != null) { 198 | getContentResolver().unregisterContentObserver(observer); 199 | observer = null; 200 | } 201 | observer = new DownloadStatusObserver(); 202 | getContentResolver().registerContentObserver(uri, true, observer); 203 | } 204 | } else if (v.getId() == R.id.pause) { 205 | dw.pauseDownload(downloadId); 206 | } else if (v.getId() == R.id.resume) { 207 | dw.resumeDownload(downloadId); 208 | } 209 | 210 | } 211 | 212 | @Override 213 | protected void onResume() { 214 | super.onResume(); 215 | } 216 | 217 | @Override 218 | protected void onDestroy() { 219 | super.onDestroy(); 220 | unregisterReceiver(downloadComplte); 221 | if (observer != null) { 222 | getContentResolver().unregisterContentObserver(observer); 223 | observer = null; 224 | } 225 | 226 | } 227 | 228 | private int[] getBytesAndStatus(long downloadId) { 229 | int[] bytesAndStatus = new int[] { -1, -1, 0 }; 230 | DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloadId); 231 | Cursor c = null; 232 | try { 233 | c = dw.query(query); 234 | if (c != null && c.moveToFirst()) { 235 | bytesAndStatus[0] = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)); 236 | bytesAndStatus[1] = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)); 237 | bytesAndStatus[2] = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS)); 238 | } 239 | } finally { 240 | if (c != null) { 241 | c.close(); 242 | } 243 | } 244 | return bytesAndStatus; 245 | } 246 | 247 | } 248 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 18 | 19 | 24 | 25 | 33 | 34 |