【问题标题】:getExternalStoragePublicDirectory deprecated in Android Q在 Android Q 中不推荐使用 getExternalStoragePublicDirectory
【发布时间】:2019-10-21 10:32:37
【问题描述】:

由于getExternalStoragePublicDirectoryAndroid Q 中已弃用,建议使用其他方式。那么我们如何指定要将相机应用程序生成的照片存储到DCIM 文件夹中,或者DCIM 中的自定义子文件夹中?

文档指出,接下来的 3 个选项是新的首选替代方案:

  1. Context#getExternalFilesDir(String)
  2. MediaStore
  3. Intent#ACTION_OPEN_DOCUMENT

选项 1 是不可能的,因为这意味着如果应用程序被卸载,照片将被删除。

选项 3 也不是一个选择,因为它需要用户通过 SAF 文件资源管理器选择位置。

我们只剩下选项 2,即 MediaStore;但在提出这个问题时,没有关于如何使用它来替代 Android Q 中的 getExternalStoragePublicDirectory 的文档。

【问题讨论】:

    标签: android


    【解决方案1】:

    根据文档,将DCIM/... 用于the RELATIVE_PATH,其中... 是您的自定义子目录。所以,你会得到这样的结果:

          val resolver = context.contentResolver
          val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, "CuteKitten001")
            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
            put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/PerracoLabs")
          }
    
          val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
    
          resolver.openOutputStream(uri).use {
            // TODO something with the stream
          }
    

    请注意,由于 RELATIVE_PATH 是 API 级别 29 的新功能,因此您需要在较新的设备上使用此方法,而在较旧的设备上使用 getExternalStoragePublicDirectory()

    【讨论】:

    • 感谢您的回答。作为旁注,我刚刚在文档中看到还有一个新字段“IS_PENDING”,我猜也许我应该在创建 ContentValues 时也包括在内,以便在编写文件时安全,然后更新文件完全保存后。
    • 添加您自己的文章:commonsware.com/blog/2019/04/22/….
    • 我想知道是否真的没有选项可以实现一次。这行得通,但我之前想了这么多。我不一定想继续使用getExternalStoragePublicDirectory() 以及新方法。现在我有多个 if / else 来确定 Android 版本抛弃了代码库,以新的和旧的方式来做。
    • 这种新方法不允许您访问以前创建的文件,不是吗?例如,如果我制作了一个将文件放入您提到的文件夹中的相机应用程序,然后我删除并重新安装了我的应用程序,我就无法再访问它们了,不是吗?我认为存储权限被其他权限所取代,这些权限受到更多限制,不(也许只能处理媒体文件)?此外,是否有允许创建的所有可能文件类型的列表?那些只是媒体文件,还是任何文件(如 PDF、ZIP、APK...)?
    • @androiddeveloper:“那么现在只有 SAF 可以访问文件了吗?” - AFAIK,是的。 “还有 MediaStore 来获取仅用于媒体文件的全局文件(我不知道“媒体”可能是什么,除了一些常见的图像、视频和音频文件)?” -- AFAIK,是的。
    【解决方案2】:

    @CommonsWare 的回答太棒了。但是对于那些想要在 Java 中使用它的人,你需要试试这个:

    ContentResolver resolver = context.getContentResolver();
    ContentValues contentValues = new ContentValues();
    contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
    contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
    contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
    
    Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
    

    根据@SamChen 的建议,文本文件的代码应如下所示:

    Uri uri = resolver.insert(MediaStore.Files.getContentUri("external"), contentValues);
    

    因为我们不希望 txt 文件停留在 Images 文件夹中。

    所以,我有mimeType的地方,你输入你想要的mime类型。例如,如果你想要txt (@Panache),你应该用这个字符串替换mimeType"text/plain"。以下是 mime 类型列表:https://www.freeformatter.com/mime-types-list.html

    另外,我有变量name,你用你的情况下的文件名替换它。

    【讨论】:

    • Gaurav,你的回答对 java 人来说很好,但你能提到 txt 文件的语法是什么吗?
    • Gaurav 我遇到错误,无法将文本/纯文本插入到 MediaStore.Images.Media。我已将名称传递为 file.getName 和 mimetype = text/plain。请帮助
    • Gaurav,只是这个代码中源文件的路径在哪里的问题,不是必需的吗?让我们在这里讨论stackoverflow.com/questions/59511147/…
    • 对于文本文件记住这个Uri uri = getContentResolver().insert(MediaStore.Files.getContentUri("external"), values);
    【解决方案3】:

    针对Android Q - API 29+ 由于安全问题默认禁用存储访问的应用。如果要启用它在 AndroidManifest.xml 中添加以下属性:

    <manifest ... >
        <!-- This attribute is "false" by default for Android Q or higher -->
        <application android:requestLegacyExternalStorage="true" ... >
         ...
        </application>
    </manifest>
    

    那么你必须使用getExternalStorageDirectory() 而不是getExternalStoragePublicDirectory()

    例子:如果你想在内部存储不存在的情况下创建一个目录。

     File mediaStorageDir = new File(Environment.getExternalStorageDirectory() + "/SampleFolder");
    
     // Create the storage directory if it does not exist
     if (! mediaStorageDir.exists()){
         if (! mediaStorageDir.mkdirs()){
             Log.d("error", "failed to create directory");
         }
     }
    

    【讨论】:

    • 您真的需要使用requestLegacyExternalStorage="true" 进行范围存储吗?我认为一旦你使用getExternalStorageDirectory(),就没有必要了。此外,旧标志在 Api 30+ 上被忽略。
    • 在目标设备api版本大于28+时错过requestLegacyExternalStorage="true"是不行的
    • 这不好,因为它也只是根据文档的临时解决方案。使用 MediaStore 和 ContentValues
    【解决方案4】:

    import android.content.ContentValues
    import android.net.Uri
    import android.os.Build
    import android.os.Bundle
    import android.provider.MediaStore
    import android.util.Log
    import android.widget.Button
    import android.widget.EditText
    import android.widget.TextView
    import androidx.annotation.RequiresApi
    import androidx.appcompat.app.AppCompatActivity
    
    class MyActivity : AppCompatActivity() {
    
        @RequiresApi(Build.VERSION_CODES.Q)
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_mine)
            val editText: EditText = findViewById(R.id.edt)
            val write: Button = findViewById(R.id.Output)
            val read: Button = findViewById(R.id.Input)
            val textView: TextView = findViewById(R.id.textView)
    
            val resolver = this.contentResolver
            val contentValues = ContentValues().apply {
                put(MediaStore.MediaColumns.DISPLAY_NAME, "myDoc1")
                put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
                put(MediaStore.MediaColumns.RELATIVE_PATH, "Documents")
            }
            val uri: Uri? = resolver.insert(MediaStore.Files.getContentUri("external"), contentValues)
            Log.d("Uri", "$uri")
    
            write.setOnClickListener {
                val edt : String = editText.text.toString()
                if (uri != null) {
                    resolver.openOutputStream(uri).use {
                        it?.write("$edt".toByteArray())
                        it?.close()
    
                    }
                }
            }
            read.setOnClickListener {
                if (uri != null) {
                    resolver.openInputStream(uri).use {
                        val data = ByteArray(50)
                        it?.read(data)
                        textView.text = String(data)
    
                    }
                }
            }
        }
    }

    在这里,我通过将文本写入编辑文本并单击“写入”按钮将文本文件存储在手机的 Document 文件夹中,它将使用写入的文本保存文件。 单击“读取”按钮时,它将从该文件中获取文本,然后将其显示在文本视图中。

    它不会在低于 android Q 或 android 10 的设备上运行,因为 RELATIVE_PATH 只能在这些版本中使用。

    【讨论】:

    • 如何在 android 11 中将我的文件从公共目录移动到 Android/Media/com.myapp 目录,就像 whatsapp 一样,无需征求任何许可?如果 getExternalStoragePublicDirectory 现在在 Q 中已弃用,如何获取公共目录路径?
    【解决方案5】:

    对于 Xamarin.Android,来自已发布项目的以下代码可能会有所帮助:

        Java.IO.File jFolder;
    
        if ((int)Android.OS.Build.VERSION.SdkInt >= 29)
        {
            jFolder = new Java.IO.File(Android.App.Application.Context.GetExternalFilesDir(Environment.DirectoryDcim), "Camera");
        }
        else
        {
            jFolder = new Java.IO.File(Environment.GetExternalStoragePublicDirectory(Environment.DirectoryDcim), "Camera");
        }
    
        if (!jFolder.Exists())
            jFolder.Mkdirs();
    
        var filename = GenerateJpgFileName();
        var jFile = new Java.IO.File(jFolder, filename);
        var fullFilename = jFile.AbsoluteFile.ToString();
    
        using (var output = new System.IO.FileStream(fullFilename, System.IO.FileMode.Create))
        {
            outputBitmap.Compress(Bitmap.CompressFormat.Jpeg, 90, output);
            output.Close();
        }
    

    在 Android 中不使用权限,它是使用 Xamarin.Essentials 从共享项目中完成的。

    【讨论】:

    • 如果我错了,请纠正我,但Context.GetExternalFilesDir(Environment.DirectoryDcim) 没有为您提供您自己的应用程序的文件夹路径,这意味着如果您删除该应用程序,这些文件也会被删除?这与另一条路径相反,它允许您创建的文件保留...
    • 如何在 android 11 中将我的文件从公共目录移动到 Android/Media/com.myapp 目录,就像 whatsapp 一样,无需征求任何许可?如果 getExternalStoragePublicDirectory 现在在 Q 中被弃用,如何获取公共目录路径?
    【解决方案6】:

    一般我都是这样用的:

         var data: File =Environment.getExternalStoragePublicDirectory
    (Environment.DIRECTORY_DOWNLOADS)
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                            data = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)!!
                        }
    

    【讨论】:

    • Environment.getExternalStoragePublicDirectory 在 sdk 30 上为我工作。仅在 sdk 29 上它不起作用。如果他们在 sdk 29 上弃用并在 30 或类似的东西上恢复,您是否有任何信息?
    • @Donki ,你检查过 --- 应用程序 android:requestLegacyExternalStorage="true" --- 在清单中吗?!
    【解决方案7】:

    如果您想将文件保存在应用特定的外部存储中,可以使用 context.getExternalFilesDir()。许多答案都指出了这一点。

    但是,这不是这个问题的答案,因为 getExternalFilesDir() 是应用特定的外部存储,getExternalStoragePublicDirectory() 是共享存储。

    例如,您要将下载的 pdf 文件保存到“共享”下载目录。你是怎样做的 ?对于 api 29 及更高版本,您可以在未经许可的情况下这样做。

    对于 api 28 及以下版本,您需要 getExternalStoragePublicDirectory() 方法,但已弃用。如果您不想使用已弃用的方法怎么办?然后您可以使用 SAF 文件资源管理器(Intent#ACTION_OPEN_DOCUMENT)。正如问题中所说,这需要用户手动选择位置。

    这正是 Google 想要的。 为改善用户隐私,不推荐直接访问共享/外部存储设备。

    当应用程序以 Build.VERSION_CODES.Q 为目标时,从这里返回的路径 应用程序不再可以直接访问方法。应用程序可以继续 通过迁移到访问存储在共享/外部存储上的内容 诸如 Context#getExternalFilesDir(String)、MediaStore、 或 Intent#ACTION_OPEN_DOCUMENT。

    详情见以下链接:

    https://developer.android.com/reference/android/os/Environment#getExternalStorageDirectory()

    【讨论】:

      猜你喜欢
      • 2019-12-15
      • 2019-11-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-10-05
      • 2018-12-16
      • 2016-10-20
      相关资源
      最近更新 更多