Support for a Context to "renounce" permissions.
Different logical components within an app may have no intention of interacting with data or services that are protected by specific permissions. The new overload added in this change provides the initial mechanism for a logical component to indicate a set of permissions that should be treated as "renounced". Interactions performed through the returned Context will ideally be treated as if the renounced permissions have not actually been granted to the application, regardless of their actual grant status. This is a low-risk change from a security standpoint, since it can only reduce the set of permissions that might have been granted to an app; it can never be used to expand the set of permissions. Note that this change only provides an initial implementation which only applies to local permission checks within the app; future changes will begin wiring this up across process boundaries. Bug: 181812281 Test: atest CtsContentTestCases:android.content.cts.ContextTest Change-Id: I96439e5344c85300fb6a0f03e572746c3c96ee95
This commit is contained in:
@@ -10376,6 +10376,7 @@ package android.content {
|
||||
method public abstract android.content.pm.PackageManager getPackageManager();
|
||||
method public abstract String getPackageName();
|
||||
method public abstract String getPackageResourcePath();
|
||||
method @Nullable public android.content.ContextParams getParams();
|
||||
method public abstract android.content.res.Resources getResources();
|
||||
method public abstract android.content.SharedPreferences getSharedPreferences(String, int);
|
||||
method @NonNull public final String getString(@StringRes int);
|
||||
|
||||
@@ -228,6 +228,7 @@ package android {
|
||||
field public static final String REMOTE_DISPLAY_PROVIDER = "android.permission.REMOTE_DISPLAY_PROVIDER";
|
||||
field public static final String REMOVE_DRM_CERTIFICATES = "android.permission.REMOVE_DRM_CERTIFICATES";
|
||||
field public static final String REMOVE_TASKS = "android.permission.REMOVE_TASKS";
|
||||
field public static final String RENOUNCE_PERMISSIONS = "android.permission.RENOUNCE_PERMISSIONS";
|
||||
field public static final String REQUEST_NETWORK_SCORES = "android.permission.REQUEST_NETWORK_SCORES";
|
||||
field public static final String REQUEST_NOTIFICATION_ASSISTANT_SERVICE = "android.permission.REQUEST_NOTIFICATION_ASSISTANT_SERVICE";
|
||||
field public static final String RESET_PASSWORD = "android.permission.RESET_PASSWORD";
|
||||
@@ -2184,6 +2185,14 @@ package android.content {
|
||||
field public static final String WIFI_SCANNING_SERVICE = "wifiscanner";
|
||||
}
|
||||
|
||||
public final class ContextParams {
|
||||
method @Nullable @RequiresPermission(android.Manifest.permission.RENOUNCE_PERMISSIONS) public java.util.Set<java.lang.String> getRenouncedPermissions();
|
||||
}
|
||||
|
||||
public static final class ContextParams.Builder {
|
||||
method @NonNull @RequiresPermission(android.Manifest.permission.RENOUNCE_PERMISSIONS) public android.content.ContextParams.Builder setRenouncedPermissions(@NonNull java.util.Set<java.lang.String>);
|
||||
}
|
||||
|
||||
public class ContextWrapper extends android.content.Context {
|
||||
method public android.content.Context createCredentialProtectedStorageContext();
|
||||
method @Nullable public java.io.File getPreloadsFileCache();
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.annotation.IntDef;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.compat.annotation.UnsupportedAppUsage;
|
||||
import android.content.ContextParams;
|
||||
import android.content.AutofillOptions;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
@@ -32,6 +31,7 @@ import android.content.ContentCaptureOptions;
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.ContextParams;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.IContentProvider;
|
||||
import android.content.IIntentReceiver;
|
||||
@@ -221,8 +221,7 @@ class ContextImpl extends Context {
|
||||
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
|
||||
private final String mOpPackageName;
|
||||
|
||||
/** Attribution tag of this context */
|
||||
private final @Nullable String mAttributionTag;
|
||||
private final @NonNull ContextParams mParams;
|
||||
|
||||
private final @NonNull ResourcesManager mResourcesManager;
|
||||
@UnsupportedAppUsage
|
||||
@@ -470,7 +469,12 @@ class ContextImpl extends Context {
|
||||
/** @hide */
|
||||
@Override
|
||||
public @Nullable String getAttributionTag() {
|
||||
return mAttributionTag;
|
||||
return mParams.getAttributionTag();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ContextParams getParams() {
|
||||
return mParams;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -2047,6 +2051,11 @@ class ContextImpl extends Context {
|
||||
if (permission == null) {
|
||||
throw new IllegalArgumentException("permission is null");
|
||||
}
|
||||
if (mParams.isRenouncedPermission(permission)
|
||||
&& pid == android.os.Process.myPid() && uid == android.os.Process.myUid()) {
|
||||
Log.v(TAG, "Treating renounced permission " + permission + " as denied");
|
||||
return PERMISSION_DENIED;
|
||||
}
|
||||
return PermissionManager.checkPermission(permission, pid, uid);
|
||||
}
|
||||
|
||||
@@ -2056,6 +2065,11 @@ class ContextImpl extends Context {
|
||||
if (permission == null) {
|
||||
throw new IllegalArgumentException("permission is null");
|
||||
}
|
||||
if (mParams.isRenouncedPermission(permission)
|
||||
&& pid == android.os.Process.myPid() && uid == android.os.Process.myUid()) {
|
||||
Log.v(TAG, "Treating renounced permission " + permission + " as denied");
|
||||
return PERMISSION_DENIED;
|
||||
}
|
||||
|
||||
try {
|
||||
return ActivityManager.getService().checkPermissionWithToken(
|
||||
@@ -2093,6 +2107,10 @@ class ContextImpl extends Context {
|
||||
if (permission == null) {
|
||||
throw new IllegalArgumentException("permission is null");
|
||||
}
|
||||
if (mParams.isRenouncedPermission(permission)) {
|
||||
Log.v(TAG, "Treating renounced permission " + permission + " as denied");
|
||||
return PERMISSION_DENIED;
|
||||
}
|
||||
|
||||
return checkPermission(permission, Process.myPid(), Process.myUid());
|
||||
}
|
||||
@@ -2393,8 +2411,9 @@ class ContextImpl extends Context {
|
||||
LoadedApk pi = mMainThread.getPackageInfo(application, mResources.getCompatibilityInfo(),
|
||||
flags | CONTEXT_REGISTER_PACKAGE);
|
||||
if (pi != null) {
|
||||
ContextImpl c = new ContextImpl(this, mMainThread, pi, null, null, mToken,
|
||||
new UserHandle(UserHandle.getUserId(application.uid)), flags, null, null);
|
||||
ContextImpl c = new ContextImpl(this, mMainThread, pi, ContextParams.EMPTY, null,
|
||||
mToken, new UserHandle(UserHandle.getUserId(application.uid)),
|
||||
flags, null, null);
|
||||
|
||||
final int displayId = getDisplayId();
|
||||
final Integer overrideDisplayId = mForceDisplayOverrideInResources
|
||||
@@ -2423,14 +2442,14 @@ class ContextImpl extends Context {
|
||||
if (packageName.equals("system") || packageName.equals("android")) {
|
||||
// The system resources are loaded in every application, so we can safely copy
|
||||
// the context without reloading Resources.
|
||||
return new ContextImpl(this, mMainThread, mPackageInfo, mAttributionTag, null,
|
||||
return new ContextImpl(this, mMainThread, mPackageInfo, mParams, null,
|
||||
mToken, user, flags, null, null);
|
||||
}
|
||||
|
||||
LoadedApk pi = mMainThread.getPackageInfo(packageName, mResources.getCompatibilityInfo(),
|
||||
flags | CONTEXT_REGISTER_PACKAGE, user.getIdentifier());
|
||||
if (pi != null) {
|
||||
ContextImpl c = new ContextImpl(this, mMainThread, pi, mAttributionTag, null,
|
||||
ContextImpl c = new ContextImpl(this, mMainThread, pi, mParams, null,
|
||||
mToken, user, flags, null, null);
|
||||
|
||||
final int displayId = getDisplayId();
|
||||
@@ -2469,7 +2488,7 @@ class ContextImpl extends Context {
|
||||
final String[] paths = mPackageInfo.getSplitPaths(splitName);
|
||||
|
||||
final ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo,
|
||||
mAttributionTag, splitName, mToken, mUser, mFlags, classLoader, null);
|
||||
mParams, splitName, mToken, mUser, mFlags, classLoader, null);
|
||||
|
||||
context.setResources(ResourcesManager.getInstance().getResources(
|
||||
mToken,
|
||||
@@ -2502,7 +2521,7 @@ class ContextImpl extends Context {
|
||||
overrideConfiguration = displayAdjustedConfig;
|
||||
}
|
||||
|
||||
ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mAttributionTag,
|
||||
ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mParams,
|
||||
mSplitName, mToken, mUser, mFlags, mClassLoader, null);
|
||||
|
||||
final int displayId = getDisplayId();
|
||||
@@ -2520,7 +2539,7 @@ class ContextImpl extends Context {
|
||||
throw new IllegalArgumentException("display must not be null");
|
||||
}
|
||||
|
||||
ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mAttributionTag,
|
||||
ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mParams,
|
||||
mSplitName, mToken, mUser, mFlags, mClassLoader, null);
|
||||
|
||||
final int displayId = display.getDisplayId();
|
||||
@@ -2578,7 +2597,7 @@ class ContextImpl extends Context {
|
||||
|
||||
|
||||
ContextImpl createBaseWindowContext(IBinder token, Display display) {
|
||||
ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mAttributionTag,
|
||||
ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mParams,
|
||||
mSplitName, token, mUser, mFlags, mClassLoader, null);
|
||||
// Window contexts receive configurations directly from the server and as such do not
|
||||
// need to override their display in ResourcesManager.
|
||||
@@ -2609,21 +2628,21 @@ class ContextImpl extends Context {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Context createContext(@NonNull ContextParams contextParams) {
|
||||
return this;
|
||||
public Context createContext(@NonNull ContextParams params) {
|
||||
return new ContextImpl(this, mMainThread, mPackageInfo, params, mSplitName,
|
||||
mToken, mUser, mFlags, mClassLoader, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Context createAttributionContext(@Nullable String attributionTag) {
|
||||
return new ContextImpl(this, mMainThread, mPackageInfo, attributionTag, mSplitName,
|
||||
mToken, mUser, mFlags, mClassLoader, null);
|
||||
return createContext(new ContextParams.Builder().setAttributionTag(attributionTag).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context createDeviceProtectedStorageContext() {
|
||||
final int flags = (mFlags & ~Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE)
|
||||
| Context.CONTEXT_DEVICE_PROTECTED_STORAGE;
|
||||
return new ContextImpl(this, mMainThread, mPackageInfo, mAttributionTag, mSplitName,
|
||||
return new ContextImpl(this, mMainThread, mPackageInfo, mParams, mSplitName,
|
||||
mToken, mUser, flags, mClassLoader, null);
|
||||
}
|
||||
|
||||
@@ -2631,7 +2650,7 @@ class ContextImpl extends Context {
|
||||
public Context createCredentialProtectedStorageContext() {
|
||||
final int flags = (mFlags & ~Context.CONTEXT_DEVICE_PROTECTED_STORAGE)
|
||||
| Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE;
|
||||
return new ContextImpl(this, mMainThread, mPackageInfo, mAttributionTag, mSplitName,
|
||||
return new ContextImpl(this, mMainThread, mPackageInfo, mParams, mSplitName,
|
||||
mToken, mUser, flags, mClassLoader, null);
|
||||
}
|
||||
|
||||
@@ -2805,8 +2824,8 @@ class ContextImpl extends Context {
|
||||
@UnsupportedAppUsage
|
||||
static ContextImpl createSystemContext(ActivityThread mainThread) {
|
||||
LoadedApk packageInfo = new LoadedApk(mainThread);
|
||||
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, null,
|
||||
0, null, null);
|
||||
ContextImpl context = new ContextImpl(null, mainThread, packageInfo,
|
||||
ContextParams.EMPTY, null, null, null, 0, null, null);
|
||||
context.setResources(packageInfo.getResources());
|
||||
context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(),
|
||||
context.mResourcesManager.getDisplayMetrics());
|
||||
@@ -2823,8 +2842,8 @@ class ContextImpl extends Context {
|
||||
*/
|
||||
static ContextImpl createSystemUiContext(ContextImpl systemContext, int displayId) {
|
||||
final LoadedApk packageInfo = systemContext.mPackageInfo;
|
||||
ContextImpl context = new ContextImpl(null, systemContext.mMainThread, packageInfo, null,
|
||||
null, null, null, 0, null, null);
|
||||
ContextImpl context = new ContextImpl(null, systemContext.mMainThread, packageInfo,
|
||||
ContextParams.EMPTY, null, null, null, 0, null, null);
|
||||
context.setResources(createResources(null, packageInfo, null, displayId, null,
|
||||
packageInfo.getCompatibilityInfo(), null));
|
||||
context.updateDisplay(displayId);
|
||||
@@ -2848,8 +2867,8 @@ class ContextImpl extends Context {
|
||||
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo,
|
||||
String opPackageName) {
|
||||
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
|
||||
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, null,
|
||||
0, null, opPackageName);
|
||||
ContextImpl context = new ContextImpl(null, mainThread, packageInfo,
|
||||
ContextParams.EMPTY, null, null, null, 0, null, opPackageName);
|
||||
context.setResources(packageInfo.getResources());
|
||||
context.mContextType = isSystemOrSystemUI(context) ? CONTEXT_TYPE_SYSTEM_OR_SYSTEM_UI
|
||||
: CONTEXT_TYPE_NON_UI;
|
||||
@@ -2878,7 +2897,7 @@ class ContextImpl extends Context {
|
||||
}
|
||||
}
|
||||
|
||||
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null,
|
||||
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, ContextParams.EMPTY,
|
||||
activityInfo.splitName, activityToken, null, 0, classLoader, null);
|
||||
context.mContextType = CONTEXT_TYPE_ACTIVITY;
|
||||
|
||||
@@ -2911,7 +2930,7 @@ class ContextImpl extends Context {
|
||||
}
|
||||
|
||||
private ContextImpl(@Nullable ContextImpl container, @NonNull ActivityThread mainThread,
|
||||
@NonNull LoadedApk packageInfo, @Nullable String attributionTag,
|
||||
@NonNull LoadedApk packageInfo, @NonNull ContextParams params,
|
||||
@Nullable String splitName, @Nullable IBinder token, @Nullable UserHandle user,
|
||||
int flags, @Nullable ClassLoader classLoader, @Nullable String overrideOpPackageName) {
|
||||
mOuterContext = this;
|
||||
@@ -2966,7 +2985,7 @@ class ContextImpl extends Context {
|
||||
}
|
||||
|
||||
mOpPackageName = overrideOpPackageName != null ? overrideOpPackageName : opPackageName;
|
||||
mAttributionTag = attributionTag;
|
||||
mParams = Objects.requireNonNull(params);
|
||||
mContentResolver = new ApplicationContentResolver(this, mainThread);
|
||||
}
|
||||
|
||||
|
||||
@@ -888,6 +888,14 @@ public abstract class Context {
|
||||
return getAttributionTag();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the set of parameters which this Context was created with, if it
|
||||
* was created via {@link #createContext(ContextParams)}.
|
||||
*/
|
||||
public @Nullable ContextParams getParams() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Return the full application info for this context's package. */
|
||||
public abstract ApplicationInfo getApplicationInfo();
|
||||
|
||||
|
||||
@@ -18,6 +18,13 @@ package android.content;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.annotation.RequiresPermission;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.SystemApi;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* This class represents rules around how a context being created via
|
||||
@@ -48,9 +55,19 @@ import android.annotation.Nullable;
|
||||
* @see Context#createContext(ContextParams)
|
||||
*/
|
||||
public final class ContextParams {
|
||||
private final String mAttributionTag;
|
||||
private final String mReceiverPackage;
|
||||
private final String mReceiverAttributionTag;
|
||||
private final Set<String> mRenouncedPermissions;
|
||||
|
||||
private ContextParams() {
|
||||
/* hide ctor */
|
||||
/** {@hide} */
|
||||
public static final ContextParams EMPTY = new ContextParams.Builder().build();
|
||||
|
||||
private ContextParams(@NonNull ContextParams.Builder builder) {
|
||||
mAttributionTag = builder.mAttributionTag;
|
||||
mReceiverPackage = builder.mReceiverPackage;
|
||||
mReceiverAttributionTag = builder.mReceiverAttributionTag;
|
||||
mRenouncedPermissions = builder.mRenouncedPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,7 +75,7 @@ public final class ContextParams {
|
||||
*/
|
||||
@Nullable
|
||||
public String getAttributionTag() {
|
||||
return null;
|
||||
return mAttributionTag;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +83,7 @@ public final class ContextParams {
|
||||
*/
|
||||
@Nullable
|
||||
public String getReceiverPackage() {
|
||||
return null;
|
||||
return mReceiverPackage;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,13 +91,33 @@ public final class ContextParams {
|
||||
*/
|
||||
@Nullable
|
||||
public String getReceiverAttributionTag() {
|
||||
return null;
|
||||
return mReceiverAttributionTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The set of permissions to treat as renounced.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
@SuppressLint("NullableCollection")
|
||||
@RequiresPermission(android.Manifest.permission.RENOUNCE_PERMISSIONS)
|
||||
public @Nullable Set<String> getRenouncedPermissions() {
|
||||
return mRenouncedPermissions;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public boolean isRenouncedPermission(@NonNull String permission) {
|
||||
return mRenouncedPermissions != null && mRenouncedPermissions.contains(permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for creating a {@link ContextParams}.
|
||||
*/
|
||||
public static final class Builder {
|
||||
private String mAttributionTag;
|
||||
private String mReceiverPackage;
|
||||
private String mReceiverAttributionTag;
|
||||
private Set<String> mRenouncedPermissions;
|
||||
|
||||
/**
|
||||
* Sets an attribution tag against which to track permission accesses.
|
||||
@@ -90,6 +127,7 @@ public final class ContextParams {
|
||||
*/
|
||||
@NonNull
|
||||
public Builder setAttributionTag(@NonNull String attributionTag) {
|
||||
mAttributionTag = Objects.requireNonNull(attributionTag);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -104,18 +142,46 @@ public final class ContextParams {
|
||||
@NonNull
|
||||
public Builder setReceiverPackage(@NonNull String packageName,
|
||||
@Nullable String attributionTag) {
|
||||
mReceiverPackage = Objects.requireNonNull(packageName);
|
||||
mReceiverAttributionTag = attributionTag;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance. You need to either specify an attribution tag
|
||||
* or a receiver package or both.
|
||||
* Sets permissions which have been voluntarily "renounced" by the
|
||||
* calling app.
|
||||
* <p>
|
||||
* Interactions performed through the created Context will ideally be
|
||||
* treated as if these "renounced" permissions have not actually been
|
||||
* granted to the app, regardless of their actual grant status.
|
||||
* <p>
|
||||
* This is designed for use by separate logical components within an app
|
||||
* which have no intention of interacting with data or services that are
|
||||
* protected by the renounced permissions.
|
||||
* <p>
|
||||
* Note that only {@link PermissionInfo#PROTECTION_DANGEROUS}
|
||||
* permissions are supported by this mechanism.
|
||||
*
|
||||
* @param renouncedPermissions The set of permissions to treat as
|
||||
* renounced.
|
||||
* @return This builder.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
@RequiresPermission(android.Manifest.permission.RENOUNCE_PERMISSIONS)
|
||||
public @NonNull Builder setRenouncedPermissions(@NonNull Set<String> renouncedPermissions) {
|
||||
mRenouncedPermissions = Collections.unmodifiableSet(renouncedPermissions);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @return The new instance.
|
||||
*/
|
||||
@NonNull
|
||||
public ContextParams build() {
|
||||
return new ContextParams();
|
||||
return new ContextParams(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +171,11 @@ public class ContextWrapper extends Context {
|
||||
return mBase.getAttributionTag();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ContextParams getParams() {
|
||||
return mBase.getParams();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApplicationInfo getApplicationInfo() {
|
||||
return mBase.getApplicationInfo();
|
||||
@@ -1044,6 +1049,12 @@ public class ContextWrapper extends Context {
|
||||
return mBase.createWindowContext(display, type, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Context createContext(@NonNull ContextParams contextParams) {
|
||||
return mBase.createContext(contextParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Context createAttributionContext(@Nullable String attributionTag) {
|
||||
return mBase.createAttributionContext(attributionTag);
|
||||
|
||||
@@ -5561,6 +5561,11 @@
|
||||
<permission android:name="android.permission.READ_PEOPLE_DATA"
|
||||
android:protectionLevel="signature|appPredictor|recents"/>
|
||||
|
||||
<!-- @hide @SystemApi Allows a logical component within an application to
|
||||
temporarily renounce a set of otherwise granted permissions. -->
|
||||
<permission android:name="android.permission.RENOUNCE_PERMISSIONS"
|
||||
android:protectionLevel="signature|privileged" />
|
||||
|
||||
<!-- Attribution for Geofencing service. -->
|
||||
<attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
|
||||
<!-- Attribution for Country Detector. -->
|
||||
|
||||
Reference in New Issue
Block a user