之前有写过利用腾讯Bugly实现APP的热更新以及版本升级Android 热更新框架Bugly-9步完成热更新/自动更新/异常上报分析,今天来讲一下不借助第三方的应用升级。
演示效果:
原理:
1.将新版本上传到自己的服务器,有服务器将最新版本信息记录
2.当用户打开app或者手动触发版本检查时向服务器请求版本信息以及最新版本apk的下载地址
3.判断当前版本是不是最新版本,如果不是则通过下载地址下载apk
4.下载完成后吊起安装程序进行安装覆盖
5.实现了版本更新
本次例子用到框架:
1.easypermissions 权限控制
2.gson json对象解析
3.xUtils-2.6.14.jar 网络请求
所用权限(注意最后一个权限):
注意点:
1."android.permission.REQUEST_INSTALL_PACKAGES"这个权限必须加上,否则可能会在apk下载完成后不能吊起安装程序(一闪而逝)
2.Android8以上如果要在通知栏显示下载进度,需要进行notification的适配Android8.0 notification channel
3.在Android6.0以上需要进行权限的申请
4.Android7.0以上文件读取需要通过FileProvider进行操作关于 Android 7.0 适配中 FileProvider 部分的总结
下面看主要代码:
MainActivity
package cn.humanetplan.updateappdemo;
import android.Manifest;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.view.View;
import android.widget.TextView;
import com.google.gson.Gson;
import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.RequestParams;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;
import com.lidroid.xutils.http.client.HttpRequest;
import java.util.List;
import pub.devrel.easypermissions.EasyPermissions;
;
public class MainActivity extends BaseActivity implements EasyPermissions.PermissionCallbacks{
TextView tv_check, tv_versionName;
@Override
public void DoSthBeforeInflate() {
}
@Override
protected int setContentLayout() {
return R.layout.activity_main;
}
@Override
protected void init() {
tv_check = findViewById(R.id.tv_check);
tv_versionName = findViewById(R.id.tv_versionName);
tv_versionName.setText("当前版本V"+BuildConfig.VERSION_NAME);
tv_check.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
CheckVersion();
}
});
}
VersionBean versionBean;
private void CheckVersion() {
tipDialog.show();
HttpUtils httpUtils = new HttpUtils();
RequestParams params = new RequestParams();
httpUtils.send(HttpRequest.HttpMethod.GET, "http://47.107.173.197/Android/GetVersionInfo.php", new RequestCallBack() {
@Override
public void onSuccess(ResponseInfo responseInfo) {
tipDialog.dismiss();
try {
Gson gson = new Gson();
versionBean = gson.fromJson(responseInfo.result, VersionBean.class);
if (versionBean != null && versionBean.isSuccess()) {
DialogUtils.ShowTipsDialog(MainActivity.this, "发现新版本,是否立即更新?", "当前版本V"+versionBean.getVersionName()+"\n"+versionBean.getRemark(), new DialogReturnListner() {
@Override
public void onResultReturn(String... params) {
}
@Override
public void onResultReturn(int p1, String... params) {
}
@Override
public void onResultReturn(boolean p1, String... params) {
if (p1) {
if (EasyPermissions.hasPermissions(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE)){
StartUpdate(versionBean);
}else {
EasyPermissions.requestPermissions(MainActivity.this,"升级程序将App下载到手的过程中需要用到手机文件操作权限,请同意后才能进行正常升级",1234,Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
}
}
});
}
} catch (Exception e) {
}
}
@Override
public void onFailure(HttpException e, String s) {
tipDialog.dismiss();
}
});
}
private void StartUpdate(VersionBean versionBean) {
if (versionBean==null){
return;
}
ToastUtils.showToast(MainActivity.this, "正在更新...");
Intent intent=new Intent(MainActivity.this,ServiceLoadNewVersion.class);
intent.putExtra("path",versionBean.getApkPath());
startService(intent);
}
@Override
public void onPermissionsGranted(int requestCode, @NonNull List perms) {
if (requestCode==1234){
if (perms.contains(Manifest.permission.READ_EXTERNAL_STORAGE) && perms.contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)){
StartUpdate(versionBean);
}
}
}
@Override
public void onPermissionsDenied(int requestCode, @NonNull List perms) {
ToastUtils.showToast(this,"没有获取相关的权限,无法正常操作!");
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
// Forward results to EasyPermissions
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
}
ServiceLoadNewVersion
package cn.humanetplan.updateappdemo;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.FileProvider;
import android.util.Log;
import android.widget.RemoteViews;
import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.HttpHandler;
import com.lidroid.xutils.http.RequestParams;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;
import java.io.File;
/**
* 下载 新 版本 的 服务
*/
public class ServiceLoadNewVersion extends Service {
private RemoteViews remoteViews = null;
private Notification notification = null;
private NotificationManager notificationManager = null;
private PendingIntent pReDownLoadIntent = null;
private Handler myHandler;
// notification id
private final int NOTIFICATION_ID = 1000;
private final int START = 1001;
private final int LOADING = 1002;
private final int FINISHED = 1003;
private final int LOAD_ERROR = 1004;
private String url;
private String filePath;
String apkName = "updateTest.apk";
String loagPath = "";
public ServiceLoadNewVersion() {
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
try {
String path = intent.getStringExtra("path");
url = path;
myHandler = new MyHandler();
initUrls();
initNotification();
// 开始 下载
new DownLoadThread(url, filePath).start();
} catch (Exception e) {
}
return super.onStartCommand(intent, flags, startId);
}
/**
* 开始 下载
*/
private void initUrls() {
File file = new File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
, apkName);
if (file.exists()) {
file.delete();
}
filePath = file.getAbsolutePath();
}
NotificationCompat.Builder builder = null;
/**
* 配置 通知栏显示 样式
*/
private void initNotification() {
String id = "chanel_update";
String name = "水务集团";
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//如果是8以上的系统。需要传一个channelId.
builder = new NotificationCompat.Builder(this, id);
} else {
builder = new NotificationCompat.Builder(this);
}
builder.setContentTitle(getResources().getString(R.string.app_name) + "新版本下载").
setContentText("下载进行中...")
.setVibrate(new long[]{0})
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.applogo))
.setSmallIcon(R.mipmap.applogo)
.setProgress(100, 0, false);
// 下载 失败 重新 下载 的 PendingIntent
Intent reDownLoadIntent = new Intent(this, this.getClass());
reDownLoadIntent.putExtra("path", loagPath);
pReDownLoadIntent = PendingIntent.getService(this, 200, reDownLoadIntent, PendingIntent.FLAG_CANCEL_CURRENT);
remoteViews = new RemoteViews(getPackageName(), R.layout.view_download_notification);
remoteViews.setTextViewText(R.id.textViewTitle, "正在下载");
remoteViews.setTextViewText(R.id.textViewProgress, "进度0%");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {//android8.0以上通知的适配
NotificationChannel mChannel = new NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW);
mChannel.enableVibration(false);
mChannel.setVibrationPattern(new long[]{0});
notificationManager.createNotificationChannel(mChannel);
notification = builder.build();
} else {
notification = builder.build();
}
}
class MyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case START:
notificationManager.notify(NOTIFICATION_ID, notification);
break;
case LOADING:
builder.setProgress(100, msg.arg1, false);
builder.setContentText("下载进行中" + msg.arg1 + "/100");
notification = builder.build();
notificationManager.notify(NOTIFICATION_ID, notification);
//关键部分,如果你不重新更新通知,进度条是不会更新的
break;
case FINISHED:
notification.flags |= Notification.FLAG_AUTO_CANCEL;
// //关键部分,如果你不重新更新通知,进度条是不会更新的
notificationManager.notify(NOTIFICATION_ID, notification);
notificationManager.cancel(NOTIFICATION_ID);
installApkNew(null);
break;
case LOAD_ERROR:
builder.setContentTitle("新版本下载失败");
builder.setContentText("下载失败,点击重新下载!");
notification.contentIntent = pReDownLoadIntent;
// notification.flags = Notification.FLAG_NO_CLEAR; // 点击通知 不消失
//关键部分,如果你不重新更新通知,进度条是不会更新的
notificationManager.notify(NOTIFICATION_ID, notification);
break;
}
}
}
//安装apk
protected void installApkNew(Uri uri) {
try {
File file = new File(filePath);
Intent intent = new Intent(Intent.ACTION_VIEW);
// 由于没有在Activity环境下启动Activity,设置下面的标签
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= 24) { //判读版本是否在7.0以上
//参数1 上下文, 参数2 Provider主机地址 和配置文件中保持一致 参数3 共享的文件
Uri apkUri =
FileProvider.
getUriForFile(getApplicationContext(),
BuildConfig.APPLICATION_ID + ".provider",
file);
//添加这一句表示对目标应用临时授权该Uri所代表的文件
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
} else {
Uri mUri = Uri.fromFile(file);
mUri = Uri.parse(mUri.toString().replace("content", "file"));
intent.setDataAndType(mUri,
"application/vnd.android.package-archive");
}
startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
Log.i("ServiceLoadNewVersion", "exc " + e.toString());
}
}
class DownLoadThread extends Thread {
private String url;
private String filePath;
public DownLoadThread(String url, String filePath) {
this.url = url;
this.filePath = filePath;
}
@Override
public void run() {
HttpUtils http = new HttpUtils();
RequestParams params = new RequestParams();
HttpHandler handler = http.download(
url,//url
filePath, // 文件保存路径,
params,
true, // 如果目标文件存在,接着未完成的部分继续下载。服务器不支持RANGE时将从新下载。
false, // 如果从请求返回信息中获取到文件名,下载完成后自动重命名。
new RequestCallBack() {
@Override
public void onStart() {
myHandler.sendEmptyMessage(START);
}
@Override
public void onLoading(long total, long current, boolean isUploading) {
Message message = myHandler.obtainMessage();
message.what = LOADING;
message.arg1 = (int) ((float) current / (float) total * 100);
myHandler.sendMessage(message);
}
@Override
public void onSuccess(ResponseInfo responseInfo) {
myHandler.sendEmptyMessage(FINISHED);
}
@Override
public void onFailure(HttpException error, String msg) {
myHandler.sendEmptyMessage(LOAD_ERROR);
}
});
}
}
}
Manifest
代码比较简单,没有太多注释。
完整代码: UpdateAppDemo.zip
Github:https://github.com/shouPol/UpdateDemo