/*
 * Copyright (C) 2016 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.providers.blockednumber;

import static com.android.providers.blockednumber.Utils.piiHandle;

import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.app.backup.BackupManager;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.UriMatcher;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.BlockedNumberContract;
import android.provider.BlockedNumberContract.SystemContract;
import android.telecom.TelecomManager;
import android.telephony.CarrierConfigManager;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;

import com.android.common.content.ProjectionMap;
import com.android.internal.annotations.VisibleForTesting;
import com.android.providers.blockednumber.BlockedNumberDatabaseHelper.Tables;

import java.util.Arrays;

/**
 * Blocked phone number provider.
 *
 * <p>Note the provider allows emergency numbers.  The caller (telecom) should never call it with
 * emergency numbers.
 */
public class BlockedNumberProvider extends ContentProvider {
    static final String TAG = "BlockedNumbers";

    private static final boolean DEBUG = false; // DO NOT SUBMIT WITH TRUE.

    private static final int BLOCKED_LIST = 1000;
    private static final int BLOCKED_ID = 1001;

    private static final UriMatcher sUriMatcher;

    private static final String PREF_FILE = "block_number_provider_prefs";
    private static final String BLOCK_SUPPRESSION_EXPIRY_TIME_PREF =
            "block_suppression_expiry_time_pref";
    private static final int MAX_BLOCKING_DISABLED_DURATION_SECONDS = 7 * 24 * 3600; // 1 week
    private static final long BLOCKING_DISABLED_FOREVER = -1;
    // Normally, we allow calls from self, *except* in unit tests, where we clear this flag
    // to emulate calls from other apps.
    @VisibleForTesting
    static boolean ALLOW_SELF_CALL = true;

    static {
        sUriMatcher = new UriMatcher(0);
        sUriMatcher.addURI(BlockedNumberContract.AUTHORITY, "blocked", BLOCKED_LIST);
        sUriMatcher.addURI(BlockedNumberContract.AUTHORITY, "blocked/#", BLOCKED_ID);
    }

    private static final ProjectionMap sBlockedNumberColumns = ProjectionMap.builder()
            .add(BlockedNumberContract.BlockedNumbers.COLUMN_ID)
            .add(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER)
            .add(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER)
            .build();

    private static final String ID_SELECTION =
            BlockedNumberContract.BlockedNumbers.COLUMN_ID + "=?";

    private static final String ORIGINAL_NUMBER_SELECTION =
            BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + "=?";

    private static final String E164_NUMBER_SELECTION =
            BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER + "=?";

    @VisibleForTesting
    protected BlockedNumberDatabaseHelper mDbHelper;
    @VisibleForTesting
    protected BackupManager mBackupManager;

    @Override
    public boolean onCreate() {
        mDbHelper = BlockedNumberDatabaseHelper.getInstance(getContext());
        mBackupManager = new BackupManager(getContext());
        return true;
    }

    @Override
    public String getType(@NonNull Uri uri) {
        final int match = sUriMatcher.match(uri);
        switch (match) {
            case BLOCKED_LIST:
                return BlockedNumberContract.BlockedNumbers.CONTENT_TYPE;
            case BLOCKED_ID:
                return BlockedNumberContract.BlockedNumbers.CONTENT_ITEM_TYPE;
            default:
                throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
    }

    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        enforceWritePermissionAndPrimaryUser();

        final int match = sUriMatcher.match(uri);
        switch (match) {
            case BLOCKED_LIST:
                Uri blockedUri = insertBlockedNumber(values);
                getContext().getContentResolver().notifyChange(blockedUri, null);
                mBackupManager.dataChanged();
                return blockedUri;
            default:
                throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
    }

    /**
     * Implements the "blocked/" insert.
     */
    private Uri insertBlockedNumber(ContentValues cv) {
        throwIfSpecified(cv, BlockedNumberContract.BlockedNumbers.COLUMN_ID);

        final String phoneNumber = cv.getAsString(
                BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER);

        if (TextUtils.isEmpty(phoneNumber)) {
            throw new IllegalArgumentException("Missing a required column " +
                    BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER);
        }

        // Fill in with autogenerated columns.
        final String e164Number = Utils.getE164Number(getContext(), phoneNumber,
                cv.getAsString(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER));
        cv.put(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER, e164Number);

        if (DEBUG) {
            Log.d(TAG, String.format("inserted blocked number: %s", cv));
        }

        // Then insert.
        final long id = mDbHelper.getWritableDatabase().insertWithOnConflict(
                BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS, null, cv,
                SQLiteDatabase.CONFLICT_REPLACE);

        return ContentUris.withAppendedId(BlockedNumberContract.BlockedNumbers.CONTENT_URI, id);
    }

    private static void throwIfSpecified(ContentValues cv, String column) {
        if (cv.containsKey(column)) {
            throw new IllegalArgumentException("Column " + column + " must not be specified");
        }
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection,
            @Nullable String[] selectionArgs) {
        enforceWritePermissionAndPrimaryUser();

        throw new UnsupportedOperationException(
                "Update is not supported.  Use delete + insert instead");
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection,
            @Nullable String[] selectionArgs) {
        enforceWritePermissionAndPrimaryUser();

        final int match = sUriMatcher.match(uri);
        int numRows;
        switch (match) {
            case BLOCKED_LIST:
                numRows = deleteBlockedNumber(selection, selectionArgs);
                break;
            case BLOCKED_ID:
                numRows = deleteBlockedNumberWithId(ContentUris.parseId(uri), selection);
                break;
            default:
                throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
        getContext().getContentResolver().notifyChange(uri, null);
        mBackupManager.dataChanged();
        return numRows;
    }

    /**
     * Implements the "blocked/#" delete.
     */
    private int deleteBlockedNumberWithId(long id, String selection) {
        throwForNonEmptySelection(selection);

        return deleteBlockedNumber(ID_SELECTION, new String[]{Long.toString(id)});
    }

    /**
     * Implements the "blocked/" delete.
     */
    private int deleteBlockedNumber(String selection, String[] selectionArgs) {
        final SQLiteDatabase db = mDbHelper.getWritableDatabase();

        // When selection is specified, compile it within (...) to detect SQL injection.
        if (!TextUtils.isEmpty(selection)) {
            db.validateSql("select 1 FROM " + Tables.BLOCKED_NUMBERS + " WHERE " +
                    Utils.wrapSelectionWithParens(selection),
                    /* cancellationSignal =*/ null);
        }

        return db.delete(
                BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS,
                selection, selectionArgs);
    }

    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
            @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        enforceReadPermissionAndPrimaryUser();

        return query(uri, projection, selection, selectionArgs, sortOrder, null);
    }

    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
            @Nullable String[] selectionArgs, @Nullable String sortOrder,
            @Nullable CancellationSignal cancellationSignal) {
        enforceReadPermissionAndPrimaryUser();

        final int match = sUriMatcher.match(uri);
        Cursor cursor;
        switch (match) {
            case BLOCKED_LIST:
                cursor = queryBlockedList(projection, selection, selectionArgs, sortOrder,
                        cancellationSignal);
                break;
            case BLOCKED_ID:
                cursor = queryBlockedListWithId(ContentUris.parseId(uri), projection, selection,
                        cancellationSignal);
                break;
            default:
                throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
        // Tell the cursor what uri to watch, so it knows when its source data changes
        cursor.setNotificationUri(getContext().getContentResolver(), uri);
        return cursor;
    }

    /**
     * Implements the "blocked/#" query.
     */
    private Cursor queryBlockedListWithId(long id, String[] projection, String selection,
            CancellationSignal cancellationSignal) {
        throwForNonEmptySelection(selection);

        return queryBlockedList(projection, ID_SELECTION, new String[]{Long.toString(id)},
                null, cancellationSignal);
    }

    /**
     * Implements the "blocked/" query.
     */
    private Cursor queryBlockedList(String[] projection, String selection, String[] selectionArgs,
            String sortOrder, CancellationSignal cancellationSignal) {
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        qb.setStrict(true);
        qb.setTables(BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS);
        qb.setProjectionMap(sBlockedNumberColumns);

        return qb.query(mDbHelper.getReadableDatabase(), projection, selection, selectionArgs,
                /* groupBy =*/ null, /* having =*/null, sortOrder,
                /* limit =*/ null, cancellationSignal);
    }

    private void throwForNonEmptySelection(String selection) {
        if (!TextUtils.isEmpty(selection)) {
            throw new IllegalArgumentException(
                    "When ID is specified in URI, selection must be null");
        }
    }

    @Override
    public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
        final Bundle res = new Bundle();
        switch (method) {
            case BlockedNumberContract.METHOD_IS_BLOCKED:
                enforceReadPermissionAndPrimaryUser();
                boolean isBlocked = isBlocked(arg);
                res.putBoolean(BlockedNumberContract.RES_NUMBER_IS_BLOCKED, isBlocked);
                res.putInt(BlockedNumberContract.RES_BLOCK_STATUS,
                        isBlocked ? BlockedNumberContract.STATUS_BLOCKED_IN_LIST
                                : BlockedNumberContract.STATUS_NOT_BLOCKED);
                break;
            case BlockedNumberContract.METHOD_CAN_CURRENT_USER_BLOCK_NUMBERS:
                // No permission checks: any app should be able to access this API.
                res.putBoolean(
                        BlockedNumberContract.RES_CAN_BLOCK_NUMBERS, canCurrentUserBlockUsers());
                break;
            case BlockedNumberContract.METHOD_UNBLOCK:
                enforceWritePermissionAndPrimaryUser();

                res.putInt(BlockedNumberContract.RES_NUM_ROWS_DELETED, unblock(arg));
                break;
            case SystemContract.METHOD_NOTIFY_EMERGENCY_CONTACT:
                enforceSystemWritePermissionAndPrimaryUser();

                notifyEmergencyContact();
                break;
            case SystemContract.METHOD_END_BLOCK_SUPPRESSION:
                enforceSystemWritePermissionAndPrimaryUser();

                endBlockSuppression();
                break;
            case SystemContract.METHOD_GET_BLOCK_SUPPRESSION_STATUS:
                enforceSystemReadPermissionAndPrimaryUser();

                SystemContract.BlockSuppressionStatus status = getBlockSuppressionStatus();
                res.putBoolean(SystemContract.RES_IS_BLOCKING_SUPPRESSED, status.isSuppressed);
                res.putLong(SystemContract.RES_BLOCKING_SUPPRESSED_UNTIL_TIMESTAMP,
                        status.untilTimestampMillis);
                break;
            case SystemContract.METHOD_SHOULD_SYSTEM_BLOCK_NUMBER:
                enforceSystemReadPermissionAndPrimaryUser();
                int blockReason = shouldSystemBlockNumber(arg, extras);
                res.putBoolean(BlockedNumberContract.RES_NUMBER_IS_BLOCKED,
                        blockReason != BlockedNumberContract.STATUS_NOT_BLOCKED);
                res.putInt(BlockedNumberContract.RES_BLOCK_STATUS, blockReason);
                break;
            case SystemContract.METHOD_SHOULD_SHOW_EMERGENCY_CALL_NOTIFICATION:
                enforceSystemReadPermissionAndPrimaryUser();
                res.putBoolean(BlockedNumberContract.RES_SHOW_EMERGENCY_CALL_NOTIFICATION,
                        shouldShowEmergencyCallNotification());
                break;
            case SystemContract.METHOD_GET_ENHANCED_BLOCK_SETTING:
                enforceSystemReadPermissionAndPrimaryUser();
                if (extras != null) {
                    String key = extras.getString(BlockedNumberContract.EXTRA_ENHANCED_SETTING_KEY);
                    boolean value = getEnhancedBlockSetting(key);
                    res.putBoolean(BlockedNumberContract.RES_ENHANCED_SETTING_IS_ENABLED, value);
                }
                break;
            case SystemContract.METHOD_SET_ENHANCED_BLOCK_SETTING:
                enforceSystemWritePermissionAndPrimaryUser();
                if (extras != null) {
                    String key = extras.getString(BlockedNumberContract.EXTRA_ENHANCED_SETTING_KEY);
                    boolean value = extras.getBoolean(
                            BlockedNumberContract.EXTRA_ENHANCED_SETTING_VALUE, false);
                    setEnhancedBlockSetting(key, value);
                }
                break;
            default:
                enforceReadPermissionAndPrimaryUser();

                throw new IllegalArgumentException("Unsupported method " + method);
        }
        return res;
    }

    private int unblock(String phoneNumber) {
        if (TextUtils.isEmpty(phoneNumber)) {
            return 0;
        }

        StringBuilder selectionBuilder = new StringBuilder(ORIGINAL_NUMBER_SELECTION);
        String[] selectionArgs = new String[]{phoneNumber};
        final String e164Number = Utils.getE164Number(getContext(), phoneNumber, null);
        if (!TextUtils.isEmpty(e164Number)) {
            selectionBuilder.append(" or " + E164_NUMBER_SELECTION);
            selectionArgs = new String[]{phoneNumber, e164Number};
        }
        String selection = selectionBuilder.toString();
        if (DEBUG) {
            Log.d(TAG, String.format("Unblocking numbers using selection: %s, args: %s",
                    selection, Arrays.toString(selectionArgs)));
        }
        return deleteBlockedNumber(selection, selectionArgs);
    }

    private boolean isEmergencyNumber(String phoneNumber) {
        if (TextUtils.isEmpty(phoneNumber)) {
            return false;
        }

        final String e164Number = Utils.getE164Number(getContext(), phoneNumber, null);
        return PhoneNumberUtils.isEmergencyNumber(phoneNumber)
                || PhoneNumberUtils.isEmergencyNumber(e164Number);
    }

    private boolean isBlocked(String phoneNumber) {
        if (TextUtils.isEmpty(phoneNumber)) {
            Log.i(TAG, "isBlocked: NOT BLOCKED; empty #");
            return false;
        }

        final String inE164 = Utils.getE164Number(getContext(), phoneNumber, null); // may be empty.

        final Cursor c = mDbHelper.getReadableDatabase().rawQuery(
                "SELECT " +
                BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + "," +
                BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER +
                " FROM " + BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS +
                " WHERE " + BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + "=?1" +
                " OR (?2 != '' AND " +
                        BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER + "=?2)",
                new String[] {phoneNumber, inE164}
                );
        try {
            while (c.moveToNext()) {
                final String original = c.getString(0);
                final String e164 = c.getString(1);
                Log.i(TAG, String.format("isBlocked: BLOCKED; number=%s, e164=%s, foundOrig=%s, "
                                + "foundE164=%s",
                        piiHandle(phoneNumber),
                        piiHandle(inE164),
                        piiHandle(original),
                        piiHandle(e164)));
                return true;
            }
        } finally {
            c.close();
        }
        // No match found.
        Log.i(TAG, String.format("isBlocked: NOT BLOCKED; number=%s, e164=%s",
                piiHandle(phoneNumber), piiHandle(inE164)));
        return false;
    }

    private boolean canCurrentUserBlockUsers() {
        return getContext().getUserId() == UserHandle.USER_SYSTEM;
    }

    private void notifyEmergencyContact() {
        long sec = getBlockSuppressSecondsFromCarrierConfig();
        long millisToWrite = sec < 0
                ? BLOCKING_DISABLED_FOREVER : System.currentTimeMillis() + (sec * 1000);
        writeBlockSuppressionExpiryTimePref(millisToWrite);
        writeEmergencyCallNotificationPref(true);
        notifyBlockSuppressionStateChange();
    }

    private void endBlockSuppression() {
        // Nothing to do if blocks are not being suppressed.
        if (getBlockSuppressionStatus().isSuppressed) {
            writeBlockSuppressionExpiryTimePref(0);
            writeEmergencyCallNotificationPref(false);
            notifyBlockSuppressionStateChange();
        }
    }

    private SystemContract.BlockSuppressionStatus getBlockSuppressionStatus() {
        SharedPreferences pref = getContext().getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE);
        long blockSuppressionExpiryTimeMillis = pref.getLong(BLOCK_SUPPRESSION_EXPIRY_TIME_PREF, 0);
        boolean isSuppressed = blockSuppressionExpiryTimeMillis == BLOCKING_DISABLED_FOREVER
                || System.currentTimeMillis() < blockSuppressionExpiryTimeMillis;
        return new SystemContract.BlockSuppressionStatus(isSuppressed,
                blockSuppressionExpiryTimeMillis);
    }

    private int shouldSystemBlockNumber(String phoneNumber, Bundle extras) {
        if (getBlockSuppressionStatus().isSuppressed) {
            return BlockedNumberContract.STATUS_NOT_BLOCKED;
        }
        if (isEmergencyNumber(phoneNumber)) {
            return BlockedNumberContract.STATUS_NOT_BLOCKED;
        }

        boolean isBlocked = false;
        int blockReason = BlockedNumberContract.STATUS_NOT_BLOCKED;
        if (extras != null && !extras.isEmpty()) {
            // check enhanced blocking setting
            boolean contactExist = extras.getBoolean(BlockedNumberContract.EXTRA_CONTACT_EXIST);
            int presentation = extras.getInt(BlockedNumberContract.EXTRA_CALL_PRESENTATION);
            switch (presentation) {
                case TelecomManager.PRESENTATION_ALLOWED:
                    if (getEnhancedBlockSetting(
                            SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED)
                                    && !contactExist) {
                        blockReason = BlockedNumberContract.STATUS_BLOCKED_NOT_IN_CONTACTS;
                    }
                    break;
                case TelecomManager.PRESENTATION_RESTRICTED:
                    if (getEnhancedBlockSetting(
                            SystemContract.ENHANCED_SETTING_KEY_BLOCK_PRIVATE)) {
                        blockReason = BlockedNumberContract.STATUS_BLOCKED_RESTRICTED;
                    }
                    break;
                case TelecomManager.PRESENTATION_PAYPHONE:
                    if (getEnhancedBlockSetting(
                            SystemContract.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE)) {
                        blockReason = BlockedNumberContract.STATUS_BLOCKED_PAYPHONE;
                    }
                    break;
                case TelecomManager.PRESENTATION_UNKNOWN:
                    if (getEnhancedBlockSetting(
                            SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN)) {
                        blockReason = BlockedNumberContract.STATUS_BLOCKED_UNKNOWN_NUMBER;
                    }
                    break;
                default:
                    break;
            }
        }
        if (blockReason == BlockedNumberContract.STATUS_NOT_BLOCKED && isBlocked(phoneNumber)) {
            blockReason = BlockedNumberContract.STATUS_BLOCKED_IN_LIST;
        }
        return blockReason;
    }

    private boolean shouldShowEmergencyCallNotification() {
        return isEnhancedCallBlockingEnabledByPlatform()
                && (isShowCallBlockingDisabledNotificationAlways()
                        || isAnyEnhancedBlockingSettingEnabled())
                && getBlockSuppressionStatus().isSuppressed
                && getEnhancedBlockSetting(
                        SystemContract.ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION);
    }

    private PersistableBundle getCarrierConfig() {
        CarrierConfigManager configManager = (CarrierConfigManager) getContext().getSystemService(
                Context.CARRIER_CONFIG_SERVICE);
        PersistableBundle carrierConfig = configManager.getConfig();
        if (carrierConfig == null) {
            carrierConfig = configManager.getDefaultConfig();
        }
        return carrierConfig;
    }

    private boolean isEnhancedCallBlockingEnabledByPlatform() {
        return getCarrierConfig().getBoolean(
                CarrierConfigManager.KEY_SUPPORT_ENHANCED_CALL_BLOCKING_BOOL);
    }

    private boolean isShowCallBlockingDisabledNotificationAlways() {
        return getCarrierConfig().getBoolean(
                CarrierConfigManager.KEY_SHOW_CALL_BLOCKING_DISABLED_NOTIFICATION_ALWAYS_BOOL);
    }

    private boolean isAnyEnhancedBlockingSettingEnabled() {
        return getEnhancedBlockSetting(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED)
                || getEnhancedBlockSetting(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PRIVATE)
                || getEnhancedBlockSetting(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE)
                || getEnhancedBlockSetting(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
    }

    private boolean getEnhancedBlockSetting(String key) {
        SharedPreferences pref = getContext().getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE);
        return pref.getBoolean(key, false);
    }

    private void setEnhancedBlockSetting(String key, boolean value) {
        SharedPreferences pref = getContext().getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = pref.edit();
        editor.putBoolean(key, value);
        editor.apply();
    }

    private void writeEmergencyCallNotificationPref(boolean show) {
        if (!isEnhancedCallBlockingEnabledByPlatform()) {
            return;
        }
        setEnhancedBlockSetting(
                SystemContract.ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION, show);
    }

    private void writeBlockSuppressionExpiryTimePref(long expiryTimeMillis) {
        SharedPreferences pref = getContext().getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = pref.edit();
        editor.putLong(BLOCK_SUPPRESSION_EXPIRY_TIME_PREF, expiryTimeMillis);
        editor.apply();
    }

    private long getBlockSuppressSecondsFromCarrierConfig() {
        CarrierConfigManager carrierConfigManager =
                getContext().getSystemService(CarrierConfigManager.class);
        int carrierConfigValue = carrierConfigManager.getConfig().getInt
                (CarrierConfigManager.KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT);
        boolean isValidValue = carrierConfigValue <= MAX_BLOCKING_DISABLED_DURATION_SECONDS;
        return isValidValue ? carrierConfigValue : CarrierConfigManager.getDefaultConfig().getInt(
                CarrierConfigManager.KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT);
    }

    /**
     * Returns {@code false} when the caller is not root, the user selected dialer, the
     * default SMS app or a carrier app.
     */
    private boolean checkForPrivilegedApplications() {
        if (Binder.getCallingUid() == Process.ROOT_UID) {
            return true;
        }

        final String callingPackage = getCallingPackage();
        if (TextUtils.isEmpty(callingPackage)) {
            Log.w(TAG, "callingPackage not accessible");
        } else {
            final TelecomManager telecom = getContext().getSystemService(TelecomManager.class);

            if (callingPackage.equals(telecom.getDefaultDialerPackage())
                    || callingPackage.equals(telecom.getSystemDialerPackage())) {
                return true;
            }
            final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
            if (appOps.noteOp(AppOpsManager.OP_WRITE_SMS,
                    Binder.getCallingUid(), callingPackage) == AppOpsManager.MODE_ALLOWED) {
                return true;
            }

            final TelephonyManager telephonyManager =
                    getContext().getSystemService(TelephonyManager.class);
            return telephonyManager.checkCarrierPrivilegesForPackageAnyPhone(callingPackage) ==
                    TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
        }
        return false;
    }

    private void notifyBlockSuppressionStateChange() {
        Intent intent = new Intent(SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
        getContext().sendBroadcast(intent, Manifest.permission.READ_BLOCKED_NUMBERS);
    }

    private void enforceReadPermission() {
        checkForPermission(android.Manifest.permission.READ_BLOCKED_NUMBERS);
    }

    private void enforceReadPermissionAndPrimaryUser() {
        checkForPermissionAndPrimaryUser(android.Manifest.permission.READ_BLOCKED_NUMBERS);
    }

    private void enforceWritePermissionAndPrimaryUser() {
        checkForPermissionAndPrimaryUser(android.Manifest.permission.WRITE_BLOCKED_NUMBERS);
    }

    private void checkForPermissionAndPrimaryUser(String permission) {
        checkForPermission(permission);
        if (!canCurrentUserBlockUsers()) {
            throwCurrentUserNotPermittedSecurityException();
        }
    }

    private void checkForPermission(String permission) {
        boolean permitted = passesSystemPermissionCheck(permission)
                || checkForPrivilegedApplications() || isSelf();
        if (!permitted) {
            throwSecurityException();
        }
    }

    private void enforceSystemReadPermissionAndPrimaryUser() {
        enforceSystemPermissionAndUser(android.Manifest.permission.READ_BLOCKED_NUMBERS);
    }

    private void enforceSystemWritePermissionAndPrimaryUser() {
        enforceSystemPermissionAndUser(android.Manifest.permission.WRITE_BLOCKED_NUMBERS);
    }

    private void enforceSystemPermissionAndUser(String permission) {
        if (!canCurrentUserBlockUsers()) {
            throwCurrentUserNotPermittedSecurityException();
        }

        if (!passesSystemPermissionCheck(permission)) {
            throwSecurityException();
        }
    }

    private boolean passesSystemPermissionCheck(String permission) {
        return getContext().checkCallingPermission(permission)
                == PackageManager.PERMISSION_GRANTED;
    }

    private boolean isSelf() {
        return ALLOW_SELF_CALL && Binder.getCallingPid() == Process.myPid();
    }

    private void throwSecurityException() {
        throw new SecurityException("Caller must be system, default dialer or default SMS app");
    }

    private void throwCurrentUserNotPermittedSecurityException() {
        throw new SecurityException("The current user cannot perform this operation");
    }
}
