第三章 Android 版本更新

96
忆念成风
0.2 2017.10.25 15:41* 字数 834

1. 概述

  在我们的Android开发过程中,版本的迭代更新是很重要的一步。产品需求提出和后续迭代的开发都需要版本的迭代更新。一般的App上传到应用商店以后,开发人员只要将新的App上传到应用市场,然后应用市场会提供App升级的操作,但是很多的App除了第三方的应用商店升级之后。我们都会有自己的版本升级操作。

2. 版本更新的思维导图

版本更新的思维导图

3.版本更新的思路

  抛开安卓这个概念,普通的更新,我们要做的就是先拿到新事物,然后将新事物替换掉旧事物,这样我们就能完成一次简单的更新了。安卓的更新也很简单。

  1. 首先我们需要得到当前的版本号或者版本名 (versionCode或者versionName)
  2. 请求服务器的更新接口,一般服务器的更新接口里面的数据,必定包含版本号的值。
  3. 将请求更新的接口最好放在启动的地方,或者主页,看自己的产品设计。
  4. 通过对比产品的版本号的比较,确定是否弹窗更新。
  5. 有时候我们的产品会要求强制更新,这是不值得推荐的,不更新就不能用,用户的体验性太差,一般后台接口中就会有是否强制更新的字符。
  6. 比较完了之后,如果服务器中的接口版本号比本地版本号大,那么我们就需要进行版本更新。
  7. 然后是对弹窗的更新数据进行优化,因为我们下载的是字节数,给用户展示的最好还是百分比这样比较直观一点。

4. 实现版本更新

  因为没有对外的服务器接口,而且不同后台的服务器返回接口不一样。我只是举个例子。

  1. 版本接口 json数据
{
      versionId : 2
      versionNum : v10.0.2      版本编号
      status : 0    //0是不强制 ,1 是强制
      delFlag : 0
      created : 1491790427
      updated : 1491790427
      downloadUrl : "http://27.221.81.15/dd.myapp.com/16891/63C4DA61823B87026BBC8C22BBBE212F.apk?mkey=575e443c53406290&f=8b5d&c=0&fsname=com.daimajia.gold_3.2.0_80.apk&p=.apk"
      content : "来测测看版本更新的内容"
}

创建一个关于版本接口的bean类


public class UpdateAppBean  implements Serializable{
    /**
     * versionId : 2
     * versionNum : v10.0.2
     * status : 0
     * delFlag : 0
     * created : 1491790427
     * updated : 1491790427
     * downloadUrl : www.baidu.com
     * content : djfkdkkkk
     */

    private int versionId;
    private String versionNum;
    private int status;
    private int delFlag;
    private int created;
    private int updated;
    private String downloadUrl;
    private String content;

    public int getVersionId() {
        return versionId;
    }

    public void setVersionId(int versionId) {
        this.versionId = versionId;
    }

    public String getVersionNum() {
        return versionNum;
    }

    public void setVersionNum(String versionNum) {
        this.versionNum = versionNum;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public int getDelFlag() {
        return delFlag;
    }

    public void setDelFlag(int delFlag) {
        this.delFlag = delFlag;
    }

    public int getCreated() {
        return created;
    }

    public void setCreated(int created) {
        this.created = created;
    }

    public int getUpdated() {
        return updated;
    }

    public void setUpdated(int updated) {
        this.updated = updated;
    }

    public String getDownloadUrl() {
        return downloadUrl;
    }

    public void setDownloadUrl(String downloadUrl) {
        this.downloadUrl = downloadUrl;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

  1. 创建一个工具类去获取版本号或版本名,也可以写在自定义的Application中。
public class AppUtils {

    private AppUtils() {
        throw new UnsupportedOperationException("你不能对我进行实例化操作");
    }

//获取VersionCode值
    public  static int  getVersionCode(Context mContext){
        if(mContext!=null){
            try {
                return   mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0).versionCode;
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
        }
        return  0;
    }

//获取Version Name
    public  static  String getVersionName(Context  mContext){
       if(mContext!=null){
           try {
               return   mContext.getPackageManager().getPackageInfo(mContext.getPackageName(),0).versionName;
           } catch (PackageManager.NameNotFoundException e) {
               e.printStackTrace();
           }
       }
       return  "";
    }
}
  1. 接下来是创建一个自定义的弹窗CommonProgressDialog
public class CommonProgressDialog extends AlertDialog {

    private static final String TAG = "CommonProgressDialog";
    private ProgressBar mProgress;
    private TextView mProgressNumber;
    private TextView mProgressPercent;
    private TextView mProgressMessage;

    private Handler   mViewUpdateHandler;

    private  int mMax;
    private CharSequence mMessage;
    private  boolean  mHasStarted;
    private  int mProgressVal;

    private  String  mProgressNumberFormat;
    private NumberFormat mProgressPercentFormat;

    protected CommonProgressDialog(Context context) {
        super(context);
        initFormats();
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.common_progress_dialog);
        mProgress = (ProgressBar) findViewById(R.id.progress);
        mProgressNumber = (TextView) findViewById(R.id.progress_number);
        mProgressPercent = (TextView) findViewById(R.id.progress_percent);
        mProgressMessage = (TextView) findViewById(R.id.progress_message);

        mViewUpdateHandler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                int progress = mProgress.getProgress();
                int max = mProgress.getMax();
                double dProgress =  (double)progress/(double)(1024*1024);
                double   dMax = (double)max/(double)(1024 * 1024);
                if(mProgressNumberFormat !=null){
                    String format = mProgressNumberFormat;
                    mProgressNumber.setText(String.format(format,dProgress,dMax));
                } else {
                    mProgressNumber.setText("");
                }

                if(mProgressNumberFormat !=null){
                    double percent = (double) progress/(double) max;
                    SpannableString  tmp = new SpannableString(
                            mProgressPercentFormat.format(percent));
                    tmp.setSpan(new StyleSpan(Typeface.BOLD),0,tmp.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                  mProgressPercent.setText(tmp);
                } else {
                    mProgressPercent.setText("");
                }
            }
        };
        onProgressChanged();
        if(mMessage != null){
            setMessage(mMessage);
        }

        if(mMax>0){
            setMax(mMax);
        }
        if(mProgressVal>0){
            setProgress(mProgressVal);
        }

    }


    private void  initFormats(){
        mProgressNumberFormat = "%1.2fM/%2.2fM";
        mProgressPercentFormat = NumberFormat.getPercentInstance();
        mProgressPercentFormat.setMaximumFractionDigits(0);
    }

    private void onProgressChanged() {
        mViewUpdateHandler.sendEmptyMessage(0);
    }

    public void setMax(int max){
        if(mProgress !=null){
            mProgress.setMax(max);
            onProgressChanged();
        }else{
            mMax = max;
        }
    }
    public  void  setProgress(int value){
        if(mHasStarted){
            mProgress.setProgress(value);
            onProgressChanged();
        } else{
            mProgressVal = value;
        }
    }
    public  void setProgressStyle(int style){
        // mProgressStyle = style;
    }

    public  void  setIndeterminate(boolean indeterminate){
        if(mProgress !=null){
            mProgress.setIndeterminate(indeterminate);
        }
    }

    @Override
    public void setMessage(CharSequence message) {
        if(mProgressMessage !=null){
            mProgressMessage.setText(message);
        } else {
            mMessage = message ;
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        mHasStarted = true;
    }

    @Override
    protected void onStop() {
        super.onStop();
        mHasStarted =false ;
    }
}

  1. 下面是对代码的处理,因为没有服务器,接口,所以这里自己模拟下载的环境。考虑到Android 6.0的权限处理问题,所以我们对权限做了请求。Android 7.0文件下载的问题。加了权限判断。
public class MainActivity extends AppCompatActivity {

    private CommonProgressDialog commonProgressDialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //进入这里之后,获取版本号是否更新
        int versionCode = AppUtils.getVersionCode(this);
        getUpdate(versionCode);
    }

    //版本更新
    private void getUpdate(int versionCode) {
        //一般情况下是在这里拿本地的版本号和服务器的版本号进行比较的,我没有服务器就模拟拿数据了
        String  newVersion="2.1";
        String content ="\n"+
        "就不告诉你我们更新了什么-。-\n" +
                "\n" +
                "----------万能的分割线-----------\n" +
                "\n" +
                "1.新产品上线了,界面全新改版\n" +
                "2.修复了若干bug,还杀了一个程序员祭天 \n";//更新内容
         String url = "http://openbox.mobilem.360.cn/index/d/sid/3429345"; //安装包下载地址
         double newVersionCode = Double.parseDouble(newVersion);
         int  cc= (int)(newVersionCode);
         if(cc!=versionCode){
             if(cc>versionCode){
                 //版本号不同,这时候我们需要开始弹窗了
                 //需要强制更新的在这里选择不同的弹窗方式
                 ShowDialog(url,content);
             }
         }
    }

    /**
     * 升级版本
     * @param url
     * @param content
     */
    private void ShowDialog(final String url, String content) {
        AlertDialog.Builder  builder = new AlertDialog.Builder(this);
        builder.setTitle("版本更新");
        builder.setMessage(content);
        builder.setPositiveButton("更新", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
               //这里进行更新的操作
                dialogInterface.dismiss();
                //提示版本更新的信息消失之后,开始在弹出本地下载的进度条。这里自定义一个dialog
                commonProgressDialog = new CommonProgressDialog(MainActivity.this);
                commonProgressDialog.setCanceledOnTouchOutside(false);
                commonProgressDialog.setTitle("正在下载");
                commonProgressDialog.setCustomTitle(
                LayoutInflater.from(MainActivity.this).inflate(R.layout.title_dialog,null)
                );
                commonProgressDialog.setIndeterminate(true);
                commonProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
                commonProgressDialog.setCancelable(true);

                //downFile(URLData.DOWNLOAD_URL);
              final DownloadTask downloadTask = new DownloadTask(MainActivity.this);
                downloadTask.execute(url);
                commonProgressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
                    @Override
                    public void onCancel(DialogInterface dialogInterface) {
                        downloadTask.cancel(true);
                    }
                });

            }
        });
        builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
                dialogInterface.dismiss();
            }
        });
        builder.create().show();

    }

    // 下载存储的文件名
    private static final String DOWNLOAD_NAME = "channelWe";

    /**
     * 下载应用
     *
     * @author Administrator
     */
    class DownloadTask extends AsyncTask<String, Integer, String> {

        private Context context;
        private PowerManager.WakeLock mWakeLock;

        public DownloadTask(Context context) {
            this.context = context;
        }

        @Override
        protected String doInBackground(String... sUrl) {
            InputStream input = null;
            OutputStream output = null;
            HttpURLConnection connection = null;
            File file = null;
            try {
                URL url = new URL(sUrl[0]);
                connection = (HttpURLConnection) url.openConnection();
                connection.connect();
                // expect HTTP 200 OK, so we don't mistakenly save error
                // report
                // instead of the file
                if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
                    return "Server returned HTTP "
                            + connection.getResponseCode() + " "
                            + connection.getResponseMessage();
                }
                int fileLength = connection.getContentLength();
                if (Environment.getExternalStorageState().equals(
                        Environment.MEDIA_MOUNTED)) {
                    file = new File(Environment.getExternalStorageDirectory(),
                            DOWNLOAD_NAME);

                    if (!file.exists()) {
                        // 判断父文件夹是否存在
                        if (!file.getParentFile().exists()) {
                            file.getParentFile().mkdirs();
                        }
                    }

                } else {
                    Toast.makeText(MainActivity.this, "sd卡未挂载",
                            Toast.LENGTH_LONG).show();
                }
                input = connection.getInputStream();
                output = new FileOutputStream(file);
                byte data[] = new byte[4096];
                long total = 0;
                int count;
                while ((count = input.read(data)) != -1) {
                    if (isCancelled()) {
                        input.close();
                        return null;
                    }
                    total += count;
                    if (fileLength > 0) // only if total length is known
                        publishProgress((int) (total * 100 / fileLength));
                       output.write(data, 0, count);

                }
            } catch (Exception e) {
                return e.toString();

            } finally {
                try {
                    if (output != null)
                        output.close();
                    if (input != null)
                        input.close();
                } catch (IOException ignored) {
                }
                if (connection != null)
                    connection.disconnect();
            }
            return null;
        }

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            PowerManager pm = (PowerManager) context
                    .getSystemService(Context.POWER_SERVICE);
            mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
                    getClass().getName());
            mWakeLock.acquire();
            commonProgressDialog.show();
        }

        @Override
        protected void onProgressUpdate(Integer... progress) {
            super.onProgressUpdate(progress);
            commonProgressDialog.setIndeterminate(false);
            commonProgressDialog.setMax(100);
            commonProgressDialog.setProgress(progress[0]);
        }

        @Override
        protected void onPostExecute(String result) {
            mWakeLock.release();
            commonProgressDialog.dismiss();
            if (result != null) {
                // 申请多个权限。
                AndPermission.with(MainActivity.this)
                        .requestCode(REQUEST_CODE_PERMISSION_SD)
                        .permission(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
                        // rationale作用是:用户拒绝一次权限,再次申请时先征求用户同意,再打开授权对话框,避免用户勾选不再提示。
                        .rationale(rationaleListener
                        )
                        .send();
                Toast.makeText(context, "您未打开SD卡权限" + result, Toast.LENGTH_LONG).show();
            } else {
                update();
            }
        }
    }

    private static final int REQUEST_CODE_PERMISSION_SD = 101;
    private static final int REQUEST_CODE_SETTING = 300;
    private RationaleListener rationaleListener = new RationaleListener() {
        @Override
        public void showRequestPermissionRationale(int requestCode, final Rationale rationale) {
            // 这里使用自定义对话框,如果不想自定义,用AndPermission默认对话框:
            // AndPermission.rationaleDialog(Context, Rationale).show();

            // 自定义对话框。
            com.yanzhenjie.alertdialog.AlertDialog.build(MainActivity.this)
                    .setTitle(R.string.title_dialog)
                    .setMessage(R.string.message_permission_rationale)
                    .setPositiveButton(R.string.btn_dialog_yes_permission, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.cancel();
                            rationale.resume();
                        }
                    })

                    .setNegativeButton(R.string.btn_dialog_no_permission, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.cancel();
                            rationale.cancel();
                        }
                    })
                    .show();
        }
    };
    //----------------------------------SD权限----------------------------------//

    @PermissionYes(REQUEST_CODE_PERMISSION_SD)
    private void getMultiYes(List<String> grantedPermissions) {
        Toast.makeText(this, R.string.message_post_succeed, Toast.LENGTH_SHORT).show();
    }

    @PermissionNo(REQUEST_CODE_PERMISSION_SD)
    private void getMultiNo(List<String> deniedPermissions) {
        Toast.makeText(this, R.string.message_post_failed, Toast.LENGTH_SHORT).show();

        // 用户否勾选了不再提示并且拒绝了权限,那么提示用户到设置中授权。
        if (AndPermission.hasAlwaysDeniedPermission(this, deniedPermissions)) {
            AndPermission.defaultSettingDialog(this, REQUEST_CODE_SETTING)
                    .setTitle(R.string.title_dialog)
                    .setMessage(R.string.message_permission_failed)
                    .setPositiveButton(R.string.btn_dialog_yes_permission)
                    .setNegativeButton(R.string.btn_dialog_no_permission, null)
                    .show();

            // 更多自定dialog,请看上面。
        }
    }

    //----------------------------------权限回调处理----------------------------------//

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[]
            grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        /**
         * 转给AndPermission分析结果。
         *
         * @param object     要接受结果的Activity、Fragment。
         * @param requestCode  请求码。
         * @param permissions  权限数组,一个或者多个。
         * @param grantResults 请求结果。
         */
        AndPermission.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
            case REQUEST_CODE_SETTING: {
                Toast.makeText(this, R.string.message_setting_back, Toast.LENGTH_LONG).show();
                //设置成功,再次请求更新
                getUpdate(AppUtils.getVersionCode(MainActivity.this));
                break;
            }
        }
    }

    private void update() {
        //安装应用
        Intent intent = new Intent(Intent.ACTION_VIEW);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            Uri contentUri = FileProvider.getUriForFile(MainActivity.this, "com.demo.mymobilephonedemo.fileprovider",new File(Environment
                      .getExternalStorageDirectory(), DOWNLOAD_NAME));
            intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
        } else {
            intent.setDataAndType(Uri.fromFile(new File(Environment
                      .getExternalStorageDirectory(), DOWNLOAD_NAME)), "application/vnd.android.package-archive");
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        }
        MainActivity.this.startActivity(intent);
    }
}

显示效果图:

下载安装

github地址:https://github.com/wangxin3119/UpdateDemo

5. 通过第三方的library来更新数据

  现在有很多开源的包,站在巨人的肩膀上可能做不到,但是借把力还是可以的。我在自己的项目中用了这个包,来进行版本更新,有兴趣的朋友可以自己去看看。使用起来也很简单。

  1. 首先在Android Studio 项目中的build.gradle文件中配置
allprojects {
    repositories {
        maven { url "https://www.jitpack.io" }
    }
}
...
dependencies {
    compile 'com.github.yaming116:UpdateApp:1.0.2'
    ...
}
  1. 然后在代码中使用
UpdateService.Builder.create(URL)
             .setStoreDir("update")
             .setIcoResId(R.mipmap.ic_launcher)
             .setIsSendBroadcast(true)
             .setDownloadSuccessNotificationFlag(Notification.DEFAULT_SOUND)
             .setDownloadErrorNotificationFlag(Notification.DEFAULT_SOUND)
             .setUpdateProgress(1)
             .build(this);
参数 描述
downloadUrl 下载地址
icoResId Notification 的icon,默认应用的icon
icoSmallResId Notification 右下角的icon,默认应用的icon
storeDir 保存在sdcard路径,默认在sdcard/Android/package/update
updateProgress 刷新notification 进度条,默认每次下载1%更新一次
downloadNotificationFlag 下载进行中的Notification Flag
downloadErrorNotificationFlag 下载失败的Notification Flag
downloadSuccessNotificationFlag 下载成功的Notification Flag
isSendBroadcast 是否会发送下载状态广播

github地址:https://github.com/yaming116/UpdateApp

Android 进阶篇小知识