/*
 * 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 com.android.mms.service;

import android.annotation.NonNull;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkRequest;
import android.net.TelephonyNetworkSpecifier;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.PersistableBundle;
import android.provider.DeviceConfig;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.PhoneConstants;
import com.android.internal.telephony.flags.Flags;
import com.android.mms.service.exception.MmsNetworkException;

/**
 * Manages the MMS network connectivity
 */
public class MmsNetworkManager {
    private static final String MMS_SERVICE_NETWORK_REQUEST_TIMEOUT_MILLIS =
            "mms_service_network_request_timeout_millis";

    // Default timeout used to call ConnectivityManager.requestNetwork if the
    // MMS_SERVICE_NETWORK_REQUEST_TIMEOUT_MILLIS flag is not set.
    // Given that the telephony layer will retry on failures, this timeout should be high enough.
    private static final int DEFAULT_MMS_SERVICE_NETWORK_REQUEST_TIMEOUT_MILLIS = 30 * 60 * 1000;

    // Wait timeout for this class, this is an additional delay after waiting the network request
    // timeout to make sure we don't bail prematurely.
    private static final int ADDITIONAL_NETWORK_ACQUIRE_TIMEOUT_MILLIS = (5 * 1000);

    /* Event created when receiving ACTION_CARRIER_CONFIG_CHANGED */
    private static final int EVENT_CARRIER_CONFIG_CHANGED = 1;

    private final Context mContext;

    // The requested MMS {@link android.net.Network} we are holding
    // We need this when we unbind from it. This is also used to indicate if the
    // MMS network is available.
    private Network mNetwork;
    // The current count of MMS requests that require the MMS network
    // If mMmsRequestCount is 0, we should release the MMS network.
    private int mMmsRequestCount;
    // This is really just for using the capability
    private final NetworkRequest mNetworkRequest;
    // The callback to register when we request MMS network
    private ConnectivityManager.NetworkCallback mNetworkCallback;

    private volatile ConnectivityManager mConnectivityManager;

    // The MMS HTTP client for this network
    private MmsHttpClient mMmsHttpClient;

    // The handler used for delayed release of the network
    private final Handler mReleaseHandler;

    // The task that does the delayed releasing of the network.
    private final Runnable mNetworkReleaseTask;

    // The SIM ID which we use to connect
    private final int mSubId;

    // The current Phone ID for this MmsNetworkManager
    private int mPhoneId;

    // If ACTION_SIM_CARD_STATE_CHANGED intent receiver is registered
    private boolean mSimCardStateChangedReceiverRegistered;

    private final Dependencies mDeps;

    private int mNetworkReleaseTimeoutMillis;

    // satellite transport status of associated mms active network
    private boolean  mIsSatelliteTransport;

    private EventHandler mEventHandler;

    private final class EventHandler extends Handler {
        EventHandler() {
            super(Looper.getMainLooper());
        }

        /**
         * Handles events coming from the phone stack. Overridden from handler.
         *
         * @param msg the message to handle
         */
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case EVENT_CARRIER_CONFIG_CHANGED:
                    // Reload mNetworkReleaseTimeoutMillis from CarrierConfigManager.
                    handleCarrierConfigChanged();
                    break;
                default:
                    LogUtil.e("MmsNetworkManager: ignoring message of unexpected type " + msg.what);
            }
        }
    }

    /**
     * This receiver listens to ACTION_SIM_CARD_STATE_CHANGED after starting a new NetworkRequest.
     * If ACTION_SIM_CARD_STATE_CHANGED with SIM_STATE_ABSENT for a SIM card corresponding to the
     * current NetworkRequest is received, it just releases the NetworkRequest without waiting for
     * timeout.
     */
    private final BroadcastReceiver mSimCardStateChangedReceiver =
            new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    final int simState =
                            intent.getIntExtra(
                                    TelephonyManager.EXTRA_SIM_STATE,
                                    TelephonyManager.SIM_STATE_UNKNOWN);
                    final int phoneId =
                            intent.getIntExtra(
                                    PhoneConstants.PHONE_KEY,
                                    SubscriptionManager.INVALID_PHONE_INDEX);
                    LogUtil.i("MmsNetworkManager: received ACTION_SIM_CARD_STATE_CHANGED"
                            + ", state=" + simStateString(simState) + ", phoneId=" + phoneId);

                    if (mPhoneId == phoneId && simState == TelephonyManager.SIM_STATE_ABSENT) {
                        synchronized (MmsNetworkManager.this) {
                            releaseRequestLocked(mNetworkCallback);
                            MmsNetworkManager.this.notifyAll();
                        }
                    }
                }
            };

    private static String simStateString(int state) {
        switch (state) {
            case TelephonyManager.SIM_STATE_UNKNOWN:
                return "UNKNOWN";
            case TelephonyManager.SIM_STATE_ABSENT:
                return "ABSENT";
            case TelephonyManager.SIM_STATE_CARD_IO_ERROR:
                return "CARD_IO_ERROR";
            case TelephonyManager.SIM_STATE_CARD_RESTRICTED:
                return "CARD_RESTRICTED";
            case TelephonyManager.SIM_STATE_PRESENT:
                return "PRESENT";
            default:
                return "INVALID";
        }
    }

    /**
     * This receiver listens to ACTION_CARRIER_CONFIG_CHANGED. Whenever receiving this event,
     * mNetworkReleaseTimeoutMillis needs to be reloaded from CarrierConfigManager.
     */
    private final BroadcastReceiver mCarrierConfigChangedReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            final String action = intent.getAction();
            if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(action)
                    && mSubId == intent.getIntExtra(
                            CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX,
                            SubscriptionManager.DEFAULT_SUBSCRIPTION_ID)) {
                mEventHandler.sendMessage(mEventHandler.obtainMessage(
                        EVENT_CARRIER_CONFIG_CHANGED));
            }
        }
    };

    private void handleCarrierConfigChanged() {
        final CarrierConfigManager configManager =
                (CarrierConfigManager)
                        mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
        final PersistableBundle config = configManager.getConfigForSubId(mSubId);
        mNetworkReleaseTimeoutMillis =
                config.getInt(CarrierConfigManager.KEY_MMS_NETWORK_RELEASE_TIMEOUT_MILLIS_INT);
        LogUtil.d("MmsNetworkManager: handleCarrierConfigChanged() mNetworkReleaseTimeoutMillis "
                + mNetworkReleaseTimeoutMillis);
    }

    /**
     * Network callback for our network request
     */
    private class NetworkRequestCallback extends ConnectivityManager.NetworkCallback {
        @Override
        public void onLost(Network network) {
            super.onLost(network);
            LogUtil.w("NetworkCallbackListener.onLost: network=" + network);
            synchronized (MmsNetworkManager.this) {
                // Wait for other available network. Not notify.
                if (network.equals(mNetwork)) {
                    mNetwork = null;
                    mMmsHttpClient = null;
                }
            }
        }

        @Override
        public void onUnavailable() {
            super.onUnavailable();
            LogUtil.w("NetworkCallbackListener.onUnavailable");
            synchronized (MmsNetworkManager.this) {
                releaseRequestLocked(this);
                MmsNetworkManager.this.notifyAll();
            }
        }

        @Override
        public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
            // onAvailable will always immediately be followed by a onCapabilitiesChanged. Check
            // network status here is enough.
            super.onCapabilitiesChanged(network, nc);
            LogUtil.w("NetworkCallbackListener.onCapabilitiesChanged: network="
                    + network + ", nc=" + nc);
            synchronized (MmsNetworkManager.this) {
                final boolean isAvailable =
                        nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED);
                if (network.equals(mNetwork) && !isAvailable) {
                    // Current network becomes suspended.
                    mNetwork = null;
                    mMmsHttpClient = null;
                    // Not notify. Either wait for other available network or current network to
                    // become available again.
                    return;
                }

                // New available network
                if (mNetwork == null && isAvailable) {
                    mIsSatelliteTransport = Flags.satelliteInternet()
                            && nc.hasTransport(NetworkCapabilities.TRANSPORT_SATELLITE);
                    mNetwork = network;
                    MmsNetworkManager.this.notifyAll();
                }
            }
        }
    }

    /**
     * Dependencies of MmsNetworkManager, for injection in tests.
     */
    @VisibleForTesting
    public static class Dependencies {
        /** Get phone Id from the given subId */
        public int getPhoneId(int subId) {
            return SubscriptionManager.getPhoneId(subId);
        }

        // Timeout used to call ConnectivityManager.requestNetwork. Given that the telephony layer
        // will retry on failures, this timeout should be high enough.
        public int getNetworkRequestTimeoutMillis() {
            return DeviceConfig.getInt(
                    DeviceConfig.NAMESPACE_TELEPHONY, MMS_SERVICE_NETWORK_REQUEST_TIMEOUT_MILLIS,
                    DEFAULT_MMS_SERVICE_NETWORK_REQUEST_TIMEOUT_MILLIS);
        }

        public int getAdditionalNetworkAcquireTimeoutMillis() {
            return ADDITIONAL_NETWORK_ACQUIRE_TIMEOUT_MILLIS;
        }
    }

    @VisibleForTesting
    protected MmsNetworkManager(Context context, int subId, Dependencies dependencies) {
        mContext = context;
        mDeps = dependencies;
        mNetworkCallback = null;
        mNetwork = null;
        mMmsRequestCount = 0;
        mConnectivityManager = null;
        mMmsHttpClient = null;
        mSubId = subId;
        mReleaseHandler = new Handler(Looper.getMainLooper());

        NetworkRequest.Builder builder = new NetworkRequest.Builder()
                .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
                .addCapability(NetworkCapabilities.NET_CAPABILITY_MMS)
                .setNetworkSpecifier(new TelephonyNetworkSpecifier.Builder()
                        .setSubscriptionId(mSubId).build());

        // With Satellite internet support, add satellite transport with restricted capability to
        // support mms over satellite network
        if (Flags.satelliteInternet()) {
            builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
            try {
                // TODO: b/331622062 remove the try/catch
                builder.addTransportType(NetworkCapabilities.TRANSPORT_SATELLITE);
                builder.removeCapability(NetworkCapabilities
                        .NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
            } catch (IllegalArgumentException exception) {
                LogUtil.e("TRANSPORT_SATELLITE or NOT_BANDWIDTH_CONSTRAINED is not supported.");
            }
        }
        mNetworkRequest = builder.build();

        mNetworkReleaseTask = new Runnable() {
            @Override
            public void run() {
                synchronized (this) {
                    if (mMmsRequestCount < 1) {
                        releaseRequestLocked(mNetworkCallback);
                    }
                }
            }
        };

        mEventHandler = new EventHandler();
        // Register a receiver to listen to ACTION_CARRIER_CONFIG_CHANGED
        mContext.registerReceiver(
                mCarrierConfigChangedReceiver,
                new IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
        handleCarrierConfigChanged();
    }

    public MmsNetworkManager(Context context, int subId) {
        this(context, subId, new Dependencies());
    }

    /**
     * Acquire the MMS network
     *
     * @param requestId request ID for logging
     * @throws com.android.mms.service.exception.MmsNetworkException if we fail to acquire it
     * @return The net Id of the acquired network.
     */
    public int acquireNetwork(final String requestId) throws MmsNetworkException {
        int networkRequestTimeoutMillis = mDeps.getNetworkRequestTimeoutMillis();

        synchronized (this) {
            // Since we are acquiring the network, remove the network release task if exists.
            mReleaseHandler.removeCallbacks(mNetworkReleaseTask);
            mMmsRequestCount += 1;
            if (mNetwork != null) {
                // Already available
                LogUtil.d(requestId, "MmsNetworkManager: already available");
                return mNetwork.getNetId();
            }

            if (!mSimCardStateChangedReceiverRegistered) {
                mPhoneId = mDeps.getPhoneId(mSubId);
                if (mPhoneId == SubscriptionManager.INVALID_PHONE_INDEX
                        || mPhoneId == SubscriptionManager.DEFAULT_PHONE_INDEX) {
                    throw new MmsNetworkException("Invalid Phone Id: " + mPhoneId);
                }

                // Register a receiver to listen to ACTION_SIM_CARD_STATE_CHANGED
                mContext.registerReceiver(
                        mSimCardStateChangedReceiver,
                        new IntentFilter(TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED));
                mSimCardStateChangedReceiverRegistered = true;
            }

            // Not available, so start a new request if not done yet
            if (mNetworkCallback == null) {
                LogUtil.d(requestId, "MmsNetworkManager: start new network request");
                startNewNetworkRequestLocked(networkRequestTimeoutMillis);
            }

            try {
                this.wait(networkRequestTimeoutMillis
                        + mDeps.getAdditionalNetworkAcquireTimeoutMillis());
            } catch (InterruptedException e) {
                LogUtil.w(requestId, "MmsNetworkManager: acquire network wait interrupted");
            }

            if (mSimCardStateChangedReceiverRegistered) {
                // Unregister the receiver.
                mContext.unregisterReceiver(mSimCardStateChangedReceiver);
                mSimCardStateChangedReceiverRegistered = false;
            }

            if (mNetwork != null) {
                // Success
                return mNetwork.getNetId();
            }

            if (mNetworkCallback != null) { // Timed out
                LogUtil.e(requestId,
                        "MmsNetworkManager: timed out with networkRequestTimeoutMillis="
                                + networkRequestTimeoutMillis
                                + " and ADDITIONAL_NETWORK_ACQUIRE_TIMEOUT_MILLIS="
                                + mDeps.getAdditionalNetworkAcquireTimeoutMillis());
                // Release the network request and wake up all the MmsRequests for fast-fail
                // together.
                // TODO: Start new network request for remaining MmsRequests?
                releaseRequestLocked(mNetworkCallback);
                this.notifyAll();
            }

            throw new MmsNetworkException("Acquiring network failed");
        }
    }

    /**
     * Release the MMS network when nobody is holding on to it.
     *
     * @param requestId          request ID for logging.
     * @param shouldDelayRelease whether the release should be delayed for a carrier-configured
     *                           timeout (default 5 seconds), the regular use case is to delay this
     *                           for DownloadRequests to use the network for sending an
     *                           acknowledgement on the same network.
     */
    public void releaseNetwork(final String requestId, final boolean shouldDelayRelease) {
        synchronized (this) {
            if (mMmsRequestCount > 0) {
                mMmsRequestCount -= 1;
                LogUtil.d(requestId, "MmsNetworkManager: release, count=" + mMmsRequestCount);
                if (mMmsRequestCount < 1) {
                    if (shouldDelayRelease) {
                        // remove previously posted task and post a delayed task on the release
                        // handler to release the network
                        mReleaseHandler.removeCallbacks(mNetworkReleaseTask);
                        mReleaseHandler.postDelayed(mNetworkReleaseTask,
                                mNetworkReleaseTimeoutMillis);
                    } else {
                        releaseRequestLocked(mNetworkCallback);
                    }
                }
            }
        }
    }

    /**
     * Start a new {@link android.net.NetworkRequest} for MMS
     */
    private void startNewNetworkRequestLocked(int networkRequestTimeoutMillis) {
        final ConnectivityManager connectivityManager = getConnectivityManager();
        mNetworkCallback = new NetworkRequestCallback();
        connectivityManager.requestNetwork(
                mNetworkRequest, mNetworkCallback, networkRequestTimeoutMillis);
    }

    /**
     * Release the current {@link android.net.NetworkRequest} for MMS
     *
     * @param callback the {@link android.net.ConnectivityManager.NetworkCallback} to unregister
     */
    private void releaseRequestLocked(ConnectivityManager.NetworkCallback callback) {
        if (callback != null) {
            final ConnectivityManager connectivityManager = getConnectivityManager();
            try {
                connectivityManager.unregisterNetworkCallback(callback);
            } catch (IllegalArgumentException e) {
                // It is possible ConnectivityManager.requestNetwork may fail silently due
                // to RemoteException. When that happens, we may get an invalid
                // NetworkCallback, which causes an IllegalArgumentexception when we try to
                // unregisterNetworkCallback. This exception in turn causes
                // MmsNetworkManager to skip resetLocked() in the below. Thus MMS service
                // would get stuck in the bad state until the device restarts. This fix
                // catches the exception so that state clean up can be executed.
                LogUtil.w("Unregister network callback exception", e);
            }
        }
        resetLocked();
    }

    /**
     * Reset the state
     */
    private void resetLocked() {
        mNetworkCallback = null;
        mNetwork = null;
        mMmsRequestCount = 0;
        mMmsHttpClient = null;
    }

    private @NonNull ConnectivityManager getConnectivityManager() {
        if (mConnectivityManager == null) {
            mConnectivityManager = (ConnectivityManager) mContext.getSystemService(
                    Context.CONNECTIVITY_SERVICE);
        }
        return mConnectivityManager;
    }

    /**
     * Get an MmsHttpClient for the current network
     *
     * @return The MmsHttpClient instance
     */
    public MmsHttpClient getOrCreateHttpClient() {
        synchronized (this) {
            if (mMmsHttpClient == null) {
                if (mNetwork != null) {
                    // Create new MmsHttpClient for the current Network
                    mMmsHttpClient = new MmsHttpClient(mContext, mNetwork, mConnectivityManager);
                }
            }
            return mMmsHttpClient;
        }
    }

    /**
     * Get the APN name for the active network
     *
     * @return The APN name if available, otherwise null
     */
    public String getApnName() {
        Network network = null;
        synchronized (this) {
            if (mNetwork == null) {
                return null;
            }
            network = mNetwork;
        }
        String apnName = null;
        final ConnectivityManager connectivityManager = getConnectivityManager();
        final NetworkInfo mmsNetworkInfo = connectivityManager.getNetworkInfo(network);
        if (mmsNetworkInfo != null) {
            apnName = mmsNetworkInfo.getExtraInfo();
        }
        return apnName;
    }

    @VisibleForTesting
    protected int getNetworkReleaseTimeoutMillis() {
        return mNetworkReleaseTimeoutMillis;
    }

    /**
     * Indicates satellite transport status for active network
     *
     * @return {@code true} if satellite transport, otherwise {@code false}
     */
    public boolean isSatelliteTransport() {
        LogUtil.w("satellite transport status: " + mIsSatelliteTransport);
        return mIsSatelliteTransport;
    }

}
