对于Android Q 也就是Android 10.0的到来大家做好准备了吗, 这篇博客用来给大家普及Android Q 适配知识的普及。
当然本人首推的是官方Google的文档,不关官方更新到那个版本我们开发者肯定关心的是如何适配。
Android Q 方面还是使用READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE 作为面向用户的存储相关运行时权限,但是现在即使有了这些权限任然无法读取外部储存。
Android Q 规定了俩种储存空间一种是Legacy View(传统视图),Filtered View(筛选视图)。
在Android Q 上 tatget SDK 大于或者等于 29 那么就会默认App赋予Filtered View,小于的话就是Legacy View,也就是小于的话如上Legacy View(筛选视图) 兼容以前,大于只能通过制定的方式访问,划重点啦。App可以在AndroidManifest.xml中进行设置新的属性requestLegacyExternalStorage,来修改外部存储空间视图模型,true为 Legacy View,false 为 Filtered View。
以为就这点不你错了还有,Android Q 还对查询,读写文件的操作上做了改动,比如我们的图片中的地理位置不在默认提供给我们,查询MediaProvider 获得的DATA字段不在可靠,新增文件pending状态, 并且在文件访问的时候DATA目录废弃,假如判断一个文件是否存在那么不应该使用DATA比较那样是不可靠的,应该使用MediaStore 接口或者SAF获取文件的Uri后利用Uri打开FD或者输入输出流,不能转换文件路径进行比较。
问题:Android Q 将App-Specific目录中的私有文件分享的时候使用了File://类型的URi
分析:Android Q 上由于App-Specific目录中私有的受保护的,其他APP无法通过URi进行访问。
解决:使用FileProvider ,将Context://类型的Uri分享给其他App,
...
...
...
...
。例如,以下paths
元素告诉FileProvider您打算为images/
私有文件区域的子目录请求内容URI
...
1.1 该
元素必须包含以下一个或多个子元素:该
元素必须包含以下一个或多个子元素:
name ” path = “ path ” />
1.2 应用程序内部存储区域的缓存子目录中的文件。该子目录的根路径与返回的值相同getCacheDir()
。
1.3 应用程序外部存储区域根目录中的文件。该子目录的根路径与返回的值相同Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)
。
name ” path = “ path ” /> name ” path = “ path ” />
1.4 应用外部媒体区域根目录中的文件。该子目录的根路径与第一个结果返回的值相同
name ” path = “ path ” /> name ” path = “ path ” />
2.根据 FileProvider 声明中的 meta data,在 res/xml 中新建 filepaths.xml ,用于定义分享的路径。
3. 在 APP 逻辑代码中生成要分享的 uri,设置权限,然后发送 uri。
String filePath = getExternalFilesDir("Documents") + "/MyTestImage.PNG";
Uri uri = FileProvider.getUriForFile(this, "androidx.core.content.FileProvider", new File(filePath));
Intent intent = new Intent(Intent.ACTION_SEND);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(uri, getContentResolver().getType(uri));
startActivity(Intent.createChooser(intent, "File Provider share"));
4. 接收方 APP 的组件设置对应的 intent-filter。
5. 接收方 APP 的组件收到 intent,解析获得 uri,通过 uri 获取文件的 FD。
ri uri = getIntent().getData();
ParcelFileDescriptor pdf = null;
try {
if (uri != null) {
LogUtil.log("Uri: " + uri);
pdf = getContentResolver().openFileDescriptor(uri, "r ");
LogUtil.log("Pdf: " + pdf);
}
} catch (FileNotFoundException e) {
LogUtil.log("open file fail
");
} finally {
try {
if (pdf != null) {
pdf.close();
}
} catch (IOException e1) {
LogUtil.log("close fd fail ");
}
}
APP 可以实现自定义 ContentProvider 来向外提供 APP 私有文件。
如果您想要访问内容提供程序中的数据,可以将应用的 Context
中的 ContentResolver
对象用作客户端来与提供程序通信。 ContentResolver
对象会与提供程序对象(即实现 ContentProvider
的类实例)通信。 提供程序对象从客户端接收数据请求,执行请求的操作并返回结果。
如果您不打算与其他应用共享数据,则无需开发自己的提供程序。 不过,您需要通过自己的提供程序在您自己的应用中提供自定义搜索建议。 如果您想将复杂的数据或文件从您的应用复制并粘贴到其他应用中,也需要创建您自己的提供程序。
Android 本身包括的内容提供程序可管理音频、视频、图像和个人联系信息等数据。任何 Android 应用都可以访问这些提供程序,但会受到某些限制。
文档提供程序的基类。文档提供程序提供对持久文件的读写访问权限,例如存储在本地磁盘上的文件或云存储服务中的文件。要创建文档提供程序,请扩展此类,实现抽象方法,并将其添加到清单中,如下所示:
...
...
...
定义提供程序时,必须使用android.Manifest.permission#MANAGE_DOCUMENTS
,这是系统只能获得的权限。应用程序无法直接使用文档提供程序; 它们必须经过Intent#ACTION_OPEN_DOCUMENT
或Intent#ACTION_CREATE_DOCUMENT
需要用户主动导航和选择文档。当用户通过该UI选择文档时,系统向请求的应用程序发出窄URI权限授予。
附上Google 官方文档讲解的DocumentsProvider:
https://developer.android.google.cn/reference/kotlin/android/provider/DocumentsProvider
还记得我们说图片默认不在提供地理信息吗,如果APP需要访问图片的 Exif Metadata 地理信息需要这样了。
Uri photoUri = Uri.withAppendedPath(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
cursor.getString(idColumnIndex));
final double[] latLong;
// Get location data from the ExifInterface class.
photoUri = MediaStore.setRequireOriginal(photoUri);
InputStream stream = getContentResolver().openInputStream(photoUri);
if (stream != null) {
ExifInterface exifInterface = new ExifInterface(stream);
double[] returnedLatLong = exifInterface.getLatLong();
// If lat/long is null, fall back to the coordinates (0, 0).
latLong = returnedLatLong != null ? returnedLatLong : new double[2];
// Don't reuse the stream associated with the instance of "ExifInterface".
stream.close();
} else {
// Failed to load the stream, so return the coordinates (0, 0).
latLong = new double[2];
}
MediaStore 中,DATA(即_data)字段,在 Android Q 中开始废弃,不再表示文件的真实路径。读写文件或判断文件是否存在,不应该使用 DATA 字段,而要使用 openFileDescriptor。
同时也无法直接使用路径访问公共目录的文件。
通过 MediaStore.Files 接口访问文件时,只展示多媒体文件(图片、视频、音频)。其他文件,例如 PDF 文件,无法访问到。
Android Q 上,MediaStore 中添加了一个 IS_PENDING Flag,用于标记当前文件是 Pending 状态。
其他 APP 通过 MediaStore 查询文件,如果没有设置 setIncludePending 接口,就查询不到设置为 Pending 状态的文件,这就能使 APP 专享此文件。
这个 flag 在一些应用场景下可以使用,例如在下载文件的时候:下载中,文件设置为 Pending 状态;下载完成,把文件 Pending 状态置为 0。
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "myImage.PNG");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
values.put(MediaStore.Images.Media.IS_PENDING, 1);
ContentResolver resolver = context.getContentResolver();
Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Uri item = resolver.insert(uri, values);
try {
ParcelFileDescriptor pfd = resolver.openFileDescriptor(item, "w", null);
// write data into the pending image.
} catch (IOException e) {
LogUtil.log("write image fail");
}
// clear IS_PENDING flag after writing finished.
values.clear();
values.put(MediaStore.Images.Media.IS_PENDING, 0);
resolver.update(item, values, null, null);
对于ID 官方文档是这样说:
在使用 Android 标识符时,请遵循以下最佳做法:
#1:避免使用硬件标识符。 在大多数用例中,您可以避免使用硬件标识符,例如 SSAID (Android ID) 和 IMEI,而不会限制所需的功能。
#2:只针对用户分析或广告用例使用广告 ID。 在使用广告 ID 时,请始终遵循用户关于广告跟踪的选择。此外,请确保标识符无法关联到个人身份信息 (PII),并避免桥接广告 ID 重置。
#3:尽一切可能针对防欺诈支付和电话以外的所有其他用例使用实例 ID 或私密存储的 GUID。 对于绝大多数非广告用例,使用实例 ID 或 GUID 应该足矣。
#4:使用适合您的用例的 API 以尽量降低隐私权风险。 使用 DRM API 保护重要内容,并使用 SafetyNet API 防止滥用行为。SafetyNet API 是能够确定设备真伪而不会招致隐私权风险的最简单方法。
Android 8.0 及更高版本中的标识符
MAC 地址具有全局唯一性,无法由用户重置,在恢复出厂设置后也不会变化。因此,一般不建议使用 MAC 地址进行任何形式的用户标识。在 Android 6.0(API 级别 23)及更高版本中,本地设备 MAC 地址(例如 WLAN 和蓝牙)无法通过第三方 API 获取。WifiInfo.getMacAddress()
方法和 BluetoothAdapter.getDefaultAdapter().getAddress()
方法都会返回 02:00:00:00:00:00
。
此外,您还必须拥有下列权限,才能获取通过蓝牙和 WLAN 扫描获得的附近外部设备的 MAC 地址:
方法/属性 | 所需权限 |
WifiManager.getScanResults() | ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION |
BluetoothDevice.ACTION_FOUND | ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION |
BluetoothLeScanner.startScan(ScanCallback) | ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION |
适配方案
使用实例 ID 和 GUID
标识运行在设备上的应用实例最简单明了的方法就是使用实例 ID,在大多数非广告用例中,这是建议的解决方案。只有进行了针对性配置的应用实例才能访问该标识符,并且标识符重置起来(相对)容易,因为它只存在于应用的安装期。
因此,与无法重置的设备级硬件 ID 相比,实例 ID 具有更好的隐私权属性。
对于实例 ID 不实用的情况,您还可以使用自定义全局唯一 ID (GUID) 对应用实例进行唯一标识。最简单的方式是使用以下代码生成您自己的 GUID。
String uniqueID = UUID.randomUUID().toString();
附上官方文档: https://developer.android.google.cn/training/articles/user-data-ids#java