安卓APP自动更新实现

一、参考文献

简单实现安卓app自动更新功能 - 简书

安卓app自动更新功能完美实现_白云天的博客-CSDN博客_android 自动更新

Android 实现自动更新及强制更新功能_farley的成长之路-CSDN博客_android开发自动更新

二、主要思路

  1. 查询线上版本号,然后拿本地版本号与之对比。
  2. 若线上版本号比本地版本号大,则下载线上版本号
  3. 把下载好的版本号安装,并替换当前旧版本

三、 服务器端

 1、更新的APK 放在服务器端

       Android 中tomcat搭建本地服务器 实现apk更新下载_lizhenmingdirk的专栏-CSDN博客

选用 Apache Tomcat 搭建一个本地的服务器,测试用。

Apache Tomcat官方地址:  Apache Tomcat® - Welcome!  我下载的为最新版本:7.0.40

     (1)下载

     (2)任意解压在磁盘上
     (3)在tomcat的解压文件中,找到bin目录。在bin文件下面找到startup.bat 。双击即可启动

     (4)在浏览器中输入:http://localhost:8080/ 出现一个tomcat的介绍页面 ,就表示成功了。

      (5)把你需要更新的 apk放入 apache-tomcat-7.0.40\webapps\ROOT 文件夹下,默认访问的文件夹。

       (6) 访问:浏览器输入 http://localhost:8080/test.apk  弹出下载界面,那就成功了。实际的远程服务器上则将 http://localhost:8080 更换成实际远程服务器的地址。

         注:如果你的服务器使用了shiro 框架,需要进行拦截配置,让apk文件不被拦截。

           配置 如下:  /*.apk = anon

2、服务器端设置接口返回APP版本 的更新信息

      将APP版本的更新信息保存到数据库的一个数据表中,服务器端获取数据库中APP 版本的更新信息,返回Json数据

      返回的Json 如下

{"code":0,
"message":"{\"serverVersion\":142,\"upgradeInfo\":\"1、更新的信息\",\"appname\":\"test\",\"updateUrl\":\"http://服务器端地址/test.apk\",\"updateTime\":\"2021-10-14
15:18:10\",\"id\":2,\"softwareVersion\":\"2.5\"}",
"success":true}

四、Android端

 1、权限

       需要用到的权限应该有网络权限、本地文件写入权限,本地文件读取权限,请求安装包权限。使用网络权限去获取线上的版本号,然后下载保存到本地,安装的时候再去本地取来。Android 6.0后需要动态申请权限。

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

2、获取本地版本号

 /**
     * 获取本地版本号
     * @return 版本号
     */
    public static int getLocalVersionCode(Context context){
        PackageInfo pkg;
        int versionCode = 0;
        try {
            pkg = context.getPackageManager().getPackageInfo(BaseApplication.getInstance().getPackageName(), 0);
            versionCode = (int) pkg.getLongVersionCode();
        } catch (PackageManager.NameNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return versionCode;
    }

3、获取服务器端版本信息

其中HttpHandle是封装后的API,实际使用OkHttp 进行请求。

 public static void getServerApkUpdateInfo(){
        HttpHandle.getInstance().getDataAsync(HttpUrl.upgradeUpdateInfo, null, new ProfileModel(), new HttpCallback<ProfileModel>() {
            @Override
            public void onFailure(IOException e) {

            }

            @Override
            public void onSuccess(int code, ProfileModel result) {
                 if(result.getCode()==0) {
                     if(result.getMessage()!=null){
                         ApkUpdateModel  apkUpdateModel = JSONObject.parseObject(result.getMessage(),ApkUpdateModel.class);
                     }
                 }
            }
        });
    }

      ApkUpdateModel如下

public class ApkUpdateModel {
    private int id;
    private String appname; //APP名称
    private int serverVersion; //服务器端APP版本号
    private String softwareVersion;  
    private String updateUrl; //更新APK的下载链接
    private String upgradeInfo; //更新信息
    private String updateTime; //更新时间
}

4、APK下载、安装

若本地版本号小于服务器端版号,则弹出对话框,用户选择是否更新。选择更新,则开始下载APK。

这里单独开启了一个后台服务来进行APP 的下载和安装。

(1)APK下载

 private void downApk(String downloadUrl){
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url(downloadUrl)
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                //下载失败
                mHandler.sendEmptyMessage(DOWNLOAD_FAIL);
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if (response.body() == null) {
                    //下载失败
                    mHandler.sendEmptyMessage(DOWNLOAD_FAIL);
                    return;
                }
                InputStream is = null;
                FileOutputStream fos = null;
                byte[] buff = new byte[2048];
                int len;
                try {
                    is = response.body().byteStream();
                    createFile(true);
                    fos = new FileOutputStream(updateFile);
                    long total = response.body().contentLength();
                  //  contentLength=total;
                    long sum = 0;
                    while ((len = is.read(buff)) != -1) {
                        fos.write(buff,0,len);
                        sum+=len;
                        int progress = (int) (sum * 1.0f / total * 100);
                        //下载中,更新下载进度
                        //emitter.onNext(progress);
                        String progressStr = getMsgSpeed(sum,total);
                        Message msg = new Message();
                        msg.what = DOWNLOAD_PROGRESS;
                        msg.obj = progressStr;
                        mHandler.sendMessage(msg);

                       // downloadLength=sum;
                    }
                    fos.flush();
                    //4.下载完成,安装apk
                    mHandler.sendEmptyMessage(DOWNLOAD_COMPLETE);
                   // installApk(TestActivity.this,file);
                } catch (Exception e) {
                    e.printStackTrace();
                    mHandler.sendEmptyMessage(DOWNLOAD_FAIL);
                } finally {
                    try {
                        if (is != null)
                            is.close();
                        if (fos != null)
                            fos.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        });

    }


 /**
     * 创建file文件
     * @param sd_available    sdcard是否可用
     */
    private void createFile(boolean sd_available) {
        if (sd_available) {
            updateDir = new File(Environment.getExternalStorageDirectory(),
                    "app");
        } else {
            updateDir = getFilesDir();
        }
        updateFile = new File(updateDir.getPath(), appName + ".apk");
        if (!updateDir.exists()) {
            updateDir.mkdirs();
        }
        if (!updateFile.exists()) {
            try {
                updateFile.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            updateFile.delete();
            try {
                updateFile.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

(2)APK 安装

Android 如何通过代码安装 APK? - 碎岁语 - 博客园

Android 7.0后需要通过如下步骤安装APK

具体的步骤大致如下:

1、配置 AndroidManifest.xml 中的 ContentProvider 信息;

 <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.xxx.xxx.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

android:name 表示FileProvider 类的完整名称。这个类可以填写两个值,一个是位于 support(android.support.v4.content.FileProvider) 包下的,另一个是位于 androidx(androidx.core.content.FileProvider) 包下的。这两种都可以填写,无区别,主要看你的工程引的是 androidx 支援包还是 support 支援包。

android:authorities表示授权者,这里的格式一般是[appId].fileprovider
android:exported只能为false
android:grantUriPermissions="true"表示授权Uri权限 ,且必须为true

meta-data里设置指定的文件目录,为引用某个xml文件这里引用file_paths
 

2、配置要开放的 paths 信息;

在工程 res 目录下新建一个 xml 目录,并在该 xml 目录下新建一个 xml 文件。文件的名称必须与第 1 步中 @xml/ 属性值中配置的一致。

xml格式如下,path:需要临时授权访问的路径(.代表所有路径) name:就是你给这个访问路径起个名字。

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

 <root-path/> 代表设备的根目录new File("/");
<files-path/> 代表context.getFilesDir()
<cache-path/> 代表context.getCacheDir()
<external-path/> 代表Environment.getExternalStorageDirectory() 
<external-files-path>代表context.getExternalFilesDirs()
<external-cache-path>代表getExternalCacheDirs()

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

</paths>

上述配置对应路径为Environment.getExternalStorageDirectory() +"/app/"。这个配置的是APK下载后的路径。

3、在 Java 代码中通过 FileProvider 封装文件信息,安装APK

public void installApk(Context context, File file) {
        if (context == null) {
            return;
        }
        String authority = getApplicationContext().getPackageName() + ".fileprovider";
        Uri apkUri = FileProvider.getUriForFile(context, authority, file);
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        //判读版本是否在7.0以上
        if (Build.VERSION.SDK_INT >= 24) {
            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);
        //弹出安装窗口把原程序关闭。
        //避免安装完毕点击打开时没反应
       /* Process.killProcess(android.os.Process.myPid());*/


    }

(3)服务

服务UpdateService.class

public class UpdateService extends Service {

    // BT字节参考量
    private static final float SIZE_BT = 1024L;
    // KB字节参考量
    private static final float SIZE_KB = SIZE_BT * 1024.0f;
    // MB字节参考量
    private static final float SIZE_MB = SIZE_KB * 1024.0f;

    private final static int DOWNLOAD_START = 0;// 完成
    private final static int DOWNLOAD_COMPLETE = 1;// 完成
    private final static int DOWNLOAD_NOMEMORY = -1;// 内存异常
    private final static int DOWNLOAD_FAIL = -2;// 失败
    private final static int DOWNLOAD_PROGRESS = 100;// 失败
    private final static int NotificationID = 2;// 失败


    private String appName = null;// 应用名字
    private String appUrl = null;// 应用升级地址
    private File updateDir = null;// 文件目录
    private File updateFile = null;// 升级文件

    // 通知栏
    private NotificationManager updateNotificationManager = null;
    private Notification updateNotification = null;
    private NotificationCompat.Builder builder;

    private Intent updateIntent = null;// 下载完成
    private PendingIntent updatePendingIntent = null;// 在下载的时候
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case DOWNLOAD_COMPLETE:
                    builder.setContentText(appName +  getString(R.string.upgrade_download_finished));
                    updateNotificationManager.notify(NotificationID,builder.build());
                    if(updateFile!=null){
                        installApk(getApplicationContext(),updateFile);
                        updateNotificationManager.cancel(NotificationID);
                    }
                    stopSelf();
                    Process.killProcess(android.os.Process.myPid());
                    break;
                case DOWNLOAD_FAIL:

                    builder.setContentText(appName +  getString(R.string.upgrade_download_failed));
                    updateNotificationManager.notify(NotificationID,
                            builder.build());
                    stopSelf();
                    break;
                case DOWNLOAD_NOMEMORY:
                    stopSelf();
                    break;
                case DOWNLOAD_PROGRESS:
                    String progressShow =(String) msg.obj;
                    builder.setContentTitle(appName+getString(R.string.upgrade_downloading));
                    builder.setContentText(progressShow);
                    updateNotificationManager.notify(NotificationID,
                            builder.build());
                    break;
                case DOWNLOAD_START:
                    Toast.makeText(getApplicationContext(),getString(R.string.upgrade_download_start),Toast.LENGTH_SHORT).show();
                    break;
            }


        }
    };

    public UpdateService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        return null;
        // throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onCreate() {
        super.onCreate();

    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        appName = intent.getStringExtra("appname");
        appUrl = intent.getStringExtra("appurl");
        updateNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

        String id = "my_channel_01";
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {  //Android 8.0以上
            NotificationChannel mChannel = new NotificationChannel(id, appName, NotificationManager.IMPORTANCE_LOW);
            Log.i("DownAPKService", mChannel.toString());
            updateNotificationManager.createNotificationChannel(mChannel);
            builder = new NotificationCompat.Builder(getApplicationContext());
            builder.setSmallIcon(R.drawable.ic_launcher);
            builder.setTicker(getString(R.string.upgrade_downloading));
            builder.setContentTitle(appName);
            builder.setContentText("0MB (0%)");
            builder.setNumber(0);
            builder.setChannelId(id);
            builder.setAutoCancel(true);
        } else {    //Android 8.0以下
            builder = new NotificationCompat.Builder(getApplicationContext());
            builder.setSmallIcon(R.drawable.ic_launcher);
            builder.setTicker(getString(R.string.upgrade_downloading));
            builder.setContentTitle(appName);
            builder.setContentText("0MB (0%)");
            builder.setNumber(0);
            builder.setAutoCancel(true);
        }

        updateNotificationManager.notify(NotificationID, builder.build());

//        new UpdateThread().execute();
        new DownloadThread().start();
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        stopSelf();
    }

  

    class DownloadThread extends Thread{
        @Override
        public void run() {
            super.run();
            mHandler.sendEmptyMessage(DOWNLOAD_START);
            downApk(appUrl);
        }
    }

    private void downApk(String downloadUrl){
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url(downloadUrl)
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                //下载失败
                mHandler.sendEmptyMessage(DOWNLOAD_FAIL);
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if (response.body() == null) {
                    //下载失败
                    mHandler.sendEmptyMessage(DOWNLOAD_FAIL);
                    return;
                }
                InputStream is = null;
                FileOutputStream fos = null;
                byte[] buff = new byte[2048];
                int len;
                try {
                    is = response.body().byteStream();
                    createFile(true);
                    fos = new FileOutputStream(updateFile);
                    long total = response.body().contentLength();
                  //  contentLength=total;
                    long sum = 0;
                    while ((len = is.read(buff)) != -1) {
                        fos.write(buff,0,len);
                        sum+=len;
                        int progress = (int) (sum * 1.0f / total * 100);
                        //下载中,更新下载进度
                        //emitter.onNext(progress);
                        String progressStr = getMsgSpeed(sum,total);
                        Message msg = new Message();
                        msg.what = DOWNLOAD_PROGRESS;
                        msg.obj = progressStr;
                        mHandler.sendMessage(msg);

                       // downloadLength=sum;
                    }
                    fos.flush();
                    //4.下载完成,安装apk
                    mHandler.sendEmptyMessage(DOWNLOAD_COMPLETE);
                   // installApk(TestActivity.this,file);
                } catch (Exception e) {
                    e.printStackTrace();
                    mHandler.sendEmptyMessage(DOWNLOAD_FAIL);
                } finally {
                    try {
                        if (is != null)
                            is.close();
                        if (fos != null)
                            fos.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        });

    }


    public void installApk(Context context, File file) {
        if (context == null) {
            return;
        }
        String authority = getApplicationContext().getPackageName() + ".fileprovider";
        Uri apkUri = FileProvider.getUriForFile(context, authority, file);
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        //判读版本是否在7.0以上
        if (Build.VERSION.SDK_INT >= 24) {
            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);
        //弹出安装窗口把原程序关闭。
        //避免安装完毕点击打开时没反应
       /* Process.killProcess(android.os.Process.myPid());*/


    }




    /**
     * 获取下载进度
     * @param downSize
     * @param allSize
     * @return
     */
    public static String getMsgSpeed(long downSize, long allSize) {
        StringBuffer sBuf = new StringBuffer();
        sBuf.append(getSize(downSize));
        sBuf.append("/");
        sBuf.append(getSize(allSize));
        sBuf.append(" ");
        sBuf.append(getPercentSize(downSize, allSize));
        return sBuf.toString();
    }

    /**
     * 获取大小
     * @param size
     * @return
     */
    public static String getSize(long size) {
        if (size >= 0 && size < SIZE_BT) {
            return (double) (Math.round(size * 10) / 10.0) + "B";
        } else if (size >= SIZE_BT && size < SIZE_KB) {
            return (double) (Math.round((size / SIZE_BT) * 10) / 10.0) + "KB";
        } else if (size >= SIZE_KB && size < SIZE_MB) {
            return (double) (Math.round((size / SIZE_KB) * 10) / 10.0) + "MB";
        }
        return "";
    }

    /**
     * 获取到当前的下载百分比
     * @param downSize   下载大小
     * @param allSize    总共大小
     * @return
     */
    public static String getPercentSize(long downSize, long allSize) {
        String percent = (allSize == 0 ? "0.0" : new DecimalFormat("0.0")
                .format((double) downSize / (double) allSize * 100));
        return "(" + percent + "%)";
    }


    private File createFile() {
        String root = Environment.getExternalStorageDirectory().getPath();
        File file = new File(root,appName+".apk");
        if (file.exists())
            file.delete();
        try {
            file.createNewFile();
            updateFile = file;
            return file;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null ;
    }


    /**
     * 创建file文件
     * @param sd_available    sdcard是否可用
     */
    private void createFile(boolean sd_available) {
        if (sd_available) {
            updateDir = new File(Environment.getExternalStorageDirectory(),
                    "app");
        } else {
            updateDir = getFilesDir();
        }
        updateFile = new File(updateDir.getPath(), appName + ".apk");
        if (!updateDir.exists()) {
            updateDir.mkdirs();
        }
        if (!updateFile.exists()) {
            try {
                updateFile.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            updateFile.delete();
            try {
                updateFile.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

开启服务 

  private void startUpdateService(Context context,String appname,String appurl){
        Intent mIntent = new Intent(context, UpdateService.class);
        mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        //传递数据
        mIntent.putExtra("appname", appname);
        mIntent.putExtra("appurl", appurl);
        context.startService(mIntent);
        enterSplashActivity();
    }

5、出现过的问题

 (1) android.os.FileUriExposedException: file:///storage/emulated/0/app/test.apk exposed beyond app through Intent.getData()

由于代码中配置的FileProvider 与AndroidManifest.xml内配置的FileProvider 不一致

(2)java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/0/app/test.apk

由于file_paths.xml内的path 配置不正确导致。

(3)Apk覆盖安装的时候,出现安装失败,与旧版本部兼容的问题

​​​​​​Android 解决apk覆盖安装的时候,出现安装失败,与旧版本部兼容的问题_Afanbaby的博客-CSDN博客

解决方案:

        1.你需要检查你的新旧apk所使用的签名文件是否是同一个。

        2.检查你的签名文件是否是发布版本

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值