一、应用场景及介绍
- 不多讲,APP更新几乎每个APP都会用到。
- 更新APP的选择:
一、根据自己APP使用的网格框架,自己写一套下载的逻辑,移植兼容性不强。
二、用系统的下载器(DownloadManager),移植没兼容性问题。
二、更新的展示方式
- APP内UI直接展示。
- 退到后台,通知栏更新。
三、更新内容分析
- APK下载的URL,APK的版本号。
- 本地存储路径,APK本地存储名称等。
- 下载进度的回调,用于内部UI更新。
四、创建更新内容实体类
/**
* 更新apk的Bean
*/
public class AppUpdateBean implements Serializable {
// 更新APK的URL
private String apkUrl;
// 后台最新的版本号
private int versionCode;
// ==============附加信息可以不填写==============
// apk文件名称(本地的名称)
private String apkName;
public AppUpdateBean(String apkUrl, String apkName, int versionCode) {
this.apkUrl = apkUrl;
this.apkName = apkName;
this.versionCode = versionCode;
}
public String getApkUrl() {
return apkUrl;
}
public int getVersionCode() {
return versionCode;
}
public String getApkName() {
return apkName;
}
}
五、定义下载进度回调监听接口
/**
* 更新APK的监听
*/
public abstract class OnUpdateListener implements Serializable {
/**
* 下载失败
*/
public abstract void onFailed(String msg);
/**
* 下载成功
*/
public abstract void onSucceed(File apkFile);
/**
* 下载进度
* @param total 总APK大小
* @param current 当前进度
* @param progress 进度百分比
*/
public abstract void onProgress(int total, int current, float progress);
}
六、更新步骤分析
- 请求后台获取更新APK的内容。
- 判断后台APK的版本和当前使用APK的版本号,根据版本号情况进行更新。
- 下载前,判断本地有没有下载好,下载好根据版本号情况直接安装或者重装下载。
七、使用DownloadManager下载器进行下载更新
/**
* 下载更新APP的工具类
*/
public class DownloadUtils {
private static final String CONFIG = "APK_UPDATE";
private static final String DOWNLOADED = "DOWNLOADED";
// 默认APK本地名称
public static final String DEF_APK_NAME = "update.apk";
private final SharedPreferences mPreferences;
// 下载器
private DownloadManager downloadManager;
private Context mContext;
// 下载的ID
private long downloadId = -1;
// 下载要用到的类
private AppUpdateBean mUpdateBean;
private OnUpdateListener mOnUpdateListener;
private static Handler mHandler = new Handler(Looper.getMainLooper());
public DownloadUtils(Context context, AppUpdateBean updateBean) {
if (mContext == null) {
new RuntimeException("context is null");
}
this.mContext = context.getApplicationContext();
this.mUpdateBean = updateBean;
mPreferences = mContext.getSharedPreferences(CONFIG, Context.MODE_PRIVATE);
}
/**
* 注意:要自己添加内存卡读取权限
* 下载apk主要方法
*/
public void downloadAPK() {
// 1. 非空校验
if (mContext == null || mUpdateBean == null) {
return;
}
// 2. URL校验
String url = mUpdateBean.getApkUrl();
if (TextUtils.isEmpty(url) || !URLUtil.isNetworkUrl(mUpdateBean.getApkUrl())) {
Toast.makeText(mContext, "APK下载地址不正确", Toast.LENGTH_SHORT).show();
return;
}
// 1. 在这里要做一下校验
File apkFile = getApkFile();
// 判断有没有下载成功
if (apkFile.exists() && apkFile.isFile() && apkFile.length() > 1024 && isDownload()) {
try {
// 判断下载好的APK版本和正在使用的APK版本
int versionCode = AppUpdateUtils.getVersionCode(mContext);
// 获取下载好的APK版本号
PackageInfo packageInfo = mContext.getPackageManager()
.getPackageArchiveInfo(apkFile.getAbsolutePath(), PackageManager.GET_ACTIVITIES);
int apkVersionCode = packageInfo.versionCode;
// 1. 下载好的APK是不是最新的
// 2. 正在使用的APK是不是最新的
if (apkVersionCode >= mUpdateBean.getVersionCode() && apkVersionCode > versionCode) {
if (mOnUpdateListener != null) {
mOnUpdateListener.onSucceed(apkFile);
} else {
installAPK();
}
return;
}
} catch (Throwable e) {
e.printStackTrace();
}
}
// 2. 删除APK
deleteApkFile(apkFile);
// 3. 创建下载任务
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
// 移动网络情况下是否允许漫游
request.setAllowedOverRoaming(false);
request.allowScanningByMediaScanner();
// 在通知栏中显示,默认就是显示的
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
request.setTitle("APP名称");
request.setDescription("版本更新");
request.setVisibleInDownloadsUi(true);
//4. 设置下载的路径
request.setDestinationUri(Uri.fromFile(getApkFile()));
// 获取DownloadManager
if (downloadManager == null)
downloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
// 将下载请求加入下载队列,加入下载队列后会给该任务返回一个long型的id,通过该id可以取消任务,重启任务、获取下载的文件等等
if (downloadManager != null) {
downloadId = downloadManager.enqueue(request);
}
// 注册广播接收者,监听下载完成状态
mContext.registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
if (mOnUpdateListener != null) {
// 开启个任务去每秒查询下载进度
mHandler.postDelayed(mTask, 1000);
}
}
private Runnable mTask = new Runnable() {
@Override
public void run() {
if (mOnUpdateListener != null) {
checkStatus();
mHandler.postDelayed(mTask, 1000);
}
}
};
/**
* 手动删除原来的APK,下载器不会覆盖。
*/
private void deleteApkFile(File apkFile) {
try {
putDownload(false);
if (apkFile.exists()) {
apkFile.delete();
}
} catch (Throwable e) {
e.printStackTrace();
}
}
/**
* 广播监听下载完成
*/
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 下载完成会调一次这里
checkStatus();
}
};
/**
* 检查下载状态
*/
private void checkStatus() {
if (downloadId == -1) {
return;
}
DownloadManager.Query query = new DownloadManager.Query();
// 通过下载的id查找
query.setFilterById(downloadId);
Cursor cursor = downloadManager.query(query);
if (cursor == null) {
return;
}
if (cursor.moveToFirst()) {
int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
try {
// 如果有下载监听就去查询文件下载进度
if (mOnUpdateListener != null) {
//已经下载文件大小
int current = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
//下载文件的总大小
int total = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
if (total >= 0 && current >= 0 && mOnUpdateListener != null) {
// 更新进度
float progress = current * 100f / total;
mOnUpdateListener.onProgress(total, current, progress);
}
}
} catch (Throwable e) {
}
switch (status) {
// 下载暂停
case DownloadManager.STATUS_PAUSED:
break;
// 下载延迟
case DownloadManager.STATUS_PENDING:
break;
// 正在下载
case DownloadManager.STATUS_RUNNING:
break;
// 下载完成
case DownloadManager.STATUS_SUCCESSFUL:
mHandler.removeCallbacks(mTask);
// 下载完成安装APK
putDownload(true);
// 有监听让用户去做
if (mOnUpdateListener != null) {
mOnUpdateListener.onSucceed(getApkFile());
} else {
installAPK();
}
cursor.close();
if (mContext != null) {
mContext.unregisterReceiver(receiver);
}
break;
// 下载失败
case DownloadManager.STATUS_FAILED:
mHandler.removeCallbacks(mTask);
if (mOnUpdateListener != null) {
mOnUpdateListener.onFailed("下载失败");
} else {
Toast.makeText(mContext, "下载失败", Toast.LENGTH_SHORT).show();
}
cursor.close();
break;
}
}
}
/**
* 安装APK
*/
private void installAPK() {
// 安装APK
AppFileProvider.installApk(mContext, getApkFile());
}
/**
* 文件下载的路径
*/
private File getApkFile() {
String fileName = mUpdateBean.getApkName();
if (TextUtils.isEmpty(fileName)) {
fileName = DEF_APK_NAME;
}
return AppUpdateUtils.getApkFile(mContext, fileName);
}
/**
* 缓存下载信息
*/
private void putDownload(boolean isDownload) {
if (mPreferences == null) {
return;
}
mPreferences.edit().putBoolean(DOWNLOADED, isDownload).commit();
}
/**
* 有没有下载完成
*/
private boolean isDownload() {
if (mPreferences == null) {
return false;
}
return mPreferences.getBoolean(DOWNLOADED, false);
}
public void setOnUpdateListener(OnUpdateListener onUpdateListener) {
mOnUpdateListener = onUpdateListener;
}
/**
* 如果设置了下载进度回调,在Activity 的OnDestroy方法调用 一下。
*/
public void stop() {
try {
mOnUpdateListener = null;
mHandler.removeCallbacks(mTask);
if (mContext != null) {
mContext.unregisterReceiver(receiver);
}
} catch (Throwable e) {
e.printStackTrace();
}
}
}
下载APK封装类,主要下载方法是downloadAPK(),如果只是通知栏更新,就不用下载进度回调了。
八、下载用到的一些方法工具类封装。
/**
* 兼容7.0文件路径配置
*/
public class AppFileProvider {
/**
* 安装APK
*/
public static void installApk(Context context, File apk) {
if (context == null || apk == null) {
return;
}
// 1.修改文件权限
setPermission(apk.getAbsolutePath());
// 2. 安装APK
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
String type = "application/vnd.android.package-archive";
if (Build.VERSION.SDK_INT >= 24) {
Uri uriForFile = FileProvider.getUriForFile(
context,
context.getPackageName() + ".app.update.FileProvider",
apk
);
intent.setDataAndType(uriForFile, type);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} else {
intent.setDataAndType(Uri.fromFile(apk), type);
}
context.startActivity(intent);
}
/**
* 修改文件权限
*/
private static void setPermission(String absolutePath) {
try {
String command = "chmod " + "777" + " " + absolutePath;
Runtime runtime = Runtime.getRuntime();
runtime.exec(command);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
/**
* APP更新工具类
*/
public class AppUpdateUtils {
/**
* 获取版本号
*/
public static int getVersionCode(Context context) {
if (getPackageInfo(context) != null) {
try {
return getPackageInfo(context).versionCode;
} catch (Exception e) {
}
}
return 0;
}
private static PackageInfo getPackageInfo(Context context) {
if (context == null) {
return null;
}
PackageInfo pi = null;
try {
PackageManager pm = context.getPackageManager();
pi = pm.getPackageInfo(context.getPackageName(), 0);
return pi;
} catch (Exception e) {
e.printStackTrace();
}
return pi;
}
/**
* 检查是否SDK准备好
*/
private static boolean checkSDExist() {
return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
}
/**
* 创建apk下载文件
* 这里如果有其它需求,可以改成你想要的下载路径
*/
public static File getApkFile(Context context, String fileName) {
// 创建目录
File directory = null;
if (checkSDExist()) {
directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
}
if (directory == null) {
directory = context.getCacheDir();
}
if (!directory.exists()) {
directory.mkdirs();
}
File apkFile = new File(directory, fileName);
return apkFile;
}
}
九、清单权限和7.0系统文件路径配置
// ====文件:file_app_update_paths.xml====
十、测试
/**
* 下载测试
*/
public class MainActivity extends AppCompatActivity {
private TextView mTextView;
private DownloadUtils mDownloadUtils;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = findViewById(R.id.tv);
}
public void update(View view) {
// 申请权限
XPermission.with(this)
.permissions(Permission.STORAGE)
.request(new PermissionListenerAdapter() {
@Override
public void onSucceed() {
downloadApk();
}
});
}
/**
* 下载代码
*/
private void downloadApk() {
String apkUrl = "https://xxx/app/B_2.8.3_0105_online.apk";
AppUpdateBean bean = new AppUpdateBean(apkUrl, "CarHouse.apk", 123);
mDownloadUtils = new DownloadUtils(MainActivity.this, bean);
mDownloadUtils.setOnUpdateListener(new OnUpdateListener() {
@Override
public void onFailed(String msg) {
Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
}
@Override
public void onSucceed(File apkFile) {
// TODO 安装
Toast.makeText(getApplicationContext(), "下载成功", Toast.LENGTH_SHORT).show();
}
@Override
public void onProgress(int total, int current, float progress) {
mTextView.setText(String.format("%.2f", progress) + "%");
}
});
mDownloadUtils.downloadAPK();
}
@Override
protected void onDestroy() {
if (mDownloadUtils != null) {
mDownloadUtils.stop();
}
super.onDestroy();
}
}
测试说明:
- 测试没有请求后台接口的实现,实际开发是请求后台的接口,拿到APK更新数据。
- 如果只是通知栏更新,就直接创建DownloadUtils对象调一下downloadAPK()方法即可。
- 权限申请可以用自己项目的。
- 全部更新代码都在这里了,自己考过去就可以用了。
十一、注意事项
- Android 6.0权限申请。
- 7.0系统文件路径配置。
- 8.0系统清单权限配置。
- 项目地址:https://github.com/wenkency/update