//@formatter:off
/**
 * DatapointHelper.java Copyright (C) 2011 Char Software Inc., DBA Localytics This code is provided under the Localytics Modified
 * BSD License. A copy of this license has been distributed in a file called LICENSE with this source code. Please visit
 * www.localytics.com for more information.
 */
//@formatter:on

package com.localytics.android;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.Manifest.permission;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.telephony.TelephonyManager;
import android.util.Log;

/**
 * Provides a number of static functions to aid in the collection and formatting of datapoints.
 * <p>
 * Note: this is not a public API.
 */
/* package */final class DatapointHelper
{
    /**
     * AndroidID known to be duplicated across many devices due to manufacturer bugs.
     */
    private static final String INVALID_ANDROID_ID = "9774d56d682e549c"; //$NON-NLS-1$

    /**
     * The path to the device_id file in previous versions of the Localytics library
     */
    private static final String LEGACY_DEVICE_ID_FILE = "/localytics/device_id"; //$NON-NLS-1$

    /**
     * Private constructor prevents instantiation
     *
     * @throws UnsupportedOperationException because this class cannot be instantiated.
     */
    private DatapointHelper()
    {
        throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$
    }

    /**
     * @return current Android API level.
     */
    /* package */static int getApiLevel()
    {
        try
        {
            // Although the Build.VERSION.SDK field has existed since API 1, it is deprecated and could be removed
            // in the future. Therefore use reflection to retrieve it for maximum forward compatibility.
            final Class<?> buildClass = Build.VERSION.class;
            final String sdkString = (String) buildClass.getField("SDK").get(null); //$NON-NLS-1$
            return Integer.parseInt(sdkString);
        }
        catch (final Exception e)
        {
            Log.w(Constants.LOG_TAG, "Caught exception", e); //$NON-NLS-1$

            // Although probably not necessary, protects from the aforementioned deprecation
            try
            {
                final Class<?> buildClass = Build.VERSION.class;
                return buildClass.getField("SDK_INT").getInt(null); //$NON-NLS-1$
            }
            catch (final Exception ignore)
            {
                if (Constants.IS_LOGGABLE)
                {
                    Log.w(Constants.LOG_TAG, "Caught exception", ignore); //$NON-NLS-1$
                }
            }
        }

        // Worse-case scenario, assume Cupcake
        return 3;
    }

    /**
     * Gets a 1-way hashed value of the device's Android ID. This value is encoded using a SHA-256 one way hash and therefore
     * cannot be used to determine what device this data came from.
     *
     * @param context The context used to access the settings resolver
     * @return An 1-way hashed version of the {@link android.provider.Settings.Secure#ANDROID_ID}. May return null if an Android
     *         ID or the hashing algorithm is not available.
     */
    public static String getAndroidIdHashOrNull(final Context context)
    {
        // Make sure a legacy version of the SDK didn't leave behind a device ID.
        // If it did, this ID must be used to keep user counts accurate
        final File fp = new File(context.getFilesDir() + LEGACY_DEVICE_ID_FILE);
        if (fp.exists() && fp.length() > 0)
        {
            try
            {
                BufferedReader reader = null;
                try
                {
                    final char[] buf = new char[100];
                    int numRead;
                    reader = new BufferedReader(new FileReader(fp), 128);
                    numRead = reader.read(buf);
                    final String deviceId = String.copyValueOf(buf, 0, numRead);
                    reader.close();
                    return deviceId;
                }
                catch (final FileNotFoundException e)
                {
                    if (Constants.IS_LOGGABLE)
                    {
                        Log.w(Constants.LOG_TAG, "Caught exception", e); //$NON-NLS-1$
                    }
                }
                finally
                {
                    if (null != reader)
                    {
                        reader.close();
                    }
                }
            }
            catch (final IOException e)
            {
                if (Constants.IS_LOGGABLE)
                {
                    Log.w(Constants.LOG_TAG, "Caught exception", e); //$NON-NLS-1$
                }
            }
        }

        final String androidId = android.provider.Settings.Secure.getString(context.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);
        if (androidId == null || androidId.toLowerCase().equals(INVALID_ANDROID_ID))
        {
            return null;
        }

        return getSha256(androidId);
    }

    /**
     * Gets a 1-way hashed value of the device's unique serial number. This value is encoded using a SHA-256 one way hash and
     * therefore cannot be used to determine what device this data came from.
     * <p>
     * Note: {@link android.os.Build#SERIAL} was introduced in SDK 9. For older SDKs, this method will return null.
     *
     * @return An 1-way hashed version of the {@link android.os.Build#SERIAL}. May return null if a serial or the hashing
     *         algorithm is not available.
     */
    /*
     * Suppress JavaDoc warnings because the {@link android.os.Build#SERIAL} fails when built with SDK 4.
     */
    public static String getSerialNumberHashOrNull()
    {
        /*
         * Obtain the device serial number using reflection, since serial number was added in SDK 9
         */
        String serialNumber = null;
        if (Constants.CURRENT_API_LEVEL >= 9)
        {
            try
            {
                serialNumber = (String) Build.class.getField("SERIAL").get(null); //$NON-NLS-1$
            }
            catch (final Exception e)
            {
                /*
                 * This should never happen, as SERIAL is a public field added in SDK 9.
                 */
                throw new RuntimeException(e);
            }
        }

        if (serialNumber == null)
        {
            return null;
        }

        return getSha256(serialNumber);
    }

    /**
     * Gets the device's telephony ID (e.g. IMEI/MEID).
     * <p>
     * Note: this method will return null if {@link permission#READ_PHONE_STATE} is not available. This method will also return
     * null on devices that do not have telephony.
     *
     * @param context The context used to access the phone state.
     * @return An the {@link TelephonyManager#getDeviceId()}. Null if an ID is not available, or if
     *         {@link permission#READ_PHONE_STATE} is not available.
     */
    public static String getTelephonyDeviceIdOrNull(final Context context)
    {
        if (Constants.CURRENT_API_LEVEL >= 8)
        {
            final Boolean hasTelephony = ReflectionUtils.tryInvokeInstance(context.getPackageManager(), "hasSystemFeature", new Class<?>[] { String.class }, new Object[] { "android.hardware.telephony" }); //$NON-NLS-1$//$NON-NLS-2$

            if (!hasTelephony.booleanValue())
            {
                if (Constants.IS_LOGGABLE)
                {
                    Log.i(Constants.LOG_TAG, "Device does not have telephony; cannot read telephony id"); //$NON-NLS-1$
                }

                return null;
            }
        }

        /*
         * Note: Sometimes Android will deny a package READ_PHONE_STATE permissions, even if the package has the permission. It
         * appears to be a race condition that occurs during installation.
         */
        String id = null;
        if (context.getPackageManager().checkPermission(permission.READ_PHONE_STATE, context.getPackageName()) == PackageManager.PERMISSION_GRANTED)
        {
            final TelephonyManager manager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
            id = manager.getDeviceId();
        }
        else
        {
            if (Constants.IS_LOGGABLE)
            {
                Log.w(Constants.LOG_TAG, "Application does not have permission READ_PHONE_STATE; determining device id is not possible.  Please consider requesting READ_PHONE_STATE in the AndroidManifest"); //$NON-NLS-1$
            }
        }

        return id;
    }

    /**
     * Gets a 1-way hashed value of the device's IMEI/MEID ID. This value is encoded using a SHA-256 one way hash and cannot be
     * used to determine what device this data came from.
     * <p>
     * Note: this method will return null if this is a non-telephony device.
     * <p>
     * Note: this method will return null if {@link permission#READ_PHONE_STATE} is not available.
     *
     * @param context The context used to access the phone state.
     * @return An 1-way hashed version of the {@link TelephonyManager#getDeviceId()}. Null if an ID or the hashing algorithm is
     *         not available, or if {@link permission#READ_PHONE_STATE} is not available.
     */
    public static String getTelephonyDeviceIdHashOrNull(final Context context)
    {
        final String id = getTelephonyDeviceIdOrNull(context);

        if (null == id)
        {
            return null;
        }

        return getSha256(id);
    }

    /**
     * Determines the type of network this device is connected to.
     *
     * @param context the context used to access the device's WIFI
     * @param telephonyManager The manager used to access telephony info
     * @return The type of network, or unknown if the information is unavailable
     */
    public static String getNetworkType(final Context context, final TelephonyManager telephonyManager)
    {
        if (context.getPackageManager().checkPermission(permission.ACCESS_WIFI_STATE, context.getPackageName()) == PackageManager.PERMISSION_GRANTED)
        {
            final NetworkInfo wifiInfo = ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)).getNetworkInfo(ConnectivityManager.TYPE_WIFI);
            if (wifiInfo != null && wifiInfo.isConnectedOrConnecting())
            {
                return "wifi"; //$NON-NLS-1$
            }
        }
        else
        {
            if (Constants.IS_LOGGABLE)
            {
                Log.w(Constants.LOG_TAG, "Application does not have permission ACCESS_WIFI_STATE; determining Wi-Fi connectivity is unavailable"); //$NON-NLS-1$
            }
        }

        return "android_network_type_" + telephonyManager.getNetworkType(); //$NON-NLS-1$
    }

    /**
     * Gets the device manufacturer's name. This is only available on SDK 4 or greater, so on SDK 3 this method returns the
     * constant string "unknown".
     *
     * @return A string naming the manufacturer
     */
    public static String getManufacturer()
    {
        String mfg = "unknown"; //$NON-NLS-1$
        if (Constants.CURRENT_API_LEVEL > 3)
        {
            try
            {
                final Class<?> buildClass = Build.class;
                mfg = (String) buildClass.getField("MANUFACTURER").get(null); //$NON-NLS-1$
            }
            catch (final Exception ignore)
            {
                if (Constants.IS_LOGGABLE)
                {
                    Log.w(Constants.LOG_TAG, "Caught exception", ignore); //$NON-NLS-1$
                }
            }
        }
        return mfg;
    }

    /**
     * Gets the versionName of the application.
     *
     * @param context {@link Context}. Cannot be null.
     * @return The application's version
     */
    public static String getAppVersion(final Context context)
    {
        final PackageManager pm = context.getPackageManager();

        try
        {
            final String versionName = pm.getPackageInfo(context.getPackageName(), 0).versionName;

            /*
             * If there is no versionName in the Android Manifest, the versionName will be null.
             */
            if (versionName == null)
            {
                if (Constants.IS_LOGGABLE)
                {
                    Log.w(Constants.LOG_TAG, "versionName was null--is a versionName attribute set in the Android Manifest?"); //$NON-NLS-1$
                }

                return "unknown"; //$NON-NLS-1$
            }

            return versionName;
        }
        catch (final PackageManager.NameNotFoundException e)
        {
            /*
             * This should never occur--our own package must exist for this code to be running
             */
            throw new RuntimeException(e);
        }
    }

    /**
     * Helper method to generate a SHA-256 hash of a given String
     *
     * @param string String to hash. Cannot be null.
     * @return hashed version of the string using SHA-256.
     */
    /* package */static String getSha256(final String string)
    {
        if (Constants.ENABLE_PARAMETER_CHECKING)
        {
            if (null == string)
            {
                throw new IllegalArgumentException("string cannot be null"); //$NON-NLS-1$
            }
        }

        try
        {
            final MessageDigest md = MessageDigest.getInstance("SHA-256"); //$NON-NLS-1$
            final byte[] digest = md.digest(string.getBytes("UTF-8")); //$NON-NLS-1$
            final BigInteger hashedNumber = new BigInteger(1, digest);
            return hashedNumber.toString(16);
        }
        catch (final NoSuchAlgorithmException e)
        {
            throw new RuntimeException(e);
        }
        catch (final UnsupportedEncodingException e)
        {
            throw new RuntimeException(e);
        }
    }
}
