在之前的一篇文章利用 Android 系統(tǒng)原生 API 實(shí)現(xiàn)分享功能中主要說(shuō)了下實(shí)現(xiàn)流程,但具體實(shí)施起來(lái)其實(shí)還是有許多坑要面對(duì)霞玄。那這篇文章就是提供一個(gè)封裝好的 Share2 庫(kù)供大家參考晃虫。
看過(guò)上一篇文章的同學(xué)應(yīng)該知道禽翼,要調(diào)用 Android 系統(tǒng)內(nèi)建的分享功能膳算,主要有三步流程:
創(chuàng)建一個(gè) Intent ,指定其 Action 為 Intent.ACTION_SEND趾浅,表示要?jiǎng)?chuàng)建一個(gè)發(fā)送指定內(nèi)容的隱式意圖愕提。
然后指定需要發(fā)送的內(nèi)容和類型,設(shè)置分享的文本內(nèi)容或文件的Uri皿哨,以及文件的類型浅侨,便于是支持該類型內(nèi)容的應(yīng)用打開(kāi)。
最后向系統(tǒng)發(fā)送隱式意圖证膨,開(kāi)啟系統(tǒng)分享選擇器如输,分享完成后收到結(jié)果返回。
更多相關(guān)內(nèi)容請(qǐng)參考上一篇央勒,這里就不再重復(fù)贅述了不见。
知道大致的實(shí)現(xiàn)流程后,其實(shí)只要解決下面幾個(gè)問(wèn)題后就可以具體實(shí)施了崔步。
確定要分享的內(nèi)容類型
這其實(shí)是直接決定了最終的實(shí)現(xiàn)形態(tài)稳吮,我們知道常見(jiàn)的使用場(chǎng)景中,只是為了在應(yīng)用間分享圖片和一些文件井濒,那對(duì)于那些只是分享文本的產(chǎn)品而言灶似,兩者實(shí)現(xiàn)起來(lái)要考慮的問(wèn)題完全不同。
所以為了解決這個(gè)問(wèn)題瑞你,我們可以預(yù)先定好支持的分享內(nèi)容類型喻奥,針對(duì)不同類型可以進(jìn)行不同的處理。
@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種類別的分享內(nèi)容,基本能覆蓋常見(jiàn)的使用場(chǎng)景润梯。在調(diào)用分享接口時(shí)可以直接指定內(nèi)容類型过牙,比如像文本、圖片纺铭、音視頻寇钉、已經(jīng)其他各種類型文件。
確定分享的內(nèi)容來(lái)源
對(duì)于不同類別的內(nèi)容舶赔,可能會(huì)有不同的來(lái)源扫倡。比如文本可能就只是一個(gè)字符串對(duì)象,而對(duì)于分享圖片或其他文件,我們需要一個(gè) Uri
來(lái)標(biāo)識(shí)一個(gè)資源撵溃。這其實(shí)就引出來(lái)具體實(shí)施時(shí)的一個(gè)大問(wèn)題疚鲤,如何獲取要分享文件的 Uri,并且這個(gè) Uri
要能被接收分享內(nèi)容的應(yīng)用處理才行 缘挑。
通常獲取文件場(chǎng)景有:用戶通過(guò)打開(kāi)文件選擇器或圖片選擇器來(lái)獲取一個(gè)指定的文件集歇;用戶通過(guò)拍照或錄制音視頻來(lái)獲取语淘;用戶下載一個(gè)文件或直接獲取本地某個(gè)文件的路徑來(lái)獲取诲宇。
那么,如何獲取要分享內(nèi)容文件的 Uri惶翻?如果處理才能讓接收方也能夠根據(jù) Uri 獲取到文件姑蓝?
我們把文件 Uri 的來(lái)源劃分為下面三種類型:
系統(tǒng)返回的 Uri
常見(jiàn)場(chǎng)景:通過(guò)文件選擇器獲取一個(gè)文件的 Uri
private static final int REQUEST_FILE_SELECT_CODE = 100;
private @ShareContentType String fileType = ShareContentType. File;
/**
* 打開(kāi)文件管理選擇文件
*/
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) {
// 獲取到的系統(tǒng)返回的 Uri
Uri shareFileUrl = data.getData();
}
}
通過(guò)這種方式獲取到的 Uri 是由系統(tǒng) ContentProvider 返回的,在 Android 4.4 之前的版本和之后的版本有較大的區(qū)別吕粗,我們后面再說(shuō)怎么處理纺荧。只要先記住這種系統(tǒng)返回給我們的 Uri 就行了。
系統(tǒng)返回的一些常見(jiàn) Uri 樣式:
content://com.android.providers.media.documents..
content://com.android.providers.downloads...
content://media/external/images/media/...
content://com.android.externalstorage.documents..
自定義 FileProvider 返回的 Uri
比如調(diào)用系統(tǒng)相機(jī)進(jìn)行拍照或錄制音視頻溯泣,要傳入一個(gè)生成目標(biāo)文件的 Uri
虐秋,從 7.0 開(kāi)始我們需要用到 FileProvider 來(lái)實(shí)現(xiàn)。
private static final int REQUEST_FILE_SELECT_CODE = 100;
/**
* 打開(kāi)系統(tǒng)相機(jī)進(jìn)行拍照
*/
private void openSystemCamera() {
//調(diào)用系統(tǒng)相機(jī)
Intent takePhotoIntent = new Intent();
takePhotoIntent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePhotoIntent.resolveActivity(getPackageManager()) == null) {
Toast.makeText(this, "當(dāng)前系統(tǒng)沒(méi)有可用的相機(jī)應(yīng)用", Toast.LENGTH_SHORT).show();
return;
}
String fileName = "TEMP_" + System.currentTimeMillis() + ".jpg";
File photoFile = new File(FileUtil.getPhotoCacheFolder(), fileName);
// 7.0和以上版本的系統(tǒng)要通過(guò) FileProvider 創(chuàng)建一個(gè) 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);
}
//將拍照結(jié)果保存至 outputFile 的Uri中垃沦,不保留在相冊(cè)中
takePhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, currentTakePhotoUri);
startActivityForResult(takePhotoIntent, TAKE_PHOTO_REQUEST_CODE);
}
// 調(diào)用系統(tǒng)相機(jī)進(jìn)行拍照與上面通過(guò)文件選擇器獲得文件 uri 的方式類似
// 在 onActivityResult 進(jìn)行回調(diào)處理客给,此時(shí) Uri 是你 FileProvider 中指定的,注意與文件選擇器獲取的 Uri 的區(qū)別肢簿。
如果用到了 FileProvider 就要注意跟系統(tǒng) ContentProvider 返回 Uri 的區(qū)別靶剑,比如我們?cè)?Manifest 中對(duì) FileProvider 配置 android:authorities="com.xx.xxx.fileProvider"
屬性,那這時(shí)系統(tǒng)返回的 Uri 格式就變成了 :content://com.xx.xxx.fileProvider...
池充,對(duì)于這種類型的 Uri 我們姑且叫自定義 FileProvider 返回的 Uri桩引,后面一并說(shuō)怎么處理。
文件的路徑
我們調(diào)用 new File 時(shí)需要傳入指定的文件路徑收夸,這個(gè)絕對(duì)路徑通常是:/storage/emulated/0/...
這種樣式坑匠,我們要想調(diào)用分享時(shí)也要變成 Uri 的形式才可以,那么如何把文件路徑變成一個(gè)文件 Uri 卧惜?這個(gè)問(wèn)題下面也一并進(jìn)行回答厘灼。
分享文件 Uri 的處理
前面提到了文件 Uri 的三種分類,對(duì)應(yīng)不同類型處理方式也不同咽瓷,不然你最先遇到的問(wèn)題就是:
java.lang.SecurityException: Uid xxx does not have permission to uri 0 @ content://com.android.providers...
這是由于對(duì)系統(tǒng)返回的 Uri 缺失訪問(wèn)權(quán)限導(dǎo)致设凹,所以要對(duì)應(yīng)用進(jìn)行臨時(shí)訪問(wèn) Uri 的授權(quán)才行,不然會(huì)提示權(quán)限缺失茅姜。
對(duì)于要分享系統(tǒng)返回的 Uri 我們可以這樣進(jìn)行處理:
// 可以對(duì)發(fā)起分享的 Intent 添加臨時(shí)訪問(wèn)授權(quán)
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// 也可以這樣:由于不知道最終用戶會(huì)選擇哪個(gè)app闪朱,所以授予所有應(yīng)用臨時(shí)訪問(wèn)權(quán)限
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);
}
}
需要注意的是對(duì)于自定義 FileProvider 返回 Uri 的處理,即使是設(shè)置臨時(shí)訪問(wèn)權(quán)限,但是分享到第三方應(yīng)用也會(huì)無(wú)法識(shí)別該 Uri
典型的場(chǎng)景就是奋姿,我們?nèi)绻炎远x FileProvider 的返回的 Uri 設(shè)置分享到微信或 QQ 之類的第三方應(yīng)用锄开,會(huì)提示文件不存在,這是因?yàn)樗麄儫o(wú)法識(shí)別該 Uri胀蛮。
關(guān)于這個(gè)問(wèn)題的處理其實(shí)跟下面要說(shuō)的把文件路徑變成系統(tǒng)返回的 Uri 一樣院刁,我們只需要把自定義 FileProvider 返回的 Uri 變成第三方應(yīng)用可以識(shí)別系統(tǒng)返回的 Uri 就行了。
創(chuàng)建 FileProvider 時(shí)需要傳入一個(gè) File 對(duì)象粪狼,所以直接可以知道文件路徑退腥,那就把問(wèn)題都轉(zhuǎn)換成了:如何通過(guò)文件路徑獲取系統(tǒng)返回的 Uri
下面是根據(jù)傳入的 File 對(duì)象和類型來(lái)查詢系統(tǒng) ContentProvider 來(lái)獲取相應(yīng)的 Uri,已經(jīng)按照不同文件類型在不同系統(tǒng)版本下的進(jìn)行了適配再榄。
其中 forceGetFileUri
方法是通過(guò)反射實(shí)現(xiàn)的狡刘,處理 7.0 以上系統(tǒng)的特殊情況下的兼容性,一般情況下不會(huì)調(diào)用到困鸥。Android 7.0 開(kāi)始不允許 file://
Uri 的方式在不同的 App 間共享文件嗅蔬,但是如果換成 FileProvider 的方式依然是無(wú)效的,我們可以通過(guò)反射把該檢測(cè)干掉疾就。
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());
}
通過(guò) File Path 轉(zhuǎn)成 Uri 的方式澜术,我們最終統(tǒng)一了調(diào)用系統(tǒng)分享時(shí)傳入內(nèi)容 Uri 的三種不同場(chǎng)景,最終全部轉(zhuǎn)換為傳遞系統(tǒng)返回的 Uri猬腰,讓第三方應(yīng)用能夠正常的獲取到分享內(nèi)容鸟废。
最終實(shí)現(xiàn)
Share2 按照上述方法進(jìn)行了具體實(shí)施,可以通過(guò)下面的方式進(jìn)行集成:
// 添加依賴
compile 'gdut.bsx:share2:0.9.0'
根據(jù) 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();