Coder Social home page Coder Social logo

filechoose's Introduction

Android浏览文件

记一次文件上传引发的血案


前情描述:

使用系统文件管理器,选择指定文件类型上传。

基础知识
  • MIME
  • 调起文件管理器
  • 指定浏览位置(路径转URI)
  • 设置多种文件类型
  • URI转路径
踩坑
  • com.tencent.mtt.fileprovider 问题

1. MIME

MIME (Multipurpose Internet Mail Extensions) 是描述消息内容类型的因特网标准。

final String DOC = "application/msword";
final String XLS = "application/vnd.ms-excel";
final String PPT = "application/vnd.ms-powerpoint";
final String DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
final String XLSX = "application/x-excel";
final String XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
final String PPTX = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
final String PDF = "application/pdf";
final String MP4 = "video/mp4";
final String M3U8 = "application/x-mpegURL";

更多文件类型,自行百度

2. 调起文件管理器

  1. 所有类型文件

    Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
    //任意类型文件
    intent.setType("*/*");
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    startActivityForResult(intent,1);
    
    
    //-------常用类型
        //图片
    //intent.setType(“image/*”);
        //音频
    //intent.setType(“audio/*”);
        //视频
    //intent.setType(“video/*”); 
    //intent.setType(“video/*;image/*”);
    
  2. 系统的相冊

     Intent intent= new Intent(Intent.ACTION_PICK, null);
     intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
     startActivityForResult(intent, REQUEST_CODE_FILE); 
    

3. 指定浏览位置(路径转URI)

跳转到指定路径下,涉及到将路径转为URI,考虑Android版本区别

/**
 * file --> uri
 * @param context
 * @param file
 *
 * @return
 */
public static Uri getUriFromFile(Context context, File file) {
    if (context == null || file == null) {
        throw new NullPointerException();
    }
    Uri uri;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        uri = FileProvider.getUriForFile(context.getApplicationContext(), BuildConfig.APPLICATION_ID + ".fileprovider", file);
    } else {
        uri = Uri.fromFile(file);
    }
    return uri;
}

由于7.0的升级还需要在AndroidManifest.xml中配置FileProvider

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

${applicationId}.fileprovider这个配置要记牢,后期遇到大坑就靠这个值了。

xml/file_paths 文件如下:

参考CSDN

<!--物理路径相当于Context.getFilesDir() + /path/-->
<files-path name="name" path="path" />

 <!--物理路径相当于Context.getCacheDir() + /path/-->
<cache-path name="name" path="path" /> 
 <!-- 物理路径相当于Environment.getExternalStorageDirectory() + /path/-->
<external-path name="name" path="path" />
 <!-- 物理路径相当于Context.getExternalFilesDir(String) + /path/-->
<external-files-path name="name" path="path" />
 <!-- 物理路径相当于Context.getExternalCacheDir() + /path/-->
<external-cache-path name="name" path="path" />

将文件路径转(使用微信下载目录做测试)为URI后,设置到Intent中。

/**
 * 根据类型,加载文件选择器
 */
private void chooseFileWithPath() {
    Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    //跳转指定路径,如果路径不存在,切到sdcard
    //需要读权限
    String path = getSDPath();
    if (!TextUtils.isEmpty(path)) {
        //使用微信下载目录测试
        path = path + File.separator + "tencent/MicroMsg/Download";
        File file = new File(path);
        if (file.exists()) {
            //主要代码
            intent.setDataAndType(FileUtil.getUriFromFile(this, new File(path)), "*/*");
        } else {
            intent.setType("*/*");
        }
    } else {
        intent.setType("*/*");
    }
    startActivityForResult(intent, REQUEST_CODE_FILE);
}

主要生效代码:

Intent intent = new Intent(action,uri);
intent.setType("*/*");
startActivityForResult(intent, REQUEST_CODE_FILE);

或者

Intent intent = new Intent(action);
intent.setDataAndType(uri, "*/*");
startActivityForResult(intent, REQUEST_CODE_FILE);

4. 设置多种文件类型

通过intent.setType()方式设置的文件类型,只能生效一次,所以如果想只选择doc、excel、pdt、ppt等办公文档,过滤掉其他文件,就不能使用intent .setType()方式,而是使用Intent.EXTRA_MIME_TYPES来传值。


final String DOC = "application/msword";
final String XLS = "application/vnd.ms-excel";
final String PPT = "application/vnd.ms-powerpoint";
final String DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
final String XLSX = "application/x-excel";
final String XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
final String PPTX = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
final String PDF = "application/pdf";

/**
 * 多种文件类型
*/
private void chooseMoreTypes() {
    Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    String[] mimeTypes = {DOC, DOCX, PDF, PPT, PPTX, XLS, XLS1, XLSX};
    intent.setType("application/*");
    
    intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
    startActivityForResult(intent, REQUEST_CODE_FILE);
}

以上是准备工作,调起文件管理器进行选择文件,对于一个Android开发者来说,以上步骤只是相当于一小步,不要忘记适配

接下来才是长征路。

5. URI转路径

在这一步主要解决的是将返回的URI转换为File,大坑也往往就在这一步出现。

从网上能找到File转File的代码,但是百度出来的内容,10篇有8篇是一样的,剩下2篇不能看。

但是这8篇中几乎都是相同的,没有解决一个至关重要的问题QQ文件管理器

也可能是不会用搜索引擎吧。

上代码

/**
 * 专为Android4.4设计的从Uri获取文件绝对路径,以前的方法已不好使
 */
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static String getPath(final Context context, final Uri uri) {

    final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
    if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
        
        //一些三方的文件浏览器会进入到这个方法中,例如ES
        //QQ文件管理器不在此列
        
        if (isExternalStorageDocument(uri)) {
            final String docId = DocumentsContract.getDocumentId(uri);
            final String[] split = docId.split(":");
            final String type = split[0];
            if ("primary".equalsIgnoreCase(type)) {
                return Environment.getExternalStorageDirectory() + "/" + split[1];
            }
        }
        // DownloadsProvider
        else if (isDownloadsDocument(uri)) {
        ...
        }
        // MediaProvider
        else if (isMediaDocument(uri)) {
        ...
        }
    } else if ("content".equalsIgnoreCase(uri.getScheme())) {// MediaStore (and general)
        return getDataColumn(context, uri, null, null);
    } else if ("file".equalsIgnoreCase(uri.getScheme())) {// File
        ...
    }
    return null;
}

发现部手机会有第三方的文件管理器,如ES,QQ等_目前只接触到这两种,不排除其他APP_,相信大部分都是好同志,但不限于鹅厂大佬。

通过QQ浏览器选择到的文件,则会报出不存在_data字段这个错误。

WTF

懵逼.jpg

返回的URI中uri.getAuthority()的值并不是自己在Manifest.xml中设置的**${applicationId}.fileprovider而是com.tencent.mtt.fileprovider**。

这时候,前面搜索到的文章,几乎都没有解决这个问题。

鉴于水平有限,被坑了半天后。

通过分析uri.getPath();的值。

写下来如下代码,如各位大佬有更好的解决方案,望转告。

//判断QQ文件管理器
if (isQQMediaDocument(uri)) {
    String path = uri.getPath();
    File fileDir = Environment.getExternalStorageDirectory();
    File file = new File(fileDir, path.substring("/QQBrowser".length(), path.length()));
    return file.exists() ? file.toString() : null;
}

测试通过机型: 魅族15,三星9300,mi6,oppoR9, 华为mate9

缺失代码自行补齐, 主要类代码FileUtil


其他问题

1. 文件类型设置

产品需求:

╔══════════════════════════════
║
║   上传文件       上传视频
║
╚══════════════════════════════

要求: 1. 点击视频选择mp4,2. 点击文件选择pdf、word等office文件。

在设置Intent的时候分为2种:

  • intent.setType("*/*")
这种情况下,本意是想选择office文档。通过```Intent.EXTRA_MIME_TYPES```来限制文件类型,但是这种情况下,会出现第三方的文件管理器,而三方的一些情况下不会生效,所有文件都可以选择。
    
我做的是,通过观察MIME类型,我设置的是```application/*```第三方的文件管理图标隐藏掉了,只能通过系统的文件管理选择文件。

![设置"*/*"](./pic/vivox21.png)
![设置"*/*"](./pic/vivox21_hidethird.png)
  • intent.setType("video/mp4);
1. 这种会显示三方文件管理器,但是会过滤掉其他的文件,只有video类型的,如果有avi类型,那么还需要在```onActivityResult```中判断文件后缀名。
2. 系统的文件管理器会生效,只能选择```Intent.EXTRA_MIME_TYPES```设置的类型
2. 返回URI的问题
  • 从文件管理器选择文件,返回的URI是content://com.android.externalstorage.documents/document/primary/update/A5679B1.mp4
  • 从『视频』选择文件,返回的URI是content://com.android.providers.media.documents/document/video:5188

遇到的问题:

判断文件格式是否是我设置的类型,如果intent.setType("video/*");,但是只想要"mp4"格式的文件,那么在onActivityResult中通过返回的数据进行判断,前期想的是通过uri.getLastPathsegment(),判断文件的后缀名,但是后来测试遇到了第二种情况,从『视频』里选择到文件,这时返回URI不符合规则了,所以偷懒是不行的,只能通过转换,将源文件的名称,判断后缀名。

选择视频

filechoose's People

Contributors

db-boy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

filechoose's Issues

android11我这样兼容,目前没啥问题

public static String getPath(final Context context, final Uri uri) {
MatchLog.E(RestRequest.TAG, "uri=" + uri);
if (DocumentsContract.isDocumentUri(context, uri)) {
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
MatchLog.E(RestRequest.TAG, "docId=" + docId);
if (!TextUtils.isEmpty(docId) && docId.startsWith("raw:")) {
return Environment.getExternalStorageDirectory() + "/" + docId.substring(4);
}
final String[] split = docId.split(":");
/final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
/
return Environment.getExternalStorageDirectory() + "/" + split[1];
} else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
if (!TextUtils.isEmpty(id) && id.startsWith("raw:")) {
return id.substring(4);
}
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
} else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
} else {
contentUri = MediaStore.Files.getContentUri("external");
}
final String selection = "_id=?";
final String[] selectionArgs = new String[]{split[1]};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
if (isGooglePhotosUri(uri)) {
String lastPathSegment = uri.getLastPathSegment();
MatchLog.E(RestRequest.TAG, "isGooglePhotosUri=" + lastPathSegment);
return lastPathSegment;
}
if (isQQMediaDocument(uri)) {
String path = uri.getPath();
File fileDir = Environment.getExternalStorageDirectory();
File file = new File(fileDir, path.substring("/QQBrowser".length(), path.length()));
String pathStr = file.exists() ? file.toString() : null;
MatchLog.E(RestRequest.TAG, "isQQMediaDocument=" + pathStr);
return pathStr;
}
return getDataColumn(context, uri, null, null);
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}

第三方文件管理器Uri转File优化,可兼容迅雷和QQ浏览器,其他的感觉应该类似

改成:

File thirdPartyUriFile = getThirdPartyUriFile(uri);
if (thirdPartyUriFile != null) {
    return thirdPartyUriFile.toString();
}
/**
 * 处理第三方文件管理器的文件Uri
 *
 * @param uri
 *
 * @return
 */
public static File getThirdPartyUriFile(Uri uri) {
    String path = uri.getPath();
    if (!TextUtils.isEmpty(path)) {
        // 去掉第一个'/'
        path = path.substring(1);
        // 去掉新path第一个'/'前面的内容
        path = path.substring(path.indexOf('/'));
        File file = new File(Environment.getExternalStorageDirectory(), path);
        if (file.exists()) {
            return file;
        }
    }
    return null;
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.