转发请注明出处:http://www.jianshu.com/p/b669940c9f3e
前言
整理功能,把这块拿出来单独做个demo,好和大家分享交流一下。
版本更新这个功能一般 app 都有实现,而用户获取新版本一般来源有两种:
- 一种是各种应用市场的新版本提醒
- 一种是打开app时拉取版本信息
- (还要一种推送形式,热修复或打补丁包时用得多点)
这两区别就在于,市场的不能强制更新、不够及时、粘度低、单调。
摘要
下面介绍这个章节,你将会学习或复习到一些技术:
- dialog 实现 key 的重写,在弹窗后用户不能通过点击虚拟后退键关闭窗口
- 忽略后不再提示,下个版本更新再弹窗
- 自定义 service 来承载下载功能
- okhttp3 下载文件到 sdcard,文件权限判断
- 绑定服务,实现回调下载进度
- 简易的 mvp 架构
- 下载完毕自动安装
这个是我们公司的项目,有版本更新时的截图。当然,我们要实现的demo不会写这么复杂的ui。
功能点(先把demo的最终效果给上看一眼)
dialog
dialog.setCanceledOnTouchOutside() 触摸窗口边界以外是否关闭窗口,设置 false 即不关闭
dialog.setOnKeyListener() 设置KeyEvent的回调监听方法。如果事件分发到dialog的话,这个事件将被触发,一般是在窗口显示时,触碰屏幕的事件先分发到给它,但默认情况下不处理直接返回false,也就是继续分发给父级处理。如果只是拦截返回键就只需要这样写
mDialog.setOnKeyListener(new DialogInterface.OnKeyListener() {
@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
return keyCode == KeyEvent.KEYCODE_BACK &&
mDialog != null && mDialog.isShowing();
}
});
忽略
忽略本次版本更新,不再弹窗提示
下次有新版本时,再继续弹窗提醒
其实这样的逻辑很好理解,并没有什么特别的代码。比较坑的是,这里往往需要每次请求接口才能判断到你app是否已经是最新版本。
这里我并没有做网络请求,只是模拟一下得到的版本号,然后做一下常规的逻辑判断,在我们项目中,获取版本号只能通过请求接口来得到,也就是说每次启动请求更新的接口,也就显得非常浪费,我是建议把这个版本号的在你们的首页和其它接口信息一起返回,然后写入在 SharedPreferences。每次先判断与忽略的版本是否一样,一样则跳过,否则下次启动时请求更新接口
public void checkUpdate(String local) {
//假设获取得到最新版本
//一般还要和忽略的版本做比对。。这里就不累赘了
String version = "2.0";
String ignore = SpUtils.getInstance().getString("ignore");
if (!ignore.equals(version) && !ignore.equals(local)) {
view.showUpdate(version);
}
}
自定义service
这里需要和 service 通讯,我们自定义一个绑定的服务,需要重写几个比较关键的方法,分别是 onBind(返回和服务通讯的频道IBinder)、unbindService(解除绑定时销毁资源)、和自己写一个 Binder 用于通讯时返回可获取service对象。进行其它操作。
context.bindService(context,conn,flags)
- context 上下文
- conn(ServiceConnnetion),实现了这个接口之后会让你实现两个方法onServiceConnected(ComponentName, IBinder) 也就是通讯连通后返回我们将要操作的那个 IBinder 对象、onServiceDisconnected(ComponentName) 断开通讯
- flags 服务绑定类型,它提供很多种类型,但常用的也就这里我我们用到的是 Service.BIND_AUTO_CREATE, 源码对它的描述大概意思是说,在你确保绑定此服务,就自动启动服务。(意思就是说,你bindService之后,传的不是这个参数,有可能你的服务就没反应咯)
通过获取这个对象就可以对 service 进行操作了。这个自定义service篇幅比较长,建议下载demo下来仔细阅读一番.
public class DownloadService extends Service {
//定义notify的id,避免与其它的notification的处理冲突
private static final int NOTIFY_ID = 0;
private static final String CHANNEL = "update";
private DownloadBinder binder = new DownloadBinder();
private NotificationManager mNotificationManager;
private NotificationCompat.Builder mBuilder;
private DownloadCallback callback;
//定义个更新速率,避免更新通知栏过于频繁导致卡顿
private float rate = .0f;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public void unbindService(ServiceConnection conn) {
super.unbindService(conn);
mNotificationManager.cancelAll();
mNotificationManager = null;
mBuilder = null;
}
/**
* 和activity通讯的binder
*/
public class DownloadBinder extends Binder{
public DownloadService getService(){
return DownloadService.this;
}
}
/**
* 创建通知栏
*/
private void setNotification() {
if (mNotificationManager == null)
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mBuilder = new NotificationCompat.Builder(this,CHANNEL);
mBuilder.setContentTitle("开始下载")
.setContentText("正在连接服务器")
.setSmallIcon(R.mipmap.ic_launcher_round)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setOngoing(true)
.setAutoCancel(true)
.setWhen(System.currentTimeMillis());
mNotificationManager.notify(NOTIFY_ID, mBuilder.build());
}
/**
* 下载完成
*/
private void complete(String msg) {
if (mBuilder != null) {
mBuilder.setContentTitle("新版本").setContentText(msg);
Notification notification = mBuilder.build();
notification.flags = Notification.FLAG_AUTO_CANCEL;
mNotificationManager.notify(NOTIFY_ID, notification);
}
stopSelf();
}
/**
* 开始下载apk
*/
public void downApk(String url,DownloadCallback callback) {
this.callback = callback;
if (TextUtils.isEmpty(url)) {
complete("下载路径错误");
return;
}
setNotification();
handler.sendEmptyMessage(0);
Request request = new Request.Builder().url(url).build();
new OkHttpClient().newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Message message = Message.obtain();
message.what = 1;
message.obj = e.getMessage();
handler.sendMessage(message);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.body() == null) {
Message message = Message.obtain();
message.what = 1;
message.obj = "下载错误";
handler.sendMessage(message);
return;
}
InputStream is = null;
byte[] buff = new byte[2048];
int len;
FileOutputStream fos = null;
try {
is = response.body().byteStream();
long total = response.body().contentLength();
File file = createFile();
fos = new FileOutputStream(file);
long sum = 0;
while ((len = is.read(buff)) != -1) {
fos.write(buff,0,len);
sum+=len;
int progress = (int) (sum * 1.0f / total * 100);
if (rate != progress) {
Message message = Message.obtain();
message.what = 2;
message.obj = progress;
handler.sendMessage(message);
rate = progress;
}
}
fos.flush();
Message message = Message.obtain();
message.what = 3;
message.obj = file.getAbsoluteFile();
handler.sendMessage(message);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (is != null)
is.close();
if (fos != null)
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
}
/**
* 路径为根目录
* 创建文件名称为 updateDemo.apk
*/
private File createFile() {
String root = Environment.getExternalStorageDirectory().getPath();
File file = new File(root,"updateDemo.apk");
if (file.exists())
file.delete();
try {
file.createNewFile();
return file;
} catch (IOException e) {
e.printStackTrace();
}
return null ;
}
/**
* 把处理结果放回ui线程
*/
private Handler handler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case 0:
callback.onPrepare();
break;
case 1:
mNotificationManager.cancel(NOTIFY_ID);
callback.onFail((String) msg.obj);
stopSelf();
break;
case 2:{
int progress = (int) msg.obj;
callback.onProgress(progress);
mBuilder.setContentTitle("正在下载:新版本...")
.setContentText(String.format(Locale.CHINESE,"%d%%",progress))
.setProgress(100,progress,false)
.setWhen(System.currentTimeMillis());
Notification notification = mBuilder.build();
notification.flags = Notification.FLAG_AUTO_CANCEL;
mNotificationManager.notify(NOTIFY_ID,notification);}
break;
case 3:{
callback.onComplete((File) msg.obj);
//app运行在界面,直接安装
//否则运行在后台则通知形式告知完成
if (onFront()) {
mNotificationManager.cancel(NOTIFY_ID);
} else {
Intent intent = installIntent((String) msg.obj);
PendingIntent pIntent = PendingIntent.getActivity(getApplicationContext()
,0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
mBuilder.setContentIntent(pIntent)
.setContentTitle(getPackageName())
.setContentText("下载完成,点击安装")
.setProgress(0,0,false)
.setDefaults(Notification.DEFAULT_ALL);
Notification notification = mBuilder.build();
notification.flags = Notification.FLAG_AUTO_CANCEL;
mNotificationManager.notify(NOTIFY_ID,notification);
}
stopSelf();}
break;
}
return false;
}
});
/**
* 是否运行在用户前面
*/
private boolean onFront() {
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();
if (appProcesses == null || appProcesses.isEmpty())
return false;
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
if (appProcess.processName.equals(getPackageName()) &&
appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
return true;
}
}
return false;
}
/**
* 安装
* 7.0 以上记得配置 fileProvider
*/
private Intent installIntent(String path){
try {
File file = new File(path);
String authority = getApplicationContext().getPackageName() + ".fileProvider";
Uri fileUri = FileProvider.getUriForFile(getApplicationContext(), authority, file);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
}
return intent;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 销毁时清空一下对notify对象的持有
*/
@Override
public void onDestroy() {
mNotificationManager = null;
super.onDestroy();
}
/**
* 定义一下回调方法
*/
public interface DownloadCallback{
void onPrepare();
void onProgress(int progress);
void onComplete(File file);
void onFail(String msg);
}
}
okhttp3下载文件
看通透事情的本质,你就可以为所欲为了。怎么发起一个 okhttp3 最简单的请求,看下面!简洁明了吧,这里抽离出来分析一下,最主要还是大家的业务、框架、需求都不一样,所以节省时间看明白写入逻辑就好了,这样移植到自己项目的时候不至于无从下手。明白之后再结合比较流行常用的如 Retrofit、Volley之类的插入这段就好了。避免引入过多的第三方库而导致编译速度变慢,项目臃肿嘛。
我们来看回上面的代码,看到 downApk 方法,我是先判断路径是否为空,为空就在通知栏提示用户下载路径错误了,这样感觉比较友好。判断后就创建一个 request 并执行这个请求。很容易就理解了,我们要下载apk,只需要一个 url 就足够了是吧(这个url一般在检测版本更新接口时后台返回)。然后第一步就配置好了,接下来是处理怎么把文件流写出到 sdcard。
写入:是指读取文件射进你app内(InputStream InputStreamReader FileInputStream BufferedInputStream)
写出:是指你app很无赖的拉出到sdcard(OutputStream OutputStreamWriter FileOutputStream BufferedOutputStream)仅此送给一直对 input、ouput 记忆混乱的同学
Request request = new Request.Builder().url(url).build();
new OkHttpClient().newCall(request).enqueue(new Callback() {});
写出文件
InputStream is = null;
byte[] buff = new byte[2048];
int len;
FileOutputStream fos = null;
try {
is = response.body().byteStream(); //读取网络文件流
long total = response.body().contentLength(); //获取文件流的总字节数
File file = createFile(); //自己的createFile() 在指定路径创建一个空文件并返回
fos = new FileOutputStream(file); //消化了上厕所准备了
long sum = 0;
while ((len = is.read(buff)) != -1) { //嘣~嘣~一点一点的往 sdcard &#$%@$%#%$
fos.write(buff,0,len);
sum+=len;
int progress = (int) (sum * 1.0f / total * 100);
if (rate != progress) {
//用handler回调通知下载进度的
rate = progress;
}
}
fos.flush();
//用handler回调通知下载完成
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (is != null)
is.close();
if (fos != null)
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
文件下载回调
在上面的okhttp下载处理中,我注释标注了回调的位置,因为下载线程不再UI线程中,大家需要通过handler把数据先放回我们能操作UI的线程中再返回会比较合理,在外面实现了该回调的时候就可以直接处理数据。
private Handler handler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case 0://下载操作之前的预备操作,如检测网络是否wifi
callback.onPrepare();
break;
case 1://下载失败,清空通知栏,并销毁服务自己
mNotificationManager.cancel(NOTIFY_ID);
callback.onFail((String) msg.obj);
stopSelf();
break;
case 2:{//回显通知栏的实时进度
int progress = (int) msg.obj;
callback.onProgress(progress);
mBuilder.setContentTitle("正在下载:新版本...")
.setContentText(String.format(Locale.CHINESE,"%d%%",progress))
.setProgress(100,progress,false)
.setWhen(System.currentTimeMillis());
Notification notification = mBuilder.build();
notification.flags = Notification.FLAG_AUTO_CANCEL;
mNotificationManager.notify(NOTIFY_ID,notification);}
break;
case 3:{//下载成功,用户在界面则直接安装,否则叮一声通知栏提醒,点击通知栏跳转到安装界面
callback.onComplete((File) msg.obj);
if (onFront()) {
mNotificationManager.cancel(NOTIFY_ID);
} else {
Intent intent = installIntent((String) msg.obj);
PendingIntent pIntent = PendingIntent.getActivity(getApplicationContext()
,0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
mBuilder.setContentIntent(pIntent)
.setContentTitle(getPackageName())
.setContentText("下载完成,点击安装")
.setProgress(0,0,false)
.setDefaults(Notification.DEFAULT_ALL);
Notification notification = mBuilder.build();
notification.flags = Notification.FLAG_AUTO_CANCEL;
mNotificationManager.notify(NOTIFY_ID,notification);
}
stopSelf();}
break;
}
return false;
}
});
自动安装
android 随着版本迭代的速度越来越快,有一些api已经被遗弃了甚至不存在了。7.0 的文件权限变得尤为严格,所以之前的一些代码在高一点的系统可能导致崩溃,比如下面的,如果不做版本判断,在7.0的手机就会抛出FileUriExposedException异常,说app不能访问你的app以外的资源。官方文档建议的做法,是用FileProvider来实现文件共享。也就是说在你项目的src/res新建个xml文件夹再自定义一个文件,并在配置清单里面配置一下这个
file_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external"
path=""/>
</paths>
安装apk
try {
String authority = getApplicationContext().getPackageName() + ".fileProvider";
Uri fileUri = FileProvider.getUriForFile(this, authority, file);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//7.0以上需要添加临时读取权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
} else {
Uri uri = Uri.fromFile(file);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
}
startActivity(intent);
//弹出安装窗口把原程序关闭。
//避免安装完毕点击打开时没反应
killProcess(android.os.Process.myPid());
} catch (Exception e) {
e.printStackTrace();
}
已把 Demo 放在github
希望大家能从中学习到东西,不再困惑