Android 实现APP的版本迭代

标签: 极客互联 | 发表时间:2017-01-18 04:18 | 作者:shendao
出处:http://www.shellsec.com

github地址

https://github.com/zhouxu88/APPUpgrade.git

一、简介:

在APP开发中,应用上线后公司肯定后期会对应用进行维护对一些Bug修复,这种情况就需要版本迭代了。检测到服务器版本比本地手机版本高的时候,手机会询问用户是否要下载最新的app,然后下载apk下来,然后进行安装。

备注:
也可以用第三方服务,比如腾讯的Bugly、Bmob云服务等,也挺方便的,不过apk要上传到第三方的平台上,如果公司要求在自己平台上,就只能自己写了。

二、实现步骤

上一张开发中的版本迭代的流程图

Android 实现APP的版本迭代

App_Upgrade_Image.png

具体来说大概是如下几步:

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);     } }

相关 [android app 版本] 推荐:

Android 实现APP的版本迭代

- - 神刀安全网
在APP开发中,应用上线后公司肯定后期会对应用进行维护对一些Bug修复,这种情况就需要版本迭代了. 检测到服务器版本比本地手机版本高的时候,手机会询问用户是否要下载最新的app,然后下载apk下来,然后进行安装. 也可以用第三方服务,比如腾讯的Bugly、Bmob云服务等,也挺方便的,不过apk要上传到第三方的平台上,如果公司要求在自己平台上,就只能自己写了.

Google将关闭Android App Inventor

- tinda - Solidot
新上任的Google CEO Larry Page已发誓要调整公司的重心,将精力集中中优先项目上,一些对用户有用但却对公司没有帮助的项目纷纷关闭,如Google字典服务,如Google Labs中的众多项目. 其中一个被关闭的项目是Android App Inventor. Android App Inventor由MIT计算机科学Hal Abelson领导开发,借鉴了入门级编程项目Scratch,让没有编程经验和知识的人开发Android应用程序,因此颇受教育界人士的欢迎.

Android dlib人脸识别 dlib-android-app: Android app to demo dlib-android(https://github.com/tzutalin/dlib-android). Use the prebuilt shared-lib built from dlib-android

- -

欢呼吧!App Inventor for Android 使用总结

- Hinc - TechCrunch中文站
昨日我们报道了Google App Inventor for Android,它是一个基于网页的开发环境,即使是没有开发背景的人也能通过他轻松创建Android应用程序. 这个产品已经测试了一年之久了,主要是和教育机构合作进行的测试,因此,在课堂上接触到它的学生们很可能成为Android应用暴增的主要力量.

如何在iOS与Android间移植APP

- plidezus - 雪鸮的啁啾
除了像”I am rich”这种定点打击苹果烧包族的APP外,大多数应用都会尽量覆盖包含尽可能多的用户. 这就需要考虑在iOS和Android两种主流操作系统间移植的问题. 如果为各个平台量身定做界面,就能让用户利用以往的使用习惯快速学习. 但为多个平台设计各异的界面毕竟是需要工作量的. 如何才能在跨平台移植的时候只做那些最有必要的工作呢.

Felix 的 60 个 Android App 推荐

- Wan - Felix&#39;s Blog
本猫入爪机(T-Mobile G2)半月, 折腾ROM/Kernel/App无数=.=. 现在我安装了下面这些常用到的App(Google自带的就不提啦), 供分享, 供参考.. 流量监控 最靠谱的一个… 有时候比ISP统计的还多一点点, 总之不会少. 有按月/周/天的统计报表, 有一个还不错的Widget, 而且, 能显示每个App使用了多少流量.

Google 联合 MIT 开源 Android App Inventor

- - 博客园_新闻
Google 联合 MIT 发布了 App Inventor for Android 的开源版本.. AppInventor 是谷歌推出一种软件工具. 这种工具可以使用户更容易的为 Android 智能手机编写应用程序. 谷歌该 Android 应用工具使人们可以拖放代码块(表现为图形图像代表不同的智能手机功能),将这些代码放在一起,类似于将 Lego blocks 放置在一起.

Android App启动画面的制作

- - CSDN博客推荐文章
  安卓软件启动时,都会有一个全屏的带LOGO,软件名称,版本号的启动屏幕. 打开eclipse,新建一个Android项目,不建Activity. 1、新建Activity文件. 点击项目管理里的res,进入layout,右键点击NEW-》Project-》Android-》Android XML Layout File 按步骤新建一个Activity的XML文件.

Android APP安全测试基础

- - 阿德马Web安全
自从去了新公司之后,工作太忙,变的有点懒了,很久没有更新Blog. 今天跟几个小伙伴一起吃饭,小伙伴提起我的Blog,想想是该更新更新了,就把我投稿给sobug的这篇转过来吧,关于Android app安全测试的基础东东,在Sobug 的url:. 最近这两年移动端真是非常火,每个单位或多或少都会有那么几款App,对于我们Web安全攻城师来说,App安全也需要或多或少的了解一些.