Android Q 适配

因为项目在华为部分手机有预装,应华为要求,适配 Android Q(Android 10) 版本,因为华为那边要求,新版本系统出来不久就会适配,项目是一步步适配上来的,Android M、Android N、Android O、Android P ,所以本次适配是从 Android P (9.0) 升到 Android Q,所以适配难度不是很大。建议新版本出来稳定后还是及时适配,否则一下跳跃升级适配上会比较麻烦。下面是我们项目适配遇到的问题,后面遇到问题再继续补充:

主要升级targetSdkVersion到29就可以了,我将编辑版本升到29了,support库用的是28.0.0,怕第三方库不支持没升androidx。#### Android Q (10.0)(API 29) 适配
因为项目在华为部分手机有预装,应华为要求,适配 Android Q(Android 10) 版本,因为华为那边要求,新版本系统出来不久就会适配,项目是一步步适配上来的,Android M、Android N、Android O、Android P ,所以本次适配是从 Android P (9.0) 升到 Android Q,所以适配难度不是很大。建议新版本出来稳定后还是及时适配,否则一下跳跃升级适配上会比较麻烦。下面是我们项目适配遇到的问题,后面遇到问题再继续补充:

主要升级targetSdkVersion到29就可以了,我将编辑版本升到29了,support库用的是28.0.0,怕第三方库不支持没升androidx。

targetSdkVersion : 29,
compileSdkVersion: 29,
buildToolsVersion: "29.0.2",

应用读取 Device ID

Android Q 之前有如下代码,获取设备Id,IMEI等

TelephonyManager telManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
telManager.getDeviceId();
telManager.getImei();

添加下面权限,并且需要动态申请权限

<uses-permission android:name="android.permission.READ_PHONE_STATE" />

在 Android Q 上调用上面方法会报错

java.lang.SecurityException: getDeviceId: The user 10143 does not meet the requirements to access device identifiers.

在 Android Q 上上面方法已经不能使用了,如果获取设备唯一Id,需要使用其他方式了,谷歌提供的获取唯一标识符做法见 文档,也可以用Android_ID,上面这些也不是绝对能得到一个永远不变的Id,可能需要多种方案获取其他Id,比如有谷歌商店的手机可以使用谷歌提供的广告Id,还有其他厂商一般都会提供手机的一个唯一Id,我们项目现在使用下面这种方式 参考链接,后面会多测试一下。

    public static String getUniqueID(Context context) {
        String id = null;
        final String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
        if (!TextUtils.isEmpty(androidId) && !"9774d56d682e549c".equals(androidId)) {
            try {
                UUID uuid = UUID.nameUUIDFromBytes(androidId.getBytes("utf8"));
                id = uuid.toString();
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }

        if (TextUtils.isEmpty(id)) {
            id = getUUID();
        }

        return TextUtils.isEmpty(id) ? UUID.randomUUID().toString() : id;
    }

    private static String getUUID() {
        String serial = null;

        String m_szDevIDShort = "35" +
                Build.BOARD.length() % 10 + Build.BRAND.length() % 10 +

                Build.CPU_ABI.length() % 10 + Build.DEVICE.length() % 10 +

                Build.DISPLAY.length() % 10 + Build.HOST.length() % 10 +

                Build.ID.length() % 10 + Build.MANUFACTURER.length() % 10 +

                Build.MODEL.length() % 10 + Build.PRODUCT.length() % 10 +

                Build.TAGS.length() % 10 + Build.TYPE.length() % 10 +

                Build.USER.length() % 10; //13 位

        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                serial = android.os.Build.getSerial();
            } else {
                serial = Build.SERIAL;
            }
            //API>=9 使用serial号
            return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
        } catch (Exception exception) {
            serial = "serial"; // 随便一个初始化
        }

        //使用硬件信息拼凑出来的15位号码
        return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
    }

文件存储

在早期的测试版本新增了READ_MEDIA_IMAGESREAD_MEDIA_AUDIOREAD_MEDIA_VIDEO三个权限,正式版已经移除,还是使用之前的两个读写权限

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

在 Android Q 之前可以访问SD卡任意目录,使用如下:

File file = Environment.getExternalStorageDirectory();

上面得到的是SD卡根目录,打印出路径为:/storage/emulated/0。在 Android Q 上已经不能访问这个目录了,Android Q 下文件存储看下面方法。

App 专属目录

在 App专属目录下本App可以随意操作,无需申请权限,不过 App专属目录会在App卸载时跟随删除。看下面几个目录(通过Application的context就可以访问)。

  • getFilesDir() :/data/user/0/本应用包名/files

  • getCacheDir():/data/user/0/本应用包名/cache

  • getExternalFilesDir(null):/storage/emulated/0/Android/data/本应用包名/files

  • getExternalCacheDir():/storage/emulated/0/Android/data/本应用包名/cache

getFilesDir和getCacheDir是在手机自带的一块存储区域(internal storage),通常比较小,SD卡取出也不会影响到,App的sqlite数据库和SharedPreferences都存储在这里。所以这里应该存放特别私密重要的东西。

getExternalFilesDir和getExternalCacheDir是在SD卡下(external storage),在sdcard/Android/data/包名/files和sdcard/Android/data/包名/cache下,会跟随App卸载被删除。

files和cache下的区别是,在手机设置-找到本应用-在存储中,点击清除缓存,cache下的文件会被删除,files下的文件不会。

谷歌推荐使用getExternalFilesDir。我们项目的下载是个本地功能,下载完成后是存本地数据库的,不是放网络上的,所以下载的音视频都放到了这下面,项目卸载时跟随App都删除了。getExternalFilesDir方法需要传入一个参数,传入null时得到就是sdcard/Android/data/包名/files,传入其他字符串比如"Picture"得到sdcard/Android/data/包名/files/Picture。

使用MediaStore访问公共目录

通过上面App专属目录只能操作本App专属目录,并且保存的文件会随着App卸载删除。通过MediaStore,App可以访问公共目录下的媒体文件,通过MediaStore操作Uri读写文件。

保存图片直接用 insertImage 方法就可以,可以传入Bitmap或图片在本地的路径,注意本地路径要是本App可以访问到的路径,否则没权读取

public void saveImage(String imagePath, String title, String desc) {
	MediaStore.Images.Media.insertImage(context.getContentResolver(), imagePath, title, desc);
}
或
public void saveImage(Bitmap bitmap, String title, String desc) {
	MediaStore.Images.Media.insertImage(context.getContentResolver(), bitmap, title, desc);
}

其他类型的文件保存就没有直接的方法了,大致可以用下面这样:

    public void saveFile(final Uri extUri, final String mimeType, final String saveName, final String desc,
                          final String netUrl) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    ContentValues values = new ContentValues();
                    values.put(MediaStore.Images.Media.DISPLAY_NAME, saveName);
                    values.put(MediaStore.Images.Media.TITLE, saveName);
                    values.put(MediaStore.Images.Media.DESCRIPTION, desc);
                    values.put(MediaStore.Images.Media.MIME_TYPE, mimeType);
                    ContentResolver cr = context.getContentResolver();
                    Uri uri = cr.insert(extUri, values);

                    byte[] buffer = new byte[1024];
                    ParcelFileDescriptor parcelFileDescriptor = cr.openFileDescriptor(uri, "w");
                    FileOutputStream fileOutputStream = new FileOutputStream(parcelFileDescriptor.getFileDescriptor());
                    URL url = new URL(netUrl);
                    InputStream inputStream = url.openStream();
                    while (true) {
                        int numRead = inputStream.read(buffer);
                        if (numRead == -1) {
                            break;
                        }
                        fileOutputStream.write(buffer, 0, numRead);
                    }
                    fileOutputStream.close();
                    parcelFileDescriptor.close();
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    // TODO close io
                }
            }
        }).start();
    }

看上面代码,前面得到uri,然后变为fileOutputStream,后面就是文件的读写了,inputStream也可以同过其他方式得到(比如本地文件等),有输入流就可以写到uri中了。

使用如下:

// 保存图片
saveFile(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/png", 
"myImage", "", "http://www.xxx.png");

// 保存视频
saveFile(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "video/mp4", 
"myVideo", "", "http://www.xxx.mp4");

// 保存音频
saveFile(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, "audio/mpeg", 
"myAudio", "", "http://www.xxx.mp3");

// Android Q 新增的下载目录
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
	saveFile(MediaStore.Downloads.EXTERNAL_CONTENT_URI, "text/plain", 
	"myText", "", "http://www.xxx.txt");
}

文件读取,以读取图片为例,其他的也一样

获取全部图片:

    public static List<Uri> loadPhotoFiles(Context context) {
        List<Uri> photoUris = new ArrayList<Uri>();
        Cursor cursor = context.getContentResolver().query(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Images.Media._ID}, null, null, null);
        while (cursor.moveToNext()) {
            int id = cursor.getInt(cursor
                    .getColumnIndex(MediaStore.Images.Media._ID));
            Uri photoUri = Uri.parse(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString() + File.separator + id);
            photoUris.add(photoUri);
        }
        return photoUris;
    }
    
     // uri 转 bitmap
     public static Bitmap getBitmapFromUri(Uri uri) throws IOException {
        ParcelFileDescriptor parcelFileDescriptor =
                context.getContentResolver().openFileDescriptor(uri, "r");
        FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
        Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
        parcelFileDescriptor.close();
        return image;
    }

根据title获取图片:

    private Bitmap getImage(String title) {
        Uri external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        ContentResolver resolver = context.getContentResolver();

        String selection = MediaStore.Images.Media.TITLE + "=?"; // 查询条件
        String[] args = new String[]{title}; // 上面?的值
        String[] projection = new String[]{MediaStore.Images.Media._ID}; // 查询的内容
        Cursor cursor = resolver.query(external, projection, selection, args, null);
        Uri imageUri = null;

        if (cursor != null && cursor.moveToFirst()) {
            imageUri = ContentUris.withAppendedId(external, cursor.getLong(0));
            cursor.close();
        }

        if (imageUri == null) {
            return null;
        }

        ParcelFileDescriptor pfd = null;
        try {
            pfd = getContentResolver().openFileDescriptor(imageUri, "r");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        if (pfd != null) {
            Bitmap bitmap = BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor());
            pfd.close();
            return bitmap;
        }
        
        return null;
    }

注意:MediaStore的DATA 在Android Q 之前表示文件的真实路径,在Android Q 被废弃,可以通过 _ID 获取Uri,通过 ContentUris.withAppendedId(external, cursor.getLong(0)); 获取。

删除文件,需要先查询出uri

context.getContentResolver().delete(imageUri, null, null);

修改文件,用的比较少

  // 修改的内容以键值对放到ContentValues中
  ContentValues values = new ContentValues();
  values.put("title", "new title");
  getContentResolver().update(imageUri, values, null, null);
使用SAF访问指定目录

存储访问框架(Storage Access Framework),这种方式操作文件时会拉起系统页面,通过用户授权操作来完成文件读取,用户可以选择任何目录,用户选完后App就有了这个目录的读写权限。官方文档

保存一个文件时,用下面方法

    private void createFile(String fileName, String mimeType) {
        Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        if (!TextUtils.isEmpty(mimeType)) {
            intent.setType(mimeType);
        }
        intent.putExtra(Intent.EXTRA_TITLE, fileName);
        startActivityForResult(intent, REQUEST_CODE);
    }
    
        @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            Uri uri = null;
            if (data != null) {
                uri = data.getData();
                final int takeFlags = getIntent().getFlags()
                        & (Intent.FLAG_GRANT_READ_URI_PERMISSION
                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                // Check for the freshest data.
                getContentResolver().takePersistableUriPermission(uri, takeFlags);
                writeFile(uri, netUrl);
            }
        }
    }
    
    private void writeFile(Uri uri, String netUrl) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "w");
                    FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor());

                    byte[] buffer = new byte[1024];
                    URL url = new URL(netUrl);
                    InputStream inputStream = url.openStream();
                    while (true) {
                        int numRead = inputStream.read(buffer);
                        if (numRead == -1) {
                            break;
                        }
                        fileOutputStream.write(buffer, 0, numRead);
                    }

                    fileOutputStream.close();
                    pfd.close();
                    inputStream.close();
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

调用createFile会拉起下面界面,用户保存后会回调到onActivityResult中,并将uri传来,得到uri后就可以写入了。
如下图就是通过SAF保存视频会拉起系统界面,让用户选择授权,读取删除等都差不多是这样一个界面,下面就不截图了。
在这里插入图片描述

读取一个文件,以读取图片为例:

    public void openImage() {
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("image/*");
        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) {
            Uri uri = null;
            if (data != null) {
                uri = data.getData();

                showImage(uri);
            }
        }
    }

    private void showImage(Uri uri) {
        if (uri == null)
            return;
        ParcelFileDescriptor pfd = null;
        try {
            pfd = getContentResolver().openFileDescriptor(uri, "r");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        if (pfd != null) {
            Bitmap bitmap = BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor());
            pdf.close();
            ImageView imageView = findViewById(R.id.imageView);
            imageView.setImageBitmap(bitmap);
        }
    }

删除文件,跟上面读取一样,在onActivityResult中调用deleteImage,代码如下:

    private void deleteImage(Uri uri) {
        final int takeFlags = getIntent().getFlags()
                & (Intent.FLAG_GRANT_READ_URI_PERMISSION
                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        // Check for the freshest data.
        getContentResolver().takePersistableUriPermission(uri, takeFlags);
        try {
            DocumentsContract.deleteDocument(getContentResolver(), uri);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

修改文件,应该也是通过Intent.ACTION_OPEN_DOCUMENT打开选择一个文件,最后在onActivityResult中得到选择文件的uri,再修改,没有具体使用过。

下面是我们项目在文件存储中遇到的问题

1、更新图片到图库

保存图片后需要更新到相册,之前下载图片到App 专属目录,然后通过方法1同步到相册,让用户在相册能看到下载的图片。在 Android Q 上面使用方法1不生效,应该是相册访问不到App专属目录,现在做法是通过方法2将图片存到公共目录。

// 方法1
Uri uri = Uri.fromFile(file);
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));

// 方法2
// 可以看上面文件存储,也可以传入Bitmap
MediaStore.Images.Media.insertImage(getContentResolver(), file.getAbsolutePath(), name, "");
2、文件上传下载

上传:如果是App内部使用,则可以选择上传 App 专属目录下的文件,使用不需要修改;如果是想选择任意目录的文件,可以使用 SAF 的方式;如果选择系统公共目录下的文件可以使用 MediaStore 方式。在Android Q上上传非App 专属目录下的文件,上传时通过File的方式上传是不可以的,我们项目使用的是 MediaStore 方式,通过 MediaStore 得一个 Uri,然后转为 InputStream 上传,方法见下面getInputStream,大部分上传文件库应该都支持传入一个 InputStream,因为最终上传也是要获取到一个 InputStream。如果非要通过File上传文件或者需要对File做一些特殊的操作的话,最简单的方案可以将公共目录下的文件拷贝到 App 专属目录下就可以随意操作了,方法见下面 getFile。

下载:如果是App内部使用,则可以选择下载到 App 专属目录下,使用不需要修改;如果是想下载到任意目录,可以使用 SAF 的方式;如果App卸载后文件不跟随删除可以使用 MediaStore 方式。我们项目大部分都是下载到App 专属目录下了,一下卸载App后保留的文件,是通过MediaStore下载到公共目录了。

    public static InputStream getInputStream(android.net.Uri uri) {
        InputStream inputStream = null;
        try {
            ContentResolver cr = context.getContentResolver();
            inputStream = cr.openInputStream(uri);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return inputStream;
    }
        /**
     * 拷贝文件,将uri拷贝到 App专属目录下
     * @param uri 要拷贝文件的Uri
     * @param saveName 保存到专属目录下的文件名
     * @return 拷贝后新的文件
     */
    public static File getFile(Uri uri, String saveName) {
        File rootFile = context.getExternalFilesDir(null);
        File file = new File(rootFile, saveName);

        try {
            byte[] buffer = new byte[1024];
            FileOutputStream fileOutputStream = new FileOutputStream(file);
            InputStream inputStream = ApplicationRepository.application().getContentResolver().openInputStream(uri);
            while (true) {
                int numRead = inputStream.read(buffer);
                if (numRead == -1) {
                    break;
                }
                fileOutputStream.write(buffer, 0, numRead);
            }
            fileOutputStream.close();
            inputStream.close();
        } catch (IOException e) {
            file = null;
            e.printStackTrace();
        }

        return file;
    }

参考链接:Android Q 要来了,给你一份很"全面"的适配指南!

targetSdkVersion : 29,
compileSdkVersion: 29,
buildToolsVersion: "29.0.2",

应用读取 Device ID

Android Q 之前有如下代码,获取设备Id,IMEI等

TelephonyManager telManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
telManager.getDeviceId();
telManager.getImei();

添加下面权限,并且需要动态申请权限

<uses-permission android:name="android.permission.READ_PHONE_STATE" />

在 Android Q 上调用上面方法会报错

java.lang.SecurityException: getDeviceId: The user 10143 does not meet the requirements to access device identifiers.

在 Android Q 上上面方法一句不能使用了,如果获取设备唯一Id,需要使用其他方式了,谷歌提供的获取唯一标识符做法见 文档,也可以用Android_ID,上面这些也不是绝对能得到一个永远不变的Id,可能需要多种方案获取其他Id,比如有谷歌商店的手机可以使用谷歌提供的广告Id,还有其他厂商一般都会提供手机的一个唯一Id,这个后面整理多测试一下补充。

文件存储

在早期的测试版本新增了READ_MEDIA_IMAGESREAD_MEDIA_AUDIOREAD_MEDIA_VIDEO三个权限,正式版已经移除,还是使用之前的两个读写权限

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

在 Android Q 之前可以访问SD卡任意目录,使用如下:

File file = Environment.getExternalStorageDirectory();

上面得到的是SD卡根目录,打印出路径为:/storage/emulated/0。在 Android Q 上已经不能访问这个目录了,Android Q 下文件存储看下面方法。

App 专属目录

在 App专属目录下本App可以随意操作,无需申请权限,不过 App专属目录会在App卸载时跟随删除。看下面几个目录(通过Application的context就可以访问)。

  • getFilesDir() :/data/user/0/本应用包名/files

  • getCacheDir():/data/user/0/本应用包名/cache

  • getExternalFilesDir(null):/storage/emulated/0/Android/data/本应用包名/files

  • getExternalCacheDir():/storage/emulated/0/Android/data/本应用包名/cache

getFilesDir和getCacheDir是在手机自带的一块存储区域(internal storage),通常比较小,SD卡取出也不会影响到,App的sqlite数据库和SharedPreferences都存储在这里。所以这里应该存放特别私密重要的东西。

getExternalFilesDir和getExternalCacheDir是在SD卡下(external storage),在sdcard/Android/data/包名/files和sdcard/Android/data/包名/cache下,会跟随App卸载被删除。

files和cache下的区别是,在手机设置-找到本应用-在存储中,点击清除缓存,cache下的文件会被删除,files下的文件不会。

谷歌推荐使用getExternalFilesDir。我们项目的下载是个本地功能,下载完成后是存本地数据库的,不是放网络上的,所以下载的音视频都放到了这下面,项目卸载时跟随App都删除了。getExternalFilesDir方法需要传入一个参数,传入null时得到就是sdcard/Android/data/包名/files,传入其他字符串比如"Picture"得到sdcard/Android/data/包名/files/Picture。

使用MediaStore访问公共目录

通过上面App专属目录只能操作本App专属目录,并且保存的文件会随着App卸载删除。通过MediaStore,App可以访问公共目录下的媒体文件,通过MediaStore操作Uri读写文件。

保存图片直接用 insertImage 方法就可以,可以传入Bitmap或图片在本地的路径,注意本地路径要是本App可以访问到的路径,否则没权读取

public void saveImage(String imagePath, String title, String desc) {
	MediaStore.Images.Media.insertImage(context.getContentResolver(), imagePath, title, desc);
}
或
public void saveImage(Bitmap bitmap, String title, String desc) {
	MediaStore.Images.Media.insertImage(context.getContentResolver(), bitmap, title, desc);
}

其他类型的文件保存就没有直接的方法了,大致可以用下面这样:

    public void saveFile(final Uri extUri, final String mimeType, final String saveName, final String desc,
                          final String netUrl) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    ContentValues values = new ContentValues();
                    values.put(MediaStore.Images.Media.DISPLAY_NAME, saveName);
                    values.put(MediaStore.Images.Media.TITLE, saveName);
                    values.put(MediaStore.Images.Media.DESCRIPTION, desc);
                    values.put(MediaStore.Images.Media.MIME_TYPE, mimeType);
                    ContentResolver cr = context.getContentResolver();
                    Uri uri = cr.insert(extUri, values);

                    byte[] buffer = new byte[1024];
                    ParcelFileDescriptor parcelFileDescriptor = cr.openFileDescriptor(uri, "w");
                    FileOutputStream fileOutputStream = new FileOutputStream(parcelFileDescriptor.getFileDescriptor());
                    URL url = new URL(netUrl);
                    InputStream inputStream = url.openStream();
                    while (true) {
                        int numRead = inputStream.read(buffer);
                        if (numRead == -1) {
                            break;
                        }
                        fileOutputStream.write(buffer, 0, numRead);
                    }
                    fileOutputStream.close();
                    parcelFileDescriptor.close();
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    // TODO close io
                }
            }
        }).start();
    }

看上面代码,前面得到uri,然后变为fileOutputStream,后面就是文件的读写了,inputStream也可以同过其他方式得到(比如本地文件等),有输入流就可以写到uri中了。

使用如下:

// 保存图片
saveFile(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/png", 
"myImage", "", "http://www.xxx.png");

// 保存视频
saveFile(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "video/mp4", 
"myVideo", "", "http://www.xxx.mp4");

// 保存音频
saveFile(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, "audio/mpeg", 
"myAudio", "", "http://www.xxx.mp3");

// Android Q 新增的下载目录
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
	saveFile(MediaStore.Downloads.EXTERNAL_CONTENT_URI, "text/plain", 
	"myText", "", "http://www.xxx.txt");
}

文件读取,以读取图片为例,其他的也一样

获取全部图片:

    public static List<Uri> loadPhotoFiles(Context context) {
        List<Uri> photoUris = new ArrayList<Uri>();
        Cursor cursor = context.getContentResolver().query(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Images.Media._ID}, null, null, null);
        while (cursor.moveToNext()) {
            int id = cursor.getInt(cursor
                    .getColumnIndex(MediaStore.Images.Media._ID));
            Uri photoUri = Uri.parse(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString() + File.separator + id);
            photoUris.add(photoUri);
        }
        return photoUris;
    }
    
     // uri 转 bitmap
     public static Bitmap getBitmapFromUri(Uri uri) throws IOException {
        ParcelFileDescriptor parcelFileDescriptor =
                context.getContentResolver().openFileDescriptor(uri, "r");
        FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
        Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
        parcelFileDescriptor.close();
        return image;
    }

根据title获取图片:

    private Bitmap getImage(String title) {
        Uri external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        ContentResolver resolver = context.getContentResolver();

        String selection = MediaStore.Images.Media.TITLE + "=?"; // 查询条件
        String[] args = new String[]{title}; // 上面?的值
        String[] projection = new String[]{MediaStore.Images.Media._ID}; // 查询的内容
        Cursor cursor = resolver.query(external, projection, selection, args, null);
        Uri imageUri = null;

        if (cursor != null && cursor.moveToFirst()) {
            imageUri = ContentUris.withAppendedId(external, cursor.getLong(0));
            cursor.close();
        }

        if (imageUri == null) {
            return null;
        }

        ParcelFileDescriptor pfd = null;
        try {
            pfd = getContentResolver().openFileDescriptor(imageUri, "r");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        if (pfd != null) {
            Bitmap bitmap = BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor());
            pfd.close();
            return bitmap;
        }
        
        return null;
    }

删除文件,需要先查询出uri

context.getContentResolver().delete(imageUri, null, null);

修改文件,用的比较少

  // 修改的内容以键值对放到ContentValues中
  ContentValues values = new ContentValues();
  values.put("title", "new title");
  getContentResolver().update(imageUri, values, null, null);
使用SAF访问指定目录

存储访问框架(Storage Access Framework),这种方式操作文件时会拉起系统页面,通过用户授权操作来完成文件读取,用户可以选择任何目录,用户选完后App就有了这个目录的读写权限。官方文档

保存一个文件时,用下面方法

    private void createFile(String fileName, String mimeType) {
        Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        if (!TextUtils.isEmpty(mimeType)) {
            intent.setType(mimeType);
        }
        intent.putExtra(Intent.EXTRA_TITLE, fileName);
        startActivityForResult(intent, REQUEST_CODE);
    }
    
        @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            Uri uri = null;
            if (data != null) {
                uri = data.getData();
                final int takeFlags = getIntent().getFlags()
                        & (Intent.FLAG_GRANT_READ_URI_PERMISSION
                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                // Check for the freshest data.
                getContentResolver().takePersistableUriPermission(uri, takeFlags);
                writeFile(uri, netUrl);
            }
        }
    }
    
    private void writeFile(Uri uri, String netUrl) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "w");
                    FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor());

                    byte[] buffer = new byte[1024];
                    URL url = new URL(netUrl);
                    InputStream inputStream = url.openStream();
                    while (true) {
                        int numRead = inputStream.read(buffer);
                        if (numRead == -1) {
                            break;
                        }
                        fileOutputStream.write(buffer, 0, numRead);
                    }

                    fileOutputStream.close();
                    pfd.close();
                    inputStream.close();
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

调用createFile会拉起下面界面,用户保存后会回调到onActivityResult中,并将uri传来,得到uri后就可以写入了。

读取一个文件,以读取图片为例:

    public void openImage() {
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("image/*");
        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) {
            Uri uri = null;
            if (data != null) {
                uri = data.getData();

                showImage(uri);
            }
        }
    }

    private void showImage(Uri uri) {
        if (uri == null)
            return;
        ParcelFileDescriptor pfd = null;
        try {
            pfd = getContentResolver().openFileDescriptor(uri, "r");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        if (pfd != null) {
            Bitmap bitmap = BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor());
            pdf.close();
            ImageView imageView = findViewById(R.id.imageView);
            imageView.setImageBitmap(bitmap);
        }
    }

删除文件,跟上面读取一样,在onActivityResult中调用deleteImage,代码如下:

    private void deleteImage(Uri uri) {
        final int takeFlags = getIntent().getFlags()
                & (Intent.FLAG_GRANT_READ_URI_PERMISSION
                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        // Check for the freshest data.
        getContentResolver().takePersistableUriPermission(uri, takeFlags);
        try {
            DocumentsContract.deleteDocument(getContentResolver(), uri);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

修改文件,应该也是通过Intent.ACTION_OPEN_DOCUMENT打开选择一个文件,最后在onActivityResult中得到选择文件的uri,再修改,没有具体使用过。

更新图片到图库

保存图片后需要更新到相册,之前下载图片到App 专属目录,然后通过方法1同步到相册,让用户在相册能看到下载的图片。在 Android Q 上面使用方法1不生效,应该是相册访问不到App专属目录,现在做法是通过方法2将图片存到公共目录。

// 方法1
Uri uri = Uri.fromFile(file);
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));

// 方法2
// 可以看上面文件存储,也可以传入Bitmap
MediaStore.Images.Media.insertImage(getContentResolver(), file.getAbsolutePath(), name, "");

参考链接:Android Q 要来了,给你一份很"全面"的适配指南!

发布了30 篇原创文章 · 获赞 13 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/y331271939/article/details/100899980