Environment.getExternalStorageDirectory() 在 API 级别 29 java 中已弃用

Environment.getExternalStorageDirectory() deprecated in API level 29 java

正在 android Java,最近将 SDK 更新到 API 级别 29,现在显示一条警告,指出

Environment.getExternalStorageDirectory() 在 API 级别 29

中已弃用

我的密码是

private void saveImage() {

if (requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {

    final String folderPath = Environment.getExternalStorageDirectory() + "/PhotoEditors";
    File folder = new File(folderPath);
    if (!folder.exists()) {
        File wallpaperDirectory = new File(folderPath);
        wallpaperDirectory.mkdirs();
    }


    showLoading("Saving...");
    final String filepath=folderPath
                + File.separator + ""
                + System.currentTimeMillis() + ".png";
    File file = new File(filepath);

    try {
        file.createNewFile();
        SaveSettings saveSettings = new SaveSettings.Builder()
                .setClearViewsEnabled(true)
                .setTransparencyEnabled(true)
                .build();
        if(isStoragePermissionGranted() ) {
            mPhotoEditor.saveAsFile(file.getAbsolutePath(), saveSettings, new PhotoEditor.OnSaveListener() {
            @Override
            public void onSuccess(@NonNull String imagePath) {
                hideLoading();
                showSnackbar("Image Saved Successfully");
                mPhotoEditorView.getSource().setImageURI(Uri.fromFile(new File(imagePath)));
                sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,Uri.fromFile(new File(filepath))));
                Intent intent = new Intent(EditImageActivity.this, StartActivity.class);
                startActivity(intent);
                finish();

            } 

            @Override
            public void onFailure(@NonNull Exception exception) {
                hideLoading();
                showSnackbar("Failed to save Image");
            }
       });
   }

替代方案是什么?

使用 getExternalFilesDir()getExternalCacheDir()getExternalMediaDirs()Context 上的方法)代替 Environment.getExternalStorageDirectory()

或者,修改 mPhotoEditor 以便能够使用 Uri,然后:

  • 使用 ACTION_CREATE_DOCUMENT 获取 Uri 到用户选择的位置,或

  • 使用 MediaStoreContentResolverinsert() 为特定类型的媒体(例如图像)获取 Uri —请参阅 this sample app 演示如何从网站下载 MP4 视频

此外,请注意您的 Uri.fromFileACTION_MEDIA_SCANNER_SCAN_FILE 应该会在 Android 7.0+ 和 FileUriExposedException 上崩溃。在 Android Q 上,只有 MediaStore/insert() 选项才能让您的内容快速被 MediaStore 索引。

请注意,如果您的 targetSdkVersion 低于 30,您可以在 Android 10 和 11 上选择退出这些“分区存储”更改,使用 [=31] 中的 android:requestLegacyExternalStorage="true" =] 清单的元素。 这不是 long-term 解决方案,因为如果您要通过 Play 商店(以及也许在其他地方)。

通过新的 API 调用获取 destPath

String destPath = mContext.getExternalFilesDir(null).getAbsolutePath();

如果您想使用默认相机拍照并将其存储在 DCIM 文件夹中 (DCIM/app_name/filename.jpg),这是一个如何获取文件 URI 的小示例:

打开相机(记住相机权限):

private var photoURI: Uri? = null

private fun openCamera() {
    Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
        photoURI = getPhotoFileUri()
        takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
        takePictureIntent.resolveActivity(requireActivity().packageManager)?.also {
            startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
        }
    }
}

并获取 URI:

private fun getPhotoFileUri(): Uri {
    val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
    val fileName = "IMG_${timeStamp}.jpg"

    var uri: Uri? = null
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        val resolver = requireContext().contentResolver
        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
            put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/app_name/")
        }

        uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
    }

    return uri ?: getUriForPreQ(fileName)
}

private fun getUriForPreQ(fileName: String): Uri {
    val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
    val photoFile = File(dir, "/app_name/$fileName")
    if (photoFile.parentFile?.exists() == false) photoFile.parentFile?.mkdir()
    return FileProvider.getUriForFile(
        requireContext(),
        "ru.app_name.fileprovider",
        photoFile
    )
}

不要忘记 pre Q 的 WRITE_EXTERNAL_STORAGE 权限,并将 FileProvider 添加到 AndroidManifest.xml。

并得到一个结果:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    when (requestCode) {
        REQUEST_IMAGE_CAPTURE -> {
            photoURI?.let {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    val thumbnail: Bitmap =
                        requireContext().contentResolver.loadThumbnail(
                            it, Size(640, 480), null
                        )
                } else {
                    // pre Q actions
                }
            }
        }
    }
}

在 Android 10.

中创建文件时,请使用 getExternalFilesDir()getExternalCacheDir() 而不是 Environment.getExternalStorageDirectory()

见下行:

val file = File(this.externalCacheDir!!.absolutePath, "/your_file_name")

对于 Android Q,您可以将 android:requestLegacyExternalStorage="true" 添加到清单中的元素。这 opts 您进入旧存储模型,您现有的外部存储代码将起作用。

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

从技术上讲,您只有在将 targetSdkVersion 更新为 29 后才需要它。具有较低 targetSdkVersion 值的应用程序默认选择使用旧存储,需要 android:requestLegacyExternalStorage="false" 选择退出。

这对我有用

manifest 文件的应用程序标签中添加这一行

android:requestLegacyExternalStorage="true"

例子

 <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:networkSecurityConfig="@xml/network_security_config"
        android:requestLegacyExternalStorage="true"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

 </application>

Target SDK is 29

  defaultConfig {
        minSdkVersion 16
        targetSdkVersion 29
        multiDexEnabled true
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

这有效

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    contentResolver?.also { resolver ->
        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, "Image_"+".jpg")
            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg")
            put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + File.separator+ "TestFolder")
        }
        val imageUri: Uri? = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
            fos = imageUri?.let { resolver.openOutputStream(it) }
            bitmap.compress(Bitmap.CompressFormat.JPEG,100,fos)
            Objects.requireNonNull(fos)
    }
}

你可以利用 StorageManager & StorageVolume

StorageVolume.getPrimaryStorageVolume():此卷与 Environment#getExternalStorageDirectory() 和 Context#getExternalFilesDir(String) 返回的存储设备相同。

    public String myGetExternalStorageDir() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
            return getPrimaryStorageVolumeForAndroid11AndAbove();
        else
            return getPrimaryStorageVolumeBeforeAndroid11();
    }

    @TargetApi(Build.VERSION_CODES.R)
    private String getPrimaryStorageVolumeForAndroid11AndAbove() {
        StorageManager myStorageManager = (StorageManager) ctx.getSystemService(Context.STORAGE_SERVICE);
        StorageVolume mySV = myStorageManager.getPrimaryStorageVolume();
        return mySV.getDirectory().getPath();
    }

    private String getPrimaryStorageVolumeBeforeAndroid11() {
        String volumeRootPath = "";
        StorageManager myStorageManager = (StorageManager) ctx.getSystemService(Context.STORAGE_SERVICE);
        StorageVolume mySV = myStorageManager.getPrimaryStorageVolume();
        Class<?> storageVolumeClazz = null;

        try {
            storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
            Method getPath = storageVolumeClazz.getMethod("getPath");
            volumeRootPath = (String) getPath.invoke(mySV);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return volumeRootPath;
    }
private fun saveImage(bitmap: Bitmap, name: String) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        val resolver = contentResolver
        val contentValues = ContentValues()
        contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
        contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/" + "YOUR_FOLDER")
        val imageUri =
            resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
        val fos = resolver.openOutputStream(imageUri!!)
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
        fos!!.flush()
        fos.close()

        toast("Saved to gallery")

    } else {
        if (isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
            val imagesDir = Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_DCIM
            ).toString() + File.separator + "YOUR_FOLDER"
            if (!file.exists()) {
                file.mkdir()
            }
            val image = File(imagesDir, "$name.png")

            val fos = FileOutputStream(image)
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
            fos.flush()
            fos.close()

        } else {
            // ask for permission
        }
    }
}

如果您使用 XAMARIN 并且对所有这些不同的答案感到困惑(像我一样),请按照以下示例操作:

var picture = new Java.IO.File(Environment.DirectoryPictures, "fileName");

我花了一些时间才弄明白。

  var tempDir: File = inContext.getExternalFilesDir("/")!!
        tempDir = File(tempDir.getAbsolutePath().toString() + "/.IncidentImages/")
        tempDir.mkdir()

最近我也遇到了类似的问题,由于我的代码很大,我不想在现有代码中添加新功能,所以我只是简单地改变路径..

Environment.getExternalStorageDirectory() 替换为

context.getExternalFilesDir(null).getAbsolutePath()

代码

private void saveImage() {

if (requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {

final String folderPath = this.getExternalFilesDir(null).getAbsolutePath() + "/PhotoEditors";
File folder = new File(folderPath);
if (!folder.exists()) {
    File wallpaperDirectory = new File(folderPath);
    wallpaperDirectory.mkdirs();
}


showLoading("Saving...");
final String filepath=folderPath
            + File.separator + ""
            + System.currentTimeMillis() + ".png";
File file = new File(filepath);

try {
    file.createNewFile();
    SaveSettings saveSettings = new SaveSettings.Builder()
            .setClearViewsEnabled(true)
            .setTransparencyEnabled(true)
            .build();
    if(isStoragePermissionGranted() ) {
        mPhotoEditor.saveAsFile(file.getAbsolutePath(), saveSettings, new PhotoEditor.OnSaveListener() {
        @Override
        public void onSuccess(@NonNull String imagePath) {
            hideLoading();
            showSnackbar("Image Saved Successfully");
            mPhotoEditorView.getSource().setImageURI(Uri.fromFile(new File(imagePath)));
            sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,Uri.fromFile(new File(filepath))));
            Intent intent = new Intent(EditImageActivity.this, StartActivity.class);
            startActivity(intent);
            finish();

        } 

        @Override
        public void onFailure(@NonNull Exception exception) {
            hideLoading();
            showSnackbar("Failed to save Image");
        }
   });

}

无需硬编码获取内部存储目录,

权限(对于所有 Android 版本)

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />

在 Android 10 及以下版本中,我们可以使用旧权限,但在 Android 10 中,我们必须在应用程序标签(AndroidManifest.xml > 应用程序标签)中使用 requestLegacyExternalStorage。

android:requestLegacyExternalStorage="true"

获取内部存储目录路径:

@Nullable
public static String getInternalStorageDirectoryPath(Context context) {
    String storageDirectoryPath;

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        StorageManager storageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
        if(storageManager == null) {
            storageDirectoryPath = null; //you can replace it with the Environment.getExternalStorageDirectory().getAbsolutePath()
        } else {
            storageDirectoryPath = storageManager.getPrimaryStorageVolume().getDirectory().getAbsolutePath();
        }
    } else {
        storageDirectoryPath = Environment.getExternalStorageDirectory().getAbsolutePath();
    }

    return storageDirectoryPath;
}