/*
 * 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 com.android.server.location;

import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.Manifest.permission.LOCATION_BYPASS;
import static android.Manifest.permission.READ_LOCATION_BYPASS_ALLOWLIST;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;

import android.annotation.IntDef;
import android.annotation.RequiresPermission;
import android.app.AppOpsManager;
import android.content.Context;
import android.os.Binder;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/** Utility class for dealing with location permissions. */
public final class LocationPermissions {

    /**
     * Indicates no location permissions are present, or no location permission are required.
     */
    public static final int PERMISSION_NONE = 0;

    /**
     * Indicates the coarse location permission is present, or either the coarse or fine permissions
     * are required.
     */
    public static final int PERMISSION_COARSE = 1;

    /**
     * Indicates the fine location permission is present, or the fine location permission is
     * required.
     */
    public static final int PERMISSION_FINE = 2;

    @Target(ElementType.TYPE_USE)
    @IntDef({PERMISSION_NONE, PERMISSION_COARSE, PERMISSION_FINE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface PermissionLevel {}

    /**
     * Converts the given permission level to the corresponding permission.
     */
    public static String asPermission(@PermissionLevel int permissionLevel) {
        switch (permissionLevel) {
            case PERMISSION_COARSE:
                return ACCESS_COARSE_LOCATION;
            case PERMISSION_FINE:
                return ACCESS_FINE_LOCATION;
            default:
                throw new IllegalArgumentException();
        }
    }

    /**
     * Converts the given permission level to the corresponding appop.
     */
    public static int asAppOp(@PermissionLevel int permissionLevel) {
        switch (permissionLevel) {
            case PERMISSION_COARSE:
                return AppOpsManager.OP_COARSE_LOCATION;
            case PERMISSION_FINE:
                return AppOpsManager.OP_FINE_LOCATION;
            default:
                throw new IllegalArgumentException();
        }
    }

    /**
     * Throws a security exception if the caller does not hold the required location permissions.
     */
    public static void enforceCallingOrSelfLocationPermission(Context context,
            @PermissionLevel int requiredPermissionLevel) {
        enforceLocationPermission(Binder.getCallingUid(),
                getPermissionLevel(context, Binder.getCallingUid(), Binder.getCallingPid()),
                requiredPermissionLevel);
    }

    /**
     * Throws a security exception if the given uid/pid does not hold the required location
     * permissions.
     */
    public static void enforceLocationPermission(Context context, int uid, int pid,
            @PermissionLevel int requiredPermissionLevel) {
        enforceLocationPermission(uid,
                getPermissionLevel(context, uid, pid),
                requiredPermissionLevel);
    }

    /**
     * Throws a security exception if the given permission level does not meet the required location
     * permission level.
     */
    public static void enforceLocationPermission(int uid, @PermissionLevel int permissionLevel,
            @PermissionLevel int requiredPermissionLevel) {
        if (checkLocationPermission(permissionLevel, requiredPermissionLevel)) {
            return;
        }

        if (requiredPermissionLevel == PERMISSION_COARSE) {
            throw new SecurityException("uid " + uid + " does not have " + ACCESS_COARSE_LOCATION
                    + " or " + ACCESS_FINE_LOCATION + ".");
        } else if (requiredPermissionLevel == PERMISSION_FINE) {
            throw new SecurityException("uid " + uid + " does not have " + ACCESS_FINE_LOCATION
                    + ".");
        }
    }

    /**
     * Throws a security exception if the caller does not hold the required bypass permissions.
     */
    public static void enforceCallingOrSelfBypassPermission(Context context) {
        enforceBypassPermission(context, Binder.getCallingUid(), Binder.getCallingPid());
    }

    /**
     * Throws a security exception if the given uid/pid does not hold the required bypass
     * perissions.
     */
    public static void enforceBypassPermission(Context context, int uid, int pid) {
        if (context.checkPermission(LOCATION_BYPASS, pid, uid) == PERMISSION_GRANTED) {
            return;
        }
        throw new SecurityException("uid" + uid + " does not have " + LOCATION_BYPASS
                + ".");
    }

    /**
     * Throws a security exception if the caller does not hold the required access bypass allowlist
     * permissions.
     */
    public static void enforceCallingOrSelfAccessBypassAllowlistPermission(Context context) {
        enforceAccessBypassAllowlistPermission(
                context, Binder.getCallingUid(), Binder.getCallingPid());
    }

    /**
     * Throws a security exception if the given uid/pid does not hold the required access bypass
     * allowlist permissions.
     */
    @RequiresPermission(READ_LOCATION_BYPASS_ALLOWLIST)
    public static void enforceAccessBypassAllowlistPermission(Context context, int uid, int pid) {
        if (context.checkPermission(READ_LOCATION_BYPASS_ALLOWLIST, pid, uid)
                == PERMISSION_GRANTED) {
            return;
        }
        throw new SecurityException(
                "uid" + uid + " does not have " + READ_LOCATION_BYPASS_ALLOWLIST + ".");
    }

    /**
     * Returns false if the caller does not hold the required location permissions.
     */
    public static boolean checkCallingOrSelfLocationPermission(Context context,
            @PermissionLevel int requiredPermissionLevel) {
        return checkLocationPermission(
                getCallingOrSelfPermissionLevel(context),
                requiredPermissionLevel);
    }

    /**
     * Returns false if the given uid/pid does not hold the required location permissions.
     */
    public static boolean checkLocationPermission(Context context, int uid, int pid,
            @PermissionLevel int requiredPermissionLevel) {
        return checkLocationPermission(
                getPermissionLevel(context, uid, pid),
                requiredPermissionLevel);
    }

    /**
     * Returns false if the given permission level does not meet the required location permission
     * level.
     */
    public static boolean checkLocationPermission(@PermissionLevel int permissionLevel,
            @PermissionLevel int requiredPermissionLevel) {
        return permissionLevel >= requiredPermissionLevel;
    }

    /**
     * Returns the permission level of the caller.
     */
    @PermissionLevel
    public static int getCallingOrSelfPermissionLevel(Context context) {
        return getPermissionLevel(context, Binder.getCallingUid(), Binder.getCallingPid());
    }

    /**
     * Returns the permission level of the given uid/pid.
     */
    @PermissionLevel
    public static int getPermissionLevel(Context context, int uid, int pid) {
        if (context.checkPermission(ACCESS_FINE_LOCATION, pid, uid) == PERMISSION_GRANTED) {
            return PERMISSION_FINE;
        }
        if (context.checkPermission(ACCESS_COARSE_LOCATION, pid, uid) == PERMISSION_GRANTED) {
            return PERMISSION_COARSE;
        }

        return PERMISSION_NONE;
    }

    private LocationPermissions() {}
}
