日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

作者:連續三屆村草

Android 10(Q)/11(R) 分區存儲適配

大部分應用都會請求 ( READ_EXTERNAL_STORAGE ) ( WRITE_EXTERNAL_STORAGE ) 存儲權限,來做一些諸如在 SD 卡中存儲文件或者讀取多媒體文件等常規操作。這些應用可能會在磁盤中存儲大量文件,即使應用被卸載了還會依然存在。另外,這些應用還可能會讀取其他應用的一些敏感文件數據。

為此,google 終于下定決心在 Android 10 中引入了分區存儲,對權限進行場景的細分,按需索取,并在 Android 11 中進行了進一步的調整。

Android 存儲分區情況

Android 中存儲可以分為兩大類:私有存儲和共享存儲

  • 私有存儲 (Private Storage) : 每個應用在都擁有自己的私有目錄,其它應用看不到,彼此也無法訪問到該目錄:內部存儲私有目錄 (/data/data/packageName) ;外部存儲私有目錄 (/sdcard/Android/data/packageName),
  • 共享存儲 (Shared Storage) : 存儲其他應用可訪問文件, 包含媒體文件、文檔文件以及其他文件,對應設備DCIM、Pictures、Alarms、Music、Notifications、Podcasts、Ringtones、Movies、Download等目錄。

Android 10(Q) :

Android 10 中主要對共享目錄進行了權限詳細的劃分,不再能通過絕對路徑訪問。

受影響的接口:

Android 10(Q)/11(R) 分區存儲適配

 

訪問不同分區的方式:

  1. 私有目錄:和以前的版本一致,可通過 File() API 訪問,無需申請權限。
  2. 共享目錄:需要通過MediaStore和Storage Access Framework API 訪問,視具體情況申請權限,下面詳細介紹。

其中,對共享目錄的權限進行了細分:

  1. 無需申請權限的操作:
    通過 MediaStore API對媒體集、文件集進行媒體/文件的添加、對 自身App 創建的 媒體/文件 進行查詢、修改、刪除的操作。
  2. 需要申請READ_EXTERNAL_STORAGE 權限:
    通過 MediaStore API對所有的媒體集進行查詢、修改、刪除的操作。
  3. 調用 Storage Access Framework API :
    會啟動系統的文件選擇器向用戶申請操作指定的文件

新的訪問方式:

Android 10(Q)/11(R) 分區存儲適配

 

Android 11 (R):

Android 11 (R) 在 Android 10 (Q) 中分區存儲的基礎上進行了調整

1. 新增執行批量操作

為實現各種設備之間的一致性并增加用戶便利性,Android 11 向 MediaStore API 中添加了多種方法。對于希望簡化特定媒體文件更改流程(例如在原位置編輯照片)的應用而言,這些方法尤為有用。

MediaStore API 新增的方法

Android 10(Q)/11(R) 分區存儲適配

 

系統在調用以上任何一個方法后,會構建一個 PendingIntent 對象。應用調用此 intent 后,用戶會看到一個對話框,請求用戶同意應用更新或刪除指定的媒體文件。

2. 使用直接文件路徑和原生庫訪問文件

為了幫助您的應用更順暢地使用第三方媒體庫,Android 11 允許您使用除 MediaStore API 之外的 API 訪問共享存儲空間中的媒體文件。不過,您也可以轉而選擇使用以下任一 API 直接訪問媒體文件:

File API。
原生庫,例如 fopen()。

簡單來說就是,可以通過 File() 等API 訪問有權限訪問的媒體集了。

性能:

通過 File () 等直接通過路徑訪問的 API 實際上也會映射為MediaStore API 。
按文件路徑順序讀取的時候性能相當;隨機讀取和寫入的時候則會更慢,所以還是推薦直接使用 MediaStoreAPI。

3. 新增權限

MANAGE_EXTERNAL_STORAGE : 類似以前的 READ_EXTERNAL_STORAGE + WRITE_EXTERNAL_STORAGE ,除了應用專有目錄都可以訪問。

應用可通過執行以下操作向用戶請求名為所有文件訪問權限的特殊應用訪問權限:

  1. 在清單中聲明 MANAGE_EXTERNAL_STORAGE 權限。
  2. 使用 ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION intent 操作將用戶引導至一個系統設置頁面,在該頁面上,用戶可以為您的應用啟用以下選項:授予所有文件的管理權限。
  • 在 Google Play 上架的話,需要提交使用此權限的說明,只有指定的幾種類型的 APP 才能使用。

Sample

  • 使用 MediaStore 增刪改查媒體集
  • 使用 Storage Access Framework 訪問文件集

1. 媒體集

1) 查詢媒體集(需要 READ_EXTERNAL_STORAGE 權限)

實際上 MediaStore 是以前就有的 API ,不同的是過去主要通過 MediaStore.Video.Media._DATA這個 colum 請求原始數據,可以得到絕對Uri ,現在需要請求MediaStore.Video.Media._ID來得到相對Uri再進行處理。

// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your
// app didn't create.

// Container for information about each video.
data class Video(
    val uri: Uri,
    val name: String,
    val duration: Int,
    val size: Int
)
val videoList = mutableListOf<Video>()

val projection = arrayOf(
    MediaStore.Video.Media._ID,
    MediaStore.Video.Media.DISPLAY_NAME,
    MediaStore.Video.Media.DURATION,
    MediaStore.Video.Media.SIZE
)

// Show only videos that are at least 5 minutes in duration.
val selection = "${MediaStore.Video.Media.DURATION} >= ?"
val selectionArgs = arrayOf(
    TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES).toString()
)

// Display videos in alphabetical order based on their display name.
val sortOrder = "${MediaStore.Video.Media.DISPLAY_NAME} ASC"

val query = ContentResolver.query(
    MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    sortOrder
)
query?.use { cursor ->
    // Cache column indices.
    val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
    val nameColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
    val durationColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION)
    val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)

    while (cursor.moveToNext()) {
        // Get values of columns for a given video.
        val id = cursor.getLong(idColumn)
        val name = cursor.getString(nameColumn)
        val duration = cursor.getInt(durationColumn)
        val size = cursor.getInt(sizeColumn)

        val contentUri: Uri = ContentUris.withAppendedId(
            MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
            id
        )

        // Stores column values and the contentUri in a local object
        // that represents the media file.
        videoList += Video(contentUri, name, duration, size)
    }
}

2)插入媒體集(無需權限)

// Add a media item that other apps shouldn't see until the item is
// fully written to the media store.
val resolver = applicationContext.contentResolver

// Find all audio files on the primary external storage device.
// On API <= 28, use VOLUME_EXTERNAL instead.
val audioCollection = MediaStore.Audio.Media
        .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)

val songDetails = ContentValues().apply {
    put(MediaStore.Audio.Media.DISPLAY_NAME, "My Workout Playlist.mp3")
    put(MediaStore.Audio.Media.IS_PENDING, 1)
}

val songContentUri = resolver.insert(audioCollection, songDetails)

resolver.openFileDescriptor(songContentUri, "w", null).use { pfd ->
    // Write data into the pending audio file.
}

// Now that we're finished, release the "pending" status, and allow other apps
// to play the audio track.
songDetails.clear()
songDetails.put(MediaStore.Audio.Media.IS_PENDING, 0)
resolver.update(songContentUri, songDetails, null, null)

3)更新自己創建的媒體集(無需權限)

刪除類似

// Updates an existing media item.
val mediaId = // MediaStore.Audio.Media._ID of item to update.
val resolver = applicationContext.contentResolver

// When performing a single item update, prefer using the ID
val selection = "${MediaStore.Audio.Media._ID} = ?"

// By using selection + args we protect against improper escaping of // values.
val selectionArgs = arrayOf(mediaId.toString())

// Update an existing song.
val updatedSongDetails = ContentValues().apply {
    put(MediaStore.Audio.Media.DISPLAY_NAME, "My Favorite Song.mp3")
}

// Use the individual song's URI to represent the collection that's
// updated.
val numSongsUpdated = resolver.update(
        myFavoriteSongUri,
        updatedSongDetails,
        selection,
        selectionArgs)

4)更新/刪除其它媒體創建的媒體集

若已經開啟分區存儲則會拋出 RecoverableSecurityException,捕獲并通過SAF請求權限

// Apply a grayscale filter to the image at the given content URI.
try {
    contentResolver.openFileDescriptor(image-content-uri, "w")?.use {
        setGrayscaleFilter(it)
    }
} catch (securityException: SecurityException) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        val recoverableSecurityException = securityException as?
            RecoverableSecurityException ?:
            throw RuntimeException(securityException.message, securityException)

        val intentSender =
            recoverableSecurityException.userAction.actionIntent.intentSender
        intentSender?.let {
            startIntentSenderForResult(intentSender, image-request-code,
                    null, 0, 0, 0, null)
        }
    } else {
        throw RuntimeException(securityException.message, securityException)
    }
}

2. 文件集 (通過 SAF)

1)創建文檔

注:創建操作若重名的話不會覆蓋原文檔,會添加 (1) 最為后綴,如 document.pdf -> document(1).pdf

// Request code for creating a PDF document.
const val CREATE_FILE = 1

private fun createFile(pickerInitialUri: Uri) {
    val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "application/pdf"
        putExtra(Intent.EXTRA_TITLE, "invoice.pdf")

        // Optionally, specify a URI for the directory that should be opened in
        // the system file picker before your app creates the document.
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }
    startActivityForResult(intent, CREATE_FILE)
}

2)打開文檔

建議使用 type 設置 MIME 類型

// Request code for selecting a PDF document.
const val PICK_PDF_FILE = 2

fun openFile(pickerInitialUri: uri) {
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "application/pdf"

        // Optionally, specify a URI for the file that should appear in the
        // system file picker when it loads.
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }

    startActivityForResult(intent, PICK_PDF_FILE)
}

3)授予對目錄內容的訪問權限

用戶選擇目錄后,可訪問該目錄下的所有內容

Android 11 中無法訪問 Downloads

fun openDirectory(pickerInitialUri: Uri) {
    // Choose a directory using the system's file picker.
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
        // Provide read access to files and sub-directories in the user-selected
        // directory.
        flags = Intent.FLAG_GRANT_READ_URI_PERMISSION

        // Optionally, specify a URI for the directory that should be opened in
        // the system file picker when it loads.
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }

    startActivityForResult(intent, your-request-code)
}

4)永久獲取目錄訪問權限

上面提到的授權是臨時性的,重啟后則會失效??梢酝ㄟ^下面的方法獲取相應目錄永久性的權限

val contentResolver = applicationContext.contentResolver

val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
        Intent.FLAG_GRANT_WRITE_URI_PERMISSION
// Check for the freshest data.
contentResolver.takePersistableUriPermission(uri, takeFlags)

5)SAF API 響應

SAF API 調用后都是通過 onActivityResult來相應動作

override fun onActivityResult(
        requestCode: Int, resultCode: Int, resultData: Intent?) {
    if (requestCode == your-request-code
            && resultCode == Activity.RESULT_OK) {
        // The result data contains a URI for the document or directory that
        // the user selected.
        resultData?.data?.also { uri ->
            // Perform operations on the document using its URI.
        }
    }
}

6) 其它操作

除了上面的操作之外,對文檔其它的復制、移動等操作都是通過設置不同的 FLAG 來實現,見 Document.COLUMN_FLAGS

3. 批量操作媒體集

構建一個媒體集的寫入操作 createWriteRequest()

val urisToModify = /* A collection of content URIs to modify. */
val editPendingIntent = MediaStore.createWriteRequest(contentResolver,
        urisToModify)

// Launch a system prompt requesting user permission for the operation.
startIntentSenderForResult(editPendingIntent.intentSender, EDIT_REQUEST_CODE,
    null, 0, 0, 0)

//相應
override fun onActivityResult(requestCode: Int, resultCode: Int,
                 data: Intent?) {
    ...
    when (requestCode) {
        EDIT_REQUEST_CODE ->
            if (resultCode == Activity.RESULT_OK) {
                /* Edit request granted; proceed. */
            } else {
                /* Edit request not granted; explain to the user. */
            }
    }
}

createFavoriteRequest() createTrashRequest() createDeleteRequest() 同理

Android 10(Q)/11(R) 分區存儲適配

 

適配和兼容

在 targetSDK = 29 APP 中,在 AndroidManifes 設置 requestLegacyExternalStorage="true" 啟用兼容模式,以傳統分區模式運行。

   <manifest ... >
      <!-- This attribute is "false" by default on apps targeting
           Android 10 or higher. -->
      <application android:requestLegacyExternalStorage="true" ... >
        ...
      </application>
    </manifest>

注意:如果某個應用在安裝時啟用了傳統外部存儲,則該應用會保持此模式,直到卸載為止。無論設備后續是否升級為搭載 Android 10 或更高版本,或者應用后續是否更新為以 Android 10 或更高版本為目標平臺,此兼容性行為均適用。

意思就是在新系統新安裝的應用才會啟用,覆蓋安裝會保持傳統分區模式,例如:

  • 系統通過 OTA 升級到 Android 10/11
  • 應用通過更新升級到 targetSdkVersion >= 29

補充

Q:之前討論過一些問題,APP 無需權限可以訪問自己創建的媒體,那么系統如何進行判斷?

A:創建媒體時系統會給媒體打上 packageName tag,應用被卸載則會清除 tag ,所以不會存在使用同樣 packageName 進行欺騙的情況。

Q:我可以在媒體集文件夾下創建文檔,就可以避開權限的問題了?

A:官方文檔上寫了只能創建相應類型的媒體/文件,具體如何限制的,沒有說明。

總結

從 Android 10提出分區存儲之后到現在已經一年多了,所以Google 從強制推行的態度到現在 targetSDK >=30 才強制啟用分區存儲來看,Google 還是漸漸地選擇給開發者留更多的時間。缺點當然是不強制啟用的話,國內 APP 適配進度估計得延后了。不過好消息是在查資料的時候,看到了國內大廠的相關適配文章,至少說明大廠在跟進了。

去年(19年)的文檔描述是無論 targetSDK 多少,明年(20年)高版本強制啟用。

Android 10(Q)/11(R) 分區存儲適配

 

今年(20)文檔描述是 targetSDK >=30 才強制啟用

Android 10(Q)/11(R) 分區存儲適配

 

關于適配的難度:

對絕對路徑相關接口依賴比較深的 APP 適配還是改動挺多的;其次權限的劃分很細,什么時候需要什么權限以及調用哪個接口,理解起來需要一定時間;MediaStore API SAF API 這類接口以前就設計好了,我也覺得也不算特別友好;最后測試也需要重新進行。

所以雖然明年才會強制執行分區存儲,但還是建議盡早理解和 review 項目中需要適配的代碼。

文末附上大廠學長給我的資料,內容包含:Android學習PDF+架構視頻+面試文檔+源碼筆記高級架構技術進階腦圖、Android開發面試專題資料,高級進階架構資料 這幾塊的內容

這些都是我現在閑暇還會反復翻閱的精品資料。里面對近幾年的大廠面試高頻知識點都有詳細的講解。相信可以有效的幫助大家掌握知識、理解原理。

分享給大家,非常適合近期有面試和想在技術道路上繼續精進的朋友。也是希望可以幫助到大家提升進階

分享到:
標簽:Android
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定