Android 实现APP的版本迭代
github地址
一、简介:
在APP开发中,应用上线后公司肯定后期会对应用进行维护对一些Bug修复,这种情况就需要版本迭代了。检测到服务器版本比本地手机版本高的时候,手机会询问用户是否要下载最新的app,然后下载apk下来,然后进行安装。
备注:
也可以用第三方服务,比如腾讯的Bugly、Bmob云服务等,也挺方便的,不过apk要上传到第三方的平台上,如果公司要求在自己平台上,就只能自己写了。
二、实现步骤
上一张开发中的版本迭代的流程图
具体来说大概是如下几步:
1、每次启动应用我们就获取放在服务器上的 版本信息,我们获取到版本号与当前应用的版本好进行对比,这样我们就可以知道应用是否更新了, 版本信息一般包含如下内容:
{ "versionCode": "2", //版本号 "versionName": "2.0", //版本名称 //服务器上最新版本的app的下载地址 "apkUrl": "http://oh0vbg8a6.bkt.clouddn.com/app-debug.apk", "updateTitle": "更新提示" , "changeLog":"1.修复xxx Bug;2.更新了某某UI界面." }
备注:
versionCode 2 //对用户不可见,仅用于应用市场、程序内部识别版本,判断新旧等用途。 versionName "2.0"//展示给用户,让用户会知道自己安装的当前版本. //versionCode的值,必须是int
2、获取用户当前使用的APP的versionCode(版本号)
/** * 获取当前APP版本号 * @param context * @return */ public static int getPackageVersionCode(Context context){ PackageManager manager = context.getPackageManager(); PackageInfo packageInfo = null; try { packageInfo = manager.getPackageInfo(context.getPackageName(),0); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } if(packageInfo != null){ return packageInfo.versionCode; }else{ return 1; } }
3、拿到本地的版本号后,与获取到的服务器的最新的版本号做对比,如果比我们本地获取的APP的versionCode 高,则就进行下一步
//如果当前版本小于新版本,则更新 //获取当前app版本 int currVersionCode = AppUtils.getPackageVersionCode(MainActivity.this); //newVersionCode自己通过网络框架访问服务器,解析数据得到 if(currVersionCode < newVersionCode){ Log.i("tag", "有新版本需要更新"); showHintDialog(); //弹出对话框,提示用户更新APP }
4、如果服务器有新的高版本,则弹出对话框提示用户更新
//显示询问用户是否更新APP的dialog private void showHintDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setIcon(R.mipmap.ic_launcher) .setMessage("检测到当前有新版本,是否更新?") .setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { //取消更新,则跳转到旧版本的APP的页面 Toast.makeText(MainActivity.this, "暂时不更新app", Toast.LENGTH_SHORT).show(); } }) .setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { //6.0以下系统,不需要请求权限,直接下载新版本的app if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { downloadApk(); } else { //6.0以上,先检查,申请权限,再下载 checkPermission(); } } }).create().show(); }
5、如果用户选择了更新APP,则对手机系统版本进行判断
- 6.0以下系统,不需要请求权限,直接下载新版本的app
- 6.0以上,先检查,申请权限,再下载
顺便给出版本迭代需要的2个主要权限
<--网络权限--> <uses-permission android:name="android.permission.INTERNET" /> <--读写sdcard的权限--> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <--访问网络状态的权限--> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
6、检查权限(6.0以上系统)
笔者此处没有使用原生的代码,用的是第三方开源库EasyPermission
https://github.com/googlesamples/easypermissions
//检查权限 private void checkPermission() { //app更新所需的权限 String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.INTERNET}; if (EasyPermissions.hasPermissions(this, permissions)) { // Already have permission, do the thing // ... downloadApk(); } else { // Do not have permissions, request them now(请求权限) EasyPermissions.requestPermissions(this, "app更新需要读写sdcard的权限", REQUEST_CODE_WRITE, permissions); } }
授权结果的回调:
//授权的结果的回调方法 @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if(requestCode == REQUEST_CODE_WRITE){ if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){ downloadApk(); } } }
备注:
Manifest.permission.INTERNET完全可以不用写,但是,我之前在比较复杂的测试中,遇到过问题,故此处就加上了。
权限申请,有时,用户拒绝了授权,并且勾选了不再提示的选项,那么用户会因为没有授权而不能使用一些功能,这样的用户体验是非常糟糕的,为了解决这个问题,我们可以通过弹出自定义的Dialog来让用户打开APP设置界面去手动开启相应的权限,这样才能完整的使用app,所以还需要实现EasyPermissions.PermissionCallbacks接口,重写如下方法
/** * 用户同意授权了 * * @param requestCode * @param perms */ @Override public void onPermissionsGranted(int requestCode, List<String> perms) { downloadApk(); Log.i("tag","--------->同意授权"); } /** * 用户拒绝了授权,则通过弹出对话框让用户打开app设置界面, * 手动授权,然后返回app进行版本更新 * * @param requestCode * @param perms */ @Override public void onPermissionsDenied(int requestCode, List<String> perms) { Toast.makeText(this, "没有同意授权", Toast.LENGTH_SHORT).show(); if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) { new AppSettingsDialog.Builder(this, "请设置权限") .setTitle("设置对话框") .setPositiveButton("设置") .setNegativeButton("取消", null /* click listener */) .setRequestCode(RC_SETTINGS_SCREEN) .build() .show(); } }
···
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == RC_SETTINGS_SCREEN) { // Do something after user returned from app settings screen, like showing a Toast. Toast.makeText(this, "从app设置返回应用界面", Toast.LENGTH_SHORT) .show(); downloadApk(); } }
···
———————-至此我们也已经把下载APK的的权限也搞定了—————–
7、接下来只需要进行下载安装即可,我们这时候就要判断, 是否处于WiFi状态下,如果是WiFi情况下就直接进行更新,如果不是,再创建对话框,然后询问用户,是否确定需要通过流量来进行下载:( 因为一般下载都是在后台,所以都是放在Service中进行操作的。通过startService(new Intent(MainActivity.this, UpdateService.class));来启动服务进行下载)
判断是否处于WiFi状态
/** * 判断是否处于WiFi状态 * getActiveNetworkInfo 是可用的网络,不一定是链接的,getNetworkInfo 是链接的。 */ public static boolean isWifi(Context context) { ConnectivityManager manager = (ConnectivityManager)context. getSystemService(CONNECTIVITY_SERVICE); //NetworkInfo info = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); NetworkInfo networkInfo = manager.getActiveNetworkInfo(); //处于WiFi连接状态 if (networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { return true; } return false; }
新版app下载
//下载最新版的app private void downloadApk() { boolean isWifi = AppUtils.isWifi(this); //是否处于WiFi状态 if (isWifi) { startService(new Intent(MainActivity.this, UpdateService.class)); Toast.makeText(MainActivity.this, "开始下载。", Toast.LENGTH_LONG).show(); } else { //弹出对话框,提示是否用流量下载 AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("提示"); builder.setMessage("是否要用流量进行下载更新"); builder.setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { dialogInterface.dismiss(); Toast.makeText(MainActivity.this, "取消更新。", Toast.LENGTH_LONG).show(); } }); builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { startService(new Intent(MainActivity.this, UpdateService.class)); Toast.makeText(MainActivity.this, "开始下载。", Toast.LENGTH_LONG).show(); } }); builder.setCancelable(false); AlertDialog dialog = builder.create(); //设置不可取消对话框 dialog.setCancelable(false); dialog.setCanceledOnTouchOutside(false); dialog.show(); } }
备注:
如果对service不是很理解的童鞋,可以看看这篇文章
深入理解Service
8、Service进行下载
这里是用DownloadManager进行下载的,下载完成后,点击通知的图标,可以自动安装。
这里顺便给出一个DownloadManager的链接,有需要的,可以自行阅读
Android系统下载管理DownloadManager
1)通过DownLoadManager来进行APK的下载,代码如下:
//开始下载最新版本的apk文件 DownloadManager downloadManager = (DownloadManager)context.getSystemService(DOWNLOAD_SERVICE); //DownloadManager实现下载 DownloadManager.Request request = new DownloadManager.Request(Uri.parse(MainConstant.NEW_VERSION_APP_URL)); request.setTitle("文件下载") .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS,MainConstant.NEW_VERSION_APK_NAME) //设置通知在下载中和下载完成都会显示 //.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) //设置通知只在下载过程中显示,下载完成后不再显示 .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE); downloadManager.enqueue(request);
2)下载完毕,自动安装的实现
当DownLoadManager下载完成后,会发送一个DownloadManager.ACTION_DOWNLOAD_COMPLETE的广播,所以我们只要刚开始在启动Service的时候,注册一个广播,监听
DownloadManager.ACTION_DOWNLOAD_COMPLETE,然后当下载完成后,在BroadcastReceiver中调用安装APK的方法即可。
//广播接收的注册 public void receiverRegist() { receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { //安装apk AppUtils.installApk(context); stopSelf(); //停止下载的Service } }; IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE); registerReceiver(receiver, filter); //注册广播 }
3)通过隐式意图安装apk
/**Apk的安装 * * @param context */ public static void installApk(Context context) { Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); //这个必须有 intent.setDataAndType( Uri.fromFile(new File(Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_DOWNLOADS), MainConstant.NEW_VERSION_APK_NAME)), "application/vnd.android.package-archive"); context.startActivity(intent); }
Service中的完整代码
public class UpdateService extends Service { public static final int NOTIFICATION_ID = 100; private static final int REQUEST_CODE = 10; //PendingIntent中的请求码 //下载的新版本的apk的存放路径 public static final String destPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + File.separator + "newversion.apk"; private Context mContext = this; private Notification mNotification; private NotificationManager manager; private NotificationCompat.Builder builder; private RemoteViews remoteViews; private BroadcastReceiver receiver; @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { receiverRegist(); //下载apk文件 AppUtils.downloadApkByDownloadManager(this); return Service.START_STICKY; } @Override public void onDestroy() { super.onDestroy(); //解除注册 unregisterReceiver(receiver); } //广播接收的注册 public void receiverRegist() { receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { //安装apk AppUtils.installApk(context); stopSelf(); //停止下载的Service } }; IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE); registerReceiver(receiver, filter); //注册广播 } }
下面这段代码是自己封装的下载apk并实现自动安装的功能,如有不妥之处,敬请 指出
public class UpdateService extends IntentService { public static final int NOTIFICATION_ID = 100; private static final int REQUEST_CODE = 10; //PendingIntent中的请求码 public static final String destPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + File.separator + "newversion.apk"; private Context mContext = this; private Notification mNotification; private NotificationManager manager; private NotificationCompat.Builder builder; private RemoteViews remoteViews; public UpdateService() { super("UpdateService"); } @Override protected void onHandleIntent(Intent intent) { if (intent != null) { //开始下载最新版本的apk文件 initNotification(); download(MainConstant.NEW_VERSION_APP_URL); } } private void download(String newVersionApkUrl) { BufferedInputStream bis = null; BufferedOutputStream bos = null; try { URL url = new URL(newVersionApkUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); //设置连接的属性 conn.setConnectTimeout(5000); conn.setReadTimeout(5000); //如果响应码为200 if (conn.getResponseCode() == 200) { bis = new BufferedInputStream(conn.getInputStream()); bos = new BufferedOutputStream(new FileOutputStream(destPath)); int totalSize; int count = 0; //读取到的字节数的计数器 int progress; //当前进度 byte[] data = new byte[1024 * 1024]; int len; //文件总的大小 totalSize = conn.getContentLength(); while ((len = bis.read(data)) != -1) { count += len; //读取当前总的字节数 bos.write(data, 0, len); bos.flush(); progress = (int) ((count / (float) totalSize) * 100); //progress = (count * 100) / totalSize; //当前下载的进度 //重新设置自定义通知的进度条的进度 remoteViews.setProgressBar(R.id.progressBar, 100, progress, false); remoteViews.setTextViewText(R.id.tv_progress, "已经下载了:" + progress + "%"); //发送通知 manager.notify(NOTIFICATION_ID, mNotification); } } } catch (IOException e) { e.printStackTrace(); } finally { if (bis != null) { try { bis.close(); } catch (IOException e) { e.printStackTrace(); } } if (bos != null) { try { bos.close(); } catch (IOException e) { e.printStackTrace(); } } } //下载文件完成以后,执行以下操作 Intent installIntent = new Intent(); /**启动系统服务的Activity,用于显示用户的数据。 比较通用,会根据用户的数据类型打开相应的Activity。 */ installIntent.setAction(Intent.ACTION_VIEW); installIntent.setDataAndType(Uri.fromFile(new File(destPath)), "application/vnd.android.package-archive"); //实例化延时的Activity PendingIntent pendingIntent = PendingIntent.getActivity(mContext, REQUEST_CODE, installIntent, PendingIntent.FLAG_ONE_SHOT); builder.setContentTitle("文件下载完毕!") .setSmallIcon(android.R.drawable.stat_sys_download_done) .setContentText("已下载100%") .setContentIntent(pendingIntent); //点击通知图标,自动消失 Notification notification = builder.build(); notification.flags |= Notification.FLAG_AUTO_CANCEL; manager.notify(NOTIFICATION_ID, notification); } //初始化通知 private void initNotification() { builder = new NotificationCompat.Builder(mContext); //自定义的Notification remoteViews = new RemoteViews(getPackageName(), R.layout.layout_main_notification); Bitmap largeIcon = BitmapFactory.decodeResource(getResources(), R.drawable.stat_sys_download_anim0); builder.setTicker("开始下载apk文件") .setSmallIcon(R.drawable.stat_sys_download_anim5) .setLargeIcon(largeIcon) .setContent(remoteViews); //实例化通知对象 mNotification = builder.build(); //获取通知的管理器 manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); } }