关于Android适配版本Q

对于Android Q  也就是Android 10.0的到来大家做好准备了吗, 这篇博客用来给大家普及Android Q 适配知识的普及。

当然本人首推的是官方Google的文档,不关官方更新到那个版本我们开发者肯定关心的是如何适配。

                                                       1 储存空间

Android Q 方面还是使用READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE 作为面向用户的存储相关运行时权限,但是现在即使有了这些权限任然无法读取外部储存。

  •  公共目录:Downloads、Documents、Pictures 、DCIM、Movies、Music、Ringtones 等
  1.    公共目录下APP卸载后不会删除。
  2. APP 可以通过 SAF(System Access Framework)、MediaStore 接口访问其中的文件。
  • App-specific 目录
  1. APP 卸载后数据清除。
  2. APP 的私密目录,APP 访问自己的 App-specific 目录时无需任何权限。

Android Q 规定了俩种储存空间一种是Legacy View(传统视图),Filtered View(筛选视图)。

  • Filtered View(筛选视图)
  1. App可以直接访问App-specific目录,但不能直接访问App-specific外部目录,访问公共目录或者其他App的App-soecific需通过MediaStore,SAF(系统访问框架的缩写),或者其他App提供的ContextPrivate,FileProvider 等访问。
  • Legacy View(传统视图)
  1. Android Q  兼容旧版本申请权限后App可以访问外部储存,拥有完成的访问权限。

在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或者输入输出流,不能转换文件路径进行比较。

                                                 2 无法正确的分享文件

问题:Android Q 将App-Specific目录中的私有文件分享的时候使用了File://类型的URi

分析:Android Q 上由于App-Specific目录中私有的受保护的,其他APP无法通过URi进行访问。

解决:使用FileProvider ,将Context://类型的Uri分享给其他App,

  1. APP可以使用FileProvider 将私有文件的读写分享给其他APP。
  • 定义FileProvider 
<manifest> 
    ... 
    <application> 
        ... 
        <provider android:name = “androidx.core.content.FileProvider”
                  android:authorities = “com.mydomain.fileprovider” 
                  android:exported = “false” 
                  android:resource="@xml/filepaths"
                  android:grantUriPermissions = “ true“ >       
      ... </ provider>  
       ... </ application>
 </ manifest>
  • 指定可用文件ile
  1. Provider只能为您事先指定的目录中的文件生成内容URI。要指定目录,请使用元素的子元素以XML格式指定其存储区域和路径<paths>。例如,以下paths元素告诉FileProvider您打算为images/私有文件区域的子目录请求内容URI 
<paths xmlns:android = “http://schemas.android.com/apk/res/android” >
 <files-path name = “my_images” path = “images /” />  
   ... </ paths> 
      

  1.1   该<paths>元素必须包含以下一个或多个子元素:该<paths>元素必须包含以下一个或多个子元素:

 <files-path name = “ name ” path = “ path ” />    

1.2 应用程序内部存储区域的缓存子目录中的文件。该子目录的根路径与返回的值相同getCacheDir()

<cache-path name =“ name ”path =“ path ”/>

1.3 应用程序外部存储区域根目录中的文件。该子目录的根路径与返回的值相同Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)


<external-files-path name = “ name ” path = “ path ” />   <external-files-path name = “ name ” path = “ path ” />   

 1.4 应用外部媒体区域根目录中的文件。该子目录的根路径与第一个结果返回的值相同


<external-media-path name = “ name ” path = “ path ” />   
<external-media-path name = “ 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 ");
    }
}

                                                   以上是使用FileProvider 

                                                2.1 使用ContentProvider分享

 APP 可以实现自定义 ContentProvider 来向外提供 APP 私有文件。

如果您想要访问内容提供程序中的数据,可以将应用的 Context 中的 ContentResolver 对象用作客户端来与提供程序通信。 ContentResolver 对象会与提供程序对象(即实现 ContentProvider 的类实例)通信。 提供程序对象从客户端接收数据请求,执行请求的操作并返回结果。

如果您不打算与其他应用共享数据,则无需开发自己的提供程序。 不过,您需要通过自己的提供程序在您自己的应用中提供自定义搜索建议。 如果您想将复杂的数据或文件从您的应用复制并粘贴到其他应用中,也需要创建您自己的提供程序。

Android 本身包括的内容提供程序可管理音频、视频、图像和个人联系信息等数据。任何 Android 应用都可以访问这些提供程序,但会受到某些限制。

 

 

                                               2.2 使用 DocumentsProvider

文档提供程序的基类。文档提供程序提供对持久文件的读写访问权限,例如存储在本地磁盘上的文件或云存储服务中的文件。要创建文档提供程序,请扩展此类,实现抽象方法,并将其添加到清单中,如下所示:

<manifest>
     ...
     <application>
         ...
         <provider
             android:name="com.example.MyCloudProvider"
             android:authorities="com.example.mycloudprovider"
             android:exported="true"
             android:grantUriPermissions="true"
             android:permission="android.permission.MANAGE_DOCUMENTS"
             android:enabled="@bool/isAtLeastKitKat">
             <intent-filter>
                 <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
             </intent-filter>
         </provider>
         ...
     </application>
 </manifest>

定义提供程序时,必须使用android.Manifest.permission#MANAGE_DOCUMENTS,这是系统只能获得的权限。应用程序无法直接使用文档提供程序; 它们必须经过Intent#ACTION_OPEN_DOCUMENTIntent#ACTION_CREATE_DOCUMENT需要用户主动导航和选择文档。当用户通过该UI选择文档时,系统向请求的应用程序发出窄URI权限授予。 

 附上Google 官方文档讲解的DocumentsProvider:

https://developer.android.google.cn/reference/kotlin/android/provider/DocumentsProvider

 

 

                                                               3 图片的地理位置信息 

  还记得我们说图片默认不在提供地理信息吗,如果APP需要访问图片的 Exif Metadata 地理信息需要这样了。

  • 1,申请 ACCESS_MEDIA_LOCATION 权限。
  • 2,通过 MediaStore.setRequireOriginal 返回新 Uri。
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];

}

                                                              4  DATA字段不在可靠

MediaStore 中,DATA(即_data)字段,在 Android Q 中开始废弃,不再表示文件的真实路径。读写文件或判断文件是否存在,不应该使用 DATA 字段,而要使用 openFileDescriptor。

同时也无法直接使用路径访问公共目录的文件。

                                                                                      

                                                         5  MediaStore.Files 接口自过滤

通过 MediaStore.Files 接口访问文件时,只展示多媒体文件(图片、视频、音频)。其他文件,例如 PDF 文件,无法访问到。

                                                

                                                             6文件的 Pending 状态

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);

                                                             

                                                                    7兼容性

  1. TargetSdkVersion 并且没有申请 READ_PHONE_STATE 权限,或者 TargetSdkVersion>=Q,获取 device id 会抛异常 SecurityException;
  2. TargetSdkVersion 并且申请了 READ_PHONE_STATE,通过 getDeviceId 接口读取的值为 Null;
  3. 无法获取到 device id,会对应用依赖 device id 的功能产生影响。

      对于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_FOUNDACCESS_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

 

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值