Android 10.0(Q api=29)适配

1、存储权限(分区存储)
Android手机分为外部储存和内部储存
内部储存:/data 目录。一般我们使用getFilesDir() 或 getCacheDir() 方法获取本应用的内部储存路径,读写该路径下的文件不需要申请储存空间读写权限,且卸载应用时会自动删除。
外部储存:/storage 或 /mnt 目录。一般我们使用getExternalStorageDirectory()方法获取的路径来存取文件。
因为不同厂商、系统版本的原因,所以上述的方法并没有一个固定的文件路径。那我们所说的外部储存访问限制是针对getExternalStorageDirectory()路径下的文件
android Q 将外部存储分为
(1)特定目录(App-specific),使用getExternalFilesDir()或 getExternalCacheDir()方法访问。无需权限,且卸载应用时会自动删除。
(2)照片、视频、音频这类媒体文件。使用MediaStore 访问,访问其他应用的媒体文件时需要READ_EXTERNAL_STORAGE权限。
(3)其他目录,使用存储访问框架SAF(Storage Access Framwork)

适配分为两种方式
1、最简单粗暴的方法就是在AndroidManifest.xml中添加 android:requestLegacyExternalStorage="true"来请求使用旧的存储模式
Android Q Beta 3之前都是强制的,但为了给开发者适配的时间才没有强制执行
2、Environment.getExternalStorageDirectory()方法,换成getExternalFilesDir()方法(包括下载的安装包这类的文件)。如果是缓存类型文件,可以放到getExternalCacheDir()路径下。
或者使用MediaStore,将文件存至对应的媒体类型中(图片:MediaStore.Images ,视频:MediaStore.Video,音频:MediaStore.Audio),不过仅限于多媒体文件。
注意:如果应用通过升级安装,那么还会使用以前的储存模式(Legacy View)。只有通过首次安装或是卸载重新安装才能启用新模式(Filtered View)。
所以在适配时,我们的判断代码如下:

// 使用Environment.isExternalStorageLegacy()来检查APP的运行模式
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && 
       !Environment.isExternalStorageLegacy()) {
          
    }

这样的好处是你可以在用户升级后,能方便的将用户的数据移动至应用的特定目录。否则你只能通过SAF去移动,这样会非常麻烦。如果你要移动数据注意只适用于Android 10下,所以现在适配反而是一个好时机。当然如果你不需要迁移数据,那适配会更省事。
2、定位权限
为了让用户更好地控制应用对位置信息的访问权限,Android Q 引入了新的位置权限 ACCESS_BACKGROUND_LOCATION。
与现有的 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION 权限不同,新权限仅会影响应用在后台运行时对位置信息的访问权。除非应用的某个 Activity 可见或应用正在运行前台服务,否则应用将被视为在后台运行。
谷歌会按照应用的targetSDK作出不同处理:
targetSDK <= P 应用如果请求了ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION权限,Q设备会自动帮你申请ACCESS_BACKGROUND_LOCATION权限。

// 位置权限
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? new PermissionMeta(2, Manifest.permission.ACCESS_BACKGROUND_LOCATION,
        "位置权限说明",
        "使用此权限只用来获得您的定位给您更好的服务。") : new PermissionMeta(2, Manifest.permission.ACCESS_FINE_LOCATION,
        "位置权限说明",
        "使用此权限只用来获得您的定位给您更好的服务。")

3、设备唯一标识符
在Android10中, 系统不允许普通App请求android.permission.READ_PHONE_STATE权限, 故新版App需要取消该动态权限的申请。
设备 ID
在Q设备上通过((TelephonyManager)getActivity().getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId()
获得设备ID,会返回空值(targetSDK<=P)或者报错(targetSDK==Q)。且官方所说的READ_PRIVILEGED_PHONE_STATE权限只提供给系统app,
所以当前获取设备唯一ID的方式为使用SSAID, 若获取为空的话则使用UUID.randomUUID().toString()获得一个随机ID并存储起来, 该ID保证唯一, 但App卸载重装之后就会改变

String androidId = android.provider.Settings.Secure.getString(context.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    String deviceId = android.provider.Settings.Secure.getString(context.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID)    
}else {
    String deviceId = ((TelephonyManager)getActivity().getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId()
}

具体实现:
SystemInfoUtils.getDeviceId(context)已处理,所以保持不变即可,但是如果项目中其他地方或者SDK调用getDeviceId()需要处理,可根据上面代码进行适配,ANDROID_ID替换DeviceId即可
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();
    如果您的 App 希望在 Android 10 以下的设备中仍然获取设备 IMEI 等信息,可按以下方式进行适配:
 <uses-permission
-         android:name="android.permission.READ_PHONE_STATE"
-         android:maxSdkVersion="28" />

4、minSDK警告
在 Android Q 中,当用户首次运行以 Android 6.0(API 级别 23)以下的版本为目标平台的任何应用时,Android平台会向用户发出警告。如果此应用要求用户授予权限,则系统会先向用户提供调整应用权限的机会,然后才会允许此应用首次运行。
谷歌要求运行在Q设备上的应用targetSDK>=23,不然会向用户发出警告。
5、EditText默认不获取焦点,不自动弹出键盘
该问题出现在 targetSdkVersion >= Build.VERSION_CODES.P 情况下,且设备版本为Android P以上版本,解决方法在onCreate中加入如下代码,可获得焦点,如需要弹出键盘可延迟一下:

mEditText.post(() -> {
  mEditText.requestFocus();
  mEditText.setFocusable(true);
  mEditText.setFocusableInTouchMode(true);
});

6、安装APK Intent及其它共享文件相关Intent
/**

  • 自Android N开始,是通过FileProvider共享相关文件,但是Android Q对公有目录 File API进行*了限制,只能通过Uri来操作,
  • 从代码上看,又变得和以前低版本一样了,只是必须加上权限代码Intent.FLAG_GRANT_READ_URI_PERMISSION
    */
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
   //适配Android Q,注意mFilePath是通过ContentResolver得到的,上述有相关代码
   Intent intent = new Intent(Intent.ACTION_VIEW);
   intent.setDataAndType(Uri.parse(mFilePath) ,"application/vnd.android.package-archive");
   intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
   intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
   startActivity(intent);
  }

7、Region.Op相关异常:
java.lang.IllegalArgumentException: Invalid Region.Op - only INTERSECT and DIFFERENCE are allowed
当 targetSdkVersion >= Build.VERSION_CODES.P 时调用 canvas.clipPath(path, http://Region.Op.XXX); 引起的异常,参考源码如下:

@Deprecated
public boolean clipPath(@NonNull Path path, @NonNull Region.Op op) {
     checkValidClipOp(op);
     return nClipPath(mNativeCanvasWrapper, path.readOnlyNI(), op.nativeInt);
}

private static void checkValidClipOp(@NonNull Region.Op op) {
     if (sCompatiblityVersion >= Build.VERSION_CODES.P
         && op != Region.Op.INTERSECT && op != Region.Op.DIFFERENCE) {
         throw new IllegalArgumentException(
                    "Invalid Region.Op - only INTERSECT and DIFFERENCE are allowed");
     }
}

我们可以看到当目标版本从Android P开始,Canvas.clipPath(@NonNull Path path, @NonNull Region.Op op) ; 已经被废弃,而且是包含异常风险的废弃API,只有 Region.Op.INTERSECT 和 Region.Op.DIFFERENCE 得到兼容
具体实现:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    canvas.clipPath(path);
} else {
    canvas.clipPath(path, Region.Op.XOR);// REPLACE、UNION 等
}

8、相册选择图片,通过URI获取Bitmap

具体实现:
选择相册,通过Uri获取Bitmap

if (resultCode == RESULT_OK) {
    Uri uri = data.getData();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        bitmap = BitmapUtils.getBitmapFromUri(getActivity(), uri);
    } else {
        bitmap = BitmapUtils.decodeBitmapFromPath(BaiTiaoBitmapUtils.getRealFilePath(getActivity(), uri));
    }
    updateCameraPic(bitmap, null);
}

// android 10 通过uri加载图片
public static Bitmap getBitmapFromUri(Context context, Uri uri) {
    try {
        ParcelFileDescriptor parcelFileDescriptor =
                context.getContentResolver().openFileDescriptor(uri, "r");
        FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
        Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
        parcelFileDescriptor.close();
        return image;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

public static Bitmap decodeBitmapFromPath(String path) {
        BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
        bitmapOptions.inSampleSize = 2;
        Bitmap bitmap = BitmapFactory.decodeFile(path, bitmapOptions);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int options = 50;
        bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);
        while (baos.toByteArray().length / 1024 > 300) {
// 循环判断如果压缩后图片是否大于300kb继续压缩

            baos.reset();
            options -= 5;
            if (options < 6) {//为了防止图片大小一直达不到200kb,options一直在递减,当options<0时,下面的方法会报错
                // 也就是说即使达不到200kb,也就压缩到10了
                bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);
                break;
            }
// 这里压缩options%,把压缩后的数据存放到baos中
            bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);
        }
        LogUtils.e("===图片压缩后显示大小===" + baos.toByteArray().length / 1024 + " kb");
        return BitmapFactory.decodeByteArray(baos.toByteArray(), 0, baos.toByteArray().length);
    }

public static String getRealFilePath(final Context context, final Uri uri) {
    if (null == uri) return null;
    final String scheme = uri.getScheme();
    String data = null;
    if (scheme == null)
        data = uri.getPath();
    else if (ContentResolver.SCHEME_FILE.equals(scheme)) {
        data = uri.getPath();
    } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
        Cursor cursor = context.getContentResolver().query(uri, new String[]{MediaStore.Images.ImageColumns.DATA}, null, null, null);
        if (null != cursor) {
            if (cursor.moveToFirst()) {
                int index = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
                if (index > -1) {
                    data = cursor.getString(index);
                }
            }
            cursor.close();
        }
    }
    return data;
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值