目录
10.2电话、WiFi、蓝牙API所需的精确位置权限... 38
本文档是基于谷歌安卓Q的beta4版本的变更输出的兼容性整改指导,如果后续beta版本有新的变更和新的特性,我们也会刷新文档的相关章节内容,请开发者持续关注。
为了让用户更好地控制自己的文件,并限制文件混乱的情况,Android Q修改了APP访问外部存储中文件的方法。外部存储的新特性被称为Scoped Storage。
Android Q仍然使用READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE作为面向用户的存储相关运行时权限,但现在即使获取了这些权限,访问外部存储也受到了限制。APP需要这些运行时权限的情景发生了变化,且各种情况下外部存储对APP的可见性也发生了变化。
在Scoped Storage新特性中,外部存储空间被分为两部分:
- 公共目录:Downloads、Documents、Pictures 、DCIM、Movies、Music、Ringtones等
- 公共目录下的文件在APP卸载后,不会删除。
- APP可以通过SAF(System Access Framework)、MediaStore接口访问其中的文件。
- App-specific目录
- APP卸载后,数据会清除。
- APP的私密目录,APP访问自己的App-specific目录时无需任何权限。
Android Q规定了APP有两种外部存储空间视图模式:Legacy View、Filtered View。
- Filtered View
- App可以直接访问App-specific目录,但不能直接访问App-specific外的文件。访问公共目录或其他APP的App-specific目录,只能通过MediaStore、SAF、或者其他APP提供的ContentProvider、FileProvider等访问。
- Legacy View
- 兼容模式。与Android Q以前一样,申请权限后App可访问外部存储,拥有完整的访问权限。
在Android Q上,target SDK大于或等于29的APP默认被赋予Filtered View,反之则默认被赋予Legacy View。APP可以在AndroidManifest.xml中设置新属性requestLegacyExternalStorage来修改外部存储空间视图模式,true为Legacy View,false为Filtered View。可以使用Environment.isExternalStorageLegacy()这个API来检查APP的运行模式。APP开启Filtered View后,Scoped Storage新特性对APP生效。
Android Q除了划分外部存储和定义Filtered View,还在查询、读写文件的一些细节上做了改进或限制,例如图片文件中的地理位置信息将不再默认提供、查询MediaProvider获得的DATA字段不再可靠、新增了文件的Pending状态等等。这些细节的具体内容请参考适配方案章节。
Scoped Storage对于APP访问外部存储方式、APP数据存放以及APP间数据共享,都产生很大影响。请开发者注意以下的兼容性影响事项。
2.2.1无法新建文件
问题原因:直接使用自身App-specific目录以外的路径新建文件。
问题分析:在Android Q上,APP只允许在自身App-specific目录以内通过路径生成的文件。
解决方案:APP自身App-specific目录下新建文件的方法与文件路径,请参见2.3.1;如果要在公共目录下新建文件,使用MediaStore接口,请参见2.3.2;如果要在任意目录下新建文件,需要使用SAF,请参见2.3.3。
2.2.2无法访问存储设备上的文件
问题原因1:直接使用路径访问公共目录文件。
问题分析1:在Android Q上,APP默认只能访问外部存储设备上的App-specific目录。
解决方法1:参见2.3.2和2.3.3,使用MediaStore接口访问公共目录中的多媒体文件,或者使用SAF访问公共目录中的任意文件。注意:从MediaStore接口中查询到的DATA字段将在Android Q开始废弃,不应该利用它来访问文件或者判断文件是否存在;从MediaStore接口或者SAF获取到文件Uri后,请利用Uri打开FD或者输入输出流,而不要转换成文件路径去访问。
问题原因2:使用MediaStore接口访问非多媒体文件。
问题分析2:在Android Q上,使用MediaStore接口只能访问公共目录中的多媒体文件。
解决方法2:使用SAF向用户申请文件或目录的读写权限,请参见2.3.3。
2.2.3无法正确分享文件
问题原因:APP将App-specific目录中的私有文件分享给其他APP时,使用了file://类型的Uri。
问题分析:在Android Q上,由于App-specific目录中的文件是私有受保护的,其他APP无法通过文件路径访问。
解决方案:参见2.3.4,使用FileProvider,将content://类型的Uri分享给其他APP。
2.2.4无法修改存储设备上的文件
问题原因1:直接使用路径访问公共目录文件。
问题分析1:同2.2.2。
解决方案1:同2.2.2,请使用正确的公共目录文件访问方式。
问题原因2:使用MediaStore接口获取公共目录多媒体文件的Uri后,直接使用该Uri打开OutputStream或文件描述符。
问题分析2:在Android Q上,修改公共目录文件,需要用户授权。
解决方案2:从MediaStore接口获取公共目录多媒体文件Uri后,打开OutputStream或FD时,注意catch RecoverableSecurityException,然后向用户申请该多媒体文件的删改权限,请参见2.3.2.6;使用SAF获取到文件或目录的Uri时,用户已经授权读写,可以直接使用,但要注意Uri权限的时效,请参见2.3.3.6。
2.2.5应用卸载后文件意外删除
问题原因:将想要保留的文件保存在外部存储的App-specific目录下。
问题分析:在Android Q上,卸载APP默认删除App-specific目录下的数据。
解决方案:APP应该将想要保留的文件通过MediaStore接口保存到公共目录下,请参见2.3.2。默认情况下,MediaStore接口会将非媒体类文件保存到Downloads目录下,推荐APP指定一级目录为Documents。如果APP想要在卸载时保留App-specific目录下的数据,要在AndroidManifest.xml中声明android:hasFragileUserData="true",这样在APP卸载时就会有弹出框提示用户是否保留应用数据。
2.2.6无法访问图片文件中的地理位置数据
问题原因:直接从图片文件输入流中解析地理位置数据。
问题分析:由于图片的地理位置信息涉及用户隐私,Android Q上默认不向APP提供该数据。
解决方案:申请ACCESS_MEDIA_LOCATION权限,并使用MediaStore.setRequireOriginal()接口更新文件Uri,请参见2.3.5.1。
2.2.7 Fota升级问题
问题原因:Fota升级后,APP被卸载,重新安装后无法访问到APP数据。
问题分析:Scoped Storage新特性只对Android Q上新安装的APP生效。设备从Android Q之前的版本升级到Android Q,已安装的APP获得Legacy View视图。这些APP如果直接通过路径的方式将文件保存到了外部存储上,例如外部存储的根目录,那么APP被卸载后重新安装,新的APP获得Filtered View视图,无法直接通过路径访问到旧数据,导致数据丢失。
解决方案:APP应该修改保存文件的方式,不再使用路径的方式直接保存,而是采用MediaStore接口将文件保存到对应的公共目录下。在Fota升级前,可以将APP的用户历史数据通过MediaStore接口迁移到公共目录下。此外,APP应当改变访问App-specific目录以外的文件的方式,请使用MediaStore接口或者SAF。
2.3 适配指导
Android Q Scoped Storage新特性谷歌官方适配文档: https://developer.android.google.cn/preview/privacy/scoped-storage
OPPO适配指导如下,分为:访问APP自身App-specific目录文件、使用MediaStore访问公共目录、使用SAF访问指定文件和目录、分享App-specific目录下文件和其他细节适配。
2.3.1 访问APP自身App-specific目录文件
无需任何权限,APP即可直接使用文件路径来读写自身App-specific目录下的文件。获取App-specific目录路径的接口如下表所示。
App-specific目录 | 接口(所有存储设备) | 接口(Primary External Storage) |
Media | getExternalMediaDirs() | NA |
Obb | getObbDirs() | getObbDir() |
Cache | getExternalCacheDirs() | getExternalCacheDir() |
Data | getExternalFilesDirs(String type) | getExternalFilesDir(String type) |
如下,以新建并写入文件为例。
// set "Documents" as subDir
final File[] dirs = getExternalFilesDirs("Documents");
File primaryDir = null;
if (dirs != null && dirs.length > 0) {
primaryDir = dirs[0];
}
if (primaryDir == null) {
return;
}
File newFile = new File(primaryDir.getAbsolutePath(), "MyTestDocument");
OutputStream fileOS = null;
try {
fileOS = new FileOutputStream(newFile);
if (fileOS != null) {
fileOS.write("file is created".getBytes(StandardCharsets.UTF_8));
fileOS.flush();
}
} catch (IOException e) {
LogUtil.log("create file fail");
} finally {
try {
if (fileOS != null) {
fileOS.close();
}
} catch (IOException e1) {
LogUtil.log("close stream fail");
}
}
2.3.2 使用MediaStore访问公共目录
APP无法直接访问公共目录下的文件。MediaStore为APP提供了访问公共目录下媒体文件的接口。APP在有适当权限时,可以通过MediaStore查询到公共目录文件的Uri,然后通过Uri读写文件。
MediaStore相关的Google官方文档:
https://developer.android.google.cn/reference/android/provider/MediaStore
2.3.2.1 MediaStore的Uri和路径对照表
MediaStore提供了下列几种类型的访问Uri,通过查询对应Uri数据(在MediaProvider中),达到访问的目的。
下列每种类型又分为三种Uri:Internal、External、可移动存储。
文件类型 | 存储设备 | Uri |
Audio | Internal | MediaStore.Audio.Media.INTERNAL_CONTENT_URI content://media/internal/audio/media |
External | MediaStore.Audio.Media.EXTERNAL_CONTENT_URI content://media/external/audio/media | |
可移动存储 | MediaStore.Audio.Media.getContentUri(volumeName) content://media/<volumeName>/audio/media | |
Video | Internal | MediaStore.Video.Media.INTERNAL_CONTENT_URI content://media/internal/video/media |
External | MediaStore.Video.Media.EXTERNAL_CONTENT_URI content://media/external/video/media | |
可移动存储 | MediaStore.Video.Media.getContentUri(volumeName) content://media/<volumeName>/video/media | |
Image | Internal | MediaStore.Images.Media.INTERNAL_CONTENT_URI content://media/internal/images/media |
External | MediaStore.Images.Media.EXTERNAL_CONTENT_URI content://media/external/images/media | |
可移动存储 | MediaStore.Images.Media.getContentUri(volumeName) content://media/<volumeName>/images/media | |
Downloads | Internal | MediaStore.Downloads.INTERNAL_CONTENT_URI content://media/internal/downloads |
External | MediaStore.Downloads.EXTERNAL_CONTENT_URI content://media/external/downloads | |
可移动存储 | MediaStore.Downloads.getContentUri(volumeName) | |
File | NA | MediaStore. Files.Media.getContentUri(volumeName) content://media/<volumeName>/file |
在Android Q上,所有的外部存储设备,包括内置卡、SD卡等,都会被命名,即设备的Volume Name。MediaStore可以通过Volume Name获取对应存储设备的Uri。
for (String volumeName : MediaStore.getExternalVolumeNames(this)) {
MediaStore.Images.Media.getContentUri(volumeName);
}
MediaProvider对于APP新建到公共目录的文件,通过ContentResolver.insert方法中的Uri来确定具体存放目录。其中下表中<Uri路径>为相对路径,完整为:
content://media/<volumeName>/<Uri路径>
Mine Type | Uri路径 | 可选一级目录(*标记为默认一级目录) |
audio/* | images/media images/media/# | Environment.DIRECTORY_ALARMS Environment.DIRECTORY_MUSIC(*) Environment.DIRECTORY_NOTIFICATIONS Environment.DIRECTORY_PODCASTS Environment.DIRECTORY_RINGTONES |
image/* | audio/albumart audio/albumart/# | Environment.DIRECTORY_MUSIC(*) |
NA | audio/playlists audio/playlists/# | Environment.DIRECTORY_MUSIC(*) |
video/* | video/media video/media/# | Environment.DIRECTORY_DCIM Environment.DIRECTORY_MOVIES(*) |
image/* | images/media images/media/# | Environment.DIRECTORY_DCIM Environment.DIRECTORY_PICTURES(*) |
image/* | video/thumbnails video/thumbnails/# | Environment.DIRECTORY_MOVIES(*) |
image/* | images/thumbnails images/thumbnails/# | Environment.DIRECTORY_PICTURES(*) |
NA | downloads downloads/# | Environment.DIRECTORY_DOWNLOADS(*) |
NA | file file/# | Environment.DIRECTORY_DOWNLOADS(*) Environment.DIRECTORY_DOCUMENTS |
2.3.2.2 APP通过MediaStore访问文件所需要的权限
| 无权限 | READ_EXTERNAL |
Audio |
可读写APP自己创建的文件,但不可直接使用路径访问 | 可以读其他APP创建的媒体类文件,删改操作需要用户授权 |
Image | ||
Audio | ||
File | 不可读写其他APP创建的非媒体类文件 | |
Downloads |
2.3.2.3 使用MediaStore新建文件
通过MediaStore提供的Uri,使用ContentResolver的insert接口,将文件保存到公共目录下。不同的Uri,可以保存到不同的公共目录中,详见2.3.2.1。
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DESCRIPTION, "This is an image");
values.put(MediaStore.Images.Media.DISPLAY_NAME, "Image.png");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
values.put(MediaStore.Images.Media.TITLE, "Image.png");
values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/test");
Uri external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
ContentResolver resolver = context.getContentResolver();
Uri insertUri = resolver.insert(external, values);
LogUtil.log("insertUri: " + insertUri);
OutputStream os = null;
try {
if (insertUri != null) {
os = resolver.openOutputStream(insertUri);
}
if (os != null) {
final Bitmap bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888);
bitmap.compress(Bitmap.CompressFormat.PNG, 90, os);
// write what you want
}
} catch (IOException e) {
LogUtil.log("fail: " + e.getCause());
} finally {
try {
if (os != null) {
os.close();
}
} catch (IOException e) {
LogUtil.log("fail in close: " + e.getCause());
}
}
2.3.2.4使用MediaStore查询文件
用MediaStore提供的Uri指定设备,selection参数指定过滤条件,通过ContentResolver.query接口查询文件Uri。
Uri external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
ContentResolver resolver = context.getContentResolver();
String selection = MediaStore.Images.Media.TITLE + "=?";
String[] args = new String[] {"Image"};
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();
}
2.3.2.5 使用MediaStore读取文件
通过以上查询方式得到Uri之后,通过以下方式读取文件:
1)通过ContentResolver openFileDescriptor接口,选择对应的打开方式。例如”r”表示读,”w”表示写,返回ParcelFileDescriptor类型的文件描述符。
ParcelFileDescriptor pfd = null;
if (imageUri != null) {
try {
pfd = context.getContentResolver().openFileDescriptor(imageUri, "r");
if (pfd != null) {
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor());
// show the bitmap, or do something else.
}
} catch (IOException e) {
LogUtil.log("fail: " + e.getCause());
} finally {
try {
if (pfd != null) {
pfd.close();
}
} catch (IOException e) {
LogUtil.log("fail in close: " + e.getCause());
}
}
}
2)访问Thumbnail,使用ContentResolver.loadThumbnail接口。通过传入size参数,MediaProvider返回指定大小的Thumbnail。
3)Native代码访问文件
如果Native代码需要访问文件,可以参考下面方式:
- 通过openFileDescriptor返回ParcelFileDescriptor
- 通过ParcelFileDescriptor.detachFd()读取FD
- 将FD传递给Native层代码
- 通过close接口关闭FD
String fileOpenMode = "r";
ParcelFileDescriptor parcelFd = resolver.openFileDescriptor(uri, fileOpenMode);
if (parcelFd != null) {
int fd = parcelFd.detachFd();
// Pass the integer value "fd" into your native code. Remember to call
// close(2) on the file descriptor when you're done using it.
}
2.3.2.6使用MediaStore修改文件
根据查询得到的文件Uri,使用MediaStore修改其他APP新建的多媒体文件,需要catch RecoverableSecurityException,由MediaProvider弹出弹框给用户选择是否允许APP修改或删除图片/视频/音频文件。用户操作的结果,将通过onActivityResult回调返回到APP。如果用户允许,APP将获得该Uri的修改权限,直到设备下一次重启。
根据文件Uri,通过下列接口,获取需要修改文件的FD或者OutputStream:
1)getContentResolver().openOutputStream(contentUri)
获取对应文件的OutputStream。
2)getContentResolver().openFile或者getContentResolver().openFileDescriptor
通过openFile或者openFileDescriptor打开文件,需要选择Mode为”w”,表示写权限。 这些接口返回一个ParcelFileDescriptor。
OutputStream os = null;
try {
if (imageUri != null) {
os = resolver.openOutputStream(imageUri);
}
} catch (IOException e) {
LogUtil.log("open image fail");
} catch (RecoverableSecurityException e1) {
LogUtil.log("get RecoverableSecurityException");
try {
((Activity) context).startIntentSenderForResult(
e1.getUserAction().getActionIntent().getIntentSender(),
100, null, 0, 0, 0);
} catch (IntentSender.SendIntentException e2) {
LogUtil.log("startIntentSender fail");
}
}
2.3.2.7使用MediaStore删除文件
删除其他APP新建的媒体文件,与修改类似,需要用户授权。删除文件使用ContentResolver.delete接口。
getContentResolver().delete(imageUri, null, null);
2.3.3使用SAF访问指定文件和目录
SAF,即Storage Access Framework。根据当前系统中存在的DocumentsProvider,让用户选择特定的文件或文件夹,使调用SAF的APP获取它们的读写权限。APP通过SAF获得文件或目录的读写权限,无需申请任何存储相关的运行时权限。
SAF相关的Google官方文档:
https://developer.android.com/guide/topics/providers/document-provider
使用SAF获取文件或目录权限的过程:
APP通过特定Intent调起DocumentUI -> 用户在DocumentUI界面上选择要授权的文件或目录 -> APP在回调中解析文件或目录的Uri,最后根据这一Uri可进行读写删操作。
2.3.3.1使用SAF选择单个文件
使用Intent.ACTION_OPEN_DOCUMENT调起DocumentUI的文件选择页面,用户可以选择一个文件,将它的读写权限授予APP。
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
// you can set type to filter files to show
intent.setType("*/*");
startActivityForResult(intent, REQUEST_CODE_FOR_SINGLE_FILE);
2.3.3.2使用SAF修改文件
通过2.3.3.1的方式,用户选择文件授权给APP后,在APP的onActivityResult回调中收到返回结果,解析出对应文件的Uri。然后使用该Uri,用户可以获取可写的ParcelFileDescriptor或者打开OutputStream进行修改。
if (requestCode == REQUEST_CODE_FOR_SINGLE_FILE && resultCode == Activity.RESULT_OK) {
Uri fileUri = null;
if (data != null) {
fileUri = data.getData();
}
if (fileUri != null) {
OutputStream os = null;
try {
os = getContentResolver().openOutputStream(fileUri);
os.write("something".getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
LogUtil.log("modify document fail");
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e1) {
LogUtil.log("close fail");
}
}
}
}
}
2.3.3.3使用SAF删除文件
类似修改文件,在回调中解析出文件Uri,然后使用DocumentsContract.deleteDocument接口进行删除操作。
if (requestCode == REQUEST_CODE_FOR_SINGLE_FILE && resultCode == Activity.RESULT_OK) {
Uri fileUri = null;
if (data != null) {
fileUri = data.getData();
}
if (fileUri != null) {
try {
DocumentsContract.deleteDocument(getContentResolver(), fileUri);
} catch (FileNotFoundException e) {
LogUtil.log("delete document fail");
}
}
}
2.3.3.4使用SAF新建文件
APP通过Intent.ACTION_CREATE_DOCUMENT调起DocumentUI界面,由用户决定文件命名,以及存放位置。
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
// you can set file mimetype
intent.setType("*/*");
// default file name
intent.putExtra(Intent.EXTRA_TITLE, "myFileName");
startActivityForResult(intent, REQUEST_CODE_FOR_CREATE_FILE);
在用户确定后,操作结果将返回到APP的onActivityResult回调中,APP解析出文件Uri,之后就可以利用这一Uri对文件进行读写删操作。
if (requestCode == REQUEST_CODE_FOR_CREATE_FILE && resultCode == Activity.RESULT_OK) {
Uri fileUri = null;
if (data != null) {
fileUri = data.getData();
}
// read/update/delete by the uri got here.
LogUtil.log("uri: " + fileUri);
}
2.3.3.5使用SAF选择目录
通过Intent.ACTION_OPEN_DOCUMENT_TREE调起DocumentUI界面,用户可以选择任意文件夹,将它及其子文件夹的读写权限授予APP。
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_CODE_FOR_DIR);
在右上角的菜单中选择show internal storage,可以在左侧菜单中选择内置存储设备,接着用户可以选择内置存储设备中的任意文件夹。
在用户确定后,APP的onActivityResult回调收到操作结果,解析出被选文件夹的uriTree。根据这一uriTree,进一步可以生成表示被选文件夹的DocumentFile,利用DocumentFile提供的API可以对目录下的文件进行各种操作。
if (requestCode == REQUEST_CODE_FOR_DIR && resultCode == Activity.RESULT_OK) {
Uri uriTree = null;
if (data != null) {
uriTree = data.getData();
}
if (uriTree != null) {
// create DocumentFile which represents the selected directory
DocumentFile root = DocumentFile.fromTreeUri(this, uriTree);
// list all sub dirs of root
DocumentFile[] files = root.listFiles();
// do anything you want with APIs provided by DocumentFile
// ...
}
}
2.3.3.6永久保存获取的目录权限
在2.3.3.5中,通过SAF获取了用户指定目录的读写权限,直至设备下一次重启。APP可以通过takePersistableUriPermission接口获取该uriTree的永久权限,并将uriTree以SharedPreferences等形式持久化保存,以备之后随时使用。
if (uriTree != null) {
// get persistable uri permission
final int takeFlags = data.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getContentResolver().takePersistableUriPermission(uriTree, takeFlags);
// save uriTree to sharedPreference
SharedPreferences sp = getSharedPreferences("DirPermission", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString("uriTree", uriTree.toString());
editor.commit();
}
在使用保存的uriTree时,首先检查是否顺利从SharedPreferences中获取到uriTree,然后通过takePersistableUriPermission接口是否抛异常来判断权限是否仍存在。如果权限不存在,则重新通过SAF申请权限。
SharedPreferences sp = getSharedPreferences("DirPermission", Context.MODE_PRIVATE);
String uriTree = sp.getString("uriTree", "");
if (TextUtils.isEmpty(uriTree)) {
startSafForDirPermission();
} else {
try {
Uri uri = Uri.parse(uriTree);
final int takeFlags = getIntent().getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getContentResolver().takePersistableUriPermission(uri, takeFlags);
// uri tree permission is granted, do what you want with this uri
LogUtil.log("uri is granted");
DocumentFile root = DocumentFile.fromTreeUri(this, uri);
} catch (SecurityException e) {
LogUtil.log("uri is not granted");
startSafForDirPermission();
}
}
APP申请到目录的永久权限后,用户可以在该APP的设置页面取消目录的访问权限,即点击如下图的“Clear access”按钮。
2.3.4分享App-specific目录下文件
APP可以选择以下的方式,将自身App-specific目录下的文件分享给其他APP读写。
2.3.4.1使用FileProvider
APP可以使用FileProvider将私有文件的读写权限赋给其他APP。这种方式十分适用于APP主动发起事件的情况,例如从APP将某个私有文件分享给其他APP。
FileProvider相关的Google官方文档:
https://developer.android.google.cn/reference/androidx/core/content/FileProvider
https://developer.android.com/training/secure-file-sharing/setup-sharing
自定义FileProvider及使用的基本步骤:
- 在AndroidManifest.xml中声明App的FileProvider
<provider
android:authorities="com.oppo.whoops.fileprovider"
android:name="androidx.core.content.FileProvider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths"/>
</provider>
2)根据FileProvider声明中的meta data,在res/xml中新建filepaths.xml,用于定义分享的路径。
<?xml version="1.0" encoding="utf-8"?>
<paths>
<!-- path represents subdir of external-files-path (Context.getExternalFilesDir(null))
name represents what other apps see in the shared uri as subdir. -->
<external-files-path name="myimages" path="Documents"/>
<!-- you can also define some subdirs of other roots, such as external-media-path -->
</paths>
- 在APP逻辑代码中生成要分享的uri,设置权限,然后发送uri。
String filePath = getExternalFilesDir("Documents") + "/MyTestImage.PNG";
Uri uri = FileProvider.getUriForFile(this, "com.oppo.whoops.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"));
- 接收方APP的组件设置对应的intent-filter。
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*"/>
</intent-filter>
- 接收方APP的组件收到intent,解析获得uri,通过uri获取文件的FD。
Uri 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");
}
}
2.3.4.2使用ContentProvider
APP可以实现自定义ContentProvider来向外提供APP私有文件。这种方式十分适用于内部文件分享,不希望有UI交互的情况。
ContentProvider相关的Google官方文档:
https://developer.android.google.cn/guide/topics/providers/content-providers
2.3.4.3使用DocumentsProvider
Android默认提供的ExternalStorageProvider、DownloadStorageProivder和MediaDocumentsProvider会显示在SAF调起的DocumentUI界面中。ExternalStorageProvider展示了所有外部存储设备的所有目录及文件,包括App-specific目录,所以App-specific目录下的文件也可以通过SAF授权给其他APP。
APP也可以自定义DocumentsProvider来提供向外授权。自定义的DocumentsProivder将作为第三方DocumentsProvider展示在SAF调起的界面中。DocumentsProvider的使用方法请参考官方文档。
DocumentsProvider相关的Google官方文档:
https://developer.android.google.cn/reference/kotlin/android/provider/DocumentsProvider
2.3.5细节适配
2.3.5.1图片的地理位置信息
Android Q上,默认情况下APP不能获取图片的地理位置信息。如果APP需要访问图片上的Exif Metadata,需要完成以下步骤:
- 申请ACCESS_MEDIA_LOCATION权限。
- 通过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];
}
2.3.5.2 DATA字段数据不再可靠
MediaStore中,DATA(即_data)字段,在Android Q中开始废弃,不再表示文件的真实路径。读写文件或判断文件是否存在,不应该使用DATA字段,而要使用openFileDescriptor。
同时也无法直接使用路径访问公共目录的文件。
2.3.5.3 MediaStore.Files接口自过滤
通过MediaStore.Files接口访问文件时,只展示多媒体文件(图片、视频、音频)。其他文件,例如PDF文件,无法访问到。
2.3.5.4文件的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);
2.3.5.5使用MediaStore接口定义好的Columns
在使用MediaStore接口时,如果用到Projection,Column Name要使用在MediaStore中定义好的。如果APP引用的库使用了其他Column Name,需要由APP做好Column Name映射。
2.3.5.6设置相对路径
Android Q上,通过MediaStore存储到公共目录的文件,除了Uri跟公共目录关系中规定的每一个存储空间的一级目录外,可以通过MediaColumns.RELATIVE_PATH来指定存储的次级目录,这个目录可以使多级,具体方法如下:
1)ContentResolver insert方法
通过values.put(Media.RELATIVE_PATH, "Pictures/album/family")指定存储目录。其中,Pictures是一级目录,album/family是子目录。
2)ContentResolver update方法
通过values.put(Media.RELATIVE_PATH, "Pictures/album/family")指定存储目录。通过update方法,可以移动文件的存储地方。
2.3.5.7卸载应用
如果APP在AndroidManifest.xml中声明:android:hasFragileUserData="true",卸载应用会有提示是否保留APP数据。默认应用卸载时App-specific目录下的数据被删除,但用户可以选择保留。
2.3.5.8新建虚拟可移动存储
APP适配时,如果一个设备没有可移动存储,可以使用下面的方法新建虚拟存储设备:
1)adb shell sm set-virtual-disk true
2)在设置 -> 存储 -> Virtual SD,进行初始化
Android Q对设备标识(Device Identifier)做了访问限制。App必须拥有系统签名权限:READ_PRIVILEGED_PHONE_STATE,才能访Device ID,包括IMEI、Serial Number,这意味着第三方应用无法获取Device ID。
(1)TargetSdkVersion<Q并且没有申请READ_PHONE_STATE权限,或者TargetSdkVersion>=Q,获取device id会抛异常SecurityException;
(2)TargetSdkVersion<Q并且申请了READ_PHONE_STATE,通过getDeviceId接口读取的值为Null;
(3)无法获取到device id,会对应用依赖device id的功能产生影响。
3.3 适配指导
a) 谷歌提供的适配指导文档:
唯一标识符最佳做法:https://developer.android.google.cn/training/articles/user-data-ids
官方文档:https://developer.android.google.cn/preview/privacy/data-identifiers
b) 避免使用硬件ID
c) 使用Advertising, ID表示用户资料或者广告用途
需要依赖于GMS包里面的AdvertisingIdClient
d) 使用Google FirebaseInstanceId,但是会在下列情况下不同:
App删除Instance ID
App在新设备恢复
用户卸载或者重新安装App
用户清除数据
e) 使用IDs跟GUIDs
String uniqueID = UUID.randomUUID().toString();
f) 使用Android ID,恢复出厂,这个值会改变
Settings.System.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
g) 接入OPPO OPEN_ID
目前方案正在开发中,开发完成后再补充适配信息
为了进一步保护用户的隐私,Android Q在连接Wi-Fi时,默认启用了Mac地址随机化的特性,如果APP不进行适配,使用原来方式获取到的Mac地址可能是随机生成的,并不是真实的Mac地址。
如果您的APP需要使用Mac地址作为设备的标识,无论您的Target SDK是否设置为Q,只要运行在Android Q上,您就需要进行适配。
4.3 适配指导
请参考谷歌适配指导https://developer.android.com/preview/privacy/data-identifiers#randomized-mac-addresses
安卓Q版本限制了应用后台启动Activity,该变更的目的是最大限度减少后台应用弹界面对用户的打扰,在Android Q上运行的应用只有在满足以下一个或多个条件时才能启动Activity
1. 应用处于前台;
2. 桌面widget点击启动Activity;
3. 由桌面点击启动应用;
4. 由Recent点击启动应用;
5. 前台应用启动后台应用;
6. 临时白名单机制,不拦截通过通知拉起的应用。
(a)应用通过通知,在pendingintent中启动activity;
(b)应用通过通知,在pendingintent中发送广播,接收广播后启动activity,加入临时白名单不拦截。
(c)应用通过通知,在pendingintent中启动service,在service中启动activity,加入临时白名单不拦截。
5.2 兼容性影响
影响所有应用的后台启动Activity,需全面排查及整改。
5.3 适配指导
请参考谷歌适配指导:https://developer.android.com/preview/privacy/background-activity-starts
谷歌在Q的beta版本并未真正打开该管控限制,但是如果应用的页面存在被管控的场景,系统会通过一个Toast告警提示,提示开发者需要整改,否则应用的某些页面在谷歌的后续版本会被拦截,具体的告警文字内容:
This background activity start from "packageName" will be blocked in future Q builds. See g.co/dev/bgblock.
App需要测试这个特性,需要在打开这个限制,使用下面任一步骤开启即可:
- Settings > Developer options > Allow background activity starts设置为关闭
- adb shell settings put global background_activity_starts_enabled 0
Android Q针对位置信息新增了ACCESS_BACKGROUND_LOCATION权限,以管控应用是否可以在后台访问位置信息。原有的ACCESS_COARSE_LOCATION和ACCESS_FINE_LOCATION权限用于管控应用在前台是否可以获取位置信息。
地图类应用在后台获取位置信息时将受到影响。
6.3 适配指导
Google官方适配指导链接:
https://developer.android.com/preview/privacy/device-location
- Target Sdk Version兼容
当应用的Target Sdk Version < Q
- 请求ACCESS_COARSE_LOCATION或者ACCESS_FINE_LOCATION权限,系统会自动同时请求Q新增的ACCESS_BACKGROUND_LOCATION(图 6-3-1)。
当应用的Target Sdk Version >= Q
- 只请求ACCESS_COARSE_LOCATION权限或ACCESS_FINE_LOCATION权限,系统将不会提供“始终允许”的选择按钮,应用只能在使用时获取位置信息(图6-3-2)。
Google建议:如果应用不需要在后台获取位置信息,不要请求 ACCESS_BACKGROUND_LOCATION权限。
- 请求ACCESS_COARSE_LOCATION权限或ACCESS_FINE_LOCATION权限,并同时请求ACCESS_BACKGROUND_LOCATION权限,则系统提供“仅在使用该应用期间允许”选择按钮(图6-3-3)。
- Android Q不允许在没有请求ACCESS_COARSE_LOCATION或ACCESS_FINE_LOCATION权限(或者ACCESS_COARSE_LOCATION或ACCESS_FINE_LOCATION没有被授权)的情况下单独请求ACCESS_BACKGROUND_LOCATION权限。
- 在ACCESS_COARSE_LOCATION或ACCESS_FINE_LOCATION已经授权的情况下,请求ACCESS_BACKGROUND_LOCATION权限,则弹出如图6-3-4的权限说明框。
图 6-3-2 图 6-3-3 图 6-3-4
- 仅在使用该应用时允许
在Q上,选择“仅在使用该应用时允许”,应用只有在可见或者使用前台服务情况下才能获取到位置信息。
使用前台服务,应用需要参考以下步骤:
a. 在应用的Manifest中的对应service中添加值为location的foregroundServiceType:
<service
android:name="MyNavigationService"
android:foregroundServiceType="location" ... >
...
</service>
b. 启动前台服务前检查是否具有获取位置信息的权限:
boolean permissionAccessCoarseLocationApproved =
ActivityCompat.checkSelfPermission(this,
permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED;
if (permissionAccessCoarseLocationApproved) {
// App has permission to access location in the foreground. Start your
// foreground service that has a foreground service type of "location".
} else {
// Make a request for foreground-only location access.
ActivityCompat.requestPermissions(this, new String[] {
Manifest.permission.ACCESS_COARSE_LOCATION},
your-permission-request-code);
}
- 始终允许
在某些场景下,比如共享位置信息,应用需要始终获取位置信息。
当用户选择了“始终允许”,应用无论在前台还是后台都可以获取到位置信息。但需要注意的是,用户选择“始终允许”后可以手动撤销掉后台访问的权限!(当用户选择“始终允许”后,系统会周期在通知栏提示用户选择了“始终允许”,点击通知会进入位置权限详情界面,用户可以重新选择位置权限的授权)。因此,应用在进入后台获取位置信息前需要判断当前是否依然具有权限,参考以下步骤:
- 定义后台服务,无需添加location的type
<!-- It's unnecessary to include a foreground service type for services that must have
access to the device's location "all the time" in order to run successfully.-->
<service
android:name="MyFamilyLocationSharingService" ... >
...
</service>
b. 进入后台前判断是否具有后台访问位置信息的权限
boolean permissionAccessCoarseLocationApproved =
ActivityCompat.checkSelfPermission(this, permission.ACCESS_COARSE_LOCATION)
== PackageManager.PERMISSION_GRANTED;
if (permissionAccessCoarseLocationApproved) {
boolean backgroundLocationPermissionApproved =
ActivityCompat.checkSelfPermission(this,
permission.ACCESS_BACKGROUND_LOCATION)
== PackageManager.PERMISSION_GRANTED;
if (backgroundLocationPermissionApproved) {
// App can access location both in the foreground and in the background.
// Start your service that doesn't have a foreground service type
// defined.
} else {
// App can only access location in the foreground. Display a dialog
// warning the user that your app must have all-the-time access to
// location in order to function properly. Then, request background
// location.
ActivityCompat.requestPermissions(this, new String[] {
Manifest.permission.ACCESS_BACKGROUND_LOCATION},
your-permission-request-code);
}
} else {
// App doesn't have access to the device's location at all. Make full request
// for permission.
ActivityCompat.requestPermissions(this, new String[] {
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
},
your-permission-request-code);
}
如果应用进入后台前,发现后台访问位置信息的权限被撤销即backgroundLocationPermissionApproved为false,需要重新申请ACCESS_BACKGROUND_LOCATION权限,这时会弹出如图6-3-4的权限说明框,提示用户重新选择。
Google认为非公开接口可能在不同版本之间进行变动从而导致应用兼容性问题,因此从Android P开始强制约定三方应用只能使用Android SDK公开的类和接口;对于非公开的API,Google按照不同名单类型进行不同程度的限制使用。
- 从原生的android.jar中能够看到的就是SDK接口,也可以从Google的开发者网站https://developer.android.com/reference/packages进行查询。
- 非公开API的分类和限制
名单类型 | 限制 |
greylist | 应用targetSDK>=P时,警告 |
greylist-max-o | targetSDK<=O时,警告;>O时,不允许调用 |
greylist-max-p | targetSDK<=P时,警告;>P时,不允许调用 |
blacklist | 三方应用不允许调用 |
所有三方应用都可能会受到影响,Android Q版本由于名单生成规则变化了,导致增加很多黑名单接口;同时有很多非SDK接口被删除,这些都会导致应用出现兼容性问题。
由于非公开接口的管控是在运行时进行管控的,因此用使用反射、JNI调用和正常的空实现封装这些深灰和黑名单的接口都不会绕过Google的限制,应用会出现异常。
具体异常信息如下:
Google的官方文档中的适配指导。
https://developer.android.com/distribute/best-practices/develop/restrictions-non-sdk-interfaces
https://android-developers.googleblog.com/2018/06/an-update-on-non-sdk-restrictions-in.html
- Google原生最新的名单见下面的链接
https://android.googlesource.com/platform/prebuilts/runtime/+/refs/heads/master/appcompat/
hiddenapi-flags.csv文件包含了所有的类和接口,第二列显示的就是名单类型。
三方应用要重点关注max-o、max-p和blacklist接口(private类型的接口会是blacklist)。这些接口不可用。
- 静态扫描APK文件,获得应用使用非公开接口的情况。
使用下面链接的veridex工具,建议在linux环境下使用
https://android.googlesource.com/platform/prebuilts/runtime/+/refs/heads/master/appcompat/
README.txt给出了详细的使用步骤
备注:该方法不能检测出使用反射方式调用黑名单接口的情况。
- 通过运行应用,获得应用使用非公开接口的情况。
通过执行adb shell settings put global hidden_api_policy 1
将系统的Hidden API强制策略禁用掉,这样应用就可以在AndroidQ设备上运行。查看日志是否有如下关键日志信息。
log中会有关键字打印:
Accessing hidden field Landroid/content/pm/PackageParser;->mParseError:I (blacklist, reflection)
应用行为异常有报错,有类似如下的报错:
java.lang.NoSuchMethodError: No virtual method getActivityIconCache(Landroid/content/ComponentName;)Landroid/graphics/drawable/Drawable; in class Landroid/content/pm/PackageManager; or its super classes (declaration of 'android.content.pm.PackageManager' appears in /system/framework/framework.jar)
当没有发现Accessing hidden日志信息时,需要确认调用的类或者方法是否由于Android版本升级有改变了。
建议开发者采用上述方法进行应用的非公开SDK接口的检查。
- 总的适配原则是针对max-o、max-p和blacklist接口,尝试在公开的SDK中找替代方案。 例如android.util.FloatMath的sqrt方法,可用java.lang.Math类return (float) Math.sqrt(value);方式来替换。
- 若应用无法找到可替代的SDK接口,但是又要使用这个非SDK接口,建议开发者直接给谷歌反馈,申请新的公共API,申请链接:https://partnerissuetracker.corp.google.com/issues/new?component=328403&template=1027267
1. 增加对于上架谷歌Play商店应用要求:
a) 新开发的应用:2019-8-1之后,上架谷歌Play商店要求应用的TargetSdkVersion>=28;
b) 更新的应用:2019-11-1之后,上架谷歌Play商店要求应用的TargetSdkVersion>=28。
2. 最小TargetSdkVersion要求:当用户首次运行 API 低级低于 23 (Android Marshmallow) 的应用时,会受到来自 Android Q 的警告信息。
8.2 兼容性影响
应用升级TargetSdkVersion之后,和应用的TargetSdkVersion相关的变更就会影响,不适配很可能导致应用出现兼容性问题或者功能问题。
8.3 适配指导
谷歌适配指导链接:https://developer.android.google.cn/about/versions/pie/android-9.0-changes-28
重点关注以下特性变化:
- 非SDK接口管控,需要重点排查是否使用P版本的深灰名单接口和Q版本的max-o名单接口,请参考7章节进行适配
- 针对 Android 9 或更高版本并使用前台服务的应用必须请求 FOREGROUND_SERVICE 权限,否则会引发SecurityException,这是普通权限,因此,系统会自动为请求权限的应用授予此权限。
- 后台执行限制,请参考谷歌适配指导:https://developer.android.google.cn/about/versions/oreo/background.html
从2019年8月1日开始,在Google Play上发布的应用必须支持64位架构
应用需要自查是否具有so库(native代码)。如果没有则已支持64位架构。如果有则需要自检是否支持并采取对应措施,否则将无法正常运行。
9.3 适配指导
Google官方适配指导链接:
https://developer.android.google.cn/distribute/best-practices/develop/64-bit
- 查找应用中的so文件
a. 通过Analyze Apk查找,参考:
b. 通过解压apk文件查找,参考:
- 确认是否支持64位架构
a. 如果没有so文件,则已支持64位架构
b. 根据ABI,so文件对应不同的目录(图10-3-1),如果每个目录下都存在so文件,则支持64位架构:
- 构建64位架构的app
- 应用的so文件自己开发,参考:
- Game游戏开发者,需要使用支持64位的引擎:
Unreal since 2015
Cocos2d since 2015
Unity since 2018
- Unity开发者,参考:
https://developer.android.google.cn/distribute/best-practices/develop/64-bit#unity_developers
- 应用使用三方的sdk库文件,需通知三方开发者适配支持,然后应用重新集成
Android Q上获取USB序列号需要android.permission.MANAGE_USB权限
-
-
- 兼容性影响
-
三方应用无法获取USB序列号
11.1.3 适配指导
Google官方适配指导链接:
https://developer.android.com/preview/privacy/data-identifiers
由于android.permission.MANAGE_USB是signature|privileged级别的权限,三方应用无法获取该权限,因此无法获取USB序列号
-
- 电话、WiFi、蓝牙API所需的精确位置权限
11.2.1 背景
为了管理APP对一些关键API的调用,Android Q对电话、Wi-Fi以及蓝牙的相关API增加了ACCESS_FINE_LOCATION权限的限制。
11.2.2 兼容性影响
具体影响的各模块接口如下:
模块 | 类 | 接口 |
Telephony | TelephonyManager | getCellLocation() getAllCellInfo() requestNetworkScan() requestCellInfoUpdate() getAvailableNetworks() getServiceStateForSubscriber getServiceState() |
TelephonyScanManager | requestNetworkScan() | |
TelephonyScanManager.NetworkScanCallback | onResults() | |
onCellLocationChanged() onCellInfoChanged() onServiceStateChanged() | ||
Wi-Fi | startScan() getScanResults() getConnectionInfo() getConfiguredNetworks() | |
attach() publish() | ||
connect() createGroup() | ||
| ||
Bluetooth | BluetoothAdapter | startDiscovery() startLeScan() LeScanCallback() |
11.2.3 适配指导
参考Google官方适配指导链接:
https://developer.android.com/preview/privacy/camera-connectivity#fine-location-telephony-wifi-bt
如果应用的 targetSdkVersion < Q,那么运行在Android Q平台时,应用将不会受到影响,只需要申请ACCESS_COARSE_LOCATION或者ACCESS_FINE_LOCATION权限即可。
如果应用的 targetSdkVersion >= Q,那么必须申请ACCESS_FINE_LOCATION权限,否则当APP运行在Android Q平台时,将无法正常使用受影响的API。
11.3.1 背景
Android P非Instant应用可以任何时刻获取剪贴板内容。
Android Q上新增READ_CLIPBOARD_IN_BACKGROUND权限限制应用后台获取剪贴板内容。
11.3.2 兼容性影响
除非应用是默认输入法编辑器(IME)或具有焦点的应用程序,否则无法获取剪贴板内容
11.3.3 适配指导
Google官方适配指导链接:
https://developer.android.com/preview/privacy/data-identifiers
由于READ_CLIPBOARD_IN_BACKGROUND权限是签名signature级别的权限,因此三方应用无法通过授权该权限在后台获取剪贴板内容。应用只有在具有焦点时才能获取剪贴板内容。
-
- 访问相机信息所需权限
- 背景
- 访问相机信息所需权限
为了进一步保护用户的隐私,从Android Q开始,google更改了默认情况下getCameraCharacteristics() 方法返回的设备信息的粒度,部分属性将受到权限限制,这些属性可能用户运动跟踪等涉及用户隐私的领域。因此,需要特别注意的是,当您的应用需要尝试获取以下列出的包含设备特定信息的元数据时,您的应用必须具有android.permission.CAMERA权限才能获取此Key对应的返回值, 否则将返回null。
关于如何获取android.permission.CAMERA权限,请访问 (https://developer.android.google.cn/training/permissions/requesting)。
如果您的应用没有CAMERA权限,则无法访问以下字段:
镜头相关属性 (在物理摄像头信息流的像素之间建立关联所需的深度相关静态元数据,一般用于运动跟踪) | ANDROID_LENS_POSE_ROTATION |
|
ANDROID_LENS_POSE_TRANSLATION |
| |
ANDROID_LENS_INTRINSIC_CALIBRATION |
| |
ANDROID_LENS_RADIAL_DISTORTION |
| |
ANDROID_LENS_POSE_REFERENCE |
| |
ANDROID_LENS_DISTORTION |
| |
ANDROID_LENS_INFO_HYPERFOCAL_DISTANCE |
| |
ANDROID_LENS_INFO_MINIMUM_FOCUS_DISTANCE |
| |
图像传感器相关属性 | ANDROID_SENSOR_REFERENCE_ILLUMINANT1 |
|
ANDROID_SENSOR_REFERENCE_ILLUMINANT2 |
| |
ANDROID_SENSOR_CALIBRATION_TRANSFORM1 |
| |
ANDROID_SENSOR_CALIBRATION_TRANSFORM2 |
| |
ANDROID_SENSOR_COLOR_TRANSFORM1 |
| |
ANDROID_SENSOR_COLOR_TRANSFORM2 |
| |
ANDROID_SENSOR_FORWARD_MATRIX1 |
| |
ANDROID_SENSOR_FORWARD_MATRIX2 |
|
-
-
- 兼容性影响
-
即使您的应用的 target 在 Android 9(API级别28)或更低级别,如果没有 CAMERA 权限,在Q版本的Android 系统上运行您的应用,通过 cameraCharacteristics.get(CameraCharacteristics.LENS_POSE_ROTATION) 获取对应属性时,返回值仍将为null。
-
-
- 适配指导
-
要获取以上的属性,请动态申请android.permission.CAMERA的权限,参照:(https://developer.android.google.cn/training/permissions/requesting)。
10.5 限制SMS/Call Log访问
10.5.1 背景
Google Play Store中限制一些高危、高敏感的权限,包括SMS、Call Log权限。如果App没有满足Google Play Store的要求,会从Google Play 移除。
活动 | 要求 |
您的应用清单请求“通话记录”权限组(例如 READ_CALL_LOG、WRITE_CALL_LOG、ROCESS_OUTGOING_CALLS) | 必须由用户主动将应用注册为设备的默认电话或辅助处理程序。 |
您的应用清单请求“短信”权限组(例如 READ_SMS、SEND_SMS、WRITE_SMS、RECEIVE_SMS、RECEIVE_WAP_PUSH、RECEIVE_MMS) | 必须由用户主动将应用注册为设备的默认短信或辅助处理程序。 |
10.5.2 兼容性影响
在Google Play 上架的App,需要注意影响。
https://support.google.com/googleplay/android-developer/answer/9047303
10.5.3 适配指导
适配指导请参考:https://play.google.com/about/privacy-security-deception/permissions/
- 如果应用不具备默认短信、电话或辅助处理程序功能,就不得在清单中(包括清单中的占位文本)声明需要使用上述权限。
- 只有在用户主动将应用注册为默认短信、电话或辅助处理程序的情况下,应用才能提示用户接受上述任何权限请求;当应用不再是默认处理程序时,则必须立即停止使用相应权限。
- 应用只能将权限(及其衍生数据)用于提供已获批准的关键核心应用功能(例如应用说明中记录并宣传的应用现有关键功能)。您绝不能出售此类数据。您只能基于提供应用关键核心功能或服务的目的,转移、分享或许可使用此类数据,不能将此类数据用于任何其他用途(例如改进其他应用或服务、投放广告或营销)。您不得使用其他方法(包括其他权限、API 或第三方来源)从上述权限中衍生数据。
- 通话记录和短信默认处理程序限制的例外情
上述限制是为了保护用户隐私。如果应用不是默认处理程序,但符合以上所有要求,并清楚明确地提供极具吸引力的功能或关键功能,而该功能目前只有在获得相关权限后才能实现,则我们可能会允许少数特例。我们会评估相应功能在隐私权或安全性方面对用户可能造成的影响。这类特例十分少见,并不适用于所有开发者。
11. WIFI相关接口变更
11.1背景
Android Q为了更好的保护用户的隐私,让用户知晓应用对Wi-Fi配置的改动,其限制了应用对WifiManager重要接口的调用,三方应用将无法正常使用这些接口。此外,针对Wi-Fi Direct相关的广播以及接口也做了调整。
11.2 兼容性影响
11.2.1 WifiManager相关接口变更
如下接口,应用需要进行适配。
接口 | targetSdkVersion >= Q 是否可用 | targetSdkVersion < Q 是否可用 |
setWifiEnabled() | 否 | 否 |
addNetwork() | 否 | 是 |
removeNetwork() | 否 | 是 |
disableNetwork() | 否 | 是 |
enableNetwork() | 否 | 是 |
updateNetwork() | 否 | 是 |
reconnect() | 否 | 是 |
disconnect() | 否 | 是 |
reassociate() | 否 | 是 |
getConfiguredNetworks() | 否 | 是 |
getPasspointConfigurations() | 否 | 否 |
removePasspointConfiguration() | 否 | 否 |
11.2.2 Wi-Fi Direct相关变更
在Android Q中,以下与Wi-Fi Direct相关的广播不再具有黏性。如果您的应用依赖于在注册的时候接收这些广播,需要进行适配。
11.3 适配指导
兼容如上接口变更,可以参照:
https://developer.android.com/preview/privacy/camera-connectivity#wifi-network-config-restrictions
https://developer.android.com/preview/behavior-changes-all#wifi-direct-broadcasts
11.3.1 开关或关闭Wi-Fi
Android Q提供了Pannel的方式打开或者关闭Wi-Fi,若只希望开关Wi-Fi,可以通过如下方式。
Intent panelIntent = new Intent(Settings.Panel.ACTION_WIFI);
startActivity(panelIntent);
若希望是连接互联网,可以通过如下方式。
Intent panelIntent = new Intent(Settings.Panel.ACTION_INTERNET_CONNECTIVITY);
startActivity(panelIntent);
11.3.2 使用NetworkRequest连接网络
Android Q限制应用使用连接或断开网络的接口,并推荐使用NetworkRequest的方式连接网络。应用通过配置NetworkSpecifier规则,并通过NetworkRequst下发到Framework,Framework会弹出选择框并触发一次Wi-Fi扫描。扫描接收后,系统会根据NetworkSpecifier规则将过滤后的扫描结果呈现在弹框中供用户选择。
PatternMatcher ssidMatcher = new PatternMatcher("OPPO", PatternMatcher.PATTERN_PREFIX);
WifiNetworkSpecifier.Builder buidler = new WifiNetworkSpecifier.Builder();
buidler.setSsidPattern(ssidMatcher);
NetworkRequest.Builder networkRequestBuilder = new NetworkRequest.Builder();
networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
networkRequestBuilder.setNetworkSpecifier(buidler.build());
mConnectivityManager.requestNetwork(networkRequestBuilder.build(), mNetworkCallback);
11.3.3 使用WifiNetworkSuggestion连接网络
应用可以创建NetworkSuggestions,将其添加到Framework。待Framework收到扫描结果后,会首先从系统已保存的网络中选网。如果没有可选的网络,就会从添加的NetworkSuggestions中选网。若Framework连接上本应用添加的NetworkSuggestions后,会发送WifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION广播到应用内。
WifiNetworkSuggestion.Builder networkSuggestionBuilder = new WifiNetworkSuggestion.Builder();
networkSuggestionBuilder.setSsid("TEST");
List<WifiNetworkSuggestion> networkSuggestionList = new ArrayList();
networkSuggestionList.add(networkSuggestionBuilder.build());
if (mWifiManager.addNetworkSuggestions(networkSuggestionList) == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {
Log.d(TAG, "Successfully add network suggestion.");
}
11.3.4 Wi-Fi Direct广播适配
原依赖于注册WIFI_P2P_CONNECTION_CHANGED_ACTION时接收广播内容的逻辑,可以使用WifiP2pManager.requestNetworkInfo()接口查询,具体使用方式如下:
p2pManager.requestNetworkInfo(p2pChannel, new WifiP2pManager.NetworkInfoListener() {
@Override
public void onNetworkInfoAvailable(NetworkInfo networkInfo) {
Log.d(TAG, "Receive network information " + networkInfo);
}
});
原依赖于注册WIFI_P2P_THIS_DEVICE_CHANGED_ACTION时接收广播内容的逻辑,可以使用WifiP2pManager.requestDeviceInfo()接口查询,注意该方法需要申请ACCESS_FINE_LOCATION权限,具体使用方式如下:
p2pManager.requestDeviceInfo(p2pChannel, new WifiP2pManager.DeviceInfoListener() {
@Override
public void onDeviceInfoAvailable(WifiP2pDevice wifiP2pDevice) {
Log.d(TAG, "Receive deivce information " + wifiP2pDevice);
}
}
12 电话API重要变更
12.1背景
TelephonyManager.java的endCall()、answerRingingCall ()、silenceRinger ()方法已失效。
12.2 兼容性影响
调用如上方法将无法生效,不会有任何响应。
12.3 适配指导
如需挂断电话:
使用android.telecom.TelecomManager#endCall(),该接口需申请权限Manifest.permission.ANSWER_PHONE_CALLS,但谷歌已不建议使用,
不久的将来存在废弃的风险。
如需接听电话:
使用android.telecom.TelecomManager# acceptRingingCall (),该接口需申请权限Manifest.permission.ANSWER_PHONE_CALLS,但谷歌已不建议使用,
不久的将来存在废弃的风险。
如需实现来电静音:
请使用android.telecom.TelecomManager# silenceRinger ()方法,但只能被用户设为默认拨号应用的前提下使用,否则将会抛出权限异常。