在做需求开发时,少不了需要从服务器上直接下载东西,比如图片、视频、apk等。下载后的东西,还需要做起一些其他操作。
对于下载,可以采用WorkManager,也可以采用现有的包,比如Fetch。下载后的apk,针对Android 9.0版本,采用静默安装。
注意:系统应用才有权限静默安装
参考链接:
Fetch
build.gradle中引入:
implementation 'androidx.tonyodev.fetch2:xfetch2:3.1.6'
AndroidManifest.xml中配置:
Fetch使用 Fetch初始化// 以上三种就OK
public void initFetch(){
final FetchConfiguration fetchConfiguration = new FetchConfiguration.Builder(mContext)
.setDownloadConcurrentLimit(4) // 并行下载数量
.setAutoRetryMaxAttempts(3) // 失败时,重试3次
.setNamespace(FETCH_NAMESPACE)
.setProgressReportingInterval(PROGRESS_REPORTING_INTERVAL)
.build();
mFetch = Fetch.Impl.getInstance(fetchConfiguration);
mFetch.addListener(fetchListener);
}
监听状态
监听Fetch的状态,例如下载时使用进度条查看百分比
private final FetchListener fetchListener = new AbstractFetchListener() {
@Override
public void onAdded(@NonNull Download download) {
super.onAdded(download);
Log.d(TAG,"fetchListener: onAdded : file="+download.getFile());
}
@Override
public void onCancelled(@NonNull Download download) {
super.onCancelled(download);
Log.d(TAG,"fetchListener: onCancelled : file="+download.getFile());
}
@Override
public void onCompleted(@NonNull Download download) {
if (listener!=null){
listener.onCompleted(download.getFile());
Log.d(TAG,"fetchListener file="+download.getFile());
}
super.onCompleted(download);
}
@Override
public void onDeleted(@NonNull Download download) {
super.onDeleted(download);
Log.d(TAG,"fetchListener: onDeleted : file="+download.getFile());
}
@Override
public void onDownloadBlockUpdated(@NonNull Download download, @NonNull DownloadBlock downloadBlock, int totalBlocks) {
super.onDownloadBlockUpdated(download, downloadBlock, totalBlocks);
Log.d(TAG,"fetchListener: onDownloadBlockUpdated : file="+download.getFile());
}
@Override
public void onError(@NonNull Download download, @NonNull Error error, @Nullable Throwable throwable) {
Log.e(TAG,"fetchListener: onError: msg="+error.toString()+" filePath="+download.getFile());
super.onError(download, error, throwable);
}
@Override
public void onPaused(@NonNull Download download) {
super.onPaused(download);
Log.d(TAG,"fetchListener: onPaused : file="+download.getFile());
}
@Override
public void onProgress(@NonNull Download download, long etaInMilliSeconds, long downloadedBytesPerSecond) {
long logTime = 0;
if (SystemClock.uptimeMillis() - logTime > TimeUnit.SECONDS.toMillis(10)) {
Log.d(TAG, "downloading: "+download.getFile()+" :progress "+download.getProgress());
logTime = SystemClock.uptimeMillis();
}
ThreadPoolUtil.runMain(()->{
mDialogManager.updateViews(download);
});
super.onProgress(download, etaInMilliSeconds, downloadedBytesPerSecond);
Log.d(TAG,"fetchListener: onProgress : file="+download.getFile());
}
@Override
public void onQueued(@NonNull Download download, boolean waitingOnNetwork) {
super.onQueued(download, waitingOnNetwork);
Log.d(TAG,"fetchListener: onQueued : file="+download.getFile());
}
@Override
public void onRemoved(@NonNull Download download) {
super.onRemoved(download);
}
@Override
public void onResumed(@NonNull Download download) {
super.onResumed(download);
}
@Override
public void onStarted(@NonNull Download download, @NonNull List extends DownloadBlock> downloadBlocks, int totalBlocks) {
super.onStarted(download, downloadBlocks, totalBlocks);
}
@Override
public void onWaitingNetwork(@NonNull Download download) {
Log.d(TAG,"fetchListener onWaitingNetwork "+download.getFile());
super.onWaitingNetwork(download);
}
};
全部代码
package com.ym.fetchdemo.util;
import android.content.Context;
import android.net.Uri;
import android.os.Environment;
import android.os.SystemClock;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.tonyodev.fetch2.AbstractFetchListener;
import com.tonyodev.fetch2.Download;
import com.tonyodev.fetch2.Error;
import com.tonyodev.fetch2.Fetch;
import com.tonyodev.fetch2.FetchConfiguration;
import com.tonyodev.fetch2.FetchListener;
import com.tonyodev.fetch2.Request;
import com.tonyodev.fetch2core.DownloadBlock;
import com.tonyodev.fetch2core.FetchObserver;
import com.tonyodev.fetch2core.Reason;
import com.ym.fetchdemo.DialogManager;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class FetchData {
private static final String TAG = "FetchData";
private static final String FETCH_NAMESPACE = "FetchData";
private static final long PROGRESS_REPORTING_INTERVAL = 100;
private Context mContext;
private Fetch mFetch;
private Request mRequest;
public DialogManager mDialogManager;
private DownloadCompletedListener listener;
public FetchData(Context context){
this.mContext = context;
mDialogManager = new DialogManager(mContext);
}
public void initFetch(){
final FetchConfiguration fetchConfiguration = new FetchConfiguration.Builder(mContext)
.setDownloadConcurrentLimit(4) // 并行下载数量
.setAutoRetryMaxAttempts(3) // 失败时,重试3次
.setNamespace(FETCH_NAMESPACE)
.setProgressReportingInterval(PROGRESS_REPORTING_INTERVAL)
.build();
mFetch = Fetch.Impl.getInstance(fetchConfiguration);
mFetch.addListener(fetchListener);
}
private final FetchListener fetchListener = new AbstractFetchListener() {
@Override
public void onAdded(@NonNull Download download) {
super.onAdded(download);
Log.d(TAG,"fetchListener: onAdded : file="+download.getFile());
}
@Override
public void onCancelled(@NonNull Download download) {
super.onCancelled(download);
Log.d(TAG,"fetchListener: onCancelled : file="+download.getFile());
}
@Override
public void onCompleted(@NonNull Download download) {
if (listener!=null){
listener.onCompleted(download.getFile());
Log.d(TAG,"fetchListener file="+download.getFile());
}
super.onCompleted(download);
}
@Override
public void onDeleted(@NonNull Download download) {
super.onDeleted(download);
Log.d(TAG,"fetchListener: onDeleted : file="+download.getFile());
}
@Override
public void onDownloadBlockUpdated(@NonNull Download download, @NonNull DownloadBlock downloadBlock, int totalBlocks) {
super.onDownloadBlockUpdated(download, downloadBlock, totalBlocks);
Log.d(TAG,"fetchListener: onDownloadBlockUpdated : file="+download.getFile());
}
@Override
public void onError(@NonNull Download download, @NonNull Error error, @Nullable Throwable throwable) {
Log.e(TAG,"fetchListener: onError: msg="+error.toString()+" filePath="+download.getFile());
super.onError(download, error, throwable);
}
@Override
public void onPaused(@NonNull Download download) {
super.onPaused(download);
Log.d(TAG,"fetchListener: onPaused : file="+download.getFile());
}
@Override
public void onProgress(@NonNull Download download, long etaInMilliSeconds, long downloadedBytesPerSecond) {
long logTime = 0;
if (SystemClock.uptimeMillis() - logTime > TimeUnit.SECONDS.toMillis(10)) {
Log.d(TAG, "downloading: "+download.getFile()+" :progress "+download.getProgress());
logTime = SystemClock.uptimeMillis();
}
ThreadPoolUtil.runMain(()->{
mDialogManager.updateViews(download);
});
super.onProgress(download, etaInMilliSeconds, downloadedBytesPerSecond);
Log.d(TAG,"fetchListener: onProgress : file="+download.getFile());
}
@Override
public void onQueued(@NonNull Download download, boolean waitingOnNetwork) {
super.onQueued(download, waitingOnNetwork);
Log.d(TAG,"fetchListener: onQueued : file="+download.getFile());
}
@Override
public void onRemoved(@NonNull Download download) {
super.onRemoved(download);
}
@Override
public void onResumed(@NonNull Download download) {
super.onResumed(download);
}
@Override
public void onStarted(@NonNull Download download, @NonNull List extends DownloadBlock> downloadBlocks, int totalBlocks) {
super.onStarted(download, downloadBlocks, totalBlocks);
}
@Override
public void onWaitingNetwork(@NonNull Download download) {
Log.d(TAG,"fetchListener onWaitingNetwork "+download.getFile());
super.onWaitingNetwork(download);
}
};
public void enqueueDownload(String tagUrl,String filePath){
Log.d(TAG,"enqueueDownload tagUrl="+tagUrl+" filePath="+filePath);
mRequest = new Request(tagUrl,filePath);
mFetch.attachFetchObserversForDownload(mRequest.getId(), new FetchObserver() {
@Override
public void onChanged(Download download, @NonNull Reason reason) {
if (reason == Reason.DOWNLOAD_ADDED){
Log.d(TAG,"enqueueDownload Reason.DOWNLOAD_ADDED");
}
}
});
}
public void closeFetch(){
mFetch.close();
Log.d(TAG,"closeFetch");
}
@NonNull
public static String getSaveDir(Context context) {
return context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + "/fetch";
}
public static String getNameFromUrl(final String url){
return Uri.parse(url).getLastPathSegment();
}
public void setDownloadCompletedListener(DownloadCompletedListener listener){
this.listener = listener;
}
public interface DownloadCompletedListener{
void onCompleted(String filePath);
}
}
安装
安装过程:
1、理论上先检测系统上是否装有该app,如果已安装,安装的前提是已下载的apk的versionCode>系统当前apk。
2、调用系统的PackageInstall安装,通过文件流传输apk,将apk数据写入到session中,最后才执行安装,并将安装结果广播。
package com.ym.fetchdemo.util;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.text.TextUtils;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class ApkInstallUtil {
private static final String TAG = "ApkInstallUtil";
public static final String ACTION_APK_INSTALL_SUCCESS = "action.apk_install_success";
public static boolean isAppInstall(Context context,String packageName){
if (TextUtils.isEmpty(packageName)){
return false;
}
try {
context.getPackageManager().getPackageInfo(packageName,0);
}catch (Exception e){
Log.e(TAG,"isAppInstall ",e);
return false;
}
return true;
}
public static int getVersionCode(Context context,String packageName){
try {
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(packageName,0);
if (packageInfo!=null){
Log.d(TAG,"getVersionCode versionCode="+packageInfo.versionCode);
return packageInfo.versionCode;
}
}catch (Exception e){
Log.d(TAG,"getVersionCode ",e);
return 0;
}
return 0;
}
public static boolean deleteFile(String filePath){
if (TextUtils.isEmpty(filePath)){
return false;
}
File file = new File(filePath);
if (file.exists() && file.isFile()){
if (file.delete()){
Log.d(TAG,"deleteFile filePath="+filePath+" delete success!");
return true;
}else {
Log.d(TAG,"deleteFile filePath="+filePath+" delete failure!");
return false;
}
}else {
Log.d(TAG,"deleteFile filePath="+filePath+" is not exists!");
return false;
}
}
public static boolean checkAndInstallApk(Context context, String filePath){
Log.d(TAG, "start install" + filePath);
boolean isSuccess = true;
File apkFile = new File(filePath);
if (!apkFile.exists()) {
isSuccess = false;
} else {
PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
sessionParams.setSize(apkFile.length());
int mSessionId = -1;
try {
mSessionId = packageInstaller.createSession(sessionParams);
} catch (IOException e) {
e.printStackTrace();
}
Log.d(TAG, "sessionId---->" + mSessionId);
if (mSessionId != -1) {
boolean copySuccess = onTransfersApkFile(context, filePath, mSessionId);
Log.d(TAG, "copySuccess---->" + copySuccess);
if (copySuccess) {
installAPP(context, mSessionId);
} else {
isSuccess = false;
}
}
}
return isSuccess;
}
public static boolean onTransfersApkFile(Context context, String apkFilePath, int mSessionId) {
InputStream in = null;
OutputStream out = null;
PackageInstaller.Session session = null;
boolean success = false;
try {
File apkFile = new File(apkFilePath);
session = context.getPackageManager().getPackageInstaller().openSession(mSessionId);
out = session.openWrite("base.apk", 0, apkFile.length());
in = new FileInputStream(apkFile);
int total = 0, c;
byte[] buffer = new byte[1024 * 1024];
while ((c = in.read(buffer)) != -1) {
total += c;
out.write(buffer, 0, c);
}
session.fsync(out);
//Log.d(TAG, "streamed " + total + " bytes");
success = true;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != session) {
session.close();
}
try {
if (null != out) {
out.close();
}
if (null != in) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return success;
}
public static void installAPP(Context context, int mSessionId) {
PackageInstaller.Session session = null;
try {
session = context.getPackageManager().getPackageInstaller().openSession(mSessionId);
Intent intent = new Intent();
intent.setAction(ACTION_APK_INSTALL_SUCCESS);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context,
1, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
Log.d(TAG, "installAPP installing...");
session.commit(pendingIntent.getIntentSender());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != session) {
session.close();
}
}
}
}
安装成功状态通知
当apk安装成功后,广播通知;
安装失败,也可广播通知,然后执行二次安装。
package com.ym.fetchdemo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInstaller;
import android.util.Log;
import com.ym.fetchdemo.util.ApkInstallUtil;
import com.ym.fetchdemo.util.FetchData;
import com.ym.fetchdemo.util.ThreadPoolUtil;
public class ApkInstallManager implements FetchData.DownloadCompletedListener{
private static final String TAG = "ApkInstallManager";
private static volatile ApkInstallManager sInstance;
private Context mContext;
private InstallResultReceiver mInstallResultReceiver;
private FetchData mFetchData;
private String apkFilePath;
public static ApkInstallManager getInstance(Context context){
if (sInstance == null){
synchronized (ApkInstallManager.class){
if (sInstance == null){
sInstance = new ApkInstallManager();
sInstance.mContext = context;
sInstance.mFetchData = new FetchData(context);
}
}
}
return sInstance;
}
private ApkInstallManager(){
}
public void registerReceiver(){
Log.d(TAG,"registerReceiver...");
if (mInstallResultReceiver != null){
mContext.unregisterReceiver(mInstallResultReceiver);
}
IntentFilter installIntentFilter = new IntentFilter(ApkInstallUtil.ACTION_APK_INSTALL_SUCCESS);
mInstallResultReceiver = new InstallResultReceiver();
mContext.registerReceiver(mInstallResultReceiver,installIntentFilter);
mFetchData.initFetch();
mFetchData.setDownloadCompletedListener(this);
ThreadPoolUtil.runMain(()->{
mFetchData.mDialogManager.showDownloadDialog();
});
// 下载apk
ThreadPoolUtil.run(()->{
String apkUrl = "https://raw.githubusercontent.com/jenly1314/AppUpdater/master/app/release/app-release.apk";
String filePath = FetchData.getSaveDir(mContext)+"/app/"+FetchData.getNameFromUrl(apkUrl);
mFetchData.enqueueDownload(apkUrl,filePath);
});
}
@Override
public void onCompleted(String filePath) {
apkFilePath = filePath;
ThreadPoolUtil.runMain(()->{
mFetchData.mDialogManager.showInstallDialog();
});
ThreadPoolUtil.run(()->{
ApkInstallUtil.checkAndInstallApk(mContext,filePath);
});
}
private class InstallResultReceiver extends BroadcastReceiver {
private final String TAG = "InstallResultReceiver";
private String path;
private int versionCode;
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null){
String action = intent.getAction();
Log.d(TAG,"Apk install broadcast action: "+action);
final int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS,
PackageInstaller.STATUS_FAILURE);
if (status == PackageInstaller.STATUS_SUCCESS){
Log.d(TAG,"Apk install success! path: "+path);
mFetchData.mDialogManager.dismissDialog();
Log.d(TAG,"dismissDialog...");
mFetchData.closeFetch();
// 删除apk文件
ThreadPoolUtil.run(()->{
ApkInstallUtil.deleteFile(apkFilePath);
});
}else {
String msg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
Log.e(TAG, "Apk install failure! status_massage: " + msg);
// TODO 二次安装?
}
}
}
}
}
其他
Dialog UI
Dialog bg
Progress bar
线程池
package com.ym.fetchdemo.util;
import android.os.Handler;
import android.os.Looper;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolUtil {
private static final int CORE_POOL_SIZE = 1;
private static final int MAX_POOL_SIZE = 10;
private static final long KEEP_ALIVE_TIME = 30;
private static final Handler mHandler = new Handler(Looper.getMainLooper());
private static ThreadPoolExecutor sDiskExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>());
public static void run(Runnable runnable){
sDiskExecutor.execute(runnable);
}
public static void runMain(Runnable runnable){
if (runnable!= null){
mHandler.post(runnable);
}
}
public static void cancelMain(Runnable runnable){
if (runnable!=null){
mHandler.removeCallbacks(runnable);
}
}
}
小结
文件的下载封装成一个类或者包,可以重复使用,同时处理一些边界case。
监听下载的状态,下载成功、失败、正在下载、网络状态监听、恢复下载等,并可视化UI。



