/*
 * Copyright (C) 2019 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.car.settings.bluetooth;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.admin.DevicePolicyManager;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;

import androidx.activity.ComponentActivity;
import androidx.annotation.VisibleForTesting;

import com.android.car.settings.R;
import com.android.car.settings.common.Logger;
import com.android.car.ui.AlertDialogBuilder;
import com.android.settingslib.bluetooth.BluetoothDiscoverableTimeoutReceiver;
import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin;

import java.util.List;

/**
 * This {@link Activity} handles requests to toggle Bluetooth by collecting user
 * consent and waiting until the state change is completed. It can also be used to make the device
 * explicitly discoverable for a given amount of time.
 */
public class BluetoothRequestPermissionActivity extends ComponentActivity {
    private static final Logger LOG = new Logger(BluetoothRequestPermissionActivity.class);

    @VisibleForTesting
    static final int REQUEST_UNKNOWN = 0;
    @VisibleForTesting
    static final int REQUEST_ENABLE = 1;
    @VisibleForTesting
    static final int REQUEST_DISABLE = 2;
    @VisibleForTesting
    static final int REQUEST_ENABLE_DISCOVERABLE = 3;

    private static final int DISCOVERABLE_TIMEOUT_TWO_MINUTES = 120;
    private static final int DISCOVERABLE_TIMEOUT_ONE_HOUR = 3600;

    @VisibleForTesting
    static final String EXTRA_BYPASS_CONFIRM_DIALOG = "bypassConfirmDialog";

    @VisibleForTesting
    static final int DEFAULT_DISCOVERABLE_TIMEOUT = DISCOVERABLE_TIMEOUT_TWO_MINUTES;
    @VisibleForTesting
    static final int MAX_DISCOVERABLE_TIMEOUT = DISCOVERABLE_TIMEOUT_ONE_HOUR;

    private AlertDialog mDialog;
    private boolean mBypassConfirmDialog = false;
    private int mRequest;
    private int mTimeout = DEFAULT_DISCOVERABLE_TIMEOUT;

    @NonNull
    private CharSequence mAppLabel;
    private LocalBluetoothAdapter mLocalBluetoothAdapter;
    private LocalBluetoothManager mLocalBluetoothManager;
    private StateChangeReceiver mReceiver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getLifecycle().addObserver(new HideNonSystemOverlayMixin(this));

        mRequest = parseIntent();
        if (mRequest == REQUEST_UNKNOWN) {
            finishWithResult(RESULT_CANCELED);
            return;
        }

        mLocalBluetoothManager = LocalBluetoothManager.getInstance(
                getApplicationContext(), /* onInitCallback= */ null);
        if (mLocalBluetoothManager == null) {
            LOG.e("Bluetooth is not supported on this device");
            finishWithResult(RESULT_CANCELED);
        }

        mLocalBluetoothAdapter = mLocalBluetoothManager.getBluetoothAdapter();

        int btState = mLocalBluetoothAdapter.getState();
        switch (mRequest) {
            case REQUEST_DISABLE:
                switch (btState) {
                    case BluetoothAdapter.STATE_OFF:
                    case BluetoothAdapter.STATE_TURNING_OFF:
                        proceedAndFinish();
                        break;

                    case BluetoothAdapter.STATE_ON:
                    case BluetoothAdapter.STATE_TURNING_ON:
                        mDialog = createRequestDisableBluetoothDialog();
                        mDialog.show();
                        break;

                    default:
                        LOG.e("Unknown adapter state: " + btState);
                        finishWithResult(RESULT_CANCELED);
                        break;
                }
                break;
            case REQUEST_ENABLE:
                switch (btState) {
                    case BluetoothAdapter.STATE_OFF:
                    case BluetoothAdapter.STATE_TURNING_OFF:
                        mDialog = createRequestEnableBluetoothDialog();
                        mDialog.show();
                        break;
                    case BluetoothAdapter.STATE_ON:
                    case BluetoothAdapter.STATE_TURNING_ON:
                        proceedAndFinish();
                        break;
                    default:
                        LOG.e("Unknown adapter state: " + btState);
                        finishWithResult(RESULT_CANCELED);
                        break;
                }
                break;
            case REQUEST_ENABLE_DISCOVERABLE:
                switch (btState) {
                    case BluetoothAdapter.STATE_OFF:
                    case BluetoothAdapter.STATE_TURNING_OFF:
                    case BluetoothAdapter.STATE_TURNING_ON:
                        /*
                         * Strictly speaking STATE_TURNING_ON belong with STATE_ON; however, BT
                         * may not be ready when the user clicks yes and we would fail to turn on
                         * discovery mode. We still show the dialog and handle this case via the
                         * broadcast receiver.
                         */
                        if (isSetupWizardDialogBypass()) {
                            /*
                             * In some cases, users may get to the setup wizard's bluetooth fragment
                             * while in this state. We still need to wait until we reach STATE_ON
                             * before enabling discovery mode but without showing a dialog.
                             */
                            enableBluetoothWithWaitingDialog(/* dialogToShowOnWait= */ null);
                        } else {
                            mDialog = createRequestEnableBluetoothDialogWithTimeout(mTimeout);
                            mDialog.show();
                        }
                        break;
                    case BluetoothAdapter.STATE_ON:
                        // Allow SetupWizard specifically to skip the discoverability dialog.
                        if (isSetupWizardDialogBypass()) {
                            proceedAndFinish();
                        } else {
                            mDialog = createDiscoverableConfirmDialog(mTimeout);
                            mDialog.show();
                        }
                        break;
                    default:
                        LOG.e("Unknown adapter state: " + btState);
                        finishWithResult(RESULT_CANCELED);
                        break;
                }
                break;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mReceiver != null) {
            unregisterReceiver(mReceiver);
        }
    }

    private boolean isSetupWizardDialogBypass() {
        String callerName = getCallingPackage();
        return mBypassConfirmDialog && callerName != null
            && callerName.equals(getSetupWizardPackageName());
    }

    @Nullable
    private String getSetupWizardPackageName() {
        Intent intent = new Intent(Intent.ACTION_MAIN);
        intent.addCategory(Intent.CATEGORY_SETUP_WIZARD);

        List<ResolveInfo> matches = getPackageManager().queryIntentActivities(intent,
                PackageManager.MATCH_SYSTEM_ONLY | PackageManager.MATCH_DIRECT_BOOT_AWARE
                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
                        | PackageManager.MATCH_DISABLED_COMPONENTS);
        if (matches.size() == 1) {
            return matches.get(0).activityInfo.packageName;
        } else {
            LOG.e("There should probably be exactly one setup wizard; found " + matches.size()
                    + ": matches=" + matches);
            return null;
        }
    }

    private void proceedAndFinish() {
        if (mRequest == REQUEST_ENABLE_DISCOVERABLE) {
            finishWithResult(setDiscoverable(mTimeout));
        } else {
            finishWithResult(RESULT_OK);
        }
    }

    // Returns the code that should be used to finish the activity.
    private int setDiscoverable(int timeoutSeconds) {
        if (!mLocalBluetoothAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE,
                timeoutSeconds)) {
            return RESULT_CANCELED;
        }

        // If already in discoverable mode, this will extend the timeout.
        long endTime = System.currentTimeMillis() + (long) timeoutSeconds * 1000;
        BluetoothUtils.persistDiscoverableEndTimestamp(/* context= */ this, endTime);
        if (timeoutSeconds > 0) {
            BluetoothDiscoverableTimeoutReceiver.setDiscoverableAlarm(/* context= */ this, endTime);
        }

        int returnCode = timeoutSeconds;
        return returnCode < RESULT_FIRST_USER ? RESULT_FIRST_USER : returnCode;
    }

    private void finishWithResult(int result) {
        if (mDialog != null) {
            mDialog.dismiss();
        }
        setResult(result);
        finish();
    }

    private int parseIntent() {
        int request;
        Intent intent = getIntent();
        if (intent == null) {
            return REQUEST_UNKNOWN;
        }

        switch (intent.getAction()) {
            case BluetoothAdapter.ACTION_REQUEST_ENABLE:
                request = REQUEST_ENABLE;
                break;
            case BluetoothAdapter.ACTION_REQUEST_DISABLE:
                request = REQUEST_DISABLE;
                break;
            case BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE:
                request = REQUEST_ENABLE_DISCOVERABLE;
                mTimeout = intent.getIntExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION,
                        DEFAULT_DISCOVERABLE_TIMEOUT);
                mBypassConfirmDialog = intent.getBooleanExtra(EXTRA_BYPASS_CONFIRM_DIALOG, false);

                if (mTimeout < 1 || mTimeout > MAX_DISCOVERABLE_TIMEOUT) {
                    mTimeout = DEFAULT_DISCOVERABLE_TIMEOUT;
                }
                break;
            default:
                LOG.e("Error: this activity may be started only with intent "
                        + BluetoothAdapter.ACTION_REQUEST_ENABLE);
                return REQUEST_UNKNOWN;
        }

        String packageName = getLaunchedFromPackage();
        int mCallingUid = getLaunchedFromUid();

        if (UserHandle.isSameApp(mCallingUid, Process.SYSTEM_UID)
                && getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME) != null) {
            packageName = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME);
        }

        if (!UserHandle.isSameApp(mCallingUid, Process.SYSTEM_UID)
                && getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME) != null) {
            LOG.w("Non-system Uid: " + mCallingUid + " tried to override packageName");
        }

        if (!mBypassConfirmDialog && !TextUtils.isEmpty(packageName)) {
            try {
                ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(
                        packageName, 0);
                mAppLabel = applicationInfo.loadLabel(getPackageManager());
            } catch (PackageManager.NameNotFoundException e) {
                LOG.e("Couldn't find app with package name " + packageName);
                return REQUEST_UNKNOWN;
            }
        }

        return request;
    }

    private AlertDialog createWaitingDialog() {
        int message = mRequest == REQUEST_DISABLE ? R.string.bluetooth_turning_off
                : R.string.bluetooth_turning_on;

        return new AlertDialogBuilder(/* context= */ this)
                .setMessage(message)
                .setCancelable(false).setOnCancelListener(
                        dialog -> finishWithResult(RESULT_CANCELED))
                .create();
    }

    // Assumes {@code timeoutSeconds} > 0.
    private AlertDialog createDiscoverableConfirmDialog(int timeoutSeconds) {
        String message = mAppLabel != null
                ? getString(R.string.bluetooth_ask_discovery, mAppLabel, timeoutSeconds)
                : getString(R.string.bluetooth_ask_discovery_no_name, timeoutSeconds);

        return new AlertDialogBuilder(/* context= */ this)
                .setMessage(message)
                .setPositiveButton(R.string.allow, (dialog, which) -> proceedAndFinish())
                .setNegativeButton(R.string.deny,
                        (dialog, which) -> finishWithResult(RESULT_CANCELED))
                .setOnCancelListener(dialog -> finishWithResult(RESULT_CANCELED))
                .create();
    }

    private AlertDialog createRequestEnableBluetoothDialog() {
        String message = mAppLabel != null
                ? getString(R.string.bluetooth_ask_enablement, mAppLabel)
                : getString(R.string.bluetooth_ask_enablement_no_name);

        return new AlertDialogBuilder(/* context= */ this)
                .setMessage(message)
                .setPositiveButton(R.string.allow, this::onConfirmEnableBluetooth)
                .setNegativeButton(R.string.deny,
                        (dialog, which) -> finishWithResult(RESULT_CANCELED))
                .setOnCancelListener(dialog -> finishWithResult(RESULT_CANCELED))
                .create();
    }

    // Assumes {@code timeoutSeconds} > 0.
    private AlertDialog createRequestEnableBluetoothDialogWithTimeout(int timeoutSeconds) {
        String message = mAppLabel != null
                ? getString(R.string.bluetooth_ask_enablement_and_discovery, mAppLabel,
                        timeoutSeconds)
                : getString(R.string.bluetooth_ask_enablement_and_discovery_no_name,
                        timeoutSeconds);

        return new AlertDialogBuilder(/* context= */ this)
                .setMessage(message)
                .setPositiveButton(R.string.allow, this::onConfirmEnableBluetooth)
                .setNegativeButton(R.string.deny,
                        (dialog, which) -> finishWithResult(RESULT_CANCELED))
                .setOnCancelListener(dialog -> finishWithResult(RESULT_CANCELED))
                .create();
    }

    private void onConfirmEnableBluetooth(DialogInterface dialog, int which) {
        UserManager userManager = getSystemService(UserManager.class);
        if (userManager.hasUserRestriction(UserManager.DISALLOW_BLUETOOTH)) {
            // If Bluetooth is disallowed, don't try to enable it, show policy
            // transparency message instead.
            DevicePolicyManager dpm = getSystemService(DevicePolicyManager.class);
            Intent intent = dpm.createAdminSupportIntent(
                    UserManager.DISALLOW_BLUETOOTH);
            if (intent != null) {
                startActivity(intent);
            }
            return;
        }

        if (mRequest == REQUEST_ENABLE) {
            enableBluetoothWithWaitingDialog(createWaitingDialog());
        } else {
            enableBluetoothWithWaitingDialog(createDiscoverableConfirmDialog(mTimeout));
        }
    }

    /*
     * Ensure bluetooth is enabled and then check if it is in STATE_ON. If it isn't, register
     * the broadcast receiver to wait for the state to change and show a waiting dialog if provided.
     */
    private void enableBluetoothWithWaitingDialog(@Nullable AlertDialog dialogToShowOnWait) {
        mLocalBluetoothAdapter.enable();

        int desiredState = BluetoothAdapter.STATE_ON;
        if (mLocalBluetoothAdapter.getState() == desiredState) {
            proceedAndFinish();
        } else {
            // Register this receiver to listen for state change after the enabling has started.
            mReceiver = new StateChangeReceiver(desiredState);
            registerReceiver(mReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));

            if (dialogToShowOnWait != null) {
                mDialog = dialogToShowOnWait;
                mDialog.show();
            }
        }
    }

    private AlertDialog createRequestDisableBluetoothDialog() {
        String message = mAppLabel != null
                ? getString(R.string.bluetooth_ask_disablement, mAppLabel)
                : getString(R.string.bluetooth_ask_disablement_no_name);

        return new AlertDialogBuilder(/* context= */ this)
                .setMessage(message)
                .setPositiveButton(R.string.allow, this::onConfirmDisableBluetooth)
                .setNegativeButton(R.string.deny,
                        (dialog, which) -> finishWithResult(RESULT_CANCELED))
                .setOnCancelListener(dialog -> finishWithResult(RESULT_CANCELED))
                .create();
    }

    private void onConfirmDisableBluetooth(DialogInterface dialog, int which) {
        mLocalBluetoothAdapter.disable();

        int desiredState = BluetoothAdapter.STATE_OFF;
        if (mLocalBluetoothAdapter.getState() == desiredState) {
            proceedAndFinish();
        } else {
            // Register this receiver to listen for state change after the disabling has started.
            mReceiver = new StateChangeReceiver(desiredState);
            registerReceiver(mReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));

            // Show dialog while waiting for disabling to complete.
            mDialog = createWaitingDialog();
            mDialog.show();
        }
    }

    @VisibleForTesting
    int getRequestType() {
        return mRequest;
    }

    @VisibleForTesting
    int getTimeout() {
        return mTimeout;
    }

    @VisibleForTesting
    AlertDialog getCurrentDialog() {
        return mDialog;
    }

    @VisibleForTesting
    StateChangeReceiver getCurrentReceiver() {
        return mReceiver;
    }

    /**
     * Listens for bluetooth state changes and finishes the activity if changed to the desired
     * state. If the desired bluetooth state is not received in time, the activity is finished with
     * {@link Activity#RESULT_CANCELED}.
     */
    @VisibleForTesting
    final class StateChangeReceiver extends BroadcastReceiver {
        private static final long TOGGLE_TIMEOUT_MILLIS = 10000; // 10 sec
        private final int mDesiredState;

        StateChangeReceiver(int desiredState) {
            mDesiredState = desiredState;

            getWindow().getDecorView().postDelayed(() -> {
                if (!isFinishing() && !isDestroyed()) {
                    finishWithResult(RESULT_CANCELED);
                }
            }, TOGGLE_TIMEOUT_MILLIS);
        }

        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent == null) {
                return;
            }

            int currentState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
                    BluetoothDevice.ERROR);
            if (mDesiredState == currentState) {
                proceedAndFinish();
            }
        }
    }
}
