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

·  阅读 8476

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

GitHub 项目地址:Share2


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

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

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

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

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


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

确定要分享的内容类型

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

所以为了解决这个问题,我们可以预先定好支持的分享内容类型,针对不同类型可以进行不同的处理。

@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