/*
 * 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.uwb;

import android.annotation.NonNull;
import android.content.AttributionSource;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Binder;
import android.os.IBinder;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.provider.Settings;
import android.util.ArrayMap;
import android.util.Log;
import android.uwb.AdapterState;
import android.uwb.IUwbAdapter;
import android.uwb.IUwbAdapterStateCallbacks;
import android.uwb.IUwbRangingCallbacks;
import android.uwb.RangingReport;
import android.uwb.RangingSession;
import android.uwb.SessionHandle;

import com.android.internal.annotations.GuardedBy;

import java.util.Map;

/**
 * Implementation of {@link android.uwb.IUwbAdapter} binder service.
 */
public class UwbServiceImpl extends IUwbAdapter.Stub implements IBinder.DeathRecipient{
    private static final String TAG = "UwbServiceImpl";

    private final Context mContext;
    private final UwbInjector mUwbInjector;
    /**
     * Map for storing the callbacks wrapper for each session.
     */
    @GuardedBy("mCallbacksMap")
    private final Map<SessionHandle, UwbRangingCallbacksWrapper> mCallbacksMap = new ArrayMap<>();

    /**
     * Used for caching the vendor implementation of {@link IUwbAdapter} interface.
     */
    private IUwbAdapter mVendorUwbAdapter;

    /**
     * Wrapper for callback registered with vendor service. This wrapper is needed for performing
     * permission check before sending the callback to the external app.
     *
     * Access to these callbacks are synchronized.
     */
    private class UwbRangingCallbacksWrapper extends IUwbRangingCallbacks.Stub
            implements IBinder.DeathRecipient {
        private final AttributionSource mAttributionSource;
        private final SessionHandle mSessionHandle;
        private final IUwbRangingCallbacks mExternalCb;
        private boolean mIsValid;

        UwbRangingCallbacksWrapper(@NonNull AttributionSource attributionSource,
                @NonNull SessionHandle sessionHandle,
                @NonNull IUwbRangingCallbacks externalCb) {
            mAttributionSource = attributionSource;
            mSessionHandle = sessionHandle;
            mExternalCb = externalCb;
            mIsValid = true;

            // Link to death for external callback.
            linkToDeath();
        }

        private void linkToDeath() {
            IBinder binder = mExternalCb.asBinder();
            try {
                binder.linkToDeath(this, 0);
            } catch (RemoteException e) {
                Log.e(TAG, "Unable to link to client death event.", e);
            }
        }

        private void removeClientAndUnlinkToDeath() {
            // Remove from the map.
            synchronized (mCallbacksMap) {
                mCallbacksMap.remove(mSessionHandle);
            }
            IBinder binder = mExternalCb.asBinder();
            binder.unlinkToDeath(this, 0);
            mIsValid = false;
        }


        @Override
        public synchronized void onRangingOpened(SessionHandle sessionHandle)
                throws RemoteException {
            if (!mIsValid) return;
            mExternalCb.onRangingOpened(sessionHandle);
        }

        @Override
        public synchronized void onRangingOpenFailed(SessionHandle sessionHandle,
                int reason, PersistableBundle parameters) throws RemoteException {
            if (!mIsValid) return;
            mExternalCb.onRangingOpenFailed(sessionHandle, reason, parameters);
        }

        @Override
        public synchronized void onRangingStarted(SessionHandle sessionHandle,
                PersistableBundle parameters)
                throws RemoteException {
            if (!mIsValid) return;
            mExternalCb.onRangingStarted(sessionHandle, parameters);
        }

        @Override
        public synchronized void onRangingStartFailed(SessionHandle sessionHandle,
                int reason, PersistableBundle parameters) throws RemoteException {
            if (!mIsValid) return;
            mExternalCb.onRangingStartFailed(sessionHandle, reason, parameters);
        }

        @Override
        public synchronized void onRangingReconfigured(SessionHandle sessionHandle,
                PersistableBundle parameters)
                throws RemoteException {
            if (!mIsValid) return;
            mExternalCb.onRangingReconfigured(sessionHandle, parameters);
        }

        @Override
        public synchronized void onRangingReconfigureFailed(SessionHandle sessionHandle,
                int reason, PersistableBundle parameters) throws RemoteException {
            if (!mIsValid) return;
            mExternalCb.onRangingReconfigureFailed(sessionHandle, reason, parameters);
        }

        @Override
        public synchronized void onRangingStopped(SessionHandle sessionHandle, int reason,
                PersistableBundle parameters)
                throws RemoteException {
            if (!mIsValid) return;
            mExternalCb.onRangingStopped(sessionHandle, reason, parameters);
        }

        @Override
        public synchronized void onRangingStopFailed(SessionHandle sessionHandle, int reason,
                PersistableBundle parameters) throws RemoteException {
            if (!mIsValid) return;
            mExternalCb.onRangingStopFailed(sessionHandle, reason, parameters);
        }

        @Override
        public synchronized void onRangingClosed(SessionHandle sessionHandle, int reason,
                PersistableBundle parameters) throws RemoteException {
            if (!mIsValid) return;
            mExternalCb.onRangingClosed(sessionHandle, reason, parameters);
            removeClientAndUnlinkToDeath();
        }

        @Override
        public synchronized void onRangingResult(SessionHandle sessionHandle,
                RangingReport rangingReport)
                throws RemoteException {
            if (!mIsValid) return;
            boolean permissionGranted = Binder.withCleanCallingIdentity(
                    () -> mUwbInjector.checkUwbRangingPermissionForDataDelivery(
                            mAttributionSource, "uwb ranging result"));
            if (!permissionGranted) {
                Log.e(TAG, "Not delivering ranging result because of permission denial"
                        + mSessionHandle);
                return;
            }
            mExternalCb.onRangingResult(sessionHandle, rangingReport);
        }

        @Override
        public synchronized void binderDied() {
            if (!mIsValid) return;
            Log.i(TAG, "Client died: ending session: " + mSessionHandle);
            try {
                removeClientAndUnlinkToDeath();
                stopRanging(mSessionHandle);
                closeRanging(mSessionHandle);
            } catch (RemoteException e) {
                Log.e(TAG, "Remote exception while handling client death", e);
            }
        }
    }

    private void linkToVendorServiceDeath() {
        IBinder binder = mVendorUwbAdapter.asBinder();
        try {
            binder.linkToDeath(this, 0);
        } catch (RemoteException e) {
            Log.e(TAG, "Unable to link to vendor service death event.", e);
        }
    }

    @Override
    public void binderDied() {
        Log.i(TAG, "Vendor service died: sending session close callbacks");
        synchronized (mCallbacksMap) {
            for (Map.Entry<SessionHandle, UwbRangingCallbacksWrapper> e : mCallbacksMap.entrySet()) {
                try {
                    e.getValue().mExternalCb.onRangingClosed(
                            e.getKey(), RangingSession.Callback.REASON_UNKNOWN,
                            new PersistableBundle());
                } catch (RemoteException ex) {
                    Log.e(TAG, "Failed to send session close callback " + e.getKey(), ex);
                }
            }
            // Clear all sessions.
            mCallbacksMap.clear();
        }
        mVendorUwbAdapter = null;
    }

    private synchronized IUwbAdapter getVendorUwbAdapter()
            throws IllegalStateException, RemoteException {
        if (mVendorUwbAdapter != null) return mVendorUwbAdapter;
        mVendorUwbAdapter = mUwbInjector.getVendorService();
        if (mVendorUwbAdapter == null) {
            throw new IllegalStateException("No vendor service found!");
        }
        Log.i(TAG, "Retrieved vendor service");
        long token = Binder.clearCallingIdentity();
        try {
            mVendorUwbAdapter.setEnabled(isEnabled());
        } finally {
          Binder.restoreCallingIdentity(token);
        }
        linkToVendorServiceDeath();
        return mVendorUwbAdapter;
    }

    UwbServiceImpl(@NonNull Context context, @NonNull UwbInjector uwbInjector) {
        mContext = context;
        mUwbInjector = uwbInjector;
        registerAirplaneModeReceiver();
    }

    private void enforceUwbPrivilegedPermission() {
        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.UWB_PRIVILEGED,
                "UwbService");
    }

    @Override
    public void registerAdapterStateCallbacks(IUwbAdapterStateCallbacks adapterStateCallbacks)
            throws RemoteException {
        enforceUwbPrivilegedPermission();
        getVendorUwbAdapter().registerAdapterStateCallbacks(adapterStateCallbacks);
    }

    @Override
    public void unregisterAdapterStateCallbacks(IUwbAdapterStateCallbacks adapterStateCallbacks)
            throws RemoteException {
        enforceUwbPrivilegedPermission();
        getVendorUwbAdapter().unregisterAdapterStateCallbacks(adapterStateCallbacks);
    }

    @Override
    public long getTimestampResolutionNanos() throws RemoteException {
        enforceUwbPrivilegedPermission();
        return getVendorUwbAdapter().getTimestampResolutionNanos();
    }

    @Override
    public PersistableBundle getSpecificationInfo() throws RemoteException {
        enforceUwbPrivilegedPermission();
        return getVendorUwbAdapter().getSpecificationInfo();
    }

    @Override
    public void openRanging(AttributionSource attributionSource,
            SessionHandle sessionHandle, IUwbRangingCallbacks rangingCallbacks,
            PersistableBundle parameters) throws RemoteException {
        enforceUwbPrivilegedPermission();
        mUwbInjector.enforceUwbRangingPermissionForPreflight(attributionSource);

        UwbRangingCallbacksWrapper wrapperCb =
                new UwbRangingCallbacksWrapper(attributionSource, sessionHandle, rangingCallbacks);
        synchronized (mCallbacksMap) {
            mCallbacksMap.put(sessionHandle, wrapperCb);
        }
        getVendorUwbAdapter().openRanging(attributionSource, sessionHandle, wrapperCb, parameters);
    }

    @Override
    public void startRanging(SessionHandle sessionHandle, PersistableBundle parameters)
            throws RemoteException {
        enforceUwbPrivilegedPermission();
        getVendorUwbAdapter().startRanging(sessionHandle, parameters);
    }

    @Override
    public void reconfigureRanging(SessionHandle sessionHandle, PersistableBundle parameters)
            throws RemoteException {
        enforceUwbPrivilegedPermission();
        getVendorUwbAdapter().reconfigureRanging(sessionHandle, parameters);
    }

    @Override
    public void stopRanging(SessionHandle sessionHandle) throws RemoteException {
        enforceUwbPrivilegedPermission();
        getVendorUwbAdapter().stopRanging(sessionHandle);
    }

    @Override
    public void closeRanging(SessionHandle sessionHandle) throws RemoteException {
        enforceUwbPrivilegedPermission();
        getVendorUwbAdapter().closeRanging(sessionHandle);
    }

    @Override
    public synchronized int getAdapterState() throws RemoteException {
        return getVendorUwbAdapter().getAdapterState();
    }

    @Override
    public synchronized void setEnabled(boolean enabled) throws RemoteException {
        persistUwbState(enabled);
        getVendorUwbAdapter().setEnabled(isEnabled());
    }

    private void persistUwbState(boolean enabled) {
        final ContentResolver cr = mContext.getContentResolver();
        int state = enabled ? AdapterState.STATE_ENABLED_ACTIVE : AdapterState.STATE_DISABLED;
        Settings.Global.putInt(cr, Settings.Global.UWB_ENABLED, state);
    }

    private void registerAirplaneModeReceiver() {
        mContext.registerReceiver(new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                handleAirplaneModeEvent();
            }
        }, new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
    }

    private void handleAirplaneModeEvent() {
        try {
            getVendorUwbAdapter().setEnabled(isEnabled());
        } catch (RemoteException | IllegalStateException e) {
            Log.e(TAG, "Unable to set UWB Adapter state.", e);
        }
    }

    private boolean isEnabled() {
        return mUwbInjector.isPersistedUwbStateEnabled() && !mUwbInjector.isAirplaneModeOn();
    }
}
