《第一行代码(第2版)》下载示例适配android8.0加简易服务端实现apk自动下载

本人新手,入行不到三个月,项目需要实现app自动更新。于是就想到抄书上这个下载示例,结果公司现在的板子用的8.0系统,运行起来直接ANR,自己又写了一个简易spring boot服务端,一个星期左右才完成的差不多。(初步翻了一下第3版好像没这个示例了,话说第2版我都没看完,第3版又出来了)。

趁周日(翘加班)把可以分享的分享一下。

首先android8.0服务的正确打开方式变了:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    
    
            //android8.0以上通过startForegroundService启动service
            startForegroundService(intent);
        } else {
    
    
            startService(intent);
        }
bindService(intent,connection,BIND_AUTO_CREATE);//绑定服务

然后是通知的适配,原:

NotificationCompat.Builder builder = new NotificationCompat.Builder(this);

需要增加一个channelId,下面的写法来源于搜索,郭神博客有更详细的写法(我没仔细看):

if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    
    
            NotificationChannel notificationChannel = new NotificationChannel("channelid1","channelname",NotificationManager.IMPORTANCE_HIGH);
            getNotificationManager().createNotificationChannel(notificationChannel);
        }
        //channelname 是会显示在通知权限页面的
NotificationCompat.Builder builder = new NotificationCompat.Builder(this,"channelid1");

接着是解决ANR错误,搜索了好久发现有一个5秒内必须调用一次startForeground()的新特性
所以,重写onCreate,让服务在创建时就调用,果然问题解决。上代码:

    @Override
    public void onCreate() {
    
    
        super.onCreate();
        startForeground(1,getNotification("Waiting...",-1));
    }

接下去就是项目功能,实现自主下载apk,下载前需要2个前提:

  1. app要知道有一个新版本存在
  2. app要知道新版本的url

于是我们要一个服务端项目,服务端先不说。书上的示例代码,是由button启动通过Binder往服务中传入一个url,然后通过url获取fileName。只有fileName里面带有版本信息,2个前提就都解决了。于是,扩展DownLoadTask(请忽略错误的大小写L):

     fileName = url.substring(url.lastIndexOf("/"));
     //可以指定别的路径
     String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
     f (fileName.isEmpty()){
    
    
           return TYPE_FAILED_FILENAME_NULL;
     }else {
    
    
           if (compareVersion(getVersion(fileName),packageName(MyApplication.getInstance())) != 1){
    
    
                //APP 不需要更新
                 return TYPE_FAILED_IS_NEW;
           }
           File fileList = new File(directory + "/");
           List<File> f = getFileList(fileList.listFiles());
           int i = 0;
           while (i < f.size()) {
    
    
               File apkFile = f.get(i);
               if (fileName.contains(apkFile.getName())){
    
    
                   return TYPE_FAILED_IS_NEW;
                   }else if (apkFile.getName().contains(".part")){
    
    
                   if (compareVersion(getVersion(fileName),packageName(MyApplication.getInstance())) == 1){
    
    
                      apkFile.delete();
                      }
                }else {
    
    
                   apkFile.delete();
               }
               i ++;
               }
             fileName = fileName.substring(0,fileName.length()-3);//去掉apk
             fileName = fileName + "part";//加上part
           }
           //这里还有其他代码     
       }

简单说一下上面的代码

  1. 通过url获取fileName(其实是/+fileName)
  2. 正确性检查,如果空返回一个失败信息,在DownLoadTask的onPostExecute中接受再用listener的onFailed打印错误信息(这里修改了书上的监听接口)
  3. 对比fileName的VersionName,判断是否需要更新。
  4. 需要更新,遍历下载目录下所有文件,如果有文件名被fileName包含(fileName多一个/),则不需要下载。如果有未完成的下载APK也就是.part文件,则核对版本。相等的留着续传,其他文件统统删除。
  5. 最后需要下载了,生成.part文件名

最后在下载完成后,重命名文件名为apk:

fileName = fileName.substring(0,fileName.length()-4);//去掉part
fileName = fileName + "apk";//加上apk
File newFile = new File(directory + fileName);
if (file.renameTo(newFile)) {
    
    
      return TYPE_SUCCESS;
      }else {
    
    
      return TYPE_FAILED;
}

getVersion方法因为我固定的文明格式(app名称_V版本号_打包时间_备注信息),所以我的getVersion比较简单,大家可以根据自己格式或者更智能的抓出来。

    private String getVersion(String name) {
    
    
        int index=name.indexOf('_');
        if (index==-1) return "";
        int index2= name.indexOf('_',index+1);
        if (index2 == -1){
    
    
            return name.substring(index+1);
        }else if (index2>index+1)
            return name.substring(index+2,index2);
        else
            return "";
    }

packageName(Context context)方法来源是搜索,MyApplication.getInstance()是全局获取的方式,书上有。

    private String packageName(Context context) {
    
    
        PackageManager manager = context.getPackageManager();
        String name = null;
        try {
    
    
            PackageInfo info = manager.getPackageInfo(context.getPackageName(), 0);
            name = info.versionName;
        } catch (PackageManager.NameNotFoundException e) {
    
    
            e.printStackTrace();
        }

        return name;
    }

compareVersion(String v1,String v2)也是面向百度编程的(如果有版权问题请留言):

    public int compareVersion(String v1,String v2){
    
    
        int i=0,j=0,x=0,y=0;
        int v1Len=v1.length();
        int v2Len=v2.length();
        char c;
        do {
    
    
            while(i<v1Len){
    
    //计算出V1中的点之前的数字
                c=v1.charAt(i++);
                if(c>='0' && c<='9'){
    
    
                    x=x*10+(c-'0');//c-‘0’表示两者的ASCLL差值
                }else if(c=='.'){
    
    
                    break;//结束
                }else{
    
    
                    //无效的字符
                }
            }
            while(j<v2Len){
    
    //计算出V2中的点之前的数字
                c=v2.charAt(j++);
                if(c>='0' && c<='9'){
    
    
                    y=y*10+(c-'0');
                }else if(c=='.'){
    
    
                    break;//结束
                }else{
    
    
                    //无效的字符
                }
            }
            if(x<y){
    
    
                return -1;
            }else if(x>y){
    
    
                return 1;
            }else{
    
    
                x=0;y=0;
                continue;
            }

        } while ((i<v1Len) || (j<v2Len));
        return 0;
    }

遍历文件夹的方法:

public List<File> getFileList(File[] oriFile) {
    
    
        List<File> fileList = new ArrayList<>();
        for (File file : oriFile) {
    
    
            if (file.isDirectory()) {
    
    
                File[] childFileArr = file.listFiles();
                Collections.addAll(fileList, childFileArr);
            } else if (file.isFile()) {
    
    
                Collections.addAll(fileList, file);
            }
        }
        return fileList;
    }

项目服务需要自启动,而不是要用户去点击,需要把原开始服务的代码放入ServiceConnection对象重写的onServiceConnected方法中去:

    private ServiceConnection connection = new ServiceConnection() {
    
    
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
    
    
            downloadBinder = (DownLoadService.DownloadBinder)service;

            String url = "http://xxx.xxxx.com:8888/getVersionInfo";
            downloadBinder.startDownload(url);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
    
    

        }
    };

最后是下载失败的处理改写了一下,1个小时重试下载(测试用的5秒,不知道1小时会不会被杀掉):

        @Override
        public void onFailed(int cause) {
    
    
            downLoadTask = null;
            //下载失败时将前台服务通知关闭,并创建一个下载失败的通知
            stopForeground(true);
            String tip = "Download Failed";
            switch (cause) {
    
    
                case DownLoadTask.TYPE_FAILED_IS_NEW:
                    tip = "不需要更新";
                    break;
                case DownLoadTask.TYPE_FAILED:
                    tip = "Download Failed";
                    break;
                case DownLoadTask.TYPE_FAILED_URL_NULL:
                    tip = "失败,无法获取更新地址";
                    break;
                case DownLoadTask.TYPE_FAILED_FILE_ERROR:
                    tip = "失败,资源错误";
                    break;
                case DownLoadTask.TYPE_FAILED_FILENAME_NULL:
                    tip = "失败,获取不到文件名";
                    break;
                default:
                    break;
            }
            getNotificationManager().notify(1, getNotification(tip, -1));
            Toast.makeText(DownLoadService.this, tip, Toast.LENGTH_SHORT).show();
            if (cause == DownLoadTask.TYPE_FAILED_IS_NEW) {
    
    
                stopSelf();
            }else {
    
    
                new Thread(new Runnable() {
    
    
                    @Override
                    public void run() {
    
    
                        try {
    
    
                            Thread.sleep(3600000);
                            //Thread.sleep(5000);
                            downLoadTask = new DownLoadTask(listener);
                            downLoadTask.execute(downloadUrl);
                            startForeground(1,getNotification("Downloading...",0));
                        } catch (InterruptedException e) {
    
    
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
        }

最后的一个问题是url是写死的,下一次版本的名字是变动的,上面我传入的url是getVersionInfo。getVersionInfo是我写的服务端的一个Controller,返回的是一个url字符串。这样我们即时是写死一个url,但是可以通过服务端来实现新版本下载。具体代码参考原有的getContentLength(String downloadUrl)实现。

服务端另写,客户端完整源码下载:
https://github.com/markrenChina/ServiceBestPractice

快速调用的方法:

  1. 复制DownLoadListener.class , DownLoadService.class , DownLoadTask.class到自身项目
  2. 修改DownLoadService下的new Intent的第二个参数,点击通知想弹出的活动。
  3. 在DownLoadTask.class 适配你的全局Context
  4. 在AndroidManifest.xml 注册服务
  5. 在需要打开服务的活动中启动服务(onDestroy记得调用unbindService(connection))
  6. 自己看着修改

猜你喜欢

转载自blog.csdn.net/weixin_42404974/article/details/106172675