目前該模塊只支持直接下載 還有很多需要優化的地方 比如增量更新 斷點下載 本地文件校驗等
下面是全部代碼模塊以及所有代碼DownloadBean
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>