利用 Android 系統原生 API 實現分享功能


利用 Android 系統原生 API 實現分享功能

這篇文章提供一個封裝好的 Share2 庫供大家參考。

GitHub 項目地址:Share2


大家知道,要調用 Android 系統內建的分享功能,主要有三步流程:

  • 創建一個 Intent ,指定其 Action 為 Intent.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 要能被接收分享內容的應用處理才行 。

通常獲取文件場景有:用戶通過打開文件選擇器或圖片選擇器來獲取一個指定的文件;用戶通過拍照或錄制音視頻來獲取;用戶下載一個文件或直接獲取本地某個文件的路徑來獲取。

那么,如何獲取要分享內容文件的 Uri?如果處理才能讓接收方也能夠根據 Uri 獲取到文件?

我們把文件 Uri 的來源划分為下面三種類型:

系統返回的 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..

自定義 FileProvider 返回的 Uri

比如調用系統相機進行拍照或錄制音視頻,要傳入一個生成目標文件的 Uri,從 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,后面一並說怎么處理。

文件的路徑

我們調用 new File 時需要傳入指定的文件路徑,這個絕對路徑通常是:/storage/emulated/0/... 這種樣式,我們要想調用分享時也要變成 Uri 的形式才可以,那么如何把文件路徑變成一個文件 Uri ?這個問題下面也一並進行回答。

分享文件 Uri 的處理

前面提到了文件 Uri 的三種分類,對應不同類型處理方式也不同,不然你最先遇到的問題就是:

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

這是由於對系統返回的 Uri 缺失訪問權限導致,所以要對應用進行臨時訪問 Uri 的授權才行,不然會提示權限缺失。

對於要分享系統返回的 Uri 我們可以這樣進行處理:

// 可以對發起分享的 Intent 添加臨時訪問授權 shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 也可以這樣:由於不知道最終用戶會選擇哪個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 的處理,即使是設置臨時訪問權限,但是分享到第三方應用也會無法識別該 Uri

典型的場景就是,我們如果把自定義 FileProvider 的返回的 Uri 設置分享到微信或 QQ 之類的第三方應用,會提示文件不存在,這是因為他們無法識別該 Uri。

關於這個問題的處理其實跟下面要說的把文件路徑變成系統返回的 Uri 一樣,我們只需要把自定義 FileProvider 返回的 Uri 變成第三方應用可以識別系統返回的 Uri 就行了。

創建 FileProvider 時需要傳入一個 File 對象,所以直接可以知道文件路徑,那就把問題都轉換成了:如何通過文件路徑獲取系統返回的 Uri

下面是根據傳入的 File 對象和類型來查詢系統 ContentProvider 來獲取相應的 Uri,已經按照不同文件類型在不同系統版本下的進行了適配。

其中 forceGetFileUri 方法是通過反射實現的,處理 7.0 以上系統的特殊情況下的兼容性,一般情況下不會調用到。Android 7.0 開始不允許 file:// Uri 的方式在不同的 App 間共享文件,但是如果換成 FileProvider 的方式依然是無效的,我們可以通過反射把該檢測干掉。

   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()); } 

通過 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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM