一般情况下,app要有在线升级的功能,以便app的功能不断完善改进。不同Android版本app升级的方法也不尽相同,版本不对,升级时,要么无法下载apk,要么下载后无法安装。今天介绍的是Android studio3.6.3版本,compileSdkVersion 为29,gradle版本为gradle-4.1-all。要想完成apk在线升级,首先需要必备的条件有四个:
第一、app具有检测自身版本的功能
第二、app具有检测服务器上存储的最新版本apk功能
第三、app具有能从网上下载最新版本apk功能
第四、app具有能自动安装最新版本apk功能,且能覆盖原有版本
根据以上4个条件,实现app在线升级的步骤如下:
目录
一、本例的layout设计
1、layout文件
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".VersionUpdateActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_stoke_black"
android:layout_marginTop="0dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintLeft_toLeftOf="parent"
android:gravity="center"
android:orientation="vertical">
<LinearLayout
android:id="@+id/app_version"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_stoke_black"
android:layout_marginTop="20dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintLeft_toLeftOf="parent"
android:gravity="center"
android:orientation="horizontal">
<!-- tools:ignore="MissingConstraints"-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:orientation="vertical" >
<TextView
android:id="@+id/app_version_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/black"
android:textSize="20dp"
android:layout_gravity="center_vertical"
android:layout_marginTop="0dp"
android:layout_marginLeft="0dp"
android:layout_marginRight="0dp"
android:layout_marginBottom="20dp"
android:text="版本信息"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:orientation="vertical" >
<TextView
android:id="@+id/app_version_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="left"
android:textColor="@color/black"
android:textSize="16dp"
android:layout_gravity="center_vertical"
android:layout_marginTop="5dp"
android:layout_marginLeft="0dp"
android:layout_marginRight="0dp"
android:layout_marginBottom="20dp"
android:background="@drawable/text"
android:text="现有版本"/>
<TextView
android:id="@+id/app_version_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="left"
android:textColor="@color/black"
android:textSize="16dp"
android:layout_gravity="center_vertical"
android:layout_marginTop="5dp"
android:layout_marginLeft="0dp"
android:layout_marginRight="0dp"
android:layout_marginBottom="20dp"
android:background="@drawable/text"
android:text="功能介绍"/>
<TextView
android:id="@+id/app_version_update"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="left"
android:textColor="@color/black"
android:textSize="16dp"
android:layout_gravity="center_vertical"
android:layout_marginTop="5dp"
android:layout_marginLeft="0dp"
android:layout_marginRight="0dp"
android:layout_marginBottom="20dp"
android:background="@drawable/text"
android:text="版本更新"/>
<com.jauker.widget.BadgeView
android:id="@+id/badge_install"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:gravity="center"
/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
2、显示效果
二、检测当前版本
1、当前版本信息在哪里?
设置apk版本信息的地方有两处:
(1)在AndroidManifest.xml文件中
在application下
android:versionCode="2"
“2”就是版本号
(2)在build.gradle(Moudle:app)中
例如:
android {
compileSdkVersion 29
buildToolsVersion "28.0.3"
defaultConfig {
applicationId "com.update"
minSdkVersion 19
targetSdkVersion 29
versionCode 2
versionName "2.0.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
versionCode为2, versionName为 "2.0.0"。
2、怎么检测当前版本?
如下代码,函数getVersionName() 为获取当前apk版本名versionName,函数getVersionCode()为获取当前apk版本号。
/*
* 获取当前apk版本名
*/
private String getVersionName() throws Exception {
PackageManager packageManager = getPackageManager();
PackageInfo packInfo = packageManager.getPackageInfo(getPackageName(), 0);
return packInfo.versionName;
}
/*
* 获取当前当前apk版本号
*/
private int getVersionCode() {
PackageManager packageManager = getPackageManager();
PackageInfo packInfo = packageManager.getPackageInfo(getPackageName(), 0);
return packInfo.versionCode;
}
三、获取服务器最新版本信息
假如服务器上apk版本存放的数据库为mysql,存放的表名为version,表设计如下
idversion | version_information | version_link |
1 | [{"appname":"app_name","apkname":"apkname-2.0-updateapk.apk","verName":2.0.1"verCode":3}] | https:/******************/app-release201.apk |
idversion是表的主键,version_information是存放版本信息的json格式数据包,数据包里包含appname、apkname和verName,verCode四项内容。version_link存放的是apk上传服务器的ip地址。每当有新版本发布,就把版本信息和链接存入表中最后一行,所以每次只需读取表中最后一行,即是目前最新版本。
1.读取服务器上最新版本apk版本信息、链接地址
new Thread(new Runnable() {
@Override
public void run() {
try {
Class.forName("com.mysql.jdbc.Driver");
//url,db_name,db_passwd为登录mysql的地址、用户名和密码
Connection connection = DriverManager.getConnection(url, db_name, db_passwd);
//取最后一条记录
String sql = "select * from pir_app_version order by idpir_app_version desc limit 1";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet set1 = preparedStatement.executeQuery();
while (set1.next()) {
//获得apk最新版本信息
version_information = set1.getObject("version_information").toString();
// 获得最新版本的链接
apkurl=set1.getObject("version_link").toString();
}
//解析出version_information的版本号
newVerCode=getServerVersionCode(version_information);
//解析出version_information的版本名
newVerName=getServerVersionName(version_information);
System.out.println("新版本号");
System.out.println(newVerCode);
set1.close();
preparedStatement.close();
connection.close();
mHandler.sendEmptyMessage(0);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
2、解析版本信息中版本名称、版本号函数
private String getServerVersionName(String verjson) {
String ServerVersionName = null;
try {
JSONArray array = new JSONArray(verjson);
if (array.length() > 0) {
JSONObject obj = array.getJSONObject(0);
try {
ServerVersionName = obj.getString("verName");
} catch (Exception e) {
ServerVersionName = "";
}
}
} catch (Exception e) {
Log.e(TAG, e.getMessage());
ServerVersionName = "false";
}
return ServerVersionName;
}
private int getServerVersionCode(String verjson) {
int ServerVersionCode = 0;
try {
JSONArray array = new JSONArray(verjson);
if (array.length() > 0) {
JSONObject obj = array.getJSONObject(0);
try {
ServerVersionCode = Integer.parseInt(obj.getString("verCode"));
} catch (Exception e) {
ServerVersionCode = -1;
}
}
} catch (Exception e) {
Log.e(TAG, e.getMessage());
ServerVersionCode = -2;
}
return ServerVersionCode;
}
四、比较当前版本与最新版本
1、版本号的作用
版本号的作用就是为了区别版本的高低,版本号是一个Integer类型的值,版本每升级一次,版本号就递增1。
2、版本名的意义
版本名只是为了描述该版本的具体序列信息的描述,这是一个值为String类型的属性,一般格式为:1.1.2(major.minor.point)的形式。
如果是一个UI风格或者业务逻辑有较大调整的应用,那么major递增1;
如果是在程序中的一些判断及算法方面做出修改,那么minor递增1;
如果只是在界面及一些性能方面做出调整及优化,那么point递增1。
3、比较版本高低和红点显示
检测完当前版本curVerCode和服务器最新版本newVerCode后,需要将两个版本进行比较,如果当前版本低于最新版本,那么需要在app中用红点进行显示,提醒用户更新版本。本例红点显示是在读取服务器版本信息之后,放在Handle中进行比较和显示。badgeView为BadgeView红点类。app_version_update为Textview类,其文字内容为“版本更新”,badgeView显示在这个Textview上。
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 0:
try {
curVerCode = getVersionCode();
System.out.println("现版本号");
System.out.println(curVerCode);
} catch (Exception e) {
e.printStackTrace();
}
if (curVerCode < newVerCode) {
badgeView.setTargetView(app_version_update); //设置哪个控件显示数字提醒,参数就是一个view对象
badgeView.setText("New");//显示“New”
badgeView.setBadgeGravity(Gravity.TOP|Gravity.CENTER_HORIZONTAL); //设置badgeview的显示位置
badgeView.setBackground(12, Color.parseColor("#ff0000")); //设置badgeview的背景色
}
break;
}
}
};
五、点击红点所在TextView下载服务器最新版本apk
1、显示程序更新对话框
app_version_update.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//如果有最新版本
if (curVerCode < newVerCode) {
showDialogUpdate();//弹出提示版本更新的对话框
} else {
//否则显示当前是最新版本
Toast.makeText(this, "当前已经是最新的版本", Toast.LENGTH_SHORT).show();
}
}
});
2、版本更新对话框函数
/**
* 提示版本更新对话框
*/
private void showDialogUpdate() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
// 设置提示框的标题
builder.setTitle("版本升级").
// 设置提示框的图标
setIcon(R.mipmap.ic_launcher).
// 设置要显示的信息
setMessage("发现新版本!请及时更新").
// 设置确定按钮
setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
loadNewVersionProgress();//下载最新的版本程序
}
}).
// 设置取消按钮,null是什么都不做,并关闭对话框
setNegativeButton("取消", null);
// 创建对话框
AlertDialog alertDialog = builder.create();
// 显示对话框
alertDialog.show();
}
3、下载程序进度条
/**
* 下载新版本程序进度条
*/
private void loadNewVersionProgress() {
final String uri=apkurl;
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 = new File(String.valueOf(getFileFromServer(uri, pd)));
sleep(3000);
installApk(file);
pd.dismiss(); //结束进度条对话框
} catch (Exception e) {
//下载apk失败
mHandler.sendEmptyMessage(0);
e.printStackTrace();
}
}
}.start();
}
4、从服务器下载程序并存储到手机Download文件夹内
/**
* 从服务器获取apk文件的代码
* 传入网址uri,进度条对象即可获得一个File文件
* (要在子线程中执行哦)
* @return
*/
public File getFileFromServer(String uri, ProgressDialog pd) throws Exception {
//如果相等的话表示当前的sdcard挂载在手机上并且是可用的
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
URL url = new URL(uri);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
//获取到文件的大小
pd.setMax(conn.getContentLength());
InputStream is = conn.getInputStream();
long time = System.currentTimeMillis();//当前时间的毫秒数
File dir = new
File(Environment.getExternalStoragePublicDirectory("/Download").getAbsolutePath());
System.out.println(dir);
if (!dir.exists()){
dir.mkdir();
System.out.println("创建目录");
}
//创建文件
File file = new File(dir+"/"+"SmartPir.apk");
if (!file.exists()){
file.createNewFile();
}
else
{
file.delete();
file.createNewFile();
}
System.out.println(file);
FileOutputStream fos = new FileOutputStream(String.valueOf(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;
}
}
5、覆盖安装
/**
* 安装apk
* @param apkPath
* @return
*/
protected boolean installApk(File apkPath) {
Intent intent= new Intent(Intent.ACTION_VIEW);
if (Build.VERSION.SDK_INT >=24) {
File file1= (new File(String.valueOf(apkPath)));
//设置intent的标签
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//参数1:上下文, 参数2:Provider主机地址 和配置文件中保持一致,参数3:共享的文件
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
Uri apkUri = FileProvider.getUriForFile(VersionUpdateActivity.this, "com.update.MyFileProvider", file1);//添加这一句表示对目标应用临时授权该Uri所代表的文件
intent.setDataAndType(apkUri,"application/vnd.android.package-archive");
}else{
intent.setDataAndType(Uri.fromFile(new File(String.valueOf(apkPath))),"application/vnd.android.package-archive");
}
VersionUpdateActivity.this.startActivity(intent);
return false;
}
VersionUpdateActivity为本例程的class名称,com.update为包名。
6、xm文件夹下file_paths.xml文件
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path path="Download/" name="download" />
</paths>
7、AndroidManifest.xml中添加
<provider
android:name="com.version.MyFileProvider"
android:authorities="com.version.MyFileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
8、动态授权函数
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == 1000)
{
if (grantResults[0] == PackageManager.PERMISSION_GRANTED)
{
//同意申请权限
} else
{
// 用户拒绝申请权限
Toast.makeText(VersionUpdateActivity.this,"请同意写操作", Toast.LENGTH_SHORT).show();
}
return;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
9、在oncreat中的一些操作
//设置标题栏
ActionBar actionBar=getSupportActionBar();
if(actionBar!=null)
{
actionBar.setTitle("版本升级");
}
//开放写入外部存储器权限
if (ContextCompat.checkSelfPermission(VersionUpdateActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager. PERMISSION_GRANTED) {
ActivityCompat. requestPermissions( this, new String[]{Manifest.permission. WRITE_EXTERNAL_STORAGE,Manifest.permission.READ_EXTERNAL_STORAGE },
1000);
}
//读取string文件中mysql地址、用户名和密码信息
url = getText(R.string.db_url).toString();
db_name = getText(R.string.db_name).toString();
db_passwd = getText(R.string.db_passwd).toString();
//绑定控件
app_version_name=findViewById(R.id.app_version_name);
app_version_content=findViewById(R.id.app_version_content);
app_version_update=findViewById(R.id.app_version_update);
badgeView = findViewById(R.id.badge_install);
10、本例中用到的一些变量
BadgeView badgeView;
TextView app_version_name,app_version_content,app_version_update;
String url,apkurl;
String db_name;
String db_passwd;
String TAG = "myupdate";
int newVerCode;
String newVerName;
int curVerCode;
String curVerName;
String version_information;
六、生成release版apk
注意,以上apk都需是release版本,如果是debug版本,则无法进行覆盖安装。
签名打包操作步骤可参考文章《Android 签名打包》
注意,签名和打包是两个过程。
签名是将key store password、key alias、keypassword三项信息写入jks文件,生成一个签名文件。
打包,是将程序文件和jks签名文件,打包成一个包含签名的apk文件。要想完成高版本覆盖低版本的安装,必须是带有签名的apk,否则无法安装。