这个学期需要完成一项课程设计,我带的小组选了一个前端项目的课题。

不过说实话,没多少人愿意写前端的。

课题要求编写一个安卓的 App,具备录音、播放和保存录音文件到外置储存的功能。

在我小组里的同学有安卓开发经验的并不多,负责打框架的同学在项目初期就遇到了棘手的问题。

我翻查了一下他写的代码……

他写的代码可读性,实在不敢恭维,大概是从网上某篇文章复制粘贴下来的吧?

经过我的调整,初期框架终于到了可运行的程度。然而一开始录音程序就闪退了。

测试环境是 Android 10,经过调试发现还没开始访问录音设备,程序就抛出了一个 IOExcepition,具体原因是 Permission Denied,即权限被拒。

后来调整了一下测试环境,换成了我的测试机,一台三星 Note 3,搭载 Android 4.3 系统,没有出现问题。

查阅了一些资料,原来是 Andorid 9 之后,对于外置储存的读写访问权限进行了调整。

除了程序需要在 AndroidManifest.xml 文件里声明下面两个权限之外,这两个关于外置储存的读写权限还需要再额外地让用户手动确认申请。

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

在 Activity 需要使用相关的权限时,调用 ActivityCompat.requestPermissions 函数可申请该权限,这会弹出一个提示框提示用户是否让程序拥有这些权限;程序可通过 Activity 的一个回调函数 onRequestPermissionsResult 确认权限是否通过申请。

下面是一个关于程序启动时主动向用户申请外置储存读写权限的例子。

    final int REQUESTCODE_PERMISSION = 1;
    private static final String[] permissions = {
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
    };

    void applyPermisssion(){
        for (String per : permissions) {
            if (ContextCompat.checkSelfPermission(this,
                    per)
                    != PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(this,
                        permissions, REQUESTCODE_PERMISSION);
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode) {
            case REQUESTCODE_PERMISSION:
                if (grantResults.length > 0 && grantResults[0] == PERMISSION_DENIED) {
                    new AlertDialog.Builder(this)
                            .setTitle("错误")
                            .setMessage("申请必要的权限失败,请在手机的设置页面手动允许 App 的相关权限。")
                            .setPositiveButton("确定", null)
                            .setOnDismissListener(new DialogInterface.OnDismissListener() {
                        @Override
                        public void onDismiss(DialogInterface dialogInterface) {
                            System.exit(0);
                        }
                    }).show();
                }
                break;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        applyPermisssion();
    }

当然 App 还需要在前文提到的安卓清单文件里首先声明这两个权限。

上面的例子会在主程序启动时先调用 ContextCompat.checkSelfPermission 判断程序是否具有该权限,如果无该权限,则弹出一个窗口让用户允许程序拥有这些权限。在回调函数中,程序会得到用户关于这些权限的反馈,如果用户不允许 App 具有相关的权限,程序将会自动关闭。

经过修改,项目的读写权限的问题得到解决。但依然无法实现录音功能,仍是闪退。

经过调试发现,这一次是 Access Denied 的 IOException,即拒绝访问。

查阅安卓开发者文档,官方给出的保存文件至外置储存的解决方案出乎我们的预料。

似乎网上那些关于写入文件到 SD 卡的例子已经全部做了废,毕竟例子是具有时效性的,他们的例子大多是针对 Android 5 以前做的开发;Andorid 9 之后对于保存文件至外置储存的程序逻辑做了很大的改动。

与以往通过 IO 类直接创建 OutputStream 不同,现在 Android 要求保存文件到公开目录,使用 MediaStore API 或使用 ACTION_CREATE_DOCUMENT intent 的储存访问框架。

前者会将文件保存到外置储存的媒体文件专有目录下,如 Music、Video和 Photo 目录,这就对要保存的文件类型有一定要求,而且最终输出到的目录亦具有局限性,因此并不采用。

后者则是类似 Windows 下的 FileDialog,即弹出一个文件浏览器对话框,程序可设置文件的 MIME-Type 和文件名,但是具体保存的目录得交由用户自行选择,待选择完毕后,会触发活动里的 onActivityResult 回调函数,程序可在回调函数内得到文件的 OutputStream,进而输出文件至外置储存。

最终我们采用了后者给的解决方案,下面是一个备份语音文件到外置储存的例子(一个简单的文件拷贝)。

    private static final int WRITE_REQUEST_CODE = 0x1;    
    void hitSaveFile() {
        pauseAudio();
        Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);

        intent.addCategory(Intent.CATEGORY_OPENABLE);

        intent.setType("audio/x-wav");
        intent.putExtra(Intent.EXTRA_TITLE,
                currentAudioFilePathWav.substring(currentAudioFilePathWav.lastIndexOf("/") + 1));
        startActivityForResult(intent, WRITE_REQUEST_CODE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode,resultCode,data);
        if (requestCode == WRITE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            Uri uri = null;
            if (data != null) {
                uri = data.getData();
                try {
                    File f = new File(currentAudioFilePathWav);
                    InputStream in = new FileInputStream(f);
                    OutputStream out = getContentResolver().openOutputStream(uri);
                    byte[] buf = new byte[1024];
                    int bytesRead;
                    while ((bytesRead = in.read(buf)) > 0) {
                        out.write(buf, 0, bytesRead);
                    }
                    out.close();
                    in.close();
                    Toast.makeText(this, "备份语音文件成功!", Toast.LENGTH_SHORT).show();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
分类: 技术小记

2 条评论

石樱灯笼 · 2019年12月12日 下午6:08

Google Chrome 78.0.3904.108 Google Chrome 78.0.3904.108 Windows 7 x64 Edition Windows 7 x64 Edition

你是不是被国内开发环境给污染了,怎么会把这个读写权限管理当成封闭性和控制权的回收?

    小宝 · 2019年12月12日 下午6:18

    Google Chrome 79.0.3945.79 Google Chrome 79.0.3945.79 Mac OS X  10.14.5 Mac OS X 10.14.5

    感谢指教,后面一想这样解释确实不太妥,已经删除。

回复 石樱灯笼 取消回复

Avatar placeholder

您的电子邮箱地址不会被公开。 必填项已用*标注


The reCAPTCHA verification period has expired. Please reload the page.