AndroidQ Compatibility Adaptation Guide

AndroidQ

android10.jpeg

Privacy changes in Android 10

Privacy Changes Affected apps mitigation strategy
Partitioned storage Filtered views for external storage, providing access to app-specific collections of files and media Apps that access and share files in external storage Use app-specific catalogs and media collection catalogs Learn more
Enhanced user control over location permissions Foreground-only permissions give users more control over app access to device location information Apps that request access to the user's location while in the background Ensures graceful degradation without background location updates Uses permissions introduced in Android 10 to get location in the background Learn
The system executes background activities and implements restrictions on starting activities from the background An application that starts an Activity without user interaction Use the Activity triggered by the notification to learn more
Non-resettable hardware identifiers Enforce restrictions on access to device serial number and IMEI Apps to access device serial number or IMEI Use an identifier that the user can reset Learn more
Wi-Fi Scanning Permissions Access to certain Wi-Fi, Wi-Fi Sense, and Bluetooth scanning methods requires precise location permission Applications using WLAN API and Bluetooth API Request ACCESS_FINE_LOCATIONpermissions Learn more

The above is the link to the AndroidQ privacy change on the official website. This article only explains some major privacy changes.

Limitations of launching an Activity from the background

Create high priority notifications

In Android 10, when the app has no activity displayed in the foreground, its startup activity will be intercepted by the system, resulting in invalid startup.
The official compromise solution for this is to use full screen Intent(full-screen intent), which is to add settings when creating notification bar notifications. full-screen intent The sample code is as follows (modified based on official documents):

		Intent fullScreenIntent = new Intent(this, CallActivity.class);
    PendingIntent fullScreenPendingIntent = PendingIntent.getActivity(this, 0,
            fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);

    NotificationCompat.Builder notificationBuilder =
            new NotificationCompat.Builder(this, CHANNEL_ID)
        .setSmallIcon(R.drawable.notification_icon)
        .setContentTitle("Incoming call")
        .setContentText("(919) 555-1234")
        .setPriority(NotificationCompat.PRIORITY_HIGH)
        .setCategory(NotificationCompat.CATEGORY_CALL)

        // Use a full-screen intent only for the highest-priority alerts where you
        // have an associated activity that you would like to launch after the user
        // interacts with the notification. Also, if your app targets Android 10
        // or higher, you need to request the USE_FULL_SCREEN_INTENT permission in
        // order for the platform to invoke this notification.
        .setFullScreenIntent(fullScreenPendingIntent, true);

    Notification incomingCallNotification = notificationBuilder.build();

Note: When the Target SDk is 29 or above, you need to add USE_FULL_SCREEN_INTENT to the AndroidManifest

//AndroidManifest中
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />

When the mobile phone is in the bright state, a notification bar will be displayed, and when the mobile phone is in the locked or off screen state, the screen will be brightened and directly entered into the CallActivity.

Non-resettable device identifiers enforce restrictions

Starting with Android 10, apps must have READ_PRIVILEGED_PHONE_STATEprivileged permissions to access the device's non-resettable identifiers (including IMEI and serial number).

Affected methods include:

ANDROID_ID generation rule: signature + device information + device user
ANDROID_ID reset rule: ANDROID_ID will be reset when the device is restored to factory settings

The current way to obtain the unique ID of the device is to use ANDROID_ID. If it is empty, use UUID.randomUUID().toString() to obtain a random ID and store it. The ID is guaranteed to be unique, but it will change after the App is uninstalled and reinstalled.

String id = android.provider.Settings.Secure.getString(context.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);

Restricted access to clipboard data

Unless your app is the default input method (IME) or is currently in focus, it cannot access clipboard data on Android 10 or higher platforms.

Because the clipboard data is obtained when the application is in the foreground, it will not affect most businesses.

location permissions

Android QIntroduced a new location permission ACCESS_BACKGROUND_LOCATIONthat only affects an app's access to location information while it's running in the background. If you apply targetSDK<=P, request ACCESS_FINE_LOCATIONor ACCESS_COARSE_LOCATIONpermission, AndroidQthe device will automatically apply for ACCESS_BACKGROUND_LOCATIONpermission for you.
If your app Android 10targets a platform version 2.0 or higher, you must declare permissions in your app's manifest file ACCESS_BACKGROUND_LOCATIONand receive user permissions to receive periodic location updates while your app is in the background.
The following code snippet shows how to request background location access in your app:

    <manifest ... >
        <!--允许获得精确的GPS定位-->
        <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
        <!--允许获得粗略的基站网络定位-->
        <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
        <!-- 兼容10.0系统,允许App在后台获得位置信息 -->
        <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
    </manifest>

An example of targeting permission checking logic is shown in the following code snippet:

    boolean permissionAccessCoarseLocationApproved =
        ActivityCompat.checkSelfPermission(this, permission.ACCESS_COARSE_LOCATION)
            == PackageManager.PERMISSION_GRANTED;
 
    if (permissionAccessCoarseLocationApproved) {
    
    
       boolean backgroundLocationPermissionApproved =
               ActivityCompat.checkSelfPermission(this,
                   permission.ACCESS_BACKGROUND_LOCATION)
                   == PackageManager.PERMISSION_GRANTED;
 
       if (backgroundLocationPermissionApproved) {
    
    
           // App can access location both in the foreground and in the background.
           // Start your service that doesn't have a foreground service type
           // defined.
       } else {
    
    
           // App can only access location in the foreground. Display a dialog
           // warning the user that your app must have all-the-time access to
           // location in order to function properly. Then, request background
           // location.
           ActivityCompat.requestPermissions(this, new String[] {
    
    
               Manifest.permission.ACCESS_BACKGROUND_LOCATION},
               your-permission-request-code);
       }
    } else {
    
    
       // App doesn't have access to the device's location at all. Make full request
       // for permission.
       ActivityCompat.requestPermissions(this, new String[] {
    
    
            Manifest.permission.ACCESS_COARSE_LOCATION,
            Manifest.permission.ACCESS_BACKGROUND_LOCATION
            },
            your-permission-request-code);
    }

If your app typically needs to access the device's location after being placed in the background, such as when the user presses the device's Home button or turns off the device's display.

To preserve access to device location information in this specific type of use case, start a foreground service that you have declared in your app's manifest with a foreground service type of "location":

    <service
        android:name="MyNavigationService"
        android:foregroundServiceType="location" ... >
        ...
    </service>

Before starting this foreground service, make sure your app still has access to the device's location:

    boolean permissionAccessCoarseLocationApproved =
        ActivityCompat.checkSelfPermission(this,
            permission.ACCESS_COARSE_LOCATION) ==
            PackageManager.PERMISSION_GRANTED;
    if (permissionAccessCoarseLocationApproved) {
    
    
        // App has permission to access location in the foreground. Start your
        // foreground service that has a foreground service type of "location".
    } else {
    
    
       // Make a request for foreground-only location access.
       ActivityCompat.requestPermissions(this, new String[] {
    
    
            Manifest.permission.ACCESS_COARSE_LOCATION},
           your-permission-request-code);
    }

Partition storage

In order to allow users to better control their own files and limit file clutter, Android Q has modified the method for APP to access files in external storage. The new feature of external storage is called Scoped Storage.

Android Q still uses runtime permissions READ_EXTERNAL_STORAGErelated WRITE_EXTERNAL_STORAGEto user-facing storage, but access to external storage is now limited even when those permissions are obtained.

The scenarios in which apps need these runtime permissions have changed, and the visibility of external storage to apps in various cases has also changed.

In Scoped Storagethe new feature, the external storage space is divided into two parts:

● Public directories: Downloads, Documents, Pictures, DCIM, Movies, Music, Ringtonesetc.

The files in the public directory will not be deleted after the APP is uninstalled.

The APP can access the files in it through the interface SAF(System Access Framework).MediaStore

App-specificDirectory: store application private data, the external storage application private directory corresponds to Android/data/packagename, and the internal storage application private directory corresponds to data/data/packagename;

After the APP is uninstalled, the data will be cleared.

APP's private directory, APP does not need any permission to access its own App-specificdirectory.

ScopedStorage.png

storage view mode

Android Q stipulates that APP has two external storage space view modes: Legacy View, Filtered View.

Filtered View: App can directly access App-specificthe directory, but cannot directly access App-specificexternal files. Access to the public directory or the directory of other APPs App-specificcan only be accessed through MediaStore, , or SAFprovided by other APPs .ContentProviderFileProvider

Legacy View: compatibility mode. Same as Android Q before, App can access external storage after applying for permission, and has full access permission

requestLegacyExternalStorage和preserveLegacyExternalStorage

requestLegacyExternalStorageIt was introduced by Android 10. If you upgrade and install the app after adapting to Android 10, the previous storage mode will still be used. The Legacy Viewnew mode can only be enabled by installing for the first time or uninstalling and reinstalling Filtered View.

And android:requestLegacyExternalStorage="true"let the app adapted to Android 10 continue to access the old storage model when it is newly installed on the Android 10 system.

Environment.isExternalStorageLegacy();//存储是否为兼容模式

When adapting to Android 11, requestLegacyExternalStoragethe tag will be ignored on devices above Android 11, preserveLegacyExternalStoragejust to allow the overlay installed app to continue to use the old storage model, if it was the old storage model before.

  • When Android10 is adapted, you can requestLegacyExternalStoragecuse the compatibility mode;
  • Android 11 adaptation can be made by preserveLegacyExternalStoragemaking Android 10 and below devices use the compatibility mode, but Android 11 and above devices cannot use the compatibility mode whether it is overwritten installation or reinstallation;

The status of the volume can be Environment.getExternalStorageState()queried . If the returned status is MEDIA_MOUNTED, then you can read and write app-specific files in external storage. You can only read these files if the returned status is MEDIA_MOUNTED_READ_ONLY.

Impact of Partitioned Storage

Image location information

Some pictures will contain location information, because the location is sensitive information for the user, and the Android 10 application cannot obtain the image location information by default in the partition storage mode. The application can obtain the image location information through the following two settings:

  • manifestApply in ACCESS_MEDIA_LOCATION;
  • Call MediaStore.setRequireOriginal(Uri uri)the interface to update the picture Uri;
// Get location data from the ExifInterface class.
val photoUri = MediaStore.setRequireOriginal(photoUri)
contentResolver.openInputStream(photoUri).use {
    
     stream ->
    ExifInterface(stream).run {
    
    
        // If lat/long is null, fall back to the coordinates (0, 0).
        val latLong = ?: doubleArrayOf(0.0, 0.0)
    }
}

access data

Private directory:

The file access method of the app's private directory is consistent with the previous Android version, and resources can be obtained through the File path.

Shared directory:

Shared directory files need MediaStore APIto Storage Access Frameworkbe accessed through or .
MediaStore APITo create files in the specified directory of the shared directory or to access the files created by the application itself, there is no need to apply for storage permissions. To access
MediaStore APImedia files (pictures, audio, and videos) created by other applications in the shared directory, you need to apply for storage permissions. If you do not apply for storage permissions, you can ContentResolverquery If the Uri of the file is not found, even if the Uri of the file is obtained by other means, an exception will be thrown when reading or creating the file;
MediaStore APInon-media files (pdf, office, doc, txt, etc.) created by other applications cannot be accessed, and can only be accessed through Storage Access Frameworkthe method ;

File path to access the affected interface

FileOutputStreamandFileInputStream

Under the partition storage model, the public directory of the SD card is not allowed to be accessed, except for the folders of the shared media. Therefore, instantiating with a path to a public directory FileOutputStreammay FileInputStreamcause 报FileNotFoundExceptionexceptions.

W/System.err: java.io.FileNotFoundException: /storage/emulated/0/Log01-28-18-10.txt: open failed: EACCES (Permission denied)
W/System.err:     at libcore.io.IoBridge.open(IoBridge.java:496)
W/System.err:     at java.io.FileInputStream.<init>(FileInputStream.java:159)

File.createNewFile

W/System.err: java.io.IOException: Permission denied
W/System.err:     at java.io.UnixFileSystem.createFileExclusively0(Native Method)
W/System.err:     at java.io.UnixFileSystem.createFileExclusively(UnixFileSystem.java:317)
W/System.err:     at java.io.File.createNewFile(File.java:1008)
  • File.renameTo
  • File.delete
  • File.renameTo
  • File.mkdir
  • File.mkdirs

The above Filemethods all return false.

BitmapFactory.decodeFileGenerated Bitmapas null.

Adaptation guide

Android Q Scoped StorageGoogle's official adaptation document for new features: https://developer.android.google.cn/preview/privacy/scoped-storage

The adaptation guide is as follows, divided into: accessing the app's own App-specific directory files, using MediaStore to access public directories, using SAF to access specified files and directories, sharing files in the App-specific directory and other detailed adaptations.

Access to App-specific directory files

Files in the App-specific directory can be directly manipulated through File without any permission.

App-specific directory Interfaces (all storage devices) Interface (Primary External Storage)
Media getExternalMediaDirs() THAT
Obv getObbDirs() getObbDir()
Cache getExternalCacheDirs() getExternalCacheDir()
Data getExternalFilesDirs(String type) getExternalFilesDir(String type)
/**
 * 在App-Specific目录下创建文件
 * 文件目录:/Android/data/包名/files/Documents/
 */
private fun createAppSpecificFile() {
    
    
    binding.createAppSpecificFileBtn.setOnClickListener {
    
    
        val documents = getExternalFilesDirs(Environment.DIRECTORY_DOCUMENTS)
        if (documents.isNotEmpty()) {
    
    
            val dir = documents[0]
            var os: FileOutputStream? = null
            try {
    
    
                val newFile = File(dir.absolutePath, "MyDocument")
                os = FileOutputStream(newFile)
                os.write("create a file".toByteArray(Charsets.UTF_8))
                os.flush()
                Log.d(TAG, "创建成功")
                dir.listFiles()?.forEach {
    
     file: File? ->
                    if (file != null) {
    
    
                        Log.d(TAG, "Documents 目录下的文件名:" + file.name)
                    }
                }
            } catch (e: IOException) {
    
    
                e.printStackTrace()
                Log.d(TAG, "创建失败")

            } finally {
    
    
                closeIO(os)
            }

        }
    }
}
/**
 * 在App-Specific目录下创建文件夹
 * 文件目录:/Android/data/包名/files/
 */
private fun createAppSpecificFolder() {
    
    
    binding.createAppSpecificFolderBtn.setOnClickListener {
    
    
        getExternalFilesDir("apk")?.let {
    
    
            if (it.exists()) {
    
    
                Log.d(TAG, "创建成功")
            } else {
    
    
                Log.d(TAG, "创建失败")
            }
        }
    }
}

Use MediaStore to access the public directory

MediaStore Uri and Path Correspondence Table

Uripath.png

MediaStore provides the following Uri, and MediaProvider can be used to query the corresponding Uri data. On AndroidQ, all external storage devices will be commanded, namely Volume Name. MediaStore can obtain the corresponding Uri through Volume Name.

MediaStore.getExternalVolumeNames(this).forEach {
    
     volumeName ->
  Log.d(TAG, "uri:${
      
      MediaStore.Images.Media.getContentUri(volumeName)}")
}

Uri path format:content:// media/<volumeName>/<Uri路径>

Uriminetype.png

Create files using MediaStore

Through the insert method of ContentResolver, multimedia files are saved in the public collection directory. Different Uri correspond to different public directories, see 3.2.1 for details; the first-level directory of RELATIVE_PATH must be the first-level directory corresponding to Uri, the second-level directory or the second Directories above level can be created and specified at will.

private lateinit var createBitmapForActivityResult: ActivityResultLauncher<String>
//注册ActivityResultLauncher
createBitmapForActivityResult =
    registerForActivityResult(ActivityResultContracts.RequestPermission()) {
    
    
        createBitmap()
    }
binding.createFileByMediaStoreBtn.setOnClickListener {
    
    
    createBitmapForActivityResult.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
private fun createBitmap() {
    
    
    val values = ContentValues()
    val displayName = "NewImage.png"
    values.put(MediaStore.Images.Media.DISPLAY_NAME, displayName)
    values.put(MediaStore.Images.Media.DESCRIPTION, "This is an image")
    values.put(MediaStore.Images.Media.MIME_TYPE, "image/png")
    values.put(MediaStore.Images.Media.TITLE, "Image.png")
    //适配AndroidQ及一下
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    
    
        values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/sl")
    } else {
    
    
        values.put(
            MediaStore.MediaColumns.DATA,
            "${
      
      Environment.getExternalStorageDirectory().path}/${
      
      Environment.DIRECTORY_DCIM}/$displayName"
        )
    }
    //requires android.permission.WRITE_EXTERNAL_STORAGE, or grantUriPermission()
    val external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    //java.lang.UnsupportedOperationException: Writing to internal storage is not supported.
    //val external = MediaStore.Images.Media.INTERNAL_CONTENT_URI
    val insertUri = contentResolver.insert(external, values)
    var os: OutputStream? = null
    try {
    
    
        if (insertUri != null) {
    
    
            os = contentResolver.openOutputStream(insertUri)
        }
        if (os != null) {
    
    
            val bitmap = Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888)
            //创建了一个红色的图片
            val canvas = Canvas(bitmap)
            canvas.drawColor(Color.RED)
            bitmap.compress(Bitmap.CompressFormat.PNG, 90, os)
            Log.d(TAG, "创建Bitmap成功")
            if (insertUri != null) {
    
    
                values.clear()
                //适配AndroidQ及一下
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    
    
                    values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/sl2")
                } else {
    
    
                    values.put(
                        MediaStore.MediaColumns.DATA,
                        "${
      
      Environment.getExternalStorageDirectory().path}/${
      
      Environment.DIRECTORY_DCIM}/$displayName"
                    )
                }
                contentResolver.update(insertUri, values, null, null)
            }
        }
    } catch (e: IOException) {
    
    
        Log.d(TAG, "创建失败:${
      
      e.message}")
    } finally {
    
    
        closeIO(os)
    }
}
Query files using MediaStore

via Cursor query(@RequiresPermission.Read @NonNull Uri uri,@Nullable String[] projection, @Nullable String selection,@Nullable String[] selectionArgs, @Nullable String sortOrder)the method .

Parameter explanation:

parameter type paraphrase
uri Uri Provide the Uri to retrieve the content, its scheme iscontent://
projection String[] The returned columns, if null is passed then all columns are returned (inefficient)
selection String Filter condition, that is, WHEREthe statement (but you don't need to write whereitself), if you pass null, all the data will be returned
selectionArgs String[] If you add in the parameter of selection ?, it will be replaced by the data in this field in order
sortOrder String Used to sort the data, that is, in the SQL statement ORDER BY(you don’t need to write ORDER BYitself), if you pass null, it will be sorted according to the default order (maybe unordered)

ContentResolver.queryQuery files through the interface Uri, and query files created by other apps requires READ_EXTERNAL_STORAGEpermission;

This query uses the database query of the mobile phone system, there may be some picture files that exist but still cannot be queried~! (PS: The pictures pushed by the adb command cannot be queried)

/**
 * 通过MediaStore查询文件
 */
private fun queryFileByMediaStore() {
    
    
  queryPictureForActivityResult = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
    
    
      queryUri = queryImageUri("yellow.jpg")
  }
  binding.queryFileByMediaStoreBtn.setOnClickListener {
    
    
      queryPictureForActivityResult.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
  }
}
/**
  * @param displayName 查询的图片文件名称
  * @return 第一个遍历到的该文件名的uri
  */
private fun queryImageUri(displayName: String): Uri? {
    
    
    val external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    val selection = "${
      
      MediaStore.Images.Media.DISPLAY_NAME}=?"
    val args = arrayOf(displayName)
    val projection = arrayOf(MediaStore.Images.Media._ID)
    val cursor = contentResolver.query(external, projection, selection, args, null)
    var queryUri: Uri? = null
    if (cursor != null) {
    
    
        //可能查询到多个同名图片
        while (cursor.moveToNext()) {
    
    
            queryUri = ContentUris.withAppendedId(external, cursor.getLong(0))
            Log.d(TAG, "查询成功,Uri路径$queryUri")
            queryUri?.let {
    
    
                cursor.close()
                return it
            }
        }
        cursor.close()
    }
    return queryUri;
}
Read files using MediaStore

First of all, the file storage permission is required. After ContentResolver.querythe query is obtained Uri, you can contentResolver.openFileDescriptorselect the corresponding opening method according to the file descriptor. "r" means read, "w" means write;

private lateinit var readPictureForActivityResult: ActivityResultLauncher<IntentSenderRequest>
/**
 * 根据查询到的uri,获取bitmap
 */
private fun readFileByMediaStore() {
    
    
    readPictureForActivityResult = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
    
    
        readBitmapNotException()
    }
    binding.readFileByMediaStoreBtn.setOnClickListener {
    
    
        readBitmapNotException()
    }
}
private fun readBitmapNotException() {
    
    
    val queryUri = queryImageUri("20221018_113937.jpg")
    if (queryUri != null) {
    
    
        var pfd: ParcelFileDescriptor? = null
        try {
    
    
            pfd = contentResolver.openFileDescriptor(queryUri, "r")
            if (pfd != null) {
    
    
                // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
                val options = BitmapFactory.Options()
                options.inJustDecodeBounds = true
                BitmapFactory.decodeFileDescriptor(pfd.fileDescriptor, null, options)
                // 调用上面定义的方法计算inSampleSize值
                options.inSampleSize = calculateInSampleSize(options, 500, 500)
                // 使用获取到的inSampleSize值再次解析图片
                options.inJustDecodeBounds = false
                val bitmap = BitmapFactory.decodeFileDescriptor(pfd.fileDescriptor, null, options)
                binding.imageIv.setImageBitmap(bitmap)
            }
        } catch (e: Exception) {
    
    

            e.printStackTrace()
        } finally {
    
    
            closeIO(pfd)
        }

    } else {
    
    
        Log.d(TAG, "还未查询到Uri")
    }
}

Get a thumbnail of an image:

Access Thumbnail, and ContentResolver.loadThumbnailreturn a thumbnail of the specified size by passing in size.

/**
  * 根据查询到的Uri,获取Thumbnail
  */
private fun loadThumbnail() {
    
    
    binding.loadThumbnailBtn.setOnClickListener {
    
    
        queryUri?.let {
    
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    
    
                val bitmap = contentResolver.loadThumbnail(it, Size(100, 200), null)
                binding.imageIv.setImageBitmap(bitmap)
            } else {
    
    
                MediaStore.Images.Thumbnails.getThumbnail(
                    contentResolver,
                    ContentUris.parseId(it),
                    MediaStore.Images.Thumbnails.MINI_KIND,
                    null)?.let {
    
     bitmap ->
                    binding.imageIv.setImageBitmap(bitmap)
                }
            }
        }
    }
}
Modify files using MediaStore

PS: Only for AndroidQ and above system versions, there are data and file out-of-sync problems, as well as out-of-sync problems between thumbnails and original images when using low-version domestic mobile phones for data update;ContentResolver

After the application has WRITE_EXTERNAL_STORAGEthe permission , another Exception will be thrown when modifying the files of other apps:

android.app.RecoverableSecurityException: com.tzx.androidsystemversionadapter has no access to content://media/external/images/media/21

If we RecoverableSecurityException hold Catchand apply to the user for permission to modify the picture, after the user operates, we can get the result in onActivityResultthe callback operate.

impower.png

/**
  * 根据查询得到的Uri,修改文件
  */
private fun updateFileByMediaStore() {
    
    
  updatePictureForActivityResult =
      registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
    
    
          updateFileNameWithException()
      }
  registerForActivityResult =
      registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
    
    
          updateFileNameWithException()
      }
  binding.updateFileByMediaStoreBtn.setOnClickListener {
    
    
      registerForActivityResult.launch(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE))
  }
}
private fun updateFileNameWithException() {
    
    
  val queryUri = queryImageUri("blue.jpg")
  var os: OutputStream? = null
  try {
    
    
      queryUri?.let {
    
     uri ->
          os = contentResolver.openOutputStream(uri)
          os?.let {
    
    
              val bitmap = Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888)
              //创建了一个红色的图片
              val canvas = Canvas(bitmap)
              canvas.drawColor(Color.YELLOW)
              //重新写入文件内容
              bitmap.compress(Bitmap.CompressFormat.PNG, 90, os)
              val contentValues = ContentValues()
              //给改文件重命名
              contentValues.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, "yellow.jpg")
              contentResolver.update(uri, contentValues, null, null)
          }
      }
  } catch (e: Exception) {
    
    
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    
    
          if (e is RecoverableSecurityException) {
    
    
              try {
    
    
                  updatePictureForActivityResult.launch(
                      IntentSenderRequest.Builder(e.userAction.actionIntent.intentSender)
                          .build()
                  )
              } catch (e2: IntentSender.SendIntentException) {
    
    
                  e2.printStackTrace()
              }
              return
          }
      }
      e.printStackTrace()
  }
}
Delete files using MediaStore

Deleting multimedia files created by oneself does not require permission, while those created by other APPs and modification types require user authorization.

/**
  * 删除MediaStore文件
  */
private fun deleteFileByMediaStore() {
    
    
    deletePictureRequestPermissionActivityResult = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
    
    
        if (it) {
    
    
            deleteFile()
        } else {
    
    
            Log.d(TAG, "deleteFileByMediaStore: 授权失败")
        }
    }
    deletePictureSenderRequestActivityResult = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
    
    
        if (it.resultCode == RESULT_OK) {
    
    
            deleteFile()
        } else {
    
    
            Log.d(TAG, "updateFileByMediaStore: 授权失败")
        }
    }
    binding.deleteFileByMediaStoreBtn.setOnClickListener {
    
    
        deletePictureRequestPermissionActivityResult.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
    }
}

private fun deleteFile() {
    
    
    val queryUri = queryImageUri("2021-10-14_11.19.18.882.png")
    try {
    
    
        if (queryUri != null) {
    
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    
    
                val delete = contentResolver.delete(queryUri, null, null)
                //delete=0删除失败,delete=1也不一定删除成功,必须要授予文件的写权限
                Log.d(TAG, "contentResolver.delete:$delete")
            } else {
    
    
                val filePathByUri = UriTool.getFilePathByUri(this@ScopedStorageActivity, queryUri)
                File(filePathByUri).delete()
            }
        }
    } catch (e: Exception) {
    
    
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    
    
            if (e is RecoverableSecurityException) {
    
    
                try {
    
    
                    deletePictureSenderRequestActivityResult.launch(
                        IntentSenderRequest.Builder(e.userAction.actionIntent.intentSender)
                            .build()
                    )
                } catch (e2: IntentSender.SendIntentException) {
    
    
                    e2.printStackTrace()
                }
                return
            }
        }
        e.printStackTrace()
    }
}

UsingStorage Access Framework

Android 4.4 (API level 19) introduced the Storage Access Framework Storage Access Framework (SAF). With SAF, users can easily browse and open documents, images and other files across all of their preferred document storage providers. Users can browse files and access recently used files in a unified way across all apps and providers through a standard, easy-to-use interface.

SAF google official documentation https://developer.android.google.cn/guide/topics/providers/document-provider

StorageAccessFramework.png

The SAF local storage service DocumentsProvideris implemented . By Intentcalling DocumentUI, the user DocumentUIselects the files and directories to be created and authorized on the Internet. After the authorization is successful, the callback is onActivityResultcalled back to get the specified ones Uri. According to this, Urioperations such as reading and writing can be performed. At this time, To grant read and write permissions to files, there is no need to dynamically apply for permissions.

Select individual files using SAF

Through Intent.ACTION_OPEN_DOCUMENTthe file selection interface, the user selects and returns one or more existing documents, and all selected documents have persistent read and write permissions granted until the device is restarted.

private lateinit var createFileActivityResult: ActivityResultLauncher<Intent>
/**
 * 选择一个文件,这里打开一个图片作为演示
 */
private fun selectSingleFile() {
    
    
    selectSingleFileActivityResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
    
    
        if (it.resultCode == RESULT_OK) {
    
    
            //获取文档
            val uri = it?.data?.data
            if (uri != null) {
    
    
                dumpImageMetaData(uri)//dump图片的信息进行打印
                getBitmapFromUri(uri)?.let {
    
    
                    binding.showIv.setImageBitmap(it)
                }
                Log.d(TAG, "图片的line :${
      
      readTextFromUri(uri)}")
            }
        }
    }
    binding.selectSingleFile.setOnClickListener {
    
    
        safSelectSingleFileActivityResult.launch(Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
    
    
            // Filter to only show results that can be "opened", such as a
            // file (as opposed to a list of contacts or timezones)
            addCategory(Intent.CATEGORY_OPENABLE)
            // Filter to show only images, using the image MIME data type.
            // If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
            // To search for all documents available via installed storage providers,
            // it would be "*/*".
            type = "image/*"
        })
    }
}
Create files using SAF

By using Intent.ACTION_CREATE_DOCUMENT, a MIME type and filename can be provided, but the end result is up to the user .

private fun createFile(mimeType: String, fileName: String) {
    
    
    createFileActivityResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
    
    
        if (it.resultCode == Activity.RESULT_OK) {
    
    
            //创建文档
            val uri = it?.data?.data
            if (uri != null) {
    
    
                Log.d(TAG, "创建文件成功")
                binding.createFileUriTv.text = TAG
                binding.createFileUriTv.visibility = View.VISIBLE
                dumpImageMetaData(uri)//dump图片的信息进行打印
            }
        }
    }
    binding.createFileBtn.setOnClickListener {
    
    
        createFileActivityResult.launch(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
    
    
            // Filter to only show results that can be "opened", such as
            // a file (as opposed to a list of contacts or timezones).
            addCategory(Intent.CATEGORY_OPENABLE)
            // Create a file with the requested MIME type.
            type = mimeType
            putExtra(Intent.EXTRA_TITLE, fileName)
        })
    }
}
Delete files using SAF

It should be noted that at this time Uriit is Documentauthorized, for example: content://com.android.providers.media.documents/document/image:14766. Instead of this content://media/external/images/media/14760.

If you get the URI of the document, and the document Document.COLUMN_FLAGScontains FLAG_SUPPORTS_DELETE, you can delete the document.

private fun checkUriFlag(uri: Uri, flag: Int): Boolean {
    
    
    try {
    
    
        val cursor = contentResolver.query(uri, null, null, null, null)
        if (cursor != null && cursor.moveToFirst()) {
    
    
            val columnIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS)
            val columnFlags = cursor.getInt(columnIndex)
            Log.i(TAG,"Column Flags:$columnFlags  Flag:$flag")
						if ((columnFlags and  flag) == flag) {
    
    
                return true
            }
            cursor.close()
        }
    } catch (e: Exception) {
    
    
        e.printStackTrace()
    }
    return false
}

Here is an explanation to go through, we can look at DocumentsContract.javathe source code first.

//android.provider.DocumentsContract.java
public static final int FLAG_SUPPORTS_THUMBNAIL = 1;
public static final int FLAG_SUPPORTS_WRITE = 1 << 1;
public static final int FLAG_SUPPORTS_DELETE = 1 << 2;
public static final int FLAG_DIR_SUPPORTS_CREATE = 1 << 3;
/**
 * Flags that apply to a document. This column is required.
 * <p>
 * Type: INTEGER (int)
 *
 * @see #FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE
 * @see #FLAG_DIR_PREFERS_GRID
 * @see #FLAG_DIR_PREFERS_LAST_MODIFIED
 * @see #FLAG_DIR_SUPPORTS_CREATE
 * @see #FLAG_PARTIAL
 * @see #FLAG_SUPPORTS_COPY
 * @see #FLAG_SUPPORTS_DELETE
 * @see #FLAG_SUPPORTS_METADATA
 * @see #FLAG_SUPPORTS_MOVE
 * @see #FLAG_SUPPORTS_REMOVE
 * @see #FLAG_SUPPORTS_RENAME
 * @see #FLAG_SUPPORTS_SETTINGS
 * @see #FLAG_SUPPORTS_THUMBNAIL
 * @see #FLAG_SUPPORTS_WRITE
 * @see #FLAG_VIRTUAL_DOCUMENT
 * @see #FLAG_WEB_LINKABLE
 */
public static final String COLUMN_FLAGS = "flags";

It can be seen that the flag is distinguished by binary bits. So to judge whether to judge whether a certain flag is included, you can use bit operation and Document.COLUMN_FLAGScompare.

 /**
  * 如果您获得了文档的 URI,并且文档的 Document.COLUMN_FLAGS 包含 FLAG_SUPPORTS_DELETE,则便可删除该文档
  */
private fun deleteFile() {
    
    
    binding.deleteFileBtn.setOnClickListener {
    
    
        queryUri = Uri.parse("content://com.android.providers.media.documents/document/image%3A14766")
        queryUri?.let {
    
     url ->
            if (checkUriFlag(url, DocumentsContract.Document.FLAG_SUPPORTS_DELETE)) {
    
    
                val deleted = DocumentsContract.deleteDocument(contentResolver, url)
                val s = "删除$url$deleted"
                Log.d(TAG, "deleteFile:$s")
                if (deleted) {
    
    
                    binding.createFileUriTv.text = ""
                }
            }
        }
    }
}
Update files using SAF

Here Uri, it is authorized through the user's choice Uri, and can be modified by Uriobtaining ParcelFileDescriptoror opening .OutputStream

private fun editDocument() {
    
    
    editFileActivityResult =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
    
    
            if (it.resultCode == RESULT_OK) {
    
    
                //编辑文档
                val uri = it?.data?.data
                if (uri != null) {
    
    
                    alterDocument(uri)//更新文档
                }
            }
        }
    binding.editDocumentBtn.setOnClickListener {
    
    
        editFileActivityResult.launch(
            // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's
            // file browser.
            Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
    
    
                // Filter to only show results that can be "opened", such as a
                // file (as opposed to a list of contacts or timezones).
                addCategory(Intent.CATEGORY_OPENABLE)
                // Filter to show only text files.
                type = "text/plain"
            })
    }
}
Use SAF to get directory & save authorization

ACTION_OPEN_DOCUMENT_TREEFor use intent, pull up DocumentUIthe method of allowing the user to actively authorize. After the user actively authorizes, the application can temporarily obtain the read and write permissions of all files and directories under the directory, and can operate the directory and the files under DocumentFileit.

In this process, if the user authorizes Uri, the read and write permissions of the device are obtained by default Uriuntil the device is restarted. The permission can be obtained permanently by saving the permission, and there is no need to ask the user to actively authorize again after restarting the phone every time.

contentResolver.takePersistableUriPermissionThe method can check whether the current file Urihas relevant authorization to read and write files;

/**
 * 使用saf选择目录
 */
private fun getDocumentTree() {
    
    
    selectDirActivityResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
    
    
        //选择目录
        val treeUri = it?.data?.data
        if (treeUri != null) {
    
    
            savePersistablePermission(treeUri)//将获取的权限持久化保存
            val root = DocumentFile.fromTreeUri(this, treeUri)
            root?.listFiles()?.forEach {
    
     it ->
                Log.d(TAG, "目录下文件名称:${
      
      it.name}")
            }
        }
    }
    binding.getDocumentTreeBtn.setOnClickListener {
    
    
        val sp = getSharedPreferences("DirPermission", Context.MODE_PRIVATE)//获取缓存的权限
        val uriString = sp.getString("uri", "")
        if (!uriString.isNullOrEmpty()) {
    
    
            try {
    
    
                val treeUri = Uri.parse(uriString)
                // Check for the freshest data.
                contentResolver.takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
                Log.d(TAG, "已经获得永久访问权限")
                val root = DocumentFile.fromTreeUri(this, treeUri)
                root?.listFiles()?.forEach {
    
     it ->
                    Log.d(TAG, "目录下文件名称:${
      
      it.name}")
                }
            } catch (e: SecurityException) {
    
    
                Log.d(TAG, "uri 权限失效,调用目录获取")
                selectDirActivityResult.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE))
            }
        } else {
    
    
            Log.d(TAG, "没有永久访问权限,调用目录获取")
            selectDirActivityResult.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE))
        }
    }
}
Use SAF for renaming
private fun renameFileName() {
    
    
    binding.renameFileBtn.setOnClickListener {
    
    
        queryUri?.let {
    
    
            val uri = it
            //小米8 Android9 抛出java.lang.UnsupportedOperationException: Rename not supported异常
            //Pixel 6a Android13可以正常重命名
            if (checkUriFlag(uri, DocumentsContract.Document.FLAG_SUPPORTS_RENAME)) {
    
    
                try {
    
    
                    //如果文件名已存在,会报错java.lang.IllegalStateException: File already exists:
                    DocumentsContract.renameDocument(contentResolver, uri, "slzs.txt")
                    Log.d(TAG, "renameFileName" + "重命名成功")
                } catch (e: FileNotFoundException) {
    
    
                    Log.d(TAG, "renameFileName" + "重命名失败,文件不存在")
                }
            } else {
    
    
                Log.d(TAG, "renameFileName" + "重命名失败,权限校验失败")
            }
        }
    }
}
Using a custom DocumentsProvider

If you want the data of your application to be documentsuiopened in , you can customize one document provider. APP can implement a custom ContentProvider to provide APP private files to the outside world. General file management software will use custom ones DocumentsProvider. This method is very suitable for internal file sharing and does not want to have UI interaction. Google official documents related to ContentProvider: https://developer.android.google.cn/guide/topics/providers/content-providers

The steps for customization are described below DocumentsProvider:

  • API version 19 or higher
  • Register the Provider in manifest.xml
  • Provider name is the class name plus package name, for example: com.example.android.storageprovider.MyCloudProvider
  • Authority is the package name + provider type name, such as: com.example.android.storageprovider.documents
  • The value of the android:exported attribute is true
<manifest... >
    ...
    <uses-sdk
        android:minSdkVersion="19"
        android:targetSdkVersion="19" />
        ....
        <provider
            android:name="com.example.android.storageprovider.MyCloudProvider"
            android:authorities="com.example.android.storageprovider.documents"
            android:grantUriPermissions="true"
            android:exported="true"
            android:permission="android.permission.MANAGE_DOCUMENTS"
            android:enabled="@bool/atLeastKitKat">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
            </intent-filter>
        </provider>
    </application>
</manifest>

compatibility impact

Scoped StorageIt has a great impact on APP access to external storage methods, APP data storage, and data sharing between APPs. Developers are requested to pay attention to the following compatibility issues.

Unable to create new file

problem causes:

Create a new file directly using a path other than its own App-specific directory.

problem analysis:

On Android Q, an app only allows files generated by paths within its own App-specific directory.

solution:

For the method and file path of creating a new file in the App-specific directory of the app, see Accessing App-specific Directory Files ; if you want to create a new file in a public directory, use the MediaStore interface, see Using MediaStore to Access Public Directory ; if you want to create a file in any directory To create a new file, you need to use SAF, see [Using Storage Access Framework](#Using Storage Access Framework).

Unable to access files on storage device

Problem cause 1:

Access public directory files directly using path.

Problem Analysis 1:

On Android Q, apps can only access App-specific directories on external storage devices by default.

Solution 1:

See Using MediaStore to Access a Public Directory and Using SAF to Select a Single File , Using the MediaStore Interface to Access Multimedia Files in a Public Directory, or Using SAF to Access Arbitrary Files in a Public Directory.

Note: MediaStoreThe DATA field queried from the interface will Android Qbe discarded at the beginning, and it should not be used to access files or determine whether a file exists; after obtaining the file Uri from MediaStorethe interface or SAF, please use Uri to open the FD or input and output stream instead of Convert it to a file path to access.

Problem reason 2:

Use the MediaStore interface to access non-multimedia files.

Problem Analysis 2:

On Android Q, only multimedia files in the public directory can be accessed using the MediaStore interface.

Solution 2:

Use SAF to apply to users for read and write permissions on files or directories. For details, see Using SAF to Select a Single File .

Couldn't share files properly

problem causes:

When the app App-specificshares the private files in the directory with other apps, it uses file://the type Uri.

problem analysis:

On Android Q, since the files in the App-specific directory are private and protected, other apps cannot access them through the file path.

solution:

See share processing , use FileProvider, and content://share the Uri of this type with other apps.

Unable to modify file on storage device

Problem cause 1:

Access public directory files directly using path.

Problem Analysis 1:

Also unable to access files on the storage device .

Solution 1:

If you cannot access the files on the storage device , please use the correct public directory file access method.

Problem reason 2:

After using MediaStorethe interface to obtain the Uri of the multimedia file in the public directory, directly use the Uri to open OutputStreamor file descriptor.

Problem Analysis 2:

On Android Qthe Internet, modifying public directory files requires user authorization.

Solution 2:

After obtaining the Uri of the multimedia file in the public directory from MediaStorethe interface, open OutputStreamor FDpay attention catch RecoverableSecurityException, and then apply to the user for permission to delete and modify the multimedia file . , can be used directly, but pay attention to the timeliness of the Uri permission, see Use SAF to obtain directory & save authorization .

Accidental file deletion after app uninstall

problem causes:

Save the files you want to keep in App-specifica directory on your external storage.

problem analysis:

On Android Qthe Internet, uninstalling the app will delete App-specificthe data in the directory by default.

solution:

The APP should save the files you want to keep MediaStorein the public directory through the interface, see Using MediaStore to access the public directory . By default, MediaStorethe interface will save non-media files to Downloadsa directory, and it is recommended that the APP specify the first-level directory as this Documents. pp-specificIf the APP wants to keep the data in the A directory when uninstalling , it must AndroidManifest.xmlbe declared in android:hasFragileUserData="true", so that when the APP is uninstalled, a pop-up box will prompt the user whether to keep the application data.

Geolocation data in image files cannot be accessed

problem causes:

Parse geolocation data directly from an image file input stream.

problem analysis:

Since the geographical location information of the picture involves user privacy, the Android Qwebsite does not provide this data to the APP by default.

solution:

Apply for ACCESS_MEDIA_LOCATIONpermission, and use MediaStore.setRequireOriginal()the interface to update the file Uri, please refer to the image location information .

ota upgrade problem (data migration)

problem causes:

After the ota upgrade, the APP is uninstalled, and the APP data cannot be accessed after reinstallation.

problem analysis:

Scoped StorageThe new features only Android Qtake effect for newly installed apps. The device is upgraded from Android Q the previous version Android Q, and the installed APP gets Legacy Viewthe view.

If these apps save files to the external storage directly through the path, such as the root directory of the external storage, then the app is uninstalled and reinstalled, and the new app obtains the view, and cannot directly access the old data through the path, resulting in data loss Filtered View.

solution:

  • The APP should modify the way of saving files, no longer using the path to save directly, but using the MediaStoreinterface to save the files to the corresponding public directory.

  • Before the ota upgrade, the APP user history data can MediaStorebe migrated to the public directory through the interface. In addition, the APP should change App-specificthe way of accessing files outside the directory, please use MediaStorethe interface or SAF.

  • For files that can only be accessed by the app itself and are allowed to be deleted after the app is uninstalled, the files need to be migrated to the private directory of the app. File resources can be accessed through File paththe method reduce adaptation costs.

  • Files that are allowed to be accessed by other applications and not allowed to be deleted after the application is uninstalled must be stored in a shared directory. The application can choose whether to perform directory rectification and migrate the files to the Androidqrequired mediadirectory.

share processing

The APP can choose the following methods to App-specificshare the files in its own directory with other APPs for reading and writing.

Use FileProvider

Official Google documents related to FileProvider: https://developer.android.google.cn/reference/androidx/core/content/FileProvider https://developer.android.com/training/secure-file-sharing/setup-sharing

FileProviderBelonging to Android7.0the behavior change of Zai, there are many various posts, so I won’t introduce it in detail here. In order to avoid conflicts with existing three-party libraries, it is recommended to use extends FileProviderthe method.

public class TakePhotoProvider extends FileProvider {
    
    ...}
<application>
        <provider
            android:name=".TakePhotoProvider"
            android:authorities="${applicationId}.fileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/take_file_path" />
        </provider>
</application>
Use ContentProvider

APP can be customized ContentProviderto provide APP private files to the outside world. This method is very suitable for internal file sharing and does not want to have UI interaction. ContentProviderRelated Google official documents: https://developer.android.google.cn/guide/topics/providers/content-providers

Use Documents Provider

See Using a Custom DocumentsProvider for details

Related API usage issues

MediaStore DATA field is no longer reliable

Android QThe in ( DATAie _data) field is obsolete and no longer represents the real path of the file.

When reading and writing files or judging whether a file exists, DATAfields should not be used, but fields should be used openFileDescriptor. At the same time, it is not possible to directly use the path to access the files of the public directory.

Add Pending status to MediaStore files

AndroidQMediaStoreAdded above and in MediaStore.Images.Media.IS_PENDING, the flag is used to indicate the Pending status of the file, 0 is visible, others are invisible, if the setIncludePendinginterface is not set, the set file cannot be queried IS_PENDIN , it can be used for downloading, or producing screenshots, etc.

ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "myImage.PNG");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
values.put(MediaStore.Images.Media.IS_PENDING, 1);
ContentResolver resolver = context.getContentResolver();
Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Uri item = resolver.insert(uri, values);
try {
    
    
    ParcelFileDescriptor pfd = resolver.openFileDescriptor(item, "w", null);
    // write data into the pending image.
} catch (IOException e) {
    
    
    LogUtil.log("write image fail");
}
// clear IS_PENDING flag after writing finished.
values.clear();
values.put(MediaStore.Images.Media.IS_PENDING, 0);
resolver.update(item, values, null, null);
MediaStore relative path

AndroidQIn , multimedia files are stored in the public directory through MediaSore. In addition to the default first-level directory, you can also specify a sub-directory. The corresponding first-level directory is detailed in the table below:

Uriminetype.png

val values = ContentValues()
//Pictures为一级目录对应Environment.DIRECTORY_PICTURES,sl为二级目录
values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/sl")
val external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val insertUri = contentResolver.insert(external, values)

values.clear()
//DCIM为一级目录对应Environment.DIRECTORY_DCIM,sl为二级目录,sl2为三级目录
values.put(MediaStore.Images.Media.RELATIVE_PATH, "DCIM/sl/sl2")
contentResolver.update(insertUri,values,null,null)

References

OPPO Open Platform Android Q Version Application Compatibility Adaptation Guide

GitHub Demo

If you want to read more articles by the author, you can check out my personal blog and public account:
Revitalize Book City

Guess you like

Origin blog.csdn.net/stven_king/article/details/128429695