具體實作思路
我們通過downloaderManager來下載下傳apk,并且本地儲存downManager.enqueue(request)傳回的id值,并且通過這個id擷取apk的下載下傳檔案路徑和下載下傳的狀态,并且通過狀态來更新通知欄的顯示。
第一次下載下傳成功,彈出安裝界面
如果使用者沒有點選安裝,而是按了傳回鍵,在某個時候,又再次使用了我們的APP
如果下載下傳成功,則判斷本地的apk的包名是否和目前程式是相同的,并且本地apk的版本号大于目前程式的版本,如果都滿足則直接啟動安裝程式。
檢查軟體更新
APP的更新檢查時機一般是在APP登陸成功後MainActivity的onResume()執行的時候,APP在冷啟動或者從背景傳回時可以彈出新版本提示,除此之外還應該在APP特定頁面中增加軟體更新檢查的入口,友善使用者手動選擇更新。對于某些特定的應用場景,比如APP需要長時間在前台展示,按照上述方法實作的更新檢查,如果沒有人為幹預的話,APP是不會得到更新的,這種場景可通過線程池執行定時任務間隔一段時間向背景輪詢目前最新軟體包的版本,如果最新軟體包的版本号大于目前APP的VersonCode,則通過背景擷取到的url下載下傳最新的軟體更新包。
mScheduleExecutor = Executors.newSingleThreadScheduledExecutor().apply {
scheduleAtFixedRate(mCheckVersionTask,1,1,TimeUnit.HOURS)
}
複制代碼private val versionCallback = object : Callback {
override fun onResponse(
call: Call,
response: Response
) {
if (response.isSuccessful) {
response.body()?.let {
if (it.success != false) {
it.data?.run {
if (code?:0 > BuildConfig.VERSION_CODE) {
mDownloadListener?.onDiscoverNewVersion(this)
}
}
}
}
}
}
override fun onFailure(call: Call, t: Throwable) {
Log.e(TAG,"checkVersion onFailure: $t.message")
}
}
複制代碼
查詢最新軟體包的接口可通過retrofit網絡架構進行封裝,在這裡不再贅述。
使用谷歌推薦的DownloadManager實作下載下傳
Android自帶的DownloadManager子產品來下載下傳,在api level 9之後,我們通過通知欄知道, 該子產品屬于系統自帶, 它已經幫我們處理了下載下傳失敗、重新下載下傳等功能。整個下載下傳 過程全部交給系統負責,不需要我們過多的處理。首先需要在manifest檔案中注明APP使用DownloadManager所需的相應權限:
複制代碼
否則不能下載下傳成功,這點需要特别注意。
DownLoadManager.Request:主要用于發起一個下載下傳請求。
先看下簡單的實作:
建立Request對象的代碼如下:
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkurl));
//設定在什麼網絡情況下進行下載下傳
request.setAllowedNetworkTypes(Request.NETWORK_WIFI);
//設定通知欄标題
request.setNotificationVisibility(Request.VISIBILITY_VISIBLE);
request.setTitle("下載下傳");
request.setDescription("apk正在下載下傳");
request.setAllowedOverRoaming(false);
//設定檔案存放目錄
request.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, "mydown");
複制代碼
這裡我們可以看下request的一些屬性:
addRequestHeader(String header,String value):添加網絡下載下傳請求的http頭資訊
allowScanningByMediaScanner():用于設定是否允許本MediaScanner掃描。
setAllowedNetworkTypes(int flags):設定用于下載下傳時的網絡類型,預設任何網絡都可以下載下傳,提供的網絡常量有:NETWORK_BLUETOOTH、NETWORK_MOBILE、NETWORK_WIFI。
setAllowedOverRoaming(Boolean allowed):用于設定漫遊狀态下是否可以下載下傳
setNotificationVisibility(int visibility):用于設定下載下傳時時候在狀态欄顯示通知資訊
setTitle(CharSequence):設定Notification的title資訊
setDescription(CharSequence):設定Notification的message資訊
setDestinationInExternalFilesDir、setDestinationInExternalPublicDir、 setDestinationUri等方法用于設定下載下傳檔案的存放路徑
複制代碼
取得系統服務後,調用downloadmanager對象的enqueue方法進行下載下傳,此方法傳回一個編号用于标示此下載下傳任務:
downManager = (DownloadManager)getSystemService(Context.DOWNLOAD_SERVICE);
id= downManager.enqueue(request);
複制代碼
DownLoadManager.Query:主要用于查詢下載下傳資訊。
private fun getBytesAndStatus(downloadId: Long): IntArray? {
val bytesAndStatus = intArrayOf(-1, -1, 0)
val query: DownloadManager.Query = DownloadManager.Query().setFilterById(downloadId)
var cursor: Cursor? = null
try {
cursor = mDownloadManager.query(query)
if (cursor != null && cursor.moveToFirst()) {
//已經下載下傳檔案大小
bytesAndStatus[0] =
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
//下載下傳檔案的總大小
bytesAndStatus[1] =
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
//下載下傳狀态
bytesAndStatus[2] =
cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
mDownloadListener?.onProgressChange(
bytesAndStatus[1],
bytesAndStatus[0],
bytesAndStatus[2]
)
}
} finally {
cursor?.close()
}
return bytesAndStatus
}
複制代碼
使用ContentObserver監聽APK的下載下傳進度
DownloadManager在Android系統内部有固定的資源URI,可以很友善的通過該URI注冊一個ContentObserver:
mDownObserver = DownloadChangeObserver(null).also {
contentResolver.registerContentObserver(
Uri.parse("content://downloads/my_downloads"),
true,
it
)
}
複制代碼
當ContentObserver構造函數中傳入的Handler對象為空時,它的onChange()回調方法是在UI線程中執行的,是以我們通過線程池去查詢下載下傳進度以防止UI阻塞:
inner class DownloadChangeObserver(handler: Handler?) : ContentObserver(handler) {
override fun onChange(selfChange: Boolean) {
//設定查詢進度的線程每隔兩秒查詢一下
mProgressFuture = mScheduleExecutor.scheduleAtFixedRate(mProgressThread, 0, 2, TimeUnit.SECONDS)
}
}
inner class ProgressThread : Runnable {
override fun run() {
getBytesAndStatus(mDownLoadId)
}
}
複制代碼
使用Android系統提供的方法安裝APK
DownloadManager下載下傳完成後會向外發出ACTION_DOWNLOAD_COMPLETE的廣播,在程式中通過動态注冊廣播接收者監聽該廣播:
mDownReceiver = CompleteReceiver().also {
registerReceiver(it, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
}
inner class CompleteReceiver : BroadcastReceiver() {
override fun onReceive(
context: Context,
intent: Intent
) {
val completeDownloadId =
intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
if (completeDownloadId == mDownLoadId) {
mProgressFuture?.cancel(false)
val myDownloadQuery = DownloadManager.Query()
myDownloadQuery.setFilterById(mDownLoadId)
mDownloadManager.query(myDownloadQuery)?.let {
if (it.moveToFirst()) {
val sizeTotal: String =
it.getString(it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
if (sizeTotal.toLong() < 0 || mFileName == null) {
return
}
}
it.close()
mFileName?.let {name ->
installAPK(context,name)
}
}
}
}
}
複制代碼
檢查下載下傳的APK是版本是否大于目前APP版本号:
private fun checkDownLoadAPK(versionCode: Int, versionName: String) {
val file = File(baseContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"my.apk")
if (file.exists() && file.isFile) {
val pm: PackageManager = packageManager
pm.getPackageArchiveInfo(
file.path,
PackageManager.GET_ACTIVITIES
)?.also {
val appInfo: ApplicationInfo = it.applicationInfo
if (appInfo.packageName == baseContext.packageName
&& it.versionCode >= versionCode){
installAPK()
}
}
file.delete()
}
}
複制代碼
跳轉Android系統APK安裝界面:
fun installAPK(context: Context, path: String) {
setPermission(path)
val intent =
Intent(Intent.ACTION_VIEW)
// 由于沒有在Activity環境下啟動Activity,設定下面的标簽
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
//Android 7.0以上要使用FileProvider
val file = File(path)
if (Build.VERSION.SDK_INT >= 24) {
//參數1 上下文, 參數2 Provider主機位址 和配置檔案中保持一緻 參數3 共享的檔案
val apkUri =
FileProvider.getUriForFile(context, "com.android.file.provider", file)
//添加這一句表示對目标應用臨時授權該Uri所代表的檔案
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.setDataAndType(apkUri, "application/vnd.android.package-archive")
} else {
intent.setDataAndType(
Uri.fromFile(
file
), "application/vnd.android.package-archive"
)
}
context.startActivity(intent)
}
複制代碼
如果使用系統秘鑰對APK簽名後可使用靜默安裝的方式:
private fun installApkSilently(apkPathName: String) {
val cmd = "LD_LIBRARY_PATH=/vendor/lib:/system/lib pm install -r $apkPathName"
val install = arrayOfNulls(3)
install[0] = "su"
install[1] = "-c"
install[2] = cmd
try {
val p = Runtime.getRuntime().exec(install)
p.waitFor()
} catch (e: Exception) {
e.printStackTrace()
}
}
複制代碼