一、背景
在Android S上,有一个新增的设计——定位开关被关闭以后,应用申请的定位权限OP_FINE_LOCATION和OP_COARSE_LOCATION会被系统回收。
二、问题
基于上面的背景,有些app会出现这种问题。app在打开以后,已经动态申请了权限android.permission.ACCESS_FINE_LOCATION和android.permission.ACCESS_COARSE_LOCATION,但是后来用户关闭了定位开关,这时候,如果app需要执行一些与定位无关但是却需要定位权限的操作时(例如进行蓝牙扫描,建立P2P连接等),就会发现上述操作无法正确执行成功。
三、 代码设计
?在LocationManagerService.java中,当定位开关被用户开启或者关闭的时候,会走到
onLocationModeChangedhttp://aosp.opersys.com/xref/android-12.0.0_r2/s?refs=onLocationModeChanged&project=frameworks
private void onLocationModeChanged(int userId) {
boolean enabled = mInjector.getSettingsHelper().isLocationEnabled(userId);
LocationManager.invalidateLocalLocationEnabledCaches();
if (D) {
Log.d(TAG, "[u" + userId + "] location enabled = " + enabled);
}
EVENT_LOG.logLocationEnabled(userId, enabled);
Intent intent = new Intent(LocationManager.MODE_CHANGED_ACTION)
.putExtra(LocationManager.EXTRA_LOCATION_ENABLED, enabled)
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
mContext.sendBroadcastAsUser(intent, UserHandle.of(userId));
refreshAppOpsRestrictions(userId);
}
然后会执行到一个新增的方法:refreshAppOpsRestrictionshttp://aosp.opersys.com/xref/android-12.0.0_r2/s?refs=refreshAppOpsRestrictions&project=frameworks
private void refreshAppOpsRestrictions(int userId) {
if (userId == UserHandle.USER_ALL) {
final int[] runningUserIds = mInjector.getUserInfoHelper().getRunningUserIds();
for (int i = 0; i < runningUserIds.length; i++) {
refreshAppOpsRestrictions(runningUserIds[i]);
}
return;
}
Preconditions.checkArgument(userId >= 0);
boolean enabled = mInjector.getSettingsHelper().isLocationEnabled(userId);
PackageTagsList allowedPackages = null;
if (!enabled) {
PackageTagsList.Builder builder = new PackageTagsList.Builder();
for (LocationProviderManager manager : mProviderManagers) {
CallerIdentity identity = manager.getIdentity();
if (identity != null) {
builder.add(identity.getPackageName(), identity.getAttributionTag());
}
}
builder.add(mInjector.getSettingsHelper().getIgnoreSettingsAllowlist());
allowedPackages = builder.build();
}
AppOpsManager appOpsManager = Objects.requireNonNull(
mContext.getSystemService(AppOpsManager.class));
appOpsManager.setUserRestrictionForUser(
AppOpsManager.OP_COARSE_LOCATION,
!enabled,
LocationManagerService.this,
allowedPackages,
userId);
appOpsManager.setUserRestrictionForUser(
AppOpsManager.OP_FINE_LOCATION,
!enabled,
LocationManagerService.this,
allowedPackages,
userId);
}
当定位开关被关闭后,
boolean enabled = mInjector.getSettingsHelper().isLocationEnabled(userId);
得到的enabled的值为false,
下面的if判断就会走进去:
PackageTagsList allowedPackages = null;
if (!enabled) {
PackageTagsList.Builder builder = new PackageTagsList.Builder();
for (LocationProviderManager manager : mProviderManagers) {
CallerIdentity identity = manager.getIdentity();
if (identity != null) {
builder.add(identity.getPackageName(), identity.getAttributionTag());
}
}
builder.add(mInjector.getSettingsHelper().getIgnoreSettingsAllowlist());
allowedPackages = builder.build();
}
allowedPackages看命名方式,它就是白名单的意思。
上述的变量赋值以后,会走到下面的回收权限的地方:
AppOpsManager appOpsManager = Objects.requireNonNull(
mContext.getSystemService(AppOpsManager.class));
appOpsManager.setUserRestrictionForUser(
AppOpsManager.OP_COARSE_LOCATION,
!enabled,
LocationManagerService.this,
allowedPackages,
userId);
appOpsManager.setUserRestrictionForUser(
AppOpsManager.OP_FINE_LOCATION,
!enabled,
LocationManagerService.this,
allowedPackages,
userId);
当enabled为false的时候,会将userId的用户空间的所有除了allowedPackages的package的OP_FINE_LOCATION和OP_COARSE_LOCATION回收;
当enabled为true的时候,会将userId的用户空间的所有除了allowedPackages的package的OP_FINE_LOCATION和OP_COARSE_LOCATION释放;
总结一下就是,当定位开关被关闭后,所有白名单以外的package的定位权限会被回收,只有当定位开关重新打开以后才会被释放。
四、 解决方案
在上面的代码设计中讲到,下面的if语句是添加白名单的地方,
PackageTagsList allowedPackages = null;
if (!enabled) {
PackageTagsList.Builder builder = new PackageTagsList.Builder();
for (LocationProviderManager manager : mProviderManagers) {
CallerIdentity identity = manager.getIdentity();
if (identity != null) {
builder.add(identity.getPackageName(), identity.getAttributionTag());
}
}
builder.add(mInjector.getSettingsHelper().getIgnoreSettingsAllowlist());
allowedPackages = builder.build();
}
当enabled为true的时候,allowedPackages为null,不谈;
当enabled为false的时候,先遍历LocationProviderManager,将4种provider(passive network fused gps)加入到白名单中;然后通过mInjector.getSettingsHelper().getIgnoreSettingsAllowlist()去获取已经添加过后并保存好的白名单。
那么,问题解决的方案可以从mInjector.getSettingsHelper().getIgnoreSettingsAllowlist()入手了。
@Override
public PackageTagsList getIgnoreSettingsAllowlist() {
return mIgnoreSettingsPackageAllowlist.getValue();
}
mIgnoreSettingsPackageAllowlist定义处:
private final PackageTagsListSetting mIgnoreSettingsPackageAllowlist;
mIgnoreSettingsPackageAllowlist初始化:
mIgnoreSettingsPackageAllowlist = new PackageTagsListSetting(
IGNORE_SETTINGS_ALLOWLIST,
() -> SystemConfig.getInstance().getAllowIgnoreLocationSettings());
从SystemConfig.java中获取AllowIgnoreLocationSettings
public ArrayMap<String, ArraySet<String>> getAllowIgnoreLocationSettings() {
return mAllowIgnoreLocationSettings;
}
mAllowIgnoreLocationSettings定义、初始化:
final ArrayMap<String, ArraySet<String>> mAllowIgnoreLocationSettings = new ArrayMap<>();
新增元素,这边就是新增白名单的地方。
case "allow-ignore-location-settings": {
if (allowOverrideAppRestrictions) {
String pkgname = parser.getAttributeValue(null, "package");
String attributionTag = parser.getAttributeValue(null,
"attributionTag");
if (pkgname == null) {
Slog.w(TAG, "<" + name + "> without package in "
+ permFile + " at " + parser.getPositionDescription());
} else {
ArraySet<String> tags = mAllowIgnoreLocationSettings.get(pkgname);
if (tags == null || !tags.isEmpty()) {
if (tags == null) {
tags = new ArraySet<>(1);
mAllowIgnoreLocationSettings.put(pkgname, tags);
}
if (!"*".equals(attributionTag)) {
if ("null".equals(attributionTag)) {
attributionTag = null;
}
tags.add(attributionTag);
}
}
}
} else {
logNotAllowedInPartition(name, permFile, parser);
}
XmlUtils.skipCurrentTag(parser);
} break;
看代码实现,这边是解析xml元素"package"和"attributionTag"的属性值,然后解析后的属性值添加到mAllowIgnoreLocationSettings中。
读取Permissons
private void readPermissionsFromXml(File permFile, int permissionFlag) {
FileReader permReader = null;
try {
permReader = new FileReader(permFile);
} catch (FileNotFoundException e) {
Slog.w(TAG, "Couldn't find or open permissions file " + permFile);
return;
}
Slog.i(TAG, "Reading permissions from " + permFile);
final boolean lowRam = ActivityManager.isLowRamDeviceStatic();
try {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(permReader);
int type;
while ((type=parser.next()) != parser.START_TAG
&& type != parser.END_DOCUMENT) {
;
}
if (type != parser.START_TAG) {
throw new XmlPullParserException("No start tag found");
}
if (!parser.getName().equals("permissions") && !parser.getName().equals("config")) {
throw new XmlPullParserException("Unexpected start tag in " + permFile
+ ": found " + parser.getName() + ", expected 'permissions' or 'config'");
}
final boolean allowAll = permissionFlag == ALLOW_ALL;
final boolean allowLibs = (permissionFlag & ALLOW_LIBS) != 0;
final boolean allowFeatures = (permissionFlag & ALLOW_FEATURES) != 0;
final boolean allowPermissions = (permissionFlag & ALLOW_PERMISSIONS) != 0;
final boolean allowAppConfigs = (permissionFlag & ALLOW_APP_CONFIGS) != 0;
final boolean allowPrivappPermissions = (permissionFlag & ALLOW_PRIVAPP_PERMISSIONS)
!= 0;
final boolean allowOemPermissions = (permissionFlag & ALLOW_OEM_PERMISSIONS) != 0;
final boolean allowApiWhitelisting = (permissionFlag & ALLOW_HIDDENAPI_WHITELISTING)
!= 0;
final boolean allowAssociations = (permissionFlag & ALLOW_ASSOCIATIONS) != 0;
final boolean allowOverrideAppRestrictions =
(permissionFlag & ALLOW_OVERRIDE_APP_RESTRICTIONS) != 0;
final boolean allowImplicitBroadcasts = (permissionFlag & ALLOW_IMPLICIT_BROADCASTS)
!= 0;
final boolean allowVendorApex = (permissionFlag & ALLOW_VENDOR_APEX) != 0;
while (true) {
XmlUtils.nextElement(parser);
if (parser.getEventType() == XmlPullParser.END_DOCUMENT) {
break;
}
String name = parser.getName();
if (name == null) {
XmlUtils.skipCurrentTag(parser);
continue;
}
switch (name) {
// ......
case "allow-ignore-location-settings": {
if (allowOverrideAppRestrictions) {
String pkgname = parser.getAttributeValue(null, "package");
String attributionTag = parser.getAttributeValue(null,
"attributionTag");
if (pkgname == null) {
Slog.w(TAG, "<" + name + "> without package in "
+ permFile + " at " + parser.getPositionDescription());
} else {
ArraySet<String> tags = mAllowIgnoreLocationSettings.get(pkgname);
if (tags == null || !tags.isEmpty()) {
if (tags == null) {
tags = new ArraySet<>(1);
mAllowIgnoreLocationSettings.put(pkgname, tags);
}
if (!"*".equals(attributionTag)) {
if ("null".equals(attributionTag)) {
attributionTag = null;
}
tags.add(attributionTag);
}
}
}
} else {
logNotAllowedInPartition(name, permFile, parser);
}
XmlUtils.skipCurrentTag(parser);
} break;
// ......
}
}
} catch (XmlPullParserException e) {
Slog.w(TAG, "Got exception parsing permissions.", e);
} catch (IOException e) {
Slog.w(TAG, "Got exception parsing permissions.", e);
} finally {
IoUtils.closeQuietly(permReader);
}
// ......
}
加载XML,从手机的各个目录下的/etc/的sysconfig和permissions中加载。
private void readAllPermissions() {
// Read configuration from system
readPermissions(Environment.buildPath(
Environment.getRootDirectory(), "etc", "sysconfig"), ALLOW_ALL);
// Read configuration from the old permissions dir
readPermissions(Environment.buildPath(
Environment.getRootDirectory(), "etc", "permissions"), ALLOW_ALL);
// Vendors are only allowed to customize these
int vendorPermissionFlag = ALLOW_LIBS | ALLOW_FEATURES | ALLOW_PRIVAPP_PERMISSIONS
| ALLOW_ASSOCIATIONS | ALLOW_VENDOR_APEX;
if (Build.VERSION.DEVICE_INITIAL_SDK_INT <= Build.VERSION_CODES.O_MR1) {
// For backward compatibility
vendorPermissionFlag |= (ALLOW_PERMISSIONS | ALLOW_APP_CONFIGS);
}
readPermissions(Environment.buildPath(
Environment.getVendorDirectory(), "etc", "sysconfig"), vendorPermissionFlag);
readPermissions(Environment.buildPath(
Environment.getVendorDirectory(), "etc", "permissions"), vendorPermissionFlag);
String vendorSkuProperty = SystemProperties.get(VENDOR_SKU_PROPERTY, "");
if (!vendorSkuProperty.isEmpty()) {
String vendorSkuDir = "sku_" + vendorSkuProperty;
readPermissions(Environment.buildPath(
Environment.getVendorDirectory(), "etc", "sysconfig", vendorSkuDir),
vendorPermissionFlag);
readPermissions(Environment.buildPath(
Environment.getVendorDirectory(), "etc", "permissions", vendorSkuDir),
vendorPermissionFlag);
}
// Allow ODM to customize system configs as much as Vendor, because /odm is another
// vendor partition other than /vendor.
int odmPermissionFlag = vendorPermissionFlag;
readPermissions(Environment.buildPath(
Environment.getOdmDirectory(), "etc", "sysconfig"), odmPermissionFlag);
readPermissions(Environment.buildPath(
Environment.getOdmDirectory(), "etc", "permissions"), odmPermissionFlag);
String skuProperty = SystemProperties.get(SKU_PROPERTY, "");
if (!skuProperty.isEmpty()) {
String skuDir = "sku_" + skuProperty;
readPermissions(Environment.buildPath(
Environment.getOdmDirectory(), "etc", "sysconfig", skuDir), odmPermissionFlag);
readPermissions(Environment.buildPath(
Environment.getOdmDirectory(), "etc", "permissions", skuDir),
odmPermissionFlag);
}
// Allow OEM to customize these
int oemPermissionFlag = ALLOW_FEATURES | ALLOW_OEM_PERMISSIONS | ALLOW_ASSOCIATIONS
| ALLOW_VENDOR_APEX;
readPermissions(Environment.buildPath(
Environment.getOemDirectory(), "etc", "sysconfig"), oemPermissionFlag);
readPermissions(Environment.buildPath(
Environment.getOemDirectory(), "etc", "permissions"), oemPermissionFlag);
// Allow Product to customize these configs
// TODO(b/157203468): ALLOW_HIDDENAPI_WHITELISTING must be removed because we prohibited
// the use of hidden APIs from the product partition.
int productPermissionFlag = ALLOW_FEATURES | ALLOW_LIBS | ALLOW_PERMISSIONS
| ALLOW_APP_CONFIGS | ALLOW_PRIVAPP_PERMISSIONS | ALLOW_HIDDENAPI_WHITELISTING
| ALLOW_ASSOCIATIONS | ALLOW_OVERRIDE_APP_RESTRICTIONS | ALLOW_IMPLICIT_BROADCASTS
| ALLOW_VENDOR_APEX;
if (Build.VERSION.DEVICE_INITIAL_SDK_INT <= Build.VERSION_CODES.R) {
// TODO(b/157393157): This must check product interface enforcement instead of
// DEVICE_INITIAL_SDK_INT for the devices without product interface enforcement.
productPermissionFlag = ALLOW_ALL;
}
readPermissions(Environment.buildPath(
Environment.getProductDirectory(), "etc", "sysconfig"), productPermissionFlag);
readPermissions(Environment.buildPath(
Environment.getProductDirectory(), "etc", "permissions"), productPermissionFlag);
// Allow /system_ext to customize all system configs
readPermissions(Environment.buildPath(
Environment.getSystemExtDirectory(), "etc", "sysconfig"), ALLOW_ALL);
readPermissions(Environment.buildPath(
Environment.getSystemExtDirectory(), "etc", "permissions"), ALLOW_ALL);
// Skip loading configuration from apex if it is not a system process.
if (!isSystemProcess()) {
return;
}
// Read configuration of libs from apex module.
// TODO: Use a solid way to filter apex module folders?
for (File f: FileUtils.listFilesOrEmpty(Environment.getApexDirectory())) {
if (f.isFile() || f.getPath().contains("@")) {
continue;
}
readPermissions(Environment.buildPath(f, "etc", "permissions"), ALLOW_LIBS);
}
}
@VisibleForTesting
public void readPermissions(File libraryDir, int permissionFlag) {
// Read permissions from given directory.
if (!libraryDir.exists() || !libraryDir.isDirectory()) {
if (permissionFlag == ALLOW_ALL) {
Slog.w(TAG, "No directory " + libraryDir + ", skipping");
}
return;
}
if (!libraryDir.canRead()) {
Slog.w(TAG, "Directory " + libraryDir + " cannot be read");
return;
}
// Iterate over the files in the directory and scan .xml files
File platformFile = null;
for (File f : libraryDir.listFiles()) {
if (!f.isFile()) {
continue;
}
// We'll read platform.xml last
if (f.getPath().endsWith("etc/permissions/platform.xml")) {
platformFile = f;
continue;
}
if (!f.getPath().endsWith(".xml")) {
Slog.i(TAG, "Non-xml file " + f + " in " + libraryDir + " directory, ignoring");
continue;
}
if (!f.canRead()) {
Slog.w(TAG, "Permissions library file " + f + " cannot be read");
continue;
}
readPermissionsFromXml(f, permissionFlag);
}
// Read platform permissions last so it will take precedence
if (platformFile != null) {
readPermissionsFromXml(platformFile, permissionFlag);
}
}
最后我们参照google添加的默认的白名单:
Bypass Allow Packages:
com.google.android.dialer[*]
com.google.android.gms[.thunderbird]
我们可以在/product/etc/sysconfig/google.xml中,可以我们需要的内容:
<?xml version="1.0" encoding="utf-8"?>
<!-- These are configurations that must exist on all GMS devices. -->
<config>
<allow-ignore-location-settings package="com.google.android.gms" attributionTag="com.google.android.gms.thunderbird" />
<allow-ignore-location-settings package="com.google.android.dialer" attributionTag="*" />
</config>
到这里解决方案呼之欲出了,我们新开一个小节单独总结下。
五、 总结
我们可以在/system/etc/sysconfig/下新增一个文件,这里我们可以命名(也可以定义为你想要的fashion的名字)为location-settings-conf.xml,
<?xml version="1.0" encoding="utf-8"?>
<config>
<allow-ignore-location-settings package="com.android.package.one" attributionTag="*" />
</config>
元素package的属性值,写入你要添加进白名单的package;
元素attributionTAG的属性值,可以直接写入*,也可以写上你想要的字符。
添加成功后,我们通过adb shell dumpsys location可以看到下面的dump信息。
Bypass Allow Packages:
com.android.package.one[*]
com.google.android.dialer[*]
com.google.android.gms[.thunderbird]
再去验证之前碰到的问题,pass。
|