在做需求开发时,少不了需要从服务器上直接下载东西,比如图片、视频、apk等。下载后的东西,还需要做起一些其他操作。
对于下载,可以采用WorkManager,也可以采用现有的包,比如Fetch。下载后的apk,针对Android 9.0版本,采用静默安装。
注意:系统应用才有权限静默安装
参考链接:
Fetch
build.gradle中引入:
implementation 'androidx.tonyodev.fetch2:xfetch2:3.1.6'
AndroidManifest.xml中配置:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
// 以上三种就OK
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
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的状态,例如下载时使用进度条查看百分比
/**
* 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;
/**
* Author:Yangmiao
* Desc:
* Time:2022/7/4 10:09
*/
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);
}
/**
* 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);
}
};
/**
* 下载
* @param tagUrl 下载链接
* @param filePath 下载存放的路径
*/
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<Download>() {
@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;
/**
* Author:Yangmiao
* Desc:
* Time:2022/7/4 11:02
*/
public class ApkInstallUtil {
private static final String TAG = "ApkInstallUtil";
public static final String ACTION_APK_INSTALL_SUCCESS = "action.apk_install_success";
/**
* 检测系统中是否安装了该apk
* @param context
* @param packageName App包名
* @return
*/
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;
}
/**
* 获取App的versionCode
* @param context
* @param packageName
* @return
*/
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;
}
/**
* 删除文件
* @param filePath
* @return
*/
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;
}
}
/**
* 安装apk
* @param context
* @param filePath
* @return
*/
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;
}
/**
* 文件流传输apk,向session内传输数据
* @param context
* @param apkFilePath
* @param mSessionId
* @return
*/
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;
}
/**
* 执行安装并接受通知结果
* @param context
* @param mSessionId
*/
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;
/**
* Author:Yangmiao
* Desc:
* Time:2022/7/4 11:36
*/
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);
});
}
/**
* 安装结果广播receiver
*/
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 二次安装?
}
}
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="325dp"
android:layout_height="300dp"
android:background="@drawable/dialog_bg"
android:orientation="vertical">
<ImageView
android:id="@+id/app_video_image"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_centerHorizontal="true"
android:src="@mipmap/ic_launcher"
android:layout_marginTop="40dp"/>
<TextView
android:id="@+id/app_video_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/app_video_image"
android:layout_centerHorizontal="true"
android:layout_marginTop="20dp"
android:text="Launcher"
android:textColor="@color/black"
android:textSize="24sp"
android:textStyle="bold"/>
<ProgressBar
android:id="@+id/app_download_progress_bar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="10dp"
android:layout_centerHorizontal="true"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:layout_marginTop="20dp"
android:layout_below="@+id/app_video_name"
android:max="100"
android:progress="10"
android:progressDrawable="@drawable/progress_bar"
android:visibility="invisible"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/app_download_progress_bar"
android:layout_centerHorizontal="true"
android:layout_marginTop="20dp">
<TextView
android:id="@+id/app_download_percentage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="18sp"
android:textColor="@color/black"
android:layout_marginStart="10dp"
android:gravity="center_vertical"
android:visibility="invisible"/>
</LinearLayout>
<ProgressBar
android:id="@+id/app_install_progress_bar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="10dp"
android:layout_below="@+id/app_video_name"
android:layout_centerHorizontal="true"
android:layout_marginStart="15dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="15dp"
android:max="100"
android:progress="5"
android:progressDrawable="@drawable/progress_bar"
android:visibility="invisible"/>
<TextView
android:id="@+id/app_install_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="安装进行中..."
android:textColor="@color/black"
android:textSize="18sp"
android:layout_below="@+id/app_install_progress_bar"
android:layout_centerHorizontal="true"
android:visibility="invisible"/>
</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="#FFFFFF" />
<corners android:radius="15dp" />
</shape>
</item>
</selector>
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="5dip" />
<gradient
android:startColor="#FFFFFF"
android:centerColor="#FFFFFF"
android:centerY="0.75"
android:endColor="#FFFFFF"
android:angle="270"
/>
</shape>
</item>
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<corners android:radius="5dip" />
<gradient
android:startColor="#80ffb600"
android:centerColor="#80ffb600"
android:centerY="0.75"
android:endColor="#80ffb600"
android:angle="270"
/>
</shape>
</clip>
</item>
<item
android:id="@android:id/progress"
>
<clip>
<shape>
<corners
android:radius="5dip" />
<gradient
android:startColor="#2196f3"
android:endColor="#2196f3"
android:angle="270" />
</shape>
</clip>
</item>
</layer-list>
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;
/**
* Author:Yangmiao
* Desc:
* Time:2022/7/4 11:39
*/
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。