對于 App 的分享功能,基本上是一個剛需,本文主要介紹運用系統(tǒng)原生分享功能時候需要注意的一些問題埠帕。對于某些特定平臺的一些高級分享特性,比如微信或者微博之類的分享來源標(biāo)注玖绿,需要在其開放平臺注冊應(yīng)用再接入其 sdk 才可以敛瓷,這里不予以討論。打算借助第三方庫類似 ShareSDK 實現(xiàn)的同學(xué)們斑匪,這篇文章可能也幫不上你呐籽。
什么是 Android 系統(tǒng)的原生分享
直接上圖,這是一個典型的調(diào)用系統(tǒng)原生分享場景下的界面,相信大家應(yīng)該都很熟悉狡蝶。
系統(tǒng)內(nèi)建的分享機制庶橱,參照官方的教程,基本上可以滿足你的一般需求:Android-training-building-content-sharing
簡單描述下創(chuàng)建分享的主要過程:
- 創(chuàng)建一個
Intent
贪惹,指定其Action
為Intent.ACTION_SEND
苏章,這表示要創(chuàng)建一個發(fā)送指定內(nèi)容的隱式意圖。
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
- 指定需要發(fā)送的內(nèi)容和類型奏瞬。
// 比如發(fā)送文本形式的數(shù)據(jù)內(nèi)容
// 指定發(fā)送的內(nèi)容
sendIntent.putExtra(Intent.EXTRA_TEXT, "This is my text to send.");
// 指定發(fā)送內(nèi)容的類型
sendIntent.setType("text/plain");
// 比如發(fā)送二進(jìn)制文件數(shù)據(jù)流內(nèi)容(比如圖片枫绅、視頻、音頻文件等等)
// 指定發(fā)送的內(nèi)容 (EXTRA_STREAM 對于文件 Uri )
shareIntent.putExtra(Intent.EXTRA_STREAM, uriToImage);
// 指定發(fā)送內(nèi)容的類型 (MIME type)
shareIntent.setType("image/jpeg");
- 向系統(tǒng)發(fā)送隱式意圖硼端,打開系統(tǒng)分享選擇器撑瞧,出現(xiàn)如上圖所示界面。
startActivity(Intent.createChooser(shareIntent, “Share to...”));
四不四看起來很簡單显蝌,四不四感覺可以分分鐘可以搞定。年輕人订咸,我跟你港曼尊,別圖樣圖森破,現(xiàn)在大家沒遇幾個坑都不好意思出來港脏嚷,不過做人嘛~最重要的開心骆撇。
那下面說一下遇到的一些問題,特別針對是 7.0 以后的系統(tǒng)父叙,以及兼容一些主流 app 時遇到的坑神郊。
1. 獲取文件類型(MimeType)
前面說到分享文件時需要知道文件的類型,不然的指定類型為 */*
趾唱,這樣分享到某些 App 會因為無法判斷文件類型而導(dǎo)致失敗涌乳,所以最好先根據(jù)文件路徑獲取其文件類型。
下面是一些常見文件的mimeType
{".3gp", "video/3gpp"},
{".apk", "application/vnd.android.package-archive"},
{".asf", "video/x-ms-asf"},
{".avi", "video/x-msvideo"},
{".bin", "application/octet-stream"},
{".bmp", "image/bmp"},
{".c", "text/plain"},
{".class", "application/octet-stream"},
{".conf", "text/plain"},
{".cpp", "text/plain"},
{".doc", "application/msword"},
{".exe", "application/octet-stream"},
{".gif", "image/gif"},
{".gtar", "application/x-gtar"},
{".gz", "application/x-gzip"},
{".h", "text/plain"},
{".htm", "text/html"},
{".html", "text/html"},
{".jar", "application/java-archive"},
{".java", "text/plain"},
{".jpeg", "image/jpeg"},
{".jpg", "image/jpeg"},
{".js", "application/x-javascript"},
{".log", "text/plain"},
{".m3u", "audio/x-mpegurl"},
{".m4a", "audio/mp4a-latm"},
{".m4b", "audio/mp4a-latm"},
{".m4p", "audio/mp4a-latm"},
{".m4u", "video/vnd.mpegurl"},
{".m4v", "video/x-m4v"},
{".mov", "video/quicktime"},
{".mp2", "audio/x-mpeg"},
{".mp3", "audio/x-mpeg"},
{".mp4", "video/mp4"},
{".mpc", "application/vnd.mpohun.certificate"},
{".mpe", "video/mpeg"},
{".mpeg", "video/mpeg"},
{".mpg", "video/mpeg"},
{".mpg4", "video/mp4"},
{".mpga", "audio/mpeg"},
{".msg", "application/vnd.ms-outlook"},
{".ogg", "audio/ogg"},
{".pdf", "application/pdf"},
{".png", "image/png"},
{".pps", "application/vnd.ms-powerpoint"},
{".ppt", "application/vnd.ms-powerpoint"},
{".prop", "text/plain"},
{".rar", "application/x-rar-compressed"},
{".rc", "text/plain"},
{".rmvb", "audio/x-pn-realaudio"},
{".rtf", "application/rtf"},
{".sh", "text/plain"},
{".tar", "application/x-tar"},
{".tgz", "application/x-compressed"},
{".txt", "text/plain"},
{".wav", "audio/x-wav"},
{".wma", "audio/x-ms-wma"},
{".wmv", "audio/x-ms-wmv"},
{".wps", "application/vnd.ms-works"},
//{".xml", "text/xml"},
{".xml", "text/plain"},
{".z", "application/x-compress"},
{".zip", "application/zip"},
{"", "*/*"}
獲取文件類型的方法:
方式一(方便但不穩(wěn)定):通過 ContentResolver 查詢 Android 系統(tǒng)提供的 ContentProvider 獲取
當(dāng) targetSdkVersion >= 24 時使用 Uri.fromFile(File file) 獲取文件
uri 會報 android.os.FileUriExposedException 異常 甜癞,應(yīng)該要使用
FileProvider 夕晓,具體請參考 Android 7.0 FileProvider 適配相關(guān),這里不再展開說明悠咱。關(guān)于 FileProvider 推薦一篇總結(jié)比較好的文章蒸辆。
// 獲取文件的 url
File shareFile = new File(shareFilePath);
Uri fileUri = Uri.fromFile(shareFile);
// 獲取系統(tǒng)的提供的 ContentResolver
ContentResolver contentResolver = getApplicationContext().getContentResolver();
// 獲取文件MimeType,如 image/png
String fileMimeType = contentResolver.getType(fileUri);
// 獲取文件Type析既,如 png
MimeTypeMap mime = MimeTypeMap.getSingleton();
String fileType = mime.getExtensionFromMimeType(fileMimeType);
使用這種方法獲取文件類型躬贡,一定要注意 ContentResolver 獲取返回為 null 的情況,不然空指針異常的崩潰率可能會讓你笑不出來眼坏。實際測試中拂玻,發(fā)現(xiàn)在某些國產(chǎn)機型下,這個方法可以說直接是不可用,查詢返回一直都是空纺讲,所以單純依賴這一個方法會很不可靠擂仍。具體問題原因請看:What causes Android's ContentResolver.query() to return null?
方式二 解析文件信息,通過匹配識別判斷:
在好用的方法卻不可靠的情況下熬甚,只能配合看起來蠢一點的方法逢渔。目前大致的思路有兩種:
1.識別文件后綴,根據(jù)后綴名來判斷文件類型乡括。
2.獲取文件頭信息肃廓,轉(zhuǎn)成十六進(jìn)制字符串后判斷文件類型。
這兩種都是根據(jù)特點信息去做匹配诲泌,因此需要先保存一份文件特點信息和文件類型的對應(yīng)參照表盲赊。
下面按照第二條思路,按照文件頭信息簡單實現(xiàn)一個獲取文件類型的例子:
/**
* 獲取文件類型
* @param filePath
* @return
*/
public static String getFileType(String filePath) {
return mFileTypes.get(getFileHeader(filePath));
}
private static final HashMap<String, String> mFileTypes = new HashMap<String, String>();
// judge file type by file header content
static {
mFileTypes.put("ffd8ffe000104a464946", "jpg"); //JPEG (jpg)
mFileTypes.put("89504e470d0a1a0a0000", "png"); //PNG (png)
mFileTypes.put("47494638396126026f01", "gif"); //GIF (gif)
mFileTypes.put("49492a00227105008037", "tif"); //TIFF (tif)
mFileTypes.put("424d228c010000000000", "bmp"); //16色位圖(bmp)
mFileTypes.put("424d8240090000000000", "bmp"); //24位位圖(bmp)
mFileTypes.put("424d8e1b030000000000", "bmp"); //256色位圖(bmp)
mFileTypes.put("41433130313500000000", "dwg"); //CAD (dwg)
mFileTypes.put("3c21444f435459504520", "html"); //HTML (html)
mFileTypes.put("3c21646f637479706520", "htm"); //HTM (htm)
mFileTypes.put("48544d4c207b0d0a0942", "css"); //css
mFileTypes.put("696b2e71623d696b2e71", "js"); //js
mFileTypes.put("7b5c727466315c616e73", "rtf"); //Rich Text Format (rtf)
mFileTypes.put("38425053000100000000", "psd"); //Photoshop (psd)
mFileTypes.put("46726f6d3a203d3f6762", "eml"); //Email [Outlook Express 6] (eml)
mFileTypes.put("d0cf11e0a1b11ae10000", "doc"); //MS Excel 注意:word敷扫、msi 和 excel的文件頭一樣
mFileTypes.put("d0cf11e0a1b11ae10000", "vsd"); //Visio 繪圖
mFileTypes.put("5374616E64617264204A", "mdb"); //MS Access (mdb)
mFileTypes.put("252150532D41646F6265", "ps");
mFileTypes.put("255044462d312e350d0a", "pdf"); //Adobe Acrobat (pdf)
mFileTypes.put("2e524d46000000120001", "rmvb"); //rmvb/rm相同
mFileTypes.put("464c5601050000000900", "flv"); //flv與f4v相同
mFileTypes.put("00000020667479706d70", "mp4");
mFileTypes.put("49443303000000002176", "mp3");
mFileTypes.put("000001ba210001000180", "mpg"); //
mFileTypes.put("3026b2758e66cf11a6d9", "wmv"); //wmv與asf相同
mFileTypes.put("52494646e27807005741", "wav"); //Wave (wav)
mFileTypes.put("52494646d07d60074156", "avi");
mFileTypes.put("4d546864000000060001", "mid"); //MIDI (mid)
mFileTypes.put("504b0304140000000800", "zip");
mFileTypes.put("526172211a0700cf9073", "rar");
mFileTypes.put("235468697320636f6e66", "ini");
mFileTypes.put("504b03040a0000000000", "jar");
mFileTypes.put("4d5a9000030000000400", "exe");//可執(zhí)行文件
mFileTypes.put("3c25402070616765206c", "jsp");//jsp文件
mFileTypes.put("4d616e69666573742d56", "mf");//MF文件
mFileTypes.put("3c3f786d6c2076657273", "xml");//xml文件
mFileTypes.put("494e5345525420494e54", "sql");//xml文件
mFileTypes.put("7061636b616765207765", "java");//java文件
mFileTypes.put("406563686f206f66660d", "bat");//bat文件
mFileTypes.put("1f8b0800000000000000", "gz");//gz文件
mFileTypes.put("6c6f67346a2e726f6f74", "properties");//bat文件
mFileTypes.put("cafebabe0000002e0041", "class");//bat文件
mFileTypes.put("49545346030000006000", "chm");//bat文件
mFileTypes.put("04000000010000001300", "mxp");//bat文件
mFileTypes.put("504b0304140006000800", "docx");//docx文件
mFileTypes.put("d0cf11e0a1b11ae10000", "wps");//WPS文字wps哀蘑、表格et、演示dps都是一樣的
mFileTypes.put("6431303a637265617465", "torrent");
mFileTypes.put("6D6F6F76", "mov"); //Quicktime (mov)
mFileTypes.put("FF575043", "wpd"); //WordPerfect (wpd)
mFileTypes.put("CFAD12FEC5FD746F", "dbx"); //Outlook Express (dbx)
mFileTypes.put("2142444E", "pst"); //Outlook (pst)
mFileTypes.put("AC9EBD8F", "qdf"); //Quicken (qdf)
mFileTypes.put("E3828596", "pwl"); //Windows Password (pwl)
mFileTypes.put("2E7261FD", "ram"); //Real Audio (ram)
mFileTypes.put("null", null); //null
}
/**
* 獲取文件頭信息
* @param filePath
* @return
*/
public static String getFileHeader(String filePath) {
File file=new File(filePath);
if(!file.exists() || file.length()<11){
return "null";
}
FileInputStream is = null;
String value = null;
try {
is = new FileInputStream(file);
byte[] b = new byte[10];
is.read(b, 0, b.length);
value = bytesToHexString(b);
} catch (Exception e) {
} finally {
if(null != is) {
try {
is.close();
} catch (IOException e) {}
}
}
return value;
}
/**
* 將byte字節(jié)轉(zhuǎn)換為十六進(jìn)制字符串
* @param src
* @return
*/
private static String bytesToHexString(byte[] src) {
StringBuilder builder = new StringBuilder();
if (src == null || src.length <= 0) {
return null;
}
String hv;
for (int i = 0; i < src.length; i++) {
hv = Integer.toHexString(src[i] & 0xFF).toUpperCase();
if (hv.length() < 2) {
builder.append(0);
}
builder.append(hv);
}
return builder.toString();
}
2. 獲取分享文件的Uri進(jìn)行分享
前面也有提到葵第,在 Android 7.0 以后绘迁,系統(tǒng)對 scheme 為 file:// 的 uri 進(jìn)行了限制,所以之前進(jìn)行文件分享的一些接口就不能用了卒密,此時就得使用其他的URI scheme 來代替 file://缀台,比如 MediaStore 的 content:// 或者FileProvider 。
public static void shareFile(Context context, String filePath) {
if (context == null || TextUtils.isEmpty(filePath)){
LogUtil.e("shareFile context is null or filePath is empty.");
return;
}
File file = new File(filePath);
if (file != null && file.exists()){
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addCategory("android.intent.category.DEFAULT");
// 如果需要指定分享到某個app哮奇,配置 componentName 即可
if (!TextUtils.isEmpty(componentName) && "com.tencent.mm".equals(componentName)){
// 分享精確到微信的頁面膛腐,朋友圈頁面,或者選擇好友分享頁面
ComponentName comp = new ComponentName("com.tencent.mm", "com.tencent.mm.ui.tools.ShareToTimeLineUI");
intent.setComponent(comp);
}
intent.putExtra(Intent.EXTRA_STREAM, uri);
// 授予目錄臨時共享權(quán)限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
String fileType;
Uri fileUri = getFileUri(context, file);
if (fileUri != null && !TextUtils.isEmpty(fileUri.toString())) {
ContentResolver contentResolver = context.getContentResolver();
fileType = contentResolver.getType(fileUri);
}
if (TextUtils.isEmpty(fileType)){
fileType = getFileType(filePath); // 使用上面的根據(jù)文件頭信息獲取文件類型的方法
}
if (TextUtils.isEmpty(fileType)){
fileType = "*/*"
}
LogUtil.d("shareFile fileType " + fileType);
LogUtil.d("shareFile uri: " + uri);
intent.setDataAndType(uri, fileType);
try {
context.startActivity(Intent.createChooser(intent, file.getName()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 獲取文件Uri
public static Uri getFileUri(Context context, File file){
Uri uri;
// 低版本直接用 Uri.fromFile
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
uri = Uri.fromFile(file);
}else {
// 使用 FileProvider 會在某些 app 下不支持(在使用FileProvider 方式情況下QQ不能支持圖片鼎俘、視頻分享哲身,微信不支持視頻分享)
uri = FileProvider.getUriForFile(context,
"gdut.bsx.videoreverser.fileprovider",
file);
ContentResolver cR = context.getContentResolver();
if (uri != null && !TextUtils.isEmpty(uri.toString())) {
String fileType = cR.getType(uri);
// 使用 MediaStore 的 content:// 而不是自己 FileProvider 提供的uri,不然有些app無法適配
if (!TextUtils.isEmpty(fileType)){
if (fileType.contains("video/")){
uri = getVideoContentUri(context, file);
}else if (fileType.contains("image/")){
uri = getImageContentUri(context, file);
}else if (fileType.contains("audio/")){
uri = getAudioContentUri(context, file);
}
}
}
}
return uri;
}
/**
* Gets the content:// URI from the given corresponding path to a file
*
* @param context
* @param imageFile
* @return content Uri
*/
public 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);
if (cursor != null && cursor.moveToFirst()) {
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
Uri baseUri = Uri.parse("content://media/external/images/media");
return Uri.withAppendedPath(baseUri, "" + id);
} else {
if (imageFile.exists()) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DATA, filePath);
return context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
} else {
return null;
}
}
}
/**
* Gets the content:// URI from the given corresponding path to a file
*
* @param context
* @param videoFile
* @return content Uri
*/
public static Uri getVideoContentUri(Context context, File videoFile) {
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 && cursor.moveToFirst()) {
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
Uri baseUri = Uri.parse("content://media/external/video/media");
return Uri.withAppendedPath(baseUri, "" + id);
} else {
if (videoFile.exists()) {
ContentValues values = new ContentValues();
values.put(MediaStore.Video.Media.DATA, filePath);
return context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
} else {
return null;
}
}
}
/**
* Gets the content:// URI from the given corresponding path to a file
*
* @param context
* @param audioFile
* @return content Uri
*/
public static Uri getAudioContentUri(Context context, File audioFile) {
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 && cursor.moveToFirst()) {
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
Uri baseUri = Uri.parse("content://media/external/audio/media");
return Uri.withAppendedPath(baseUri, "" + id);
} else {
if (audioFile.exists()) {
ContentValues values = new ContentValues();
values.put(MediaStore.Audio.Media.DATA, filePath);
return context.getContentResolver().insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values);
} else {
return null;
}
}
}
要向在 MediaStore 中查詢到文件而芥,要不就是通知媒體庫更新查詢或則往里面插入一條新記錄(會比較耗時)
/**
* 刪除或增加圖片律罢、視頻等媒體資源文件時 通知系統(tǒng)更新媒體庫,重新掃描
* @param filePath 文件路徑棍丐,包括后綴
*/
public static void notifyScanMediaFile(Context context, String filePath)
{
if (context == null || TextUtils.isEmpty(filePath)){
LogUtil.e("notifyScanMediaFile context is null or filePath is empty.");
return;
}
MediaScannerConnection.scanFile(context,
new String[] { filePath }, null,
new MediaScannerConnection.OnScanCompletedListener() {
public void onScanCompleted(String path, Uri uri) {
LogUtil.i("notifyScanMediaFile Scanned " + path);
LogUtil.i("notifyScanMediaFile -> uri=" + uri);
}
});
}
具體實現(xiàn)
可以參考我的另外一篇文章:利用 Android 系統(tǒng)原生 API 實現(xiàn)分享功能(2)
最后安利一波
最近自己開發(fā)的這個 App 按照了本篇文章中提到的分享方案進(jìn)行了實現(xiàn)误辑,實際效果大家直接去 Google Play 或國內(nèi)酷安市場下載安裝試試,歡迎拍磚歌逢。
VEditor - 酷安
VEditor - Google Play