Markdown版本筆記 | 我的GitHub首頁 | 我的博客 | 我的微信 | 我的郵箱 |
---|---|---|---|---|
MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
FileProvider N 7.0 升級 安裝APK 選擇文件 拍照 臨時權限 MD
目錄
問題
我們在開發 app 時避免不了需要添加應用內升級功能。當 app 啟動時,如果檢測到最新版本,將 apk 安裝包從服務器下載下來,執行安裝。
安裝apk的代碼一般寫法如下,網上隨處可以搜到
public static void installApk(Context context, File file) {
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri data = Uri.fromFile(file); //核心代碼
intent.setDataAndType(data, "application/vnd.android.package-archive");
context.startActivity(intent);
}
然而,當我們在Android7.0
手機中執行時,會發現會報如下錯誤日志:
Caused by: android.os.FileUriExposedException: file:///storage/emulated/0/Android/data/net.csdn.blog.ruancoder/cache/test.apk exposed beyond app through Intent.getData()
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
at android.net.Uri.checkFileUriExposed(Uri.java:2346)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8933)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8894)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1517)
at android.app.Activity.startActivityForResult(Activity.java:4224)
at android.support.v4.app.BaseFragmentActivityJB.startActivityForResult(BaseFragmentActivityJB.java:50)
at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:79)
at android.app.Activity.startActivityForResult(Activity.java:4183)
at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:859)
at android.app.Activity.startActivity(Activity.java:4507)
at android.app.Activity.startActivity(Activity.java:4475)
官方文檔的相關描述
FileUriExposedException官方文檔 的一些描述:
- 當應用程序將文件以【file://】形式的Uri公開到另一個應用程序時拋出的異常
- 不鼓勵這種曝光方式,因為接收的app可能無法訪問你所共享的路徑。例如,接收app可能未請求運行時權限
Manifest.permission.READ_EXTERNAL_STORAGE
,或者平台可能跨用戶配置文件邊界[user profile boundaries]
共享Uri。 - 相反,應用程序應使用【content://】形式的Uris,以便平台可以擴展接收應用程序的臨時權限以訪問資源。
- 僅針對
Build.VERSION_CODES.N
或更高版本的應用程序拋出此操作。 早期SDK版本的app可以以【file://】形式的Uri共享文件,但強烈建議不要這樣做。
FileProvider 官方文檔 的一些描述:
- FileProvider是
ContentProvider
的一個特殊子類,它通過創建content://
Uri 而不是file:///
Uri 來促進與應用程序關聯的文件的安全共享。 - content URI 允許您使用臨時訪問權限來獲取讀寫訪問權限。當您創建包含
content URI
的Intent時,為了將content URI
發送到客戶端app,您還可以調用Intent.setFlags()
來添加權限。只要接收 Activity 的堆棧處於活動狀態,客戶端應用程序就可以使用這些權限。對於轉到Service的Intent,只要Service正在運行,權限就可用。 - 相比之下,要控制對
file:/// Uri
的訪問,您必須修改基礎文件的文件系統權限。您提供的權限可供任何app使用,並在您更改之前保持有效。這種訪問level從根本上說是不安全的。 - 通過 content URI 提高文件訪問安全性使FileProvider成為Android安全基礎架構的關鍵部分。
配置
不要再去看垃圾官方文檔了,也不要去看網上各種垃圾文章了,也不要通過看源碼什么的去研究怎么個性化設置了,MLGB,我在幾個月的時間內幾次嘗試理清怎么配置,結果還是發現了各種各樣的bug,真的不要再折騰了,這些個性化的東西不重要,只要按照我下面的配置就行了,保證是最簡單穩定的。
聲明 FileProvider
在清單文件中聲明FileProvider:
<manifest>
<application>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
其中:
android:name
固定寫法如果要覆蓋FileProvider方法的任何默認行為,可擴展FileProvider類並在這里使用完全限定的類名。
android:authorities
需自定義,是用來標識該 provider 的唯一標識,建議結合包名來保證 authority 的唯一性Set the android:authorities attribute to a URI authority based on a domain you control; for example, if you control the domain
mydomain.com
you should use the authoritycom.mydomain.fileprovider
android:exported
必須設置成 false,否則運行時會報錯java.lang.SecurityException: Provider must not be exported
the FileProvider does not need to be public
android:grantUriPermissions
用來控制是否允許臨時授予文件的訪問權限,必須設置成 true- meta-data 節點
android:name
固定寫法。android:resource
指定共享文件的路徑,此文件放在res/xml/
下
配置 resource
文件內容完全照抄就行了,鱉折騰。
在res/xml/
下添加file_paths.xml
配置文件
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path
name="files"
path="/"/>
<cache-path
name="cache"
path="/"/>
<external-path
name="external"
path="/"/>
<external-files-path
name="external_file_path"
path="/"/>
<external-cache-path
name="external_cache_path"
path="/"/>
<!--<external-media-path
name="external-media-path"
path=""/>-->
</paths>
可配置的元素:
files-path
對應內部存儲目錄Context.getFilesDir()
cache-path
對應內部存儲目錄Context.getCacheDir()
external-path
對應Environment.getExternalStorageDirectory()
external-files-path
對應Context.getExternalFilesDir()
external-cache-path
對應Context.getExternalCacheDir()
external-media-path
對應Context.getExternalMediaDirs()
系統提供的各種文件路徑
很少會用到的路徑:
String downloadCache = Environment.getDownloadCacheDirectory().getAbsolutePath(); //【/cache】
String data = Environment.getDataDirectory().getAbsolutePath(); //【/data】
String root = Environment.getRootDirectory().getAbsolutePath(); //【/system】
SD卡上的路徑
String ext = Environment.getExternalStorageDirectory().getAbsolutePath(); //【/storage/emulated/0】
String extDowmload = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath(); //【/storage/emulated/0/Download】
String extDcim = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath(); //【/storage/emulated/0/DCIM】
data/包名
下的路徑:
String filesPath = getFilesDir().getAbsolutePath(); //【/data/user/0/包名/files】
String cachePath = getCacheDir().getAbsolutePath(); //【/data/user/0/包名/cache】
String extCachePath = getExternalCacheDir().getAbsolutePath(); //【/storage/emulated/0/Android/data/包名/cache】
String extFileDowmloadPath = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();//【/storage/emulated/0/Android/data/包名/files/Download】
String extFilesDCIMPath = getExternalFilesDir(Environment.DIRECTORY_DCIM).getAbsolutePath();//【/storage/emulated/0/Android/data/包名/files/DCIM】
使用案例
安裝指定路徑的apk
記得要申請權限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
示例代碼:
public class MainActivity extends AppCompatActivity {
private File apkFile;
public static final String FROM_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/temp.apk";
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
apkFile = new File(FROM_PATH);
findViewById(R.id.tv).setOnClickListener(v -> {
//在6.0的華為手機上不能安裝【getFilesDir()】目錄下的安裝包,但可以安裝【getExternalStorageDirectory()】下的安裝包
//而在8.0的小米手機上既可以安裝【getFilesDir()】目錄下的安裝包,也可以安裝【getExternalStorageDirectory()】下的安裝包
File fileDir = new File(getFilesDir(), "bqt");
if (!fileDir.exists()) fileDir.mkdirs();
apkFile = new File(fileDir, "temp2.apk");
copyFile(new File(FROM_PATH), apkFile);
});
findViewById(R.id.tv2).setOnClickListener(v -> installApk(this, apkFile));
}
public static void copyFile(File from, File to) {
try {
FileInputStream fis = new FileInputStream(from);
FileOutputStream fos = new FileOutputStream(to);
byte[] buf = new byte[1024];
int len;
while ((len = fis.read(buf)) != -1) {
fos.write(buf, 0, len);
}
fis.close();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private static void installApk(Context context, File file) {
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
//【content://{$authority}/external/temp.apk】或【content://{$authority}/files/bqt/temp2.apk】
} else {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//【file:///storage/emulated/0/temp.apk】
uri = Uri.fromFile(file);
}
Log.i("bqt", "【Uri】" + uri);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
context.startActivity(intent);
}
}
拍照並指定保存位置
記得要申請權限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA"/>
示例代碼:
public class MainActivity extends Activity {
private int REQUEST_CODE_CAMERA = 10086;
private File tempFile;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.tv).setOnClickListener(v -> {
//TMLGB,在6.0的華為手機上,使用 getFilesDir() 鐵定失敗
//在8.0的小米6上,使用Environment.getExternalStorageDirectory()也同樣失敗
File fileDir = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? getFilesDir() : Environment.getExternalStorageDirectory();
fileDir = new File(fileDir, "bqt");
if (!fileDir.exists()) fileDir.mkdirs();
tempFile = new File(fileDir, "temp");
showCamera(this, tempFile, REQUEST_CODE_CAMERA);
});
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_CAMERA && resultCode == Activity.RESULT_OK) {
Log.i("【bqt", tempFile.getAbsolutePath());//【data/user/0/包名/files/bqt/temp】或【/storage/emulated/0/bqt/temp】
}
}
public static void showCamera(Activity activity, File file, int requestCode) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
String authority = activity.getPackageName() + ".fileprovider"; //【清單文件中provider的authorities屬性的值】
uri = FileProvider.getUriForFile(activity, authority, file);
} else {
uri = Uri.fromFile(file);
}
Log.i("bqt", "【uri】" + uri);//【content://{$authority}/files/bqt/temp】或【file:///storage/emulated/0/bqt/temp】
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
activity.startActivityForResult(intent, requestCode);
}
}
2018-8-28