原文地址:http://www.reibang.com/p/a6cad97ea54f
相信很多應用都是采用內部下載的方式粥脚,這樣的體驗肯定比跳轉到瀏覽器好得多亭螟!而應用商店審核周期長秋茫,無法實時更新最新應用夯膀!所以內部下載更新就顯得尤為重要鲸匿!
1.要美觀好看逗抑,給用戶實時的反饋下載情況:
界面體現(xiàn)為下載百分比%,下載速度 kb/s夹攒,圓環(huán)進度
2.下載完成后要自動安裝:
Android6.0蜘醋,需要動態(tài)申請權限,讀取寫入咏尝。
Android7.0压语,需要通過fileprovider的方式創(chuàng)建Uri
Android8.0,需要申請【安裝未知來源應用權限】
針對第一個問題编检,我們采用自定義View來完成胎食,可定制化高,樣式想怎樣改怎樣改允懂。而第二個問題就需要我們隊權限的申請和對路徑創(chuàng)建方式的注意了厕怜。
先來一個效果圖:
這個其實就是簡單的一個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;
@Override
protected 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);
}
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>
<!-- 是否浮現(xiàn)在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"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/layout_notice"
android:background="@color/transparent"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.qiqia.duosheng.custom.DownloadCircleView
android:id="@+id/circle_view"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_centerInParent="true"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_logo"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerInParent="true"
android:layout_marginBottom="24dp"
android:src="@mipmap/logo"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/iv_logo"
android:layout_centerHorizontal="true"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="正在下載..."
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv_logo" />
</android.support.constraint.ConstraintLayout>
1.2中間下載進度圓圈:DownloadCircleView
public class DownloadCircleView extends View {
Paint mBgPaint;
Paint mStepPaint;
Paint mTxtCirclePaint;
Paint mTxtPaint;
int outsideRadius=DpPxUtils.dp2px(100);
int progressWidth =DpPxUtils.dp2px(2);
float progressTextSize = DpPxUtils.dp2px(12);
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);
}
@Override
protected 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) {
int progressColor = Color.parseColor("#FF5836");//進度球顏色
this.context = context;
//灰色背景圓環(huán)
mBgPaint = new Paint();
mBgPaint.setStrokeWidth(progressWidth);
mBgPaint.setColor(Color.GRAY);
this.mBgPaint.setAntiAlias(true);
this.mBgPaint.setStyle(Paint.Style.STROKE); //繪制空心圓
//進度圓環(huán)
mStepPaint = new Paint();
mStepPaint.setStrokeWidth(progressWidth);
mStepPaint.setColor(progressColor);
this.mStepPaint.setAntiAlias(true);
this.mStepPaint.setStyle(Paint.Style.STROKE); //繪制空心圓
//進度衛(wèi)星球
mTxtCirclePaint = new Paint();
mTxtCirclePaint.setColor(progressColor);
this.mTxtCirclePaint.setAntiAlias(true);
this.mTxtCirclePaint.setStyle(Paint.Style.FILL); //繪制實心圓
//進度文字5%
mTxtPaint = new Paint();
mTxtPaint.setTextSize(progressTextSize);
mTxtPaint.setColor(Color.WHITE);
this.mTxtPaint.setAntiAlias(true);
}
float maxProgress=100f;
float progress =0f;
public void setProgress(float progress) {
this.progress = progress;
postInvalidate();
}
@Override
protected 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); //根據(jù)進度畫圓弧
//軌道圓和文字
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, progressTextSize*1.3f, mTxtCirclePaint);
String txt = (int) progress + "%";
float strwid = mTxtPaint.measureText(txt);//直接返回參數(shù)字符串所占用的寬度
canvas.drawText(txt,(float) x1-strwid/2, (float) y1+progressTextSize/2-progressWidth/2,mTxtPaint);
}
}
這樣躁锡,下載樣式基本就完成了午绳,每次通過方法setProgress和setMsg就可以去設置下載的進度和速度了置侍!
下面說說下載:采用 okhttp來下載apk文件,通過 ProgressManager來監(jiān)聽進度拦焚,通過 AndPermission簡化動態(tài)申請權限
2.1首先我們寫個下載工具類:DownloadUtils
import android.os.Handler;
import android.os.Looper;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import me.jessyan.progressmanager.ProgressListener;
import me.jessyan.progressmanager.ProgressManager;
import me.jessyan.progressmanager.body.ProgressInfo;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class DownloadUtils {
private static DownloadUtils instance;
private OkHttpClient okHttpClient;
private Handler mHandler; //所有監(jiān)聽器在 Handler 中被執(zhí)行,所以可以保證所有監(jiān)聽器在主線程中被執(zhí)行
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 下載監(jiān)聽
*/
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() {
@Override
public void onFailure(Call call, IOException e) {
// 下載失敗
listener.onDownloadFailed();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
// Okhttp/Retofit 下載監(jiān)聽
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() {
@Override
public 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() {
@Override
public void onProgress(ProgressInfo progressInfo) {
listener.onDownloading(progressInfo);
}
@Override
public void onError(long l, Exception e) {
listener.onDownloadFailed();
}
});
}
}
上面通過ProgressManager.getInstance().with(builder).build();
創(chuàng)建的okHttpClient
也就相當于把ProgressManager加入到了OkHttp中蜡坊,這樣ProgressManager監(jiān)聽才會有效!
3.使用
- a .首先我們需要加入安裝位置來源權限到AndroidManifest.xml中:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
- b. 然后我們在需要下載apk的頁面中加入:
DownloadCircleDialog dialogProgress;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dialogProgress = new DownloadCircleDialog(this);
showNewVersion();
}
//1.權限申請赎败,通過后開始下載
private void showNewVersion() {
AndPermission.with(this)
.runtime()
.permission(Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE)
.onGranted(data -> {
L.e("以獲得權限" + data.toString());
new AlertDialog.Builder(this).setTitle("軟件更新").setMessage("發(fā)現(xiàn)新版本")
.setPositiveButton("確定", (dialog, which) -> {
String down_url = "https://qd.myapp.com/myapp/qqteam/AndroidQQ/mobileqq_android.apk";
downloadApk(MainActivity.this, down_url);
})
.setNegativeButton("取消",null).show();
})
.onDenied(data -> L.e("未獲得權限" + data.toString())).start();
}
//2.開始下載apk
public void downloadApk(final Activity context, String down_url) {
dialogProgress.show();
DownloadUtils.getInstance().download(down_url, SdUtils.getDownloadPath(), "QQ.apk", new DownloadUtils.OnDownloadListener() {
@Override
public void onDownloadSuccess() {
dialogProgress.dismiss();
L.i("恭喜你下載成功秕衙,開始安裝!==" + SdUtils.getDownloadPath() + "QQ.apk");
ToastUtil.showShort("恭喜你下載成功僵刮,開始安裝据忘!");
String successDownloadApkPath = SdUtils.getDownloadPath() + "QQ.apk";
installApkO(MainActivity.this, successDownloadApkPath);
}
@Override
public void onDownloading(ProgressInfo progressInfo) {
dialogProgress.setProgress(progressInfo.getPercent());
boolean finish = progressInfo.isFinish();
if (!finish) {
long speed = progressInfo.getSpeed();
dialogProgress.setMsg("(" + (speed > 0 ? FormatUtils.formatSize(context, speed) : speed) + "/s)正在下載...");
} else {
dialogProgress.setMsg("下載完成!");
}
}
@Override
public void onDownloadFailed() {
dialogProgress.dismiss();
ToastUtil.showShort("下載失敻愀狻勇吊!");
}
});
}
// 3.下載成功,開始安裝,兼容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手機已經(jīng)擁有安裝未知來源應用的權限窍仰,直接安裝汉规!");
AppUtils.installApk(context, downloadApkPath);
} else {
new CakeResolveDialog(context, "安裝應用需要打開安裝未知來源應用權限,請去設置中開啟權限", new CakeResolveDialog.OnOkListener() {
@Override
public void onOkClick() {
Uri packageUri = Uri.parse("package:"+ AppUtils.getAppPackageName());
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,packageUri);
startActivityForResult(intent,10086);
}
}).show();
}
} else {
AppUtils.installApk(context, downloadApkPath);
}
}
//4.開啟了安裝未知來源應用權限后驹吮,再次進行步驟3的安裝针史。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 10086) {
L.i("設置了安裝未知應用后的回調晶伦。。啄枕。");
String successDownloadApkPath = SdUtils.getDownloadPath() + "QQ.apk";
installApkO(MainActivity.this, successDownloadApkPath);
}
}
上面代碼第一段showNewVersion就是對讀寫權限的申請婚陪,downloadApk下載進度的監(jiān)聽,下載完成通過installApkO來安裝频祝,installApkO中判斷如果沒有安裝位置來源的權限就跳轉到設置開啟安裝位置來源權限的頁面近忙,設置完成后回到這個頁面繼續(xù)安裝!
上面的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來創(chuàng)建Uri,避免安裝出現(xiàn)解析包異常及舍!6.0的讀寫權限通過showNewVersion()方法進行了申明,8.0的安裝未知來源應用權限在installApkO進行了判斷申請窟绷,從而使安裝APK兼容了6锯玛,7,8兼蜈!9.0的機子還沒用過攘残,不過如果沒改動,應該也可以安裝为狸!
這樣一個apk從開始下載歼郭,進度顯示到安裝就完成了!說起來就是一個apk的下載安裝辐棒,但是其實代碼量和坑還是挺多的:
坑一:最開始沒有使用ProgressManager來進度監(jiān)聽病曾,而是在download方法的寫文件中監(jiān)聽下載進度:
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() {
@Override
public void onFailure(Call call, IOException e) {
// 下載失敗
listener.onDownloadFailed();
}
@Override
public 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) {
}
}
}
});
}
這樣監(jiān)聽到的下載進度并不是真的下載進度,而是下載文件后寫入到手機的速度漾根,體現(xiàn)到界面就是最開始下載進度是0%一直不動泰涂,然后2秒鐘就從0%轉圈到100%,就下載完成了辐怕,給用戶感覺一點都不真實逼蒙!
坑二:下載完成后解析包錯誤:
a.主要原因就是沒有使用Uri apkUri = FileProvider.getUriForFile(context, AppUtils.getAppPackageName()+".fileprovider", file);
的方式來創(chuàng)建Uri 安裝,
b.還有就是因為文件名字不正確寄疏,最開始我的download方法中沒有saveName方法是牢,而是通過下載地址截取最后的“/”來寫入文件名的,但是有的下載地址并不是以apk結尾陕截,從而導致解析包錯誤驳棱!
c.還有就是根本沒有這個文件路徑,從而導致寫錯誤艘策,所以在download方法中寫入本地文件前我加入了如果沒有文件路徑就先創(chuàng)建當前路徑
File file = new File(saveDir, saveName);
if (!file.getParentFile().exists()) file.getParentFile().mkdirs();
坑三:下載出錯:CertPathValidatorException: Trust anchor for certification path not found
蹈胡,(上面的代碼沒有加入,因為每個人的OkHttpClient都不同,我是寫了個工具類來創(chuàng)建的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以上還需要安裝未知來源的權限谈息,不然也無法安裝缘屹,可以參考:
http://www.reibang.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-path
name="images"
path="test/"/>
</paths>
導致無法讀取路徑而無法安裝,因為我們是下載到Download的所以還要加入:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="images"
path="test/"/>
<external-path
name="download"
path="Download/"/>
</paths>
這樣才算完整侠仇!關于fileprovider路徑介紹:
https://blog.csdn.net/leilifengxingmw/article/details/57405908
坑六:安裝完成后閃退轻姿,或安裝完成后點擊打開閃退
在大部分手機上沒有問題,但在Vivo X9上居然安裝完成后閃退了逻炊,我覺得這應該是應用已經(jīng)死了互亮,所有安裝完成后立即啟動有問題,而其他手機就沒有問題余素,我覺得還是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!
PS:由于一些同學要工具炊林,我又不可能要一個工具發(fā)一個。所以我把我收集的工具類發(fā)出來卷要,需要什么工具可以自行篩選:SmallUtils