summaryrefslogtreecommitdiff
path: root/telephony/common/android
diff options
context:
space:
mode:
authorPeter Wang <tpwang@google.com>2020-01-06 16:26:05 -0800
committerPeter Wang <tpwang@google.com>2020-01-07 02:44:34 -0800
commitd10972957fd350b4a13b0af21fdc1ca0cf4c0c2e (patch)
tree6bd64987660a32d71fe8c90cc93064869948a5ed /telephony/common/android
parent3f51e53b12aac32ff67810eaa77293248ca99643 (diff)
[Telephony Mainline] Move CarrierAppUtils and LocationAccessPolicy to telephony common
Bug: 146904426 Test: Build Change-Id: I6f6d24fd0adab803899b25a47199839bcbb09eb0 Merged-In: I6f6d24fd0adab803899b25a47199839bcbb09eb0
Diffstat (limited to 'telephony/common/android')
-rw-r--r--telephony/common/android/telephony/LocationAccessPolicy.java348
1 files changed, 348 insertions, 0 deletions
diff --git a/telephony/common/android/telephony/LocationAccessPolicy.java b/telephony/common/android/telephony/LocationAccessPolicy.java
new file mode 100644
index 000000000000..825ce2fc1a00
--- /dev/null
+++ b/telephony/common/android/telephony/LocationAccessPolicy.java
@@ -0,0 +1,348 @@
+/*
+ * 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.UserIdInt;
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
+import android.location.LocationManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.internal.telephony.util.TelephonyUtils;
+
+import java.util.List;
+
+/**
+ * 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 master 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 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, int callingUid, int callingPid,
+ int minSdkVersionForCoarse, int minSdkVersionForFine, boolean logAsInfo,
+ String method) {
+ this.callingPackage = callingPackage;
+ 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 int mCallingUid;
+ private int mCallingPid;
+ private int mMinSdkVersionForCoarse = Integer.MAX_VALUE;
+ private int mMinSdkVersionForFine = Integer.MAX_VALUE;
+ 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 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. Defaults to INT_MAX (which means don't check)
+ */
+ public Builder setMinSdkVersionForCoarse(
+ int minSdkVersionForCoarse) {
+ mMinSdkVersionForCoarse = minSdkVersionForCoarse;
+ return this;
+ }
+
+ /**
+ * Apps that target at least this sdk version will be checked for fine location
+ * permission. Defaults to INT_MAX (which means don't check)
+ */
+ public Builder setMinSdkVersionForFine(
+ int minSdkVersionForFine) {
+ mMinSdkVersionForFine = minSdkVersionForFine;
+ 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() {
+ return new LocationPermissionQuery(mCallingPackage, 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 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(AppOpsManager.permissionToOpCode(permissionToCheck),
+ query.callingUid, query.callingPackage);
+ 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 and system server 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.ROOT_UID) {
+ return LocationPermissionResult.ALLOWED;
+ }
+
+ // Check the system-wide requirements. If the location master switch is off or
+ // the app's profile isn't in foreground, return a soft denial.
+ if (!checkSystemLocationAccess(context, query.callingUid, query.callingPid)) {
+ 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 grandfathering 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) {
+ if (!isLocationModeEnabled(context, UserHandle.getUserId(uid))) {
+ 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, uid, pid);
+ }
+
+ private 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 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) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ final int currentUser = ActivityManager.getCurrentUser();
+ final int callingUserId = UserHandle.getUserId(uid);
+ if (callingUserId == currentUser) {
+ return true;
+ } else {
+ List<UserInfo> userProfiles = context.getSystemService(
+ UserManager.class).getProfiles(currentUser);
+ for (UserInfo user : userProfiles) {
+ if (user.id == callingUserId) {
+ return true;
+ }
+ }
+ }
+ 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;
+ }
+}