天下武功,唯快不破。大道至简的道理人人都懂,但是具体落地时很多人收不住,甚至把简单的事情复杂化。
做产品要快,但有些误区需绕开:不必等一切想明白再动手。产品并不是设计出来的,而是“改”出来的。一件产品,只有不断改进,才能把竞争者甩开;只有快速迭代,才能创造需求的赢家。
需求辣么多,要如何确定产品迭代的需求?要如何制定合理的版本迭代计划?又该如何通过市场和用户的反馈数据决策下一步的迭代方向?
具体来说大概是如下几步:
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以上,先检查,申请权限,再下载
<--网络权限-->
<--读写sdcard的权限-->
<--访问网络状态的权限-->
6、检查权限(6.0以上系统)
//检查权限
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 perms) {
downloadApk();
Log.i("tag","--------->同意授权");
}
/**
* 用户拒绝了授权,则通过弹出对话框让用户打开app设置界面,
* 手动授权,然后返回app进行版本更新
*
* @param requestCode
* @param perms
*/
@Override
public void onPermissionsDenied(int requestCode, List 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();
}
}
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
- 通过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);
- 下载完毕,自动安装的实现
当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); //注册广播
}
- 通过隐式意图安装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);
}
}