Android 10(API 級別 29)引入了多項功能和行為變更,目的是更好地保護(hù)用戶的隱私權(quán)。其中最重要的變化之一就是存儲訪問權(quán)限。
Android 10中,Google針對外部存儲引入了一個新特性,它的名字叫:Scoped Storage,Google官方對它的翻譯為分區(qū)存儲,我們也可以把它叫做作用域存儲,至于為什么?
本文中,我們還是將它翻譯為分區(qū)存儲。好了,我要開始提問了!
問題一、外部存儲?內(nèi)部存儲?內(nèi)存?
1.1、內(nèi)存
有些朋友經(jīng)常將內(nèi)存和內(nèi)部存儲搞混,因為內(nèi)部存儲也可以被簡稱為內(nèi)存:
A:聽說你買了新手機(jī)?內(nèi)存多大的啊?后臺能開多少啊?
B:哈哈,256G,裝幾百個都不是問題!
A:呵呵
但實際上它們是兩個不一樣的東西。內(nèi)存(RAM)簡單理解就是程序運(yùn)行時臨時的數(shù)據(jù)存儲器,某個程序進(jìn)程結(jié)束后,關(guān)于此程序的所有內(nèi)存數(shù)據(jù)都會消失,而斷電后整個內(nèi)存里面的數(shù)據(jù)都會丟失。由于內(nèi)存經(jīng)常與CPU打交道,因此它的讀寫速度是相當(dāng)快的,內(nèi)存也是我們通常所說的隨機(jī)存取存儲器(Random Access Memory)。
1.2、內(nèi)部存儲
內(nèi)部存儲顧名思義就是手機(jī)自帶的存儲空間,一般情況下,系統(tǒng)和應(yīng)用都是安裝在內(nèi)部存儲中的。在沒有root的情況下,普通用戶是無法查看內(nèi)部存儲中的文件的。對于Android開發(fā)者,最熟悉的應(yīng)該就是:/data/user/0/<package>
這個路徑,其中0表示用戶ID
(似乎Android 6.0以后開始支持多用戶)。而我們更熟悉的/data/data/<package>
實際上是/data/user/<current_user_id>/<package>
的一個鏈接,注意是current_user_id
。
這個路徑是應(yīng)用的內(nèi)部存儲私有目錄,保存到這個路徑下的文件是應(yīng)用的私有文件,其他應(yīng)用不能訪問這些文件(除非擁有 Root 訪問權(quán)限),非常適合保存用戶無需直接訪問的內(nèi)部應(yīng)用數(shù)據(jù)。
當(dāng)我們卸載應(yīng)用后,保存在私有路徑中的文件也會被刪除。因此,我們不應(yīng)該將那些希望應(yīng)用卸載以后還保留的數(shù)據(jù)文件放在私有路徑中。
主要有以下幾個常用的目錄:
-
files目錄
完整路徑為:
/data/data/<package>/files
。用于保存應(yīng)用創(chuàng)建的文件,可以通過Context對象獲取其路徑:// java String path = getFilesDir().getAbsolutePath(); // kotlin val path = filesDir.absolutePath
-
cache目錄
完整路徑為:
/data/data/<package>/cache
。用于保存應(yīng)用的臨時緩存文件,可以通過Context對象獲取其路徑:// java String path = getCacheDir().getAbsolutePath(); // kotlin val path = cacheDir.absolutePath
-
shared_prefs目錄
完整路徑為:
/data/data/<package>/shared_prefs
。用于保存SharedPreferences
的數(shù)據(jù)文件。 -
databses目錄
完整路徑為:
/data/data/<package>/databses
。用于保存Sqlite
數(shù)據(jù)庫文件。
1.3、外部存儲
在很久很久以前,幾乎所有的Android手機(jī)都可以插入一張micro SD卡,因為內(nèi)部存儲實在太小了,我第一款A(yù)ndroid手機(jī)是SONY LT18i,內(nèi)部存儲只有1GB,最大支持32GB的SD卡。我們所說的外部存儲,指的就是我們插入的那張SD卡。SD卡一般會被掛載到/storage/sdcard1
,根據(jù)設(shè)備的不同,不一定叫sdcard1
,比如在我的模擬器中,路徑為:/storage/1106-3A09
。
而現(xiàn)在,幾乎沒有Android手機(jī)再提供SD卡的插口。
那...為什么不提供了?不能插SD卡意味著我的手機(jī)沒有外部存儲了?那為什么我還能用文件管理器看到很多文件?不是說內(nèi)部存儲不root看不到嗎?
1.3.1、為什么不再提供SD卡插口?
我個人覺得,有以下原因:
- SD卡可隨時被用戶移出,因此,在嘗試訪問應(yīng)用外部存儲中的文件時,都需要檢查外部存儲目錄及嘗試訪問的文件是否可用。
- SD卡讀寫速度慢,限制了應(yīng)用(比如游戲)的加載速度。拿頂級的micro SD卡來說,它的寫入速度只有90MB/s,讀取速度只有170MB/s,和現(xiàn)在手機(jī)內(nèi)置存儲的UFS 3.1芯片相比,太弱了。(UFS 3.1 寫入:700+MB/s,讀取:1700MB+/s)
- 為了手機(jī)設(shè)計美觀,減少開孔
1.3.2、不能插SD卡意味著我的手機(jī)沒有外部存儲了嗎?
現(xiàn)在幾乎所有手機(jī)都會自帶比較可觀的內(nèi)部存儲容量,動不動上來就是128GB、256GB、512GB,起步都是128GB了,64GB都幾乎看不到了,不過64GB是真的不夠用,親身經(jīng)歷!
系統(tǒng)會將內(nèi)部存儲空間通過fuse技術(shù)掛載到/storage/emulated/0
上,這個掛載點就是外部存儲,沒有SD卡一樣可以擁有外部存儲了,這個外部存儲嚴(yán)格意義上叫做內(nèi)置外部存儲,和內(nèi)部存儲共享空間,它有如下好處:
- 永遠(yuǎn)在線,不可被用戶移出
- 和內(nèi)置存儲共享空間,速度也快
1.3.3、外部存儲分私有目錄和公有目錄嗎?
-
外部存儲私有目錄
是的,外部存儲也有應(yīng)用的私有目錄,嚴(yán)格來說叫做外部存儲私有目錄,它的路徑位于:
/storage/emulated/0/Android/data/<package>
中,同樣擁有files文件夾和cache文件夾,使用如下代碼可以獲取相應(yīng)路徑:-
files目錄
// java File file = getExternalFilesDir(""); String path = ""; if (file != null){ path = file.getAbsolutePath(); } // kotlin val path = getExternalFilesDir("")?.absolutePath ?: ""
getExternalFilesDir()
是需要一個字符串參數(shù)的,如果我們傳入空字符串,則獲取到/storage/emulated/0/Android/data/<package>/files
,如果我們傳入"test"
,則獲取到:/storage/emulated/0/Android/data/<package>/files/test
。官方建議我們不要隨意命名,應(yīng)該使用Environment
下定義好的名字:
-
-
cache目錄
// java File file = getExternalCacheDir(""); String path = ""; if (file != null){ path = file.getAbsolutePath(); } // kotlin val path = externalCacheDir?.absolutePath ?: ""
卸載應(yīng)用后,外部存儲私有路徑中的文件同樣會被清除。那...內(nèi)部存儲中的私有目錄和外部存儲中的私有目錄有什么區(qū)別呢?官方文檔的解釋如下:
盡管這些文件在技術(shù)上可被用戶和其他應(yīng)用訪問(因為它們存儲在外部存儲上),但它們不能為應(yīng)用之外的用戶提供價值。可以使用此目錄來存儲您不想與其他應(yīng)用共享的文件。
-
公有目錄
外部存儲中,除了私有目錄以外的目錄,都是公有目錄。程序保存在公有目錄中的數(shù)據(jù),在應(yīng)用被刪除后,仍然保留。
問題二、Android 10的分區(qū)存儲究竟影響了什么?
相信每一臺Android手機(jī)的外部存儲根目錄都是亂得一塌糊涂,這是因為在Android 10以前,只要程序獲得了READ_EXTERNAL_STORAGE
權(quán)限,就可以隨意讀取外部存儲的公有目錄;只要程序獲得了WRITE_EXTERNAL_STORAGE
權(quán)限,就可以隨意在外部存儲的公有目錄上新建文件夾或文件。
于是Google終于開始動手了,在Android 10中提出了分區(qū)存儲,意在限制程序?qū)ν獠看鎯χ泄心夸浀臑樗麨椤?strong>分區(qū)存儲對 內(nèi)部存儲私有目錄 和 外部存儲私有目錄 都沒有影響。
簡而言之,在Android 10中,對于私有目錄的讀寫沒有變化,仍然可以使用File那一套,且不需要任何權(quán)限。而對于公有目錄的讀寫,則必須使用MediaStore
提供的API或是SAF
(存儲訪問框架),意味著我們不能再使用File那一套來隨意操作公有目錄了。
使用分區(qū)存儲的應(yīng)用對自己創(chuàng)建的文件始終擁有讀/寫權(quán)限,無論文件是否位于應(yīng)用的私有目錄內(nèi)。因此,如果您的應(yīng)用僅保存和訪問自己創(chuàng)建的文件,則無需請求獲得
READ_EXTERNAL_STORAGE
或WRITE_EXTERNAL_STORAGE
權(quán)限。
若要訪問其他應(yīng)用創(chuàng)建的文件,則需要READ_EXTERNAL_STORAGE
權(quán)限。并且仍然只能使用MediaStore
提供的API或是SAF
(存儲訪問框架)訪問。
需要注意的是,MediaStore
提供的API只能訪問:圖片、視頻、音頻,如果需要訪問其它任意格式的文件,需要使用SAF
(存儲訪問框架),它會調(diào)用系統(tǒng)內(nèi)置的文件瀏覽器供用戶自主選擇文件。
似乎沒WRITE_EXTERNAL_STORAGE
什么事兒了?確實,聽說這個權(quán)限在后續(xù)會被廢除。
問題三、必須要適配嗎?
本來從Android 10開始,Google就決定強(qiáng)制采用分區(qū)存儲,但從預(yù)覽版的反饋來看,很多應(yīng)用都GG了,因此Google決定給開發(fā)者一段過渡時間,暫時不強(qiáng)制要求,但早晚會強(qiáng)制要求的。
如果我們將targetSdkVersion
設(shè)置為低于29的值,那么即使不做任何關(guān)于Android 10的適配,我們的項目也可以成功運(yùn)行到Android 10手機(jī)上。
如果我們將targetSdkVersion
設(shè)置為29了,但就是不想適配分區(qū)存儲,可以在清單文件中做如下設(shè)置:
<manifest ... >
<application android:requestLegacyExternalStorage="true" ... >
...
</application>
</manifest>
問題四、真的不能在外部存儲撒野了?我不信
首先我們將應(yīng)用的compileSdkVersion
和targetSdkVersion
都設(shè)置為29,并且授予程序讀寫的權(quán)限,然后編寫如下代碼:
val file = File(Environment.getExternalStorageDirectory(), "MyAppDir")
if (!file.exists()) {
Log.d("createDir", file.mkdir().toString())
}
代碼很簡單,作用是在外部存儲的根目錄下創(chuàng)建一個名叫MyDir的文件夾。我們將這個程序運(yùn)行在Android 10的設(shè)備上,發(fā)現(xiàn)并沒有創(chuàng)建成功(file.mkdir().toString()
為false
)。
同樣的程序,我們將它運(yùn)行在Android 10以下的設(shè)備,就可以成功創(chuàng)建這個文件夾。
問題五、那我應(yīng)該怎么操作外部存儲?
現(xiàn)在我們已經(jīng)知道,在應(yīng)用確認(rèn)支持分區(qū)存儲之后,就不能再使用以前那一套來操作外部存儲了。那現(xiàn)在應(yīng)該怎么辦呢?Google官方推薦我們使用MediaStore
提供的API訪問圖片、視頻、音頻資源,使用SAF
(存儲訪問框架)訪問其它任意類型的資源。
5.1、使用MediaStore將圖片保存到Pictures目錄
在Environment
中我們能找到很多公有目錄文件夾的名字,其中Pictures
這個文件夾就適合用來保存圖片數(shù)據(jù):
在以前,我們經(jīng)常會根據(jù)目錄或文件的絕對路徑得到File對象,再將File對象傳給FileOutputStream
得到輸出流,然后就可以愉快地寫入數(shù)據(jù)了。Android 10以后,我們要向這些公有目錄寫入數(shù)據(jù),必須要用MediaStore
了。
下面,我們通過代碼學(xué)習(xí)如何將Bitmap保存到Pictures文件夾下:
const val APP_FOLDER_NAME = "ExternalScopeTestApp"
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.die)
val displayName = "${System.currentTimeMillis()}.jpg"
val mimeType = "image/jpeg"
val compressFormat = Bitmap.CompressFormat.JPEG
private fun saveBitmapToPicturePublicFolder(
bitmap: Bitmap,
displayName: String,
mimeType: String,
compressFormat: Bitmap.CompressFormat
) {
val contentValues = ContentValues()
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
val path = getAppPicturePath()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, path)
} else {
val fileDir = File(path)
if (!fileDir.exists()){
fileDir.mkdir()
}
contentValues.put(MediaStore.MediaColumns.DATA, path + displayName)
}
val uri =
contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
uri?.also {
val outputStream = contentResolver.openOutputStream(it)
outputStream?.also { os ->
bitmap.compress(compressFormat, 100, os)
os.close()
Toast.makeText(this, "添加圖片成功", Toast.LENGTH_SHORT).show()
}
}
}
fun getAppPicturePath(): String {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// full path
"${Environment.getExternalStorageDirectory().absolutePath}/" +
"${Environment.DIRECTORY_PICTURES}/$APP_FOLDER_NAME/"
} else {
// relative path
"${Environment.DIRECTORY_PICTURES}/$APP_FOLDER_NAME/"
}
}
代碼為了方便展示做了位置調(diào)整,大家可以直接查看完整代碼。項目地址在文末給出。
首先我們創(chuàng)建了ContentValues
對象,并往里面添加了三種信息:
DISPLAY_NAME:圖片的名字,需要包含后綴名。在這里,我們使用的是當(dāng)前的時間戳命名
MIME_TYPE:文件的mime類型。在這里,我們使用的是
image/jpeg
-
RELATIVE_PATH、DATA:文件的存儲路徑。在Android 10中,新增了RELATIVE_PATH,它表示文件存儲的相對路徑,可選值其實就是
Environment
里面那堆,比如Pictures
、Music
等。但是注意看我們的getAppPicturePath()
中的代碼:"${Environment.DIRECTORY_PICTURES}/$APP_FOLDER_NAME/"
,后面還跟了一個$APP_FOLDER_NAME
,表示在Pictures
這個目錄下面 ,還要創(chuàng)建一個名叫:ExternalScopeTestApp
的文件夾,這是因為如果所有應(yīng)用都將圖片保存到Pictures
的根目錄,勢必會非常混亂,因此我們針對自己的應(yīng)用建立了二級文件夾,將圖片都保存到自己的二級文件夾中。DATA這個字段是Android 10以前使用的字段,在Android 10中已經(jīng)廢棄,但為了兼容老版本系統(tǒng), 我們還是要用。這個字段需要文件的絕對路徑。
ContentValues
里面的值都設(shè)置完成后,我們就可以使用ContentResolver
的insert()
方法插入數(shù)據(jù)了,插入完成后會得到插入圖片的Uri,接下來我們要根據(jù)這個Uri得到OutputStream
對象,通過:contentResolver.openOutputStream(uri)
就可得到,剩下的就是將Bitmap寫入了。
我們發(fā)現(xiàn),以前我們可以通過真實路徑得到輸出流,而現(xiàn)在只能通過Uri得到了。
如果我們不是將Bitmap保存到公有目錄,而是網(wǎng)絡(luò)上的圖片呢?其實原理都是一樣的,網(wǎng)絡(luò)上的圖片我們肯定是可以得到輸入流的,輸出流還是通過Uri獲取,然后讀取輸入流寫入輸出流不就行了嗎?
到此,保存圖片就學(xué)習(xí)完了,保存音頻、視頻都類似。注意,這些操作都是不需要權(quán)限的。
5.2、使用MediaStore獲取媒體庫中的圖片
我們向Pictures中添加了圖片文件,怎么才能獲取到呢?也必須通過MediaStore。如果我們沒有獲得存儲空間權(quán)限,那么我們只能通過MediaStore獲取到自己應(yīng)用創(chuàng)建的圖片;如果我們獲取了存儲空間權(quán)限,那么我們就可以獲取到其它應(yīng)用創(chuàng)建的圖片了。
我們通過代碼來學(xué)習(xí):
val cursor = contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
null,
null,
null,
"${MediaStore.MediaColumns.DATE_ADDED} desc"
)
cursor?.also {
while (it.moveToNext()) {
val id = it.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
val displayName =
it.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME))
val uri =
ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
}
}
cursor?.close()
代碼很簡單,最終通過Uri與圖片文件id的組合,得到了圖片文件的Uri。得到了這個圖片的Uri后,怎么顯示出來呢?可以使用Glide,因為Glide原生支持Uri:
Glide.with(this).load(uri).into(ivPicture)
如果沒有使用Glide呢?可以這樣來做,得到一個Bitmap:
val openFileDescriptor = contentResolver.openFileDescriptor(uri, "r")
openFileDescriptor?.apply {
val bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor)
ivPicture.setImageBitmap(bitmap)
}
openFileDescriptor?.close()
但是需要注意,如果圖片分辨率高,bitmap會很占用內(nèi)存,而實際要顯示的區(qū)域可能比原圖小得多,需要自己控制下bitmap的像素。
在沒有獲取存儲空間權(quán)限對情況下,我們只能獲取到應(yīng)用自己創(chuàng)建的圖片:
下面我們預(yù)先在手機(jī)中放置幾張圖片,模擬它們是其它應(yīng)用創(chuàng)建的,這幾張圖片如下:
它們有兩張在DCIM/Camera,有一張在Download/OtherApp_01目錄下。現(xiàn)在我們授予應(yīng)用存儲權(quán)限,看看是否能獲取到這三張圖片:
果然顯示出來了。大家可能有一個疑問:應(yīng)用是怎么在擁有權(quán)限的情況下只顯示自己保存的圖片的?不是一旦有權(quán)限后,就是查詢的所有嗎?答案就是增加WHERE語句,過濾DATA和RELATIVE_PATH:
private fun queryImages(queryAll: Boolean = false): List<ImageBean> {
var pathKey = ""
var pathValue = ""
if (!queryAll) {
pathKey = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
MediaStore.MediaColumns.DATA
} else {
MediaStore.MediaColumns.RELATIVE_PATH
}
// RELATIVE_PATH會在路徑的最后自動添加/
pathValue = getAppPicturePath()
}
val dataList = mutableListOf<ImageBean>()
val cursor = contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
null,
if (pathKey.isEmpty()) {
null
} else {
"$pathKey LIKE ?"
},
if (pathValue.isEmpty()) {
null
} else {
arrayOf("%$pathValue%")
},
"${MediaStore.MediaColumns.DATE_ADDED} desc"
)
cursor?.also {
while (it.moveToNext()) {
val id = it.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
val displayName = it.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME))
val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
dataList.add(ImageBean(id, uri, displayName))
}
}
cursor?.close()
return dataList
}
用LIKE的原因是DATA中存儲的是圖片的絕對路徑,我們需要匹配應(yīng)用自己圖片路徑下的所有圖片。到此,獲取媒體庫中的圖片就學(xué)習(xí)完畢。
5.3、使用MediaStore刪除媒體庫中的圖片
同樣的,刪除自己創(chuàng)建的圖片不需要任何權(quán)限,但是刪除或者修改其它應(yīng)用創(chuàng)建的圖片就需要權(quán)限了,而且即使我們擁有了存儲權(quán)限,也不能修改或刪除其它APP的資源,需要由MediaProvider
彈出彈框給用戶選擇是否允許APP修改或刪除圖片、視頻、音頻文件。用戶操作的結(jié)果,將通過onActivityResult
回調(diào)返回到APP。如果用戶允許,APP將獲得該uri的修改權(quán)限,直到設(shè)備下一次重啟。我們先來學(xué)習(xí)刪除自己應(yīng)用的圖片:
val row = contentResolver.delete(imageUri, null, null)
if (row > 0) {
Toast.makeText(this, "刪除成功", Toast.LENGTH_SHORT).show()
}
很簡單,圖片從ContentResolver
中查詢出來的時候,我們可以獲取到id,圖片的uri就是通過MediaStore.Images.Media.EXTERNAL_CONTENT_URI
與圖片的id組合而來。現(xiàn)在只需要通過uri進(jìn)行刪除即可。我們來看下在沒有存儲權(quán)限時,刪除應(yīng)用創(chuàng)建圖片的效果,當(dāng)然如果有存儲權(quán)限,也是一樣的:
但是這段代碼是不夠嚴(yán)謹(jǐn)?shù)模驗楫?dāng)我們刪除的是其它應(yīng)用的資源,程序會閃退,并拋出:RecoverableSecurityException
異常。因此我們需要捕獲這個異常,提示用戶給予此uri修改或刪除的權(quán)限:
companion object {
const val REQUEST_DELETE_PERMISSION = 1
}
private var pendingDeleteImageUri: Uri? = null
private var pendingDeletePosition: Int = -1
private fun deleteImage(imageUri: Uri, adapterPosition: Int) {
var row = 0
try {
// Android 10+中,如果刪除的是其它應(yīng)用的Uri,則需要用戶授權(quán)
// 會拋出RecoverableSecurityException異常
row = contentResolver.delete(imageUri, null, null)
} catch (securityException: SecurityException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val recoverableSecurityException =
securityException as? RecoverableSecurityException
?: throw securityException
pendingDeleteImageUri = imageUri
pendingDeletePosition = adapterPosition
// 我們可以使用IntentSender向用戶發(fā)起授權(quán)
requestRemovePermission(recoverableSecurityException.userAction.actionIntent.intentSender)
} else {
throw securityException
}
}
if (row > 0) {
Toast.makeText(this, "刪除成功", Toast.LENGTH_SHORT).show()
pictureAdapter.deletePosition(adapterPosition)
}
}
private fun requestRemovePermission(intentSender: IntentSender) {
startIntentSenderForResult(intentSender, REQUEST_DELETE_PERMISSION,
null, 0, 0, 0, null)
}
private fun deletePendingImageUri(){
pendingDeleteImageUri?.let {
pendingDeleteImageUri = null
deleteImage(it,pendingDeletePosition)
pendingDeletePosition = -1
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK &&
requestCode == REQUEST_DELETE_PERMISSION
) {
// 執(zhí)行之前的刪除邏輯
deletePendingImageUri()
}
}
簡單解釋下,當(dāng)修改或刪除非本應(yīng)用創(chuàng)建的文件uri時,在Android 10+的系統(tǒng)中,會拋出RecoverableSecurityException
,我們捕獲到這個異常后,從異常中獲得了IntentSender
,并使用它來向用戶索取該uri的修改、刪除權(quán)限。代碼都很簡單,不再贅述,效果如下:
至此,刪除媒體庫中的圖片就學(xué)習(xí)完成了。
問題六、我想讀取Download文件夾下的某個非媒體文件怎么辦?
拿PDF舉例,顯然,PDF不屬于音頻、視頻、圖片,因此我們不能使用MediaStore
來獲取。對于這種其它類型的文件,我們一般使用SAF
(存儲訪問框架)讓用戶選擇:
private fun selectPdfUseSAF() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
type = "application/pdf"
// 我們需要使用ContentResolver.openFileDescriptor讀取數(shù)據(jù)
addCategory(Intent.CATEGORY_OPENABLE)
}
startActivityForResult(intent, REQUEST_OPEN_PDF)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
REQUEST_OPEN_PDF -> {
if (resultCode == Activity.RESULT_OK) {
data?.data?.also { documentUri ->
val fileDescriptor =
contentResolver.openFileDescriptor(documentUri, "r") ?: return
// 現(xiàn)在,我們可以使用PdfRenderer等類通過fileDescriptor讀取pdf內(nèi)容
Toast.makeText(this, "pdf讀取成功", Toast.LENGTH_SHORT).show()
}
}
}
}
}
注意,ACTION_OPEN_DOCUMENT
用于打開文件。
問題七、我想創(chuàng)建任意類型文件怎么辦?
也是使用SAF
(存儲訪問框架)讓用戶去創(chuàng)建。其Intent Action為:ACTION_CREATE_DOCUMENT
,我們可以使用Intent的putExtra()來指定文件的名字:intent.putExtra(Intent.EXTRA_TITLE, "Android.pdf")
,有點類似于"另存為"功能。其它的用法差不多,就不多說了。
問題八、我想將文件下載到Download目錄怎么辦?
拿下載app為例,在Android 10以前,只要獲取到了File對象,就能得到輸入流,而由于我們適配了Android 10中的分區(qū)存儲,因此不能這樣做了。MediaStore
中提供了一種Downloads集合,專門用于執(zhí)行文件下載操作。它的使用和添加圖片是幾乎一樣的:
private fun downloadApkAndInstall(fileUrl: String, apkName: String) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Toast.makeText(this, "請使用原始方式", Toast.LENGTH_SHORT).show()
return
}
thread {
try {
val url = URL(fileUrl)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
val inputStream = connection.inputStream
val bis = BufferedInputStream(inputStream)
val values = ContentValues()
values.put(MediaStore.MediaColumns.DISPLAY_NAME, apkName)
values.put(MediaStore.MediaColumns.RELATIVE_PATH, getAppDownloadPath())
val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
uri?.also {
val outputStream = contentResolver.openOutputStream(uri) ?: return@thread
val bos = BufferedOutputStream(outputStream)
val buffer = ByteArray(1024)
var bytes = bis.read(buffer)
while (bytes >= 0) {
bos.write(buffer, 0, bytes)
bos.flush()
bytes = bis.read(buffer)
}
bos.close()
runOnUiThread {
installAPK(uri)
}
}
bis.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun installAPK(uri: Uri) {
val intent = Intent(Intent.ACTION_VIEW)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.setDataAndType(uri, "application/vnd.android.package-archive")
startActivity(intent)
}
由于我們獲取到的這個uri本來就是content://
開頭,所以不需要使用FileProvider
。對于應(yīng)用私有目錄的文件,我們可以使用FileProvider
進(jìn)行分享。
項目地址
本文的所有代碼都可以在項目中查看。傳送門