Android Q适配之文件存储

Android中不同系统的适配一直是应用开发中非常重要的问题,如果不能及时适配不同的系统版本,应用极有可能发生崩溃,给用户带来不好的体验。
随着Google发布 Android Q(API 29),我们需要及时的根据系统的变化做出相应的适配。Android Q中隐私权一块发生了较大的改变。特别是外部存储的访问权限的改变。Android Q中引入了分区存储的概念,应用默认拥有
分区存储的访问(读写)权限,而对于分区存储之外的文件,将无法直接通过文件路径以及File API来进行访问以及操作。

分区存储:

  • 应用的私有目录(Context.getExternalFilesDir()目录)
    该目录下的文件可以通过文件路径以及File API进行操作,但是应用被卸载时,该目录会被清除
  • 应用自己创建的媒体文件(照片,音频,视频)
    可以通过MediaStore进行操作

应用访问(读/写)分区存储下的文件不需要再获取任何权限,可以直接访问。

如果应用要访问其它应用创建的文件则必须满足以下两个条件
1.应用已获得READ_EXTERNAL_STORAGE权限
2.文件位于以下其中一个明确的媒体集合中
照片:MediaStore.Image
音频:MediaStore.Audio
视频:MediaStore.Video

为了访问其它应用创建的任何其它文件(包括“downloads”目录下的文件)
“downloads”目录是Android Q才引入的对应MediaStore.Download媒体集合,应用必须使用SAF(存储访问框架),也就是打开系统的文件选择器
可以访问任何文件。

下面我们来具体实践一下Android Q中文件操作;

1.分区存储中应用私有目录中文件的操作
外部存储中应用私有目录就是Context.getExternalFiles(“”)目录
该目录下的文件我们可以使用文件路径和File API进行操作,和以前的文件操作完全一样。

   /**
     * 创建文件
     */
    private void createFile() {
        String path = mActivity.getExternalFilesDir("").getAbsolutePath() + File.separator + System.currentTimeMillis() + ".txt";
        File file = new File(path);
        if (!file.exists()) {
            try {
                boolean success = file.createNewFile();
                Log.e(TAG, "createFile: "+success );
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

文件的写操作也是一样,通过File拿到文件的输出流,然后进行写操作,这里就不再赘述了

2.应用自己创建的媒体文件或者其他应用创建的媒体文件的操作

如果是自己应用创建的文件,访问无需再获取任何权限,如果是其他应用创建的文件,访问需要获取READ_EXTERNAL_STORAGE权限。操作不再可以使用文件路径和File API进行操作,必须使用MediaStore和ContentResolver来进行文件的访问和操作。

   /**
     * 新增文件
     */
    private void createFile() {
        String fileName = System.currentTimeMillis() + "";
        ContentValues contentValues = new ContentValues();
        contentValues.put(MediaStore.Files.FileColumns.DISPLAY_NAME, fileName);
		contentValues.put(MediaStore.Video.VideoColumns.MIME_TYPE, "video/mp4");
        Uri uri = mActivity.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues);
        Log.e(TAG, "createFile: " + uri);
    }

上面代码会向数据库里面插入一条记录,会返回一个Uri,但是并不会生成新的文件,只有我们通过
Uri往文件里面写入内容,才会生成文件。

    /**
     * 查询外部存储所有媒体文件
     */
    private void queryFile() {
        Cursor cursor = mActivity.getContentResolver().query(MediaStore.Files.getContentUri("external"), null, null, null, null);
        while (cursor.moveToNext()) {
            String id=cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID));
            String data = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Downloads.DATA));
            Log.e(TAG, "queryFile: " + data);
        }
    }

这里的查询出来的所有的媒体文件的data在Android Q之前是文件的实际路径,而在Android Q中该路径将不再有用,因为我们无法通过文件路径和File API来操作公共目录下的文件。只能通过Uri和ContentResolver来获取文件的输出流来进行操作,而媒体文件的Uri我们可以通过文件的ID来获取

    /** 
     * 根据媒体文件的ID来获取文件的Uri
     * @param id
     * @return
     */
    public String getMediaFileUriFromID(String id) {
        return MediaStore.Files.getContentUri("external").buildUpon().appendPath(String.valueOf(id)).build().toString();
    }

获取到文件的Uri之后我们就能够通过ContentResolver来获取文件的输出流,来对文件进行写操作

    /**
     * 对文件进行写操作
     * @param uri
     */
    private void writeFile(Uri uri) {
        try {
            AssetFileDescriptor assetFileDescriptor = mActivity.getContentResolver().openAssetFileDescriptor(uri, "rw");
            FileOutputStream outputStream = assetFileDescriptor.createOutputStream();
            String str = "123";
            outputStream.write(str.getBytes());
            OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
            BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
            bufferedWriter.write(str);
            bufferedWriter.close();
            outputStream.close();
            Log.e(TAG, "updateFile: " + uri);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
     /**
     * 删除文件
     */
    private void deleteFile(Uri uri) {
        int deleteNum = mActivity.getContentResolver().delete(uri, null, null);
        Log.e(TAG, "deleteFile: " + deleteNum);
    }

以上就是分区存储中媒体文件增删改查的操作

3.非私有目录非媒体文件的操作
对于非私有目录,非媒体文件,也就是说如果我们想访问一个外部存储中非私有目录的非媒体文件,或者说是非私有目录的任何类型文件,只能通过SAF(存储访问框架)来进行访问。
可以使用SAF(存储访问框架)访问任何一个文件,而无需请求任何权限。所谓的SAF(存储访问框架)就是
打开系统的文件选择器来进行操作。

    /**
     * 打开文件选择器选择文件
     */
 private void SAF() {
        // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
        // browser.
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

        // Filter to only show results that can be "opened", such as a
        // file (as opposed to a list of contacts or timezones)
       intent.addCategory(Intent.CATEGORY_OPENABLE);
       
        // Filter to show only images, using the image MIME data type.
        // If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
        // To search for all documents available via installed storage providers,
        // it would be "*/*".
        intent.setType("*/*");

        startActivityForResult(intent, READ_REQUEST_CODE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            // The document selected by the user won't be returned in the intent.
            // Instead, a URI to that document will be contained in the return intent
            // provided to this method as a parameter.
            // Pull that URI using resultData.getData().
            Uri uri = null;
            if (data != null) {
                uri = data.getData();
                Log.e(TAG, "Uri: " + uri.toString());
            }
        }
    }

打开文件选择器选择文件后,会回调onActivityResult,在返回的data中可以拿到文件的Uri,然后对文件进行操作

  /**
     * 创建文件
     * @param mimeType
     * @param fileName
     */
   private void SAFCreateFile(String mimeType, String fileName) {
        Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);

        // Filter to only show results that can be "opened", such as
        // a file (as opposed to a list of contacts or timezones).
        intent.addCategory(Intent.CATEGORY_OPENABLE);

        // Create a file with the requested MIME type.
        intent.setType(mimeType);
        intent.putExtra(Intent.EXTRA_TITLE, fileName);
        startActivityForResult(intent, SAF_CREATE_FILE_REQUEST_CODE);
    }

  @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == SAF_CREATE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            Uri uri = null;
            if (data != null) {
                uri = data.getData();
                Log.e(TAG, "Uri: " + uri.toString());
            }
        }
    }

打开文件选择器创建好文件后,会回调onActivityResult在返回的data中可以拿到创建的文件的Uri,然后进行
操作。

 /**
     * 删除文件
     */
    private void SAFDeleteFile(Uri uri) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            try {
                boolean success = DocumentsContract.deleteDocument(getContentResolver(), uri);
                Log.e(TAG, "SAFDeleteFile: " + success);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

如果获得了文件的 URI,并且文件的 Document.COLUMN_FLAGS 包含 SUPPORTS_DELETE,则便可通过上述代码删除该文件。

至于通过SAF对文件进行编辑,就是通过SAF选择文件后,拿到文件的Uri,然后和上面对媒体文件进行写操作一样的方式通过文件Uri和ContentResolver拿到文件的输出流,然后进行写操作。

以上就是通过SAF(存储访问框架)来对外部存储中任意类型文件进行增删改查操作。

总结:Android 10的分区存储极大的控制了应用混乱的使用设备的外部存储,从Android 6.0开始的动态权限,Android 7.0的应用私有文件的共享,再到Android 10的分区存储等等关于用户隐私权限的变更,Google无一不在把控制权更多的交给用户,让用户更好的管理和使用自己的设备和应用。

你可能感兴趣的:(Android开发)