/*
 * Copyright 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 static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;

import android.app.ActivityManager;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.car.drivingstate.CarUxRestrictions;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.IBinder;
import android.os.RemoteException;

import androidx.preference.PreferenceGroup;

import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;

/**
 * Controller which sets the Bluetooth adapter to discovery mode and begins scanning for
 * discoverable devices for as long as the preference group is shown. Discovery
 * and scanning are halted while any device is pairing. Users with the {@link
 * DISALLOW_CONFIG_BLUETOOTH} restriction cannot scan for devices, so only cached devices will be
 * shown.
 */
public abstract class BluetoothScanningDevicesGroupPreferenceController extends
        BluetoothDevicesGroupPreferenceController {

    private static final Logger LOG = new Logger(
            BluetoothScanningDevicesGroupPreferenceController.class);

    protected final BluetoothAdapter mBluetoothAdapter;
    private final AlwaysDiscoverable mAlwaysDiscoverable;
    private final String mCallingAppPackageName;

    private boolean mIsScanningEnabled;

    public BluetoothScanningDevicesGroupPreferenceController(Context context, String preferenceKey,
            FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
        super(context, preferenceKey, fragmentController, uxRestrictions);
        mBluetoothAdapter = getContext().getSystemService(BluetoothManager.class).getAdapter();
        mAlwaysDiscoverable = new AlwaysDiscoverable(context, mBluetoothAdapter);
        mCallingAppPackageName = getCallingAppPackageName(getContext().getActivityToken());
    }

    @Override
    protected final void onDeviceClicked(CachedBluetoothDevice cachedDevice) {
        LOG.d("onDeviceClicked: " + cachedDevice);
        disableScanning();
        onDeviceClickedInternal(cachedDevice);
    }

    /**
     * Called when the user selects a device in the group.
     *
     * @param cachedDevice the device represented by the selected preference.
     */
    protected abstract void onDeviceClickedInternal(CachedBluetoothDevice cachedDevice);

    @Override
    protected void onStartInternal() {
        super.onStartInternal();
        mIsScanningEnabled = true;
    }

    @Override
    protected void onStopInternal() {
        super.onStopInternal();
        disableScanning();
        getBluetoothManager().getCachedDeviceManager().clearNonBondedDevices();
        getPreferenceMap().clear();
        getPreference().removeAll();
    }

    @Override
    protected void updateState(PreferenceGroup preferenceGroup) {
        super.updateState(preferenceGroup);
        if (shouldEnableScanning() && mIsScanningEnabled) {
            enableScanning();
        } else {
            disableScanning();
        }
    }

    @Override
    protected boolean shouldShowDisconnectedStateSubtitle() {
        return false;
    }

    protected void reenableScanning() {
        if (isStarted()) {
            mIsScanningEnabled = true;
        }
        refreshUi();
    }

    private boolean shouldEnableScanning() {
        for (CachedBluetoothDevice device : getPreferenceMap().keySet()) {
            if (device.getBondState() == BluetoothDevice.BOND_BONDING) {
                return false;
            }
        }
        // Users who cannot configure Bluetooth cannot scan.
        return !getUserManager().hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH);
    }

    /**
     * Starts scanning for devices which will be displayed in the group for a user to select.
     * Calls are idempotent.
     */
    private void enableScanning() {
        mIsScanningEnabled = true;
        if (!mBluetoothAdapter.isDiscovering()) {
            mBluetoothAdapter.startDiscovery();
        }

        if (BluetoothUtils.shouldEnableBTScanning(getContext(), mCallingAppPackageName)) {
            mAlwaysDiscoverable.start();
        } else {
            LOG.d("Not enabling bluetooth scanning. Calling application " + mCallingAppPackageName
                    + " is not Settings or SystemUi");
        }
        getPreference().setEnabled(true);
    }

    /** Stops scanning for devices and disables interaction. Calls are idempotent. */
    private void disableScanning() {
        mIsScanningEnabled = false;
        getPreference().setEnabled(false);
        mAlwaysDiscoverable.stop();
        if (mBluetoothAdapter.isDiscovering()) {
            mBluetoothAdapter.cancelDiscovery();
        }
    }

    @Override
    public void onScanningStateChanged(boolean started) {
        LOG.d("onScanningStateChanged started: " + started + " mIsScanningEnabled: "
                + mIsScanningEnabled);
        if (!started && mIsScanningEnabled) {
            enableScanning();
        }
    }

    @Override
    public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
        LOG.d("onDeviceBondStateChanged device: " + cachedDevice + " state: " + bondState);
        if (bondState == BluetoothDevice.BOND_NONE && isStarted()) {
            mIsScanningEnabled = true;
        }
        refreshUi();
    }

    private String getCallingAppPackageName(IBinder activityToken) {
        String pkg = null;
        try {
            pkg = ActivityManager.getService().getLaunchedFromPackage(activityToken);
        } catch (RemoteException e) {
            LOG.e("Could not talk to activity manager.", e);
        }
        return pkg;
    }

    /**
     * Helper class to keep the {@link BluetoothAdapter} in discoverable mode indefinitely. By
     * default, setting the scan mode to BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE will
     * timeout, but for pairing, we want to keep the device discoverable as long as the page is
     * scanning.
     */
    private static final class AlwaysDiscoverable extends BroadcastReceiver {

        private final Context mContext;
        private final BluetoothAdapter mAdapter;
        private final IntentFilter mIntentFilter = new IntentFilter(
                BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);

        private boolean mStarted;

        AlwaysDiscoverable(Context context, BluetoothAdapter adapter) {
            mContext = context;
            mAdapter = adapter;
        }

        /**
         * Sets the adapter scan mode to
         * {@link BluetoothAdapter#SCAN_MODE_CONNECTABLE_DISCOVERABLE}. {@link #start()} calls
         * should have a matching calls to {@link #stop()} when discover mode is no longer needed.
         */
        void start() {
            if (mStarted) {
                return;
            }
            mContext.registerReceiver(this, mIntentFilter);
            mStarted = true;
            setDiscoverable();
        }

        void stop() {
            if (!mStarted) {
                return;
            }
            mContext.unregisterReceiver(this);
            mStarted = false;
            mAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
        }

        @Override
        public void onReceive(Context context, Intent intent) {
            setDiscoverable();
        }

        private void setDiscoverable() {
            if (mAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
                mAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
            }
        }
    }
}
