| /* |
| * 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 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 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 = 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 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. 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, 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 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.getUserHandleForUid(uid).getIdentifier())) { |
| 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 { |
| 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; |
| } |
| } |