Android实现app内部下载更新并通过通知栏展示进度并控制下载状态

Android实现app内部下载更新通过通知栏展示进度并控制下载状态

版本号对比

首先要实现版本更新肯定需要检查当前版本是否是最新版本,这个就需要从服务器获取到最新版本的版本号了,这里就看怎么实现网络访问和你服务器接口了,由于公司项目使用的RXJava和Retrofit,我一个菜逼也还没学会,不过能拿到接口给的值就可以了。
本地版本号就简单了,一行代码

int localVersionCode = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode

两个版本号一比较,如果本地版本低了就执行更新或更新弹窗。

更新弹窗

我这里之前本来使用了别人写好的工具UpdateAppUtils(UpdateAppUtils),UpdateAppUtils支持自定义弹窗内容和下载相关设置,详情可以去github查看。不过我们项目的需求是需要控制下载状态就是实现暂停继续以及停止下载操作,UpdateAppUtils下载操作使用了另一个第三方包FileDownloader(FileDownloader),使用前需要进行初始化,在它官方中文文档有介绍我就不说了。FileDownloader本来是支持断点续传满足我们的需求的,可是UpdateAppUtils并没有放出内部下载接口,所以我没法通过它实现控制下载状态的操作。
咋办,写都写一半了,没办法自己写下载相关方法呗,只用了UpdateAppUtils的弹窗功能(早知道我还不如自己写一个弹窗呢,这搞的真麻烦)。
UpdateAppUtils弹窗相关设置,一个UiConfig一个UpdateConfig,一个控制弹窗ui,一个控制相关逻辑,可以在它的网站上面看到相关的属性

		//更新弹窗自定义
        UiConfig uiConfig = new UiConfig();
        //弹窗类型
        uiConfig.setUiType(UiType.CUSTOM);
        //自定义弹窗布局ID
        uiConfig.setCustomLayoutId(R.layout.dialog_update_apk);
        //更新按钮设置
        uiConfig.setUpdateBtnText("更新");
        //退出按钮设置
        uiConfig.setCancelBtnText("下次");
        //更新开始提示弹窗
        uiConfig.setDownloadingToastText("下载更新开始");

        //更新配置说明
        UpdateConfig updateConfig = new UpdateConfig();
        //是否调试,输出日志
        updateConfig.setDebug(true);
        //是否强制更新
        updateConfig.setForce(false);
        //服务器apk版本号
        updateConfig.setServerVersionCode(Integer.parseInt(MyApplication.VersionCode));
        //服务器apk版本名
        updateConfig.setServerVersionName(MyApplication.versionName);
        //是否显示通知栏进度  (自定义通知栏,不使用它的)
        updateConfig.setShowNotification(false);
        //更新下载方式
        updateConfig.setDownloadBy(DownLoadBy.APP);

开启弹窗

UpdateAppUtils.getInstance()
                    .apkUrl(MyApplication.apkPath)        //你的新版下载路径
                    .updateTitle("新版本功能介绍")         //弹窗标题,如果你自定义ui那一定要看他的官方文档,有些布局id是一定要保留的
                    .updateContent(versionDescription)    //弹窗更新内容
                    .updateConfig(updateConfig)
                    .uiConfig(uiConfig)
                    //如果你有自定义另外的布局相关那么在这个listener中写出来,我这里有两个文本内容需要展示和右上角的叉叉退出按钮,因为布局有一个退出按钮所以这里拿到退出button实现perclick方法就可以了(我只想到这个办法退出弹窗,脑子太笨了)
                    .setOnInitUiListener((view, updateConfig1, uiConfig1) -> {
                        //最新版本号
                        TextView tvServerVersionName = view.findViewById(R.id.tv_update_server_version_name);
                        tvServerVersionName.setText(getResources().getString(R.string.str_newest_version) + MyApplication.versionName);
                        //本地版本号
                        TextView tvLocalVersionName = view.findViewById(R.id.tv_update_local_version_name);
                        tvLocalVersionName.setText(getResources().getString(R.string.str_local_version) + MyApplication.localVersionName);

                        buttonCancle[0] = view.findViewById(R.id.btn_update_cancel);
                        //自定义右上角退出按钮点击事件           注:如果强制更新应该设置本按钮不可见
                        ImageButton ibUpdataClose = view.findViewById(R.id.btn_update_close);
                        ibUpdataClose.setOnClickListener(new View.OnClickListener() {
                            @Override
                            public void onClick(View v) {
                                buttonCancle[0].performClick();
                            }
                        });
                    })
                    //这是更新按钮的点击事件监听,返回值为false就执行UpdateAppUtils的内部更新方法,因为我这得自己写所以直接返回true
                    .setUpdateBtnClickListener(new OnBtnClickListener() {
                        @Override
                        public boolean onClick() {
                            //不执行第三方UpdateAppUtils的更新方法,所以一律返回true
                            //我这里做了个权限检查,要通知栏弹窗肯定要获取通知栏权限啊是吧
                            if (isNotificationEnabled(mContext)) {
                                //已获得权限直接开启下载
                                MyApplication.notificationUpdateApkUtils.startFileDownload();
                                Toast.makeText(mContext, "开始下载", Toast.LENGTH_SHORT).show();
                                buttonCancle[0].performClick();
                            } else {
                                //未获得权限进行权限页面跳转
                                Toast.makeText(mContext, "请给与系统显示通知权限,以获得通知栏更新进度条", Toast.LENGTH_SHORT).show();
                                PermissionUtil.gotoPermission(mContext);
                            }
                            return true;
                        }
                    })
                    .update();

通知栏权限获取

    private boolean isNotificationEnabled(Context context) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            //8.0手机以上
            if (((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).getImportance() == NotificationManager.IMPORTANCE_NONE) {
                return false;
            }
        }
        String CHECK_OP_NO_THROW = "checkOpNoThrow";
        String OP_POST_NOTIFICATION = "OP_POST_NOTIFICATION";

        AppOpsManager mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
        ApplicationInfo appInfo = context.getApplicationInfo();
        String pkg = context.getApplicationContext().getPackageName();
        int uid = appInfo.uid;

        Class appOpsClass = null;
        try {
            appOpsClass = Class.forName(AppOpsManager.class.getName());
            Method checkOpNoThrowMethod = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE,
                    String.class);
            Field opPostNotificationValue = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION);

            int value = (Integer) opPostNotificationValue.get(Integer.class);
            return ((Integer) checkOpNoThrowMethod.invoke(mAppOps, value, uid, pkg) == AppOpsManager.MODE_ALLOWED);

        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

开启apk下载

下载apk肯定是单任务下载,如果有其他需求可以去FileDownloader官网看中文文档。

    private BaseDownloadTask downloadTask;
    
    downloadTask = FileDownloader.getImpl().create(MyApplication.apkPath)
                .setPath(mSaveName)                 //文件保存路径(包括文件名和后缀哈)
                .setCallbackProgressTimes(300)      //设置整个下载过程中FileDownloadListener#progress最大回调次数
                .setMinIntervalUpdateSpeed(400)     //设置下载中刷新下载速度的最小间隔
                .setForceReDownload(true)           //强制重新下载,将会忽略检测文件是否健在
//                .addHeader("Accept-Encoding", "identity")
//                .addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36")
                .setListener(fileDownloadSampleListener);//监听下载过程
        downloadTaskId = downloadTask.start();
   
   //监听在它官网也有说明的,看一看就知道了
    private FileDownloadSampleListener fileDownloadSampleListener = new FileDownloadSampleListener() {
        @Override
        protected void pending(BaseDownloadTask task, int soFarBytes, int totalBytes) {
            super.pending(task, soFarBytes, totalBytes);
            Log.i("update_pending:", "下载开始");
            //开始下载,创建notification
            startNotification();
        }

        @Override
        protected void paused(BaseDownloadTask task, int soFarBytes, int totalBytes) {
            super.paused(task, soFarBytes, totalBytes);
            Log.i("update_paused:", "下载暂停");
            isPause = true;
            getManager().notify(NOTIFICATION_ID, notification);
        }

        @Override
        protected void progress(BaseDownloadTask task, int soFarBytes, int totalBytes) {
            super.progress(task, soFarBytes, totalBytes);
            //下载过程中传递下载进度
            progress = (int) (soFarBytes * 100.0 / totalBytes);
//            Log.i("update_progress:", "下载中" + progress + "%");
        }

        @Override
        protected void blockComplete(BaseDownloadTask task) {
            super.blockComplete(task);
            Log.i("update_bloackcompleted:", "update_bloackcompleted");
        }

        @Override
        protected void completed(BaseDownloadTask task) {
            super.completed(task);
            isPause = false;
            countClick = 0;
            Log.i("update_countClickLess", String.valueOf(countClick));
            Log.i("update_completed:", "下载完成");
            //下载完成关闭notification并执行安装程序
            stopNotification();
            getManager().cancel(NOTIFICATION_ID);
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
                getManager().deleteNotificationChannel(id);
            }
            installApk(mSaveName);
        }

        @Override
        protected void error(BaseDownloadTask task, Throwable e) {
            super.error(task, e);
            isPause = false;
            countClick = 0;
            Log.i("update_error:", "下载出错");
            Toast.makeText(mContext, "下载出错,请重试!", Toast.LENGTH_SHORT).show();
            delete_single();
            stopNotification();
        }

        @Override
        protected void warn(BaseDownloadTask task) {
            super.warn(task);
            Log.i("update_warn:", "update_bloackcompleted");
        }     

在监听的pending(任务准备)中开启通知栏,通知栏还需要判断Android版本,Android8.0以上需要通过channel进行通知栏的启动,咱也不了解那么多,用就完了。

public void startNotification() {
        Intent intentNotifi = new Intent(ACTION_BUTTON);
        intentNotifi.putExtra(INTENT_BUTTONID_TAG, NOTIFI_CLICK_ID);
        PendingIntent pi = PendingIntent.getBroadcast(mContext, NOTIFI_CLICK_ID,
                intentNotifi, PendingIntent.FLAG_UPDATE_CURRENT);
        if (Build.VERSION.SDK_INT >= 26) {
            int importance = NotificationManager.IMPORTANCE_HIGH;
            NotificationChannel mChannel = new NotificationChannel(id, name, importance);
            mChannel.enableLights(false);                                  //消息栏通知闪光灯设置
            mChannel.enableVibration(false);                               //消息栏通知震动设置
            mChannel.setVibrationPattern(new long[]{0});                   //消息栏通知振动模式设置
            mChannel.setSound(null, null);                                 //消息栏通知声音设置
            getManager().createNotificationChannel(mChannel);
            notification = new NotificationCompat.Builder(mContext, id)
                    .setSmallIcon(R.mipmap.logo_round)
                    .setWhen(System.currentTimeMillis())
                    .setContent(getContentView())     //这儿是自定义通知栏view的生成和相关绑定
                    .setContentIntent(pi)
                    .setOngoing(true)
                    .setChannelId(mChannel.getId())
                    .build();
            getManager().notify(NOTIFICATION_ID, notification);  //执行一次布局刷新
        } else {
            notification = new NotificationCompat.Builder(mContext, id)
                    .setSmallIcon(R.mipmap.logo_round)
                    .setWhen(System.currentTimeMillis())
                    .setContent(getContentView())
                    .setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE)
                    .setContentIntent(pi)
                    .setOngoing(true)
                    .build();
            getManager().notify(NOTIFICATION_ID, notification);
        }
        getManager().notify(NOTIFICATION_ID, notification);
        //通知栏启动后开启计时,1秒执行一次刷新数据   runable里做刷新和写一个posetdelayed,等于说这个handler无限循环直到对handle做removeCallbacks操作
        handlerUpdate.postDelayed(runnable, 1000);
    }

	//这是我的自定义通知栏view,布局不能太高哈,通知栏有几种不同模式,每种都不一样这里就不说了。
    private RemoteViews getContentView() {
        remoteViews = new RemoteViews(getPackageName(), R.layout.view_update_notification);
        ButtonBroadcastReceiver receiverPause = new ButtonBroadcastReceiver(); //注册广播接收监听
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(ACTION_BUTTON);
        mContext.registerReceiver(receiverPause, intentFilter);
        Intent intentPause = new Intent(ACTION_BUTTON); //这是我通知栏暂停继续按钮传递的标识
        intentPause.putExtra(INTENT_BUTTONID_TAG, BUTTON_CONTINUE_ID);
        Intent intenStop = new Intent(ACTION_BUTTON);  //停止按钮
        intenStop.putExtra(INTENT_BUTTONID_TAG, BUTTON_STOP_ID);
        Intent intenLayout = new Intent(ACTION_BUTTON); //整个布局(我项目需要实现点击布局直接暂停下载)
        intenLayout.putExtra(INTENT_BUTTONID_TAG, LAYOUT_ID);

        PendingIntent pendingIntentLayout = PendingIntent.getBroadcast(mContext, LAYOUT_ID, intenLayout, PendingIntent.FLAG_UPDATE_CURRENT);
        PendingIntent pendingIntentPause = PendingIntent.getBroadcast(mContext, BUTTON_CONTINUE_ID, intentPause, PendingIntent.FLAG_UPDATE_CURRENT);
        PendingIntent pendingIntentStop = PendingIntent.getBroadcast(mContext, BUTTON_STOP_ID, intenStop, PendingIntent.FLAG_UPDATE_CURRENT);
        //下面是通过id绑定相关监听事件
        remoteViews.setOnClickPendingIntent(R.id.rl_update_notification, pendingIntentLayout);
        remoteViews.setOnClickPendingIntent(R.id.btn_update_notification_stop, pendingIntentStop);
        remoteViews.setOnClickPendingIntent(R.id.btn_update_notification_continue, pendingIntentPause);
        return remoteViews;
    }

下面是监听的相关处理
考虑到频繁点击触发事件时我的处理办法是用一个变量计数,用timer计时2秒每一秒执行一次加或减,在事件处理时判断一下变量的值,为0才可以暂停,为2才可以继续

public class ButtonBroadcastReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (action.equals(ACTION_BUTTON)) {
            	//刚刚的每一个intent都有个id是不一样的自己设置
                int buttonid = intent.getIntExtra(INTENT_BUTTONID_TAG, 0);
                switch (buttonid) {
                    case BUTTON_CONTINUE_ID:
                        Log.i("update_buttonclick:", "继续按钮被点击");
                        //继续按钮点击事件
                        //下载暂停标识为true表示当前下载任务暂停,做继续下载操作
                        if (isPause) {
                            Log.i("update_pauseclick:", "继续被点击");
                            //判断下载进程是否在暂停过程中
                            if (FileDownloader.getImpl().getStatusIgnoreCompleted(downloadTaskId) == FileDownloadStatus.paused) {
                                //countClick = 2表示当前可继续,在内部执行减计数器,减到0时点击布局才能触发点击事件
                                if (countClick == 2) {
                                    isPause = false;
                                    Log.i("update_pauseclicktrue:", "执行继续方法");
                                    //下载开始了继续刷新handler
                                    handlerUpdate.postDelayed(runnable, 1000);
                                    //继续下载切换进度条相关布局
                                    .........
                                    startFileDownload();//继续按钮可点击时执行的开始下载,因为FileDownloader支持断点续传所以直接调用开始下载方法就会继续

                                }
                            } else {
                                Toast.makeText(mContext, "请勿频繁点击!", Toast.LENGTH_SHORT).show();
                            }
                        }
                        break;
                    case BUTTON_STOP_ID:
                        isPause = false;
                        Log.i("update_buttonclick:", "停止按钮被点击");
                        //停止按钮点击事件:
                        delete_single();//这是删除当前下载id记录,清除下载任务和下载文件
                        stopNotification();//停止通知栏
                        break;
                    case NOTIFI_CLICK_ID:
                        //通知栏条目点击事件
//                        Toast.makeText(mContext, "条目被点击", Toast.LENGTH_SHORT).show();
                        break;
                    case LAYOUT_ID:
                        Log.i("update_buttonclick:", "整个布局被点击");
                        //整个条目布局点击事件 只有在非暂停状态才能点击切换
                        //isPause为false时做暂停操作并切换布局
                        if (!isPause) {
                            Log.i("update_layoutclick:", "布局被点击");
                            //判断下载线程是否在下载过程中
                            if (FileDownloader.getImpl().getStatusIgnoreCompleted(downloadTaskId) == FileDownloadStatus.progress) {
                                //countClick = 0表示当前可暂停   在内部执行加计数器,加到2时点击继续才可以触发下载事件
                                if (countClick == 0) {
                                    isPause = true;
                                    Log.i("update_layoutclicktrue:", "执行暂停方法");
                                    //下载暂停了移除刷新handler
                                    handlerUpdate.removeCallbacks(runnable);
                                    //暂停下载切换到暂停相关按钮布局
                                    .........
                                    pauseFileDownload();//暂停下载方法
                                }
                            }
                        }
                        break;
                    default:
                        break;
                }
            }
        }
    }

/**
     * 删除当前downloadTaskId的记录
     */
    public void delete_single() {
        /**
         *  延迟点击计数设置为0,因为整个util在application中初始化,
         *  当不退出后台而重新进入下载更新时不会重新设置计数初始值,
         *  这样在点击事件判断时就会出错,所以在下载任务被终止与完成时设定计数为初始值0.
         */
        countClick = 0;
        Log.i("update_countClickLess", String.valueOf(countClick));
        //清除掉downloadid的下载任务
        boolean deleteData = FileDownloader.getImpl().clear(downloadTaskId, mSavePath);
        File targetFile = new File(mSaveName);
        boolean delete = false;
        //删除下载文件
        if (targetFile.exists()) {
            delete = targetFile.delete();
        }
        new File(FileDownloadUtils.getTempPath(mSaveName)).delete();
    }
    /**
     * 暂停FileDowloader下载
     */
    public void pauseFileDownload() {
        FileDownloader.getImpl().pause(downloadTaskId);
        getManager().notify(NOTIFICATION_ID, notification);
    }
        /**
     * apk下载过程发送下载进度并更新
     *
     * @param progress
     */
    public void sendDownLoadProgress(int progress) {
        remoteViews.setTextViewText(R.id.tv_update_notification, "正在下载:" + progress + "%");
        remoteViews.setProgressBar(R.id.pb_update_notification, 100, progress, false);
        getManager().notify(NOTIFICATION_ID, notification);
    }

    /**
     * app下载完成,出错或点击停止时退出通知栏
     */
    public void stopNotification() {
        //通知栏退出时关闭计时刷新
        handlerUpdate.removeCallbacks(runnable);
        getManager().cancel(NOTIFICATION_ID);
    }

本人是个菜鸟,记忆力太差这边只是做个记录,如果有什么错误或更好的方法可以指出来。

代码github地址:https://github.com/big-guogai/NotificationUpdateApkUtils

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值