Android在线更新Apk的思路

庾君博
2023-12-01

Android在线更新Apk的思路:

01.使用DownLoadManager进行下载
02.使用OkHttpClient()进行下载


一、完整下载代码

1.创建下载回调:

public interface DownloadCallBack {
    void onProgress(int i);
}

2.创建FileProvider,安装apk文件需要使用FileProvider,同时注意添加相关的权限:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        package="com.imooc.updatedemo">
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />


<!--    <uses-permission android:name="android.permission.INSTALL_PACKAGES"-->
<!--            tools:ignore="ProtectedPermissions" />-->
    <uses-permission android:name='android.permission.INTERNET'/>	<!-- 联网权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>  <!-- 写入SD卡权限 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
            tools:ignore="ProtectedPermissions" />	<!-- 在SD卡中创建和删除文件的权限 -->

    <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/Theme.UpdateDemo">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <receiver
                android:name=".InstallReceiver">
            <intent-filter>
                <action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
            </intent-filter>
        </receiver>
        <provider
                android:name="androidx.core.content.FileProvider"
                android:authorities="com.imooc.updatedemo.fileProvider"
                android:exported="false"
                android:grantUriPermissions="true">
            <meta-data
                    android:name="android.support.FILE_PROVIDER_PATHS"
                    android:resource="@xml/file_paths" />
        </provider>
    </application>

</manifest>

4.创建FileProvider的xml(定义共享文件路径)文件:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path
            name="external"
            path="Android/data/com.imooc.updatedemo/files" />

    <external-path
            name="root"
            path="Download" />
    <external-path
            name="external.Download"
            path="Android/data/com.imooc.updatedemo/files/Download" />

</paths>

5.使用DownLoadManager或者OkHttpClient()进行下载:

//参考文章:https://www.jianshu.com/p/b57628947aec
class MainActivity : AppCompatActivity() {

    private var downloadId: Long? = null
    private lateinit var downLoadManager: DownloadManager
    private val handler: Handler = object : Handler() {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            when(msg.what) {
                0 -> {
                    val status = msg.obj
                    Log.i("hy55", "status=$status")
                    if (status == DownloadManager.STATUS_RUNNING || status == DownloadManager.STATUS_PAUSED || status == DownloadManager.STATUS_PENDING) {
                        val num1 = msg.arg1
                        Log.i("hy55", "num1=$num1")
                        val num2 = msg.arg2
                        Log.i("hy55", "num2=$num2")
                        val progress = (num1 * 1.0f / num2 * 100).toInt()
                        Log.i("hy55", "progress=$progress")
                        runOnUiThread {
                            findViewById<TextView>(R.id.downloadtext).text = "$progress%"
                            findViewById<ProgressBar>(R.id.downloadBar).progress = progress
                            findViewById<TextView>(R.id.progressText).text = "$progress%"
                        }
                    } else {
                        showMessage("下载完毕")
                        runOnUiThread {
                            findViewById<TextView>(R.id.downloadtext).text = "100%"
                            findViewById<ProgressBar>(R.id.downloadBar).progress = 100
                            findViewById<TextView>(R.id.progressText).text = "100%"
                        }
                    }
                }
            }
        }
    }

    private val runnable = object : Runnable {
        override fun run() {
            val query = DownloadManager.Query()
            query.setFilterById(downloadId!!)
            val cursor = downLoadManager.query(query)
            if (cursor == null) {
                showMessage("下载失败")
            } else {
                val bytesAndStatus = getDownloadProgress(cursor)
                val msg = handler.obtainMessage(0, bytesAndStatus[0], bytesAndStatus[1], bytesAndStatus[2])
                handler.sendMessage(msg)
            }
            handler.postDelayed(this, 100)
        }
    }

    override fun onStop() {
        super.onStop()
        //清除Handler,取消广播
        handler.removeCallbacksAndMessages(null)
    }

    override fun onDestroy() {
        super.onDestroy()
        //清除Handler,取消广播
        handler.removeCallbacksAndMessages(null)
        this.unregisterReceiver(downloadStatusReceiver)
    }

    private val downloadStatusReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            checkStatus()
        }
    }


    private fun showMessage(str: String) {
        Toast.makeText(this, str, Toast.LENGTH_SHORT).show()
    }

    private fun checkStatus() {
        val query = DownloadManager.Query()
        query.setFilterById(downloadId!!)
        val cursor = downLoadManager.query(query)
        if(cursor.moveToFirst()) {
            val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
            when (status) {
                DownloadManager.STATUS_PAUSED -> {
                    showMessage("下载暂停")
                }

                DownloadManager.STATUS_PENDING -> {
                    showMessage("下载延迟")
                }

                DownloadManager.STATUS_RUNNING -> {
                    showMessage("正在下载")
//                    getDownloadProgress(cursor)
                }

                DownloadManager.STATUS_SUCCESSFUL -> {
                    showMessage("下载成功")
//                    val loadRoot = Environment.getExternalStorageDirectory()?.absolutePath + File.separator + "Download"
                    val loadRoot = getExternalFilesDir(null)?.absolutePath + File.separator + "Download"
                    val loadFile = File(loadRoot, "updateDemo.apk")
                    installApk(applicationContext, loadFile)
                }

                DownloadManager.STATUS_FAILED -> {
                    showMessage("下载失败")
                }
            }
        }
    }

    private fun getDownloadProgress(cursor: Cursor): IntArray {
        val bytesAndStatus = IntArray(3)
        try{
            if (cursor.moveToFirst()) {
                //已下载字节数
                val downloadBytesIdx = cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
                //总的字节数
                val totalBytesIdx = cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
                val status = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
                bytesAndStatus[0] = cursor.getLong(downloadBytesIdx).toInt()
                bytesAndStatus[1] = cursor.getLong(totalBytesIdx).toInt()
                bytesAndStatus[2] = cursor.getLong(status).toInt()
                Log.i("hy55", "bytesAndStatus=$bytesAndStatus")
            }
        } finally {
            if (cursor != null) {
                cursor.close()
            }
        }
        return bytesAndStatus
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        downLoadManager = getSystemService(DOWNLOAD_SERVICE) as DownloadManager
        //点击下载apk
        findViewById<Button>(R.id.btn).setOnClickListener {
            val uri = Uri.parse("https://rel.huya.com/apk/live.apk")
            val request = DownloadManager.Request(uri)
            request.setAllowedOverRoaming(true)
            request.setNotificationVisibility(1)
            //设置文件类型
            val mimeTypeMap = MimeTypeMap.getSingleton()
            val mimeString = mimeTypeMap.getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl("https://rel.huya.com/apk/live.apk"))
            request.setMimeType(mimeString)
            request.setTitle("正在下载")
            request.setVisibleInDownloadsUi(true)
            //setDestinationInExternalPublicDir()->Environment.DIRECTORY_DOWNLOADS:文件路径/storage/emulated/0/Download/updateDemo.apk
            //setDestinationInExternalFilesDir->Environment.DIRECTORY_DOWNLOADS:文件路径:/storage/emulated/0/Android/data/com.imooc.updatedemo/files/Download/updateDemo.apk
            request.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, "updateDemo.apk")
            // 将下载请求放入队列
            downloadId = downLoadManager.enqueue(request)
            Log.i("hy55", "downloadId=$downloadId")
            handler.post(runnable)
            //检查初始下载状态
            checkStatus()
            //注册下载完成的广播,进行apk安装
            this.registerReceiver(downloadStatusReceiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
        }

        //使用Http请求下载
        findViewById<Button>(R.id.btn2).setOnClickListener {

            val client = OkHttpClient()
            val request = Request.Builder()
                .url("https://rel.huya.com/apk/live.apk")
                .build()
            client.newCall(request).enqueue(object : Callback {
                override fun onFailure(call: okhttp3.Call, e: IOException) {

                }

                override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) {
                    Log.i("hy55", "onResponse response=$response")
                    updateApk(response, object :  DownloadCallBack {
                        override fun onProgress(i: Int) {
                            runOnUiThread {
                                findViewById<TextView>(R.id.progressText).text = "$i%"
                            }

                        }
                    })
                }

            })
        }
    }

    private fun updateApk(response: okhttp3.Response, downloadCallBack: DownloadCallBack) {
        Log.i("hy55", "updateApk response=$response")
        downloadApk(response, downloadCallBack)
//        DownLoadUtil.downloadApk(response)
    }

    @SuppressLint("SetTextI18n")
    fun downloadApk(response: Response, downloadCallBack: DownloadCallBack) {
        Log.i("hy55", "downloadApk response=$response")
        var `is`: InputStream? = null
        var fos: FileOutputStream? = null
        val buff = ByteArray(2048)
        var len: Int
        try {
            `is` = response.body()!!.byteStream()
            Log.i("hy55", "is=$`is`")
            val file = createFile()
            Log.i("hy55", "downloadApk file=$file")
            val total = response.body()!!.contentLength()
            Log.i("hy55", "total=$total")
            val contentLength = total
            var sum: Long = 0
            fos = FileOutputStream(file)
            Log.i("hy55", "fos=$fos")
            while (`is`.read(buff).also { len = it } != -1) {
                fos.write(buff, 0, len)
                sum += len.toLong()
                val progress = (sum * 1.0f / total * 100).toInt()
                downloadCallBack.onProgress(progress)
                runOnUiThread {
                    findViewById<TextView>(R.id.downloadtext).text = "$progress%"
                    findViewById<ProgressBar>(R.id.downloadBar).progress = progress
                }
                Log.i("hy55", "progress=$progress")
            }
            fos.flush()
            Log.i("hy55", "installApk file=$file")
            //4.下载完成,安装apk

//            UpdateUtil.installUseRoot(file.absolutePath)
            installApk(applicationContext, file)
        } catch (e: Exception) {
            e.printStackTrace()
            //            breakpoint(downloadUrl,emitter);
        } finally {
            try {
                `is`?.close()
                fos?.close()
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }

    fun installApk(context: Context?, file: File?) {
//        val loadRoot = Environment.getExternalStorageDirectory()?.absolutePath + File.separator + "Download"
//        val loadFile = File(loadRoot, "updateDemo.apk")
//        Log.i("hy55", "loadFile=$loadFile")
        if (context == null) {
            return
        }
        val authority = context.packageName + ".fileProvider"
        Log.i("hy55", "authority=$authority")
        val apkUri = FileProvider.getUriForFile(context, authority, file!!)
        Log.i("hy55", "apkUri=$apkUri")
        val intent = Intent(Intent.ACTION_VIEW)
        intent.action = Intent.ACTION_INSTALL_PACKAGE
        intent.addCategory(Intent.CATEGORY_DEFAULT)
        Log.i("hy55", "----->>>>>1")
        try {
            //判读版本是否在7.0以上
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                Log.i("hy55", "----->>>>>2")
                intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
                intent.setDataAndType(apkUri, "application/vnd.android.package-archive")
                Log.i("hy55", "----->>>>>3")
            } else {
                intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                Log.i("hy55", "----->>>>>4")
                intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive")
            }
            context.startActivity(intent)
        } catch (e : IllegalArgumentException) {
            e.printStackTrace()
        } catch (e : ActivityNotFoundException) {
            e.printStackTrace()
        } catch (e : java.lang.Exception) {
            e.printStackTrace()
        }
        Log.i("hy55", "intent=$intent")
        Log.i("hy55", "----->>>>>5")

        //弹出安装窗口把原程序关闭,强制杀掉进程可能会导致安装包解析失败
//        android.os.Process.killProcess(android.os.Process.myPid())
    }



    private fun createFile(): File {
//        val root = Environment.DIRECTORY_DOWNLOADS
        val root = getExternalFilesDir(null)?.absolutePath
        val file = File(root, "updateDemo.apk")
        if (file.exists()) {
            file.delete()
        }
        try {
            file.createNewFile()
            return file
        } catch (e: IOException) {
            e.printStackTrace()
        }
        Log.i("hy55", "createFile file=$file")
        return file
    }

}

二、使用DownLoadManager进行下载:

注意:

//注意:/storage/emulated/0相当于sdcard/
//文件路径:/storage/emulated/0/Android/data/com.imooc.updatedemo/files/Download/updateDemo.apk
request.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, "updateDemo.apk")
//安装Apk的时候需要设置路径:/storage/emulated/0/Android/data/com.imooc.updatedemo/files/Download/updateDemo.apk
val loadRoot = getExternalFilesDir(null)?.absolutePath + File.separator + "Download"
val loadFile = File(loadRoot, "updateDemo.apk")
installApk(applicationContext, loadFile)

//注意:/storage/emulated/0相当于sdcard/
//文件路径/storage/emulated/0/Download/updateDemo.apk
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "updateDemo.apk")
//安装Apk需要设置路径:/storage/emulated/0/Download/updateDemo.apk
val loadRoot = Environment.getExternalStorageDirectory()?.absolutePath + File.separator + "Download"
val loadFile = File(loadRoot, "updateDemo.apk")
installApk(applicationContext, loadFile)


三、使用OkHttpClient()进行下载:

//注意:/storage/emulated/0相当于sdcard/
//getExternalFilesDir() -> 文件路径:/storage/emulated/0/Android/data/com.imooc.updatedemo/files/updateDemo.apk
val root = getExternalFilesDir(null)?.absolutePath
val file = File(root, "updateDemo.apk")


//注意:/storage/emulated/0相当于sdcard/
//Environment.getExternalStorageDirectory() -> 文件路径:/storage/emulated/0/Download
val loadRoot = Environment.getExternalStorageDirectory()?.absolutePath + File.separator + "Download"
val loadFile = File(loadRoot, "updateDemo.apk")

四、静默安装和断点续传:

    /**
     * 静默安装
     *
     * @param filePath
     * @return
     */
    @Throws(java.lang.Exception::class)
    fun installUseRoot(filePath: String): Boolean {
        require(!TextUtils.isEmpty(filePath)) { "Please check apk file path!" }
        var result = false
        var outputStream: OutputStream? = null
        var process: Process? = null
        var errorStream: BufferedReader? = null
        try {
            process = Runtime.getRuntime().exec("su")
            outputStream = process.outputStream
            val command = "pm install -r $filePath\n"
            outputStream.write(command.toByteArray())
            outputStream.flush()
            outputStream.write("exit\n".toByteArray())
            outputStream.flush()
            process.waitFor()
            errorStream = BufferedReader(InputStreamReader(process.errorStream))
            val msg = StringBuilder()
            var line: String?
            while (errorStream.readLine().also { line = it } != null) {
                msg.append(line)
            }
            //            Logger.d("install msg is " + msg);
            if (!msg.toString().contains("Failure")) {
                result = true
            }
        } finally {
            try {
                outputStream?.close()
                errorStream?.close()
            } catch (e: IOException) {
                result = false
                process!!.destroy()
            }
        }
        return result
    }


    private fun breakpoint(downloadUrl: String, emitter: ObservableEmitter<Int>) {
        val client = OkHttpClient()
        val request = Request.Builder()
            .url(downloadUrl)
//            .addHeader("RANGE", "bytes=" + downloadLength.toString() + "-" + contentLength)
            .build()
        client.newCall(request).enqueue(object : Callback() {
            override fun onFailure(call: Call?, e: IOException?) {
                //下载失败
                breakpoint(downloadUrl, emitter)
            }

            @Throws(IOException::class)
            override fun onResponse(call: Call?, response: Response) {
                if (response.body() == null) {
                    //下载失败
                    breakpoint(downloadUrl, emitter)
                    return
                }
                var `is`: InputStream? = null
                var randomFile: RandomAccessFile? = null
                val buff = ByteArray(2048)
                var len: Int
                try {
                    `is` = response.body()!!.byteStream()
                    val root = Environment.getExternalStorageDirectory().path
                    val file = File(root, "updateDemo.apk")
                    randomFile = RandomAccessFile(file, "rwd")
                    randomFile.seek(downloadLength)
                    val total: Long = contentLength
                    var sum: Long = downloadLength
                    while (`is`.read(buff).also { len = it } != -1) {
                        randomFile.write(buff, 0, len)
                        sum += len.toLong()
                        val progress = (sum * 1.0f / total * 100).toInt()
                        //下载中,更新下载进度
                        emitter.onNext(progress)
                        downloadLength = sum
                    }
                    //4.下载完成,安装apk
                    installApk(this@MainActivity, file)
                } catch (e: java.lang.Exception) {
                    e.printStackTrace()
                    breakpoint(downloadUrl, emitter)
                } finally {
                    try {
                        `is`?.close()
                        randomFile?.close()
                    } catch (e: java.lang.Exception) {
                        e.printStackTrace()
                    }
                }
            }
        })
    }

 类似资料: