下載需要集成第三方?Android原生下載服務DownloadManager不行嗎?
theme: juejin highlight: a11y-dark
攜手創作,共同成長!這是我參與「掘金日新計劃 · 8 月更文挑戰」的第20天,點擊查看活動詳情
前言
App 內的下載功能也是我們常用的場景,比如下載最新的 Apk 安裝包,還有些會下載圖片,或者資源,插件等場景。
下載不是很簡單的功能嗎?OkHttp就能下載,基於OkHttp實現的一些框架那更多,比較出名的有FileDownloader okdownload RxDownload 等等。
同時我們 Android 系統服務 DownloadManager 同樣可以使用下載服務,他們之間有什麼區別?
一、DownloadManager的默認使用
DownloadManager 是android2.3以後,系統下載的方法。可以讓 Android 設備請求的 URI 被下載到一個特定的目標文件。客户端將會在後台與http交互進行下載,或者在下載失敗,或者連接改變,重新啟動系統後重新下載。還可以進入系統的下載管理界面查看進度。
內部主要包含 DownloadManager.Query 和 DownloadManager.Request 兩個重要類。一個是封裝一些下載請求的參數,一個是用於查詢下載的信息。Request 是必須的,Query是非必須的。
通常使用 DownloadManager 推薦我們使用通知欄展示真正進行下載,並且我們可以跳轉到下載器頁面查看。
```kotlin private fun startDownLoad() {
//下載鏈接 這裏下載手機B站為示例
val downloadUrl = "http://dl.hdslb.com/mobile/latest/iBiliPlayer-html5_app_bili.apk"
val fileName = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1)
//這裏下載到指定的目錄,我們存在公共目錄下的download文件夾下
val fileUri = Uri.fromFile(
File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
System.currentTimeMillis().toString() + "-" + fileName
)
)
//開始構建 DownloadRequest 對象
val request = DownloadManager.Request(Uri.parse(downloadUrl))
//構建通知欄樣式
request.setTitle("測試下載標題")
request.setDescription("測試下載的內容文本")
//下載或下載完成的時候顯示通知欄
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE or DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
//指定下載的文件類型為APK
request.setMimeType("application/vnd.android.package-archive")
// request.addRequestHeader() //還能加入請求頭 // request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI) //能指定下載的網絡
//指定下載到本地的路徑(可以指定URI)
request.setDestinationUri(fileUri)
//開始構建 DownloadManager 對象
val downloadManager = commContext().getSystemService(DOWNLOAD_SERVICE) as DownloadManager
//加入Request到系統下載隊列,在條件滿足時會自動開始下載。返回的為下載任務的唯一ID
val requestID = downloadManager.enqueue(request)
//註冊下載任務完成的監聽
commContext().registerReceiver(object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
//已經完成
if (intent.action.equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) {
//獲取下載ID
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
val uri = downloadManager.getUriForDownloadedFile(id)
YYLogUtils.w("下載完成了- uri:$uri")
installApk(uri)
} else if (intent.action.equals(DownloadManager.ACTION_NOTIFICATION_CLICKED)) {
//如果還未完成下載,跳轉到下載中心
YYLogUtils.w("跳轉到下載中心")
val viewDownloadIntent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
viewDownloadIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(viewDownloadIntent)
}
}
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
}
```
註釋的很詳細,步驟如下: 1. 我們封裝一個 Request 對象設置下載的鏈接Uri,設置下載到的目標文件夾,設置是否需要展示通知等。 2. 構建 DownloadManager 服務,把 Request 任務放入隊列,如果滿足條件即可生效。 3. 一般來説我們都希望下載完成之後能處理一些事情,我們就需要監聽完成的廣播(非必須的)。
這裏需要注意的是: 1. 可能需要申請SD卡權限, 2. 如果下載是公共目錄,在Android12以上只有download等少數文件夾是開放的,其他的文件夾可能無法訪問。 3. 如果下載的是沙盒目錄,你無需申請SD卡權限,但是如果外部應用想要訪問到此文件,需要定義FileProvider提供給對方使用(比如Apk安裝)
完成的效果:
我們下載的是一個Apk,由於我們下載到了公共目錄的download文件夾下面,所以我們可以直接調用安裝方法,(注意Android8.0的兼容)
兼容8.0以上 聲明權限
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
直接調用即可
kotlin
private fun installApk(uri: Uri) {
val intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.setDataAndType(uri, "application/vnd.android.package-archive")
startActivity(intent)
}
效果:
由於測試機器為Android12,所以需要同意未知的安裝包安裝權限
一系列的操作就安裝成功了。
不行!我不能讓我的Apk就這麼暴露在公共目錄下面!我要隱私,我要下載在沙盒裏面!行不行?
當然行,太行了,我們下載到沙盒的目錄中的話,我們只能自己的應用有訪問權限,其他的應用程序訪問就需要FileProvider,這裏簡單的過一下吧。
```xml
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
```
```xml
<!--下載apk-->
<external-path
name="download"
path=""/>
```
那麼我們獲取Uri的時候我們就需要通過FileProvider來獲取Uri對象了
java
Uri apkUri = FileProvider.getUriForFile(context, "com.meiyue.smartcity.fileprovider", file);
關於FileProvider感覺已經被開發者玩壞了,有機會會單獨出一期,今天的主題是下載服務的使用,我們迴歸主題。
二、DownloadManager的靜默下載
哇,真的能下載了呢!好簡單哦。但是你這麼好Low啊,用户一看就知道我在幹什麼了,我想下載個資源包或插件那怎麼辦,總不能讓用户看到我在下載吧。
萬一偷偷的下載點東西乾點壞事,不是搞得大家都知道了。啊,你這個通知欄也太醜了,只能設置Title Content,又不能定製UI,放棄!
(下載的時候通知欄的樣式是由廠商或系統決定的)
放心,都可以實現的!DownloadManager 其實可以設置不使用通知欄的。
那我怎麼知道進度和狀態?其實 DownloadManager 內部有 Query 可以查詢這些狀態的。那我們實現一個偷偷的靜默下載邏輯看看。
```kotlin private val scheduledExecutorService: ScheduledExecutorService = Executors.newScheduledThreadPool(3)
private fun startDownLoad() {
//下載鏈接 這裏下載手機B站為示例
val downloadUrl = "http://dl.hdslb.com/mobile/latest/iBiliPlayer-html5_app_bili.apk"
val fileName = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1)
//這裏下載到指定的目錄,我們存在公共目錄下的download文件夾下
val fileUri = Uri.fromFile(
File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
System.currentTimeMillis().toString() + "-" + fileName
)
)
//開始構建 DownloadRequest 對象
val request = DownloadManager.Request(Uri.parse(downloadUrl))
//下載時候隱藏通知欄
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
//指定下載的文件類型為APK
request.setMimeType("application/vnd.android.package-archive")
// request.addRequestHeader() //還能加入請求頭 // request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI) //能指定下載的網絡
//指定下載到本地的路徑(可以指定URI)
request.setDestinationUri(fileUri)
//開始構建 DownloadManager 對象
val downloadManager = commContext().getSystemService(DOWNLOAD_SERVICE) as DownloadManager
//加入Request到系統下載隊列,在條件滿足時會自動開始下載。返回的為下載任務的唯一ID
val requestID = downloadManager.enqueue(request)
//註冊獲取進度的監聽
YYLogUtils.w("開始下載:fileUri:$fileUri requestID:$requestID")
//每秒定時刷新一次
val command = Runnable {
getBytesAndStatus(requestID)
}
scheduledExecutorService.scheduleAtFixedRate(command, 0, 1, TimeUnit.SECONDS)
//註冊下載任務完成的監聽
commContext().registerReceiver(object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
//已經完成
if (intent.action.equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) {
//解綁進度監聽
scheduledExecutorService.shutdown()
//獲取下載ID
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
val uri = downloadManager.getUriForDownloadedFile(id)
YYLogUtils.w("下載完成了- uri:$uri")
installApk(uri)
} else if (intent.action.equals(DownloadManager.ACTION_NOTIFICATION_CLICKED)) {
//如果還未完成下載,跳轉到下載中心
YYLogUtils.w("跳轉到下載中心")
val viewDownloadIntent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
viewDownloadIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(viewDownloadIntent)
}
}
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
}
//獲取當前進度,和總進度
private fun getBytesAndStatus(downloadId: Long) {
val query = DownloadManager.Query().setFilterById(downloadId)
var cursor: Cursor? = null
val downloadManager = commContext().getSystemService(DOWNLOAD_SERVICE) as DownloadManager
try {
cursor = downloadManager.query(query)
if (cursor != null && cursor.moveToFirst()) {
// //Notification 標題 // val title = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_TITLE))
// //描述 // val description = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION))
val downloaded = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
val total = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
val progress = downloaded * 100 / total
YYLogUtils.w("當前下載大小:$downloaded 總共大小:$total")
}
} finally {
cursor?.close()
}
}
private fun installApk(uri: Uri) {
val intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.setDataAndType(uri, "application/vnd.android.package-archive")
startActivity(intent)
}
```
注意點: 1. 一定要設置 VISIBILITY_HIDDEN 才能不顯示通知欄 2. 如果高版本設置 VISIBILITY_HIDDEN 報錯,需要設置權限
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
- 我們使用 Query 來查詢下載的狀態,如果要監聽下載進度,我們使用定時任務即可,比如每一秒查詢一次。(這裏的定時任務可以以任意方式來實現)
這樣我們就可以實現和應用內部OkHttp來下載一樣的效果啦。
通知欄不能自定義UI?現在我們是靜默下載了,你想彈窗展示進度,佈局展示進度,通知欄展示進度,自定義通知欄什麼的,只要拿到下載的進度,那不是任你揉搓了!屬實是想怎麼玩就怎麼玩了。
總結
DownloadManager 同樣很靈活 ,其實他提供了很多 Api 。我們可以使用它實現各種定製化的下載需求。(比如斷點續傳,重新下載等),如有有需求,大家可以基於 DownloadManager 實現一個下載的框架。
我覺得 DownloadManager 對比其他的類似OkHttp這樣的下載框架,最大的一個優點是系統服務,由於它是系統服務,只要我們的App開啟了一個下載任務,那麼退出App,這個下載任務一樣能繼續下載,而使用OkHttp下載就算放在前台Service中,也是有機率掛掉的,而 DownloadManager 則不會。
當然兩種方案都是可以用的,看不同的使用場景了,讓我選的話,如果我做的應用是多媒體類型的,有很多的隊列併發下載,並查看媒體文件之類的,我可能會使用 okdownload ,但是如果我做的就是很普通的應用,大量併發下載的場景不多,我可能就會使用DownloadManager實現了。
同時我們可以基於系統服務進行一些聯動,比如我們之前講到的 WorkManager 。每12小時檢查一下遠程的資源與版本,我們就可以搭配 DownloadManager 在後台偷偷的下載資源與插件。並且他們都支持指定Wifi環境下的下載。簡直完美。
想測試的同學可以看看代碼,運行一下,源碼在此。
最後吐槽一句,DownloadManager 可比 坑爹的 LocationManager 好用多了。
好了,我如有講解不到位或錯漏的地方,希望同學們可以指出交流。
如果感覺本文對你有一點點點的啟發,還望你能點贊
支持一下,你的支持是我最大的動力。
Ok,這一期就此完結。
- Android操作文件也太難了趴,File vs DocumentFile 以及 DocumentsProvider vs FileProvider 的異同
- findViewById不香嗎?為什麼要把簡單的問題複雜化?為什麼要用DataBinding?
- Android自定義View繪製進階-水波浪温度刻度表
- Android自定義ViewGroup佈局進階,完整的九宮格實現
- 記錄仿抖音的視頻播放並緩存預加載視頻的效果實現
- Kotlin對象的懶加載方式?by lazy 與 lateinit 的異同
- 定位都得集成第三方?Android原生定位服務LocationManager不行嗎?
- 還用第三方庫管理狀態欄嗎?Android關於狀態欄管理的幾種方案實現!
- 下載需要集成第三方?Android原生下載服務DownloadManager不行嗎?
- Android陰影實現的幾種方案-自定義圓角ViewGroup加入陰影效果
- 操作Android窗口的幾種方式?WindowInsets與其兼容庫的使用與踩坑
- Android軟鍵盤與佈局的協調-不同的效果與實現方案的探討
- ViewPager2:ViewPager都能自動嵌套滾動了,我不行?我麻了!該怎麼做?
- Android軟鍵盤的監聽與高度控制的幾種方案及常用效果
- 圓角升級啦,來手把手一起實現自定義ViewGroup的各種圓角與背景
- Android導航欄的處理-HostStatusLayout加入底部的導航欄適配
- 一次搞懂怎麼設置圓角圖片,ImageView的各種圓角設置
- 一看就會 Android框架DataBinding的使用與封裝
- 別濫用FileProvider了,Android中FileProvider的各種場景應用
- Android登錄攔截的場景-基於攔截器模式實現