Android Q 适配

【参考文档】
谷歌官方预览
PictureSelector 2.0 Android Q 适配之旅
AndroidQ(10)分区存储完美适配

设备 ID

从 Android 10 开始已经无法完全标识一个设备,曾经用 mac 地址、IMEI 等设备信息标识设备的方法,从 Android 10 开始统统失效。而且无论你的 APP 是否是配过 Android 10。

IMEI 等设备信息

从 Android10 开始普通应用不再允许请求权限 android.permission.READ_PHONE_STATE。而且,无论你的 App 是否适配过 Android Q(既 targetSdkVersion 是否大于等于 29),均无法再获取到设备 IMEI 等设备信息。
受影响的 API

Build.getSerial();
TelephonyManager.getImei();
TelephonyManager.getMeid();
TelephonyManager.getDeviceId();
TelephonyManager.getSubscriberId();
TelephonyManager.getSimSerialNumber();

targetSdkVersion<29 的应用,其在获取设备 ID 时,会直接返回 null
targetSdkVersion>=29 的应用,其在获取设备 ID 时,会直接跑出异常 SecurityException
如果您的 App 希望在 Android 10 以下的设备中仍然获取设备 IMEI 等信息,可按以下方式进行适配:


Mac 地址随机分配

从 Android10 开始,默认情况下,在搭载 Android 10 或更高版本的设备上,系统会传输随机分配的 MAC 地址。(既从 Android 10 开始,普通应用已经无法获取设备的真正 mac 地址,标识设备已经无法使用 mac 地址)

唯一 ID 的替代

Google 给出的解决方案是:如果您的应用有 追踪非登录用户重装 的需求,可用 ANDROID_ID 来标识设备。
ANDROID_ID 的生成规则为:签名 + 设备信息 + 设备用户
ANDROID_ID 重置规则:设备恢复出厂设置时,ANDROID_ID 将被重置

String androidId = Settings.Secure.getString(this.getContentResolver(), Settings.Secure.ANDROID_ID);

也就是从 Android 10 开始已经无法完全标识一个设备,曾经用 mac 地址、IMEI 等设备信息标识设备的方法,从 Android 10 开始统统失效。而且无论你的 APP 是否是配过 Android 10。

外部存储

为了使用户更改的管理 Sdcard 中的文件,解决文件混乱的问题。从 Android 10 开始(API level 29),Android 将对外部存储进行一定的限制。默认情况下,对于外部存储,App 只能通过 Context.getExternalFilesDir () 访问自己的特定文件目录;以及系统特定的文件类型目录(例:照片、屏幕快照、视频 等)。

着重说一下存储

  1. Android Q 为每个应用程序在外部存储设备提供了一个独立的存储沙箱,应用直接通过文件路径保存的文件都会保存在应用的沙箱目录,另外应用卸载的时候默认所有应用沙箱目录是会被删除。
  2. 共享集合:不希望应用卸载删除的文件,需要应用通过 MediaProvider 或者 SAF 的方式保存在公共共享集合目录,公共集合目录包括:多媒体文件集合(音频、视频和图片)以及下载文件集合。
  3. 权限变更:应用读写自己沙箱和共享集合目录中应用自己的文件是不需要申请任何权限的,但是如果应用需要读取其他应用生成的多媒体文件就需要申请权限:
    (1)读取其他应用存放在共享集合的图片和视频文件,就需要分别申请 READ_MEDIA_IMAGES 和 READ_MEDIA_VIDEO 权限,具体要申请哪个权限取决于应用需要访问的文件类型;
    (2)读取其他应用存放在共享集合的音乐类型文件,就需要申请 READ_MEDIA_AUDIO 权限;
    (3)读取其他应用生成的多媒体文件,需要通过 MediaProvider 的接口读取,无法直接通过文件路径读取;
    (4)系统只提供了多媒体文件的读权限,没有提供写权限,应用无法通过申请写权限修改其他应用生成的文件;
    (5)下载目录的文件没有增加对应的权限,读取下载目录的文件需要通过 SAF 的方式读取。
  4. 写其他应用的多媒体文件,需要通过申请成为默认系统图库和音乐应用,或者让用户主动授权的方式实现
  5. 需要读写指定的任意目录的文件只能通过 SAF 的方式实现。

APP 专属路径

对于 App 专属 内部存储路径与外部存储路径的访问,将不再需要 READ_EXTERNAL_STORAGE 与 WRITE_EXTERNAL_STORAGE 权限:
内部存储路径 /data/data/<包名>/
外部存储路径 /storage/Android/data/<包名>/

手机共享路径

读取其他 APP 创建的共享文件,例:相册、屏幕快照 等,则需要申请 READ_EXTERNAL_STORAGE 权限:
图片:Photos、Screenshots 使用 MediaStore.Images API 访问
视频 使用 MediaStore.Video API 访问
音频 使用 MediaStore.Audio API 访问

Downloads 文件夹

读取手机的 Downloads 文件夹,不需要任何权限,需要使用 API Storage Access Framework

权限相关

主要包括:
在后台运行时访问设备位置信息
Android 10 引入了 ACCESS_BACKGROUND_LOCATION 权限。若应用在后台运行时,访问手机位置,需要动态申请该权限,用户则可以选择拒绝。官方给出的数据,大部分用户对位置信息是比较敏感的。而且大部分用户是不允许应用在后台使用位置信息的。
从后台启动 Activity 的限制
屏幕录制
不需要手动申请权限,但官方 API 内部会向用户弹窗申请权限
摄像头和麦克风
Android 9 摄像头和麦克风 后台权限已经移除了

活动探知 —— 新增权限

在访问用户步数或其他物理活动类别是需要获取运行时权限
剪切板隐私限制
从 Android P 开始,除非你的应用是默认输入法,否则它无法访问用户的剪贴板数据;但向剪切板写入数据不影响。

   //用于保存拍照图片的uri
    private Uri mCameraUri;

    // 用于保存图片的文件路径,Android 10以下使用图片路径访问图片
    private String mCameraImagePath;

    // 是否是Android 10以上手机
    private boolean isAndroidQ = Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q;

    /**
     * 调起相机拍照
     */
    private void openCamera() {
        Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        // 判断是否有相机
        if (captureIntent.resolveActivity(getPackageManager()) != null) {
            File photoFile = null;
            Uri photoUri = null;

            if (isAndroidQ) {
                // 适配android 10
                photoUri = createImageUri();
            } else {
                try {
                    photoFile = createImageFile();
                } catch (IOException e) {
                    e.printStackTrace();
                }

                if (photoFile != null) {
                    mCameraImagePath = photoFile.getAbsolutePath();
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                        //适配Android 7.0文件权限,通过FileProvider创建一个content类型的Uri
                        photoUri = FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", photoFile);
                    } else {
                        photoUri = Uri.fromFile(photoFile);
                    }
                }
            }

            mCameraUri = photoUri;
            if (photoUri != null) {
                captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
                captureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                startActivityForResult(captureIntent, CAMERA_REQUEST_CODE);
            }
        }
    }

    /**
     * 创建图片地址uri,用于保存拍照后的照片 Android 10以后使用这种方法
     */
    private Uri createImageUri() {
        String status = Environment.getExternalStorageState();
        // 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储
        if (status.equals(Environment.MEDIA_MOUNTED)) {
           return getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new ContentValues());
        } else {
            return getContentResolver().insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, new ContentValues());
        }
    }

    /**
     * 创建保存图片的文件
     */
    private File createImageFile() throws IOException {
        String imageName = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
        File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
        if (!storageDir.exists()) {
            storageDir.mkdir();
        }
        File tempFile = new File(storageDir, imageName);
        if (!Environment.MEDIA_MOUNTED.equals(EnvironmentCompat.getStorageState(tempFile))) {
            return null;
        }
        return tempFile;
 


公共文件的增、删、改、查

公共文件的操作需要用到ContentResolver和Cursor

向公共文件夹添加文件

//将文件保存到公共的媒体文件夹
//这里的filepath不是绝对路径,而是某个媒体文件夹下的子路径,和沙盒子文件夹类似
//这里的filename单纯的指文件名,不包含路径
public void saveSignImage(String filePath,String fileName, Bitmap bitmap) {
    try {
        //设置保存参数到ContentValues中
        ContentValues contentValues = new ContentValues();
        //设置文件名
        contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
        //兼容Android Q和以下版本
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            //android Q中不再使用DATA字段,而用RELATIVE_PATH代替
            //RELATIVE_PATH是相对路径不是绝对路径
            //DCIM是系统文件夹,关于系统文件夹可以到系统自带的文件管理器中查看,不可以写没存在的名字
            contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "DCIM/signImage");
            //contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "Music/signImage");
        } else {
            contentValues.put(MediaStore.Images.Media.DATA, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getPath());
        }
        //设置文件类型
        contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/JPEG");
        //执行insert操作,向系统文件夹中添加文件
        //EXTERNAL_CONTENT_URI代表外部存储器,该值不变
        Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
        if (uri != null) {
            //若生成了uri,则表示该文件添加成功
            //使用流将内容写入该uri中即可
            OutputStream outputStream = getContentResolver().openOutputStream(uri);
            if (outputStream != null) {
                bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);
                outputStream.flush();
                outputStream.close();
            }
        }
    } catch (Exception e) {
    }
}

原文链接:https://blog.csdn.net/yehui928186846/article/details/101706238

查询公共文件夹下的文件

//在公共文件夹下查询图片
//这里的filepath在androidQ中表示相对路径
//在androidQ以下是绝对路径
public Bitmap querySignImage(String filePath) {
    try {
        //兼容androidQ和以下版本
        String queryPathKey = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q ? MediaStore.Images.Media.RELATIVE_PATH : MediaStore.Images.Media.DATA;
        //查询的条件语句
        String selection = queryPathKey + "=? ";
        //查询的sql
        //Uri:指向外部存储Uri
        //projection:查询那些结果
        //selection:查询的where条件
        //sortOrder:排序
        Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                new String[]{MediaStore.Images.Media._ID, queryPathKey, MediaStore.Images.Media.MIME_TYPE, MediaStore.Images.Media.DISPLAY_NAME},
                selection,
                new String[]{filePath},
                null);

        //是否查询到了
        if (cursor != null && cursor.moveToFirst()) {
            //循环取出所有查询到的数据
            do {
                //一张图片的基本信息
                int id = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media._ID));//uri的id,用于获取图片
                String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.RELATIVE_PATH));//图片的相对路径
                String type = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE));//图片类型
                String name = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME));//图片名字
                //根据图片id获取uri,这里的操作是拼接uri
                Uri uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "" + id);
                //官方代码:
                 Uri contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
                if (uri != null) {
                    //通过流转化成bitmap对象
                    InputStream inputStream = getContentResolver().openInputStream(uri);
                    return BitmapFactory.decodeStream(inputStream);
                }
            } while (cursor.moveToNext());
        }
        if (cursor != null)
            cursor.close();
    } catch (Exception e) {
    }
    return null;
}
————————————————
原文链接:https://blog.csdn.net/yehui928186846/article/details/101706238

System directories

Method Result
Environment.getDataDirectory() /data
Environment.getDownloadCacheDirectory() /cache
Environment.getRootDirectory() /system

External storage directories

Method Result
Environment.getExternalStorageDirectory() /storage/sdcard0
Environment.getExternalStoragePublicDirectory(DIRECTORY_ALARMS) /storage/sdcard0/Alarms
Environment.getExternalStoragePublicDirectory(DIRECTORY_DCIM) /storage/sdcard0/DCIM
Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS) /storage/sdcard0/Download
Environment.getExternalStoragePublicDirectory(DIRECTORY_MOVIES) /storage/sdcard0/Movies
Environment.getExternalStoragePublicDirectory(DIRECTORY_MUSIC) /storage/sdcard0/Music
Environment.getExternalStoragePublicDirectory(DIRECTORY_NOTIFICATIONS) /storage/sdcard0/Notifications
Environment.getExternalStoragePublicDirectory(DIRECTORY_PICTURES) /storage/sdcard0/Pictures
Environment.getExternalStoragePublicDirectory(DIRECTORY_PODCASTS) /storage/sdcard0/Podcasts
Environment.getExternalStoragePublicDirectory(DIRECTORY_RINGTONES) /storage/sdcard0/Ringtones

Application directories

Method Result
getCacheDir() /data/data/package/cache
getFilesDir() /data/data/package/files
getFilesDir().getParent() /data/data/package

Application External storage directories

Method Result
getExternalCacheDir() /storage/sdcard0/Android/data/package/cache
getExternalFilesDir(null) /storage/sdcard0/Android/data/package/files
getExternalFilesDir(DIRECTORY_ALARMS) /storage/sdcard0/Android/data/package/files/Alarms
getExternalFilesDir(DIRECTORY_DCIM) /storage/sdcard0/Android/data/package/files/DCIM
getExternalFilesDir(DIRECTORY_DOWNLOADS) /storage/sdcard0/Android/data/package/files/Download
getExternalFilesDir(DIRECTORY_MOVIES) /storage/sdcard0/Android/data/package/files/Movies
getExternalFilesDir(DIRECTORY_MUSIC) /storage/sdcard0/Android/data/package/files/Music
getExternalFilesDir(DIRECTORY_NOTIFICATIONS) /storage/sdcard0/Android/data/package/files/Notifications
getExternalFilesDir(DIRECTORY_PICTURES) /storage/sdcard0/Android/data/package/files/Pictures
getExternalFilesDir(DIRECTORY_PODCASTS) /storage/sdcard0/Android/data/package/files/Podcasts
getExternalFilesDir(DIRECTORY_RINGTONES) /storage/sdcard0/Android/data/package/files/Ringtones

Android Q存储适配

Android Q 分区存储

  1. Android Q文件存储机制修改成了沙盒模式
  2. 应用只能访问自己沙盒下的文件和公共媒体文件,只能通过 Context.getExternalFilesDir () 访问自己的特定文件目录;以及系统特定的文件类型目录(例:照片、屏幕快照、视频 等)
  3. 对于Android Q以下,还是使用老的文件存储方式

权限

Android Q不再需要申请文件读写权限,默认可以读写自己沙盒文件和公共媒体文件。所以,Q以上不需要再动态申请文件读写权限。

沙盒存储/读写

/废弃方法
//不再用以下代码获取文件根目录了
Environment.getExternalStorageDirectory();
Environment.getExternalStoragePublicDirectory();
//获取沙盒下的文件目录
//沙盒下的图片文件夹
File filePictures = getExternalFilesDir(Environment.DIRECTORY_PICTURES);

package android.os;
public class Environment {
    private static final String TAG = "Environment";
    
    public static String DIRECTORY_NOTIFICATIONS = "Notifications";
    
    public static String DIRECTORY_PICTURES = "Pictures";

    public static String DIRECTORY_MOVIES = "Movies";

    public static String DIRECTORY_DOWNLOADS = "Download";
    
    ...

创建指定文件 存储

 		SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
        String imageFileName = "JPEG_" + sdf.format(new Date()) + ".jpg";
        
        File directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
        if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.Q){
            directory=getContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES);
        }
        
        File storageDir = new File(directory, "compress");

你可能感兴趣的:(Android Q 适配)