android 应用版本更新的记录

Categories: android

开始

公司的APP发布在蒲公英平台管理的本来用的是蒲公英SDK里面的自带的下载更新功能。但是最近老是出问题,而已UI方面也不符合自己应用的情况,所以就决定自己改造下,下载以及更新的流程和代码。

缓存目录

文件下载缓存起来,一般我们用的有这样的几个方式

    /**
     * 应用内部缓存目录
     */
    fun appCacheDir(context: Context): File?  = context.cacheDir

    /**
     * 应用外部缓存目录
     */
    fun appExternalCacheDir(context: Context): File? = context.externalCacheDir

    /**
     * sd卡根目录
     */
    fun sdcardDir() = Environment.getExternalStorageDirectory()

从android 6.0开始系统加入了动态权限验证,需要读写外部存储的时候就需要验证权限了,为了减少用户验证权限,所以sd卡存储就pass了。应用内部缓存目录,写文件没有问题,不需要验证权限,但是这个目录外部访问有问题,我们通过Intent执行action操作,然后这个Intent传输的Uri指向的文件,外部应用访问不到。所有最后决定用应用外部缓存目录来存放这个下载下来的文件。

然后,android 7.0 开始读取Uri方式有改变了,原来我们一直使用的Uri.fromFile不能用了,需要我们使用provider才能访问。所有访问uri代码就有改动:

manifest需要配置一个provider:

<provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="net.zoneland.x.bpm.mobile.v1.zoneXBPM.fileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

file_paths.xml:

<paths>
    <external-path name="external_path" path="."/>
    <cache-path name="cache_path" path="."/>
</paths>

访问uri:

uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            FileProvider.getUriForFile(context, "net.zoneland.x.bpm.mobile.v1.zoneXBPM.fileProvider", file)
        } else {
            Uri.fromFile(file)
        }

通知显示下载进度

下载进度条一般都在通知栏中显示,因为直接用Dialog在应用中启动下载,就把UI给阻塞了,用户必须得等待,体验不好。通知栏显示也有个问题就是,有些用户会把通知栏禁用了就看不到,这里就多了个通知栏是否启用的判断,然后引导用户去设置界面开启,或者是在不想看到通知也可以跳过。

    /**
     * 是否开启通知
     */
    fun isNotificationEnabled(context: Context): Boolean {
        return try{
            val manager = NotificationManagerCompat.from(context)
            manager.areNotificationsEnabled()
        }catch (e: Exception) {
            true
        }
    }

如果通知被禁用,提示用户去设置:

/**
  * 提示用户去设置开启通知,也可以跳过直接后台下载
  */
fun alertNotificationProcessBar(activity: Activity) {
        MaterialDialog.Builder(activity)
                .title(activity.getString(R.string.alert_permission))
                .content(activity.getString(R.string.permission_notification))
                .positiveText(activity.getString(R.string.positive))
                .negativeText(activity.getString(R.string.cancel))
                .neutralText(activity.getString(R.string.ignore))
                .negativeColor(Color.parseColor("#9B9B9B"))
                .onPositive { dialog, which ->
                    //去设置通知权限
                    AndroidUtils.gotoSettingApplication(activity)
                }.onNeutral { dialog, which ->
                    //跳过 不开启通知显示进度条
                    toDownloadService(activity)
                }
                .show()
    }

下载服务和代码

下载过程就是启动一个Service,把apk文件下载到上面说的缓存目录中,并且在通知栏更新下载进度。


class DownloadAPKService: IntentService("DownloadAPKService") {

    companion object {
        val DOWNLOAD_SERVICE_ACTION = "net.zoneland.x.bpm.mobile.v1.zoneXBPM.action.UPDATE"
        val VERSIN_NAME_EXTRA_NAME = "VERSIN_NAME_EXTRA_NAME"
        val DOWNLOAD_URL_EXTRA_NAME = "DOWNLOAD_URL_EXTRA_NAME"
    }

    private lateinit var downloadHandler: Handler
    private val mNotificationManager: NotificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
    private var mRemoteViews: RemoteViews? = null
    private var mNotification: Notification? = null

    override fun onHandleIntent(intent: Intent?) {
        val action = intent?.action
        XLog.info("service action $action..........")
        if (DOWNLOAD_SERVICE_ACTION  == action) {
            startDownload(intent)
        }
    }

    private fun startDownload(intent: Intent) {
        downloadHandler = Handler(Looper.getMainLooper(), {msg: Message? ->
            XLog.info("receive message type:${msg?.arg1}")
            when(msg?.arg1) {
                0 ->{//create notify
                    createNotify()
                }
                1->{//update notify
                    updateNotify(msg)
                }
                2->{//finish download
                    installAPK(msg)
                }
            }

            true
        })

        val message = downloadHandler.obtainMessage()
        message.arg1 = 0 //开始下载
        downloadHandler.sendMessage(message)

        val file:File? = downloadFile(intent)
        finishUpdateMessage(file)
    }


    private fun downloadFile(intent: Intent): File? {
        
        return try {
            val versionName = intent.getStringExtra(VERSIN_NAME_EXTRA_NAME)
            val downloadUrl = intent.getStringExtra(DOWNLOAD_URL_EXTRA_NAME)
            val file  = File(
                FileUtil.appExternalCacheDir(applicationContext)?.absolutePath 
                + File.separator 
                + versionName + ".apk")
            val url = URL(downloadUrl)
            val conn = url.openConnection() as HttpURLConnection
            conn.setRequestProperty("Accept-Encoding", "identity")
            conn.connect()
            val length = conn.contentLength
            val inputStream = conn.inputStream
            val fos = FileOutputStream(file, true)
            var oldProgress = 0
            val buf = ByteArray(1024 * 8)
            var currentLength = 0
            while (true) {
                val num = inputStream.read(buf)
                currentLength += num
                // 计算进度条位置
                val progress = (currentLength / length.toFloat() * 100).toInt()
                if (progress > oldProgress) {
                    updateMessage(progress)
                    oldProgress = progress
                }
                if (num <= 0) {
                    break
                }
                fos.write(buf, 0, num)
                fos.flush()
            }
            fos.flush()
            fos.close()
            inputStream.close()
            file
        }catch (e: Exception) {
            XLog.error("下载应用安装包失败", e)
            null
        }

    }

    private fun createNotify() {
        val mBuilder = NotificationCompat.Builder(this)
        mBuilder.setSmallIcon(R.mipmap.logo)
        mBuilder.setTicker(getString(R.string.downloading_begin))
        mRemoteViews = RemoteViews(packageName, R.layout.remote_notification_download)
        mRemoteViews?.setProgressBar(R.id.progressBar_remote_notification_download, 100, 0, false)
        mBuilder.setCustomContentView(mRemoteViews)
        mNotification = mBuilder.build()
        mNotification?.flags = Notification.FLAG_NO_CLEAR
        mNotificationManager.notify(0, mNotification)

    }

    private fun updateNotify(msg:Message) {
        val flag = msg.obj
        if (flag!=null){
            flag as Int
            mRemoteViews?.setProgressBar(R.id.progressBar_remote_notification_download, 100, flag, false)
            mRemoteViews?.setTextViewText(R.id.tv_remote_notification_download_note, "正在下载 $flag %")
            mNotificationManager.notify(0, mNotification)
        }
    }


    private fun installAPK(msg: Message) {
        val file = msg.obj
        if (file!=null) {
            file as File
            mRemoteViews?.setProgressBar(R.id.progressBar_remote_notification_download, 100, 100, false)
            mRemoteViews?.setTextViewText(R.id.tv_remote_notification_download_note, "下载完成!")
            mNotification?.flags = Notification.FLAG_AUTO_CANCEL
            val uri = FileUtil.getUriFromFile(applicationContext, file)
            val intent = Intent(Intent.ACTION_VIEW)
            intent.addCategory("android.intent.category.DEFAULT")
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            val type = FileUtil.getMIMEType(file)
            intent.setDataAndType(uri, type)
            mNotification?.contentIntent = PendingIntent.getActivity(this, 0, intent, 0)
            mNotificationManager.notify(0, mNotification)
            startActivity(intent)
        }else {
            mRemoteViews?.setTextViewText(R.id.tv_remote_notification_download_note, "下载失败!")
            mNotification?.flags = Notification.FLAG_AUTO_CANCEL
            mNotificationManager.notify(0, mNotification)
        }
    }


    private fun updateMessage(progress:Int) {
        val message = downloadHandler.obtainMessage()
        message.arg1 = 1
        message.obj = progress
        downloadHandler.sendMessage(message)
    }

    private fun finishUpdateMessage(file: File?) {
        val message = downloadHandler.obtainMessage()
        message.arg1 = 2
        message.obj = file
        downloadHandler.sendMessage(message)
    }


}

The End !