如何将文件保存到存储空间(Android 13 - API33)

3

我正在为我的应用程序编写屏幕录制功能。因此,我需要将视频mp4文件保存到外部存储器中。我的功能在API 29及以下版本上运行正常,但在API 32及以上版本上无法工作。请向我展示解决此问题的步骤。

我遵循了这个源代码:https://github.com/Truiton/ScreenCapture

MainActivity.kt

class MainActivity : AppCompatActivity() {
private var mScreenDensity = 0
private var mProjectionManager: MediaProjectionManager? = null
private var mMediaProjection: MediaProjection? = null
private var mVirtualDisplay: VirtualDisplay? = null
private var mMediaProjectionCallback: MediaProjectionCallback? = null
private var mMediaRecorder: MediaRecorder? = null
private lateinit var binding: ActivityMainBinding
public override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    val metrics = DisplayMetrics()
    windowManager.defaultDisplay.getMetrics(metrics)
    mScreenDensity = metrics.densityDpi
    mMediaRecorder = MediaRecorder()
    mProjectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
    binding.toggle.setOnClickListener { v: View? ->
        if ((ContextCompat.checkSelfPermission(
                this@MainActivity,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
            ) + ContextCompat
                .checkSelfPermission(
                    this@MainActivity,
                    Manifest.permission.RECORD_AUDIO
                ) + ContextCompat.checkSelfPermission(
                this@MainActivity,
                MANAGE_EXTERNAL_STORAGE
            ))
            != PackageManager.PERMISSION_GRANTED
        ) {
            if (ActivityCompat.shouldShowRequestPermissionRationale(
                    this@MainActivity,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE
                ) ||
                ActivityCompat.shouldShowRequestPermissionRationale(
                    this@MainActivity,
                    Manifest.permission.RECORD_AUDIO
                ) || ActivityCompat.shouldShowRequestPermissionRationale(
                    this@MainActivity,
                    MANAGE_EXTERNAL_STORAGE
                )
            ) {
                binding.toggle.isChecked = false
                Snackbar.make(
                    findViewById(android.R.id.content), R.string.label_permissions,
                    Snackbar.LENGTH_INDEFINITE
                ).setAction("ENABLE",
                    View.OnClickListener { v1: View? ->
                        ActivityCompat.requestPermissions(
                            this@MainActivity,
                            arrayOf(
                                Manifest.permission.WRITE_EXTERNAL_STORAGE,
                                Manifest.permission.RECORD_AUDIO,
                                MANAGE_EXTERNAL_STORAGE
                            ),
                            REQUEST_PERMISSIONS
                        )
                    }).show()
            } else {
                ActivityCompat.requestPermissions(
                    this@MainActivity,
                    arrayOf(
                        Manifest.permission.WRITE_EXTERNAL_STORAGE,
                        Manifest.permission.RECORD_AUDIO,
                        MANAGE_EXTERNAL_STORAGE
                    ),
                    REQUEST_PERMISSIONS
                )
            }
        } else {
            onToggleScreenShare(v)
        }
    }
}

public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode != REQUEST_CODE) {
        Log.e(TAG, "Unknown request code: $requestCode")
        return
    }
    if (resultCode != RESULT_OK) {
        Toast.makeText(
            this,
            "Screen Cast Permission Denied", Toast.LENGTH_SHORT
        ).show()
        binding.toggle.isChecked = false
        return
    }
    mMediaProjectionCallback = MediaProjectionCallback()
    mMediaProjection = mProjectionManager!!.getMediaProjection(resultCode, data!!)
    mMediaProjection?.registerCallback(mMediaProjectionCallback, null)
    mVirtualDisplay = createVirtualDisplay()
    mMediaRecorder!!.start()
}

fun onToggleScreenShare(view: View?) {
    if ((view as ToggleButton?)!!.isChecked) {
        initRecorder()
        shareScreen()
    } else {
        mMediaRecorder!!.stop()
        mMediaRecorder!!.reset()
        Log.v(TAG, "Stopping Recording")
        stopScreenSharing()
    }
}

private fun shareScreen() {
    if (mMediaProjection == null) {
        startActivityForResult(mProjectionManager!!.createScreenCaptureIntent(), REQUEST_CODE)
        return
    }
    mVirtualDisplay = createVirtualDisplay()
    mMediaRecorder!!.start()
}

private fun createVirtualDisplay(): VirtualDisplay {
    return mMediaProjection!!.createVirtualDisplay(
        "MainActivity",
        DISPLAY_WIDTH, DISPLAY_HEIGHT, mScreenDensity,
        DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
        mMediaRecorder!!.surface, null /*Callbacks*/, null /*Handler*/
    )
}

@SuppressLint("SimpleDateFormat")
private fun initRecorder() {
    try {
        mMediaRecorder!!.setAudioSource(MediaRecorder.AudioSource.MIC)
        mMediaRecorder!!.setVideoSource(MediaRecorder.VideoSource.SURFACE)
        mMediaRecorder!!.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
        mMediaRecorder!!.setOutputFile(
            Environment
                .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
                .toString() + "/" + "Prank_Record_" + SimpleDateFormat("dd-MM-yyyy-hh_mm_ss").format(
                Date()
            ) + ".mp4"
        )
        mMediaRecorder!!.setVideoSize(DISPLAY_WIDTH, DISPLAY_HEIGHT)
        mMediaRecorder!!.setVideoEncoder(MediaRecorder.VideoEncoder.H264)
        mMediaRecorder!!.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
        mMediaRecorder!!.setVideoEncodingBitRate(512 * 1000)
        mMediaRecorder!!.setVideoFrameRate(30)
        val rotation = windowManager.defaultDisplay.rotation
        val orientation = ORIENTATIONS[rotation + 90]
        mMediaRecorder!!.setOrientationHint(orientation)
        mMediaRecorder!!.prepare()
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

private inner class MediaProjectionCallback : MediaProjection.Callback() {
    override fun onStop() {
        if (binding.toggle.isChecked) {
            binding.toggle.isChecked = false
            mMediaRecorder!!.stop()
            mMediaRecorder!!.reset()
            Log.v(TAG, "Recording Stopped")
        }
        mMediaProjection = null
        stopScreenSharing()
    }
}

private fun stopScreenSharing() {
    if (mVirtualDisplay == null) {
        return
    }
    mVirtualDisplay!!.release()
    //mMediaRecorder.release(); //If used: mMediaRecorder object cannot
    // be reused again
    destroyMediaProjection()
}

public override fun onDestroy() {
    super.onDestroy()
    destroyMediaProjection()
}

private fun destroyMediaProjection() {
    if (mMediaProjection != null) {
        mMediaProjection!!.unregisterCallback(mMediaProjectionCallback)
        mMediaProjection!!.stop()
        mMediaProjection = null
    }
    Log.i(TAG, "MediaProjection Stopped")
}

@RequiresApi(Build.VERSION_CODES.R)
override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    when (requestCode) {
        REQUEST_PERMISSIONS -> {
            if (true) {
                onToggleScreenShare(binding.toggle)
            } else {
                binding.toggle.isChecked = false
                Snackbar.make(
                    findViewById(android.R.id.content), R.string.label_permissions,
                    Snackbar.LENGTH_INDEFINITE
                ).setAction("ENABLE",
                    View.OnClickListener {
                        val intent = Intent()
                        intent.action = Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
                        intent.addCategory(Intent.CATEGORY_DEFAULT)
                        intent.data = Uri.parse("package:$packageName")
                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                        intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
                        intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
                        startActivity(intent)
                    }).show()
            }
            return
        }
    }
}

companion object {
    private const val TAG = "MainActivity"
    private const val REQUEST_CODE = 1000
    private const val DISPLAY_WIDTH = 720
    private const val DISPLAY_HEIGHT = 1280
    private val ORIENTATIONS = SparseIntArray()
    private const val REQUEST_PERMISSIONS = 10

    init {
        ORIENTATIONS.append(Surface.ROTATION_0, 90)
        ORIENTATIONS.append(Surface.ROTATION_90, 0)
        ORIENTATIONS.append(Surface.ROTATION_180, 270)
        ORIENTATIONS.append(Surface.ROTATION_270, 180)
    }
}

}

AndroidManifest.xml

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

展示你的代码。说出不起作用的地方。使用正确的公共目录,它也可以在SDK 33上运行。 - blackapps
@blackapps 我跟随这个源代码 https://github.com/Truiton/ScreenCapture - WilliamVietnam
代码太多了,都是无关的。为了证明你不能创建一个文件,只需要五行代码就足够了。 - blackapps
由于您无法存储文件,因此它不起作用。因此,请仅使用五行代码演示无法创建文件。言归正传。 - blackapps
我检查了一下,发现WRITE_EXTERNAL_STORAGE权限没有被授予。所以我期待着有人能提出解决方案。或者提供解决上述问题的步骤。@blackapps - WilliamVietnam
显示剩余2条评论
3个回答

2

从API级别29开始,为了提高用户隐私和安全性,访问外部存储已被限制。因此,如果您的应用程序使用API级别32,则我猜测您已经在清单中添加了存储权限,并在运行时请求了它们。

我的应用程序中有代码片段可以执行您想要的相同功能。

val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
    addCategory(Intent.CATEGORY_OPENABLE)
    type = "video/mp4"
    putExtra(Intent.EXTRA_TITLE, "my_video.mp4")
}

startActivityForResult(intent, CREATE_DOCUMENT_REQUEST_CODE)

使用ACTION_CREATE_DOCUMENT意图创建新文档。指定CATEGORY_OPENABLE类别以允许用户选择位置。类型指定文件的MIME类型。在这种情况下,我们使用“video/mp4”。putExtra方法用于指定默认文件名。

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    if (requestCode == CREATE_DOCUMENT_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
        val uri: Uri? = data?.data
        if (uri != null) {
            // Save the video file to the chosen location
            saveVideoToUri(uri)
        }
    }
}

结果是RESULT_OK。如果是这样,我们会检索所选位置的Uri并将其传递给saveVideoToUri函数。

fun saveVideoToUri(uri: Uri,context:Context) {
    try {
        context.contentResolver.openOutputStream(uri)?.use { outputStream ->
            // Write the video data to the output stream
            outputStream.write(videoData)
        }
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

我猜测您已经在清单文件中添加了存储权限,并在运行时请求了它们。但是,使用ACTION_CREATE_DOCUMENT并不需要全部。 - blackapps
抱歉,经过检查,我的这个源代码只能在API 29及以下版本上运行。在API 29及以上版本上无法工作。我遵循了https://github.com/Truiton/ScreenCapture的指导。请帮我完成这个功能。 - WilliamVietnam

2
  1. 您需要在清单文件中添加以下两个权限,并通过编程向用户请求权限:

    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    
  2. 在 Android R+ 中,您无法访问存储空间而不使用 MANAGE_EXTERNAL_STORAGE (请参见此处的文档),但您可以访问共享存储,例如 Downloads 和您的应用程序文件夹。

  3. 现在,您可以使用以下代码创建您的文件夹目录:

     fun createAppDirectoryInDownloads(context: Context): File? {
         val downloadsDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
         val appDirectory = File(downloadsDirectory, "YourAppDirectoryName")
    
         if (!appDirectory.exists()) {
             val directoryCreated = appDirectory.mkdir()
             if (!directoryCreated) {
                 // 无法创建目录
                 return null
             }
         }
    
         return appDirectory
     }
    
  4. 现在,您可以使用以下代码在您的目录中创建视频文件:

     companion object {
     @JvmStatic
     fun createFileInAppDirectory(context: Context, fileName: String): File? {
         val appDirectory = createAppDirectoryInDownloads(context)
         if (appDirectory != null) {
             val file = File(appDirectory, fileName)
             try {
                 if (!file.exists()) {
                     val fileCreated = file.createNewFile()
                     if (!fileCreated) {
                         // 无法创建文件
                         return null
                     }
                 }
                 return file
             } catch (e: IOException) {
                 e.printStackTrace()
             }
         }
         return null
     }
    

对我来说它有效,我只希望这能解决你的问题。


1
在Android sdk 33设备上,您一开始就不需要请求写入外部存储权限。
您默认拥有对外部存储器上所有公共目录的写入权限。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接