转载 :https://www.jianshu.com/p/a6cad97ea54f
相信很多应用都是采用内部下载的方式,这样的体验肯定比跳转到浏览器好得多!而应用商店审核周期长,无法实时更新最新应用!所以内部下载更新就显得尤为重要!
1.要美观好看,给用户实时的反馈下载情况:
界面体现为下载百分比%,下载速度 kb/s,圆环进度
2.下载完成后要自动安装:
Android6.0,需要动态申请权限,读取写入。
Android7.0,需要通过fileprovider的方式创建Uri
Android8.0,需要申请【安装未知来源应用权限】
针对第一个问题,我们采用自定义View来完成,可定制化高,样式想怎样改怎样改。而第二个问题就需要我们队权限的申请和对路径创建方式的注意了。
先来一个效果图:
下载安装apk.gif
这个其实就是简单的一个Dialog了,中间的狮子图片是应用LOGO,下面的正在下载就是一个文字描述,难点主要是中间的进度圆圈和圆圈点上的行星进度。
1.1新建一个Dialog弹出框:DownloadCircleDialog
public class DownloadCircleDialog extends Dialog {public DownloadCircleDialog(Context context) {super(context, R.style.Theme_Ios_Dialog);}DownloadCircleView circleView;TextView tvMsg;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.download_circle_dialog_layout);this.setCancelable(false);//设置点击弹出框外部,无法取消对话框circleView = findViewById(R.id.circle_view);tvMsg = findViewById(R.id.tv_msg);}public void setProgress(int progress) {circleView.setProgress(progress);circleView.postInvalidate();}public void setMsg(String msg){tvMsg.setText(msg);}
}
在style.xml中写入样式:Theme.Ios.Dialog
<!-- IOSDialog --><style name="Theme.Ios.Dialog" parent="@android:style/Theme.Dialog"><!-- Dialog的windowFrame框为无 --><!-- <item name="android:windowFrame">@null</item> --><!-- 边框 --><item name="android:windowIsFloating">true</item><!-- 是否浮现在activity之上 --><item name="android:windowIsTranslucent">true</item><!-- 半透明 --><item name="android:windowNoTitle">true</item><!-- 设置dialog的背景 --><item name="android:windowBackground">@android:color/transparent</item><!-- 背景是否模糊显示 --><item name="android:backgroundDimEnabled">true</item><!-- 模糊 --><item name="android:textColorPrimaryInverse">@android:color/black</item></style>
layout中的布局文件:download_circle_dialog_layout
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/layout_notice"android:background="@color/transparent"android:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"android:orientation="vertical"><com.yy.trade.customview.DownloadCircleViewandroid:id="@+id/circle_view"android:layout_centerInParent="true"android:layout_width="180dp"android:layout_height="180dp"/><ImageViewandroid:layout_centerInParent="true"android:src="@mipmap/icon_logo"android:layout_width="64dp"android:layout_height="64dp"/><TextViewandroid:id="@+id/tv_msg"android:layout_centerHorizontal="true"android:layout_below="@+id/circle_view"android:text="正在下载..."android:layout_width="wrap_content"android:layout_height="wrap_content"/>
</RelativeLayout>
1.2中间下载进度圆圈:DownloadCircleView
public class DownloadCircleView extends View {Paint mBgPaint;Paint mStepPaint;Paint mTxtCirclePaint;Paint mTxtPaint;int outsideRadius=160;int progressWidth =8;float progresTtextSize = 24;Context context;public DownloadCircleView(Context context) {super(context);}public DownloadCircleView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);init(context);}public DownloadCircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int width;int height;int size = MeasureSpec.getSize(widthMeasureSpec);int mode = MeasureSpec.getMode(widthMeasureSpec);if (mode == MeasureSpec.EXACTLY) {width = size;} else {width = (int) ((2 * outsideRadius) + progressWidth);}size = MeasureSpec.getSize(heightMeasureSpec);mode = MeasureSpec.getMode(heightMeasureSpec);if (mode == MeasureSpec.EXACTLY) {height = size;} else {height = (int) ((2 * outsideRadius) + progressWidth);}setMeasuredDimension(width, height);}private void init(Context context) {this.context = context;mBgPaint = new Paint();mBgPaint.setStrokeWidth(8);mBgPaint.setColor(Color.GRAY);this.mBgPaint.setAntiAlias(true); //消除锯齿this.mBgPaint.setStyle(Paint.Style.STROKE); //绘制空心圆mStepPaint = new Paint();mStepPaint.setStrokeWidth(8);mStepPaint.setColor(Color.parseColor("#3B4463"));this.mStepPaint.setAntiAlias(true); //消除锯齿this.mStepPaint.setStyle(Paint.Style.STROKE); //绘制空心圆mTxtCirclePaint = new Paint();mTxtCirclePaint.setColor(Color.parseColor("#3B4463"));this.mTxtCirclePaint.setAntiAlias(true); //消除锯齿this.mTxtCirclePaint.setStyle(Paint.Style.FILL); //绘制实心圆mTxtPaint = new Paint();mTxtPaint.setTextSize(progresTtextSize);mTxtPaint.setColor(Color.WHITE);this.mTxtPaint.setAntiAlias(true); //消除锯齿this.mTxtPaint.setStyle(Paint.Style.FILL); //绘制实心圆}float maxProgress=100f;float progress =0f;public void setProgress(float progress) {this.progress = progress;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);//灰色圆圈int circlePoint = getWidth() / 2;canvas.drawCircle(circlePoint, circlePoint, outsideRadius, mBgPaint); //画出圆//进度RectF oval = new RectF();oval.left=circlePoint - outsideRadius;oval.top=circlePoint - outsideRadius;oval.right=circlePoint + outsideRadius;oval.bottom=circlePoint + outsideRadius;float range = 360 * (progress / maxProgress);canvas.drawArc(oval, -90, range, false, mStepPaint); //根据进度画圆弧//轨道圆和文字double x1 = circlePoint + outsideRadius * Math.cos((range-90) * 3.14 / 180);double y1 = circlePoint + outsideRadius * Math.sin((range-90) * 3.14 / 180);canvas.drawCircle((float) x1, (float) y1, outsideRadius/4, mTxtCirclePaint);String txt = (int) progress + "%";float strwid = mTxtPaint.measureText(txt);//直接返回参数字符串所占用的宽度canvas.drawText(txt,(float) x1-strwid/2, (float) y1+progresTtextSize/2,mTxtPaint);}
}
这样,下载样式基本就完成了,每次通过方法setProgress和setMsg就可以去设置下载的进度和速度了!
下面说说下载:采用 okhttp来下载apk文件,通过 ProgressManager来监听进度,通过 easypermissions简化动态申请权限
2.1首先我们写个下载工具类:DownloadUtils
public class DownloadUtils {private static DownloadUtils instance;private OkHttpClient okHttpClient;private Handler mHandler; //所有监听器在 Handler 中被执行,所以可以保证所有监听器在主线程中被执行public static DownloadUtils getInstance() {if (instance == null) instance = new DownloadUtils();return instance;}private DownloadUtils() {this.mHandler = new Handler(Looper.getMainLooper());OkHttpClient.Builder builder = new OkHttpClient.Builder();okHttpClient = ProgressManager.getInstance().with(builder).build();}public interface OnDownloadListener{/*** 下载成功*/void onDownloadSuccess();/*** @param progress 下载进度*/void onDownloading(ProgressInfo progress);/*** 下载失败*/void onDownloadFailed();}/*** @param url 下载连接* @param saveDir 储存下载文件的SDCard目录* @param listener 下载监听*/public void download(final String url, final String saveDir, final String saveName, final OnDownloadListener listener) {Request request = new Request.Builder().url(url).build();okHttpClient.newCall(request).enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) {// 下载失败listener.onDownloadFailed();}@Overridepublic void onResponse(Call call, Response response) throws IOException {// Okhttp/Retofit 下载监听InputStream is = null;byte[] buf = new byte[2048];int len = 0;FileOutputStream fos = null;// 储存下载文件的目录try {is = response.body().byteStream();File file = new File(saveDir, saveName);if (!file.getParentFile().exists()) file.getParentFile().mkdirs();fos = new FileOutputStream(file);while ((len = is.read(buf)) != -1) {fos.write(buf, 0, len);}fos.flush();mHandler.post(new Runnable() {@Overridepublic void run() {// 下载完成listener.onDownloadSuccess();}});} catch (Exception e) {Log.e("下载异常", e.getMessage());listener.onDownloadFailed();} finally {try {if (is != null) is.close();} catch (IOException e) {}try {if (fos != null) fos.close();} catch (IOException e) {}}}});ProgressManager.getInstance().addResponseListener(url, new ProgressListener() {@Overridepublic void onProgress(ProgressInfo progressInfo) {listener.onDownloading(progressInfo);}@Overridepublic void onError(long l, Exception e) {listener.onDownloadFailed();}});}}
上面通过ProgressManager.getInstance().with(builder).build();
创建的okHttpClient
也就相当于把ProgressManager加入到了OkHttp中,这样ProgressManager监听才会有效!
然后我们在需要下载apk的页面中加入:
/*** 有新版本*/final static int PG_WRITE = 0X0008;@AfterPermissionGranted(PG_WRITE)private void showNewVersion() {String[] perms = {READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE};if (!EasyPermissions.hasPermissions(this, perms)) {EasyPermissions.requestPermissions(MainActivity.this, getString(R.string.need_permiss), PG_WRITE, perms);return;}new ApkUpdateDialog(MainActivity.this, "软件更新", "下载QQ测试", new ApkUpdateDialog.OnOkListener() {@Overridepublic void onOkClick() {String down_url = "https://qd.myapp.com/myapp/qqteam/AndroidQQ/mobileqq_android.apk";downloadApk(MainActivity.this, down_url);}}).show();}public void downloadApk(final Activity context, String down_url) {dialog.show();DownloadUtils.getInstance().download(down_url, SdUtils.getDownloadPath(), "QQ.apk", new DownloadUtils.OnDownloadListener() {@Overridepublic void onDownloadSuccess() {dialog.dismiss();L.i("恭喜你下载成功,开始安装!==" + SdUtils.getDownloadPath() + "QQ.apk");showToast("恭喜你下载成功,开始安装!");String successDownloadApkPath = SdUtils.getDownloadPath() + "QQ.apk";installApkO(MainActivity.this, successDownloadApkPath);}@Overridepublic void onDownloading(ProgressInfo progressInfo) {dialog.setProgress(progressInfo.getPercent());boolean finish = progressInfo.isFinish();if (!finish) {long speed = progressInfo.getSpeed();dialog.setMsg("(" + (speed > 0 ? FormatUtils.formatSize(context, speed) : speed) + "/s)正在下载...");} else {dialog.setMsg("下载完成!");}}@Overridepublic void onDownloadFailed() {dialog.dismiss();showToast("下载失败!");}});}/*** 兼容8.0安装位置来源的权限*/private void installApkO(Context context, String downloadApkPath) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {//是否有安装位置来源的权限boolean haveInstallPermission = getPackageManager().canRequestPackageInstalls();if (haveInstallPermission) {L.i("8.0手机已经拥有安装未知来源应用的权限,直接安装!");AppUtils.installApk(context, downloadApkPath);} else {new CakeResolveDialog(context, "安装应用需要打开安装未知来源应用权限,请去设置中开启权限", new CakeResolveDialog.OnOkListener() {@Overridepublic void onOkClick() {Uri packageUri = Uri.parse("package:"+ AppUtils.getAppPackageName());Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,packageUri);}}).show();}} else {AppUtils.installApk(context, downloadApkPath);}}@Overridepublic void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {super.onRequestPermissionsResult(requestCode, permissions, grantResults);EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);}@Overridepublic void onPermissionsGranted(int requestCode, List<String> perms) {T.showShort("已经获得此权限!");}@Overridepublic void onPermissionsDenied(int requestCode, List<String> perms) {L.i("申请的权限被拒绝了,提醒用户去设置");if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {new AppSettingsDialog.Builder(this).setTitle(R.string.need_open_relevant_power).setRationale(R.string.some_permission_must_need).build().show();}}@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) {super.onActivityResult(requestCode, resultCode, data);L.i("回调。。。requestCode==" + requestCode + " resultCode==" + resultCode);if (requestCode == AppSettingsDialog.DEFAULT_SETTINGS_REQ_CODE) {Toast.makeText(this, R.string.some_permission_must_need, Toast.LENGTH_SHORT).show();}if (requestCode == 10086) {L.i("设置了安装未知应用后的回调。。。");String successDownloadApkPath = SdUtils.getDownloadPath() + "QQ.apk";installApkO(MainActivity.this, successDownloadApkPath);}}
上面代码第一段showNewVersion就是对读写权限的申请,downloadApk下载进度的监听,下载完成通过installApkO来安装,installApkO中判断如果没有安装位置来源的权限就跳转到设置开启安装位置来源权限的页面,设置完成后回到这个页面继续安装!
上面的AppUtils.installApk是我写的一个工具方法,
public static void installApk(Context context,String downloadApk) {Intent intent = new Intent(Intent.ACTION_VIEW);File file = new File(downloadApk);L.i("安装路径=="+downloadApk);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {Uri apkUri = FileProvider.getUriForFile(context, AppUtils.getAppPackageName()+".fileprovider", file);intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);intent.setDataAndType(apkUri, "application/vnd.android.package-archive");} else {intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);Uri uri = Uri.fromFile(file);intent.setDataAndType(uri, "application/vnd.android.package-archive");}context.startActivity(intent);}
判断如果>=7.0就通过fileprovider来创建Uri,避免安装出现解析包异常!6.0的读写权限通过showNewVersion()方法进行了申明,8.0的安装未知来源应用权限在installApkO进行了判断申请,从而使安装APK兼容了6,7,8!9.0的机子还没用过,不过如果没改动,应该也可以安装!
这样一个apk从开始下载,进度显示到安装就完成了!说起来就是一个apk的下载安装,但是其实代码量和坑还是挺多的:
坑一:最开始没有使用ProgressManager来进度监听,而是在download方法的写文件中监听下载进度:
public void download(final String url, final String saveDir, final OnDownloadListener listener) {Request request = new Request.Builder().url(url).build();okHttpClient.newCall(request).enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) {// 下载失败listener.onDownloadFailed();}@Overridepublic void onResponse(Call call, Response response) throws IOException {InputStream is = null;byte[] buf = new byte[2048];int len = 0;FileOutputStream fos = null;// 储存下载文件的目录String savePath = isExistDir(saveDir);try {is = response.body().byteStream();long total = response.body().contentLength();File file = new File(savePath, getNameFromUrl(url));fos = new FileOutputStream(file);long sum = 0;while ((len = is.read(buf)) != -1) {fos.write(buf, 0, len);sum += len;int progress = (int) (sum * 1.0f / total * 100);// 下载中listener.onDownloading(progress);}fos.flush();// 下载完成listener.onDownloadSuccess();} catch (Exception e) {listener.onDownloadFailed();} finally {try {if (is != null)is.close();} catch (IOException e) {}try {if (fos != null)fos.close();} catch (IOException e) {}}}});}
这样监听到的下载进度并不是真的下载进度,而是下载文件后写入到手机的速度,体现到界面就是最开始下载进度是0%一直不动,然后2秒钟就从0%转圈到100%,就下载完成了,给用户感觉一点都不真实!
坑二:下载完成后解析包错误:
a.主要原因就是没有使用Uri apkUri = FileProvider.getUriForFile(context, AppUtils.getAppPackageName()+".fileprovider", file);
的方式来创建Uri 安装,
b.还有就是因为文件名字不正确,最开始我的download方法中没有saveName方法,而是通过下载地址截取最后的“/”来写入文件名的,但是有的下载地址并不是以apk结尾,从而导致解析包错误!
c.还有就是根本没有这个文件路径,从而导致写错误,所以在download方法中写入本地文件前我加入了如果没有文件路径就先创建当前路径
File file = new File(saveDir, saveName);
if (!file.getParentFile().exists()) file.getParentFile().mkdirs();
坑三:下载出错:CertPathValidatorException: Trust anchor for certification path not found
,(上面的代码没有加入,因为每个人的OkHttpClient都不同,我是写了个工具类来创建的OkHttpClient,所以工具类中加入了进度读取和跳过SSL验证的,由于自私原因,大家自己加吧。)
相信有的应用是放在自己的服务器的,而又有https,但很多都是没有证书的,导致下载不了!所以我们就需要Okhttp绕过证书验证,参考:
https://blog.csdn.net/O0mm0O/article/details/76686917
坑四:无法安装:
Android8.0需要安装权限:<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
,同时7.0以上还需要安装未知来源的权限,不然也无法安装,可以参考:
https://www.jianshu.com/p/a6209440a518
坑五:无法安装:由于配置xml中的provider_paths.xml只写了
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android"><external-pathname="images"path="test/"/>
</paths>
导致无法读取路径而无法安装,因为我们是下载到Download的所以还要加入:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android"><external-pathname="images"path="test/"/><external-pathname="download"path="Download/"/>
</paths>
这样才算完整!关于fileprovider路径介绍:
https://blog.csdn.net/leilifengxingmw/article/details/57405908
坑六:安装完成后闪退,或安装完成后点击打开闪退
在大部分手机上没有问题,但在Vivo X9上居然安装完成后闪退了,我觉得这应该是应用已经死了,所有安装完成后立即启动有问题,而其他手机就没有问题,我觉得还是Vivo手机的厂商定制问题!所以解决办法就是安装的时候启动一个新的任务栈来安装:
public static void installApk(Context context,String downloadApk) {Intent intent = new Intent(Intent.ACTION_VIEW);File file = new File(downloadApk);L.i("安装路径=="+downloadApk);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {Uri apkUri = FileProvider.getUriForFile(context, AppUtils.getAppPackageName()+".fileprovider", file);intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);intent.setDataAndType(apkUri, "application/vnd.android.package-archive");} else {intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);Uri uri = Uri.fromFile(file);intent.setDataAndType(uri, "application/vnd.android.package-archive");}context.startActivity(intent);}
其中:intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
就是关键啦!
下个应用如果还需要下载,我就把上面的代码复制进去,免得每次都要去找写了下载安装APK代码的项目,然后一部分一部分去查找复制!这么多代码记肯定是记不住的,这辈子都不可能记住的,所以写这里方便下次Copy!