/*
 * Copyright (C) 2014 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 android.net;

import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;

import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.os.Build;
import android.os.OutcomeReceiver;
import android.os.RemoteException;
import android.util.ArrayMap;

import com.android.internal.annotations.GuardedBy;
import com.android.modules.utils.BackgroundThread;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.IntConsumer;

/**
 * A class that manages and configures Ethernet interfaces.
 *
 * @hide
 */
@SystemApi
@SystemService(Context.ETHERNET_SERVICE)
public class EthernetManager {
    private static final String TAG = "EthernetManager";

    private final IEthernetManager mService;
    @GuardedBy("mListenerLock")
    private final ArrayMap<InterfaceStateListener, IEthernetServiceListener>
            mIfaceServiceListeners = new ArrayMap<>();
    @GuardedBy("mListenerLock")
    private final ArrayMap<IntConsumer, IEthernetServiceListener> mStateServiceListeners =
            new ArrayMap<>();
    final Object mListenerLock = new Object();

    /**
     * Indicates that Ethernet is disabled.
     *
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public static final int ETHERNET_STATE_DISABLED = 0;

    /**
     * Indicates that Ethernet is enabled.
     *
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public static final int ETHERNET_STATE_ENABLED  = 1;

    /**
     * The interface is absent.
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public static final int STATE_ABSENT = 0;

    /**
     * The interface is present but link is down.
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public static final int STATE_LINK_DOWN = 1;

    /**
     * The interface is present and link is up.
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public static final int STATE_LINK_UP = 2;

    /** @hide */
    @IntDef(prefix = "STATE_", value = {STATE_ABSENT, STATE_LINK_DOWN, STATE_LINK_UP})
    @Retention(RetentionPolicy.SOURCE)
    public @interface InterfaceState {}

    /**
     * The interface currently does not have any specific role.
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public static final int ROLE_NONE = 0;

    /**
     * The interface is in client mode (e.g., connected to the Internet).
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public static final int ROLE_CLIENT = 1;

    /**
     * Ethernet interface is in server mode (e.g., providing Internet access to tethered devices).
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public static final int ROLE_SERVER = 2;

    /** @hide */
    @IntDef(prefix = "ROLE_", value = {ROLE_NONE, ROLE_CLIENT, ROLE_SERVER})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Role {}

    /**
     * A listener that receives notifications about the state of Ethernet interfaces on the system.
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public interface InterfaceStateListener {
        /**
         * Called when an Ethernet interface changes state.
         *
         * @param iface the name of the interface.
         * @param state the current state of the interface, or {@link #STATE_ABSENT} if the
         *              interface was removed.
         * @param role whether the interface is in client mode or server mode.
         * @param configuration the current IP configuration of the interface.
         * @hide
         */
        @SystemApi(client = MODULE_LIBRARIES)
        void onInterfaceStateChanged(@NonNull String iface, @InterfaceState int state,
                @Role int role, @Nullable IpConfiguration configuration);
    }

    /**
     * A listener interface to receive notification on changes in Ethernet.
     * This has never been a supported API. Use {@link InterfaceStateListener} instead.
     * @hide
     */
    public interface Listener extends InterfaceStateListener {
        /**
         * Called when Ethernet port's availability is changed.
         * @param iface Ethernet interface name
         * @param isAvailable {@code true} if Ethernet port exists.
         * @hide
         */
        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
        void onAvailabilityChanged(String iface, boolean isAvailable);

        /** Default implementation for backwards compatibility. Only calls the legacy listener. */
        default void onInterfaceStateChanged(@NonNull String iface, @InterfaceState int state,
                @Role int role, @Nullable IpConfiguration configuration) {
            onAvailabilityChanged(iface, (state >= STATE_LINK_UP));
        }

    }

    /**
     * Create a new EthernetManager instance.
     * Applications will almost always want to use
     * {@link android.content.Context#getSystemService Context.getSystemService()} to retrieve
     * the standard {@link android.content.Context#ETHERNET_SERVICE Context.ETHERNET_SERVICE}.
     * @hide
     */
    public EthernetManager(Context context, IEthernetManager service) {
        mService = service;
    }

    /**
     * Get Ethernet configuration.
     * @return the Ethernet Configuration, contained in {@link IpConfiguration}.
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public IpConfiguration getConfiguration(String iface) {
        try {
            return mService.getConfiguration(iface);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Set Ethernet configuration.
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public void setConfiguration(@NonNull String iface, @NonNull IpConfiguration config) {
        try {
            mService.setConfiguration(iface, config);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Indicates whether the system currently has one or more Ethernet interfaces.
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public boolean isAvailable() {
        return getAvailableInterfaces().length > 0;
    }

    /**
     * Indicates whether the system has given interface.
     *
     * @param iface Ethernet interface name
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public boolean isAvailable(String iface) {
        try {
            return mService.isAvailable(iface);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Adds a listener.
     * This has never been a supported API. Use {@link #addInterfaceStateListener} instead.
     *
     * @param listener A {@link Listener} to add.
     * @throws IllegalArgumentException If the listener is null.
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public void addListener(@NonNull Listener listener) {
        addListener(listener, BackgroundThread.getExecutor());
    }

    /**
     * Adds a listener.
     * This has never been a supported API. Use {@link #addInterfaceStateListener} instead.
     *
     * @param listener A {@link Listener} to add.
     * @param executor Executor to run callbacks on.
     * @throws IllegalArgumentException If the listener or executor is null.
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public void addListener(@NonNull Listener listener, @NonNull Executor executor) {
        addInterfaceStateListener(executor, listener);
    }

    /**
     * Listen to changes in the state of Ethernet interfaces.
     *
     * Adds a listener to receive notification for any state change of all existing Ethernet
     * interfaces.
     * <p>{@link Listener#onInterfaceStateChanged} will be triggered immediately for all
     * existing interfaces upon adding a listener. The same method will be called on the
     * listener every time any of the interface changes state. In particular, if an
     * interface is removed, it will be called with state {@link #STATE_ABSENT}.
     * <p>Use {@link #removeInterfaceStateListener} with the same object to stop listening.
     *
     * @param executor Executor to run callbacks on.
     * @param listener A {@link Listener} to add.
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
    @SystemApi(client = MODULE_LIBRARIES)
    public void addInterfaceStateListener(@NonNull Executor executor,
            @NonNull InterfaceStateListener listener) {
        if (listener == null || executor == null) {
            throw new NullPointerException("listener and executor must not be null");
        }

        final IEthernetServiceListener.Stub serviceListener = new IEthernetServiceListener.Stub() {
            @Override
            public void onEthernetStateChanged(int state) {}

            @Override
            public void onInterfaceStateChanged(String iface, int state, int role,
                    IpConfiguration configuration) {
                executor.execute(() ->
                        listener.onInterfaceStateChanged(iface, state, role, configuration));
            }
        };
        synchronized (mListenerLock) {
            addServiceListener(serviceListener);
            mIfaceServiceListeners.put(listener, serviceListener);
        }
    }

    @GuardedBy("mListenerLock")
    private void addServiceListener(@NonNull final IEthernetServiceListener listener) {
        try {
            mService.addListener(listener);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }

    }

    /**
     * Returns an array of available Ethernet interface names.
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public String[] getAvailableInterfaces() {
        try {
            return mService.getAvailableInterfaces();
        } catch (RemoteException e) {
            throw e.rethrowAsRuntimeException();
        }
    }

    /**
     * Removes a listener.
     *
     * @param listener A {@link Listener} to remove.
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public void removeInterfaceStateListener(@NonNull InterfaceStateListener listener) {
        Objects.requireNonNull(listener);
        synchronized (mListenerLock) {
            maybeRemoveServiceListener(mIfaceServiceListeners.remove(listener));
        }
    }

    @GuardedBy("mListenerLock")
    private void maybeRemoveServiceListener(@Nullable final IEthernetServiceListener listener) {
        if (listener == null) return;

        try {
            mService.removeListener(listener);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Removes a listener.
     * This has never been a supported API. Use {@link #removeInterfaceStateListener} instead.
     * @param listener A {@link Listener} to remove.
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public void removeListener(@NonNull Listener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("listener must not be null");
        }
        removeInterfaceStateListener(listener);
    }

    /**
     * Whether to treat interfaces created by {@link TestNetworkManager#createTapInterface}
     * as Ethernet interfaces. The effects of this method apply to any test interfaces that are
     * already present on the system.
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public void setIncludeTestInterfaces(boolean include) {
        try {
            mService.setIncludeTestInterfaces(include);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * A request for a tethered interface.
     */
    public static class TetheredInterfaceRequest {
        private final IEthernetManager mService;
        private final ITetheredInterfaceCallback mCb;

        private TetheredInterfaceRequest(@NonNull IEthernetManager service,
                @NonNull ITetheredInterfaceCallback cb) {
            this.mService = service;
            this.mCb = cb;
        }

        /**
         * Release the request, causing the interface to revert back from tethering mode if there
         * is no other requestor.
         */
        public void release() {
            try {
                mService.releaseTetheredInterface(mCb);
            } catch (RemoteException e) {
                e.rethrowFromSystemServer();
            }
        }
    }

    /**
     * Callback for {@link #requestTetheredInterface(TetheredInterfaceCallback)}.
     */
    public interface TetheredInterfaceCallback {
        /**
         * Called when the tethered interface is available.
         * @param iface The name of the interface.
         */
        void onAvailable(@NonNull String iface);

        /**
         * Called when the tethered interface is now unavailable.
         */
        void onUnavailable();
    }

    /**
     * Request a tethered interface in tethering mode.
     *
     * <p>When this method is called and there is at least one ethernet interface available, the
     * system will designate one to act as a tethered interface. If there is already a tethered
     * interface, the existing interface will be used.
     * @param callback A callback to be called once the request has been fulfilled.
     */
    @RequiresPermission(anyOf = {
            android.Manifest.permission.NETWORK_STACK,
            android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
    })
    @NonNull
    public TetheredInterfaceRequest requestTetheredInterface(@NonNull final Executor executor,
            @NonNull final TetheredInterfaceCallback callback) {
        Objects.requireNonNull(callback, "Callback must be non-null");
        Objects.requireNonNull(executor, "Executor must be non-null");
        final ITetheredInterfaceCallback cbInternal = new ITetheredInterfaceCallback.Stub() {
            @Override
            public void onAvailable(String iface) {
                executor.execute(() -> callback.onAvailable(iface));
            }

            @Override
            public void onUnavailable() {
                executor.execute(() -> callback.onUnavailable());
            }
        };

        try {
            mService.requestTetheredInterface(cbInternal);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
        return new TetheredInterfaceRequest(mService, cbInternal);
    }

    private static final class NetworkInterfaceOutcomeReceiver
            extends INetworkInterfaceOutcomeReceiver.Stub {
        @NonNull
        private final Executor mExecutor;
        @NonNull
        private final OutcomeReceiver<String, EthernetNetworkManagementException> mCallback;

        NetworkInterfaceOutcomeReceiver(
                @NonNull final Executor executor,
                @NonNull final OutcomeReceiver<String, EthernetNetworkManagementException>
                        callback) {
            Objects.requireNonNull(executor, "Pass a non-null executor");
            Objects.requireNonNull(callback, "Pass a non-null callback");
            mExecutor = executor;
            mCallback = callback;
        }

        @Override
        public void onResult(@NonNull String iface) {
            mExecutor.execute(() -> mCallback.onResult(iface));
        }

        @Override
        public void onError(@NonNull EthernetNetworkManagementException e) {
            mExecutor.execute(() -> mCallback.onError(e));
        }
    }

    private NetworkInterfaceOutcomeReceiver makeNetworkInterfaceOutcomeReceiver(
            @Nullable final Executor executor,
            @Nullable final OutcomeReceiver<String, EthernetNetworkManagementException> callback) {
        if (null != callback) {
            Objects.requireNonNull(executor, "Pass a non-null executor, or a null callback");
        }
        final NetworkInterfaceOutcomeReceiver proxy;
        if (null == callback) {
            proxy = null;
        } else {
            proxy = new NetworkInterfaceOutcomeReceiver(executor, callback);
        }
        return proxy;
    }

    /**
     * Updates the configuration of an automotive device's ethernet network.
     *
     * The {@link EthernetNetworkUpdateRequest} {@code request} argument describes how to update the
     * configuration for this network.
     * Use {@link StaticIpConfiguration.Builder} to build a {@code StaticIpConfiguration} object for
     * this network to put inside the {@code request}.
     * Similarly, use {@link NetworkCapabilities.Builder} to build a {@code NetworkCapabilities}
     * object for this network to put inside the {@code request}.
     *
     * The provided {@link OutcomeReceiver} is called once the operation has finished execution.
     *
     * @param iface the name of the interface to act upon.
     * @param request the {@link EthernetNetworkUpdateRequest} used to set an ethernet network's
     *                {@link StaticIpConfiguration} and {@link NetworkCapabilities} values.
     * @param executor an {@link Executor} to execute the callback on. Optional if callback is null.
     * @param callback an optional {@link OutcomeReceiver} to listen for completion of the
     *                 operation. On success, {@link OutcomeReceiver#onResult} is called with the
     *                 interface name. On error, {@link OutcomeReceiver#onError} is called with more
     *                 information about the error.
     * @throws SecurityException if the process doesn't hold
     *                          {@link android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}.
     * @throws UnsupportedOperationException if the {@link NetworkCapabilities} are updated on a
     *                                       non-automotive device or this function is called on an
     *                                       unsupported interface.
     * @hide
     */
    @SystemApi
    @RequiresPermission(anyOf = {
            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
            android.Manifest.permission.NETWORK_STACK,
            android.Manifest.permission.MANAGE_ETHERNET_NETWORKS})
    public void updateConfiguration(
            @NonNull String iface,
            @NonNull EthernetNetworkUpdateRequest request,
            @Nullable @CallbackExecutor Executor executor,
            @Nullable OutcomeReceiver<String, EthernetNetworkManagementException> callback) {
        Objects.requireNonNull(iface, "iface must be non-null");
        Objects.requireNonNull(request, "request must be non-null");
        final NetworkInterfaceOutcomeReceiver proxy = makeNetworkInterfaceOutcomeReceiver(
                executor, callback);
        try {
            mService.updateConfiguration(iface, request, proxy);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Enable a network interface.
     *
     * Enables a previously disabled network interface. An attempt to enable an already-enabled
     * interface is ignored.
     * The provided {@link OutcomeReceiver} is called once the operation has finished execution.
     *
     * @param iface the name of the interface to enable.
     * @param executor an {@link Executor} to execute the callback on. Optional if callback is null.
     * @param callback an optional {@link OutcomeReceiver} to listen for completion of the
     *                 operation. On success, {@link OutcomeReceiver#onResult} is called with the
     *                 interface name. On error, {@link OutcomeReceiver#onError} is called with more
     *                 information about the error.
     * @throws SecurityException if the process doesn't hold
     *                          {@link android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}.
     * @hide
     */
    @SystemApi
    @RequiresPermission(anyOf = {
            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
            android.Manifest.permission.NETWORK_STACK,
            android.Manifest.permission.MANAGE_ETHERNET_NETWORKS})
    public void enableInterface(
            @NonNull String iface,
            @Nullable @CallbackExecutor Executor executor,
            @Nullable OutcomeReceiver<String, EthernetNetworkManagementException> callback) {
        Objects.requireNonNull(iface, "iface must be non-null");
        final NetworkInterfaceOutcomeReceiver proxy = makeNetworkInterfaceOutcomeReceiver(
                executor, callback);
        try {
            mService.enableInterface(iface, proxy);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Disable a network interface.
     *
     * Disables the specified interface. If this interface is in use in a connected
     * {@link android.net.Network}, then that {@code Network} will be torn down.
     * The provided {@link OutcomeReceiver} is called once the operation has finished execution.
     *
     * @param iface the name of the interface to disable.
     * @param executor an {@link Executor} to execute the callback on. Optional if callback is null.
     * @param callback an optional {@link OutcomeReceiver} to listen for completion of the
     *                 operation. On success, {@link OutcomeReceiver#onResult} is called with the
     *                 interface name. On error, {@link OutcomeReceiver#onError} is called with more
     *                 information about the error.
     * @throws SecurityException if the process doesn't hold
     *                          {@link android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}.
     * @hide
     */
    @SystemApi
    @RequiresPermission(anyOf = {
            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
            android.Manifest.permission.NETWORK_STACK,
            android.Manifest.permission.MANAGE_ETHERNET_NETWORKS})
    public void disableInterface(
            @NonNull String iface,
            @Nullable @CallbackExecutor Executor executor,
            @Nullable OutcomeReceiver<String, EthernetNetworkManagementException> callback) {
        Objects.requireNonNull(iface, "iface must be non-null");
        final NetworkInterfaceOutcomeReceiver proxy = makeNetworkInterfaceOutcomeReceiver(
                executor, callback);
        try {
            mService.disableInterface(iface, proxy);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Change ethernet setting.
     *
     * @param enabled enable or disable ethernet settings.
     *
     * @hide
     */
    @RequiresPermission(anyOf = {
            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
            android.Manifest.permission.NETWORK_STACK,
            android.Manifest.permission.NETWORK_SETTINGS})
    @SystemApi(client = MODULE_LIBRARIES)
    public void setEthernetEnabled(boolean enabled) {
        try {
            mService.setEthernetEnabled(enabled);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Listen to changes in the state of ethernet.
     *
     * @param executor to run callbacks on.
     * @param listener to listen ethernet state changed.
     *
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
    @SystemApi(client = MODULE_LIBRARIES)
    public void addEthernetStateListener(@NonNull Executor executor,
            @NonNull IntConsumer listener) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(listener);
        final IEthernetServiceListener.Stub serviceListener = new IEthernetServiceListener.Stub() {
            @Override
            public void onEthernetStateChanged(int state) {
                executor.execute(() -> listener.accept(state));
            }

            @Override
            public void onInterfaceStateChanged(String iface, int state, int role,
                    IpConfiguration configuration) {}
        };
        synchronized (mListenerLock) {
            addServiceListener(serviceListener);
            mStateServiceListeners.put(listener, serviceListener);
        }
    }

    /**
     * Removes a listener.
     *
     * @param listener to listen ethernet state changed.
     *
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
    @SystemApi(client = MODULE_LIBRARIES)
    public void removeEthernetStateListener(@NonNull IntConsumer listener) {
        Objects.requireNonNull(listener);
        synchronized (mListenerLock) {
            maybeRemoveServiceListener(mStateServiceListeners.remove(listener));
        }
    }

    /**
     * Returns an array of existing Ethernet interface names regardless whether the interface
     * is available or not currently.
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
    @SystemApi(client = MODULE_LIBRARIES)
    @NonNull
    public List<String> getInterfaceList() {
        try {
            return mService.getInterfaceList();
        } catch (RemoteException e) {
            throw e.rethrowAsRuntimeException();
        }
    }
}
