对于 App 的分享功能,基本上是一个刚需,本文主要介绍运用系统原生分享功能时候需要注意的一些问题。对于某些特定平台的一些高级分享特性,比如微信或者微博之类的分享来源标注,需要在其开放平台注册应用再接入其 sdk 才可以,这里不予以讨论。打算借助第三方库类似 ShareSDK 实现的同学们,这篇文章可能也帮不上你。
什么是 Android 系统的原生分享
直接上图,这是一个典型的调用系统原生分享场景下的界面,相信大家应该都很熟悉。
系统内建的分享机制,参照官方的教程,基本上可以满足你的一般需求:
简单描述下创建分享的主要过程:
- 创建一个
Intent
,指定其Action
为Intent.ACTION_SEND
,这表示要创建一个发送指定内容的隐式意图。
Intent sendIntent = new Intent();sendIntent.setAction(Intent.ACTION_SEND);复制代码
- 指定需要发送的内容和类型。
// 比如发送文本形式的数据内容// 指定发送的内容sendIntent.putExtra(Intent.EXTRA_TEXT, "This is my text to send.");// 指定发送内容的类型sendIntent.setType("text/plain"); 复制代码
// 比如发送二进制文件数据流内容(比如图片、视频、音频文件等等)// 指定发送的内容 (EXTRA_STREAM 对于文件 Uri )shareIntent.putExtra(Intent.EXTRA_STREAM, uriToImage);// 指定发送内容的类型 (MIME type)shareIntent.setType("image/jpeg");复制代码
- 向系统发送隐式意图,打开系统分享选择器,出现如上图所示界面。
startActivity(Intent.createChooser(shareIntent, “Share to...”));复制代码
四不四看起来很简单,四不四感觉可以分分钟可以搞定。年轻人,我跟你港,别图样图森破,现在大家没遇几个坑都不好意思出来港,不过做人嘛~最重要的开心。
那下面说一下遇到的一些问题,特别针对是 7.0 以后的系统,以及兼容一些主流 app 时遇到的坑。
1. 获取文件类型(MimeType)
前面说到分享文件时需要知道文件的类型,不然的指定类型为 */*
,这样分享到某些 App 会因为无法判断文件类型而导致失败,所以最好先根据文件路径获取其文件类型。
下面是一些常见文件的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"}, { "", "*/*"} 复制代码
####获取文件类型的方法: 方式一(方便但不稳定):通过 ContentResolver 查询 Android 系统提供的 ContentProvider 获取
当 targetSdkVersion >= 24 时使用 Uri.fromFile(File file) 获取文件 uri 会报 android.os.FileUriExposedException 异常 ,应该要使用 ,具体请参考 Android 7.0 FileProvider 适配相关,这里不再展开说明。关于 推荐一篇总结比较好的文章。
// 获取文件的 urlFile shareFile = new File(shareFilePath);Uri fileUri = Uri.fromFile(shareFile);// 获取系统的提供的 ContentResolverContentResolver contentResolver = getApplicationContext().getContentResolver();// 获取文件MimeType,如 image/pngString fileMimeType = contentResolver.getType(fileUri);// 获取文件Type,如 pngMimeTypeMap mime = MimeTypeMap.getSingleton();String fileType = mime.getExtensionFromMimeType(fileMimeType);复制代码
使用这种方法获取文件类型,一定要注意 ContentResolver 获取返回为 null 的情况,不然空指针异常的崩溃率可能会让你笑不出来。实际测试中,发现在某些国产机型下,这个方法可以说直接是不可用,查询返回一直都是空,所以单纯依赖这一个方法会很不可靠。具体问题原因请看:
方式二 解析文件信息,通过匹配识别判断: 在好用的方法却不可靠的情况下,只能配合看起来蠢一点的方法。目前大致的思路有两种: 1.识别文件后缀,根据后缀名来判断文件类型。 2.获取文件头信息,转成十六进制字符串后判断文件类型。 这两种都是根据特点信息去做匹配,因此需要先保存一份文件特点信息和文件类型的对应参照表。
下面按照第二条思路,按照文件头信息简单实现一个获取文件类型的例子:
/** * 获取文件类型 * @param filePath * @return */ public static String getFileType(String filePath) { return mFileTypes.get(getFileHeader(filePath)); } private static final HashMapmFileTypes = new HashMap (); // 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");//可执行文件 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字节转换为十六进制字符串 * @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进行分享
前面也有提到,在 Android 7.0 以后,系统对 scheme 为 file:// 的 uri 进行了限制,所以之前进行文件分享的一些接口就不能用了,此时就得使用其他的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); // 授予目录临时共享权限 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); // 使用上面的根据文件头信息获取文件类型的方法 } 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 中查询到文件,要不就是通知媒体库更新查询或则往里面插入一条新记录(会比较耗时)
/** * 删除或增加图片、视频等媒体资源文件时 通知系统更新媒体库,重新扫描 * @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); } }); }复制代码
具体实现
可以参考我的另外一篇文章:
最后安利一波
最近自己开发的这个 App 按照了本篇文章中提到的分享方案进行了实现,实际效果大家直接去 Google Play 或国内酷安市场下载安装试试,欢迎拍砖。