一、参考文献
安卓app自动更新功能完美实现_白云天的博客-CSDN博客_android 自动更新
Android 实现自动更新及强制更新功能_farley的成长之路-CSDN博客_android开发自动更新
二、主要思路
- 查询线上版本号,然后拿本地版本号与之对比。
- 若线上版本号比本地版本号大,则下载线上版本号
- 把下载好的版本号安装,并替换当前旧版本
三、 服务器端
1、更新的APK 放在服务器端
Android 中tomcat搭建本地服务器 实现apk更新下载_lizhenmingdirk的专栏-CSDN博客
选用 Apache Tomcat 搭建一个本地的服务器,测试用。
Apache Tomcat官方地址: Apache Tomcat® - Welcome! 我下载的为最新版本:7.0.40
(1)下载
(2)任意解压在磁盘上
(3)在tomcat的解压文件中,找到bin目录。在bin文件下面找到startup.bat 。双击即可启动
(4)在浏览器中输入:http://localhost:8080/ 出现一个tomcat的介绍页面 ,就表示成功了。
(5)把你需要更新的 apk放入 apache-tomcat-7.0.40\webapps\ROOT 文件夹下,默认访问的文件夹。
(6) 访问:浏览器输入 http://localhost:8080/test.apk 弹出下载界面,那就成功了。实际的远程服务器上则将 http://localhost:8080 更换成实际远程服务器的地址。
注:如果你的服务器使用了shiro 框架,需要进行拦截配置,让apk文件不被拦截。
配置 如下: /*.apk = anon
2、服务器端设置接口返回APP版本 的更新信息
将APP版本的更新信息保存到数据库的一个数据表中,服务器端获取数据库中APP 版本的更新信息,返回Json数据
返回的Json 如下
{"code":0,
"message":"{\"serverVersion\":142,\"upgradeInfo\":\"1、更新的信息\",\"appname\":\"test\",\"updateUrl\":\"http://服务器端地址/test.apk\",\"updateTime\":\"2021-10-14
15:18:10\",\"id\":2,\"softwareVersion\":\"2.5\"}",
"success":true}
四、Android端
1、权限
需要用到的权限应该有网络权限、本地文件写入权限,本地文件读取权限,请求安装包权限。使用网络权限去获取线上的版本号,然后下载保存到本地,安装的时候再去本地取来。Android 6.0后需要动态申请权限。
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
2、获取本地版本号
/**
* 获取本地版本号
* @return 版本号
*/
public static int getLocalVersionCode(Context context){
PackageInfo pkg;
int versionCode = 0;
try {
pkg = context.getPackageManager().getPackageInfo(BaseApplication.getInstance().getPackageName(), 0);
versionCode = (int) pkg.getLongVersionCode();
} catch (PackageManager.NameNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return versionCode;
}
3、获取服务器端版本信息
其中HttpHandle是封装后的API,实际使用OkHttp 进行请求。
public static void getServerApkUpdateInfo(){
HttpHandle.getInstance().getDataAsync(HttpUrl.upgradeUpdateInfo, null, new ProfileModel(), new HttpCallback<ProfileModel>() {
@Override
public void onFailure(IOException e) {
}
@Override
public void onSuccess(int code, ProfileModel result) {
if(result.getCode()==0) {
if(result.getMessage()!=null){
ApkUpdateModel apkUpdateModel = JSONObject.parseObject(result.getMessage(),ApkUpdateModel.class);
}
}
}
});
}
ApkUpdateModel如下
public class ApkUpdateModel {
private int id;
private String appname; //APP名称
private int serverVersion; //服务器端APP版本号
private String softwareVersion;
private String updateUrl; //更新APK的下载链接
private String upgradeInfo; //更新信息
private String updateTime; //更新时间
}
4、APK下载、安装
若本地版本号小于服务器端版号,则弹出对话框,用户选择是否更新。选择更新,则开始下载APK。
这里单独开启了一个后台服务来进行APP 的下载和安装。
(1)APK下载
private void downApk(String downloadUrl){
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(downloadUrl)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
//下载失败
mHandler.sendEmptyMessage(DOWNLOAD_FAIL);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.body() == null) {
//下载失败
mHandler.sendEmptyMessage(DOWNLOAD_FAIL);
return;
}
InputStream is = null;
FileOutputStream fos = null;
byte[] buff = new byte[2048];
int len;
try {
is = response.body().byteStream();
createFile(true);
fos = new FileOutputStream(updateFile);
long total = response.body().contentLength();
// contentLength=total;
long sum = 0;
while ((len = is.read(buff)) != -1) {
fos.write(buff,0,len);
sum+=len;
int progress = (int) (sum * 1.0f / total * 100);
//下载中,更新下载进度
//emitter.onNext(progress);
String progressStr = getMsgSpeed(sum,total);
Message msg = new Message();
msg.what = DOWNLOAD_PROGRESS;
msg.obj = progressStr;
mHandler.sendMessage(msg);
// downloadLength=sum;
}
fos.flush();
//4.下载完成,安装apk
mHandler.sendEmptyMessage(DOWNLOAD_COMPLETE);
// installApk(TestActivity.this,file);
} catch (Exception e) {
e.printStackTrace();
mHandler.sendEmptyMessage(DOWNLOAD_FAIL);
} finally {
try {
if (is != null)
is.close();
if (fos != null)
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
}
/**
* 创建file文件
* @param sd_available sdcard是否可用
*/
private void createFile(boolean sd_available) {
if (sd_available) {
updateDir = new File(Environment.getExternalStorageDirectory(),
"app");
} else {
updateDir = getFilesDir();
}
updateFile = new File(updateDir.getPath(), appName + ".apk");
if (!updateDir.exists()) {
updateDir.mkdirs();
}
if (!updateFile.exists()) {
try {
updateFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
} else {
updateFile.delete();
try {
updateFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
}
(2)APK 安装
Android 如何通过代码安装 APK? - 碎岁语 - 博客园
Android 7.0后需要通过如下步骤安装APK
具体的步骤大致如下:
1、配置 AndroidManifest.xml 中的 ContentProvider 信息;
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.xxx.xxx.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
android:name 表示FileProvider 类的完整名称。这个类可以填写两个值,一个是位于 support(android.support.v4.content.FileProvider) 包下的,另一个是位于 androidx(androidx.core.content.FileProvider) 包下的。这两种都可以填写,无区别,主要看你的工程引的是 androidx 支援包还是 support 支援包。
android:authorities表示授权者,这里的格式一般是[appId].fileprovider
android:exported只能为false
android:grantUriPermissions="true"表示授权Uri权限 ,且必须为true
meta-data里设置指定的文件目录,为引用某个xml文件这里引用file_paths
2、配置要开放的 paths 信息;
在工程 res 目录下新建一个 xml 目录,并在该 xml 目录下新建一个 xml 文件。文件的名称必须与第 1 步中 @xml/ 属性值中配置的一致。
xml格式如下,path:需要临时授权访问的路径(.代表所有路径) name:就是你给这个访问路径起个名字。
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<root-path name="root" path="" />
<files-path name="files" path="" />
<cache-path name="cache" path="" />
<external-path name="external" path="" />
<external-files-path name="name" path="path" />
<external-cache-path name="name" path="path" />
</paths>
<root-path/> 代表设备的根目录new File("/");
<files-path/> 代表context.getFilesDir()
<cache-path/> 代表context.getCacheDir()
<external-path/> 代表Environment.getExternalStorageDirectory()
<external-files-path>代表context.getExternalFilesDirs()
<external-cache-path>代表getExternalCacheDirs()
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path path="app" name="app" />
</paths>
上述配置对应路径为Environment.getExternalStorageDirectory() +"/app/"。这个配置的是APK下载后的路径。
3、在 Java 代码中通过 FileProvider 封装文件信息,安装APK
public void installApk(Context context, File file) {
if (context == null) {
return;
}
String authority = getApplicationContext().getPackageName() + ".fileprovider";
Uri apkUri = FileProvider.getUriForFile(context, authority, file);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//判读版本是否在7.0以上
if (Build.VERSION.SDK_INT >= 24) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
}
context.startActivity(intent);
//弹出安装窗口把原程序关闭。
//避免安装完毕点击打开时没反应
/* Process.killProcess(android.os.Process.myPid());*/
}
(3)服务
服务UpdateService.class
public class UpdateService extends Service {
// BT字节参考量
private static final float SIZE_BT = 1024L;
// KB字节参考量
private static final float SIZE_KB = SIZE_BT * 1024.0f;
// MB字节参考量
private static final float SIZE_MB = SIZE_KB * 1024.0f;
private final static int DOWNLOAD_START = 0;// 完成
private final static int DOWNLOAD_COMPLETE = 1;// 完成
private final static int DOWNLOAD_NOMEMORY = -1;// 内存异常
private final static int DOWNLOAD_FAIL = -2;// 失败
private final static int DOWNLOAD_PROGRESS = 100;// 失败
private final static int NotificationID = 2;// 失败
private String appName = null;// 应用名字
private String appUrl = null;// 应用升级地址
private File updateDir = null;// 文件目录
private File updateFile = null;// 升级文件
// 通知栏
private NotificationManager updateNotificationManager = null;
private Notification updateNotification = null;
private NotificationCompat.Builder builder;
private Intent updateIntent = null;// 下载完成
private PendingIntent updatePendingIntent = null;// 在下载的时候
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what){
case DOWNLOAD_COMPLETE:
builder.setContentText(appName + getString(R.string.upgrade_download_finished));
updateNotificationManager.notify(NotificationID,builder.build());
if(updateFile!=null){
installApk(getApplicationContext(),updateFile);
updateNotificationManager.cancel(NotificationID);
}
stopSelf();
Process.killProcess(android.os.Process.myPid());
break;
case DOWNLOAD_FAIL:
builder.setContentText(appName + getString(R.string.upgrade_download_failed));
updateNotificationManager.notify(NotificationID,
builder.build());
stopSelf();
break;
case DOWNLOAD_NOMEMORY:
stopSelf();
break;
case DOWNLOAD_PROGRESS:
String progressShow =(String) msg.obj;
builder.setContentTitle(appName+getString(R.string.upgrade_downloading));
builder.setContentText(progressShow);
updateNotificationManager.notify(NotificationID,
builder.build());
break;
case DOWNLOAD_START:
Toast.makeText(getApplicationContext(),getString(R.string.upgrade_download_start),Toast.LENGTH_SHORT).show();
break;
}
}
};
public UpdateService() {
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
return null;
// throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
appName = intent.getStringExtra("appname");
appUrl = intent.getStringExtra("appurl");
updateNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
String id = "my_channel_01";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { //Android 8.0以上
NotificationChannel mChannel = new NotificationChannel(id, appName, NotificationManager.IMPORTANCE_LOW);
Log.i("DownAPKService", mChannel.toString());
updateNotificationManager.createNotificationChannel(mChannel);
builder = new NotificationCompat.Builder(getApplicationContext());
builder.setSmallIcon(R.drawable.ic_launcher);
builder.setTicker(getString(R.string.upgrade_downloading));
builder.setContentTitle(appName);
builder.setContentText("0MB (0%)");
builder.setNumber(0);
builder.setChannelId(id);
builder.setAutoCancel(true);
} else { //Android 8.0以下
builder = new NotificationCompat.Builder(getApplicationContext());
builder.setSmallIcon(R.drawable.ic_launcher);
builder.setTicker(getString(R.string.upgrade_downloading));
builder.setContentTitle(appName);
builder.setContentText("0MB (0%)");
builder.setNumber(0);
builder.setAutoCancel(true);
}
updateNotificationManager.notify(NotificationID, builder.build());
// new UpdateThread().execute();
new DownloadThread().start();
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
stopSelf();
}
class DownloadThread extends Thread{
@Override
public void run() {
super.run();
mHandler.sendEmptyMessage(DOWNLOAD_START);
downApk(appUrl);
}
}
private void downApk(String downloadUrl){
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(downloadUrl)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
//下载失败
mHandler.sendEmptyMessage(DOWNLOAD_FAIL);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.body() == null) {
//下载失败
mHandler.sendEmptyMessage(DOWNLOAD_FAIL);
return;
}
InputStream is = null;
FileOutputStream fos = null;
byte[] buff = new byte[2048];
int len;
try {
is = response.body().byteStream();
createFile(true);
fos = new FileOutputStream(updateFile);
long total = response.body().contentLength();
// contentLength=total;
long sum = 0;
while ((len = is.read(buff)) != -1) {
fos.write(buff,0,len);
sum+=len;
int progress = (int) (sum * 1.0f / total * 100);
//下载中,更新下载进度
//emitter.onNext(progress);
String progressStr = getMsgSpeed(sum,total);
Message msg = new Message();
msg.what = DOWNLOAD_PROGRESS;
msg.obj = progressStr;
mHandler.sendMessage(msg);
// downloadLength=sum;
}
fos.flush();
//4.下载完成,安装apk
mHandler.sendEmptyMessage(DOWNLOAD_COMPLETE);
// installApk(TestActivity.this,file);
} catch (Exception e) {
e.printStackTrace();
mHandler.sendEmptyMessage(DOWNLOAD_FAIL);
} finally {
try {
if (is != null)
is.close();
if (fos != null)
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
}
public void installApk(Context context, File file) {
if (context == null) {
return;
}
String authority = getApplicationContext().getPackageName() + ".fileprovider";
Uri apkUri = FileProvider.getUriForFile(context, authority, file);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//判读版本是否在7.0以上
if (Build.VERSION.SDK_INT >= 24) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
}
context.startActivity(intent);
//弹出安装窗口把原程序关闭。
//避免安装完毕点击打开时没反应
/* Process.killProcess(android.os.Process.myPid());*/
}
/**
* 获取下载进度
* @param downSize
* @param allSize
* @return
*/
public static String getMsgSpeed(long downSize, long allSize) {
StringBuffer sBuf = new StringBuffer();
sBuf.append(getSize(downSize));
sBuf.append("/");
sBuf.append(getSize(allSize));
sBuf.append(" ");
sBuf.append(getPercentSize(downSize, allSize));
return sBuf.toString();
}
/**
* 获取大小
* @param size
* @return
*/
public static String getSize(long size) {
if (size >= 0 && size < SIZE_BT) {
return (double) (Math.round(size * 10) / 10.0) + "B";
} else if (size >= SIZE_BT && size < SIZE_KB) {
return (double) (Math.round((size / SIZE_BT) * 10) / 10.0) + "KB";
} else if (size >= SIZE_KB && size < SIZE_MB) {
return (double) (Math.round((size / SIZE_KB) * 10) / 10.0) + "MB";
}
return "";
}
/**
* 获取到当前的下载百分比
* @param downSize 下载大小
* @param allSize 总共大小
* @return
*/
public static String getPercentSize(long downSize, long allSize) {
String percent = (allSize == 0 ? "0.0" : new DecimalFormat("0.0")
.format((double) downSize / (double) allSize * 100));
return "(" + percent + "%)";
}
private File createFile() {
String root = Environment.getExternalStorageDirectory().getPath();
File file = new File(root,appName+".apk");
if (file.exists())
file.delete();
try {
file.createNewFile();
updateFile = file;
return file;
} catch (IOException e) {
e.printStackTrace();
}
return null ;
}
/**
* 创建file文件
* @param sd_available sdcard是否可用
*/
private void createFile(boolean sd_available) {
if (sd_available) {
updateDir = new File(Environment.getExternalStorageDirectory(),
"app");
} else {
updateDir = getFilesDir();
}
updateFile = new File(updateDir.getPath(), appName + ".apk");
if (!updateDir.exists()) {
updateDir.mkdirs();
}
if (!updateFile.exists()) {
try {
updateFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
} else {
updateFile.delete();
try {
updateFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
开启服务
private void startUpdateService(Context context,String appname,String appurl){
Intent mIntent = new Intent(context, UpdateService.class);
mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//传递数据
mIntent.putExtra("appname", appname);
mIntent.putExtra("appurl", appurl);
context.startService(mIntent);
enterSplashActivity();
}
5、出现过的问题
(1) android.os.FileUriExposedException: file:///storage/emulated/0/app/test.apk exposed beyond app through Intent.getData()
由于代码中配置的FileProvider 与AndroidManifest.xml内配置的FileProvider 不一致
(2)java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/0/app/test.apk
由于file_paths.xml内的path 配置不正确导致。
(3)Apk覆盖安装的时候,出现安装失败,与旧版本部兼容的问题
Android 解决apk覆盖安装的时候,出现安装失败,与旧版本部兼容的问题_Afanbaby的博客-CSDN博客
解决方案:
1.你需要检查你的新旧apk所使用的签名文件是否是同一个。
2.检查你的签名文件是否是发布版本