利用 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

相關文章
相關標籤/搜索