一.准备工作
1.打包更新的apk文件
注意更新的apk文件版本号必须比之前高,否则可能会导致之后安装失败!
在清单文件中设置或在build.gradle文件中设置,注意若二者不统一,会以build.gradle文件的版本号为准,具体字段为:versionCode和versionName。其中versionCode为整数,versionName为字符串类型。
首先,安卓应用分为debug版本和release版本,其中debug版本为调试版本,release版本为发布版本,一般来说在普通用户的设备上进行安装的都是release版本。故而我们需要打包release版本的应用,具体打包步骤如下:
·如图点击:
选择APK并点击next
由于是第一次打包,所以我们需要先创建一个签名证书文件,点击create new...
点击进去后可以看到这个界面,直接填就好啦,不过两个密码要记住哦,因为以后如果还想复用这个签名证书的话就需要输入密码。填写好后点击OK
点击要打包的版本,点击Create就可以打包啦
等一会就会出现如图的release文件夹啦,下面的两个文件就分别一个是打包好的安装包、一个是更新Json文件啦
2..在服务器中准备好数据,包括:更新的安装包和更新json文件(这里使用Tomcat服务器为例模拟一下)
我这边是将刚刚打包好的apk文件复制到Tomcat的ROOT文件路径下,然后自己写了一个更新json文件来给应用读取的
json文件示例如下:
{
"versionCode": 2,
"versionName": "2.0",
"description": "新增功能,赶紧来体验吧!",
"downloadUrl": "http://192.168.1.101:8080/safeAPP/mobileSafeGuard-release.apk"
}
3.在清单文件中申明需要的权限
由于此处我们使用的Tomcat服务器,故而更新安装包的下载地址为http格式的而不是https格式的,故而需要在AndroidManifest.xml文件下的application标签增加属性,以允许明文流量
android:usesCleartextTraffic="true"
二.访问服务器的json文件检查更新
这里使用OkHttpClient进行子线程访问服务器,所有相关示例代码如下:
1.访问服务器代码:
此处将访问到的JSON数据解析后封装为UpdateInfo类,类中的属性包括:更新后的apk描述、apk版本号、apk版本名称、apk下载地址
private void initData() {
//创建okhttpClient类的对象
OkHttpClient okHttpClient = new OkHttpClient();
//传递要连接的URL
Request url = new Request.Builder().url(Constant.WEB_SITE + Constant.UPDATE_INFO).build();
//使用okhttpClient类的对象调用newCall()方法获取到Call类的对象
Call call = okHttpClient.newCall(url);
//用Call类的对象调用enqueue()方法开启异步线程访问网络
call.enqueue(new Callback() {
//访问失败
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
//封装信息发送到主线程
Message msg = Message.obtain();
msg.what = MSG_NOT_OK;
handler.sendMessage(msg);
Log.d("tag", e.getMessage());
}
//访问成功
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
//获取服务器返回的json数据并转化为字符串
String json = response.body().string();
//解析json字符串数据
UpdateInfo data = JsonUtils.getData(json);
//封装信息发送到主线程
Message msg = Message.obtain();
msg.what = MSG_OK;
msg.obj = data;
handler.sendMessage(msg);
Log.d("tag", "服务器访问成功" + data.toString());
}
});
}
2.Handler类
此处使用Handler类来处理子线程访问服务器后需要做出的反应:即需不需要更新
需要,则弹出对话框供用户选择
不需要,则直接跳转到另一个界面
private final Handler handler = new Handler(new Handler.Callback() {
//处理子线程传递过来的信息
@Override
public boolean handleMessage(@NonNull Message msg) {
//判断服务器是否访问成功
switch (msg.what) {
case MSG_NOT_OK: {
//访问失败
Log.d("tag", "服务器数据请求失败");
Toast.makeText(SplashActivity.this, "服务器错误或网络错误", Toast.LENGTH_SHORT).show();
break;
}
case MSG_OK: {
//访问成功
//更新UI
updateData = (UpdateInfo) msg.obj;
Log.d("tag", updateData.getDescription());
//判断版本是否更新
if (checkVersion()) {
//更新
//弹出更新对话框
showUpdateDialog();
}
} else {
//不更新
//直接跳转到主界面
Intent intent = new Intent(getApplicationContext(),MainActivity.class);
startActivity(intent);
finish();
}
}
break;
}
}
return true;
}
});
3.更新对话框 :
是,则创建AutoUpdater类的对象并调用DownloadAPK()方法进行下载(注意,此处的AutoUpdtater类为我们自己封装的类,在下面会仔细介绍),关闭对话框
否,关闭对话框,直接跳转到另一个界面
//弹出更新对话框
private void showUpdateDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("最新版本:" + updateData.getVersionCode());
builder.setMessage(updateData.getDescription());
builder.setPositiveButton("马上更新", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//下载apk
/*FileManager.downUpdateAPK(getApplicationContext(),updateData.downloadUrl,"手机安全卫士app" + SplashActivity.updateData.getVersionName());*/
AutoUpdater autoUpdater = new AutoUpdater(getApplicationContext());
autoUpdater.DownloadApk();
alertDialog.dismiss();//关闭对话框
}
});
builder.setNegativeButton("以后再说", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
alertDialog.dismiss();//关闭对话框
//跳转到主界面
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
startActivity(intent);
//关闭此界面
finish();
}
});
alertDialog = builder.create();
alertDialog.show();
}
4.获取本地版本号
//获取本地版本号
private int getVersionCode() {
PackageInfo packageInfo = null;
try {
packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
return packageInfo.versionCode;
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
}
5.判断版本是否更新:
把从服务器上访问的数据中的版本号与获取到的本地版本号进行对比,若大于则需要更新
//判断版本是否更新
private boolean checkVersion() {
return updateData.getVersionCode() > getVersionCode();
}
6.JSON数据解析工具类(主要用于解析从服务器访问到的更新Json文件):
package com.example.mobiilesafeguard.utils;
import com.example.mobiilesafeguard.bean.UpdateInfo;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
//解析从服务器中获取的json数据
public class JsonUtils {
public static UpdateInfo getData(String json){
Gson gson = new Gson();
Type type = new TypeToken
}.getType();
UpdateInfo updateInfo = gson.fromJson(json, type);
return updateInfo;
}
}
三.使用DownloadManager到服务器上下载apk文件并自动安装
1.AutoUpdtater类
此处我们将下载APK,安装APK等一系列操作封装为一个类AutoUpdtater。
示例代码如下:
package com.example.mobiilesafeguard.utils;
import android.annotation.SuppressLint;
import android.app.DownloadManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import androidx.core.content.FileProvider;
import com.example.mobiilesafeguard.activity.SplashActivity;
import java.io.File;
//将检查更新,下载apk,安装apk等操作封装成了一个类
public class AutoUpdater {
// 下载安装包的网络路径
private String apkUri = SplashActivity.updateData.downloadUrl;
// 保存APK的文件名
private static final String saveFileName = "/mobileSafeGuard-release.apk";
// 下载线程
private Thread downLoadThread;
// 应用程序Context
private Context mContext;
public static DownloadManager downloadManager;
public AutoUpdater(Context context) {
mContext = context;
}
//下载安装包
public void DownloadApk() {
downLoadThread = new Thread(DownApkWork);
downLoadThread.start();
}
//下载线程任务
private final Runnable DownApkWork = new Runnable() {
@Override
public void run() {
//获取更新安装包的下载地址
Uri uri = Uri.parse(apkUri);
// 创建下载任务
DownloadManager.Request request = new DownloadManager.Request(uri);
//设置通知栏标题
request.setVisibleInDownloadsUi(true);
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setTitle("手机安全卫士"+SplashActivity.updateData.getVersionName());
request.setDescription("正在下载更新...");
request.setMimeType("application/vnd.android.package-archive");
//设置文件存放目录
// 设置下载路径和下载的apk名称
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, saveFileName);
Log.d("tag", Environment.DIRECTORY_DOWNLOADS + saveFileName);
//获取系统服务
downloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
// 将下载任务加入下载队列,并返回下载的id值
long downloadId = downloadManager.enqueue(request);
//将下载id存储到SharedPreferences中
SharedPreferences sharedPreferences = mContext.getSharedPreferences("com.example.mobiilesafeguard.utils.AutoUpdater", Context.MODE_PRIVATE);
SharedPreferences.Editor edit = sharedPreferences.edit();
edit.putLong("downloadId", downloadId);
edit.apply();
}
};
/**
* 安装APK内容
*/
//跳转安装 apk 需要适配不同的安卓版本,
// Android 7.0 以上需要 “content://” 的路径
// Android 6.0-7.0 需要老式的 “file://” 的路径,
public void installAPK() {
//获取内存中存储的下载id
SharedPreferences sharedPreferences = mContext.getSharedPreferences("com.example.mobiilesafeguard.utils.AutoUpdater", Context.MODE_PRIVATE);
long downloadId = sharedPreferences.getLong("downloadId", -1L);
//根据id查找apk的下载uri路径以安装
Uri downloadUri = getDownloadUri(downloadId);
//根据id查找apk的下载file://开头路径以安装
File downloadFile = getDownloadFile(mContext, downloadId);
try {
if (!downloadFile.exists()) {
Log.d("tag", "downloadFile路径:" + downloadFile.getAbsolutePath());
Log.d("tag", "安装包不存在");
return;
}
Log.d("tag", "正在安装");
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//安装完成后打开新版本
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 给目标应用一个临时授权
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
//如果SDK版本>24,即:Build.VERSION.SDK_INT > 24,使用FileProvider兼容安装apk
Uri uri = FileProvider.getUriForFile(mContext, mContext.getPackageName() + ".fileprovider", downloadFile);
intent.setDataAndType(downloadUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(downloadFile), "application/vnd.android.package-archive");
}
mContext.startActivity(intent);
/*
android.os.Process.killProcess(android.os.Process.myPid());//安装完之后会提示”完成” “打开”。
*/
} catch (Exception e) {
e.printStackTrace();
}
}
//获取下载安装包的保存地址
public static Uri getDownloadUri(long downloadId) {
return downloadManager.getUriForDownloadedFile(downloadId);
}
//根据下载id获取到 “file://” 的路径
private static File getDownloadFile(Context context, long downloadId) {
File targetApkFile = null;
DownloadManager downloader = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
if (downloadId != -1L) {
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(downloadId);
query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);
Cursor cur = downloader.query(query);
if (cur != null) {
if (cur.moveToFirst()) {
@SuppressLint("Range") String uriString = cur.getString(cur.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
if (!TextUtils.isEmpty(uriString)) {
targetApkFile = new File(Uri.parse(uriString).getPath());
}
}
cur.close();
}
}
return targetApkFile;
}
}
注意:
(1)在安装apk时,需要注意适配不同的安卓版本,否则可能导致安装失败
Android 7.0 以上需要 “content://” 的路径
Android 6.0-7.0 需要老式的 “file://” 的路径
(2)在Android7.0以后,对权限有了更加严格的控制,故而在访问设备的内部文件时也需要更多的权限,而我们在安装AKP时需要访问到设备中的一些内部文件,因而需要在清单文件中采用FileProvider来对需要访问的文件进行暴露,否则将无法访问到。FileProvider其实是内容提供者的一个子类,可以对让一些文件进行临时暴露,以被我们访问。示例代码如下:
android:name="androidx.core.content.FileProvider" android:authorities="com.example.mobiilesafeguard.fileprovider" android:exported="false" android:grantUriPermissions="true"> android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
其中 file_paths文件是需要我们自定义的,我们需要在res文件夹下创建一个xml类型的文件夹,来存放此文件,此文件中定义了我们需要暴露(访问)的文件路径,示例代码如下:
name="root_path" path=""/> name="download" path="download" /> 2.监听APK下载以安装APK 由于之前我们使用的下载APK方式是DownloadManager,故而只需要静态注册一个广播接收者来监听DownloadManager下载完成之后发送过来的广播即可,示例代码如下: (1)创建一个广播接收者的子类: APKInstallReceiver 在监听到 DownloadManager.ACTION_DOWNLOAD_COMPLETE广播时,则意味着已经下载成功,可以继续安装了 package com.example.mobiilesafeguard.BroadcastReceiver; import android.app.DownloadManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; import android.widget.Toast; import com.example.mobiilesafeguard.utils.AutoUpdater; //监听app是否下载成功的广播接收者 public class APKInstallReceiver extends BroadcastReceiver { //DownloadManager下载完成后会自动发送一个包含ACTION_DOWNLOAD_COMPLETE动作的广播 @Override public void onReceive(Context context, Intent intent) { if(intent.getAction().equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)){ //下载完成 Toast.makeText(context, "下载完成!", Toast.LENGTH_SHORT).show(); Log.d("tag","下载完成-------"); //开始安装 AutoUpdater autoUpdater = new AutoUpdater(context); autoUpdater.installAPK(); } } } (2)在清单文件中静态注册广播接收者: android:name=".BroadcastReceiver.APKInstallReceiver" android:enabled="true" android:exported="true"> 3.签名文件问题:(必看) 到此处还没有结束,还记得之前讲的debug版本和release版本吗?如果你是直接使用Android Studio运行到模拟器上的,那么完成上述操作后你一定可以下载成功,但不一定不能安装成功(除非把原来的apk卸载了,再去安装你下载下来的那个安装包)。这是为什么呢? 这正是因为apk的版本问题,其实我们上面的安装操作都是交由系统去处理的,而系统又是怎么知道我们现在安装的这个apk是我们之前软件的新版安装包,而不是别的软件的呢?原因是,在进行apk安装时,系统会主要检查几个东西:软件的包名是否与之前的一致、软件的版本号、软件的签名证书。 我们直接从Android Studio运行到模拟器上的应用是debug版本的,并没有像之前打包发布release版本的apk一样给它一个正式的签名证书,而是由系统自动生成一个测试的签名证书(此证书默认在C盘路径下的.Android文件夹中)如我的电脑上的路径: 故而若在下载完安装包后直接安装,会提示当前软件包与现有软件包冲突(即签名证书不一致的问题)。 要解决这个问题,只需要在当前应用的build.gradle文件下为你发布的debug版本apk也设置与之前release版本一样的签名证书就可以了。具体示例代码如下: android { namespace 'com.example.mobiilesafeguard' compileSdk 33 //签名配置 signingConfigs { release { { //签名文件路径,我是放到项目中了 storeFile file { "D:\\AndroidStudioCode\\MyApplication1\\mobileSafeGuard\\mobileSafeGuard.jks" } //签名密码 storePassword "123456" //别名 keyAlias "mobileSafeGuard" //别名密码 keyPassword "xxxxxx" } } defaultConfig { applicationId "com.example.mobiilesafeguard" minSdk 24 targetSdk 33 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { signingConfig signingConfigs.release minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' android.applicationVariants.all { variant -> variant.outputs.all { outputFileName = "mobileSafeGuard-release.apk" } } } //调试签名:使用正式签名 debug { signingConfig signingConfigs.release proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } } 参考文章: 【Android】app应用内版本更新升级(DownloadManager下载,适配Android6.0以上所有版本)_android app更新_Full guts的博客-CSDN博客 Android开发——debug模式配置正式签名_android debug包签名_轻量开发的博客-CSDN博客