/*
 * Copyright (C) 2011 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 static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.net.NetworkTemplate.NETWORK_TYPE_ALL;
import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.app.usage.NetworkStatsManager;
import android.content.Context;
import android.net.wifi.WifiInfo;
import android.service.NetworkIdentityProto;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.util.proto.ProtoOutputStream;

import com.android.net.module.util.BitUtils;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.NetworkIdentityUtils;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Objects;

/**
 * Network definition that includes strong identity. Analogous to combining
 * {@link NetworkCapabilities} and an IMSI.
 *
 * @hide
 */
@SystemApi(client = MODULE_LIBRARIES)
public class NetworkIdentity {
    private static final String TAG = "NetworkIdentity";

    /** @hide */
    // TODO: Remove this after migrating all callers to use
    //  {@link NetworkTemplate#NETWORK_TYPE_ALL} instead.
    public static final int SUBTYPE_COMBINED = -1;

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = { "OEM_MANAGED_" }, flag = true, value = {
            NetworkTemplate.OEM_MANAGED_NO,
            NetworkTemplate.OEM_MANAGED_PAID,
            NetworkTemplate.OEM_MANAGED_PRIVATE
    })
    public @interface OemManaged{}

    /**
     * Network has no {@code NetworkCapabilities#NET_CAPABILITY_OEM_*}.
     * @hide
     */
    public static final int OEM_NONE = 0x0;
    /**
     * Network has {@link NetworkCapabilities#NET_CAPABILITY_OEM_PAID}.
     * @hide
     */
    public static final int OEM_PAID = 1 << 0;
    /**
     * Network has {@link NetworkCapabilities#NET_CAPABILITY_OEM_PRIVATE}.
     * @hide
     */
    public static final int OEM_PRIVATE = 1 << 1;

    private static final long SUPPORTED_OEM_MANAGED_TYPES = OEM_PAID | OEM_PRIVATE;

    // Need to be synchronized with ConnectivityManager.
    // TODO: Use {@code ConnectivityManager#*} when visible.
    static final int TYPE_TEST = 18;
    private static final int MAX_NETWORK_TYPE = TYPE_TEST;
    private static final int MIN_NETWORK_TYPE = TYPE_MOBILE;

    final int mType;
    final int mRatType;
    final int mSubId;
    final String mSubscriberId;
    final String mWifiNetworkKey;
    final boolean mRoaming;
    final boolean mMetered;
    final boolean mDefaultNetwork;
    final int mOemManaged;

    /** @hide */
    public NetworkIdentity(
            int type, int ratType, @Nullable String subscriberId, @Nullable String wifiNetworkKey,
            boolean roaming, boolean metered, boolean defaultNetwork, int oemManaged, int subId) {
        mType = type;
        mRatType = ratType;
        mSubscriberId = subscriberId;
        mWifiNetworkKey = wifiNetworkKey;
        mRoaming = roaming;
        mMetered = metered;
        mDefaultNetwork = defaultNetwork;
        mOemManaged = oemManaged;
        mSubId = subId;
    }

    @Override
    public int hashCode() {
        return Objects.hash(mType, mRatType, mSubscriberId, mWifiNetworkKey, mRoaming, mMetered,
                mDefaultNetwork, mOemManaged, mSubId);
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (obj instanceof NetworkIdentity) {
            final NetworkIdentity ident = (NetworkIdentity) obj;
            return mType == ident.mType && mRatType == ident.mRatType && mRoaming == ident.mRoaming
                    && Objects.equals(mSubscriberId, ident.mSubscriberId)
                    && Objects.equals(mWifiNetworkKey, ident.mWifiNetworkKey)
                    && mMetered == ident.mMetered
                    && mDefaultNetwork == ident.mDefaultNetwork
                    && mOemManaged == ident.mOemManaged
                    && mSubId == ident.mSubId;
        }
        return false;
    }

    @Override
    public String toString() {
        final StringBuilder builder = new StringBuilder("{");
        builder.append("type=").append(mType);
        builder.append(", ratType=");
        if (mRatType == NETWORK_TYPE_ALL) {
            builder.append("COMBINED");
        } else {
            builder.append(mRatType);
        }
        if (mSubscriberId != null) {
            builder.append(", subscriberId=")
                    .append(NetworkIdentityUtils.scrubSubscriberId(mSubscriberId));
        }
        if (mWifiNetworkKey != null) {
            builder.append(", wifiNetworkKey=").append(mWifiNetworkKey);
        }
        if (mRoaming) {
            builder.append(", ROAMING");
        }
        builder.append(", metered=").append(mMetered);
        builder.append(", defaultNetwork=").append(mDefaultNetwork);
        builder.append(", oemManaged=").append(getOemManagedNames(mOemManaged));
        builder.append(", subId=").append(mSubId);
        return builder.append("}").toString();
    }

    /**
     * Get the human readable representation of a bitfield representing the OEM managed state of a
     * network.
     */
    static String getOemManagedNames(int oemManaged) {
        if (oemManaged == OEM_NONE) {
            return "OEM_NONE";
        }
        final int[] bitPositions = BitUtils.unpackBits(oemManaged);
        final ArrayList<String> oemManagedNames = new ArrayList<String>();
        for (int position : bitPositions) {
            oemManagedNames.add(nameOfOemManaged(1 << position));
        }
        return String.join(",", oemManagedNames);
    }

    private static String nameOfOemManaged(int oemManagedBit) {
        switch (oemManagedBit) {
            case OEM_PAID:
                return "OEM_PAID";
            case OEM_PRIVATE:
                return "OEM_PRIVATE";
            default:
                return "Invalid(" + oemManagedBit + ")";
        }
    }

    /** @hide */
    public void dumpDebug(ProtoOutputStream proto, long tag) {
        final long start = proto.start(tag);

        proto.write(NetworkIdentityProto.TYPE, mType);

        // TODO: dump mRatType as well.

        proto.write(NetworkIdentityProto.ROAMING, mRoaming);
        proto.write(NetworkIdentityProto.METERED, mMetered);
        proto.write(NetworkIdentityProto.DEFAULT_NETWORK, mDefaultNetwork);
        proto.write(NetworkIdentityProto.OEM_MANAGED_NETWORK, mOemManaged);

        proto.end(start);
    }

    /** Get the network type of this instance. */
    public int getType() {
        return mType;
    }

    /** Get the Radio Access Technology(RAT) type of this instance. */
    public int getRatType() {
        return mRatType;
    }

    /** Get the Subscriber Id of this instance. */
    @Nullable
    public String getSubscriberId() {
        return mSubscriberId;
    }

    /** Get the Wifi Network Key of this instance. See {@link WifiInfo#getNetworkKey()}. */
    @Nullable
    public String getWifiNetworkKey() {
        return mWifiNetworkKey;
    }

    /** @hide */
    // TODO: Remove this function after all callers are removed.
    public boolean getRoaming() {
        return mRoaming;
    }

    /** Return whether this network is roaming. */
    public boolean isRoaming() {
        return mRoaming;
    }

    /** @hide */
    // TODO: Remove this function after all callers are removed.
    public boolean getMetered() {
        return mMetered;
    }

    /** Return whether this network is metered. */
    public boolean isMetered() {
        return mMetered;
    }

    /** @hide */
    // TODO: Remove this function after all callers are removed.
    public boolean getDefaultNetwork() {
        return mDefaultNetwork;
    }

    /** Return whether this network is the default network. */
    public boolean isDefaultNetwork() {
        return mDefaultNetwork;
    }

    /** Get the OEM managed type of this instance. */
    public int getOemManaged() {
        return mOemManaged;
    }

    /** Get the SubId of this instance. */
    public int getSubId() {
        return mSubId;
    }

    /**
     * Assemble a {@link NetworkIdentity} from the passed arguments.
     *
     * This methods builds an identity based on the capabilities of the network in the
     * snapshot and other passed arguments. The identity is used as a key to record data usage.
     *
     * @param snapshot the snapshot of network state. See {@link NetworkStateSnapshot}.
     * @param defaultNetwork whether the network is a default network.
     * @param ratType the Radio Access Technology(RAT) type of the network. Or
     *                {@link TelephonyManager#NETWORK_TYPE_UNKNOWN} if not applicable.
     *                See {@code TelephonyManager.NETWORK_TYPE_*}.
     * @hide
     * @deprecated See {@link NetworkIdentity.Builder}.
     */
    // TODO: Remove this after all callers are migrated to use new Api.
    @Deprecated
    @NonNull
    public static NetworkIdentity buildNetworkIdentity(Context context,
            @NonNull NetworkStateSnapshot snapshot, boolean defaultNetwork, int ratType) {
        final NetworkIdentity.Builder builder = new NetworkIdentity.Builder()
                .setNetworkStateSnapshot(snapshot).setDefaultNetwork(defaultNetwork)
                .setSubId(snapshot.getSubId());
        if (snapshot.getLegacyType() == TYPE_MOBILE && ratType != NETWORK_TYPE_ALL) {
            builder.setRatType(ratType);
        }
        return builder.build();
    }

    /**
     * Builds a bitfield of {@code NetworkIdentity.OEM_*} based on {@link NetworkCapabilities}.
     * @hide
     */
    public static int getOemBitfield(@NonNull NetworkCapabilities nc) {
        int oemManaged = OEM_NONE;

        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID)) {
            oemManaged |= OEM_PAID;
        }
        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE)) {
            oemManaged |= OEM_PRIVATE;
        }

        return oemManaged;
    }

    /** @hide */
    public static int compare(@NonNull NetworkIdentity left, @NonNull NetworkIdentity right) {
        Objects.requireNonNull(right);
        int res = Integer.compare(left.mType, right.mType);
        if (res == 0) {
            res = Integer.compare(left.mRatType, right.mRatType);
        }
        if (res == 0 && left.mSubscriberId != null && right.mSubscriberId != null) {
            res = left.mSubscriberId.compareTo(right.mSubscriberId);
        }
        if (res == 0 && left.mWifiNetworkKey != null && right.mWifiNetworkKey != null) {
            res = left.mWifiNetworkKey.compareTo(right.mWifiNetworkKey);
        }
        if (res == 0) {
            res = Boolean.compare(left.mRoaming, right.mRoaming);
        }
        if (res == 0) {
            res = Boolean.compare(left.mMetered, right.mMetered);
        }
        if (res == 0) {
            res = Boolean.compare(left.mDefaultNetwork, right.mDefaultNetwork);
        }
        if (res == 0) {
            res = Integer.compare(left.mOemManaged, right.mOemManaged);
        }
        if (res == 0) {
            res = Integer.compare(left.mSubId, right.mSubId);
        }
        return res;
    }

    /**
     * Builder class for {@link NetworkIdentity}.
     */
    public static final class Builder {
        private int mType;
        private int mRatType;
        private String mSubscriberId;
        private String mWifiNetworkKey;
        private boolean mRoaming;
        private boolean mMetered;
        private boolean mDefaultNetwork;
        private int mOemManaged;
        private int mSubId;

        /**
         * Creates a new Builder.
         */
        public Builder() {
            // Initialize with default values. Will be overwritten by setters.
            mType = ConnectivityManager.TYPE_NONE;
            mRatType = NetworkTemplate.NETWORK_TYPE_ALL;
            mSubscriberId = null;
            mWifiNetworkKey = null;
            mRoaming = false;
            mMetered = false;
            mDefaultNetwork = false;
            mOemManaged = NetworkTemplate.OEM_MANAGED_NO;
            mSubId = INVALID_SUBSCRIPTION_ID;
        }

        /**
         * Add an {@link NetworkStateSnapshot} into the {@link NetworkIdentity} instance.
         * This is a useful shorthand that will read from the snapshot and set the
         * following fields, if they are set in the snapshot :
         *  - type
         *  - subscriberId
         *  - roaming
         *  - metered
         *  - oemManaged
         *  - wifiNetworkKey
         *
         * @param snapshot The target {@link NetworkStateSnapshot} object.
         * @return The builder object.
         */
        @SuppressLint("MissingGetterMatchingBuilder")
        @NonNull
        public Builder setNetworkStateSnapshot(@NonNull NetworkStateSnapshot snapshot) {
            setType(snapshot.getLegacyType());

            setSubscriberId(snapshot.getSubscriberId());
            setRoaming(!snapshot.getNetworkCapabilities().hasCapability(
                    NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING));
            setMetered(!snapshot.getNetworkCapabilities().hasCapability(
                    NetworkCapabilities.NET_CAPABILITY_NOT_METERED));

            setOemManaged(getOemBitfield(snapshot.getNetworkCapabilities()));

            if (mType == TYPE_WIFI) {
                final NetworkCapabilities nc = snapshot.getNetworkCapabilities();
                final TransportInfo transportInfo = nc.getTransportInfo();
                if (transportInfo instanceof WifiInfo) {
                    final WifiInfo info = (WifiInfo) transportInfo;
                    // Log.wtf to catch trying to set a null wifiNetworkKey into NetworkIdentity.
                    // See b/266598304. The problematic data that has null wifi network key is
                    // thrown out when storing data, which is handled by the service.
                    if (info.getNetworkKey() == null) {
                        Log.wtf(TAG, "WifiInfo contains a null wifiNetworkKey and it will"
                                + " be set into NetworkIdentity, netId=" + snapshot.getNetwork()
                                + "NetworkCapabilities=" + nc);
                    }
                    setWifiNetworkKey(info.getNetworkKey());
                }
            } else if (mType == TYPE_TEST) {
                final NetworkSpecifier ns = snapshot.getNetworkCapabilities().getNetworkSpecifier();
                if (ns instanceof TestNetworkSpecifier) {
                    // Reuse the wifi network key field to identify individual test networks.
                    setWifiNetworkKey(((TestNetworkSpecifier) ns).getInterfaceName());
                }
            }
            return this;
        }

        /**
         * Set the network type of the network.
         *
         * @param type the network type. See {@link ConnectivityManager#TYPE_*}.
         *
         * @return this builder.
         */
        @NonNull
        public Builder setType(int type) {
            // Include TYPE_NONE for compatibility, type field might not be filled by some
            // networks such as test networks.
            if ((type < MIN_NETWORK_TYPE || MAX_NETWORK_TYPE < type)
                    && type != ConnectivityManager.TYPE_NONE) {
                throw new IllegalArgumentException("Invalid network type: " + type);
            }
            mType = type;
            return this;
        }

        /**
         * Set the Radio Access Technology(RAT) type of the network.
         *
         * No RAT type is specified by default. Call clearRatType to reset.
         *
         * @param ratType the Radio Access Technology(RAT) type if applicable. See
         *                {@code TelephonyManager.NETWORK_TYPE_*}.
         *
         * @return this builder.
         */
        @NonNull
        public Builder setRatType(int ratType) {
            if (!CollectionUtils.contains(TelephonyManager.getAllNetworkTypes(), ratType)
                    && ratType != TelephonyManager.NETWORK_TYPE_UNKNOWN
                    && ratType != NetworkStatsManager.NETWORK_TYPE_5G_NSA) {
                throw new IllegalArgumentException("Invalid ratType " + ratType);
            }
            mRatType = ratType;
            return this;
        }

        /**
         * Clear the Radio Access Technology(RAT) type of the network.
         *
         * @return this builder.
         */
        @NonNull
        public Builder clearRatType() {
            mRatType = NetworkTemplate.NETWORK_TYPE_ALL;
            return this;
        }

        /**
         * Set the Subscriber Id.
         *
         * @param subscriberId the Subscriber Id of the network. Or null if not applicable.
         * @return this builder.
         */
        @NonNull
        public Builder setSubscriberId(@Nullable String subscriberId) {
            mSubscriberId = subscriberId;
            return this;
        }

        /**
         * Set the Wifi Network Key.
         *
         * @param wifiNetworkKey Wifi Network Key of the network,
         *                        see {@link WifiInfo#getNetworkKey()}.
         *                        Or null if not applicable.
         * @return this builder.
         */
        @NonNull
        public Builder setWifiNetworkKey(@Nullable String wifiNetworkKey) {
            mWifiNetworkKey = wifiNetworkKey;
            return this;
        }

        /**
         * Set whether this network is roaming.
         *
         * This field is false by default. Call with false to reset.
         *
         * @param roaming the roaming status of the network.
         * @return this builder.
         */
        @NonNull
        public Builder setRoaming(boolean roaming) {
            mRoaming = roaming;
            return this;
        }

        /**
         * Set whether this network is metered.
         *
         * This field is false by default. Call with false to reset.
         *
         * @param metered the meteredness of the network.
         * @return this builder.
         */
        @NonNull
        public Builder setMetered(boolean metered) {
            mMetered = metered;
            return this;
        }

        /**
         * Set whether this network is the default network.
         *
         * This field is false by default. Call with false to reset.
         *
         * @param defaultNetwork the default network status of the network.
         * @return this builder.
         */
        @NonNull
        public Builder setDefaultNetwork(boolean defaultNetwork) {
            mDefaultNetwork = defaultNetwork;
            return this;
        }

        /**
         * Set the OEM managed type.
         *
         * @param oemManaged Type of OEM managed network or unmanaged networks.
         *                   See {@code NetworkTemplate#OEM_MANAGED_*}.
         * @return this builder.
         */
        @NonNull
        public Builder setOemManaged(@OemManaged int oemManaged) {
            // Assert input does not contain illegal oemManage bits.
            if ((~SUPPORTED_OEM_MANAGED_TYPES & oemManaged) != 0) {
                throw new IllegalArgumentException("Invalid value for OemManaged : " + oemManaged);
            }
            mOemManaged = oemManaged;
            return this;
        }

        /**
         * Set the Subscription Id.
         *
         * @param subId the Subscription Id of the network. Or INVALID_SUBSCRIPTION_ID if not
         *              applicable.
         * @return this builder.
         */
        @NonNull
        public Builder setSubId(int subId) {
            mSubId = subId;
            return this;
        }

        private void ensureValidParameters() {
            // Assert non-mobile network cannot have a ratType.
            if (mType != TYPE_MOBILE && mRatType != NetworkTemplate.NETWORK_TYPE_ALL) {
                throw new IllegalArgumentException(
                        "Invalid ratType " + mRatType + " for type " + mType);
            }

            // Assert non-wifi network cannot have a wifi network key.
            if (mType != TYPE_WIFI && mType != TYPE_TEST && mWifiNetworkKey != null) {
                throw new IllegalArgumentException("Invalid wifi network key for type " + mType);
            }
        }

        /**
         * Builds the instance of the {@link NetworkIdentity}.
         *
         * @return the built instance of {@link NetworkIdentity}.
         */
        @NonNull
        public NetworkIdentity build() {
            ensureValidParameters();
            return new NetworkIdentity(mType, mRatType, mSubscriberId, mWifiNetworkKey,
                    mRoaming, mMetered, mDefaultNetwork, mOemManaged, mSubId);
        }
    }
}
