/*
 * Copyright (C) 2020 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.carrierconfig;

import android.annotation.Nullable;
import android.content.Context;
import android.os.Build;
import android.os.PersistableBundle;
import android.os.SystemProperties;
import android.service.carrier.CarrierIdentifier;
import android.service.carrier.CarrierService;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Provides network overrides for carrier configuration.
 *
 * The configuration available through CarrierConfigManager is a combination of default values,
 * default network overrides, and carrier overrides. The default network overrides are provided by
 * this service. For a given network, we look for a matching XML file in our assets folder, and
 * return the PersistableBundle from that file. Assets are preferred over Resources because resource
 * overlays only support using MCC+MNC and that doesn't work with MVNOs. The only resource file used
 * is vendor.xml, to provide vendor-specific overrides.
 */
public class DefaultCarrierConfigService extends CarrierService {

    private static final String SPN_EMPTY_MATCH = "null";

    private static final String CARRIER_ID_PREFIX = "carrier_config_carrierid_";

    private static final String MCCMNC_PREFIX = "carrier_config_mccmnc_";

    private static final String NO_SIM_CONFIG_FILE_NAME = "carrier_config_no_sim.xml";

    private static final String TAG = "DefaultCarrierConfigService";

    private XmlPullParserFactory mFactory;

    public DefaultCarrierConfigService() {
        Log.d(TAG, "Service created");
        mFactory = null;
    }

    /**
     * Returns per-network overrides for carrier configuration.
     *
     * This returns a carrier config bundle appropriate for the given carrier by reading data from
     * files in our assets folder. Config files in assets folder are carrier-id-indexed
     * {@link TelephonyManager#getSimCarrierId()}. NOTE: config files named after mccmnc
     * are for those without a matching carrier id and should be renamed to carrier id once the
     * missing IDs are added to
     * <a href="https://android.googlesource.com/platform/packages/providers/TelephonyProvider/+/master/assets/latest_carrier_id/carrier_list.textpb">carrier id list</a>
     *
     * First, look for file named after
     * carrier_config_carrierid_<carrierid>_<carriername>.xml if carrier id is not
     * {@link TelephonyManager#UNKNOWN_CARRIER_ID}. Note <carriername> is to improve the
     * readability which should not be used to search asset files. If there is no configuration,
     * then we look for a file named after the MCC+MNC of {@code id} as a fallback. Last, we read
     * res/xml/vendor.xml.
     *
     * carrierid.xml doesn't support multiple bundles with filters as each carrier including MVNOs
     * has its own config file named after its carrier id.
     * Both vendor.xml and MCC+MNC.xml files may contain multiple bundles with filters on them.
     * All the matching bundles are flattened to return one carrier config bundle.
     */
    @Override
    public PersistableBundle onLoadConfig(@Nullable CarrierIdentifier id) {
        Log.d(TAG, "Config being fetched");

        try {
            synchronized (this) {
                if (mFactory == null) {
                    mFactory = XmlPullParserFactory.newInstance();
                }
            }

            XmlPullParser parser = mFactory.newPullParser();

            return loadConfig(parser, id);
        }
        catch (XmlPullParserException e) {
            Log.e(TAG, "Failed to load config", e);
            return new PersistableBundle();
        }
    }

    PersistableBundle loadConfig(XmlPullParser parser, @Nullable CarrierIdentifier id) {
        PersistableBundle config = new PersistableBundle();
        // OEM customizable filter for carrier requirements not related to hardware/vendor SKU.
        String sku = getApplicationContext().getResources().getString(R.string.sku_filter);

        if (id == null) {
            try {
                // Load no SIM config if carrier id is not set.
                parser.setInput(getApplicationContext().getAssets().open(
                        NO_SIM_CONFIG_FILE_NAME), "utf-8");
                config = readConfigFromXml(parser, null, sku);

                // Treat vendor_no_sim.xml as if it were appended to the no sim config file.
                XmlPullParser vendorInput =
                        getApplicationContext().getResources().getXml(R.xml.vendor_no_sim);
                PersistableBundle vendorConfig = readConfigFromXml(vendorInput, null, sku);
                config.putAll(vendorConfig);
            }
            catch (IOException|XmlPullParserException e) {
                Log.e(TAG, "Failed to load config for no SIM", e);
            }

            return config;
        }

        try {
            if (id.getCarrierId() != TelephonyManager.UNKNOWN_CARRIER_ID) {
                PersistableBundle configByCarrierId = new PersistableBundle();
                PersistableBundle configBySpecificCarrierId = new PersistableBundle();
                PersistableBundle configByMccMncFallBackCarrierId = new PersistableBundle();
                TelephonyManager telephonyManager = getApplicationContext()
                        .getSystemService(TelephonyManager.class);
                int mccmncCarrierId = telephonyManager
                        .getCarrierIdFromMccMnc(id.getMcc() + id.getMnc());
                for (String file : getApplicationContext().getAssets().list("")) {
                    if (file.startsWith(CARRIER_ID_PREFIX + id.getSpecificCarrierId() + "_")) {
                        parser.setInput(getApplicationContext().getAssets().open(file), "utf-8");
                        configBySpecificCarrierId = readConfigFromXml(parser, null, sku);
                        break;
                    } else if (file.startsWith(CARRIER_ID_PREFIX + id.getCarrierId() + "_")) {
                        parser.setInput(getApplicationContext().getAssets().open(file), "utf-8");
                        configByCarrierId = readConfigFromXml(parser, null, sku);
                    } else if (file.startsWith(CARRIER_ID_PREFIX + mccmncCarrierId + "_")) {
                        parser.setInput(getApplicationContext().getAssets().open(file), "utf-8");
                        configByMccMncFallBackCarrierId = readConfigFromXml(parser, null, sku);
                    }
                }

                // priority: specific carrier id > carrier id > mccmnc fallback carrier id
                if (!configBySpecificCarrierId.isEmpty()) {
                    config = configBySpecificCarrierId;
                } else if (!configByCarrierId.isEmpty()) {
                    config = configByCarrierId;
                } else if (!configByMccMncFallBackCarrierId.isEmpty()) {
                    config = configByMccMncFallBackCarrierId;
                }
            }
            if (config.isEmpty()) {
                // fallback to use mccmnc.xml when there is no carrier id named config found.
                parser.setInput(getApplicationContext().getAssets().open(
                        MCCMNC_PREFIX + id.getMcc() + id.getMnc() + ".xml"), "utf-8");
                config = readConfigFromXml(parser, id, sku);
            }
        }
        catch (IOException | XmlPullParserException e) {
            Log.d(TAG, e.toString());
            // We can return an empty config for unknown networks.
            config = new PersistableBundle();
        }

        // Treat vendor.xml as if it were appended to the carrier config file we read.
        XmlPullParser vendorInput = getApplicationContext().getResources().getXml(R.xml.vendor);
        try {
            PersistableBundle vendorConfig = readConfigFromXml(vendorInput, id, sku);
            config.putAll(vendorConfig);
        }
        catch (IOException | XmlPullParserException e) {
            Log.e(TAG, e.toString());
        }

        return config;
    }

    /**
     * Parses an XML document and returns a PersistableBundle.
     *
     * <p>This function iterates over each {@code <carrier_config>} node in the XML document and
     * parses it into a bundle if its filters match {@code id}. XML documents named after carrier id
     * doesn't support filter match as each carrier including MVNOs will have its own config file.
     * The format of XML bundles is defined
     * by {@link PersistableBundle#restoreFromXml}. All the matching bundles will be flattened and
     * returned as a single bundle.</p>
     *
     * <p>Here is an example document in vendor.xml.
     * <pre>{@code
     * <carrier_config_list>
     *     <carrier_config cid="1938" name="verizon">
     *         <boolean name="voicemail_notification_persistent_bool" value="true" />
     *     </carrier_config>
     *     <carrier_config cid="1788" name="sprint">
     *         <boolean name="voicemail_notification_persistent_bool" value="false" />
     *     </carrier_config>
     * </carrier_config_list>
     * }</pre></p>
     *
     * <p>Here is an example document. The second bundle will be applied to the first only if the
     * GID1 is ABCD.
     * <pre>{@code
     * <carrier_config_list>
     *     <carrier_config>
     *         <boolean name="voicemail_notification_persistent_bool" value="true" />
     *     </carrier_config>
     *     <carrier_config gid1="ABCD">
     *         <boolean name="voicemail_notification_persistent_bool" value="false" />
     *     </carrier_config>
     * </carrier_config_list>
     * }</pre></p>
     *
     * @param parser an XmlPullParser pointing at the beginning of the document.
     * @param id the details of the SIM operator used to filter parts of the document. If read from
     *           files named after carrier id, this will be set to {@null code} as no filter match
     *           needed.
     * @param sku a filter to be customizable.
     * @return a possibly empty PersistableBundle containing the config values.
     */
    static PersistableBundle readConfigFromXml(XmlPullParser parser, @Nullable CarrierIdentifier id,
            String sku) throws IOException, XmlPullParserException {
        PersistableBundle config = new PersistableBundle();

        if (parser == null) {
          return config;
        }

        // Iterate over each <carrier_config> node in the document and add it to the returned
        // bundle if its filters match.
        int event;
        while (((event = parser.next()) != XmlPullParser.END_DOCUMENT)) {
            if (event == XmlPullParser.START_TAG && "carrier_config".equals(parser.getName())) {
                // Skip this fragment if it has filters that don't match.
                if (!checkFilters(parser, id, sku)) {
                    continue;
                }
                PersistableBundle configFragment = PersistableBundle.restoreFromXml(parser);
                config.putAll(configFragment);
            }
        }

        return config;
    }

    /**
     * Checks to see if an XML node matches carrier filters.
     *
     * <p>This iterates over the attributes of the current tag pointed to by {@code parser} and
     * checks each one against {@code id} or {@link Build.DEVICE} or {@link R.string#sku_filter} or
     * {@link Build.BOARD}. Attributes that are not specified in the node will not be checked, so a
     * node with no attributes will always return true. The supported filter attributes are,
     * <ul>
     *   <li>mcc: {@link CarrierIdentifier#getMcc}</li>
     *   <li>mnc: {@link CarrierIdentifier#getMnc}</li>
     *   <li>gid1: {@link CarrierIdentifier#getGid1}</li>
     *   <li>gid2: {@link CarrierIdentifier#getGid2}</li>
     *   <li>spn: {@link CarrierIdentifier#getSpn}</li>
     *   <li>imsi: {@link CarrierIdentifier#getImsi}</li>
     *   <li>device: {@link Build.DEVICE}</li>
     *   <li>vendorSku: {@link SystemConfig.VENDOR_SKU_PROPERTY}</li>
     *   <li>hardwareSku: {@link SystemConfig.SKU_PROPERTY}</li>
     *   <li>board: {@link Build.BOARD}</li>
     *   <li>cid: {@link CarrierIdentifier#getCarrierId()}
     *   or {@link CarrierIdentifier#getSpecificCarrierId()}</li>
     *   <li>sku: {@link R.string#sku_filter} "sku_filter" that OEM customizable filter</li>
     * </ul>
     * </p>
     *
     * <p>
     * The attributes imsi and spn can be expressed as regexp to filter on patterns.
     * The spn attribute can be set to the string "null" to allow matching against a SIM
     * with no spn set.
     * </p>
     *
     * @param parser an XmlPullParser pointing at a START_TAG with the attributes to check.
     * @param id the carrier details to check against.
     * @param sku a filter to be customizable.
     * @return false if any XML attribute does not match the corresponding value.
     */
    static boolean checkFilters(XmlPullParser parser, @Nullable CarrierIdentifier id, String sku) {
        String vendorSkuProperty = SystemProperties.get(
            "ro.boot.product.vendor.sku", "");
        String hardwareSkuProperty = SystemProperties.get(
            "ro.boot.product.hardware.sku", "");
        for (int i = 0; i < parser.getAttributeCount(); ++i) {
            boolean result = true;
            String attribute = parser.getAttributeName(i);
            String value = parser.getAttributeValue(i);
            switch (attribute) {
                case "mcc":
                    result = (id == null) || value.equals(id.getMcc());
                    break;
                case "mnc":
                    result = (id == null) || value.equals(id.getMnc());
                    break;
                case "gid1":
                    result = (id == null) || value.equalsIgnoreCase(id.getGid1());
                    break;
                case "gid2":
                    result = (id == null) || value.equalsIgnoreCase(id.getGid2());
                    break;
                case "spn":
                    result = (id == null) || matchOnSP(value, id);
                    break;
                case "imsi":
                    result = (id == null) || matchOnImsi(value, id);
                    break;
                case "device":
                    result = value.equalsIgnoreCase(Build.DEVICE);
                    break;
                case "vendorSku":
                    result = value.equalsIgnoreCase(vendorSkuProperty);
                    break;
                case "hardwareSku":
                    result = value.equalsIgnoreCase(hardwareSkuProperty);
                    break;
                case "board":
                    result = value.equalsIgnoreCase(Build.BOARD);
                    break;
                case "cid":
                    result = (id == null) || (Integer.parseInt(value) == id.getCarrierId())
                                || (Integer.parseInt(value) == id.getSpecificCarrierId());
                    break;
                case "name":
                    // name is used together with cid for readability. ignore for filter.
                    break;
                case "sku":
                    result = value.equalsIgnoreCase(sku);
                    break;
                default:
                    Log.e(TAG, "Unknown attribute " + attribute + "=" + value);
                    result = false;
                    break;
            }

            if (!result) {
                return false;
            }
        }
        return true;
    }

    /**
     * Check to see if the IMSI expression from the XML matches the IMSI of the
     * Carrier.
     *
     * @param xmlImsi IMSI expression fetched from the resource XML
     * @param id Id of the evaluated CarrierIdentifier
     * @return true if the XML IMSI matches the IMSI of CarrierIdentifier, false
     *         otherwise.
     */
    static boolean matchOnImsi(String xmlImsi, CarrierIdentifier id) {
        boolean matchFound = false;

        String currentImsi = id.getImsi();
        // If we were able to retrieve current IMSI, see if it matches.
        if (currentImsi != null) {
            Pattern imsiPattern = Pattern.compile(xmlImsi, Pattern.CASE_INSENSITIVE);
            Matcher matcher = imsiPattern.matcher(currentImsi);
            matchFound = matcher.matches();
        }
        return matchFound;
    }

    /**
     * Check to see if the service provider name expression from the XML matches the
     * CarrierIdentifier.
     *
     * @param xmlSP SP expression fetched from the resource XML
     * @param id Id of the evaluated CarrierIdentifier
     * @return true if the XML SP matches the phone's SP, false otherwise.
     */
    static boolean matchOnSP(String xmlSP, CarrierIdentifier id) {
        boolean matchFound = false;

        String currentSP = id.getSpn();
        if (SPN_EMPTY_MATCH.equalsIgnoreCase(xmlSP)) {
            if (TextUtils.isEmpty(currentSP)) {
                matchFound = true;
            }
        } else if (currentSP != null) {
            Pattern spPattern = Pattern.compile(xmlSP, Pattern.CASE_INSENSITIVE);
            Matcher matcher = spPattern.matcher(currentSP);
            matchFound = matcher.matches();
        }
        return matchFound;
    }
}
