一、问题来源

  1. 最近项目上有个需求,需要使用Adobe Acrobat Reader打开PDF文件。文件可以正常打开,但是如果对文件进行批注,则无法保存至原文件路径。文件被保存到Acrobat软件内部:
    file
    file

二、解决思路

  1. 使用file://方式打开PDF文件,可以正常打开和保存,前提条件是Android编译版本要在Android N(24)以下。示例代码:
    public void openPDF(){
        File targetFile = new File(Environment.getExternalStorageDirectory().getPath()+"/ynApp/1.pdf" );
        if(!targetFile.exists()){
            Toast.makeText(MainActivity.this, "测试文件不存在", Toast.LENGTH_SHORT).show();
            return;
        }
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        Uri data = Uri.fromFile(targetFile);
        intent.setDataAndType(data, "application/pdf");
        startActivity(intent);
    }
  1. 使用FileProvider方式共享文件,可以打开文件,但是修改文件后无法保存至原路径。示例代码:
    public void openPDF(){
        File targetFile = new File(Environment.getExternalStorageDirectory().getPath()+"/ynApp/1.pdf" );
        if(!targetFile.exists()){
            Toast.makeText(MainActivity.this, "测试文件不存在", Toast.LENGTH_SHORT).show();
            return;
        }
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        Uri data = FileProvider.getUriForFile(MainActivity.this, getApplicationContext().getPackageName() +".provider", targetFile);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        intent.setDataAndType(data, "application/pdf");
        startActivity(intent);
    }
  1. 经过观察测试,发现QQ下载的文件可以正常打开、修改、保存。于是乎观察Logcat,发现QQ使用的就是方式一。截图为证:
    file
    file
  2. 经过分析QQ的targetSdkVersion为17,也就是QQ使用的是Android N(24)以下编译的。首先想到就是降低现在项目的compileSdkVersion,但是由于项目比较庞大,降级会带来一系列问题,降级方案pass。
    file
    file
  3. 经过观察测试,发现华为自带的文件管理器也是可以正常打开、修改、保存PDF文件的,打开的Intent如下:
    file
    file
  • 使用的是FileProvider的方式,既然自带管理器可以,那我们写的程序也是可以打开的。仿照着截图上的方式,我们自己构造一个Intent:
    public void openPDF(){
        File targetFile = new File(Environment.getExternalStorageDirectory().getPath()+"/ynApp/1.pdf" );
        if(!targetFile.exists()){
            Toast.makeText(MainActivity.this, "测试文件不存在", Toast.LENGTH_SHORT).show();
            return;
        }
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        Uri data = FileProvider.getUriForFile(MainActivity.this, getApplicationContext().getPackageName() +".provider", targetFile);
        // 构造一个0x13000003,可以写成|的形式
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
        intent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
        intent.setDataAndType(data, "application/pdf");
        startActivity(intent);
    }
  • 运行上述代码,WTF!!还是不能保存!!仔细对照下Intent,原来是少了一个extra,但是extra具体是什么东西呢?下面就开启我们的找extra之路。
  1. 寻找Extra
  • 第一种方法,简单粗暴,直接下载一个华为文件管理器反编译看源码,我想怕是没有比这种方式更直接的了。不过一点是现在找到的这个安装包版本有点旧了,代码结构都不一样。另外一点源码混淆过,看起来有点吃力。
  • 第二种方法,让我的应用也出现在备选软件中,我直接来接收这个Intent,这样传来什么数据我都一目了然了。修改程序: 但是经过多次尝试,始终无法看到mParcelledData的值,如果有知道的麻烦告知一下。
    file
    file
    file
    file
  1. 等等
    我重新看了下自带浏览器发出的uri,格式怎么跟我的不一样啊!原来文件浏览器使用的是Android系统自带的ContentProvider。那我们再来改进下获取文件uri的方式:
    public static Uri getImageContentUri(Context context, java.io.File imageFile) {
        String filePath = imageFile.getAbsolutePath();
        Uri baseUri = MediaStore.Files.getContentUri("external");
        Cursor cursor = context.getContentResolver().query(baseUri,
                new String[] { MediaStore.MediaColumns._ID }, MediaStore.MediaColumns.DATA + "=? ",
                new String[] { filePath }, null);
        if (cursor != null && cursor.moveToFirst()) {
            int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
            return Uri.withAppendedPath(baseUri, String.valueOf(id));
        } else {
            return null;
        }
    }

最新获取到的uri:content://media/external/file/67。其实也就是查询的/data/data/com.android.providers.media/databases/external.db

file
file
搞定!!!

三、解决方法

使用Android自带的ContentProvider来获取文件的uri

四、参考链接

  1. Android 7.0 行为变更
  2. Android FileProvider的使用
  3. Android 文件绝对路径和Content开头的Uri互相转换