436 lines
19 KiB
Java
436 lines
19 KiB
Java
/*
|
|
* Copyright (C) 2020 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package android.telephony;
|
|
|
|
import android.Manifest;
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.annotation.UserIdInt;
|
|
import android.app.ActivityManager;
|
|
import android.app.AppOpsManager;
|
|
import android.content.Context;
|
|
import android.content.pm.PackageManager;
|
|
import android.location.LocationManager;
|
|
import android.os.Binder;
|
|
import android.os.Build;
|
|
import android.os.Process;
|
|
import android.os.UserHandle;
|
|
import android.util.Log;
|
|
import android.widget.Toast;
|
|
|
|
import com.android.internal.telephony.util.TelephonyUtils;
|
|
|
|
/**
|
|
* Helper for performing location access checks.
|
|
* @hide
|
|
*/
|
|
public final class LocationAccessPolicy {
|
|
private static final String TAG = "LocationAccessPolicy";
|
|
private static final boolean DBG = false;
|
|
public static final int MAX_SDK_FOR_ANY_ENFORCEMENT = Build.VERSION_CODES.CUR_DEVELOPMENT;
|
|
|
|
public enum LocationPermissionResult {
|
|
ALLOWED,
|
|
/**
|
|
* Indicates that the denial is due to a transient device state
|
|
* (e.g. app-ops, location main switch)
|
|
*/
|
|
DENIED_SOFT,
|
|
/**
|
|
* Indicates that the denial is due to a misconfigured app (e.g. missing entry in manifest)
|
|
*/
|
|
DENIED_HARD,
|
|
}
|
|
|
|
/** Data structure for location permission query */
|
|
public static class LocationPermissionQuery {
|
|
public final String callingPackage;
|
|
public final String callingFeatureId;
|
|
public final int callingUid;
|
|
public final int callingPid;
|
|
public final int minSdkVersionForCoarse;
|
|
public final int minSdkVersionForFine;
|
|
public final boolean logAsInfo;
|
|
public final String method;
|
|
|
|
private LocationPermissionQuery(String callingPackage, @Nullable String callingFeatureId,
|
|
int callingUid, int callingPid, int minSdkVersionForCoarse,
|
|
int minSdkVersionForFine, boolean logAsInfo, String method) {
|
|
this.callingPackage = callingPackage;
|
|
this.callingFeatureId = callingFeatureId;
|
|
this.callingUid = callingUid;
|
|
this.callingPid = callingPid;
|
|
this.minSdkVersionForCoarse = minSdkVersionForCoarse;
|
|
this.minSdkVersionForFine = minSdkVersionForFine;
|
|
this.logAsInfo = logAsInfo;
|
|
this.method = method;
|
|
}
|
|
|
|
/** Builder for LocationPermissionQuery */
|
|
public static class Builder {
|
|
private String mCallingPackage;
|
|
private String mCallingFeatureId;
|
|
private int mCallingUid;
|
|
private int mCallingPid;
|
|
private int mMinSdkVersionForCoarse = -1;
|
|
private int mMinSdkVersionForFine = -1;
|
|
private int mMinSdkVersionForEnforcement = -1;
|
|
private boolean mLogAsInfo = false;
|
|
private String mMethod;
|
|
|
|
/**
|
|
* Mandatory parameter, used for performing permission checks.
|
|
*/
|
|
public Builder setCallingPackage(String callingPackage) {
|
|
mCallingPackage = callingPackage;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Mandatory parameter, used for performing permission checks.
|
|
*/
|
|
public Builder setCallingFeatureId(@Nullable String callingFeatureId) {
|
|
mCallingFeatureId = callingFeatureId;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Mandatory parameter, used for performing permission checks.
|
|
*/
|
|
public Builder setCallingUid(int callingUid) {
|
|
mCallingUid = callingUid;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Mandatory parameter, used for performing permission checks.
|
|
*/
|
|
public Builder setCallingPid(int callingPid) {
|
|
mCallingPid = callingPid;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Apps that target at least this sdk version will be checked for coarse location
|
|
* permission. This method MUST be called before calling {@link #build()}. Otherwise, an
|
|
* {@link IllegalArgumentException} will be thrown.
|
|
*
|
|
* Additionally, if both the argument to this method and
|
|
* {@link #setMinSdkVersionForFine} are greater than {@link Build.VERSION_CODES#BASE},
|
|
* you must call {@link #setMinSdkVersionForEnforcement} with the min of the two to
|
|
* affirm that you do not want any location checks below a certain SDK version.
|
|
* Otherwise, {@link #build} will throw an {@link IllegalArgumentException}.
|
|
*/
|
|
public Builder setMinSdkVersionForCoarse(
|
|
int minSdkVersionForCoarse) {
|
|
mMinSdkVersionForCoarse = minSdkVersionForCoarse;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Apps that target at least this sdk version will be checked for fine location
|
|
* permission. This method MUST be called before calling {@link #build()}.
|
|
* Otherwise, an {@link IllegalArgumentException} will be thrown.
|
|
*
|
|
* Additionally, if both the argument to this method and
|
|
* {@link #setMinSdkVersionForCoarse} are greater than {@link Build.VERSION_CODES#BASE},
|
|
* you must call {@link #setMinSdkVersionForEnforcement} with the min of the two to
|
|
* affirm that you do not want any location checks below a certain SDK version.
|
|
* Otherwise, {@link #build} will throw an {@link IllegalArgumentException}.
|
|
*/
|
|
public Builder setMinSdkVersionForFine(
|
|
int minSdkVersionForFine) {
|
|
mMinSdkVersionForFine = minSdkVersionForFine;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* If both the argument to {@link #setMinSdkVersionForFine} and
|
|
* {@link #setMinSdkVersionForCoarse} are greater than {@link Build.VERSION_CODES#BASE},
|
|
* this method must be called with the min of the two to
|
|
* affirm that you do not want any location checks below a certain SDK version.
|
|
*/
|
|
public Builder setMinSdkVersionForEnforcement(int minSdkVersionForEnforcement) {
|
|
mMinSdkVersionForEnforcement = minSdkVersionForEnforcement;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Optional, for logging purposes only.
|
|
*/
|
|
public Builder setMethod(String method) {
|
|
mMethod = method;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* If called with {@code true}, log messages will only be printed at the info level.
|
|
*/
|
|
public Builder setLogAsInfo(boolean logAsInfo) {
|
|
mLogAsInfo = logAsInfo;
|
|
return this;
|
|
}
|
|
|
|
/** build LocationPermissionQuery */
|
|
public LocationPermissionQuery build() {
|
|
if (mMinSdkVersionForCoarse < 0 || mMinSdkVersionForFine < 0) {
|
|
throw new IllegalArgumentException("Must specify min sdk versions for"
|
|
+ " enforcement for both coarse and fine permissions");
|
|
}
|
|
if (mMinSdkVersionForFine > Build.VERSION_CODES.BASE
|
|
&& mMinSdkVersionForCoarse > Build.VERSION_CODES.BASE) {
|
|
if (mMinSdkVersionForEnforcement != Math.min(
|
|
mMinSdkVersionForCoarse, mMinSdkVersionForFine)) {
|
|
throw new IllegalArgumentException("setMinSdkVersionForEnforcement must be"
|
|
+ " called.");
|
|
}
|
|
}
|
|
|
|
if (mMinSdkVersionForFine < mMinSdkVersionForCoarse) {
|
|
throw new IllegalArgumentException("Since fine location permission includes"
|
|
+ " access to coarse location, the min sdk level for enforcement of"
|
|
+ " the fine location permission must not be less than the min sdk"
|
|
+ " level for enforcement of the coarse location permission.");
|
|
}
|
|
|
|
return new LocationPermissionQuery(mCallingPackage, mCallingFeatureId,
|
|
mCallingUid, mCallingPid, mMinSdkVersionForCoarse, mMinSdkVersionForFine,
|
|
mLogAsInfo, mMethod);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void logError(Context context, LocationPermissionQuery query, String errorMsg) {
|
|
if (query.logAsInfo) {
|
|
Log.i(TAG, errorMsg);
|
|
return;
|
|
}
|
|
Log.e(TAG, errorMsg);
|
|
try {
|
|
if (TelephonyUtils.IS_DEBUGGABLE) {
|
|
Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show();
|
|
}
|
|
} catch (Throwable t) {
|
|
// whatever, not important
|
|
}
|
|
}
|
|
|
|
private static LocationPermissionResult appOpsModeToPermissionResult(int appOpsMode) {
|
|
switch (appOpsMode) {
|
|
case AppOpsManager.MODE_ALLOWED:
|
|
return LocationPermissionResult.ALLOWED;
|
|
case AppOpsManager.MODE_ERRORED:
|
|
return LocationPermissionResult.DENIED_HARD;
|
|
default:
|
|
return LocationPermissionResult.DENIED_SOFT;
|
|
}
|
|
}
|
|
|
|
private static String getAppOpsString(String manifestPermission) {
|
|
switch (manifestPermission) {
|
|
case Manifest.permission.ACCESS_FINE_LOCATION:
|
|
return AppOpsManager.OPSTR_FINE_LOCATION;
|
|
case Manifest.permission.ACCESS_COARSE_LOCATION:
|
|
return AppOpsManager.OPSTR_COARSE_LOCATION;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static LocationPermissionResult checkAppLocationPermissionHelper(Context context,
|
|
LocationPermissionQuery query, String permissionToCheck) {
|
|
String locationTypeForLog =
|
|
Manifest.permission.ACCESS_FINE_LOCATION.equals(permissionToCheck)
|
|
? "fine" : "coarse";
|
|
|
|
// Do the app-ops and the manifest check without any of the allow-overrides first.
|
|
boolean hasManifestPermission = checkManifestPermission(context, query.callingPid,
|
|
query.callingUid, permissionToCheck);
|
|
|
|
if (hasManifestPermission) {
|
|
// Only check the app op if the app has the permission.
|
|
int appOpMode = context.getSystemService(AppOpsManager.class)
|
|
.noteOpNoThrow(getAppOpsString(permissionToCheck), query.callingUid,
|
|
query.callingPackage, query.callingFeatureId, null);
|
|
if (appOpMode == AppOpsManager.MODE_ALLOWED) {
|
|
// If the app did everything right, return without logging.
|
|
return LocationPermissionResult.ALLOWED;
|
|
} else {
|
|
// If the app has the manifest permission but not the app-op permission, it means
|
|
// that it's aware of the requirement and the user denied permission explicitly.
|
|
// If we see this, don't let any of the overrides happen.
|
|
Log.i(TAG, query.callingPackage + " is aware of " + locationTypeForLog + " but the"
|
|
+ " app-ops permission is specifically denied.");
|
|
return appOpsModeToPermissionResult(appOpMode);
|
|
}
|
|
}
|
|
|
|
int minSdkVersion = Manifest.permission.ACCESS_FINE_LOCATION.equals(permissionToCheck)
|
|
? query.minSdkVersionForFine : query.minSdkVersionForCoarse;
|
|
|
|
// If the app fails for some reason, see if it should be allowed to proceed.
|
|
if (minSdkVersion > MAX_SDK_FOR_ANY_ENFORCEMENT) {
|
|
String errorMsg = "Allowing " + query.callingPackage + " " + locationTypeForLog
|
|
+ " because we're not enforcing API " + minSdkVersion + " yet."
|
|
+ " Please fix this app because it will break in the future. Called from "
|
|
+ query.method;
|
|
logError(context, query, errorMsg);
|
|
return null;
|
|
} else if (!isAppAtLeastSdkVersion(context, query.callingPackage, minSdkVersion)) {
|
|
String errorMsg = "Allowing " + query.callingPackage + " " + locationTypeForLog
|
|
+ " because it doesn't target API " + minSdkVersion + " yet."
|
|
+ " Please fix this app. Called from " + query.method;
|
|
logError(context, query, errorMsg);
|
|
return null;
|
|
} else {
|
|
// If we're not allowing it due to the above two conditions, this means that the app
|
|
// did not declare the permission in their manifest.
|
|
return LocationPermissionResult.DENIED_HARD;
|
|
}
|
|
}
|
|
|
|
/** Check if location permissions have been granted */
|
|
public static LocationPermissionResult checkLocationPermission(
|
|
Context context, LocationPermissionQuery query) {
|
|
// Always allow the phone process, system server, and network stack to access location.
|
|
// This avoid breaking legacy code that rely on public-facing APIs to access cell location,
|
|
// and it doesn't create an info leak risk because the cell location is stored in the phone
|
|
// process anyway, and the system server already has location access.
|
|
if (query.callingUid == Process.PHONE_UID || query.callingUid == Process.SYSTEM_UID
|
|
|| query.callingUid == Process.NETWORK_STACK_UID
|
|
|| query.callingUid == Process.ROOT_UID) {
|
|
return LocationPermissionResult.ALLOWED;
|
|
}
|
|
|
|
// Check the system-wide requirements. If the location main switch is off and the caller is
|
|
// not in the allowlist of apps that always have loation access or the app's profile
|
|
// isn't in the foreground, return a soft denial.
|
|
if (!checkSystemLocationAccess(context, query.callingUid, query.callingPid,
|
|
query.callingPackage)) {
|
|
return LocationPermissionResult.DENIED_SOFT;
|
|
}
|
|
|
|
// Do the check for fine, then for coarse.
|
|
if (query.minSdkVersionForFine < Integer.MAX_VALUE) {
|
|
LocationPermissionResult resultForFine = checkAppLocationPermissionHelper(
|
|
context, query, Manifest.permission.ACCESS_FINE_LOCATION);
|
|
if (resultForFine != null) {
|
|
return resultForFine;
|
|
}
|
|
}
|
|
|
|
if (query.minSdkVersionForCoarse < Integer.MAX_VALUE) {
|
|
LocationPermissionResult resultForCoarse = checkAppLocationPermissionHelper(
|
|
context, query, Manifest.permission.ACCESS_COARSE_LOCATION);
|
|
if (resultForCoarse != null) {
|
|
return resultForCoarse;
|
|
}
|
|
}
|
|
|
|
// At this point, we're out of location checks to do. If the app bypassed all the previous
|
|
// ones due to the SDK backwards compatibility schemes, allow it access.
|
|
return LocationPermissionResult.ALLOWED;
|
|
}
|
|
|
|
private static boolean checkManifestPermission(Context context, int pid, int uid,
|
|
String permissionToCheck) {
|
|
return context.checkPermission(permissionToCheck, pid, uid)
|
|
== PackageManager.PERMISSION_GRANTED;
|
|
}
|
|
|
|
private static boolean checkSystemLocationAccess(@NonNull Context context, int uid, int pid,
|
|
@NonNull String callingPackage) {
|
|
if (!isLocationModeEnabled(context, UserHandle.getUserHandleForUid(uid).getIdentifier())
|
|
&& !isLocationBypassAllowed(context, callingPackage)) {
|
|
if (DBG) Log.w(TAG, "Location disabled, failed, (" + uid + ")");
|
|
return false;
|
|
}
|
|
// If the user or profile is current, permission is granted.
|
|
// Otherwise, uid must have INTERACT_ACROSS_USERS_FULL permission.
|
|
return isCurrentProfile(context, uid) || checkInteractAcrossUsersFull(context, pid, uid);
|
|
}
|
|
|
|
/**
|
|
* @return Whether location is enabled for the given user.
|
|
*/
|
|
public static boolean isLocationModeEnabled(@NonNull Context context, @UserIdInt int userId) {
|
|
LocationManager locationManager = context.getSystemService(LocationManager.class);
|
|
if (locationManager == null) {
|
|
Log.w(TAG, "Couldn't get location manager, denying location access");
|
|
return false;
|
|
}
|
|
return locationManager.isLocationEnabledForUser(UserHandle.of(userId));
|
|
}
|
|
|
|
private static boolean isLocationBypassAllowed(@NonNull Context context,
|
|
@NonNull String callingPackage) {
|
|
for (String bypassPackage : getLocationBypassPackages(context)) {
|
|
if (callingPackage.equals(bypassPackage)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return An array of packages that are always allowed to access location.
|
|
*/
|
|
public static @NonNull String[] getLocationBypassPackages(@NonNull Context context) {
|
|
return context.getResources().getStringArray(
|
|
com.android.internal.R.array.config_serviceStateLocationAllowedPackages);
|
|
}
|
|
|
|
private static boolean checkInteractAcrossUsersFull(
|
|
@NonNull Context context, int pid, int uid) {
|
|
return checkManifestPermission(context, pid, uid,
|
|
Manifest.permission.INTERACT_ACROSS_USERS_FULL);
|
|
}
|
|
|
|
private static boolean isCurrentProfile(@NonNull Context context, int uid) {
|
|
final long token = Binder.clearCallingIdentity();
|
|
try {
|
|
if (UserHandle.getUserHandleForUid(uid).getIdentifier()
|
|
== ActivityManager.getCurrentUser()) {
|
|
return true;
|
|
}
|
|
ActivityManager activityManager = context.getSystemService(ActivityManager.class);
|
|
if (activityManager != null) {
|
|
return activityManager.isProfileForeground(
|
|
UserHandle.getUserHandleForUid(ActivityManager.getCurrentUser()));
|
|
} else {
|
|
return false;
|
|
}
|
|
} finally {
|
|
Binder.restoreCallingIdentity(token);
|
|
}
|
|
}
|
|
|
|
private static boolean isAppAtLeastSdkVersion(Context context, String pkgName, int sdkVersion) {
|
|
try {
|
|
if (context.getPackageManager().getApplicationInfo(pkgName, 0).targetSdkVersion
|
|
>= sdkVersion) {
|
|
return true;
|
|
}
|
|
} catch (PackageManager.NameNotFoundException e) {
|
|
// In case of exception, assume known app (more strict checking)
|
|
// Note: This case will never happen since checkPackage is
|
|
// called to verify validity before checking app's version.
|
|
}
|
|
return false;
|
|
}
|
|
}
|