Android 完整版的下載示例
首先我們需要將專案中會使用到的依賴庫新增好,編輯app/build.gradle檔案,在dependencies閉包中新增如下內容:
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support.constraint:constraint-layout:1.0.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' implementation 'com.squareup.okhttp3:okhttp:3.9.1' }
這裡只需新增一個OkHttp的依賴就行了,待會兒在編寫網路相關的功能時,我們將使用OkHttp來進行實現.
接下來需要定義一個回撥介面,用於對下載過程中的各種狀態進行監聽和回撥,新建一個DownloadListener介面,程式碼如下:
public interface DownloadListener { void onProgress(int progress); void onSuccess(); void onFailed(); void onPaused(); void onCanceled(); }
可以看到,這裡我們一共定義了5個回撥方法,onProgress()方法用於通知當前的下載進度,onSuccess()方法用於通知下載成功事件,onFailed()方法用於通知下載失敗事件,onPaused()方法用於通知下載暫停事件,onCanceled()方法用於通知下載取消事件.
新建一個DownloadTask繼承自AsyncTask,程式碼如下:
public class DownloadTask extends AsyncTask<String, Integer, Integer> { public static final int TYPE_SUCCESS = 0; public static final int TYPE_FAILED = 1; public static final int TYPE_PAUSED = 2; public static final int TYPE_CANCELED = 3; private DownloadListener mListener; private boolean isCanceled = false; private boolean isPaused = false; private int lastProgress; public DownloadTask(DownloadListener listener) { this.mListener = listener; } @Override protected Integer doInBackground(String... params) { InputStream is = null; RandomAccessFile savedFile = null; File file = null; try { long downloadedLength = 0;//記錄已下載的檔案長度 String downloadUrl = params[0]; String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/")); String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath(); file = new File(directory + fileName); if (file.exists()) { downloadedLength = file.length(); } long contentLength = getContentLength(downloadUrl); if (contentLength == 0) { return TYPE_FAILED; } else if (contentLength == downloadedLength) { //一下載位元組和檔案總位元組相等,說明已經下載完成了 return TYPE_SUCCESS; } OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() //斷點下載,指定從哪個位元組開始下載 .addHeader("RANGE", "bytes=" + downloadedLength + "-") .url(downloadUrl) .build(); Response response = client.newCall(request).execute(); if (response != null) { is = response.body().byteStream(); savedFile = new RandomAccessFile(file, "rw"); savedFile.seek(downloadedLength);//跳過已下載的位元組 byte[] b = new byte[1024]; int total = 0; int len; while ((len = is.read(b)) != -1) { if (isCanceled) { return TYPE_CANCELED; } else if (isPaused) { return TYPE_PAUSED; } else { total += len; savedFile.write(b, 0, len); //計算已下載的百分比 int progress = (int) ((total + downloadedLength) * 100 / contentLength); publishProgress(progress); } } response.body().close(); return TYPE_SUCCESS; } } catch (Exception e) { e.printStackTrace(); } finally { try { if (is != null) { is.close(); } if (savedFile != null) { savedFile.close(); } if (isCanceled && file != null) { file.delete(); } } catch (Exception e) { e.printStackTrace(); } } return TYPE_FAILED; } @Override protected void onProgressUpdate(Integer... values) { int progress = values[0]; if (progress > lastProgress) { mListener.onProgress(progress); lastProgress = progress; } } @Override protected void onPostExecute(Integer status) { switch (status) { case TYPE_SUCCESS: mListener.onSuccess(); break; case TYPE_FAILED: mListener.onFailed(); break; case TYPE_PAUSED: mListener.onPaused(); break; case TYPE_CANCELED: mListener.onCanceled(); default: break; } } public void pauseDownload(){ isPaused = true; } public void cancelDownload(){ isCanceled = true; } private long getContentLength(String downloadUrl) throws IOException { OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url(downloadUrl) .build(); Response response = client.newCall(request).execute(); if (response != null && response.isSuccessful()) { long contentLength = response.body().contentLength(); response.close(); return contentLength; } return 0; } }
這段程式碼比較長,我們需要一步步進行分析,首先看一下AsyncTask中的3個泛型引數,第一個泛型引數指定為String,表示在執行AsyncTask的時候需要傳入一個字串引數給後臺任務,第二個泛型引數指定為Integer,表示使用整型資料來作為進度顯示單位,第三個泛型引數指定為Integer,則表示使用整型資料來反饋執行結果.
接下來定義了4個整型常量用於表示下載的狀態,TYPE_SUCCESS表示下載成功,TYPE_FAILED表示下載失敗,TYPE_PAUSED表示暫停下載,TYPE_CANCELED表示取消下載,然後在DownloadTask的建構函式中要求傳入一個剛剛定義的DownloadListener引數,我們待會就會將下載的狀態通過這個引數進行回撥.
接著就是要重寫doInBackgound(),onProgressUpdate()和onPostExecute()這3個方法了,doInBackgound()方法用於在後臺執行具體的下載邏輯,onProgressUpdate()方法用於在介面上更新當前的下載進度,onPostExecute()方法通知最終的下載結果.
那麼先來看一下doInBackgound()方法,首先我們從引數中獲取到了下載的URL地址,並根據URL地址解析出了下載的檔名,然後指定將檔案下載到Environment.DIRECTORY_DOWNLOADS目錄下,也就是SD卡的Download目錄,我們還要判斷一下Download目錄中是不是已經存在要下載的檔案了,如果已經存在的話則讀取已下載的位元組數,這樣就可以在後面啟用斷點續傳的功能,接下來先是呼叫了getContentLength()方法來獲取待下載檔案的總長度,如果檔案長度等於0則說明檔案有問題,直接返回TYPE_FAILED,如果檔案長度等於已下載檔案長度,那麼說明檔案已經下載完了,直接返回TYPE_SUCCESS即可,緊接著使用OkHttp來傳送一條網路請求,需要注意的是,這裡在請求中新增了一個header,用於告訴伺服器我們想要從哪個位元組開始下載,因為已下載過的部分就不需要再重新下載了,接下來讀取伺服器相應的資料,並使用Java的檔案流的方法,不斷從網路上讀取資料,不斷寫入到本地,一直到檔案全部下載完成為止,在這個過程中,我們還要判斷使用者有沒有觸發暫停或者取消的操作,如果有的話則返回TYPE_PAUSED或TYPE_CANCELED來中斷下載,如果沒有的話則實時計算當前的下載進度,然後呼叫publishProgress()方法進行通知,暫停和取消操作都是使用一個布林型的變數來進行控制的,呼叫pauseDownload()或cancelDownload()方法即可更改變數的值.
接下來看一下onProgressUpdate()方法,也非常簡單,就是根據引數中傳入的下載狀態來進行回撥,下載成功就呼叫DownloadListener的onSuccess()方法,下載失敗就呼叫onFailed()方法,暫停下載就呼叫onPaused()方法,取消下載就呼叫onCanceled()方法.
這樣就把具體的下載功能完成了,下面為了保證DownloadTask可以一直在後臺執行,我們還需要建立一個下載的服務,新建DownloadService,程式碼如下:
public class DownloadService extends Service { private DownloadTask downloadTask; private String downloadUrl; private DownloadListener listener = new DownloadListener() { @Override public void onProgress(int progress) { getNotificationManager().notify(1,getNotification("Downloading...",progress)); } @Override public void onSuccess() { downloadTask = null; //下載成功時將前臺服務通知關閉,並建立一個下載成功的通知 stopForeground(true); getNotificationManager().notify(1,getNotification("Download Success",-1)); Toast.makeText(DownloadService.this,"Download Success",Toast.LENGTH_SHORT).show(); } @Override public void onFailed() { downloadTask = null; //下載失敗時將前臺服務通知關閉,並建立一個下載失敗的通知 stopForeground(true); getNotificationManager().notify(1,getNotification("Download Failed",-1)); Toast.makeText(DownloadService.this,"Download Failed",Toast.LENGTH_SHORT).show(); } @Override public void onPaused() { downloadTask = null; Toast.makeText(DownloadService.this,"Paused",Toast.LENGTH_SHORT).show(); } @Override public void onCanceled() { downloadTask = null; stopForeground(true); Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_SHORT).show(); } }; private DownloadBinder mBinder = new DownloadBinder(); @Nullable @Override public IBinder onBind(Intent intent) { return mBinder; } class DownloadBinder extends Binder{ public void startDownload(String url){ if (downloadTask == null){ downloadUrl = url; downloadTask = new DownloadTask(listener); downloadTask.equals(downloadUrl); startForeground(1,getNotification("Downloading",0)); Toast.makeText(DownloadService.this,"Downloading...",Toast.LENGTH_SHORT).show(); } } public void pauseDownload(){ if (downloadTask != null){ downloadTask.pauseDownload(); } } public void cancelDownload(){ if (downloadTask != null){ downloadTask.cancelDownload(); }else { if (downloadUrl != null){ //取消下載時需將檔案刪除,並將通知關閉 String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/")); String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath(); File file = new File(directory + fileName); if (file.exists()){ file.delete(); } getNotificationManager().cancel(1); stopForeground(true); Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_SHORT).show(); } } } } private NotificationManager getNotificationManager(){ return (NotificationManager) getSystemService(NOTIFICATION_SERVICE); } private Notification getNotification(String title,int progress){ Intent intent = new Intent(this,MainActivity.class); PendingIntent pi = PendingIntent.getActivity(this,0,intent,0); NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setSmallIcon(R.mipmap.ic_launcher); builder.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher)); builder.setContentIntent(pi); builder.setContentTitle(title); if (progress > 0){ //當progress大於或等於0時才需要下載進度 builder.setContentText(progress + "%"); builder.setProgress(100,progress,false); } return builder.build(); } }
這段程式碼同樣也比較長,我們還是得耐心慢慢看,首先這裡建立了一個DownloadListener的匿名類例項,並在匿名類中去實現了onProgress(),onSuccess(),onFailed(),onPaused()和onCanceled()這5個方法,在onProgress()方法中,我們呼叫了getNotification()方法構建了一個用於顯示下載進度的通知,然後呼叫NotificationManager的notify()方法去觸發這個通知,這樣就可以在下拉狀態列實時看到當前下載的進度了,在onSuccess()方法中,我們首先是將正在下載的前臺通知關閉,然後建立一個新的通知用於告訴使用者下載成功了,其他幾個方法也都是類似的,分別用於告訴使用者下載失敗,暫停和取消這幾個事件.
接下來為了要讓DownloadService可以和活動進行通訊,我們又建立了一個DownloadBinder,DownloadBinder中提供了startDownload(),pauseDownload()和cancelDownload()這3個方法,那麼顧名思義,它們分別是用於開始下載,暫停下載和取消下載的,在startDownload()方法中,我們建立了一個DownloadTask的例項,把剛才的Down作為引數傳入,然後呼叫execute()方法開啟下載,並將下載檔案的URL地址傳入到execute()方法中,同時,為了讓這個下載服務成為一個前臺服務,我們還呼叫了startForeground()方法,這樣就會在系統狀態列中建立一個持續執行的通知了,接著往下看,pauseDownload()方法中的程式碼就非常簡單了,就是簡單的呼叫了一下DownloadTask中的pauseDownload()方法,cancelDownload()方法中的邏輯也基本類似,但是要注意,取消下載的時候我們需要將正在下載的檔案刪除掉,這一點和暫停下載是不同的.
另外,DownloadService類中所有使用到的通知都是呼叫getNotification()方法進行構建的,其中setProgress()方法接收3個引數,第一個引數傳入通知的最大進度,第二個引數傳入通知的當前進度,第三個參數列示是否使用模糊進度條,這裡傳入false,設定完setProgress()方法,通知上就會有進度條顯示出來了.
現在下載的服務已經成功實現了,後端的工作基本都完成了,那麼接下來我們開始編寫前端的部分了,activity_main.xml中的程式碼.如下:
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.servicebestpractice.MainActivity"> <Button android:id="@+id/start_download" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Start Download" android:textAllCaps="false"/> <Button android:id="@+id/pause_download" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Pause Download" android:textAllCaps="false" app:layout_constraintTop_toBottomOf="@id/start_download"/> <Button android:id="@+id/cancel_download" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Cancel Download" android:textAllCaps="false" app:layout_constraintTop_toBottomOf="@id/pause_download"/> </android.support.constraint.ConstraintLayout>
佈局檔案還是非常簡單的,這裡放置了3個按鈕,分別用於開始下載,暫停下載和取消下澡.
MainActivity.java中的程式碼,如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{ private DownloadService.DownloadBinder downloadBinder; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName componentName, IBinder service) { downloadBinder = (DownloadService.DownloadBinder) service; } @Override public void onServiceDisconnected(ComponentName componentName) { } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button startDownload = findViewById(R.id.start_download); Button pauseDownload = findViewById(R.id.pause_download); Button cancelDownload = findViewById(R.id.cancel_download); startDownload.setOnClickListener(this); pauseDownload.setOnClickListener(this); cancelDownload.setOnClickListener(this); Intent intent = new Intent(this,DownloadService.class); startService(intent); bindService(intent,connection,BIND_AUTO_CREATE);//繫結服務 if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){ ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1); } } @Override public void onClick(View view) { if (downloadBinder == null){ return; } switch (view.getId()){ case R.id.start_download: String url = "https://image.baidu.com/search/detail?ct=503316480&z=0&ipn=d&word=%E8%BE%B9%E7%89%A7&step_word=&hs=0&pn=2&spn=0&di=49639799020&pi=0&rn=1&tn=baiduimagedetail&is=0%2C0&istype=0&ie=utf-8&oe=utf-8&in=&cl=2&lm=-1&st=undefined&cs=3763498388%2C2861370255&os=1617117754%2C3440027732&simid=0%2C0&adpicid=0&lpn=0&ln=1991&fr=&fmq=1518064065581_R&fm=&ic=undefined&s=undefined&se=&sme=&tab=0&width=undefined&height=undefined&face=undefined&ist=&jit=&cg=&bdtype=13&oriquery=&objurl=http%3A%2F%2Fimgsrc.baidu.com%2Fimage%2Fc0%3Dpixel_huitu%2C0%2C0%2C294%2C40%2Fsign%3D3f7840f1114c510fbac9ea5a09214041%2F96dda144ad3459820a8b7bab07f431adcbef8482.jpg&fromurl=ippr_z2C%24qAzdH3FAzdH3Fooo_z%26e3Bi7tp7_z%26e3Bv54AzdH3Fri5p5AzdH3Ffi5oAzdH3Fda8ma8abAzdH3F8m89d9dcmn9a_z%26e3Bip4s&gsm=0&rpstart=0&rpnum=0"; downloadBinder.startDownload(url); break; case R.id.pause_download: downloadBinder.pauseDownload(); break; case R.id.cancel_download: downloadBinder.cancelDownload(); break; default: break; } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode){ case 1: if (grantResults.length >0 && grantResults[0] != PackageManager.PERMISSION_GRANTED){ Toast.makeText(this,"拒絕許可權將無法使用程式",Toast.LENGTH_SHORT).show(); finish(); } break; default: break; } } @Override protected void onDestroy() { super.onDestroy(); unbindService(connection); } }
可以看到,這裡我們首先建立了一個ServiceConnection的匿名類,然後在onServiceConnected()方法中獲取到DownloadBinder的例項,有了這個例項,我們就可以在活動中呼叫服務提供的各種方法了.
接下來看一下onCreate()方法,在這裡我們對各個按鈕進行了初始化操作並設定了點選事件,然後分別呼叫了StartService()和bindService()方法來啟動和繫結服務,這一點是至關重要的,因為啟動服務可以保證DownloadService一直在後臺執行,繫結服務則可以讓MainActivity和DownloadService進行通訊,因此兩個方法呼叫都是必不可少的,在onCreate()方法的最後,我們還進行了WRITE_EXTERNAL_STORAGE的執行時許可權申請,因為下載檔案是要下載到SD卡的Download目錄下的,如果沒有這個許可權的話,我們整個程式都無法正常工作.
接下來的程式碼就非常簡單了,在onClick()方法中我們對點選事件進行判斷,如果點選了開始按鈕就呼叫DownloadBinder的startDownload()方法,如果點選了暫停按鈕就呼叫pauseDownload()方法,如果點選了取消按鈕就呼叫cancelDownload()方法,startDownload()方法中你可以傳入任意的下載地址.
另外還有一點需要注意,如果活動被銷燬了,那麼一定要記得對服務進行解綁,不然就有可能會造成記憶體洩漏,這裡我們在onDestroy()方法中完成了解綁的操作.
現在只差最後一步了,我們還需要在AndroidManifest.xml檔案中宣告使用到的許可權,如下:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.servicebestpractice"> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name=".DownloadService" android:enabled="true" android:exported="true"/> </application> </manifest>
其中,由於我們的程式使用到了網路和訪問SD卡的功能,因此需要宣告INTERNET和WRITE_EXTERNAL_STORAGE這兩個許可權.
這樣所有的程式碼都編寫完了.
相關文章
- Android通過startService實現批量下載示例Android
- maven的下載、安裝與配置 倉庫配置完整版Maven
- 偉大投資者的十個特徵(附完整版下載)特徵
- 免費的Lucene 原理與程式碼分析完整版下載
- Android工具下載Android
- [Android]Android原始碼下載Android原始碼
- 下載Android程式碼Android
- android 離線下載Android
- Android 原始碼下載Android原始碼
- android資源下載Android
- Mac下載Android原始碼的方法MacAndroid原始碼
- Android studio的Android SDK沒有下載項Android
- Flutter版本的玩Android完整版本(終結)FlutterAndroid
- Android 視訊教程下載Android
- [Android]CTS -google下載地址AndroidGo
- jQuery實現省市區三級聯動完整版原始碼演示下載jQuery原始碼
- HanLP Android 示例HanLPAndroid
- JavaWeb之實現檔案上傳與下載示例JavaWeb
- (連載)Android 8.0 : 如何下載和閱讀Android原始碼Android原始碼
- Android原生下載(下篇)多檔案下載+多執行緒下載Android執行緒
- 貝萊德智庫:2016年投資展望(附完整版下載)
- 重磅!89頁PPT揭祕,人工智慧產業爆發的拐點(附完整版下載)人工智慧產業
- 玩一玩Android下載框架Android框架
- Android sdk 下載/更新失敗Android
- Android斷點下載小結Android斷點
- Android開發工具下載地址Android
- android adt 最新下載地址Android
- Android studio下載安裝Android
- Android下載檔案(一)下載進度&斷點續傳Android斷點
- android 彩信接收到附件的下載原理分析Android
- Android Studio匯入github下載的工程AndroidGithub
- Android Jni開發環境搭建完整版Android開發環境
- 邊下載邊播放的播放器Android邊下邊播播放器Android
- 下載Android單個專案原始碼的方法Android原始碼
- Swift 開發視訊 iOS 開發視訊教程完整版下載 (共四季)SwiftiOS
- 快速解決 Android SDK Manager 無法下載或者下載速度慢Android
- Android studio下載安裝後常見問題中的gradle下載失敗AndroidGradle
- Nginx簡單的負載均衡配置示例Nginx負載