使用Android DownloadManager更新APP

具體實現思路

咱們經過downloaderManager來下載apk,而且本地保存downManager.enqueue(request)返回的id值,而且經過這個id獲取apk的下載文件路徑和下載的狀態,而且經過狀態來更新通知欄的顯示。android

第一次下載成功,彈出安裝界面api

若是用戶沒有點擊安裝,而是按了返回鍵,在某個時候,又再次使用了咱們的APPbash

若是下載成功,則判斷本地的apk的包名是否和當前程序是相同的,而且本地apk的版本號大於當前程序的版本,若是都知足則直接啓動安裝程序。網絡

檢查軟件更新

APP的更新檢查時機通常是在APP登錄成功後MainActivity的onResume()執行的時候,APP在冷啓動或者從後臺返回時能夠彈出新版本提示,除此以外還應該在APP特定頁面中增長軟件更新檢查的入口,方便用戶手動選擇更新。對於某些特定的應用場景,好比APP須要長時間在前臺展現,按照上述方法實現的更新檢查,若是沒有人爲干預的話,APP是不會獲得更新的,這種場景可經過線程池執行定時任務間隔一段時間向後臺輪詢當前最新軟件包的版本,若是最新軟件包的版本號大於當前APP的VersonCode,則經過後臺獲取到的url下載最新的軟件升級包。app

mScheduleExecutor = Executors.newSingleThreadScheduledExecutor().apply {
            scheduleAtFixedRate(mCheckVersionTask,1,1,TimeUnit.HOURS)
        }
複製代碼
private val versionCallback = object : Callback<CheckVersionResponse> {
        override fun onResponse(
            call: Call<CheckVersionResponse>,
            response: Response<CheckVersionResponse>
        ) {
            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<CheckVersionResponse>, t: Throwable) {
            Log.e(TAG,"checkVersion onFailure: $t.message")
        }
    }
複製代碼

查詢最新軟件包的接口可經過retrofit網絡框架進行封裝,在這裏再也不贅述。框架

使用谷歌推薦的DownloadManager實現下載

Android自帶的DownloadManager模塊來下載,在api level 9以後,咱們經過通知欄知道, 該模塊屬於系統自帶, 它已經幫咱們處理了下載失敗、從新下載等功能。整個下載 過程所有交給系統負責,不須要咱們過多的處理。首先須要在manifest文件中註明APP使用DownloadManager所需的相應權限:ide

<!--DownloadManager-->
    <uses-permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER"/>
複製代碼

不然不能下載成功,這點須要特別注意。函數

DownLoadManager.Request:主要用於發起一個下載請求。ui

先看下簡單的實現:this

建立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:主要用於查詢下載信息。

/*
     * 經過downloadID查詢下載的進度信息
     * */
    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<String>(3)
    install[0] = "su"
    install[1] = "-c"
    install[2] = cmd
    try {
        val p = Runtime.getRuntime().exec(install)
        p.waitFor()
    } catch (e: Exception) {
        e.printStackTrace()
    }
}
複製代碼
相關文章
相關標籤/搜索