Android 拍照和图库功能(适配Android 6.0和7.0系统和华为机型问题)

众所周知,调用相机拍照和图库中获取图片的功能,基本上是每个程序App必备的。

实现适配Android每个版本,国内手机,要处理的问题却也不少。例如:Android6.0权限问题,Android7.0 FileProvider问题,华为手机图库获取不到图片的问题。

本篇内容概述

  • 调用系统相机拍照

  • 图库选取图片

  • 处理华为图库获取不到图片问题

  • 处理部分手机拍照后,图片旋转角度问题

  • RxJava加载图片,向上取整计算合适比例。

  • EasyPermission库处理去读写权限( 适配Android6.0系统及其以上)

  • FileProvider访问文件(适配Android7.0系统及其以上)

  • 跳转其他程序,Activity被系统因内存不足回收,处理数据保存问题。

项目前期配置

依赖库添加

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:26.+'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'
    //谷歌官方权限库
    compile 'pub.devrel:easypermissions:1.0.1'
    //异步消息通知库
    compile 'io.reactivex:rxjava:1.3.3'
    compile 'io.reactivex:rxandroid:1.2.1'
}

编码方式:Java+retrolambda库实现Java8特性

Android拍照功能


1. 赋予读写权限

从Android6.0开始,需要动态赋予权限,而不是安装时候赋予权限。拍照功能需要用到写入磁盘的权限。

在AndroidManifest.xml中注册读写权限:

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

第一步:检查权限和申请读写权限。 这里,使用EasyPermission库处理权限问题。

public class MainActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks{

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

        checkWritePermission();
    }
    /**
     * 检查读写权限权限
     */
    private void checkWritePermission() {
        boolean result = PermissionManager.checkPermission(this, Constance.PERMS_WRITE);
        if (!result) {
            PermissionManager.requestPermission(this, Constance.WRITE_PERMISSION_TIP 
                , Constance.WRITE_PERMISSION_CODE, Constance.PERMS_WRITE);
        }
    }
    /**
     * 重写onRequestPermissionsResult,用于接受请求结果
     *
     * @param requestCode
     * @param permissions
     * @param grantResults
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        //将请求结果传递EasyPermission库处理
        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
    }
    /**
     * 请求权限成功
     *
     * @param requestCode
     * @param perms
     */
    @Override
    public void onPermissionsGranted(int requestCode, List perms) {
        ToastUtils.showToast(getApplicationContext(), "用户授权成功");
    }
    /**
     * 请求权限失败
     *
     * @param requestCode
     * @param perms
     */
    @Override
    public void onPermissionsDenied(int requestCode, List perms) {
        ToastUtils.showToast(getApplicationContext(), "用户授权失败");
        /**
         * 若是在权限弹窗中,用户勾选了'NEVER ASK AGAIN.'或者'不在提示',且拒绝权限。
         * 这时候,需要跳转到设置界面去,让用户手动开启。
         */
        if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
            new AppSettingsDialog.Builder(this).build().show();
        }
    }
}

权限管理类PermissionManager,代码如下:

public class PermissionManager {
    /**
     * @param context
     * return true:已经获取权限
     * return false: 未获取权限,主动请求权限
     */
   // @AfterPermissionGranted(Constance.WRITE_PERMISSION_CODE) 是可选的
    public static boolean checkPermission(Activity context, String[] perms) {
        return EasyPermissions.hasPermissions(context, perms);
    }
    /**
     * 请求权限
     * @param context
     */
    public static void requestPermission(Activity context,String tip,int requestCode,String[] perms) {
        EasyPermissions.requestPermissions(context, tip,requestCode,perms);
    }
}

更多详情,请阅读Android EasyPermissions官方库,高效处理权限。

2. Intent调用相机进行拍照

开启相机拍照是通过Intent来实现,在Intent中指定输出图片路径,相机拍照成功后,系统会将图片数据自动输出到指定路径,生成对应的图片。关闭相机后,会在对应的Activity中的onActivityResult()中返回结果,是否拍照成功的标示。

private String picturePath;

/**
  *Activity中通过Intent调用相机,指定输出图片路径。
  */
@Override
public void camera() {
        this.picturePath = FileUtils.getBitmapDiskFile(this.getApplicationContext());
        CameraUtils.openCamera(this, Constance.PICTURE_CODE, this.picturePath);
}


public class CameraUtils {

    /**
     * 打开相机
     * @param context
     * @param requestCode
     * @return
     */
    public static void openCamera(Activity context, int requestCode, String picturePath){
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (intent.resolveActivity(context.getPackageManager()) != null) {
            /**
             * 指定拍照存储路径
             * 7.0 及其以上使用FileProvider替换'file://'访问
             */
            if (Build.VERSION.SDK_INT>=24){
                //这里的BuildConfig,需要是程序包下BuildConfig。
                intent.putExtra(MediaStore.EXTRA_OUTPUT,  
                        FileProvider.getUriForFile(context.getApplicationContext(), 
                        BuildConfig.APPLICATION_ID+".provider",new File(picturePath)));

                intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            }else{
                intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(picturePath)));
            }
            context.startActivityForResult(intent, requestCode);
        }
    }
}

这里你会发觉多了,一个匹配android7.0的FileProvider,用于处理file://访问的问题。接下来,会讲解到它。

工具类FileUtils生成图片的路径,代码如下:

public class FileUtils {
    /**
     * 获得存储bitmap的文件
     * getExternalFilesDir()提供的是私有的目录,在app卸载后会被删除
     *
     * @param context
     * @param
     * @return
     */
    public static String getBitmapDiskFile(Context context) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())  
              || !Environment.isExternalStorageRemovable()) {
            cachePath = context.getExternalFilesDir(DIRECTORY_PICTURES).getAbsolutePath();
        } else {
            cachePath =context.getFilesDir().getAbsolutePath();
        }
        return new File(cachePath +File.separator+ getBitmapFileName()).getAbsolutePath();
    }

    public static final String bitmapFormat = ".png";

    /**
     * 生成bitmap的文件名:日期,md5加密
     *
     * @return
     */
    public static String getBitmapFileName() {
        StringBuilder stringBuilder = new StringBuilder();
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            String currentDate = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
            mDigest.update(currentDate.getBytes("utf-8"));
            byte[] b = mDigest.digest();
            for (int i = 0; i < b.length; ++i) {
                String hex = Integer.toHexString(0xFF & b[i]);
                if (hex.length() == 1) {
                    stringBuilder.append('0');
                }
                stringBuilder.append(hex);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        String fileName = stringBuilder.toString() + bitmapFormat;
        return fileName;
    }
}

3. 处理anroid7.0中禁止file的Uri问题

anroid7.0 行为变更:

android 7.0发生了一些行为变化,禁止应用程序向外部公开file://的URI。

尝试传递file://URI会触发FileUriExposedException。

应用程序之间共享数据,应该发送content://的URI,且授予URI临时访问权限。推举使用FileProvider。更多详情,阅读android 7.0行为变更。

配置FileProvider:

在src\main\res路径下创建xml文件夹,然后在创建一个provider_paths.xml文件,编写以下代码


<paths xmlns:android="http://schemas.android.com/apk/res/android">

    <files-path name="Pictures" path="/">files-path>
    <external-path path="Android/data/${applicationId}/" name="files_root" />
    <root-path
        name="root"
        path="/" />
paths>

接下来,在AndroidManifest.xml中注册FileProvider:为FileProvidre配置,指定authorities,name ,不许对外共享,临时授权,访问目录配置

     
        <provider
            android:name="android.support.v4.content.FileProvider"
           android:authorities="${applicationId}.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"/>
        provider>

配置完成后,便可以直接使用定义authorities所对应的FileProvider。

更多详情,请阅读Android 7.0 报android.os.FileUriExposedException异常。

若是配置过程中遇到问题,请阅读Android FileProvider配置报错android.content.pm.ProviderInfo.loadXmlMetaData问题。

4. 处理系统内存不足时候,导致界面回收,数据丢失的问题

当跳转到其它运行程序时候,系统可能因内存不足,回收了当前的Activity。而Activity当前数据没有保存,即使系统重新创建该Activity后,也会出现空白页面。

当系统因内存不足,回收activity前,会执行onSaveInstanceState(Bundle outState),因此,将拍照后的图片路径存储起来。

private String picturePath;

   /**
     * 防止系统内存不足销毁Activity
     * ,这里保存数据,便于恢复。
     * @param outState
     */
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString(TAG, picturePath);
    }

当系统重新创建该Activity后,从onCreate()中参数中获取,图片路径:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        recoverState(savedInstanceState);
    }
    /**
     * 恢复被系统销毁的数据
     * @param savedInstanceState
     */
    private void recoverState(Bundle savedInstanceState) {
        if (savedInstanceState != null) {
            this.picturePath = savedInstanceState.getString(TAG);
        }
    }

这里,举一个例子:

一个界面需要拍照很多张图片,然后显示出。因需要多次打开相机程序,再返回来加载生成的图片。这种需要,铁定容易碰到以上问题。

Activity被系统回收,具备偶然性,但存在问题,终究还是要处理。

这里,延伸一点:

android保存数据,要么放在内存中,要么放在磁盘中。磁盘读写是IO操作,又得筛选数据,面对这种需求,不推举使用。

5. RxJava异步加载拍照图片,向上取整加载

当拍照完成或者取消,都会在Activity的onActivityResult()中返回结果,是否拍照成功的标示。

在磁盘中生成的图片是一个文件,加载文件是IO操作,耗时,考虑RxJava异步加载。

   @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
            //拍照返回
            case Constance.PICTURE_CODE:
                if (resultCode == Activity.RESULT_OK) {
                    loadPictureBitmap();
                }
                break;
            default:

                break;
        }
    }

   private void loadPictureBitmap() {
       Observable bitmapObservable= ObservableUtils.loadPictureBitmap(getApplicationContext(), picturePath, show_iv);
        executeObservableTask(bitmapObservable);
    }
    private void executeObservableTask(Observable observable) {
        Subscription subscription = observable
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(bitmap ->
                                show_iv.setImageBitmap(bitmap)
                        , error ->
                                ToastUtils.showToast(getApplicationContext(), "加载图片出错")
                );
        this.compositeSubscription.add(subscription);
    }

一个工具类ObservableUtils,构建Observable对象:

public class ObservableUtils {
    /**
     * 加载拍照的相片
     *
     * @param context
     * @param picturePath
     * @param imageView
     * @return
     */
    public static Observable loadPictureBitmap(Context context, String picturePath, ImageView imageView) {
        return Observable.create(subscriber -> {
            Bitmap bitmap = BitmapUtils.decodeFileBitmap(context, picturePath 
                           , imageView.getWidth(), imageView.getHeight());
            subscriber.onNext(bitmap);
        });
    }

}

在Activity中显示的ImageView是具备大小的,按尺寸加载对应比率的Bitamp,可以节省内存。这里采用向上取整方式,计算合适的比率。

public class BitmapUtils {

    /**
     * @param context
     * @param path
     * @param targetWith
     * @param targetHeight
     * @return
     */
    public synchronized static Bitmap decodeFileBitmap(Context context, String path, int targetWith, int targetHeight) {
        try {
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            decodeStreamToBitmap(context, path, options);
            options.inSampleSize = calculateScaleSize(options, targetWith, targetHeight);
            options.inJustDecodeBounds = false;
            Bitmap bitmap = decodeStreamToBitmap(context, path, options);
            return getNormalBitmap(bitmap, path);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private static Bitmap decodeStreamToBitmap(Context context, String path, BitmapFactory.Options options) {
        Bitmap bitmap = null;
        ContentResolver contentResolver = context.getContentResolver();
        try {
            //MIME type需要添加前缀
            InputStream inputStream = contentResolver.openInputStream( 
                      Uri.parse(path.contains("file:") ? path : "file://" + path));
            bitmap = BitmapFactory.decodeStream(inputStream, null, options);
            inputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bitmap;
    }

    /**
     * 采用向上取整的方式,计算压缩尺寸
     *
     * @param options
     * @param targetWith
     * @param targetHeight
     * @return
     */
    private static int calculateScaleSize(BitmapFactory.Options options, int targetWith, int targetHeight) {
        int simpleSize;
        if (targetWith > 0 && targetHeight > 0) {
            int scaleWith = (int) Math.ceil((options.outWidth * 1.0f) / targetWith);
            int scaleHeight = (int) Math.ceil((options.outHeight * 1.0f) / targetHeight);
            simpleSize = Math.max(scaleWith, scaleHeight);
        } else {
            simpleSize = 1;
        }
        if (simpleSize == 0) {
            simpleSize = 1;
        }
        return simpleSize;
    }

}

细心的人会发觉getNormalBitmap(bitmap, path),这个用于处理图片旋转的问题。

6. 处理部分手机拍照后,图片旋转角度问题

当图片角度旋转后,若是直接加载出来,对用户体验是非常差劲的。可通过ExifInterface对象,进行角度判断,加以处理。

 /**
     * 根据存储的bitmap中旋转角度,来创建正常的bitmap
     *
     * @param bitmap
     * @param path
     * @return
     */
    private static Bitmap getNormalBitmap(Bitmap bitmap, String path) {
        int rotate = getBitmapRotate(path);
        Bitmap normalBitmap;
        switch (rotate) {
            case 90:
            case 180:
            case 270:
                try {
                    Matrix matrix = new Matrix();
                    matrix.postRotate(rotate);
                    normalBitmap = Bitmap.createBitmap(bitmap, 0, 0, 
                                 bitmap.getWidth(), bitmap.getHeight(), matrix, true);
                    if (bitmap != null && !bitmap.isRecycled()) {
                        bitmap.recycle();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    normalBitmap = bitmap;
                }
                break;
            default:
                normalBitmap = bitmap;
                break;
        }
        return normalBitmap;
    }

    /**
     * ExifInterface :这个类为jpeg文件记录一些image 的标记
     * 这里,获取图片的旋转角度
     *
     * @param path
     * @return
     */
    private static int getBitmapRotate(String path) {
        int degree = 0;
        try {
            ExifInterface exifInterface = new ExifInterface(path);
            int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, 
                              ExifInterface.ORIENTATION_NORMAL);
            switch (orientation) {
                case ExifInterface.ORIENTATION_ROTATE_90:
                    degree = 90;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_180:
                    degree = 180;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_270:
                    degree = 270;
                    break;
                default:
                    break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return degree;
    }

实现一个完美的拍照功能,填了6个坑,真心不容易,相信不少的开发者都遇到过这些问题。接下来,检验成果的时候到了。

运行效果

Android图库功能


实现图库选择相片的代码很简单,通过Intent开启图库,然后选择需要的图片,会在activity中onActivityResult()中返回Uri。接下来,根据Uri查询到对应的图片路径,最后根据路径加载Bitmap,显示到UI上。

1. 读取权限处理

图库也是需要读取权限的,但上面的拍照功能具备了写入权限,写入权限包含读取权限,因此,这里不需要再做处理。更多详情,可以阅读 Android 6.0 访问图库时,报错 requires android.permission.READ_EXTERNAL_STORAGE异常.

2. 通过Intent开启相册

    /**
     * 打开图库
     * @param context
     * @param requestCode
     */
    public static void openGallery(Activity context, int requestCode) {
        Intent intent = new Intent(Intent.ACTION_PICK, null);
        intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,"image/*");
        context.startActivityForResult(intent, requestCode);
    }

3. 处理图库程序返回的Uri:

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {

            //图库返回
            case Constance.GALLERY_CODE:
                if (resultCode == Activity.RESULT_OK) {
                    Uri uri = data.getData();
                    loadGalleryBitmap(uri);
                }
                break;
            default:

                break;
        }
    }

很多小伙伴们都发觉,在华为某些型号的手机上,通过图库返回的Uri,查询不出来对应的图片路径。这就相当悲催了的事情。

4. 处理华为手机图库查询不到图片路径

除开权限问题外,还有处理Uri的authority问题。

采用RxJava执行异步操作,处理Uri查询图片路径,根据路径加载合适的bitmap。

    private void loadPictureBitmap() {
       Observable bitmapObservable= ObservableUtils.loadPictureBitmap(  
                            getApplicationContext() , picturePath, show_iv);
        executeObservableTask(bitmapObservable);
    }
    private void executeObservableTask(Observable observable) {
        Subscription subscription = observable
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(bitmap ->
                                show_iv.setImageBitmap(bitmap)
                        , error ->
                                ToastUtils.showToast(getApplicationContext(), "加载图片出错")
                );
        this.compositeSubscription.add(subscription);
    }

查询到图片路径后,直接生成对应的bitmap:

public class ObservableUtils {
     /**
     * 加载拍照的相片
     *
     * @param context
     * @param picturePath
     * @param imageView
     * @return
     */
    public static Observable loadPictureBitmap(Context context, String picturePath, ImageView imageView) {
        return Observable.create(subscriber -> {
            Bitmap bitmap = BitmapUtils.decodeFileBitmap(context, picturePath 
                             , imageView.getWidth(), imageView.getHeight());
            subscriber.onNext(bitmap);
        });
    }
    /**
     * 加载图库中选取的相片
     * @param context
     * @param uri
     * @param imageView
     * @return
     */
    public static Observable loadGalleryBitmap(Context context, Uri uri, ImageView imageView) {
        return Observable.create(subscriber -> {
            String picturePath = CameraUtils.uriConvertPath(context, uri);
            subscriber.onNext(picturePath);
        }).flatMap(path -> loadPictureBitmap(context, (String) path, imageView));
    }
}

解决方法来源于网络:

public class CameraUtils {
    /**
     * 从相册中返回的Uri查询到对应图片的Path
     * @param context
     * @param uri
     * @return
     */
    public static String uriConvertPath(Context context,Uri uri){
        String path = null;
        String scheme = uri.getScheme();
        if (scheme.equals("content")) {
            path =getPath(context, uri);
        } else {
            path = uri.getEncodedPath();
        }
        return path;
    }
    /**
     * 
功能简述:4.4及以上获取图片的方法 *
功能详细描述: *
注意: * @param context * @param uri * @return */
@TargetApi(Build.VERSION_CODES.KITKAT) private static String getPath(final Context context, final Uri uri) { final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; // DocumentProvider if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider if (isExternalStorageDocument(uri)) { final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; if ("primary".equalsIgnoreCase(type)) { return Environment.getExternalStorageDirectory() + "/" + split[1]; } } // DownloadsProvider else if (isDownloadsDocument(uri)) { final String id = DocumentsContract.getDocumentId(uri); final Uri contentUri = ContentUris.withAppendedId( Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); return getDataColumn(context, contentUri, null, null); } // MediaProvider else if (isMediaDocument(uri)) { final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; Uri contentUri = null; if ("image".equals(type)) { contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; } else if ("video".equals(type)) { contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; } else if ("audio".equals(type)) { contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; } final String selection = "_id=?"; final String[] selectionArgs = new String[] { split[1] }; return getDataColumn(context, contentUri, selection, selectionArgs); } } // MediaStore (and general) else if ("content".equalsIgnoreCase(uri.getScheme())) { if (isGooglePhotosUri(uri)){ return uri.getLastPathSegment();} return getDataColumn(context, uri, null, null); } // File else if ("file".equalsIgnoreCase(uri.getScheme())) { return uri.getPath(); } return null; } private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { Cursor cursor = null; final String column = "_data"; final String[] projection = { column }; try { cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); if (cursor != null && cursor.moveToFirst()) { final int index = cursor.getColumnIndexOrThrow(column); return cursor.getString(index); } } finally { if (cursor != null){ cursor.close();} } return null; } /** * @param uri The Uri to check. * @return Whether the Uri authority is ExternalStorageProvider. */ private static boolean isExternalStorageDocument(Uri uri) { return "com.android.externalstorage.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is DownloadsProvider. */ private static boolean isDownloadsDocument(Uri uri) { return "com.android.providers.downloads.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is MediaProvider. */ private static boolean isMediaDocument(Uri uri) { return "com.android.providers.media.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is Google Photos. */ private static boolean isGooglePhotosUri(Uri uri) { return "com.google.android.apps.photos.content".equals(uri.getAuthority()); } }

踩完坑,直接看效果如何。

5. 效果如下

Android的拍照和图库选择图片功能介绍完了,期间遇到的坑,心里都有数。本项目的代码也会分享出来,下面有连接。

项目案例:https://github.com/13767004362/EasyPermissionDemo


相关资源

  • Android 6.0 访问图库时,报错 requires android.permission.READ_EXTERNAL_STORAGE异常
  • Android EasyPermissions官方库,高效处理权限
  • Android 7.0 报android.os.FileUriExposedException异常
  • Android FileProvider配置报错android.content.pm.ProviderInfo.loadXmlMetaData问题
  • Android 7.0处理系统裁剪功能异常(适配版)

你可能感兴趣的:(Android,应用层开发)