Android N 拍照

Android N正式版已经出来了,像L和M一样,N也对开发者有了新的要求。
这篇文章主要讲解笔者在开发过程中遇到的一个Android N中Uri调用的例子。

谷歌在Android官网中也对N的行为变更有了讲解。见 https://developer.android.com/about/versions/nougat/android-7.0-changes.html

其中对应用间共享文件有如下介绍:

对于面向 Android N 的应用,Android 框架执行的 StrictMode API 政策禁止向您的应用外公开 file:// URI。 如果一项包含文件 URI 的 Intent 离开您的应用,应用失败,并出现 FileUriExposedException 异常。

若要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。 进行此授权的最简单方式是使用 FileProvider 类。 如需有关权限和共享文件的更多信息,请参阅共享文件。

而我们一般拍照并指定照片存储位置的代码一般如下:

     private void startTakePhoto() {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        File file = new File(Config.PHOTO_DIR + FileNameUtil.createFileName("png"));
        file.setWritable(true, true);
        if (file.exists()) {
            startTakePhoto();
        } else {
            if (!file.getParentFile().exists())
                file.getParentFile().mkdirs();
            try {
                file.createNewFile();
                this.photoSavePath = file.getAbsolutePath();
                intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));

                startActivityForResult(intent, REQ_TAKE_PHOTO);
            } catch (IOException e) {
                log().e("创建照片文件失败!", e);
                toast("抱歉,无法创建照片文件!");
            }
        }
    }

其中,FileNameUtil.createFileName(String ext) 为随机文件名生成方法,用于生成随机文件名。 Config.PHOTO_DIR 为照片存储目录 ,这里声明为 public static String PHOTO_DIR = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() + "/sccss/"; 也就是手机中DCIM下的sccss文件夹。

这段代码在Android M及以下版本(最低只测试到4.0,再低版本不做考虑)运行起来,即使Android N的最后一个preview版本也可以跑得起来。唯独Android N上调用之后就挂掉了。

报错信息如下:

android.os.FileUriExposedException: file:///storage/emulated/0/DCIM/sccss/1472883323385.png exposed beyond app through ClipData.Item.getUri()
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
at android.net.Uri.checkFileUriExposed(Uri.java:2346)
at android.content.ClipData.prepareToLeaveProcess(ClipData.java:832)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8909)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8894)  
...                                                                

Android官方对FileUriExposedException的介绍如下:
https://developer.android.com/reference/android/os/FileUriExposedException.html

The exception that is thrown when an application exposes a file:// Uri to another app.

This exposure is discouraged since the receiving app may not have access to the shared path. For example, the receiving app may not have requested the READ_EXTERNAL_STORAGE runtime permission, or the platform may be sharing the Uri across user profile boundaries.

Instead, apps should use content:// Uris so the platform can extend temporary permission for the receiving app to access the resource.

This is only thrown for applications targeting N or higher. Applications targeting earlier SDK versions are allowed to share file:// Uri, but it’s strongly discouraged.

意思就是不让用file:// 类型的Uri,转而使用content://格式的Uri来访问跨App的文件访问。

谷歌官方为此有写一个示例,地址:
https://github.com/googlesamples/android-ScopedDirectoryAccess

根据示例,修改拍照代码:

 /** 调用系统拍照 */
    private void takePhoto() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
            requestTakePhoto();
        else
            startTakePhoto();
    }

    @TargetApi(24)
    private void requestTakePhoto() {
        String dirName = Environment.DIRECTORY_DCIM;
        StorageVolume storageVolume = mStorageManager.getPrimaryStorageVolume();
        Intent accessIntent = storageVolume.createAccessIntent(dirName);
        startActivityForResult(accessIntent, REQ_ACCESS_DCIM);
    }

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
         case REQ_ACCESS_DCIM: //拍照时请求DCIM访问权限
                        if (resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                            getContentResolver().takePersistableUriPermission(data.getData(), Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

                            try {
                                dealPhotoUri(data.getData());
                            } catch (IOException e) {
                                e.printStackTrace();
                                log().e("处理图片Uri出错:", e);
                            }
                        }
                        break;
                }
    }

 @TargetApi(24)
    @SuppressWarnings("ResultOfMethodCallIgnored")
    private void dealPhotoUri(Uri uri) throws IOException {
        ContentResolver contentResolver = getContentResolver();
        Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri,
                DocumentsContract.getTreeDocumentId(uri));
        Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri,
                DocumentsContract.getTreeDocumentId(uri));

        File file = new File(Config.PHOTO_DIR + FileNameUtil.createFileName("png"));
        if(!file.getParentFile().exists())
            file.getParentFile().mkdirs();
        if (!file.exists())
            file.createNewFile();

        log().d("docUri = " + docUri);
        log().d("childrenUri = " + childrenUri);

        String caikuDirId = null;
        try (Cursor docCursor = contentResolver.query(childrenUri, DIRECTORY_SELECTION, null, null, null)) {
            while (docCursor != null && docCursor.moveToNext()) {
                log().d("dir name= " + docCursor.getString(docCursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)));
                if ("sccss".equals(docCursor.getString(docCursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME))))
                    caikuDirId = docCursor.getString(docCursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID));
            }
        }

        if (!TextUtils.isEmpty(caikuDirId)) {
            log().d("caikuDirId: " + caikuDirId);
            Uri caikuUri = DocumentsContract.buildDocumentUriUsingTree(docUri, caikuDirId);
            log().d("caikuUri=" + caikuUri.toString());

            Uri caikuDirUri = DocumentsContract.buildChildDocumentsUriUsingTree(caikuUri, caikuDirId);
            log().d("caikuDirUri=" + caikuDirUri.toString());

            String imgId = null;
            try(Cursor fileCursor = contentResolver.query(caikuDirUri, DIRECTORY_SELECTION, null, null, null)) {
                while(fileCursor != null && fileCursor.moveToNext()) {
                    log().d("file name= " + fileCursor.getString(fileCursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)));
                    if (file.getName().equals(fileCursor.getString(fileCursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)))) {
                        imgId = fileCursor.getString(fileCursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID));
                    }
                }
            }

            if (!TextUtils.isEmpty(imgId)) {
                log().d("caikuDirId: " + imgId);
                Uri imgUri = DocumentsContract.buildDocumentUriUsingTree(caikuUri, imgId);
                log().d("fileUri = " + imgUri);

                Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
                this.photoSavePath = file.getAbsolutePath();
                intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri);
                startActivityForResult(intent, REQ_TAKE_PHOTO);
            } else {
                log().d("图片文件未找到!");
                toast("图片文件未找到!");
            }
        } else {
            log().d("照片文件夹未找到!");
            toast("照片文件夹未找到!");
        }
    }

再次运行,触发takePhoto() 方法,会弹出如下对话框:
这里写图片描述

点击允许 后,会调用代码中onActivityResult 方法并且resultCode参数为RESULE_OK,如果用户选择取消,则reultCodeRESULT_CANCELED

在例中图片存放地址为手机存储下的DCIM\sccss\ 下。
本例中已经处理Android M中规定调用相机所需要的相机权限运行时声明(非本例讨论内容,可参考谷歌示例中的easypermission: https://github.com/googlesamples/easypermissions)。
若权为测试本例中的内容而不想关心运行时权限的内容,可手动在手机’设置’–’应用’–’[应用名称]’–’权限’,在这里手动将相机权限打勾。

当用户授权过外部存储的访问权限后,’设置’–’应用’–’[应用名称]’–’存储’中会出现相关内容:
这里写图片描述

ps: 因本人很少写博客,描述中可能有所遗漏。若有问题,可留言或发Email: sccss@sccss.cn

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android摄像头拍照时可以通过调用系统的位置服务获取当前的经纬度信息,并将其添加到照片的EXIF信息中。在拍照之前,可以通过获取LocationManager实例并注册LocationListener监听器来获取当前位置信息。当拍照完成后,可以通过ExifInterface类将经纬度信息添加到照片的EXIF信息中,具体方法如下: 1. 获取当前位置信息 ```java LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); LocationListener locationListener = new LocationListener() { @Override public void onLocationChanged(Location location) { // 获取经纬度信息 double latitude = location.getLatitude(); double longitude = location.getLongitude(); } @Override public void onStatusChanged(String provider, int status, Bundle extras) {} @Override public void onProviderEnabled(String provider) {} @Override public void onProviderDisabled(String provider) {} }; // 注册位置监听器 locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locationListener); ``` 2. 将经纬度信息添加到照片的EXIF信息中 ```java ExifInterface exif = new ExifInterface(photoPath); exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE, decimalToDMS(latitude)); exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, latitude > 0 ? "N" : "S"); exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, decimalToDMS(longitude)); exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, longitude > 0 ? "E" : "W"); exif.saveAttributes(); ``` 其中,decimalToDMS()方法将十进制经纬度转换为度分秒格式的字符串,例如:35.12345678901234567890 转换为 "35/1,7/1,7385/10000"。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值