利用 Android 系统原生 API 实现分享功能(2)

在以前的一篇文章 利用 Android 系统原生 API 实现分享功能 中主要说了下实现流程,但具体实施起来其实仍是有许多坑要面对。那这篇文章就是提供一个封装好的 Share2 库供你们参考。java

GitHub 项目地址:Share2android


看过上一篇文章的同窗应该知道,要调用 Android 系统内建的分享功能,主要有三步流程:git

  • 建立一个 Intent ,指定其 ActionIntent.ACTION_SEND,表示要建立一个发送指定内容的隐式意图。github

  • 而后指定须要发送的内容和类型,即设置分享的文本内容或文件的 Uri ,以及声明文件的类型,便于支持该类型内容的应用打开。bash

  • 最后向系统发送隐式意图,开启系统分享选择器,分享完成后收到结果返回。微信

更多相关内容请参考上一篇,这里就再也不重复赘述了。app


知道大体的实现流程后,其实只要解决下面几个问题后就能够具体实施了。ide

肯定要分享的内容类型

分享的内容类型,这实际上是直接决定了最终的实现形态。咱们知道常见的使用场景中,是为了在应用间分享图片和一些文件,而对于那些只是分享文本的产品而言,二者实现起来要考虑的问题彻底不一样。post

因此为了解决这个问题,咱们能够预先定好支持的分享内容类型,针对不一样类型能够进行不一样的处理。ui

@StringDef({ShareContentType.TEXT, ShareContentType.IMAGE, ShareContentType.AUDIO, ShareContentType.VIDEO, ShareContentType.File})
@Retention(RetentionPolicy.SOURCE)
@interface ShareContentType {
    /**
     * Share Text
     */
    final String TEXT = "text/plain";

    /**
     * Share Image
     */
    final String IMAGE = "image/*";

    /**
     * Share Audio
     */
    final String AUDIO = "audio/*";

    /**
     * Share Video
     */
    final String VIDEO = "video/*";

    /**
     * Share File
     */
    final String File = "*/*";
}`
复制代码

在 Share2 中,一共定义了 5 种类别的分享内容,基本能覆盖常见的使用场景。在调用分享接口时能够直接指定内容类型,好比像文本、图片、音视频、以及其余各类类型文件。

肯定分享的内容来源

对于不一样类型的内容,可能会有不一样的来源。好比文本可能就只是一个字符串对象。而对于分享图片或其余文件,咱们一般须要一个 Uri 来标识一个资源。这其实就引出了在具体实施时的一个关键问题:如何获取被分享文件的 Uri,而且这个 Uri 能够被接收的应用处理?

再把这个问题进一步细化,转化为须要解决的具体问题时就是:

  1. 如何获取要分享内容文件的 Uri
  2. 如何才能让接收方也可以根据 Uri 获取到文件?

要回答上面这些问题,咱们先来看看分享文件的来源。一般咱们在应用中获取一个文件的具体方式有:

  • 用户经过打开文件选择器或图片选择器来获取一个指定的文件;
  • 用户经过拍照或录制音视频来获取一个媒体文件;
  • 用户经过下载或直接经过本地文件路径来获取一个文件。

那下面咱们就按照获取文件来源把文件的 Uri 划分为下面几种类型:

1. 系统返回的 Uri

常见场景:经过文件选择器获取一个文件的 Uri

private static final int REQUEST_FILE_SELECT_CODE = 100;
  private @ShareContentType String fileType = ShareContentType. File;

  /**
   * 打开文件管理选择文件
   */
   private void openFileChooser() {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("*/*");
        intent.addCategory(Intent.CATEGORY_OPENABLE);

        try {
            startActivityForResult(Intent.createChooser(intent, "Choose File"), REQUEST_FILE_SELECT_CODE);
        } catch (Exception ex) {
            // not install file manager.
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == FILE_SELECT_CODE && resultCode == RESULT_OK) {
            // 获取到的系统返回的 Uri
            Uri shareFileUrl = data.getData();
        }
    }
复制代码

经过这种方式获取到的 Uri 是由系统 ContentProvider 返回的,在 Android 4.4 以前的版本和以后的版本有较大的区别,咱们后面再说怎么处理。只要先记住这种系统返回给咱们的 Uri 就好了。

系统返回的文件 Uri 中的一些常见样式: content://com.android.providers.media.documents.. content://com.android.providers.downloads... content://media/external/images/media/... content://com.android.externalstorage.documents..

2. 自定义 FileProvider 返回的 Uri

常见场景:好比调用系统相机进行拍照或录制音视频,要传入一个生成目标文件的 Uri,从 Android 7.0 开始咱们须要用到 FileProvider 来实现。

private static final int REQUEST_FILE_SELECT_CODE = 100;
   /**
     * 打开系统相机进行拍照
     */
    private void openSystemCamera() {
        //调用系统相机
        Intent takePhotoIntent = new Intent();
        takePhotoIntent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);

        if (takePhotoIntent.resolveActivity(getPackageManager()) == null) {
            Toast.makeText(this, "当前系统没有可用的相机应用", Toast.LENGTH_SHORT).show();
            return;
        }

        String fileName = "TEMP_" + System.currentTimeMillis() + ".jpg";
        File photoFile = new File(FileUtil.getPhotoCacheFolder(), fileName);

        // 7.0 和以上版本的系统要经过 FileProvider 建立一个 content 类型的 Uri
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            currentTakePhotoUri = FileProvider.getUriForFile(this, getPackageName() + ".fileProvider", photoFile);
            takePhotoIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION|);
        } else {
            currentTakePhotoUri = Uri.fromFile(photoFile);
        }

        //将拍照结果保存至 outputFile 的Uri中,不保留在相册中
        takePhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, currentTakePhotoUri);
        startActivityForResult(takePhotoIntent, TAKE_PHOTO_REQUEST_CODE);
    }

     // 调用系统相机进行拍照与上面经过文件选择器得到文件 uri 的方式相似
     // 在 onActivityResult 进行回调处理,此时 Uri 是自定义 FileProvider 中指定的,注意与文件选择器获取的系统返回 Uri 的区别。
复制代码

若是用到了 FileProvider 就要注意跟系统 ContentProvider 返回 Uri 的区别,好比咱们在 Manifest 中对 FileProvider 配置 android:authorities="com.xx.xxx.fileProvider" 属性,那这时系统返回的 Uri 格式就变成了:content://com.xx.xxx.fileProvider...,对于这种类型的 Uri 咱们姑且叫自定义 FileProvider 返回的 Uri

3. 经过文件的路径获取到的 Uri

这其实不能单独做为一种文件 Uri 类型,但这是很常见的一种调用场景,因此单独拿出来进行说明。

咱们调用 new File(String path) 时须要传入指定的文件路径,这个绝对路径一般是:/storage/emulated/0/... 这种样式,那么如何把一个文件路径变成一个文件 Uri 的形式?要回答这个问题,其实就须要对分享文件进行处理。

分享文件 Uri 的处理

处理访问权限

前面提到了文件 Uri 的三种来源,对应不一样类型处理方式也不一样,否则你最早遇到的问题就是:

java.lang.SecurityException: Uid xxx does not have permission to uri 0 @ content://com.android.providers...
复制代码

这是因为对系统返回的 Uri 缺失访问权限致使,因此要对应用进行临时访问 Uri 的受权才行,否则会提示权限缺失。

对于要分享系统返回的 Uri 咱们能够这样进行处理:

// 1. 能够对发起分享的 Intent 添加临时访问受权
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

// 2. 也能够这样:因为不知道最终用户会选择哪一个app,因此授予全部应用临时访问权限
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
    List<ResolveInfo> resInfoList = activity.getPackageManager().queryIntentActivities(shareIntent, PackageManager.MATCH_DEFAULT_ONLY);
    for (ResolveInfo resolveInfo : resInfoList) {
        String packageName = resolveInfo.activityInfo.packageName;
        activity.grantUriPermission(packageName, shareFileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
    }
}
复制代码

处理 FileProvider 返回 Uri

须要注意的是对于自定义 FileProvider 返回 Uri 的处理,即便是设置临时访问权限,可是分享到第三方应用也会没法识别该 Uri

典型的场景就是,咱们若是把自定义 FileProvider 的返回的 Uri 设置分享到微信或 QQ 之类的第三方应用时提示文件不存在,这是由于他们没法识别该 Uri

关于这个问题的处理其实跟下面要说的把文件路径变成系统返回的 Uri 同样,咱们只须要把自定义 FileProvider 返回的 Uri 变成第三方应用能够识别系统返回的 Uri 就好了。

建立 FileProvider 时须要传入一个 File 对象,因此直接能够知道文件路径,那就把问题都转换成了:如何经过文件路径获取系统返回的 Uri

经过文件路径获取系统返回的 Uri

对于 Android 7.0 如下版本的系统,要回答这个问题很简单:

Uri uri = Uri.fromFile(file);
复制代码

但在 Android 7.0 及以上系统处理起来就要繁琐许多,下面就来讲说如何在不一样系统版本下的进行适配。下面的 getFileUri 方法实现了经过传入的 File 对象和类型来查询系统 ContentProvider 的方式获取相应的文件 Uri

public static Uri getFileUri (Context context, @ShareContentType String shareContentType, File file){

        if (context == null) {
            Log.e(TAG,"getFileUri current activity is null.");
            return null;
        }

        if (file == null || !file.exists()) {
            Log.e(TAG,"getFileUri file is null or not exists.");
            return null;
        }

        Uri uri = null;
        
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            uri = Uri.fromFile(file);
        } else {

            if (TextUtils.isEmpty(shareContentType)) {
                shareContentType = "*/*";
            }

            switch (shareContentType) {
                case ShareContentType.IMAGE :
                    uri = getImageContentUri(context, file);
                    break;
                case ShareContentType.VIDEO :
                    uri = getVideoContentUri(context, file);
                    break;
                case ShareContentType.AUDIO :
                    uri = getAudioContentUri(context, file);
                    break;
                case ShareContentType.File :
                    uri = getFileContentUri(context, file);
                    break;
                default: break;
            }
        }
        
        if (uri == null) {
            uri = forceGetFileUri(file);
        }
        
        return uri;
    }


    private static Uri getFileContentUri(Context context, File file) {
        String volumeName = "external";
        String filePath = file.getAbsolutePath();
        String[] projection = new String[]{MediaStore.Files.FileColumns._ID};
        Uri uri = null;

        Cursor cursor = context.getContentResolver().query(MediaStore.Files.getContentUri(volumeName), projection,
                MediaStore.Images.Media.DATA + "=? ", new String[] { filePath }, null);
        if (cursor != null) {
            if (cursor.moveToFirst()) {
                int id = cursor.getInt(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID));
                uri = MediaStore.Files.getContentUri(volumeName, id);
            }
            cursor.close();
        }

        return uri;
    }

    private static Uri getImageContentUri(Context context, File imageFile) {
        String filePath = imageFile.getAbsolutePath();
        Cursor cursor = context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                new String[] { MediaStore.Images.Media._ID }, MediaStore.Images.Media.DATA + "=? ",
                new String[] { filePath }, null);
        Uri uri = null;

        if (cursor != null) {
            if (cursor.moveToFirst()) {
                int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
                Uri baseUri = Uri.parse("content://media/external/images/media");
                uri = Uri.withAppendedPath(baseUri, "" + id);
            }
            
            cursor.close();
        }
        
        if (uri == null) {
            ContentValues values = new ContentValues();
            values.put(MediaStore.Images.Media.DATA, filePath);
            uri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
        }

        return uri;
    }

    private static Uri getVideoContentUri(Context context, File videoFile) {
        Uri uri = null;
        String filePath = videoFile.getAbsolutePath();
        Cursor cursor = context.getContentResolver().query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
                new String[] { MediaStore.Video.Media._ID }, MediaStore.Video.Media.DATA + "=? ",
                new String[] { filePath }, null);
        
        if (cursor != null) { 
            if (cursor.moveToFirst()) { 
                int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
                Uri baseUri = Uri.parse("content://media/external/video/media");
                uri = Uri.withAppendedPath(baseUri, "" + id);
            }
            
            cursor.close();
        } 
        
        if (uri == null) {
            ContentValues values = new ContentValues();
            values.put(MediaStore.Video.Media.DATA, filePath);
            uri = context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
        }
        
        return uri;
    }


    private static Uri getAudioContentUri(Context context, File audioFile) {
        Uri uri = null;
        String filePath = audioFile.getAbsolutePath();
        Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                new String[] { MediaStore.Audio.Media._ID }, MediaStore.Audio.Media.DATA + "=? ",
                new String[] { filePath }, null);
        
        if (cursor != null) {
            if (cursor.moveToFirst()) {
                int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
                Uri baseUri = Uri.parse("content://media/external/audio/media");
                uri = Uri.withAppendedPath(baseUri, "" + id);
            }
            
            cursor.close();
        }
        if (uri == null) {
            ContentValues values = new ContentValues();
            values.put(MediaStore.Audio.Media.DATA, filePath);
            uri = context.getContentResolver().insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values);
        } 
        
        return uri;
    }

    private static Uri forceGetFileUri(File shareFile) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            try {
                @SuppressLint("PrivateApi")
                Method rMethod = StrictMode.class.getDeclaredMethod("disableDeathOnFileUriExposure");
                rMethod.invoke(null);
            } catch (Exception e) {
                Log.e(TAG, Log.getStackTraceString(e));
            }
        }

        return Uri.parse("file://" + shareFile.getAbsolutePath());
    }
复制代码

其中 forceGetFileUri 方法是经过反射实现的,Android 7.0 开始不容许 file:// Uri 的方式在不一样的 App 间共享文件,可是若是换成 FileProvider 的方式依然是无效的,咱们能够经过反射把该检测干掉。

经过 File Path 转成 Uri 的方式,咱们最终统一了调用系统分享时传入内容 Uri 的三种不一样场景,最终所有转换为传递系统返回的 Uri,让第三方应用可以正常的获取到分享内容。

最终实现

Share2 按照上述方法进行了具体实施,能够经过下面的方式进行集成:

// 添加依赖
compile 'gdut.bsx:share2:0.9.0'
复制代码

根据 FilePath 获取 Uri

public Uri getShareFileUri() {
       return FileUtil.getFileUri(this, ShareContentType.FILE, new File(filePath));;
 }
复制代码

分享文本

new Share2.Builder(this)
    .setContentType(ShareContentType.TEXT)
    .setTextContent("This is a test message.")
    .setTitle("Share Text")
    .build()
    .shareBySystem();
复制代码

分享图片

new Share2.Builder(this)
    .setContentType(ShareContentType.IMAGE)
    .setShareFileUri(getShareFileUri())
    .setTitle("Share Image")
    .build()
    .shareBySystem();
复制代码

分享图片到指定界面,好比分享到微信朋友圈

new Share2.Builder(this)
    .setContentType(ShareContentType.IMAGE)
    .setShareFileUri(getShareFileUri())
    .setShareToComponent("com.tencent.mm", "com.tencent.mm.ui.tools.ShareToTimeLineUI")
    .setTitle("Share Image To WeChat")
    .build()
    .shareBySystem();
复制代码

分享文件

new Share2.Builder(this)
    .setContentType(ShareContentType.FILE)
    .setShareFileUri(getShareFileUri())
    .setTitle("Share File")
    .build()
    .shareBySystem();
复制代码

最终效果

GitHub 项目地址:Share2

相关文章
相关标签/搜索