Android版本更新模块
2020-09-10 本文已影响0人
Ad大成
目前该模块只支持直接下载 还有很多需要优化的地方 比如增量更新 断点下载 本地文件校验等
下面是全部代码模块以及所有代码 微信图片_20200910180722.pngDownloadBean
package com.example.newviewtiny.add.appupdater.bean;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.Serializable;
//数据会用到dialog中 会用到bundle传值 所以要实现序列化
public class DownloadBean implements Serializable {
public String title;
public String content;
public String url;
public String md5;
public String versionCode;
public static DownloadBean parse(String response) {
try {
JSONObject repJson = new JSONObject(response);
//optString方法会在对应的key中的值不存在的时候返回一个空字符串或者返回你指定的默认值,但是getString方法会出现空指针异常的错误。
String title=repJson.optString("title");
String content=repJson.optString("content");
String url=repJson.optString("url");
String md5=repJson.optString("md5");
String versionCode=repJson.optString("versionCode");
DownloadBean downloadBean = new DownloadBean();
downloadBean.title=title;
downloadBean.content=content;
downloadBean.url=url;
downloadBean.md5=md5;
downloadBean.versionCode=versionCode;
return downloadBean;
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
}
INetCallBack
package com.example.newviewtiny.add.appupdater.net;
public interface INetCallBack {
//get请求的 callback 一般对于网络请求 要么成功要么失败
//成功返回结果
void success(String response);
//失败 抛出异常
void failed(Throwable throwable);
}
INetDownloadCallBack
package com.example.newviewtiny.add.appupdater.net;
import java.io.File;
public interface INetDownloadCallBack {
//下载接口 通常也是下载成功 和失败
//成功 返回给用户是一个成功的apk文件
void success(File apkFile);
//下载的进度
void progress(int progress);
//失败 抛出异常
void failed(Throwable throwable);
}
INetManager
package com.example.newviewtiny.add.appupdater.net;
import java.io.File;
public interface INetManager {
//接口对外提供什么样的能力
//支持简单的get 请求 一般请求都是异步的处理, 所以这里边需要穿一个callback
void get(String url , INetCallBack callBack,Object tag);
//下载文件的一个请求 需要一个下载的文件保存到哪里去 同样是异步需要一个回调接口callback 以上可能需要实现的内容不一样所以建立两个接口
void download(String url , File targhetFile , INetDownloadCallBack callBack,Object tag);
//如果下载未完成 用户点击了cancel退出了 那么会造成系统宕机 因为弹窗的activity被销毁了 会出现null 所以要加一个方法来处理这个问题
//做一个tag 标识 所以上面的get download 方法都需要加上这个tag参数 不然没有办法去匹配
void cancel(Object tag);
}
OkHttpNetManager
package com.example.newviewtiny.add.appupdater.net;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class OkHttpNetManager implements INetManager {
private static String TAG="OkHttpNetManager";
private static OkHttpClient sOkHttpClient;
private static Handler sHandler=new Handler(Looper.getMainLooper());
static {
OkHttpClient.Builder builder=new OkHttpClient.Builder();
builder.connectTimeout(15, TimeUnit.SECONDS);
sOkHttpClient=builder.build();
//如果请求的是https接口 会出现一个握手的错误 需要设置 builder.sslSocketFactory();
}
@Override
public void get(String url, final INetCallBack callBack,Object tag) {
Request.Builder builder = new Request.Builder();
final Request request = builder.url(url).get().tag(tag).build();
Call call = sOkHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, final IOException e) {
//回调需要在ui线程 所以创建一个handler
sHandler.post(new Runnable() {
@Override
public void run() {
callBack.failed(e);
}
});
}
@Override
public void onResponse(final Call call, Response response) throws IOException {
final String string = response.body().string();
try {
sHandler.post(new Runnable() {
@Override
public void run() {
callBack.success(string);
}
});
} catch (Throwable e) {
e.printStackTrace();
callBack.failed(e);
}
}
});
}
@Override
public void download(String url, final File targhetFile, final INetDownloadCallBack callBack,Object tag) {
if (targhetFile.exists()){
targhetFile.getParentFile().mkdirs();
}
Request.Builder builder = new Request.Builder();
Request request = builder.url(url).get().tag(tag).build();
Call call = sOkHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(final Call call, final IOException e) {
sHandler.post(new Runnable() {
@Override
public void run() {
callBack.failed(e);
}
});
}
@Override
public void onResponse(Call call, Response response) throws IOException {
//文件的保存
InputStream is=null;
OutputStream os=null;
try {
//总的字节长度 进度条需要的数据
final long totalLen=response.body().contentLength();
is=response.body().byteStream();
os=new FileOutputStream(targhetFile);
byte[] buffer = new byte[8 * 1024];
//当前的保存字节 进度条需要的数据
long curLen=0;
//保存的总字节
int bufferLen=0;
while (!call.isCanceled()&&(bufferLen=is.read(buffer))!=-1){
os.write(buffer,0,bufferLen);
os.flush();
curLen+=bufferLen;
final long finalCurLen = curLen;
sHandler.post(new Runnable() {
@Override
public void run() {
// 这里curlen 一个小数除以一个大的数是为0的 *一个1.0f curlen就会变成float浮点型
//用 float去除 就会得到一个小数 再*100就可以得到一个int的 100以内的进度数字
callBack.progress((int) (finalCurLen *1.0f/totalLen*100));
}
});
}
//这里创建的文件需要系统去识别并安装 所以需要设置文件的可执行 可读写
//因为当前进程创建的文件只有owner所有者自己有权限操作
//如果文件创建在sdcard 里面就不用考虑这些权限了
//如果call被cancel 需要停止执行 因为后面会用到activity实例 会空指针
if (call.isCanceled()){
return;
}
try {
targhetFile.setExecutable(true,false);
targhetFile.setWritable(true,false);
targhetFile.setReadable(true,false);
} catch (Exception e) {
e.printStackTrace();
}
sHandler.post(new Runnable() {
@Override
public void run() {
callBack.success(targhetFile);
}
});
} catch (final Throwable e) {
//如果call被cancel 需要停止执行 因为后面会用到activity实例 会空指针
if (call.isCanceled()){
return;
}
e.printStackTrace();
//我们所有的带流的操作可能会出现错误 所以这里需要所有代码try catch一下 并且callback传出去
sHandler.post(new Runnable() {
@Override
public void run() {
callBack.failed(e);
}
});
}finally {
//关闭流
if (is!=null){
is.close();
}
if (os!=null){
os.close();
}
}
}
});
}
@Override
public void cancel(Object tag) {
//要想根据tag 去停止一个正在进行的call
//其中这个call 有两个队列 一个正在执行的队列 还有一个就是等待队列
//需要 sOkHttpClient 对象去找到调度者 dispatcher 来获取这两个队列
List<Call> queuedCall = sOkHttpClient.dispatcher().queuedCalls();//排队的calls
if (queuedCall!=null){
for (Call call : queuedCall) {
if (tag.equals(call.request().tag())){
Log.i(TAG, "cancel:queuedCall = "+tag);
call.cancel();
}
}
}
List<Call> runningCall = sOkHttpClient.dispatcher().runningCalls();//执行的calls
if (runningCall!=null){
for (Call call : runningCall) {
if (tag.equals(call.request().tag())){
Log.i(TAG, "cancel:runningCall = "+tag);
call.cancel();
}
}
}
}
}
UpdateVersionShowDialog
package com.example.newviewtiny.add.appupdater.ui;
import android.content.DialogInterface;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentActivity;
import com.example.newviewtiny.R;
import com.example.newviewtiny.add.appupdater.AppUpdater;
import com.example.newviewtiny.add.appupdater.UpdaterActivity;
import com.example.newviewtiny.add.appupdater.bean.DownloadBean;
import com.example.newviewtiny.add.appupdater.net.INetDownloadCallBack;
import com.example.newviewtiny.add.appupdater.utils.AppUtils;
import org.w3c.dom.Text;
import java.io.File;
public class UpdateVersionShowDialog extends DialogFragment {
private static String TAG="UpdateVersionShowDialog";
private static final String KEY_DOWNLOAD_BEAN="download_bean";
private DownloadBean mDownloadBean;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle arguments = getArguments();
if (arguments!=null){
mDownloadBean= (DownloadBean) arguments.getSerializable(KEY_DOWNLOAD_BEAN);
}
}
//dialogFragment 创建弹窗有两个方法 onCreateView onCreateDialog 二选一
/**
*
* 创建 DialogFragment 有两种方式:
* 覆写其 onCreateDialog 方法 — ① 利用AlertDialog或者Dialog创建出Dialog。
* 覆写其 onCreateView 方法 — ② 使用定义的xml布局文件展示Dialog。
* 虽然这两种方式都能实现相同的效果,但是它们各有自己适合的应用场景:
* 方法 ①,一般用于创建替代传统的 Dialog 对话框的场景,UI 简单,功能单一。
* 方法 ②,一般用于创建复杂内容弹窗或全屏展示效果的场景,UI 复杂,功能复杂,一般有网络请求等异步操作。
* 另外它又是Fragment,所以当旋转屏幕和按下后退键时可以更好的管理其声明周期,它和Fragment有着基本一致的声明周期。
*
* @param inflater
* @param container
* @param savedInstanceState
* @return
*/
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view=inflater.inflate(R.layout.dialog_updater,container,false);
//初始化操作
bindEvents(view);
return view;
}
/**
* 复写 onViewCreated
* @param view
*/
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);//无标题
getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));//透明背景色
}
private void bindEvents(View view) {
TextView title = view.findViewById(R.id.tv_title);
TextView content = view.findViewById(R.id.tv_content);
final TextView update = view.findViewById(R.id.tv_update);
title.setText(mDownloadBean.title);
content.setText(mDownloadBean.content);
update.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
//点击下载 只能点击一次 不能重复点击
v.setEnabled(false);
final File targetFile = new File(getActivity().getCacheDir(), "target.apk");
AppUpdater.getInstance().getNetManager().download(mDownloadBean.url, targetFile , new INetDownloadCallBack() {
@Override
public void progress(int progress) {
//更新界面
Log.i(TAG, "success: progress"+progress);
update.setText(progress+"%");
}
@Override
public void failed(Throwable throwable) {
Toast.makeText(getActivity(),"文件下载失败",Toast.LENGTH_SHORT).show();
//下载失败也要恢复按钮
v.setEnabled(true);
}
@Override
public void success(File apkFile) {
//安装
Log.i(TAG, "success: apkFile"+apkFile.getAbsolutePath());
//安装成功 恢复按钮
v.setEnabled(true);
dismiss();
//做一个md5的匹配 md5的检测可以告诉我们文件有没有改动或者文件有没有完整的被下载下来
String fileMd5=AppUtils.getFileMd5(targetFile);
Log.i(TAG, "success: md5="+fileMd5);
if (fileMd5!=null&&fileMd5.equals(mDownloadBean.md5)){
AppUtils.installApk(getActivity(),apkFile);
}else{
Toast.makeText(getActivity(),"Md5检测失败",Toast.LENGTH_SHORT).show();
}
}
},UpdateVersionShowDialog.this);
}
});
}
@Override
public void onDismiss(@NonNull DialogInterface dialog) {
super.onDismiss(dialog);
Log.i(TAG, "onDismiss: ");
AppUpdater.getInstance().getNetManager().cancel(this);
}
public static void show (FragmentActivity fragmentActivity, DownloadBean downloadBean){
Bundle bundle = new Bundle();
bundle.putSerializable(KEY_DOWNLOAD_BEAN,downloadBean);
UpdateVersionShowDialog updateVersionShowDialog = new UpdateVersionShowDialog();
updateVersionShowDialog.setArguments(bundle);
updateVersionShowDialog.show(fragmentActivity.getSupportFragmentManager(),"updateVersionShowDialog");
}
}
AppUtils
package com.example.newviewtiny.add.appupdater.utils;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import androidx.core.content.FileProvider;
import androidx.fragment.app.FragmentActivity;
import com.example.newviewtiny.R;
import com.example.newviewtiny.add.appupdater.UpdaterActivity;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class AppUtils {
public static long getVersionCode(Context context) {
PackageManager packageManager = context.getPackageManager();
try {
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
//这里注意千万不要忽视这种带有@Deprecated 的API
//所以尽可能的写全这种兼容性的代码 P以上getLongVersionCode P一下versionCode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return packageInfo.getLongVersionCode();
}else{
return packageInfo.versionCode;
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return -1;
}
public static void installApk(Activity activity, File apkFile) {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_VIEW);
Uri uri=null;
//android N 不允许将file这样的uri直接暴露给别的进程 或者说通过Intent.getData这种方式分享出去
//所以 Android N需要做FileProvider的适配 通过contentProvider去对外暴露 四大组件就需要去清单文件注册一下
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
uri= FileProvider.getUriForFile(activity,activity.getPackageName()+".fileprovider",apkFile);
//添加一下读写权限的flags
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}else{
uri = Uri.fromFile(apkFile);
}
//这里还需要适配一个O 的适配 就是在清单文件中申请一个权限 REUEST_INSTALL_PACKAGES
intent.setDataAndType(uri,"application/vnd.android.package-archive");
activity.startActivity(intent);
}
public static String getFileMd5(File targetFile) {
if (!targetFile.isFile()||targetFile==null){
return null;
}
MessageDigest digest=null;
FileInputStream in =null;
byte[] buffer=new byte[1024];
int len=0;
try {
digest=MessageDigest.getInstance("MD5");
in=new FileInputStream(targetFile);
while ((len=in.read(buffer))!=-1){
digest.update(buffer,0,len);
}
} catch (Exception e) {
e.printStackTrace();
return null;
}finally {
if (in!=null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
byte[] result = digest.digest();
//通BigInteger 来转换字节数组 然后toString 16进制
BigInteger bigInteger=new BigInteger(1,result);
String s = bigInteger.toString(16);
return s;
}
}
AppUpdater 这是对外使用者开放的类
package com.example.newviewtiny.add.appupdater;
import com.example.newviewtiny.add.appupdater.net.INetManager;
import com.example.newviewtiny.add.appupdater.net.OkHttpNetManager;
public class AppUpdater {
//做一个单例
private static AppUpdater instance =new AppUpdater();
//网络请求 下载的能力 利用接口隔离实现 这里需要写一个具体实现类OKHttpNetManager来实现INetManager
private INetManager mNetManager =new OkHttpNetManager();
//如果想做的更灵活一些 可以做一个set方法 让使用者来决定使用哪一个实现INetManager(目前是okHttpNetManager,如果不想用OK实现 也可以换一个NetManager)
// public void setNetManager (INetManager manager){
// mNetManager=manager;
// }
public INetManager getNetManager(){
return mNetManager;
}
//对外提供一个获取实例的方法
public static AppUpdater getInstance(){
return instance;
}
}
UpdaterActivity
package com.example.newviewtiny.add.appupdater;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import com.example.newviewtiny.R;
import com.example.newviewtiny.add.appupdater.bean.DownloadBean;
import com.example.newviewtiny.add.appupdater.net.INetCallBack;
import com.example.newviewtiny.add.appupdater.net.INetDownloadCallBack;
import com.example.newviewtiny.add.appupdater.ui.UpdateVersionShowDialog;
import com.example.newviewtiny.add.appupdater.utils.AppUtils;
import java.io.File;
public class UpdaterActivity extends AppCompatActivity {
private static String TAG="UpdaterActivity";
private Button btn_updater;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_updater);
init();
}
private void init() {
btn_updater = findViewById(R.id.btn_updater);
btn_updater.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//其实当NetManager里面一行代码都没有写的时候 我们就可以一直写整个流程的代码
/**
* http://59.110.162.30/app_updater_version.json
* 通过接口去屏蔽具体的实现
* 1.方便未来替换具体实现
* 2.多个开发者并行
* {
* "title":"4.5.0更新啦!",
* "content":"1. 优化了阅读体验;\n2. 上线了 hyman 的课程;\n3. 修复了一些已知问题。",
* "url":"http://59.110.162.30/v450_imooc_updater.apk",
* "md5":"14480fc08932105d55b9217c6d2fb90b",
* "versionCode":"450"
* }
*
*/
AppUpdater.getInstance().getNetManager().get("http://59.110.162.30/app_updater_version.json", new INetCallBack() {
@Override
public void success(String response) {
Log.i(TAG, "success: response"+response);
//1.解析 json
//这里因为返回的json串 只与downloadbean有关 所以在downloadbean中做解析处理更优雅一些
DownloadBean bean=DownloadBean.parse(response);
//如果为空终止下载
if (bean==null){
//此处更严谨的方法就是DownloadBean里面 创建一个check方法 检测每一个字段是否为null
Toast.makeText(UpdaterActivity.this,"版本检测接口返回数据异常",Toast.LENGTH_SHORT).show();
return;
}
//2.做本地版本和返回的版本匹配
//这里versionCode有可能是字符串或者汉字 所以这里有潜在风险 有潜在风险的代码一定要try
try {
long versionCode = Long.parseLong(bean.versionCode);
if (versionCode<= AppUtils.getVersionCode(UpdaterActivity.this)){
Toast.makeText(UpdaterActivity.this,AppUtils.getVersionCode(UpdaterActivity.this)+"已经是最新版本无需更新",Toast.LENGTH_SHORT).show();
return;
}
} catch (NumberFormatException e) {
e.printStackTrace();
Toast.makeText(UpdaterActivity.this,"版本检测接口返回版本号异常",Toast.LENGTH_SHORT).show();
return;
}
//如果需要更新
//3.弹窗
UpdateVersionShowDialog.show(UpdaterActivity.this,bean);
//4.点击下载
}
@Override
public void failed(Throwable throwable) {
Toast.makeText(UpdaterActivity.this,"版本更新失败",Toast.LENGTH_SHORT).show();
}
},UpdaterActivity.this);
}
});
}
/**
* 如果activity被销毁了 我们就cancel掉这个请求的call
*/
@Override
protected void onDestroy() {
super.onDestroy();
AppUpdater.getInstance().getNetManager().cancel(this);
}
}
shape_dialog_updater.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#D94343"></solid>
<corners android:radius="8dp"></corners>
</shape>
dialog_updater.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="@dimen/dp_500"
android:background="@android:color/white"
android:layout_height="wrap_content">
<TextView
android:layout_marginTop="@dimen/dp_20"
android:id="@+id/tv_title"
android:layout_gravity="center_horizontal"
tools:text="标题"
android:textSize="@dimen/sp_24"
android:textStyle="bold"
android:textColor="@android:color/black"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_marginTop="@dimen/dp_20"
android:id="@+id/tv_content"
android:layout_gravity="center_horizontal"
tools:text="内容"
android:textSize="@dimen/sp_20"
android:textStyle="bold"
android:textColor="#666666"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_margin="@dimen/dp_20"
android:id="@+id/tv_update"
android:layout_gravity="center_horizontal"
android:text="升级"
android:gravity="center"
android:background="@drawable/shape_dialog_updater"
android:textSize="@dimen/sp_24"
android:textStyle="bold"
android:textColor="@android:color/white"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_60"/>
</LinearLayout>
fileproviderpath.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path
name="files"
path="."></files-path>
<!-- 假设有一个cacheDir/targetFile 就会转换成 content://cache/targetFile-->
<!-- 所有contentUri通过name=cache 就能定位到 cache-path这个目录-->
<!-- 所以逆向就会是这样的-->
<!-- content://cache/targetFile-–> cache-path/targetFile-–>getCacheDir/targetFile 其中cache-path就是getCacheDir-->
<!-- 也就说通过contentUri 逆向到一个具体的文件getCacheDir 所以外界才能够进行读写操作-->
<!-- 整个的操作过程都是屏蔽在ContentProvider里面的-->
<!-- 这也就是说我们要把fileUri暴露给外界共享数据 就要通过contentProvider-->
<!-- 为什么contentProvider需要一个xml呢 因为他需要把contentUri映射成具体的文件getCacheDir 只是把文件路径隐藏到contentProvier里面了-->
<cache-path
name="cache"
path="."></cache-path>
</paths>
清单文件
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<application
android:name=".MyApp"
android:allowBackup="true"
android:icon="@mipmap/logo"
android:label="@string/app_name"
android:roundIcon="@mipmap/logo"
android:supportsRtl="true"
android:theme="@style/AppThemeBG"
android:usesCleartextTraffic="true">
<!-- ${applicationId} 会在外面打包的时候 替换成我们当前应用的包名-->
<!-- 系统要求exported 一定是false-->
<!-- 为什么要使用contentProvider呢?-->
<!-- 主要就是系统不希望我们把filePath传递给别人-->
<!-- 所以吧filePath 替换成content uri 提供给外界 -->
<!-- 外界会通过contentProvider去转化成filePath的-->
<!-- 其中转化规则就是@xml/fileproviderpath 用处-->
<provider
android:authorities="${applicationId}.fileprovider"
android:name="androidx.core.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true"
>
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/fileproviderpath"/>
</provider>