/*
 * Copyright (C) 2017 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.internal.telephony;

import static java.nio.charset.StandardCharsets.UTF_8;

import android.annotation.NonNull;
import android.app.AlarmManager;
import android.app.DownloadManager;
import android.app.KeyguardManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.os.PersistableBundle;
import android.os.UserManager;
import android.telephony.CarrierConfigManager;
import android.telephony.ImsiEncryptionInfo;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;


import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.flags.Flags;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;


import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.PublicKey;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipException;

/**
 * This class contains logic to get Certificates and keep them current.
 * The class will be instantiated by various Phone implementations.
 */
public class CarrierKeyDownloadManager extends Handler {
    private static final String LOG_TAG = "CarrierKeyDownloadManager";

    private static final String CERT_BEGIN_STRING = "-----BEGIN CERTIFICATE-----";

    private static final String CERT_END_STRING = "-----END CERTIFICATE-----";

    private static final int DAY_IN_MILLIS = 24 * 3600 * 1000;

    // Create a window prior to the key expiration, during which the cert will be
    // downloaded. Defines the start date of that window. So if the key expires on
    // Dec  21st, the start of the renewal window will be Dec 1st.
    private static final int START_RENEWAL_WINDOW_DAYS = 21;

    // This will define the end date of the window.
    private static final int END_RENEWAL_WINDOW_DAYS = 7;

    /* Intent for downloading the public key */
    private static final String INTENT_KEY_RENEWAL_ALARM_PREFIX =
            "com.android.internal.telephony.carrier_key_download_alarm";

    @VisibleForTesting
    public int mKeyAvailability = 0;

    private static final String JSON_CERTIFICATE = "certificate";
    private static final String JSON_CERTIFICATE_ALTERNATE = "public-key";
    private static final String JSON_TYPE = "key-type";
    private static final String JSON_IDENTIFIER = "key-identifier";
    private static final String JSON_CARRIER_KEYS = "carrier-keys";
    private static final String JSON_TYPE_VALUE_WLAN = "WLAN";
    private static final String JSON_TYPE_VALUE_EPDG = "EPDG";

    private static final int EVENT_ALARM_OR_CONFIG_CHANGE = 0;
    private static final int EVENT_DOWNLOAD_COMPLETE = 1;
    private static final int EVENT_NETWORK_AVAILABLE = 2;
    private static final int EVENT_SCREEN_UNLOCKED = 3;


    private static final int[] CARRIER_KEY_TYPES = {TelephonyManager.KEY_TYPE_EPDG,
            TelephonyManager.KEY_TYPE_WLAN};

    private final Phone mPhone;
    private final Context mContext;
    public final DownloadManager mDownloadManager;
    private String mURL;
    private boolean mAllowedOverMeteredNetwork = false;
    private boolean mDeleteOldKeyAfterDownload = false;
    private boolean mIsRequiredToHandleUnlock;
    private final TelephonyManager mTelephonyManager;
    private UserManager mUserManager;
    @VisibleForTesting
    public String mMccMncForDownload = "";
    public int mCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
    @VisibleForTesting
    public long mDownloadId;
    private DefaultNetworkCallback mDefaultNetworkCallback;
    private ConnectivityManager mConnectivityManager;
    private KeyguardManager mKeyguardManager;

    public CarrierKeyDownloadManager(Phone phone) {
        mPhone = phone;
        mContext = phone.getContext();
        IntentFilter filter = new IntentFilter();
        filter.addAction(INTENT_KEY_RENEWAL_ALARM_PREFIX);
        filter.addAction(TelephonyIntents.ACTION_CARRIER_CERTIFICATE_DOWNLOAD);
        if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) {
            filter.addAction(Intent.ACTION_USER_UNLOCKED);
        }
        mContext.registerReceiver(mBroadcastReceiver, filter, null, phone);
        mDownloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
        mTelephonyManager = mContext.getSystemService(TelephonyManager.class)
                .createForSubscriptionId(mPhone.getSubId());
        if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) {
            mKeyguardManager = mContext.getSystemService(KeyguardManager.class);
        } else {
            mUserManager = mContext.getSystemService(UserManager.class);
        }
        CarrierConfigManager carrierConfigManager = mContext.getSystemService(
                CarrierConfigManager.class);
        // Callback which directly handle config change should be executed on handler thread
        carrierConfigManager.registerCarrierConfigChangeListener(this::post,
                (slotIndex, subId, carrierId, specificCarrierId) -> {
                    if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) {
                        logd("CarrierConfig changed slotIndex = " + slotIndex + " subId = " + subId
                                + " CarrierId = " + carrierId + " phoneId = "
                                + mPhone.getPhoneId());
                        // Below checks are necessary to optimise the logic.
                        if ((slotIndex == mPhone.getPhoneId()) && (carrierId > 0
                                || !TextUtils.isEmpty(
                                mMccMncForDownload))) {
                            mCarrierId = carrierId;
                            updateSimOperator();
                            // If device is screen locked do not proceed to handle
                            // EVENT_ALARM_OR_CONFIG_CHANGE
                            if (mKeyguardManager.isDeviceLocked()) {
                                logd("Device is Locked");
                                mIsRequiredToHandleUnlock = true;
                            } else {
                                logd("Carrier Config changed: slotIndex=" + slotIndex);
                                sendEmptyMessage(EVENT_ALARM_OR_CONFIG_CHANGE);
                            }
                        }
                    } else {
                        boolean isUserUnlocked = mUserManager.isUserUnlocked();

                        if (isUserUnlocked && slotIndex == mPhone.getPhoneId()) {
                            Log.d(LOG_TAG, "Carrier Config changed: slotIndex=" + slotIndex);
                            handleAlarmOrConfigChange();
                        } else {
                            Log.d(LOG_TAG, "User is locked");
                            mContext.registerReceiver(mUserUnlockedReceiver, new IntentFilter(
                                    Intent.ACTION_USER_UNLOCKED));
                        }
                    }
                });
        mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
    }

    // TODO remove this method upon imsiKeyRetryDownloadOnPhoneUnlock enabled.
    private final BroadcastReceiver mUserUnlockedReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
                Log.d(LOG_TAG, "Received UserUnlockedReceiver");
                handleAlarmOrConfigChange();
            }
        }
    };

    private final BroadcastReceiver mDownloadReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (action.equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) {
                logd("Download Complete");
                sendMessage(obtainMessage(EVENT_DOWNLOAD_COMPLETE,
                        intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0)));
            }
        }
    };

    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            int slotIndex = SubscriptionManager.getSlotIndex(mPhone.getSubId());
            int phoneId = mPhone.getPhoneId();
            switch (action) {
                case INTENT_KEY_RENEWAL_ALARM_PREFIX -> {
                    int slotIndexExtra = intent.getIntExtra(SubscriptionManager.EXTRA_SLOT_INDEX,
                            -1);
                    if (slotIndexExtra == slotIndex) {
                        logd("Handling key renewal alarm: " + action);
                        if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) {
                            updateSimOperator();
                        }
                        sendEmptyMessage(EVENT_ALARM_OR_CONFIG_CHANGE);
                    }
                }
                case TelephonyIntents.ACTION_CARRIER_CERTIFICATE_DOWNLOAD -> {
                    if (phoneId == intent.getIntExtra(PhoneConstants.PHONE_KEY,
                            SubscriptionManager.INVALID_SIM_SLOT_INDEX)) {
                        logd("Handling reset intent: " + action);
                        sendEmptyMessage(EVENT_ALARM_OR_CONFIG_CHANGE);
                    }
                }
                case Intent.ACTION_USER_UNLOCKED -> {
                    // The Carrier key download fails when SIM is inserted while device is locked
                    // hence adding a retry logic when device is unlocked.
                    logd("device fully unlocked, isRequiredToHandleUnlock = "
                            + mIsRequiredToHandleUnlock
                            + ", slotIndex = " + slotIndex + "  hasActiveDataNetwork = " + (
                            mConnectivityManager.getActiveNetwork() != null));
                    if (mIsRequiredToHandleUnlock) {
                        mIsRequiredToHandleUnlock = false;
                        sendEmptyMessage(EVENT_SCREEN_UNLOCKED);
                    }
                }
            }
        }
    };

    @Override
    public void handleMessage (Message msg) {
        switch (msg.what) {
            case EVENT_ALARM_OR_CONFIG_CHANGE, EVENT_NETWORK_AVAILABLE, EVENT_SCREEN_UNLOCKED ->
                    handleAlarmOrConfigChange();
            case EVENT_DOWNLOAD_COMPLETE -> {
                long carrierKeyDownloadIdentifier = (long) msg.obj;
                String currentMccMnc = Flags.imsiKeyRetryDownloadOnPhoneUnlock()
                        ? mTelephonyManager.getSimOperator(mPhone.getSubId()) : getSimOperator();
                int carrierId = Flags.imsiKeyRetryDownloadOnPhoneUnlock()
                        ? mTelephonyManager.getSimCarrierId() : getSimCarrierId();
                if (isValidDownload(currentMccMnc, carrierKeyDownloadIdentifier, carrierId)) {
                    onDownloadComplete(carrierKeyDownloadIdentifier, currentMccMnc, carrierId);
                    onPostDownloadProcessing();
                }
            }
        }
    }

    private void onPostDownloadProcessing() {
        resetRenewalAlarm();
        if(Flags.imsiKeyRetryDownloadOnPhoneUnlock()) {
            mDownloadId = -1;
        } else {
            cleanupDownloadInfo();
        }
        // unregister from DOWNLOAD_COMPLETE
        mContext.unregisterReceiver(mDownloadReceiver);
    }

    private void handleAlarmOrConfigChange() {
        if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) {
            if (carrierUsesKeys()) {
                if (areCarrierKeysAbsentOrExpiring()) {
                    boolean hasActiveDataNetwork =
                            (mConnectivityManager.getActiveNetwork() != null);
                    boolean downloadStartedSuccessfully = hasActiveDataNetwork && downloadKey();
                    // if the download was attempted, but not started successfully, and if
                    // carriers uses keys, we'll still want to renew the alarms, and try
                    // downloading the key a day later.
                    int slotIndex = SubscriptionManager.getSlotIndex(mPhone.getSubId());
                    if (downloadStartedSuccessfully) {
                        unregisterDefaultNetworkCb(slotIndex);
                    } else {
                        // If download fails due to the device lock, we will reattempt once the
                        // device is unlocked.
                        mIsRequiredToHandleUnlock = mKeyguardManager.isDeviceLocked();
                        loge("hasActiveDataConnection = " + hasActiveDataNetwork
                                + "    isDeviceLocked = " + mIsRequiredToHandleUnlock);
                        if (!hasActiveDataNetwork) {
                            registerDefaultNetworkCb(slotIndex);
                        }
                        resetRenewalAlarm();
                    }
                }
                logd("handleAlarmOrConfigChange :: areCarrierKeysAbsentOrExpiring returned false");
            } else {
                cleanupRenewalAlarms();
                if (!isOtherSlotHasCarrier()) {
                    // delete any existing alarms.
                    mPhone.deleteCarrierInfoForImsiEncryption(getSimCarrierId(), getSimOperator());
                }
                cleanupDownloadInfo();
            }
        } else {
            if (carrierUsesKeys()) {
                if (areCarrierKeysAbsentOrExpiring()) {
                    boolean downloadStartedSuccessfully = downloadKey();
                    // if the download was attempted, but not started successfully, and if
                    // carriers uses keys, we'll still want to renew the alarms, and try
                    // downloading the key a day later.
                    if (!downloadStartedSuccessfully) {
                        resetRenewalAlarm();
                    }
                }
            } else {
                // delete any existing alarms.
                cleanupRenewalAlarms();
                mPhone.deleteCarrierInfoForImsiEncryption(getSimCarrierId());
            }
        }
    }

    private boolean isOtherSlotHasCarrier() {
        SubscriptionManager subscriptionManager = mPhone.getContext().getSystemService(
                SubscriptionManager.class);
        List<SubscriptionInfo> subscriptionInfoList =
                subscriptionManager.getActiveSubscriptionInfoList();
        loge("handleAlarmOrConfigChange ActiveSubscriptionInfoList = " + (
                (subscriptionInfoList != null) ? subscriptionInfoList.size() : null));
        for (SubscriptionInfo subInfo : subscriptionInfoList) {
            if (mPhone.getSubId() != subInfo.getSubscriptionId()
                    && subInfo.getCarrierId() == mPhone.getCarrierId()) {
                // We do not proceed to remove the Key from the DB as another slot contains
                // same operator sim which is in active state.
                loge("handleAlarmOrConfigChange same operator sim in another slot");
                return true;
            }
        }
        return false;
    }

    private void cleanupDownloadInfo() {
        logd("Cleaning up download info");
        mDownloadId = -1;
        if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) {
            mMccMncForDownload = "";
        } else {
            mMccMncForDownload = null;
        }
        mCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
    }

    private void cleanupRenewalAlarms() {
        logd("Cleaning up existing renewal alarms");
        int slotIndex = SubscriptionManager.getSlotIndex(mPhone.getSubId());
        Intent intent = new Intent(INTENT_KEY_RENEWAL_ALARM_PREFIX);
        intent.putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, slotIndex);
        PendingIntent carrierKeyDownloadIntent = PendingIntent.getBroadcast(mContext, 0, intent,
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
        AlarmManager alarmManager =mContext.getSystemService(AlarmManager.class);
        alarmManager.cancel(carrierKeyDownloadIntent);
    }

    /**
     * this method returns the date to be used to decide on when to start downloading the key.
     * from the carrier.
     **/
    @VisibleForTesting
    public long getExpirationDate()  {
        long minExpirationDate = Long.MAX_VALUE;
        for (int key_type : CARRIER_KEY_TYPES) {
            if (!isKeyEnabled(key_type)) {
                continue;
            }
            ImsiEncryptionInfo imsiEncryptionInfo =
                    mPhone.getCarrierInfoForImsiEncryption(key_type, false);
            if (imsiEncryptionInfo != null && imsiEncryptionInfo.getExpirationTime() != null) {
                if (minExpirationDate > imsiEncryptionInfo.getExpirationTime().getTime()) {
                    minExpirationDate = imsiEncryptionInfo.getExpirationTime().getTime();
                }
            }
        }

        // if there are no keys, or expiration date is in the past, or within 7 days, then we
        // set the alarm to run in a day. Else, we'll set the alarm to run 7 days prior to
        // expiration.
        if (minExpirationDate == Long.MAX_VALUE || (minExpirationDate
                < System.currentTimeMillis() + END_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS)) {
            minExpirationDate = System.currentTimeMillis() + DAY_IN_MILLIS;
        } else {
            // We don't want all the phones to download the certs simultaneously, so
            // we pick a random time during the download window to avoid this situation.
            Random random = new Random();
            int max = START_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS;
            int min = END_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS;
            int randomTime = random.nextInt(max - min) + min;
            minExpirationDate = minExpirationDate - randomTime;
        }
        return minExpirationDate;
    }

    /**
     * this method resets the alarm. Starts by cleaning up the existing alarms.
     * We look at the earliest expiration date, and setup an alarms X days prior.
     * If the expiration date is in the past, we'll setup an alarm to run the next day. This
     * could happen if the download has failed.
     **/
    @VisibleForTesting
    public void resetRenewalAlarm() {
        cleanupRenewalAlarms();
        int slotIndex = SubscriptionManager.getSlotIndex(mPhone.getSubId());
        long minExpirationDate = getExpirationDate();
        logd("minExpirationDate: " + new Date(minExpirationDate));
        final AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(
                Context.ALARM_SERVICE);
        Intent intent = new Intent(INTENT_KEY_RENEWAL_ALARM_PREFIX);
        intent.putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, slotIndex);
        PendingIntent carrierKeyDownloadIntent = PendingIntent.getBroadcast(mContext, 0, intent,
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
        alarmManager.set(AlarmManager.RTC_WAKEUP, minExpirationDate, carrierKeyDownloadIntent);
        logd("setRenewalAlarm: action=" + intent.getAction() + " time="
                + new Date(minExpirationDate));
    }

    /**
     * Read the store the sim operetor value and update the value in case of change in the sim
     * operetor.
     */
    public void updateSimOperator() {
        if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) {
            String simOperator = mPhone.getOperatorNumeric();
            if (!TextUtils.isEmpty(simOperator) && !simOperator.equals(mMccMncForDownload)) {
                mMccMncForDownload = simOperator;
                logd("updateSimOperator, Initialized mMccMncForDownload = " + mMccMncForDownload);
            }
        }
    }

    /**
     * Returns the sim operator.
     **/
    @VisibleForTesting
    public String getSimOperator() {
        if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) {
            updateSimOperator();
            return mMccMncForDownload;
        } else {
            return mTelephonyManager.getSimOperator(mPhone.getSubId());
        }
    }

    /**
     * Returns the sim operator.
     **/
    @VisibleForTesting
    public int getSimCarrierId() {
        if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) {
            return (mCarrierId > 0) ? mCarrierId : mPhone.getCarrierId();
        } else {
            return mTelephonyManager.getSimCarrierId();
        }
    }

    /**
     *  checks if the download was sent by this particular instance. We do this by including the
     *  slot id in the key. If no value is found, we know that the download was not for this
     *  instance of the phone.
     **/
    @VisibleForTesting
    public boolean isValidDownload(String currentMccMnc, long currentDownloadId, int carrierId) {
        if (currentDownloadId != mDownloadId) {
            loge( "download ID=" + currentDownloadId
                    + " for completed download does not match stored id=" + mDownloadId);
            return false;
        }

        if (TextUtils.isEmpty(currentMccMnc) || TextUtils.isEmpty(mMccMncForDownload)
                || !TextUtils.equals(currentMccMnc, mMccMncForDownload)
                || mCarrierId != carrierId) {
            loge( "currentMccMnc=" + currentMccMnc + " storedMccMnc =" + mMccMncForDownload
                    + "currentCarrierId = " + carrierId + "  storedCarrierId = " + mCarrierId);
            return false;
        }

        logd("Matched MccMnc =  " + currentMccMnc + ", carrierId = " + carrierId
                + ", downloadId: " + currentDownloadId);
        return true;
    }

    /**
     * This method will try to parse the downloaded information, and persist it in the database.
     **/
    private void onDownloadComplete(long carrierKeyDownloadIdentifier, String mccMnc,
            int carrierId) {
        logd("onDownloadComplete: " + carrierKeyDownloadIdentifier);
        String jsonStr;
        DownloadManager.Query query = new DownloadManager.Query();
        query.setFilterById(carrierKeyDownloadIdentifier);
        Cursor cursor = mDownloadManager.query(query);

        if (cursor == null) {
            return;
        }
        if (cursor.moveToFirst()) {
            int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
            if (DownloadManager.STATUS_SUCCESSFUL == cursor.getInt(columnIndex)) {
                try {
                    jsonStr = convertToString(mDownloadManager, carrierKeyDownloadIdentifier);
                    if (TextUtils.isEmpty(jsonStr)) {
                        logd("fallback to no gzip");
                        jsonStr = convertToStringNoGZip(mDownloadManager,
                                carrierKeyDownloadIdentifier);
                    }
                    parseJsonAndPersistKey(jsonStr, mccMnc, carrierId);
                } catch (Exception e) {
                    loge( "Error in download:" + carrierKeyDownloadIdentifier
                            + ". " + e);
                } finally {
                    mDownloadManager.remove(carrierKeyDownloadIdentifier);
                }
            }
            logd("Completed downloading keys");
        }
        cursor.close();
    }

    /**
     * This method checks if the carrier requires key. We'll read the carrier config to make that
     * determination.
     * @return boolean returns true if carrier requires keys, else false.
     **/
    private boolean carrierUsesKeys() {
        CarrierConfigManager carrierConfigManager = (CarrierConfigManager)
                mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
        if (carrierConfigManager == null) {
            return false;
        }
        int subId = mPhone.getSubId();
        PersistableBundle b = null;
        try {
            b = carrierConfigManager.getConfigForSubId(subId,
                    CarrierConfigManager.IMSI_KEY_AVAILABILITY_INT,
                    CarrierConfigManager.IMSI_KEY_DOWNLOAD_URL_STRING,
                    CarrierConfigManager.KEY_ALLOW_METERED_NETWORK_FOR_CERT_DOWNLOAD_BOOL);
        } catch (RuntimeException e) {
            loge( "CarrierConfigLoader is not available.");
        }
        if (b == null || b.isEmpty()) {
            return false;
        }

        mKeyAvailability = b.getInt(CarrierConfigManager.IMSI_KEY_AVAILABILITY_INT);
        mURL = b.getString(CarrierConfigManager.IMSI_KEY_DOWNLOAD_URL_STRING);
        mAllowedOverMeteredNetwork = b.getBoolean(
                CarrierConfigManager.KEY_ALLOW_METERED_NETWORK_FOR_CERT_DOWNLOAD_BOOL);

        if (mKeyAvailability == 0 || TextUtils.isEmpty(mURL)) {
            logd("Carrier not enabled or invalid values. mKeyAvailability=" + mKeyAvailability
                    + " mURL=" + mURL);
            return false;
        }
        for (int key_type : CARRIER_KEY_TYPES) {
            if (isKeyEnabled(key_type)) {
                return true;
            }
        }
        return false;
    }

    private static String convertToStringNoGZip(DownloadManager downloadManager, long downloadId) {
        StringBuilder sb = new StringBuilder();
        try (InputStream source = new FileInputStream(
                downloadManager.openDownloadedFile(downloadId).getFileDescriptor())) {
            // If the carrier does not have the data gzipped, fallback to assuming it is not zipped.
            // parseJsonAndPersistKey may still fail if the data is malformed, so we won't be
            // persisting random bogus strings thinking it's the cert
            BufferedReader reader = new BufferedReader(new InputStreamReader(source, UTF_8));

            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line).append('\n');
            }
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
        return sb.toString();
    }

    private static String convertToString(DownloadManager downloadManager, long downloadId) {
        try (InputStream source = new FileInputStream(
                downloadManager.openDownloadedFile(downloadId).getFileDescriptor());
             InputStream gzipIs = new GZIPInputStream(source)) {
            BufferedReader reader = new BufferedReader(new InputStreamReader(gzipIs, UTF_8));
            StringBuilder sb = new StringBuilder();

            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line).append('\n');
            }
            return sb.toString();
        } catch (ZipException e) {
            // GZIPInputStream constructor will throw exception if stream is not GZIP
            Log.d(LOG_TAG, "Stream is not gzipped e=" + e);
            return null;
        } catch (IOException e) {
            Log.e(LOG_TAG, "Unexpected exception in convertToString e=" + e);
            return null;
        }
    }

    /**
     * Converts the string into a json object to retreive the nodes. The Json should have 3 nodes,
     * including the Carrier public key, the key type and the key identifier. Once the nodes have
     * been extracted, they get persisted to the database. Sample:
     *      "carrier-keys": [ { "certificate": "",
     *                         "key-type": "WLAN",
     *                         "key-identifier": ""
     *                        } ]
     * @param jsonStr the json string.
     * @param mccMnc contains the mcc, mnc.
     */
    @VisibleForTesting
    public void parseJsonAndPersistKey(String jsonStr, String mccMnc, int carrierId) {
        if (TextUtils.isEmpty(jsonStr) || TextUtils.isEmpty(mccMnc)
                || carrierId == TelephonyManager.UNKNOWN_CARRIER_ID) {
            loge( "jsonStr or mcc, mnc: is empty or carrierId is UNKNOWN_CARRIER_ID");
            return;
        }
        try {
            String mcc = mccMnc.substring(0, 3);
            String mnc = mccMnc.substring(3);
            JSONObject jsonObj = new JSONObject(jsonStr);
            JSONArray keys = jsonObj.getJSONArray(JSON_CARRIER_KEYS);
            for (int i = 0; i < keys.length(); i++) {
                JSONObject key = keys.getJSONObject(i);
                // Support both "public-key" and "certificate" String property.
                String cert = null;
                if (key.has(JSON_CERTIFICATE)) {
                    cert = key.getString(JSON_CERTIFICATE);
                } else {
                    cert = key.getString(JSON_CERTIFICATE_ALTERNATE);
                }
                // The key-type property is optional, therefore, the default value is WLAN type if
                // not specified.
                int type = TelephonyManager.KEY_TYPE_WLAN;
                if (key.has(JSON_TYPE)) {
                    String typeString = key.getString(JSON_TYPE);
                    if (typeString.equals(JSON_TYPE_VALUE_EPDG)) {
                        type = TelephonyManager.KEY_TYPE_EPDG;
                    } else if (!typeString.equals(JSON_TYPE_VALUE_WLAN)) {
                        loge( "Invalid key-type specified: " + typeString);
                    }
                }
                String identifier = key.getString(JSON_IDENTIFIER);
                Pair<PublicKey, Long> keyInfo =
                        getKeyInformation(cleanCertString(cert).getBytes());
                if (mDeleteOldKeyAfterDownload) {
                    if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) {
                        mPhone.deleteCarrierInfoForImsiEncryption(
                                TelephonyManager.UNKNOWN_CARRIER_ID, null);
                    } else {
                        mPhone.deleteCarrierInfoForImsiEncryption(
                                TelephonyManager.UNKNOWN_CARRIER_ID);
                    }
                    mDeleteOldKeyAfterDownload = false;
                }
                savePublicKey(keyInfo.first, type, identifier, keyInfo.second, mcc, mnc, carrierId);
            }
        } catch (final JSONException e) {
            loge( "Json parsing error: " + e.getMessage());
        } catch (final Exception e) {
            loge( "Exception getting certificate: " + e);
        }
    }

    /**
     * introspects the mKeyAvailability bitmask
     * @return true if the digit at position k is 1, else false.
     */
    @VisibleForTesting
    public boolean isKeyEnabled(int keyType) {
        // since keytype has values of 1, 2.... we need to subtract 1 from the keytype.
        return isKeyEnabled(keyType, mKeyAvailability);
    }

    /**
     * introspects the mKeyAvailability bitmask
     * @return true if the digit at position k is 1, else false.
     */
    public static boolean isKeyEnabled(int keyType, int keyAvailability) {
        // since keytype has values of 1, 2.... we need to subtract 1 from the keytype.
        int returnValue = (keyAvailability >> (keyType - 1)) & 1;
        return returnValue == 1;
    }

    /**
     * Checks whether is the keys are absent or close to expiration. Returns true, if either of
     * those conditions are true.
     * @return boolean returns true when keys are absent or close to expiration, else false.
     */
    @VisibleForTesting
    public boolean areCarrierKeysAbsentOrExpiring() {
        for (int key_type : CARRIER_KEY_TYPES) {
            if (!isKeyEnabled(key_type)) {
                continue;
            }
            // get encryption info with fallback=false so that we attempt a download even if there's
            // backup info stored in carrier config
            ImsiEncryptionInfo imsiEncryptionInfo =
                    mPhone.getCarrierInfoForImsiEncryption(key_type, false);
            if (imsiEncryptionInfo == null) {
                logd("Key not found for: " + key_type);
                return true;
            } else if (imsiEncryptionInfo.getCarrierId() == TelephonyManager.UNKNOWN_CARRIER_ID) {
                logd("carrier key is unknown carrier, so prefer to reDownload");
                mDeleteOldKeyAfterDownload = true;
                return true;
            }
            Date imsiDate = imsiEncryptionInfo.getExpirationTime();
            long timeToExpire = imsiDate.getTime() - System.currentTimeMillis();
            return timeToExpire < START_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS;
        }
        return false;
    }

    private boolean downloadKey() {
        logd("starting download from: " + mURL);
        String mccMnc = null;
        int carrierId = -1;
        if (Flags.imsiKeyRetryDownloadOnPhoneUnlock()) {
            if (TextUtils.isEmpty(mMccMncForDownload)
                    || mCarrierId == TelephonyManager.UNKNOWN_CARRIER_ID) {
                loge("mccmnc or carrierId is UnKnown");
                return false;
            }
        } else {
            mccMnc = getSimOperator();
            carrierId = getSimCarrierId();
            if (!TextUtils.isEmpty(mccMnc) || carrierId != TelephonyManager.UNKNOWN_CARRIER_ID) {
                Log.d(LOG_TAG, "downloading key for mccmnc : " + mccMnc + ", carrierId : "
                        + carrierId);
            } else {
                Log.e(LOG_TAG, "mccmnc or carrierId is UnKnown");
                return false;
            }
        }

        try {
            // register the broadcast receiver to listen for download complete
            IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
            mContext.registerReceiver(mDownloadReceiver, filter, null, mPhone,
                    Context.RECEIVER_EXPORTED);

            DownloadManager.Request request = new DownloadManager.Request(Uri.parse(mURL));

            // TODO(b/128550341): Implement the logic to minimize using metered network such as
            // LTE for downloading a certificate.
            request.setAllowedOverMetered(mAllowedOverMeteredNetwork);
            request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);
            request.addRequestHeader("Accept-Encoding", "gzip");
            long carrierKeyDownloadRequestId = mDownloadManager.enqueue(request);
            if (!Flags.imsiKeyRetryDownloadOnPhoneUnlock()) {
                mMccMncForDownload = mccMnc;
                mCarrierId = carrierId;
            }
            mDownloadId = carrierKeyDownloadRequestId;
            logd("saving values mccmnc: " + mMccMncForDownload + ", downloadId: "
                    + carrierKeyDownloadRequestId + ", carrierId: " + mCarrierId);
        } catch (Exception e) {
            loge( "exception trying to download key from url: " + mURL + ",  Exception = "
                    + e.getMessage());
            return false;
        }
        return true;
    }

    /**
     * Save the public key
     * @param certificate certificate that contains the public key.
     * @return Pair containing the Public Key and the expiration date.
     **/
    @VisibleForTesting
    public static Pair<PublicKey, Long> getKeyInformation(byte[] certificate) throws Exception {
        InputStream inStream = new ByteArrayInputStream(certificate);
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        X509Certificate cert = (X509Certificate) cf.generateCertificate(inStream);
        Pair<PublicKey, Long> keyInformation =
                new Pair<>(cert.getPublicKey(), cert.getNotAfter().getTime());
        return keyInformation;
    }

    /**
     * Save the public key
     * @param publicKey public key.
     * @param type key-type.
     * @param identifier which is an opaque string.
     * @param expirationDate expiration date of the key.
     * @param mcc
     * @param mnc
     **/
    @VisibleForTesting
    public void savePublicKey(PublicKey publicKey, int type, String identifier, long expirationDate,
            String mcc, String mnc, int carrierId) {
        ImsiEncryptionInfo imsiEncryptionInfo = new ImsiEncryptionInfo(mcc, mnc,
                type, identifier, publicKey, new Date(expirationDate), carrierId);
        mPhone.setCarrierInfoForImsiEncryption(imsiEncryptionInfo);
    }

    /**
     * Remove potential extraneous text in a certificate string
     * @param cert certificate string
     * @return Cleaned up version of the certificate string
     */
    @VisibleForTesting
    public static String cleanCertString(String cert) {
        return cert.substring(
                cert.indexOf(CERT_BEGIN_STRING),
                cert.indexOf(CERT_END_STRING) + CERT_END_STRING.length());
    }

    /**
     * Registering the callback to listen on data connection availability.
     *
     * @param slotId Sim slotIndex that tries to download the key.
     */
    private void registerDefaultNetworkCb(int slotId) {
        logd("RegisterDefaultNetworkCb for slotId = " + slotId);
        if (mDefaultNetworkCallback == null
                && slotId != SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
            mDefaultNetworkCallback = new DefaultNetworkCallback(slotId);
            mConnectivityManager.registerDefaultNetworkCallback(mDefaultNetworkCallback, this);
        }
    }

    /**
     * Unregister the data connection monitor listener.
     */
    private void unregisterDefaultNetworkCb(int slotId) {
        logd("unregisterDefaultNetworkCb for slotId = " + slotId);
        if (mDefaultNetworkCallback != null) {
            mConnectivityManager.unregisterNetworkCallback(mDefaultNetworkCallback);
            mDefaultNetworkCallback = null;
        }
    }

    final class DefaultNetworkCallback extends ConnectivityManager.NetworkCallback {
        final int mSlotIndex;

        public DefaultNetworkCallback(int slotId) {
            mSlotIndex = slotId;
        }

        /** Called when the framework connects and has declared a new network ready for use. */
        @Override
        public void onAvailable(@NonNull Network network) {
            logd("Data network connected, slotId = " + mSlotIndex);
            if (mConnectivityManager.getActiveNetwork() != null && SubscriptionManager.getSlotIndex(
                    mPhone.getSubId()) == mSlotIndex) {
                mIsRequiredToHandleUnlock = false;
                unregisterDefaultNetworkCb(mSlotIndex);
                sendEmptyMessage(EVENT_NETWORK_AVAILABLE);
            }
        }
    }

    private void loge(String logStr) {
        String TAG = LOG_TAG + " [" + mPhone.getPhoneId() + "]";
        Log.e(TAG, logStr);
    }

    private void logd(String logStr) {
        String TAG = LOG_TAG + " [" + mPhone.getPhoneId() + "]";
        Log.d(TAG, logStr);
    }
}
