|
| 1 | +## 在Android里面下载文件,并在ProgressDialog显示进度 |
| 2 | + |
| 3 | +### 问题 |
| 4 | +尝试写一个可以获得更新的应用程序。 为了达到这个效果,我写了一个可以下载文件并且在一个`ProgressDialog`里面显示进度的简单方法。我知道怎么使用`ProgressDialog`,但是我不太确定怎么显示当前进度和下载文件。 |
| 5 | + |
| 6 | +### 回答 |
| 7 | + |
| 8 | +有很多方式去下载文件。我给出一些最常用的方法;由你来选择选择哪一个最适合你的应用。 |
| 9 | + |
| 10 | +#### 1. 使用AsyncTask,并且在一个dialog里面显示进度 |
| 11 | +这种方法允许你执行一些后台任务,并且同时更新UI(在这里,我们是更新进度条progress bar)。 |
| 12 | + |
| 13 | +首先是实例代码 |
| 14 | +```java |
| 15 | +// 定义一个dialog为Activity的成员变量 |
| 16 | +ProgressDialog mProgressDialog; |
| 17 | + |
| 18 | +// 在OnCreate()方法里面初始化 |
| 19 | +mProgressDialog = new ProgressDialog(YourActivity.this); |
| 20 | +mProgressDialog.setMessage("A message"); |
| 21 | +mProgressDialog.setIndeterminate(true); |
| 22 | +mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); |
| 23 | +mProgressDialog.setCancelable(true); |
| 24 | + |
| 25 | +// 执行下载器 |
| 26 | +final DownloadTask downloadTask = new DownloadTask(YourActivity.this); |
| 27 | +downloadTask.execute("你要下载文件的Url"); |
| 28 | + |
| 29 | +mProgressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { |
| 30 | + @Override |
| 31 | + public void onCancel(DialogInterface dialog) { |
| 32 | + downloadTask.cancel(true); |
| 33 | + } |
| 34 | +}); |
| 35 | +``` |
| 36 | + |
| 37 | +`AsyncTask`看起来像这样 |
| 38 | +```java |
| 39 | +// 一般我们把AsyncTask的子类定义在Activity的内部 |
| 40 | +// 通过这种方式,我们就可以轻松地在这里更改UI线程 |
| 41 | +private class DownloadTask extends AsyncTask<String, Integer, String> { |
| 42 | + |
| 43 | + private Context context; |
| 44 | + private PowerManager.WakeLock mWakeLock; |
| 45 | + |
| 46 | + public DownloadTask(Context context) { |
| 47 | + this.context = context; |
| 48 | + } |
| 49 | + |
| 50 | + @Override |
| 51 | + protected String doInBackground(String... sUrl) { |
| 52 | + InputStream input = null; |
| 53 | + OutputStream output = null; |
| 54 | + HttpURLConnection connection = null; |
| 55 | + try { |
| 56 | + URL url = new URL(sUrl[0]); |
| 57 | + connection = (HttpURLConnection) url.openConnection(); |
| 58 | + connection.connect(); |
| 59 | + |
| 60 | + // 避免因为接收到非HTTP 200 OK状态,而导致只或者错误代码,而不是要下载的文件 |
| 61 | + if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { |
| 62 | + return "Server returned HTTP " + connection.getResponseCode() |
| 63 | + + " " + connection.getResponseMessage(); |
| 64 | + } |
| 65 | + |
| 66 | + // 这对显示下载百分比有帮助 |
| 67 | + // 当服务器没有返回文件的大小时,数字可能为-1 |
| 68 | + int fileLength = connection.getContentLength(); |
| 69 | + |
| 70 | + // 下载文件 |
| 71 | + input = connection.getInputStream(); |
| 72 | + output = new FileOutputStream("/sdcard/file_name.extension"); |
| 73 | + |
| 74 | + byte data[] = new byte[4096]; |
| 75 | + long total = 0; |
| 76 | + int count; |
| 77 | + while ((count = input.read(data)) != -1) { |
| 78 | + // 允许用返回键取消下载 |
| 79 | + if (isCancelled()) { |
| 80 | + input.close(); |
| 81 | + return null; |
| 82 | + } |
| 83 | + total += count; |
| 84 | + // 更新下载进度 |
| 85 | + if (fileLength > 0) // 只有当 fileLength>0 的时候才会调用 |
| 86 | + publishProgress((int) (total * 100 / fileLength)); |
| 87 | + output.write(data, 0, count); |
| 88 | + } |
| 89 | + } catch (Exception e) { |
| 90 | + return e.toString(); |
| 91 | + } finally { |
| 92 | + try { |
| 93 | + if (output != null) |
| 94 | + output.close(); |
| 95 | + if (input != null) |
| 96 | + input.close(); |
| 97 | + } catch (IOException ignored) { |
| 98 | + } |
| 99 | + |
| 100 | + if (connection != null) |
| 101 | + connection.disconnect(); |
| 102 | + } |
| 103 | + return null; |
| 104 | + } |
| 105 | +``` |
| 106 | + |
| 107 | +上面的`doInBackground`方法总是在后台线程中运行。你不能在这里做任何UI线程相关的任务。另一方面,`onProgressUpdate`和`onPreExecute`是在UI线程里面运行的,所以你可以在这里更改进度条。 |
| 108 | + |
| 109 | +```java |
| 110 | +@Override |
| 111 | + protected void onPreExecute() { |
| 112 | + super.onPreExecute(); |
| 113 | + // 取得CPU锁,避免因为用户在下载过程中按了电源键而导致的失效 |
| 114 | + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); |
| 115 | + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, |
| 116 | + getClass().getName()); |
| 117 | + mWakeLock.acquire(); |
| 118 | + mProgressDialog.show(); |
| 119 | + } |
| 120 | + |
| 121 | + @Override |
| 122 | + protected void onProgressUpdate(Integer... progress) { |
| 123 | + super.onProgressUpdate(progress); |
| 124 | + // 如果到了这里,文件长度是确定的,设置indeterminate为false |
| 125 | + mProgressDialog.setIndeterminate(false); |
| 126 | + mProgressDialog.setMax(100); |
| 127 | + mProgressDialog.setProgress(progress[0]); |
| 128 | + } |
| 129 | + |
| 130 | + @Override |
| 131 | + protected void onPostExecute(String result) { |
| 132 | + mWakeLock.release(); |
| 133 | + mProgressDialog.dismiss(); |
| 134 | + if (result != null) |
| 135 | + Toast.makeText(context,"Download error: "+result, Toast.LENGTH_LONG).show(); |
| 136 | + else |
| 137 | + Toast.makeText(context,"File downloaded", Toast.LENGTH_SHORT).show(); |
| 138 | + } |
| 139 | +``` |
| 140 | + |
| 141 | +为了可正常运行,你还要取得WAKE_LOCK权限 |
| 142 | +``` |
| 143 | +<uses-permission android:name="android.permission.WAKE_LOCK" /> |
| 144 | +``` |
| 145 | + |
| 146 | +#### 2. 从服务器上下载文件 |
| 147 | + |
| 148 | +这里有个最大的问题:*我怎么从service来更新我的activity?*。 |
| 149 | +在下一个例子当中我们会使用两个你可能不熟悉的类:`ResultReceiver`和`IntentService`。`ResultReceiver`是一个可以允许我们用Service来更新线程的类;`IntentService`是一个可以生成用来处理后台任务的线程的`Service`子类(你需要知道,`Service`实际上是和你的应用运行在同一个线程的;当你继承了`Service`之后,你必须手动生成一个新的线程来处理费时操作)。 |
| 150 | + |
| 151 | +一个提供下载功能的`Service`看起来像这样: |
| 152 | + |
| 153 | +```java |
| 154 | +public class DownloadService extends IntentService { |
| 155 | + public static final int UPDATE_PROGRESS = 8344; |
| 156 | + public DownloadService() { |
| 157 | + super("DownloadService"); |
| 158 | + } |
| 159 | + @Override |
| 160 | + protected void onHandleIntent(Intent intent) { |
| 161 | + String urlToDownload = intent.getStringExtra("url"); |
| 162 | + ResultReceiver receiver = (ResultReceiver) intent.getParcelableExtra("receiver"); |
| 163 | + try { |
| 164 | + URL url = new URL(urlToDownload); |
| 165 | + URLConnection connection = url.openConnection(); |
| 166 | + connection.connect(); |
| 167 | + // 这对你在进度条上面显示百分比很有用 |
| 168 | + int fileLength = connection.getContentLength(); |
| 169 | + |
| 170 | + // download the file |
| 171 | + InputStream input = new BufferedInputStream(connection.getInputStream()); |
| 172 | + OutputStream output = new FileOutputStream("/sdcard/BarcodeScanner-debug.apk"); |
| 173 | + |
| 174 | + byte data[] = new byte[1024]; |
| 175 | + long total = 0; |
| 176 | + int count; |
| 177 | + while ((count = input.read(data)) != -1) { |
| 178 | + total += count; |
| 179 | + // 更新进度条.... |
| 180 | + Bundle resultData = new Bundle(); |
| 181 | + resultData.putInt("progress" ,(int) (total * 100 / fileLength)); |
| 182 | + receiver.send(UPDATE_PROGRESS, resultData); |
| 183 | + output.write(data, 0, count); |
| 184 | + } |
| 185 | + |
| 186 | + output.flush(); |
| 187 | + output.close(); |
| 188 | + input.close(); |
| 189 | + } catch (IOException e) { |
| 190 | + e.printStackTrace(); |
| 191 | + } |
| 192 | + |
| 193 | + Bundle resultData = new Bundle(); |
| 194 | + resultData.putInt("progress" ,100); |
| 195 | + receiver.send(UPDATE_PROGRESS, resultData); |
| 196 | + } |
| 197 | +} |
| 198 | +``` |
| 199 | + |
| 200 | +把这个`Service`添加到清单文件中: |
| 201 | +``` |
| 202 | +<service android:name=".DownloadService"/> |
| 203 | +``` |
| 204 | + |
| 205 | +activity里面的代码: |
| 206 | + |
| 207 | +```java |
| 208 | +// 像第一个例子里面一样初始化ProgressBar |
| 209 | + |
| 210 | +// 在这里启动下载 |
| 211 | +mProgressDialog.show(); |
| 212 | +Intent intent = new Intent(this, DownloadService.class); |
| 213 | +intent.putExtra("url", "url of the file to download"); |
| 214 | +intent.putExtra("receiver", new DownloadReceiver(new Handler())); |
| 215 | +startService(intent); |
| 216 | +``` |
| 217 | + |
| 218 | +然后像这样来使用`ResultReceiver`: |
| 219 | + |
| 220 | +```java |
| 221 | +private class DownloadReceiver extends ResultReceiver{ |
| 222 | + public DownloadReceiver(Handler handler) { |
| 223 | + super(handler); |
| 224 | + } |
| 225 | + |
| 226 | + @Override |
| 227 | + protected void onReceiveResult(int resultCode, Bundle resultData) { |
| 228 | + super.onReceiveResult(resultCode, resultData); |
| 229 | + if (resultCode == DownloadService.UPDATE_PROGRESS) { |
| 230 | + int progress = resultData.getInt("progress"); |
| 231 | + mProgressDialog.setProgress(progress); |
| 232 | + if (progress == 100) { |
| 233 | + mProgressDialog.dismiss(); |
| 234 | + } |
| 235 | + } |
| 236 | + } |
| 237 | +} |
| 238 | +``` |
| 239 | + |
| 240 | +##### 2.1 使用Groundy库 |
| 241 | +[Groundy](http://casidiablo.github.com/groundy)是一个可以帮助你在后台服务中运行代码片段的库,它是基于`ResultReceiver`这一概念。但是这个库现在已经被标记为**过时**了(deprecated)。下面是**完整**代码的样子。 |
| 242 | + |
| 243 | +你要展示dialog的Activity: |
| 244 | + |
| 245 | +```java |
| 246 | +public class MainActivity extends Activity { |
| 247 | + |
| 248 | + private ProgressDialog mProgressDialog; |
| 249 | + |
| 250 | + @Override |
| 251 | + public void onCreate(Bundle savedInstanceState) { |
| 252 | + super.onCreate(savedInstanceState); |
| 253 | + setContentView(R.layout.main); |
| 254 | + |
| 255 | + findViewById(R.id.btn_download).setOnClickListener(new View.OnClickListener() { |
| 256 | + public void onClick(View view) { |
| 257 | + String url = ((EditText) findViewById(R.id.edit_url)).getText().toString().trim(); |
| 258 | + Bundle extras = new Bundler().add(DownloadTask.PARAM_URL, url).build(); |
| 259 | + Groundy.create(DownloadExample.this, DownloadTask.class) |
| 260 | + .receiver(mReceiver) |
| 261 | + .params(extras) |
| 262 | + .queue(); |
| 263 | + |
| 264 | + mProgressDialog = new ProgressDialog(MainActivity.this); |
| 265 | + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); |
| 266 | + mProgressDialog.setCancelable(false); |
| 267 | + mProgressDialog.show(); |
| 268 | + } |
| 269 | + }); |
| 270 | + } |
| 271 | + |
| 272 | + private ResultReceiver mReceiver = new ResultReceiver(new Handler()) { |
| 273 | + @Override |
| 274 | + protected void onReceiveResult(int resultCode, Bundle resultData) { |
| 275 | + super.onReceiveResult(resultCode, resultData); |
| 276 | + switch (resultCode) { |
| 277 | + case Groundy.STATUS_PROGRESS: |
| 278 | + mProgressDialog.setProgress(resultData.getInt(Groundy.KEY_PROGRESS)); |
| 279 | + break; |
| 280 | + case Groundy.STATUS_FINISHED: |
| 281 | + Toast.makeText(DownloadExample.this, R.string.file_downloaded, Toast.LENGTH_LONG); |
| 282 | + mProgressDialog.dismiss(); |
| 283 | + break; |
| 284 | + case Groundy.STATUS_ERROR: |
| 285 | + Toast.makeText(DownloadExample.this, resultData.getString(Groundy.KEY_ERROR), Toast.LENGTH_LONG).show(); |
| 286 | + mProgressDialog.dismiss(); |
| 287 | + break; |
| 288 | + } |
| 289 | + } |
| 290 | + }; |
| 291 | +} |
| 292 | +``` |
| 293 | + |
| 294 | +**Groundy**使用一个`GroundyTask`的实现类来下载文件和显示进度: |
| 295 | + |
| 296 | +```java |
| 297 | +public class DownloadTask extends GroundyTask { |
| 298 | + public static final String PARAM_URL = "com.groundy.sample.param.url"; |
| 299 | + |
| 300 | + @Override |
| 301 | + protected boolean doInBackground() { |
| 302 | + try { |
| 303 | + String url = getParameters().getString(PARAM_URL); |
| 304 | + File dest = new File(getContext().getFilesDir(), new File(url).getName()); |
| 305 | + DownloadUtils.downloadFile(getContext(), url, dest, DownloadUtils.getDownloadListenerForTask(this)); |
| 306 | + return true; |
| 307 | + } catch (Exception pokemon) { |
| 308 | + return false; |
| 309 | + } |
| 310 | + } |
| 311 | +} |
| 312 | +``` |
| 313 | + |
| 314 | +添加这行代码到清单文件中: |
| 315 | +``` |
| 316 | +<service android:name="com.codeslap.groundy.GroundyService"/> |
| 317 | +``` |
| 318 | + |
| 319 | +这实在是太简单了!只需要从[Github](https://github.com/casidiablo/groundy/downloads)上下载最新的jar文件就可以开始了。但是要记住,Groundy的主要用途是在后台服务中调用外部的REST API,然后更简单地在UI上更新结果。如果你要在你的应用里面做类似的事情,这个库将非常有帮助。 |
| 320 | + |
| 321 | +##### 2.2 使用[ion](https://github.com/koush/ion) |
| 322 | + |
| 323 | +#### 3. 使用`DownloadManager`类(只适用于GingerBread及其以上的系统) |
| 324 | + |
| 325 | +这个方法很酷炫,你不需要担心手动下载文件、处理线程和流之类等乱七八糟的东西。GingerBread带来一项新功能:`DownloadManager`。`DownloadManager`允许你轻松地下载文件和把复杂计算的任务委托给系统。 |
| 326 | + |
| 327 | +首先,我们来看一下工具方法: |
| 328 | + |
| 329 | +```java |
| 330 | +/** |
| 331 | + * @param 使用context来检查设备信息和 DownloadManager 的信息 |
| 332 | + * @return 如果downloadmanager可用则返回 true |
| 333 | + */ |
| 334 | +public static boolean isDownloadManagerAvailable(Context context) { |
| 335 | + |
| 336 | + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { |
| 337 | + return true; |
| 338 | + } |
| 339 | + return false; |
| 340 | +} |
| 341 | +``` |
| 342 | + |
| 343 | +方法的名字就已经告诉了我们一切,只有当你确保可以使用`DownloadManager`的时候,你才可以做下面的事情: |
| 344 | + |
| 345 | +```java |
| 346 | +String url = "url you want to download"; |
| 347 | +DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url)); |
| 348 | +request.setDescription("Some descrition"); |
| 349 | +request.setTitle("Some title"); |
| 350 | +// in order for this if to run, you must use the android 3.2 to compile your app |
| 351 | +// 为了保证这个if语句会运行,你必须使用android 3.2来编译 (译者注:应该是大于android 3.2的版本) |
| 352 | +if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { |
| 353 | + request.allowScanningByMediaScanner(); |
| 354 | + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); |
| 355 | +} |
| 356 | +request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "name-of-the-file.ext"); |
| 357 | + |
| 358 | +// 获得下载服务和队列文件 |
| 359 | +DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); |
| 360 | +manager.enqueue(request); |
| 361 | +``` |
| 362 | + |
| 363 | +#### 最后的一些思考 |
| 364 | + |
| 365 | +第一个和第二个方法只是冰山一角。如果你想你的应用更加健壮,你得留意许多事情。这里是一些建议: |
| 366 | + |
| 367 | ++ 你必须检查用户是否有Internet连接。 |
| 368 | ++ 确保你有正确的权限(`Internet`和`WRITE_EXTERNAL_STORAGE`),如果要检查网络可用性,你还需要`ACCESS_NETWORK_STATE`权限。 |
| 369 | ++ 确保你要保存下载文件的目录存在,并且有相应的写入权限。 |
| 370 | ++ 如果下载的文件太大,你可能需要实现一种方法来确保上次的请求失败后,可以接着从来。 |
| 371 | ++ 如果可以有暂停或者取消下载的选项,用户会很感激你的! |
| 372 | + |
| 373 | +除非你想对下载过程有绝对的控制权,否则我强烈推荐你使用`DownloadManager`。因为他已经处理好了上面的大部分建议。 |
0 commit comments