问题引入:
最近实习的项目要实现一个需求,实现app检测更新并且覆盖安装。
简单思路:
1.听说现成也有一些热更新插件轮子可以搬,直接使用插件就可以。(网页与原生混合开发比较适合)
2.另一个想法是,在本地应用定一个版本号,在远程服务端定义一个版本号,并且放置安装apk.执行,版本比较并且实现下载安装。
思路还是比较简单的,但是实现过程却走了一些坑。(适合原生Java开发)
遇到的坑:
由于实现思路是前人想的,他们才是真正造轮子的人,要感谢下面几位博客贡献源:
首先是调试的问题,如上我们如果要检测版本必须要有个远程服务器进行版本比较,这就涉及到服务器的搭建,如果没有购买服务器,一般我是用tomcat来进行搭建,那么真机要如何访问到这个地址呢,要知道,本地搭建的url在不同模拟器上是不一样的,真机,genymotion和AS自带AVD又是不一样的访问方式。
所以首先解决访问问题要感谢下面这位博主:
http://supportopensource.iteye.com/blog/1959711
<uses-permission android:name="android.permission.INTERNET" >uses-permission>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MOUNT_UNMOUT_FILESYSTEMS"/>
另外,当用户拒绝超过两次之后,权限申请框会有个不再提示选项,一旦用户点击不再提示,那么程序再也无法调用申请,只能提示用户去设置界面对app进行手动信任,这也是一个程序友好性以及健壮性有待提高的地方- -。
3. 文件存取路径问题:
Android在手机端的文件系统分为外部存储和内部存储, 但是,中文经常听到内存和外存SD卡,其实内存和SD卡并不是内部存储和外部存储的区分方式。
具体可以参考:
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2013/0923/1557.html
本地版本为1.0,放在Androidmanifest.xml
"http://schemas.android.com/apk/res/android"
xmlns:Android="http://schemas.android.com/apk/res-auto"
package="com.example.pixelpig.update_demo_"
android:versionCode="1"
android:versionName="1.0">
服务器那边放置待下载的apk,和一个version.xml方便版本比较以及更新特性描述。
<info>
<script/>
<version>2.0version>
<url>http://101.201.64.95:8080/pp/t.apkurl>
<description>检测到最新版本,请及时更新! 1.SADFSADF 2.@#FSADFFdescription>
info>
//权限检测
public static final int EXTERNAL_STORAGE_REQ_CODE = 10 ;
public void requestPermission(){
//判断当前Activity是否已经获得了该权限
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
//当用户拒绝权限之后,再次调用对用户做出解释
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
Log.i(TAG,"曾经申请,被用户禁止权限(还未点击不再提示)");
Toast.makeText(this, "为了正常更新,请授予该应用读写文件权限。", Toast.LENGTH_SHORT).show();
//进行权限请求
}else{
Log.i(TAG, "第一次申请");
}
Log.i(TAG, "获取权限");
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
EXTERNAL_STORAGE_REQ_CODE);
}else{
//已经有权限,下载
downLoadApk();
}
}
// 用户经过requestPermission()选择允许或拒绝后,会回调此onRequestPermissionsResult方法
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case EXTERNAL_STORAGE_REQ_CODE: {
// 如果请求被拒绝,那么通常grantResults数组为空
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//权限申请成功,进行相应操作
Log.i(TAG,"权限GET,下载apk,更新");
downLoadApk();
} else {
Toast.makeText(this,"未获得权限,更新失效。",Toast.LENGTH_SHORT).show();
//等待用户重新申请,如果 用户永久拒绝,则需要手动更改。
}
return;
}
}
}
/*
*
* 弹出对话框通知用户更新程序
*
* 弹出对话框的步骤:
* 1.创建alertDialog的builder.
* 2.要给builder设置属性, 对话框的内容,样式,按钮
* 3.通过builder 创建一个对话框
* 4.对话框show()出来
*/
protected void showUpdataDialog() {
AlertDialog.Builder builer = new AlertDialog.Builder(this) ;
builer.setTitle("当前版本:"+ Local_verson_No +" 新版本:"+New_verson_NO);
builer.setMessage(description);//info.getDescription());
//当点确定按钮时从服务器上下载 新的apk 然后安装
builer.setPositiveButton("下载安装", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
Log.i(TAG,"检测权限");
requestPermission();
}
});
//当点取消按钮时忽略
builer.setNegativeButton("取消更新", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
Log.i(TAG,"取消更新");
dialog.dismiss();//dismiss返回取消值,区别于cancel() ,cancel类似于按back键,不返回值
}
});
AlertDialog dialog = builer.create();
dialog.show();
}
//获取本地Manifest记录版本号
private String getLocalVersion(){
PackageInfo pkg;
String versionName = null;
try {
pkg = getPackageManager().getPackageInfo(getApplication().getPackageName(), 0);
String appName = pkg.applicationInfo.loadLabel(getPackageManager()).toString();
versionName = pkg.versionName;
Log.d(TAG,"appName:" + appName);
Log.d(TAG,"versionName:" + versionName);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return versionName;
}
//获取服务器版本号
public String getHttpVersion(){
//在子线程中获取服务器的数据
Thread thread = new Thread(){
@Override
public void run() {
try {
URL url = new URL(path);
//建立连接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
//设置请求方式
conn.setRequestMethod("GET");
//设置请求超时时间
conn.setConnectTimeout(5000);
//设置读取超时时间
conn.setReadTimeout(5000);
//判断是否获取成功
if(conn.getResponseCode() == 200)
{
Log.d(TAG,"连接服务器成功");
//获得输入流
InputStream is = conn.getInputStream();
//解析输入流中的数据
New_verson_NO = parseXmlInfo(is);
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
//启动线程
thread.start();
return New_verson_NO;
}
//解析xml
public String parseXmlInfo(InputStream is)
{
UpdataInfo info = new UpdataInfo();//实体
/*我们用pull解析器解析xml文件*/
//1.先拿到pull解析器
XmlPullParser xParser = Xml.newPullParser();
try {
xParser.setInput(is, "utf-8");
//获取事件的类型
int eventType = xParser.getEventType();
while(eventType != XmlPullParser.END_DOCUMENT)
{
switch (eventType) {
case XmlPullParser.START_TAG:
//当事件的开始类型newslist,代表的是xml文件的数据开始
if("version".equals(xParser.getName())){
info.setVersion(xParser.nextText()); //获取版本号
}else if ("url".equals(xParser.getName())){
info.setUrl(xParser.nextText()); //获取要升级的APK文件
}else if ("description".equals(xParser.getName())){
info.setDescription(xParser.nextText()); //获取该文件的信息
}
break;
}
eventType = xParser.next();
}
//打印服务器各项信息
Log.d(TAG,info.getVersion() + info.getDescription() + info.getUrl());
//全局变量赋值
description = info.getDescription();
//获取服务器安装包存放的地址
apk_url = info.getUrl();
} catch (Exception e) {
e.printStackTrace();
}
return info.getVersion();
}
// 从服务器中下载APK
protected void downLoadApk() {
final ProgressDialog pd; //进度条对话框
pd = new ProgressDialog(this);
pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
pd.setMessage("正在下载更新...");
pd.show();
new Thread(){
@Override
public void run() {
try {
File file = getFileFromServer(apk_url, pd);
sleep(3000);
installApk(file);
pd.dismiss(); //结束掉进度条对话框
} catch (Exception e) {
Message msg = new Message();
msg.what = -1;
handler.sendMessage(msg);
e.printStackTrace();
}
}}.start();
}
Handler handler = new Handler();
//执行安装apk
protected void installApk(File file) {
Intent intent = new Intent();
//执行动作
intent.setAction(Intent.ACTION_VIEW);
//执行的数据类型 针对apk: {".apk", "application/vnd.android.package-archive"}
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
startActivity(intent);
Log.i(TAG,"______________________执行安装apk");
}
public static File getFileFromServer(String path, ProgressDialog pd) throws Exception{
//如果相等的话表示当前的手机上内部存储存在且可用的
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
Log.i(TAG,"______________________手机内部存储可用");
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
//获取到文件的大小
pd.setMax(conn.getContentLength());
InputStream is = conn.getInputStream();
//此处填写安装包名称以及存取位置,下方表示存在手机内部存储根目录,以t.apk命名
Log.i(TAG,"______________________存取手机内部存储根目录");
File file = new File(Environment.getExternalStorageDirectory(), "t.apk");
FileOutputStream fos = new FileOutputStream(file);
BufferedInputStream bis = new BufferedInputStream(is);
byte[] buffer = new byte[1024];
int len ;
int total=0;
while((len =bis.read(buffer))!=-1){
fos.write(buffer, 0, len);
total+= len;
//获取当前下载量,更新进度条
pd.setProgress(total);
}
fos.close();
bis.close();
is.close();
return file;
}
else{
return null;
}
}