/*
 * Copyright (C) 2021 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.nearby;

import static com.android.server.SystemService.PHASE_BOOT_COMPLETED;
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;

import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.app.AppOpsManager;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.hardware.location.ContextHubManager;
import android.nearby.BroadcastRequestParcelable;
import android.nearby.IBroadcastListener;
import android.nearby.INearbyManager;
import android.nearby.IScanListener;
import android.nearby.NearbyManager;
import android.nearby.ScanRequest;
import android.nearby.aidl.IOffloadCallback;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.nearby.injector.Injector;
import com.android.server.nearby.managers.BroadcastProviderManager;
import com.android.server.nearby.managers.DiscoveryManager;
import com.android.server.nearby.managers.DiscoveryProviderManager;
import com.android.server.nearby.managers.DiscoveryProviderManagerLegacy;
import com.android.server.nearby.presence.PresenceManager;
import com.android.server.nearby.util.identity.CallerIdentity;
import com.android.server.nearby.util.permissions.BroadcastPermissions;
import com.android.server.nearby.util.permissions.DiscoveryPermissions;

/** Service implementing nearby functionality. */
public class NearbyService extends INearbyManager.Stub {
    public static final String TAG = "NearbyService";
    // Sets to true to start BLE scan from PresenceManager for manual testing.
    public static final Boolean MANUAL_TEST = false;

    private final Context mContext;
    private final PresenceManager mPresenceManager;
    private final NearbyConfiguration mNearbyConfiguration;
    private Injector mInjector;
    private final BroadcastReceiver mBluetoothReceiver =
            new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    int state =
                            intent.getIntExtra(
                                    BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
                    if (state == BluetoothAdapter.STATE_ON) {
                        if (mInjector != null && mInjector instanceof SystemInjector) {
                            // Have to do this logic in listener. Even during PHASE_BOOT_COMPLETED
                            // phase, BluetoothAdapter is not null, the BleScanner is null.
                            Log.v(TAG, "Initiating BluetoothAdapter when Bluetooth is turned on.");
                            ((SystemInjector) mInjector).initializeBluetoothAdapter();
                        }
                    }
                }
            };
    private final DiscoveryManager mDiscoveryProviderManager;
    private final BroadcastProviderManager mBroadcastProviderManager;

    public NearbyService(Context context) {
        mContext = context;
        mInjector = new SystemInjector(context);
        mBroadcastProviderManager = new BroadcastProviderManager(context, mInjector);
        mPresenceManager = new PresenceManager(context);
        mNearbyConfiguration = new NearbyConfiguration();
        mDiscoveryProviderManager =
                mNearbyConfiguration.refactorDiscoveryManager()
                        ? new DiscoveryProviderManager(context, mInjector)
                        : new DiscoveryProviderManagerLegacy(context, mInjector);
    }

    @VisibleForTesting
    void setInjector(Injector injector) {
        this.mInjector = injector;
    }

    @Override
    @NearbyManager.ScanStatus
    public int registerScanListener(ScanRequest scanRequest, IScanListener listener,
            String packageName, @Nullable String attributionTag) {
        // Permissions check
        enforceBluetoothPrivilegedPermission(mContext);
        CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag);
        DiscoveryPermissions.enforceDiscoveryPermission(mContext, identity);

        return mDiscoveryProviderManager.registerScanListener(scanRequest, listener, identity);
    }

    @Override
    public void unregisterScanListener(IScanListener listener, String packageName,
            @Nullable String attributionTag) {
        // Permissions check
        enforceBluetoothPrivilegedPermission(mContext);
        CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag);
        DiscoveryPermissions.enforceDiscoveryPermission(mContext, identity);

        mDiscoveryProviderManager.unregisterScanListener(listener);
    }

    @Override
    public void startBroadcast(BroadcastRequestParcelable broadcastRequestParcelable,
            IBroadcastListener listener, String packageName, @Nullable String attributionTag) {
        // Permissions check
        enforceBluetoothPrivilegedPermission(mContext);
        BroadcastPermissions.enforceBroadcastPermission(
                mContext, CallerIdentity.fromBinder(mContext, packageName, attributionTag));

        mBroadcastProviderManager.startBroadcast(
                broadcastRequestParcelable.getBroadcastRequest(), listener);
    }

    @Override
    public void stopBroadcast(IBroadcastListener listener, String packageName,
            @Nullable String attributionTag) {
        // Permissions check
        enforceBluetoothPrivilegedPermission(mContext);
        CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag);
        BroadcastPermissions.enforceBroadcastPermission(mContext, identity);

        mBroadcastProviderManager.stopBroadcast(listener);
    }

    @Override
    public void queryOffloadCapability(IOffloadCallback callback) {
        mDiscoveryProviderManager.queryOffloadCapability(callback);
    }

    /**
     * Called by the service initializer.
     *
     * <p>{@see com.android.server.SystemService#onBootPhase}.
     */
    public void onBootPhase(int phase) {
        switch (phase) {
            case PHASE_SYSTEM_SERVICES_READY:
                if (mInjector instanceof SystemInjector) {
                    ((SystemInjector) mInjector).initializeAppOpsManager();
                }
                break;
            case PHASE_BOOT_COMPLETED:
                // mInjector needs to be initialized before mProviderManager.
                if (mInjector instanceof SystemInjector) {
                    // The nearby service must be functioning after this boot phase.
                    ((SystemInjector) mInjector).initializeBluetoothAdapter();
                    // Initialize ContextManager for CHRE scan.
                    ((SystemInjector) mInjector).initializeContextHubManager();
                }
                mDiscoveryProviderManager.init();
                mContext.registerReceiver(
                        mBluetoothReceiver,
                        new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
                // Only enable for manual Presence test on device.
                if (MANUAL_TEST) {
                    mPresenceManager.initiate();
                }
                break;
        }
    }

    /**
     * If the calling process of has not been granted
     * {@link android.Manifest.permission.BLUETOOTH_PRIVILEGED} permission,
     * throw a {@link SecurityException}.
     */
    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
    private void enforceBluetoothPrivilegedPermission(Context context) {
        if (!mNearbyConfiguration.isTestAppSupported()) {
            context.enforceCallingOrSelfPermission(
                    android.Manifest.permission.BLUETOOTH_PRIVILEGED,
                    "Need BLUETOOTH PRIVILEGED permission");
        }
    }

    private static final class SystemInjector implements Injector {
        private final Context mContext;
        @Nullable private BluetoothAdapter mBluetoothAdapter;
        @Nullable private ContextHubManager mContextHubManager;
        @Nullable private AppOpsManager mAppOpsManager;

        SystemInjector(Context context) {
            mContext = context;
        }

        @Override
        @Nullable
        public BluetoothAdapter getBluetoothAdapter() {
            return mBluetoothAdapter;
        }

        @Override
        @Nullable
        public ContextHubManager getContextHubManager() {
            return mContextHubManager;
        }

        @Override
        @Nullable
        public AppOpsManager getAppOpsManager() {
            return mAppOpsManager;
        }

        synchronized void initializeBluetoothAdapter() {
            if (mBluetoothAdapter != null) {
                return;
            }
            BluetoothManager manager = mContext.getSystemService(BluetoothManager.class);
            if (manager == null) {
                return;
            }
            mBluetoothAdapter = manager.getAdapter();
        }

        synchronized void initializeContextHubManager() {
            if (mContextHubManager != null) {
                return;
            }
            if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONTEXT_HUB)) {
                mContextHubManager = mContext.getSystemService(ContextHubManager.class);
            }
        }

        synchronized void initializeAppOpsManager() {
            if (mAppOpsManager != null) {
                return;
            }
            mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
        }
    }
}
