package com.paypal.android.sdk.onetouch.core.config;

import com.paypal.android.sdk.onetouch.core.enums.RequestTarget;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * The configuration loaded from default or stored in preferences.
 */
public class OtcConfiguration {

    private final ArrayList<OAuth2Recipe> mOauth2RecipesInDecreasingPriorityOrder =
            new ArrayList<>();
    private final ArrayList<CheckoutRecipe> mCheckoutRecipesInDecreasingPriorityOrder =
            new ArrayList<>();
    private final ArrayList<BillingAgreementRecipe>
            mBillingAgreementRecipesInDecreasingPriorityOrder = new ArrayList<>();
    private String mFileTimestamp;

    public OtcConfiguration withOauth2Recipe(OAuth2Recipe recipe) {
        mOauth2RecipesInDecreasingPriorityOrder.add(recipe);
        return this;
    }

    public OtcConfiguration fileTimestamp(String fileTimestamp) {
        mFileTimestamp = fileTimestamp;
        return this;
    }

    public String getFileTimestamp() {
        return mFileTimestamp;
    }

    /**
     * Returns the browser recipe that can handle checkout, or null if there is none.
     *
     * @return
     */
    public CheckoutRecipe getBrowserCheckoutConfig() {
        for (CheckoutRecipe recipe : mCheckoutRecipesInDecreasingPriorityOrder) {
            if (recipe.getTarget() == RequestTarget.browser) {
                return recipe;
            }
        }

        return null;
    }

    /**
     * Returns the browser recipe that can handle billing agreement, or null if there is none.
     *
     * @return
     */
    public BillingAgreementRecipe getBrowserBillingAgreementConfig() {
        for (BillingAgreementRecipe recipe : mBillingAgreementRecipesInDecreasingPriorityOrder) {
            if (recipe.getTarget() == RequestTarget.browser) {
                return recipe;
            }
        }

        return null;
    }

    public List<OAuth2Recipe> getOauth2Recipes() {
        return new ArrayList<>(mOauth2RecipesInDecreasingPriorityOrder);
    }

    public void withCheckoutRecipe(CheckoutRecipe recipe) {
        mCheckoutRecipesInDecreasingPriorityOrder.add(recipe);
    }

    public List<CheckoutRecipe> getCheckoutRecipes() {
        return new ArrayList<>(mCheckoutRecipesInDecreasingPriorityOrder);
    }

    public void withBillingAgreementRecipe(BillingAgreementRecipe recipe) {
        mBillingAgreementRecipesInDecreasingPriorityOrder.add(recipe);
    }

    public List<BillingAgreementRecipe> getBillingAgreementRecipes() {
        return new ArrayList<>(mBillingAgreementRecipesInDecreasingPriorityOrder);
    }
}


package com.braintreepayments.api.models;

import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;

import com.braintreepayments.api.Json;

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

import static com.braintreepayments.api.models.BinData.BIN_DATA_KEY;

/**
 * {@link PaymentMethodNonce} representing a credit or debit card.
 */
public class CardNonce extends PaymentMethodNonce implements Parcelable {

    protected static final String TYPE = "CreditCard";
    protected static final String API_RESOURCE_KEY = "creditCards";

    private static final String GRAPHQL_TOKENIZE_CREDIT_CARD_KEY = "tokenizeCreditCard";
    private static final String GRAPHQL_CREDIT_CARD_KEY = "creditCard";
    private static final String GRAPHQL_BRAND_KEY = "brand";
    private static final String GRAPHQL_LAST_FOUR_KEY = "last4";
    private static final String THREE_D_SECURE_INFO_KEY = "threeDSecureInfo";
    private static final String CARD_DETAILS_KEY = "details";
    private static final String CARD_TYPE_KEY = "cardType";
    private static final String LAST_TWO_KEY = "lastTwo";
    private static final String LAST_FOUR_KEY = "lastFour";

    private String mCardType;
    private String mLastTwo;
    private String mLastFour;
    private ThreeDSecureInfo mThreeDSecureInfo;
    private BinData mBinData;

    /**
     * Convert an API response to a {@link CardNonce}.
     *
     * @param json Raw JSON response from Braintree of a {@link CardNonce}.
     * @return {@link CardNonce}.
     * @throws JSONException when parsing the response fails.
     */
    public static CardNonce fromJson(String json) throws JSONException {
        CardNonce cardNonce = new CardNonce();
        JSONObject jsonObject = new JSONObject(json);

        if (jsonObject.has(DATA_KEY)) {
            cardNonce.fromGraphQLJson(jsonObject);
        } else {
            cardNonce.fromJson(CardNonce.getJsonObjectForType(API_RESOURCE_KEY, jsonObject));
        }

        return cardNonce;
    }

    /**
     * Populate properties with values from a {@link JSONObject} .
     *
     * @param json {@link JSONObject}
     * @throws JSONException when parsing fails.
     */
    protected void fromJson(JSONObject json) throws JSONException {
        super.fromJson(json);

        JSONObject details = json.getJSONObject(CARD_DETAILS_KEY);
        mLastTwo = details.getString(LAST_TWO_KEY);
        mLastFour = details.getString(LAST_FOUR_KEY);
        mCardType = details.getString(CARD_TYPE_KEY);
        mThreeDSecureInfo = ThreeDSecureInfo.fromJson(json.optJSONObject(THREE_D_SECURE_INFO_KEY));
        mBinData = BinData.fromJson(json.optJSONObject(BIN_DATA_KEY));
    }

    private void fromGraphQLJson(JSONObject json) throws JSONException {
        JSONObject data = json.getJSONObject(DATA_KEY);

        if (data.has(GRAPHQL_TOKENIZE_CREDIT_CARD_KEY)) {
            JSONObject payload = data.getJSONObject(GRAPHQL_TOKENIZE_CREDIT_CARD_KEY);

            JSONObject creditCard = payload.getJSONObject(GRAPHQL_CREDIT_CARD_KEY);
            mLastFour = Json.optString(creditCard, GRAPHQL_LAST_FOUR_KEY, "");
            mLastTwo = mLastFour.length() < 4 ? "" : mLastFour.substring(2);
            mCardType = Json.optString(creditCard, GRAPHQL_BRAND_KEY, "Unknown");
            mThreeDSecureInfo = ThreeDSecureInfo.fromJson(null);
            mBinData = BinData.fromJson(creditCard.optJSONObject(BIN_DATA_KEY));

            mNonce = payload.getString(TOKEN_KEY);
            mDescription = TextUtils.isEmpty(mLastTwo) ? "" : "ending in ••" + mLastTwo;
            mDefault = false;
        } else {
            throw new JSONException("Failed to parse GraphQL response JSON");
        }
    }

    /**
     * @return Type of this card (e.g. MasterCard, American Express)
     */
    @Override
    public String getTypeLabel() {
        return mCardType;
    }

    /**
     * @return Type of this card (e.g. Visa, MasterCard, American Express)
     */
    public String getCardType() {
        return mCardType;
    }

    /**
     * @return Last two digits of the card, intended for display purposes.
     */
    public String getLastTwo() {
        return mLastTwo;
    }

    /**
     * @return Last four digits of the card.
     */
    public String getLastFour() {
        return mLastFour;
    }

    /**
     * @return The 3D Secure info for the current {@link CardNonce} or
     * {@code null}
     */
    public ThreeDSecureInfo getThreeDSecureInfo() {
        return mThreeDSecureInfo;
    }

    /**
     * @return The BIN data for the card number associated with {@link CardNonce}
     */
    public BinData getBinData() {
        return mBinData;
    }

    public CardNonce() {}

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        super.writeToParcel(dest, flags);
        dest.writeString(mCardType);
        dest.writeString(mLastTwo);
        dest.writeString(mLastFour);
        dest.writeParcelable(mBinData, flags);
        dest.writeParcelable(mThreeDSecureInfo, flags);
    }

    protected CardNonce(Parcel in) {
        super(in);
        mCardType = in.readString();
        mLastTwo = in.readString();
        mLastFour = in.readString();
        mBinData = in.readParcelable(BinData.class.getClassLoader());
        mThreeDSecureInfo = in.readParcelable(ThreeDSecureInfo.class.getClassLoader());
    }

    public static final Creator<CardNonce> CREATOR = new Creator<CardNonce>() {
        public CardNonce createFromParcel(Parcel source) {
            return new CardNonce(source);
        }

        public CardNonce[] newArray(int size) {
            return new CardNonce[size];
        }
    };
}


package com.braintreepayments.api.models;

import android.text.TextUtils;

import com.braintreepayments.api.Json;

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

/**
 * Contains configuration for Braintree analytics calls
 */
public class AnalyticsConfiguration {

    private static final String URL_KEY = "url";

    private String mUrl;

    /**
     * Parse an {@link AnalyticsConfiguration} from json.
     *
     * @param json The {@link JSONObject} to parse.
     * @return An {@link AnalyticsConfiguration} instance with data that was able to be parsed from
     *         the {@link JSONObject}.
     */
    public static AnalyticsConfiguration fromJson(JSONObject json) {
        if (json == null) {
            json = new JSONObject();
        }

        AnalyticsConfiguration analyticsConfiguration = new AnalyticsConfiguration();
        analyticsConfiguration.mUrl = Json.optString(json, URL_KEY, null);

        return analyticsConfiguration;
    }

    /**
     * Serialize the {@link AnalyticsConfiguration} to json.
     *
     * @return {@link JSONObject} containing the {@link AnalyticsConfiguration}.
     */
    public JSONObject toJson() {
        try {
            return new JSONObject().put(URL_KEY, mUrl);
        } catch (JSONException e) {
            return new JSONObject();
        }
    }

    /**
     * @return {@link String} url of the Braintree analytics service.
     */
    public String getUrl() {
        return mUrl;
    }

    /**
     * @return {@code true} if analytics are enabled, {@code false} otherwise.
     */
    public boolean isEnabled() {
        return !TextUtils.isEmpty(mUrl);
    }
}


package com.braintreepayments.api;

import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;

import com.braintreepayments.api.exceptions.BraintreeException;
import com.braintreepayments.api.exceptions.ErrorWithResponse;
import com.braintreepayments.api.interfaces.ConfigurationListener;
import com.braintreepayments.api.interfaces.HttpResponseCallback;
import com.braintreepayments.api.interfaces.PaymentMethodNonceCallback;
import com.braintreepayments.api.internal.GraphQLConstants.Features;
import com.braintreepayments.api.models.CardBuilder;
import com.braintreepayments.api.models.Configuration;
import com.braintreepayments.api.models.PaymentMethodBuilder;
import com.braintreepayments.api.models.PaymentMethodNonce;

import org.json.JSONException;

import static com.braintreepayments.api.models.PaymentMethodNonce.parsePaymentMethodNonces;

class TokenizationClient {

    static final String PAYMENT_METHOD_ENDPOINT = "payment_methods";

    /**
     * Create a {@link PaymentMethodNonce} in the Braintree Gateway.
     *

     * On completion, returns the {@link PaymentMethodNonce} to {@link PaymentMethodNonceCallback}.
     *

     * If creation fails validation, {@link com.braintreepayments.api.interfaces.BraintreeErrorListener#onError(Exception)}
     * will be called with the resulting {@link ErrorWithResponse}.
     *

     * If an error not due to validation (server error, network issue, etc.) occurs, {@link
     * com.braintreepayments.api.interfaces.BraintreeErrorListener#onError(Exception)} (Throwable)}
     * will be called with the {@link Exception} that occurred.
     *
     * @param paymentMethodBuilder {@link PaymentMethodBuilder} for the {@link PaymentMethodNonce}
     *        to be created.
     */
    static void tokenize(final BraintreeFragment fragment, final PaymentMethodBuilder paymentMethodBuilder,
            final PaymentMethodNonceCallback callback) {
        paymentMethodBuilder.setSessionId(fragment.getSessionId());

        fragment.waitForConfiguration(new ConfigurationListener() {
            @Override
            public void onConfigurationFetched(Configuration configuration) {
                if (paymentMethodBuilder instanceof CardBuilder &&
                        configuration.getGraphQL().isFeatureEnabled(Features.TOKENIZE_CREDIT_CARDS)) {
                    tokenizeGraphQL(fragment, (CardBuilder) paymentMethodBuilder, callback);
                } else {
                    tokenizeRest(fragment, paymentMethodBuilder, callback);
                }
            }
        });
    }

    static String versionedPath(String path) {
        return "/v1/" + path;
    }

    private static void tokenizeGraphQL(final BraintreeFragment fragment, final CardBuilder cardBuilder,
            final PaymentMethodNonceCallback callback) {
        fragment.sendAnalyticsEvent("card.graphql.tokenization.started");
        String payload;
        try {
            payload = cardBuilder.buildGraphQL(fragment.getApplicationContext(), fragment.getAuthorization());
        } catch (BraintreeException e) {
            callback.failure(e);
            return;
        }

        fragment.getGraphQLHttpClient().post(payload, new HttpResponseCallback() {
            @Override
            public void success(String responseBody) {
                try {
                    callback.success(parsePaymentMethodNonces(responseBody, cardBuilder.getResponsePaymentMethodType()));
                    fragment.sendAnalyticsEvent("card.graphql.tokenization.success");
                } catch (JSONException e) {
                    callback.failure(e);
                }
            }

            @Override
            public void failure(Exception exception) {
                fragment.sendAnalyticsEvent("card.graphql.tokenization.failure");
                callback.failure(exception);
            }
        });
    }

    private static void tokenizeRest(final BraintreeFragment fragment, final PaymentMethodBuilder paymentMethodBuilder,
            final PaymentMethodNonceCallback callback) {
        fragment.getHttpClient().post(TokenizationClient.versionedPath(
                TokenizationClient.PAYMENT_METHOD_ENDPOINT + "/" + paymentMethodBuilder.getApiPath()),
                paymentMethodBuilder.build(), new HttpResponseCallback() {
                    @Override
                    public void success(String responseBody) {
                        try {
                            callback.success(parsePaymentMethodNonces(responseBody,
                                    paymentMethodBuilder.getResponsePaymentMethodType()));
                        } catch (JSONException e) {
                            callback.failure(e);
                        }
                    }

                    @Override
                    public void failure(Exception exception) {
                        callback.failure(exception);
                    }
                });
    }
}


package com.braintreepayments.api.internal;

import android.os.Handler;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.braintreepayments.api.core.BuildConfig;
import com.braintreepayments.api.exceptions.AuthenticationException;
import com.braintreepayments.api.exceptions.AuthorizationException;
import com.braintreepayments.api.exceptions.DownForMaintenanceException;
import com.braintreepayments.api.exceptions.RateLimitException;
import com.braintreepayments.api.exceptions.ServerException;
import com.braintreepayments.api.exceptions.UnexpectedException;
import com.braintreepayments.api.exceptions.UnprocessableEntityException;
import com.braintreepayments.api.exceptions.UpgradeRequiredException;
import com.braintreepayments.api.interfaces.HttpResponseCallback;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPInputStream;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocketFactory;

import static java.net.HttpURLConnection.HTTP_ACCEPTED;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import static java.net.HttpURLConnection.HTTP_CREATED;
import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;

public class HttpClient<T extends HttpClient> {

    private static final String METHOD_GET = "GET";
    private static final String METHOD_POST = "POST";
    private static final String UTF_8 = "UTF-8";

    private final Handler mMainThreadHandler;

    @VisibleForTesting
    protected final ExecutorService mThreadPool;

    private String mUserAgent;
    private SSLSocketFactory mSSLSocketFactory;
    private int mConnectTimeout;
    private int mReadTimeout;

    protected String mBaseUrl;

    public HttpClient() {
        mThreadPool =  Executors.newCachedThreadPool();
        mMainThreadHandler = new Handler(Looper.getMainLooper());
        mUserAgent = "braintree/core/" + BuildConfig.VERSION_NAME;
        mConnectTimeout = (int) TimeUnit.SECONDS.toMillis(30);
        mReadTimeout = (int) TimeUnit.SECONDS.toMillis(30);

        try {
            mSSLSocketFactory = new TLSSocketFactory();
        } catch (SSLException e) {
            mSSLSocketFactory = null;
        }
    }

    /**
     * @param userAgent the user agent to be sent with all http requests.
     * @return {@link HttpClient} for method chaining.
     */
    @SuppressWarnings("unchecked")
    public T setUserAgent(String userAgent) {
        mUserAgent = userAgent;
        return (T) this;
    }

    /**
     * @param sslSocketFactory the {@link SSLSocketFactory} to use for all https requests.
     * @return {@link HttpClient} for method chaining.
     */
    @SuppressWarnings("unchecked")
    public T setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
        mSSLSocketFactory = sslSocketFactory;
        return (T) this;
    }

    /**
     * @param baseUrl the base url to use when only a path is supplied to
     * {@link #get(String, HttpResponseCallback)} or {@link #post(String, String, HttpResponseCallback)}
     * @return {@link HttpClient} for method chaining.
     */
    @SuppressWarnings("unchecked")
    public T setBaseUrl(String baseUrl) {
        mBaseUrl = (baseUrl == null) ? "" : baseUrl;
        return (T) this;
    }

    /**
     * @param timeout the time in milliseconds to wait for a connection before timing out.
     * @return {@link HttpClient} for method chaining.
     */
    @SuppressWarnings("unchecked")
    public T setConnectTimeout(int timeout) {
        mConnectTimeout = timeout;
        return (T) this;
    }

    /**
     * @param timeout the time in milliseconds to read a response from the server before timing out.
     * @return {@link HttpClient} for method chaining.
     */
    @SuppressWarnings("unchecked")
    public T setReadTimeout(int timeout) {
        mReadTimeout = timeout;
        return (T) this;
    }

    /**
     * Make a HTTP GET request to using the base url and path provided. If the path is a full url,
     * it will be used instead of the previously provided base url.
     *
     * @param path The path or url to request from the server via GET
     * @param callback The {@link HttpResponseCallback} to receive the response or error.
     */
    public void get(final String path, final HttpResponseCallback callback) {
        if (path == null) {
            postCallbackOnMainThread(callback, new IllegalArgumentException("Path cannot be null"));
            return;
        }

        final String url;
        if (path.startsWith("http")) {
            url = path;
        } else {
            url = mBaseUrl + path;
        }

        mThreadPool.submit(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                try {
                    connection = init(url);
                    connection.setRequestMethod(METHOD_GET);
                    postCallbackOnMainThread(callback, parseResponse(connection));
                } catch (Exception e) {
                    postCallbackOnMainThread(callback, e);
                } finally {
                    if (connection != null) {
                        connection.disconnect();
                    }
                }
            }
        });
    }

    /**
     * Make a HTTP POST request using the base url and path provided. If the path is a full url,
     * it will be used instead of the previously provided url.
     *
     * @param path The path or url to request from the server via HTTP POST
     * @param data The body of the POST request
     * @param callback The {@link HttpResponseCallback} to receive the response or error.
     */
    public void post(final String path, final String data, final HttpResponseCallback callback) {
        if (path == null) {
            postCallbackOnMainThread(callback, new IllegalArgumentException("Path cannot be null"));
            return;
        }

        mThreadPool.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    postCallbackOnMainThread(callback, post(path, data));
                } catch (Exception e) {
                    postCallbackOnMainThread(callback, e);
                }
            }
        });
    }

    /**
     * Performs a synchronous post request.
     *
     * @param path the path or url to request from the server via HTTP POST
     * @param data the body of the post request
     * @return The HTTP body the of the response
     *
     * @see HttpClient#post(String, String, HttpResponseCallback)
     * @throws Exception
     */
    public String post(String path, String data) throws Exception {
        HttpURLConnection connection = null;
        try {
            if (path.startsWith("http")) {
                connection = init(path);
            } else {
                connection = init(mBaseUrl + path);
            }

            connection.setRequestProperty("Content-Type", "application/json");
            connection.setRequestMethod(METHOD_POST);
            connection.setDoOutput(true);

            writeOutputStream(connection.getOutputStream(), data);

            return parseResponse(connection);
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    }

    protected HttpURLConnection init(String url) throws IOException {
        HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();

        if (connection instanceof HttpsURLConnection) {
            if (mSSLSocketFactory == null) {
                throw new SSLException("SSLSocketFactory was not set or failed to initialize");
            }

            ((HttpsURLConnection) connection).setSSLSocketFactory(mSSLSocketFactory);
        }

        connection.setRequestProperty("User-Agent", mUserAgent);
        connection.setRequestProperty("Accept-Language", Locale.getDefault().getLanguage());
        connection.setRequestProperty("Accept-Encoding", "gzip");
        connection.setConnectTimeout(mConnectTimeout);
        connection.setReadTimeout(mReadTimeout);

        return connection;
    }

    protected void writeOutputStream(OutputStream outputStream, String data) throws IOException {
        Writer out = new OutputStreamWriter(outputStream, UTF_8);
        out.write(data, 0, data.length());
        out.flush();
        out.close();
    }

    protected String parseResponse(HttpURLConnection connection) throws Exception {
        int responseCode = connection.getResponseCode();
        boolean gzip = "gzip".equals(connection.getContentEncoding());
        switch(responseCode) {
            case HTTP_OK: case HTTP_CREATED: case HTTP_ACCEPTED:
                return readStream(connection.getInputStream(), gzip);
            case HTTP_UNAUTHORIZED:
                throw new AuthenticationException(readStream(connection.getErrorStream(), gzip));
            case HTTP_FORBIDDEN:
                throw new AuthorizationException(readStream(connection.getErrorStream(), gzip));
            case HTTP_BAD_REQUEST:
            case 422: // HTTP_UNPROCESSABLE_ENTITY
                throw new UnprocessableEntityException(readStream(connection.getErrorStream(), gzip));
            case 426: // HTTP_UPGRADE_REQUIRED
                throw new UpgradeRequiredException(readStream(connection.getErrorStream(), gzip));
            case 429: // HTTP_TOO_MANY_REQUESTS
                throw new RateLimitException("You are being rate-limited. Please try again in a few minutes.");
            case HTTP_INTERNAL_ERROR:
                throw new ServerException(readStream(connection.getErrorStream(), gzip));
            case HTTP_UNAVAILABLE:
                throw new DownForMaintenanceException(readStream(connection.getErrorStream(), gzip));
            default:
                throw new UnexpectedException(readStream(connection.getErrorStream(), gzip));
        }
    }

    void postCallbackOnMainThread(final HttpResponseCallback callback, final String response) {
        if (callback == null) {
            return;
        }

        mMainThreadHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.success(response);
            }
        });
    }

    void postCallbackOnMainThread(final HttpResponseCallback callback, final Exception exception) {
        if (callback == null) {
            return;
        }

        mMainThreadHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.failure(exception);
            }
        });
    }

    @Nullable
    private String readStream(InputStream in, boolean gzip) throws IOException {
        if (in == null) {
            return null;
        }

        try {
            if (gzip) {
                in = new GZIPInputStream(in);
            }

            ByteArrayOutputStream out = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            for (int count; (count = in.read(buffer)) != -1; ) {
                out.write(buffer, 0, count);
            }

            return new String(out.toByteArray(), UTF_8);
        } finally {
            try {
                in.close();
            } catch (IOException ignored) {}
        }
    }
}


package com.braintreepayments.api.models;

import android.os.Parcel;
import android.os.Parcelable;

import com.braintreepayments.api.Json;

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

import static com.braintreepayments.api.models.BinData.BIN_DATA_KEY;

/**
 * {@link PaymentMethodNonce} representing a Visa Checkout card.
 * @see PaymentMethodNonce
 */
public class VisaCheckoutNonce extends PaymentMethodNonce implements Parcelable {

    protected static final String TYPE = "VisaCheckoutCard";
    protected static final String API_RESOURCE_KEY = "visaCheckoutCards";

    private static final String CARD_DETAILS_KEY = "details";
    private static final String CARD_TYPE_KEY = "cardType";
    private static final String LAST_TWO_KEY = "lastTwo";
    private static final String BILLING_ADDRESS_KEY = "billingAddress";
    private static final String SHIPPING_ADDRESS_KEY = "shippingAddress";
    private static final String USER_DATA_KEY = "userData";
    private static final String CALL_ID_KEY = "callId";

    private String mLastTwo;
    private String mCardType;
    private VisaCheckoutAddress mBillingAddress;
    private VisaCheckoutAddress mShippingAddress;
    private VisaCheckoutUserData mUserData;
    private String mCallId;
    private BinData mBinData;

    /**
     * Convert an API response to a {@link VisaCheckoutNonce}.
     *
     * @param json Raw JSON response from Braintree of a {@link VisaCheckoutNonce}.
     * @return {@link VisaCheckoutNonce}.
     * @throws JSONException when parsing the response fails.
     */
    public static VisaCheckoutNonce fromJson(String json) throws JSONException {
        VisaCheckoutNonce visaCheckoutNonce = new VisaCheckoutNonce();
        visaCheckoutNonce.fromJson(PaymentMethodNonce.getJsonObjectForType(API_RESOURCE_KEY, new JSONObject(json)));

        return visaCheckoutNonce;
    }

    @Override
    protected void fromJson(JSONObject json) throws JSONException {
        super.fromJson(json);

        JSONObject details = json.getJSONObject(CARD_DETAILS_KEY);
        mLastTwo = details.getString(LAST_TWO_KEY);
        mCardType = details.getString(CARD_TYPE_KEY);
        mBillingAddress = VisaCheckoutAddress.fromJson(json.optJSONObject(BILLING_ADDRESS_KEY));
        mShippingAddress = VisaCheckoutAddress.fromJson(json.optJSONObject(SHIPPING_ADDRESS_KEY));
        mUserData = VisaCheckoutUserData.fromJson(json.optJSONObject(USER_DATA_KEY));
        mCallId = Json.optString(json, CALL_ID_KEY, "");
        mBinData = BinData.fromJson(json.optJSONObject(BIN_DATA_KEY));
    }

    /**
     * @return Last two digits of the user's underlying card, intended for display purposes.
     */
    public String getLastTwo() {
        return mLastTwo;
    }

    /**
     * @return Type of this card (e.g. Visa, MasterCard, American Express)
     */
    public String getCardType() {
        return mCardType;
    }

    /**
     * @return The user's billing address.
     */
    public VisaCheckoutAddress getBillingAddress() {
        return mBillingAddress;
    }

    /**
     * @return The user's shipping address.
     */
    public VisaCheckoutAddress getShippingAddress() {
        return mShippingAddress;
    }

    /**
     * @return The user's data.
     */
    public VisaCheckoutUserData getUserData() {
        return mUserData;
    }

    /**
     * @return The Call ID from the {@link com.visa.checkout.VisaPaymentSummary}.
     */
    public String getCallId() {
        return mCallId;
    }

    @Override
    public String getTypeLabel() {
        return "Visa Checkout";
    }

    /**
     * @return The BIN data for the card number associated with {@link VisaCheckoutNonce}
     */
    public BinData getBinData() {
        return mBinData;
    }

    public VisaCheckoutNonce() {}

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        super.writeToParcel(dest, flags);
        dest.writeString(mLastTwo);
        dest.writeString(mCardType);
        dest.writeParcelable(mBillingAddress, flags);
        dest.writeParcelable(mShippingAddress, flags);
        dest.writeParcelable(mUserData, flags);
        dest.writeString(mCallId);
        dest.writeParcelable(mBinData, flags);
    }

    protected VisaCheckoutNonce(Parcel in) {
        super(in);
        mLastTwo = in.readString();
        mCardType = in.readString();
        mBillingAddress = in.readParcelable(VisaCheckoutAddress.class.getClassLoader());
        mShippingAddress = in.readParcelable(VisaCheckoutAddress.class.getClassLoader());
        mUserData = in.readParcelable(VisaCheckoutUserData.class.getClassLoader());
        mCallId = in.readString();
        mBinData = in.readParcelable(BinData.class.getClassLoader());
    }

    public static final Creator<VisaCheckoutNonce> CREATOR =
            new Creator<VisaCheckoutNonce>() {
                @Override
                public VisaCheckoutNonce createFromParcel(Parcel in) {
                    return new VisaCheckoutNonce(in);
                }

                @Override
                public VisaCheckoutNonce[] newArray(int size) {
                    return new VisaCheckoutNonce[size];
                }
            };
}


package com.braintreepayments.api;

import android.content.Intent;

import com.braintreepayments.api.exceptions.BraintreeException;
import com.braintreepayments.api.exceptions.ConfigurationException;
import com.braintreepayments.api.interfaces.BraintreeResponseListener;
import com.braintreepayments.api.interfaces.ConfigurationListener;
import com.braintreepayments.api.interfaces.HttpResponseCallback;
import com.braintreepayments.api.models.BraintreeRequestCodes;
import com.braintreepayments.api.models.Configuration;
import com.braintreepayments.api.models.LocalPaymentRequest;
import com.braintreepayments.api.models.LocalPaymentResult;
import com.paypal.android.sdk.data.collector.PayPalDataCollector;

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

/**
 * Used to integrate with local payments.
 */
public class LocalPayment {

    private static final String LOCAL_PAYMENT_CANCEL = "local-payment-cancel";
    private static final String LOCAL_PAYMENT_SUCCESSS = "local-payment-success";

    private static String sMerchantAccountId;
    private static String sPaymentType;

    /**
     * Prepares the payment flow for a specific type of local payment.
     *
     * @param fragment {@link BraintreeFragment}
     * @param request {@link LocalPaymentRequest} with the payment details.
     * @param listener {@link BraintreeResponseListener} the callback to which the {@link LocalPaymentRequest} will be sent
     * with a payment ID for preprocessing and an approval URL. Once ready, use {@link #approvePayment(BraintreeFragment, LocalPaymentRequest)}
     * to initiate the browser switch.
     */
    public static void startPayment(final BraintreeFragment fragment, final LocalPaymentRequest request,
            final BraintreeResponseListener<LocalPaymentRequest> listener) {
        if (request == null) {
            fragment.postCallback(new BraintreeException("A LocalPaymentRequest is required."));
            return;
        } else if (request.getApprovalUrl() != null || request.getPaymentId() != null) {
            fragment.postCallback(new BraintreeException("LocalPaymentRequest is invalid, " +
                    "appovalUrl and paymentId should not be set."));
            return;
        } else if (request.getPaymentType() == null || request.getAmount() == null) {
            fragment.postCallback(new BraintreeException("LocalPaymentRequest is invalid, " +
                    "paymentType and amount are required."));
            return;
        } else if (listener == null) {
            fragment.postCallback(new BraintreeException("BraintreeResponseListener<LocalPaymentRequest> " +
                    "is required."));
            return;
        }

        fragment.waitForConfiguration(new ConfigurationListener() {
            @Override
            public void onConfigurationFetched(Configuration configuration) {
                if (!configuration.getPayPal().isEnabled()) {
                    fragment.postCallback(new ConfigurationException("Local payments are not enabled for this merchant."));
                    return;
                }

                sMerchantAccountId = request.getMerchantAccountId();
                sPaymentType = request.getPaymentType();
                String returnUrl =  fragment.getReturnUrlScheme() + "://" + LOCAL_PAYMENT_SUCCESSS;
                String cancel =  fragment.getReturnUrlScheme() + "://" + LOCAL_PAYMENT_CANCEL;

                fragment.sendAnalyticsEvent(paymentTypeForAnalytics() + ".local-payment.start-payment.selected");

                fragment.getHttpClient().post("/v1/paypal_hermes/create_payment_resource", request.build(returnUrl, cancel),
                        new HttpResponseCallback() {
                            @Override
                            public void success(String responseBody) {
                                try {
                                    JSONObject responseJson = new JSONObject(responseBody);
                                    request.approvalUrl(responseJson.getJSONObject("paymentResource").getString("redirectUrl"));
                                    request.paymentId(responseJson.getJSONObject("paymentResource").getString("paymentToken"));

                                    fragment.sendAnalyticsEvent(paymentTypeForAnalytics() + ".local-payment.create.succeeded");
                                    listener.onResponse(request);

                                } catch (JSONException jsonException) {
                                    failure(jsonException);
                                }
                            }

                            @Override
                            public void failure(Exception exception) {
                                fragment.sendAnalyticsEvent(paymentTypeForAnalytics() + ".local-payment.webswitch.initiate.failed");
                                fragment.postCallback(exception);
                            }
                        });
            }
        });
    }

    /**
     * Initiates the browser switch for a payment flow by opening a browser where the customer can authenticate with their bank.
     *
     * @param fragment {@link BraintreeFragment}
     * @param request {@link LocalPaymentRequest} which has already been sent to {@link #startPayment(BraintreeFragment, LocalPaymentRequest, BraintreeResponseListener)}
     * and now has an approvalUrl and paymentId.
     */
    public static void approvePayment(BraintreeFragment fragment, LocalPaymentRequest request) {
        fragment.browserSwitch(BraintreeRequestCodes.LOCAL_PAYMENT, request.getApprovalUrl());
        fragment.sendAnalyticsEvent(paymentTypeForAnalytics() + ".local-payment.webswitch.initiate.succeeded");
    }

    static void onActivityResult(final BraintreeFragment fragment, int resultCode, Intent data) {
        if (data != null && data.getData() != null) {
            String responseString = data.getData().toString();

            if (responseString.toLowerCase().contains(LOCAL_PAYMENT_CANCEL.toLowerCase())) {
                fragment.sendAnalyticsEvent(paymentTypeForAnalytics() + ".local-payment.webswitch.canceled");
                fragment.postCancelCallback(BraintreeRequestCodes.LOCAL_PAYMENT);
                return;
            }

            JSONObject payload = new JSONObject();

            try {
                payload.put("merchant_account_id", sMerchantAccountId);

                JSONObject paypalAccount = new JSONObject()
                        .put("intent", "sale")
                        .put("response", new JSONObject().put("webURL", responseString))
                        .put("options", new JSONObject().put("validate", false))
                        .put("response_type", "web")
                        .put("correlation_id", PayPalDataCollector.getClientMetadataId(fragment.getApplicationContext()));
                payload.put("paypal_account", paypalAccount);

                JSONObject metaData = new JSONObject()
                        .put("source", "client")
                        .put("integration", fragment.getIntegrationType())
                        .put("sessionId", fragment.getSessionId());
                payload.put("_meta", metaData);

                fragment.getHttpClient().post("/v1/payment_methods/paypal_accounts", payload.toString(),
                        new HttpResponseCallback() {
                            @Override
                            public void success(String responseBody) {
                                try {
                                    LocalPaymentResult result = LocalPaymentResult.fromJson(responseBody);
                                    fragment.sendAnalyticsEvent(paymentTypeForAnalytics() + ".local-payment.tokenize.succeeded");
                                    fragment.postCallback(result);
                                } catch (JSONException jsonException) {
                                    failure(jsonException);
                                }
                            }

                            @Override
                            public void failure(Exception exception) {
                                fragment.sendAnalyticsEvent(paymentTypeForAnalytics() + ".local-payment.tokenize.failed");
                                fragment.postCallback(exception);
                            }
                        });

            } catch (JSONException ignored) {}

        } else {
            fragment.sendAnalyticsEvent(paymentTypeForAnalytics() + ".local-payment.webswitch-response.invalid");
            fragment.postCallback(new BraintreeException("LocalPayment encountered an error, " +
                    "return URL is invalid."));
        }
    }

    private static String paymentTypeForAnalytics() {
        return sPaymentType != null ? sPaymentType : "unknown";
    }
}


package com.braintreepayments.api.models;

import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import android.text.TextUtils;

import com.braintreepayments.api.exceptions.InvalidArgumentException;

/**
 * Generic base class for Braintree authorization
 */
public abstract class Authorization implements Parcelable {

    private final String mRawValue;

    public Authorization(String rawValue) {
        mRawValue = rawValue;
    }

    /**
     * Returns an {@link Authorization} of the correct type for a given {@link String}.
     *
     * @param authorizationString Given string to transform into an {@link Authorization}.
     * @return {@link Authorization}
     * @throws InvalidArgumentException This method will throw this exception type if the string
     * passed does not meet any of the criteria supplied for {@link ClientToken} or {@link TokenizationKey}.
     */
    public static Authorization fromString(@Nullable String authorizationString) throws InvalidArgumentException {
        if (isTokenizationKey(authorizationString)) {
            return new TokenizationKey(authorizationString);
        } else {
            return new ClientToken(authorizationString);
        }
    }

    /**
     * @return The url to fetch configuration for the current Braintree environment.
     */
    public abstract String getConfigUrl();

    /**
     * @return The authorization bearer string for authorizing requests.
     */
    public abstract String getBearer();

    /**
     * @return The original Client token or Tokenization Key string, which can be used for serialization
     */
    @Override
    public String toString() {
        return mRawValue;
    }

    /**
     * @param tokenizationKey The {@link String} to check if it is a tokenization key
     * @return {@code true} if the {@link String} is a tokenization key, {@code false} otherwise.
     */
    public static boolean isTokenizationKey(String tokenizationKey) {
        return !TextUtils.isEmpty(tokenizationKey) && tokenizationKey.matches(TokenizationKey.MATCHER);
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeString(mRawValue);
    }

    public Authorization(Parcel in) {
        mRawValue = in.readString();
    }
}


package com.braintreepayments.api.exceptions;

import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;

import com.braintreepayments.api.Json;
import com.braintreepayments.api.internal.GraphQLConstants.ErrorTypes;
import com.braintreepayments.api.internal.GraphQLConstants.Keys;

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

import java.util.ArrayList;
import java.util.List;

public class BraintreeError implements Parcelable {

    private static final String FIELD_KEY = "field";
    private static final String MESSAGE_KEY = "message";
    private static final String FIELD_ERRORS_KEY = "fieldErrors";

    private String mField;
    private String mMessage;
    private List<BraintreeError> mFieldErrors;

    public static List<BraintreeError> fromJsonArray(JSONArray json) {
        if (json == null) {
            json = new JSONArray();
        }

        List<BraintreeError> errors = new ArrayList<>();
        for (int i = 0; i < json.length(); i++) {
            try {
                errors.add(BraintreeError.fromJson(json.getJSONObject(i)));
            } catch (JSONException ignored) {}
        }

        return errors;
    }

    protected static List<BraintreeError> fromGraphQLJsonArray(JSONArray graphQLErrors) {
        List<BraintreeError> errors = new ArrayList<>();

        if (graphQLErrors == null) {
            return errors;
        }

        for (int i = 0; i < graphQLErrors.length(); i++) {
            try {
                JSONObject graphQLError = graphQLErrors.getJSONObject(i);
                JSONObject extensions = graphQLError.optJSONObject(Keys.EXTENSIONS);

                if (extensions == null || !ErrorTypes.USER.equals(extensions.optString(Keys.ERROR_TYPE))) {
                    continue;
                }

                ArrayList<String> inputPath = new ArrayList<>();
                JSONArray inputPathJSON = extensions.getJSONArray(Keys.INPUT_PATH);

                for (int j = 1; j < inputPathJSON.length(); j++) {
                    inputPath.add(inputPathJSON.getString(j));
                }

                addGraphQLFieldError(inputPath, graphQLError, errors);
            } catch (JSONException ignored) {}
        }

        return errors;
    }

    public static BraintreeError fromJson(JSONObject json) {
        BraintreeError error = new BraintreeError();
        error.mField = Json.optString(json, FIELD_KEY, null);
        error.mMessage = Json.optString(json, MESSAGE_KEY, null);
        error.mFieldErrors = BraintreeError.fromJsonArray(json.optJSONArray(FIELD_ERRORS_KEY));

        return error;
    }

    private static void addGraphQLFieldError(List<String> inputPath, JSONObject errorJSON, List<BraintreeError> errors) throws JSONException {
        String field = inputPath.get(0);

        if (inputPath.size() == 1) {
            BraintreeError error = new BraintreeError();
            error.mField = field;
            error.mMessage = errorJSON.getString(Keys.MESSAGE);
            error.mFieldErrors = new ArrayList<>();

            errors.add(error);
            return;
        }

        BraintreeError nestedError = null;
        List<String> nestedInputPath = inputPath.subList(1, inputPath.size());

        for (BraintreeError error : errors) {
            if (error.mField.equals(field)) {
                nestedError = error;
            }
        }

        if (nestedError == null) {
            nestedError = new BraintreeError();
            nestedError.mField = field;
            nestedError.mFieldErrors = new ArrayList<>();

            errors.add(nestedError);
        }

        addGraphQLFieldError(nestedInputPath, errorJSON, nestedError.mFieldErrors);
    }

    /**
     * @return Human readable summary of the error for field. May be {@code null}.
     */
    @Nullable
    public String getMessage() {
        return mMessage;
    }

    /**
     * @return Field name this object represents.
     */
    public String getField() {
        return mField;
    }

    /**
     * @return {@link BraintreeError} objects for any errors nested under this field.
     */
    public List<BraintreeError> getFieldErrors() {
        return mFieldErrors;
    }

    /**
     * Method to extract an error for an individual field, e.g. creditCard, customer, etc.
     *
     * @param field Name of the field desired, expected to be in camelCase.
     * @return {@link BraintreeError} for the field searched, or {@code null} if not found.
     */
    @Nullable
    public BraintreeError errorFor(String field) {
        BraintreeError returnError;
        if (mFieldErrors != null) {
            for (BraintreeError error : mFieldErrors) {
                if (error.getField().equals(field)) {
                    return error;
                } else if (error.getFieldErrors() != null) {
                    returnError = error.errorFor(field);
                    if (returnError != null) {
                        return returnError;
                    }
                }
            }
        }
        return null;
    }

    public String toString() {
        return "BraintreeError for " + mField + ": " + mMessage + " -> " +
                (mFieldErrors != null ? mFieldErrors.toString() : "");
    }

    public BraintreeError() {}

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(mField);
        dest.writeString(mMessage);
        dest.writeTypedList(mFieldErrors);
    }

    protected BraintreeError(Parcel in) {
        mField = in.readString();
        mMessage = in.readString();
        mFieldErrors = in.createTypedArrayList(BraintreeError.CREATOR);
    }

    public static final Creator<BraintreeError> CREATOR = new Creator<BraintreeError>() {
        public BraintreeError createFromParcel(Parcel source) {
            return new BraintreeError(source);
        }

        public BraintreeError[] newArray(int size) {
            return new BraintreeError[size];
        }
    };
}


package com.braintreepayments.api.models;

import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import android.text.TextUtils;

import com.braintreepayments.api.Json;

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

/**
 * {@link PaymentMethodNonce} representing a PayPal account.
 *
 * @see {@link CardNonce}
 * @see {@link PaymentMethodNonce}
 */
public class PayPalAccountNonce extends PaymentMethodNonce implements Parcelable {

    protected static final String TYPE = "PayPalAccount";
    protected static final String API_RESOURCE_KEY = "paypalAccounts";
    protected static final String PAYMENT_METHOD_DATA_KEY = "paymentMethodData";
    protected static final String TOKENIZATION_DATA_KEY = "tokenizationData";
    protected static final String TOKEN_KEY = "token";

    private static final String CREDIT_FINANCING_KEY = "creditFinancingOffered";
    private static final String DETAILS_KEY = "details";
    private static final String EMAIL_KEY = "email";
    private static final String PAYER_INFO_KEY = "payerInfo";
    private static final String ACCOUNT_ADDRESS_KEY = "accountAddress";
    private static final String SHIPPING_ADDRESS_KEY = "shippingAddress";
    private static final String BILLING_ADDRESS_KEY = "billingAddress";
    private static final String FIRST_NAME_KEY = "firstName";
    private static final String LAST_NAME_KEY = "lastName";
    private static final String PHONE_KEY = "phone";
    private static final String PAYER_ID_KEY = "payerId";
    private static final String CLIENT_METADATA_ID_KEY = "correlationId";

    private String mClientMetadataId;
    private PostalAddress mBillingAddress;
    private PostalAddress mShippingAddress;
    private String mFirstName;
    private String mLastName;
    private String mPhone;
    private String mEmail;
    private String mPayerId;
    private PayPalCreditFinancing mCreditFinancing;

    /**
     * Convert an API response to a {@link PayPalAccountNonce}.
     *
     * @param jsonString Raw JSON representation of a {@link PayPalAccountNonce}.
     * @return {@link PayPalAccountNonce} for use in payment method selection UIs.
     */
    public static PayPalAccountNonce fromJson(String jsonString) throws JSONException {
        JSONObject jsonObj = new JSONObject(jsonString);
        PayPalAccountNonce payPalAccountNonce = new PayPalAccountNonce();

        if(jsonObj.has(PayPalAccountNonce.API_RESOURCE_KEY)) {
            payPalAccountNonce.fromJson(PayPalAccountNonce.getJsonObjectForType(API_RESOURCE_KEY, jsonObj));
        } else if(jsonObj.has(PayPalAccountNonce.PAYMENT_METHOD_DATA_KEY)) {
            JSONObject tokenObj = new JSONObject(new JSONObject(jsonString)
                    .getJSONObject(PayPalAccountNonce.PAYMENT_METHOD_DATA_KEY)
                    .getJSONObject(PayPalAccountNonce.TOKENIZATION_DATA_KEY)
                    .getString(PayPalAccountNonce.TOKEN_KEY));

            payPalAccountNonce.fromJson(PayPalAccountNonce.getJsonObjectForType(API_RESOURCE_KEY, tokenObj));
            JSONObject shippingAddress = jsonObj.optJSONObject(SHIPPING_ADDRESS_KEY);
            if (shippingAddress != null) {
                payPalAccountNonce.mShippingAddress = PostalAddressParser.fromJson(shippingAddress);
            }
        } else {
            throw new JSONException("Could not parse JSON for a payment method nonce");
        }

        return payPalAccountNonce;
    }

    /**
     * Generates a {@link PayPalAccountNonce} from the {@link JSONObject}.
     *
     * @param json {@link JSONObject} that holds properties for {@link PayPalAccountNonce}.
     * @throws JSONException
     */
    protected void fromJson(JSONObject json) throws JSONException {
        super.fromJson(json);

        JSONObject details = json.getJSONObject(DETAILS_KEY);
        mEmail = Json.optString(details, EMAIL_KEY, null);
        mClientMetadataId = Json.optString(details, CLIENT_METADATA_ID_KEY, null);

        try {
            if (details.has(CREDIT_FINANCING_KEY)) {
                JSONObject creditFinancing = details.getJSONObject(CREDIT_FINANCING_KEY);
                mCreditFinancing = PayPalCreditFinancing.fromJson(creditFinancing);
            }

            JSONObject payerInfo = details.getJSONObject(PAYER_INFO_KEY);

            JSONObject billingAddress = payerInfo.optJSONObject(BILLING_ADDRESS_KEY);
            if (payerInfo.has(ACCOUNT_ADDRESS_KEY)) {
                billingAddress = payerInfo.optJSONObject(ACCOUNT_ADDRESS_KEY);
            }

            mShippingAddress = PostalAddressParser.fromJson(payerInfo.optJSONObject(SHIPPING_ADDRESS_KEY));
            mBillingAddress = PostalAddressParser.fromJson(billingAddress);
            mFirstName = Json.optString(payerInfo, FIRST_NAME_KEY, "");
            mLastName = Json.optString(payerInfo, LAST_NAME_KEY, "");
            mPhone = Json.optString(payerInfo, PHONE_KEY, "");
            mPayerId = Json.optString(payerInfo, PAYER_ID_KEY, "");

            if(mEmail == null) {
                mEmail = Json.optString(payerInfo, EMAIL_KEY, null);
            }
        } catch (JSONException e) {
            mBillingAddress = new PostalAddress();
            mShippingAddress = new PostalAddress();
        }
    }

    /**
     * @return The email address associated with this PayPal account
     */
    public String getEmail() {
        return mEmail;
    }

    /**
     * @return The description of this PayPal account for displaying to a customer, either email or
     * 'PayPal'
     */
    @Override
    public String getDescription() {
        if (TextUtils.equals(super.getDescription(), "PayPal") && !TextUtils.isEmpty(getEmail())) {
            return getEmail();
        } else {
            return super.getDescription();
        }
    }

    /**
     * @return The type of this {@link PaymentMethodNonce} (always "PayPal")
     */
    @Override
    public String getTypeLabel() {
        return "PayPal";
    }

    /**
     * @return The billing address of the user if requested with additional scopes.
     */
    public PostalAddress getBillingAddress() {
        return mBillingAddress;
    }

    /**
     * @return The shipping address of the user provided by checkout flows.
     */
    public PostalAddress getShippingAddress() {
        return mShippingAddress;
    }

    /**
     * @return The first name associated with the PayPal account.
     */
    public String getFirstName() {
        return mFirstName;
    }

    /**
     * @return The last name associated with the PayPal account.
     */
    public String getLastName() {
        return mLastName;
    }

    /**
     * @return The phone number associated with the PayPal account.
     */
    public String getPhone() {
        return mPhone;
    }

    /**
     * @return The ClientMetadataId associated with this transaction.
     */
    public String getClientMetadataId(){
        return mClientMetadataId;
    }

    /**
     * @return The Payer ID provided in checkout flows.
     */
    public String getPayerId(){
        return mPayerId;
    }

    /**
     * @return The credit financing details. This property will only be present when the customer pays with PayPal Credit.
     */
    @Nullable
    public PayPalCreditFinancing getCreditFinancing() {
        return mCreditFinancing;
    }

    public PayPalAccountNonce() {}

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        super.writeToParcel(dest, flags);
        dest.writeString(mClientMetadataId);
        dest.writeParcelable(mBillingAddress, flags);
        dest.writeParcelable(mShippingAddress, flags);
        dest.writeString(mFirstName);
        dest.writeString(mLastName);
        dest.writeString(mEmail);
        dest.writeString(mPhone);
        dest.writeString(mPayerId);
        dest.writeParcelable(mCreditFinancing, flags);
    }

    private PayPalAccountNonce(Parcel in) {
        super(in);
        mClientMetadataId = in.readString();
        mBillingAddress = in.readParcelable(PostalAddress.class.getClassLoader());
        mShippingAddress = in.readParcelable(PostalAddress.class.getClassLoader());
        mFirstName = in.readString();
        mLastName = in.readString();
        mEmail = in.readString();
        mPhone = in.readString();
        mPayerId = in.readString();
        mCreditFinancing = in.readParcelable(PayPalCreditFinancing.class.getClassLoader());
    }

    public static final Creator<PayPalAccountNonce> CREATOR = new Creator<PayPalAccountNonce>() {
        public PayPalAccountNonce createFromParcel(Parcel source) {
            return new PayPalAccountNonce(source);
        }

        public PayPalAccountNonce[] newArray(int size) {
            return new PayPalAccountNonce[size];
        }
    };
}


package com.braintreepayments.api;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentManager;

import com.braintreepayments.api.exceptions.BraintreeException;
import com.braintreepayments.api.exceptions.ConfigurationException;
import com.braintreepayments.api.exceptions.GoogleApiClientException;
import com.braintreepayments.api.exceptions.GoogleApiClientException.ErrorType;
import com.braintreepayments.api.exceptions.InvalidArgumentException;
import com.braintreepayments.api.interfaces.AmericanExpressListener;
import com.braintreepayments.api.interfaces.BraintreeCancelListener;
import com.braintreepayments.api.interfaces.BraintreeErrorListener;
import com.braintreepayments.api.interfaces.BraintreeListener;
import com.braintreepayments.api.interfaces.BraintreePaymentResultListener;
import com.braintreepayments.api.interfaces.BraintreeResponseListener;
import com.braintreepayments.api.interfaces.ConfigurationListener;
import com.braintreepayments.api.interfaces.PaymentMethodNonceCreatedListener;
import com.braintreepayments.api.interfaces.PaymentMethodNonceDeletedListener;
import com.braintreepayments.api.interfaces.PaymentMethodNoncesUpdatedListener;
import com.braintreepayments.api.interfaces.QueuedCallback;
import com.braintreepayments.api.interfaces.UnionPayListener;
import com.braintreepayments.api.internal.AnalyticsDatabase;
import com.braintreepayments.api.internal.AnalyticsEvent;
import com.braintreepayments.api.internal.AnalyticsIntentService;
import com.braintreepayments.api.internal.AnalyticsSender;
import com.braintreepayments.api.internal.BraintreeApiHttpClient;
import com.braintreepayments.api.internal.BraintreeGraphQLHttpClient;
import com.braintreepayments.api.internal.BraintreeHttpClient;
import com.braintreepayments.api.internal.IntegrationType;
import com.braintreepayments.api.internal.UUIDHelper;
import com.braintreepayments.api.models.AmericanExpressRewardsBalance;
import com.braintreepayments.api.models.Authorization;
import com.braintreepayments.api.models.BraintreePaymentResult;
import com.braintreepayments.api.models.BraintreeRequestCodes;
import com.braintreepayments.api.models.Configuration;
import com.braintreepayments.api.models.PaymentMethodNonce;
import com.braintreepayments.api.models.TokenizationKey;
import com.braintreepayments.api.models.UnionPayCapabilities;
import com.braintreepayments.browserswitch.BrowserSwitchFragment;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;
import com.google.android.gms.wallet.Wallet;
import com.google.android.gms.wallet.WalletConstants;

import org.json.JSONException;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Queue;

/**
 * Core Braintree class that handles network requests and managing callbacks.
 */
public class BraintreeFragment extends BrowserSwitchFragment {

    public static final String TAG = "com.braintreepayments.api.BraintreeFragment";

    public static final String EXTRA_WAS_BROWSER_SWITCH_RESULT = "com.braintreepayments.api.WAS_BROWSER_SWITCH_RESULT";

    private static final String EXTRA_AUTHORIZATION_TOKEN = "com.braintreepayments.api.EXTRA_AUTHORIZATION_TOKEN";
    private static final String EXTRA_INTEGRATION_TYPE = "com.braintreepayments.api.EXTRA_INTEGRATION_TYPE";
    private static final String EXTRA_SESSION_ID = "com.braintreepayments.api.EXTRA_SESSION_ID";

    @VisibleForTesting
    static final String EXTRA_CONFIGURATION = "com.braintreepayments.api.EXTRA_CONFIGURATION";
    @VisibleForTesting
    static final String EXTRA_CACHED_PAYMENT_METHOD_NONCES = "com.braintreepayments.api.EXTRA_CACHED_PAYMENT_METHOD_NONCES";
    @VisibleForTesting
    static final String EXTRA_FETCHED_PAYMENT_METHOD_NONCES = "com.braintreepayments.api.EXTRA_FETCHED_PAYMENT_METHOD_NONCES";

    @VisibleForTesting
    protected BraintreeHttpClient mHttpClient;
    @VisibleForTesting
    protected BraintreeApiHttpClient mBraintreeApiClient;
    @VisibleForTesting
    protected BraintreeGraphQLHttpClient mGraphQLHttpClient;
    @VisibleForTesting
    protected GoogleApiClient mGoogleApiClient;

    private CrashReporter mCrashReporter;
    private Authorization mAuthorization;
    private Configuration mConfiguration;
    private final Queue<QueuedCallback> mCallbackQueue = new ArrayDeque<>();
    private final List<PaymentMethodNonce> mCachedPaymentMethodNonces = new ArrayList<>();
    private boolean mHasFetchedPaymentMethodNonces = false;
    private boolean mNewActivityNeedsConfiguration;
    private int mConfigurationRequestAttempts = 0;
    private String mIntegrationType;
    private String mSessionId;
    private AnalyticsDatabase mAnalyticsDatabase;

    private ConfigurationListener mConfigurationListener;
    private BraintreeResponseListener<Exception> mConfigurationErrorListener;
    private BraintreeCancelListener mCancelListener;
    private PaymentMethodNoncesUpdatedListener mPaymentMethodNoncesUpdatedListener;
    private PaymentMethodNonceCreatedListener mPaymentMethodNonceCreatedListener;
    private PaymentMethodNonceDeletedListener mPaymentMethodNonceDeletedListener;
    private BraintreeErrorListener mErrorListener;
    private BraintreePaymentResultListener mBraintreePaymentResultListener;
    private UnionPayListener mUnionPayListener;
    private AmericanExpressListener mAmericanExpressListener;

    public BraintreeFragment() {}

    /**
     * Create a new instance of {@link BraintreeFragment} using the client token and add it to the
     * {@link AppCompatActivity}'s {@link FragmentManager}.
     *
     * @param activity The {@link AppCompatActivity} to add the {@link BraintreeFragment} to.
     * @param authorization The tokenization key or client token to use.
     * @return {@link BraintreeFragment}
     * @throws InvalidArgumentException If the tokenization key or client token is not valid or cannot be
     *         parsed.
     */
    public static BraintreeFragment newInstance(AppCompatActivity activity, String authorization)
            throws InvalidArgumentException {
        if (activity == null) {
            throw new InvalidArgumentException("Activity is null");
        }

        FragmentManager fm = activity.getSupportFragmentManager();
        BraintreeFragment braintreeFragment = (BraintreeFragment) fm.findFragmentByTag(TAG);
        if (braintreeFragment == null) {
            braintreeFragment = new BraintreeFragment();
            Bundle bundle = new Bundle();

            try {
                Authorization auth = Authorization.fromString(authorization);
                bundle.putParcelable(EXTRA_AUTHORIZATION_TOKEN, auth);
            } catch (InvalidArgumentException e) {
                throw new InvalidArgumentException("Tokenization Key or client token was invalid.");
            }

            bundle.putString(EXTRA_SESSION_ID, UUIDHelper.getFormattedUUID());
            bundle.putString(EXTRA_INTEGRATION_TYPE, IntegrationType.get(activity));
            braintreeFragment.setArguments(bundle);

            try {
                if (VERSION.SDK_INT >= VERSION_CODES.N) {
                    try {
                        fm.beginTransaction().add(braintreeFragment, TAG).commitNow();
                    } catch (IllegalStateException | NullPointerException e) {
                        fm.beginTransaction().add(braintreeFragment, TAG).commit();
                        try {
                            fm.executePendingTransactions();
                        } catch (IllegalStateException ignored) {}
                    }
                } else {
                    fm.beginTransaction().add(braintreeFragment, TAG).commit();
                    try {
                        fm.executePendingTransactions();
                    } catch (IllegalStateException ignored) {}
                }
            } catch (IllegalStateException e) {
                throw new InvalidArgumentException(e.getMessage());
            }
        }

        braintreeFragment.mContext = activity.getApplicationContext();

        return braintreeFragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);

        if (mContext == null) {
            mContext = getActivity().getApplicationContext();
        }

        mNewActivityNeedsConfiguration = false;
        mCrashReporter = CrashReporter.setup(this);
        mSessionId = getArguments().getString(EXTRA_SESSION_ID);
        mIntegrationType = getArguments().getString(EXTRA_INTEGRATION_TYPE);
        mAuthorization = getArguments().getParcelable(EXTRA_AUTHORIZATION_TOKEN);
        mAnalyticsDatabase = AnalyticsDatabase.getInstance(getApplicationContext());

        if (mHttpClient == null) {
            mHttpClient = new BraintreeHttpClient(mAuthorization);
        }

        if (savedInstanceState != null) {
            List<PaymentMethodNonce> paymentMethodNonces =
                    savedInstanceState.getParcelableArrayList(EXTRA_CACHED_PAYMENT_METHOD_NONCES);
            if (paymentMethodNonces != null) {
                mCachedPaymentMethodNonces.addAll(paymentMethodNonces);
            }

            mHasFetchedPaymentMethodNonces = savedInstanceState.getBoolean(EXTRA_FETCHED_PAYMENT_METHOD_NONCES);
            try {
                setConfiguration(Configuration.fromJson(savedInstanceState.getString(EXTRA_CONFIGURATION)));
            } catch (JSONException ignored) {}
        } else {
            if (mAuthorization instanceof TokenizationKey) {
                sendAnalyticsEvent("started.client-key");
            } else {
                sendAnalyticsEvent("started.client-token");
            }
        }

        fetchConfiguration();
    }

    @TargetApi(VERSION_CODES.M)
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        onAttach(getActivity());
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        mNewActivityNeedsConfiguration = true;
    }

    @Override
    public void onResume() {
        super.onResume();

        if (getActivity() instanceof BraintreeListener) {
            addListener((BraintreeListener) getActivity());

            if (mNewActivityNeedsConfiguration && getConfiguration() != null) {
                mNewActivityNeedsConfiguration = false;
                postConfigurationCallback();
            }
        }

        flushCallbacks();

        if (mGoogleApiClient != null && !mGoogleApiClient.isConnected() &&
                !mGoogleApiClient.isConnecting()) {
            mGoogleApiClient.connect();
        }
    }

    @Override
    public void onPause() {
        super.onPause();

        if (getActivity() instanceof BraintreeListener) {
            removeListener((BraintreeListener) getActivity());
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putParcelableArrayList(EXTRA_CACHED_PAYMENT_METHOD_NONCES,
                (ArrayList<? extends Parcelable>) mCachedPaymentMethodNonces);
        outState.putBoolean(EXTRA_FETCHED_PAYMENT_METHOD_NONCES, mHasFetchedPaymentMethodNonces);

        if (mConfiguration != null) {
            outState.putString(EXTRA_CONFIGURATION, mConfiguration.toJson());
        }
    }

    @Override
    public void onStop() {
        super.onStop();

        if (mGoogleApiClient != null) {
            mGoogleApiClient.disconnect();
        }

        flushAnalyticsEvents();
    }

    @Override
    public void onDetach() {
        super.onDetach();

        if (mGoogleApiClient != null) {
            mGoogleApiClient.disconnect();
            mGoogleApiClient = null;
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        mCrashReporter.tearDown();
    }

    @Override
    public void startActivityForResult(Intent intent, int requestCode) {
        if (!isAdded()) {
            postCallback(new BraintreeException("BraintreeFragment is not attached to an Activity. Please ensure it " +
                    "is attached and try again."));
        } else {
            super.startActivityForResult(intent, requestCode);
        }
    }

    @Override
    public String getReturnUrlScheme() {
        return getApplicationContext().getPackageName().toLowerCase(Locale.ROOT)
                .replace("_", "") + ".braintree";
    }

    @Override
    public void onBrowserSwitchResult(int requestCode, BrowserSwitchResult browserSwitchResult, @Nullable Uri uri) {
        String type = "";
        Intent intent = new Intent()
                .putExtra(EXTRA_WAS_BROWSER_SWITCH_RESULT, true);

        switch (requestCode) {
            case BraintreeRequestCodes.PAYPAL:
                type = "paypal";
                break;
            case BraintreeRequestCodes.THREE_D_SECURE:
                type =  "three-d-secure";
                break;
            case BraintreeRequestCodes.LOCAL_PAYMENT:
                type = "local-payment";
                break;
        }

        int resultCode = AppCompatActivity.RESULT_FIRST_USER;
        if (browserSwitchResult == BrowserSwitchResult.OK) {
            resultCode = AppCompatActivity.RESULT_OK;
            sendAnalyticsEvent(type + ".browser-switch.succeeded");
        } else if (browserSwitchResult == BrowserSwitchResult.CANCELED) {
            resultCode = AppCompatActivity.RESULT_CANCELED;
            sendAnalyticsEvent(type + ".browser-switch.canceled");
        } else if (browserSwitchResult == BrowserSwitchResult.ERROR) {
            if (browserSwitchResult.getErrorMessage().startsWith("No installed activities")) {
                sendAnalyticsEvent(type + ".browser-switch.failed.no-browser-installed");
            } else {
                sendAnalyticsEvent(type + ".browser-switch.failed.not-setup");
            }
        }

        onActivityResult(requestCode, resultCode, intent.setData(uri));
    }

    @Override
    public void onActivityResult(final int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
            case BraintreeRequestCodes.PAYPAL:
                PayPal.onActivityResult(this, resultCode, data);
                break;
            case BraintreeRequestCodes.THREE_D_SECURE:
                ThreeDSecure.onActivityResult(this, resultCode, data);
                break;
            case BraintreeRequestCodes.VENMO:
                Venmo.onActivityResult(this, resultCode, data);
                break;
            case BraintreeRequestCodes.VISA_CHECKOUT:
                VisaCheckoutFacade.onActivityResult(this, resultCode, data);
                break;
            case BraintreeRequestCodes.LOCAL_PAYMENT:
                LocalPayment.onActivityResult(this, resultCode, data);
                break;
            case BraintreeRequestCodes.GOOGLE_PAYMENT:
                GooglePayment.onActivityResult(this, resultCode, data);
                break;
        }

        if (resultCode == AppCompatActivity.RESULT_CANCELED) {
            postCancelCallback(requestCode);
        }
    }

    /**
     * Adds a listener.
     *
     * @param listener the listener to add.
     */
    public <T extends BraintreeListener> void addListener(T listener) {
        if (listener instanceof ConfigurationListener) {
            mConfigurationListener = (ConfigurationListener) listener;
        }

        if (listener instanceof BraintreeCancelListener) {
            mCancelListener = (BraintreeCancelListener) listener;
        }

        if (listener instanceof PaymentMethodNoncesUpdatedListener) {
            mPaymentMethodNoncesUpdatedListener = (PaymentMethodNoncesUpdatedListener) listener;
        }

        if (listener instanceof PaymentMethodNonceCreatedListener) {
            mPaymentMethodNonceCreatedListener = (PaymentMethodNonceCreatedListener) listener;
        }

        if (listener instanceof PaymentMethodNonceDeletedListener) {
            mPaymentMethodNonceDeletedListener = (PaymentMethodNonceDeletedListener) listener;
        }

        if (listener instanceof BraintreePaymentResultListener) {
            mBraintreePaymentResultListener = (BraintreePaymentResultListener) listener;
        }

        if (listener instanceof BraintreeErrorListener) {
            mErrorListener = (BraintreeErrorListener) listener;
        }

        if (listener instanceof UnionPayListener) {
            mUnionPayListener = (UnionPayListener) listener;
        }

        if (listener instanceof AmericanExpressListener) {
            mAmericanExpressListener = (AmericanExpressListener) listener;
        }

        flushCallbacks();
    }

    /**
     * Removes a previously added listener.
     *
     * @param listener the listener to remove.
     */
    public <T extends BraintreeListener> void removeListener(T listener) {
        if (listener instanceof ConfigurationListener) {
            mConfigurationListener = null;
        }

        if (listener instanceof BraintreeCancelListener) {
            mCancelListener = null;
        }

        if (listener instanceof PaymentMethodNoncesUpdatedListener) {
            mPaymentMethodNoncesUpdatedListener = null;
        }

        if (listener instanceof PaymentMethodNonceCreatedListener) {
            mPaymentMethodNonceCreatedListener = null;
        }

        if (listener instanceof PaymentMethodNonceDeletedListener) {
            mPaymentMethodNonceDeletedListener = null;
        }

        if (listener instanceof BraintreePaymentResultListener) {
            mBraintreePaymentResultListener = null;
        }

        if (listener instanceof BraintreeErrorListener) {
            mErrorListener = null;
        }

        if (listener instanceof UnionPayListener) {
            mUnionPayListener = null;
        }

        if (listener instanceof AmericanExpressListener) {
            mAmericanExpressListener = null;
        }
    }

    /**
     * @return {@link ArrayList<BraintreeListener>} of the currently attached listeners
     */
    public List<BraintreeListener> getListeners() {
        List<BraintreeListener> listeners = new ArrayList<>();

        if (mConfigurationListener != null) {
            listeners.add(mConfigurationListener);
        }

        if (mCancelListener != null) {
            listeners.add(mCancelListener);
        }

        if (mPaymentMethodNoncesUpdatedListener != null) {
            listeners.add(mPaymentMethodNoncesUpdatedListener);
        }

        if (mPaymentMethodNonceCreatedListener != null) {
            listeners.add(mPaymentMethodNonceCreatedListener);
        }

        if (mPaymentMethodNonceDeletedListener != null) {
            listeners.add(mPaymentMethodNonceDeletedListener);
        }

        if (mBraintreePaymentResultListener != null) {
            listeners.add(mBraintreePaymentResultListener);
        }

        if (mErrorListener != null) {
            listeners.add(mErrorListener);
        }

        if (mUnionPayListener != null) {
            listeners.add(mUnionPayListener);
        }

        if (mAmericanExpressListener != null) {
            listeners.add(mAmericanExpressListener);
        }

        return listeners;
    }

    /**
     * A boolean indicating whether the current customer's payment methods have been fetched with
     * {@link PaymentMethod#getPaymentMethodNonces(BraintreeFragment)} yet.
     *
     * @return {@code true} if the current customer's payment methods have been fetched, {@code false} otherwise.
     */
    public boolean hasFetchedPaymentMethodNonces() {
        return mHasFetchedPaymentMethodNonces;
    }

    /**
     * After fetching the current customer's {@link PaymentMethodNonce}s using
     * {@link PaymentMethod#getPaymentMethodNonces(BraintreeFragment)}, the {@link PaymentMethodNonce}s will be cached
     * for the life time of this instance of {@link BraintreeFragment} and can be returned without additional network
     * requests using this method.
     *
     * @return {@link List<PaymentMethodNonce>}s for the current customer.
     */
    public List<PaymentMethodNonce> getCachedPaymentMethodNonces() {
        return Collections.unmodifiableList(mCachedPaymentMethodNonces);
    }

    public void sendAnalyticsEvent(final String eventFragment) {
        final AnalyticsEvent request = new AnalyticsEvent(mContext, getSessionId(), mIntegrationType, eventFragment);
        waitForConfiguration(new ConfigurationListener() {
            @Override
            public void onConfigurationFetched(Configuration configuration) {
                if (configuration.getAnalytics().isEnabled()) {
                    mAnalyticsDatabase.addEvent(request);
                }
            }
        });
    }

    private void flushAnalyticsEvents() {
        if (getConfiguration() != null && getConfiguration().toJson() != null &&
                getConfiguration().getAnalytics().isEnabled()) {
            Intent intent = new Intent(mContext, AnalyticsIntentService.class)
                    .putExtra(AnalyticsIntentService.EXTRA_AUTHORIZATION, getAuthorization().toString())
                    .putExtra(AnalyticsIntentService.EXTRA_CONFIGURATION, getConfiguration().toJson());

            try {
                getApplicationContext().startService(intent);
            } catch (RuntimeException e) {
                AnalyticsSender.send(getApplicationContext(), mAuthorization, getHttpClient(),
                        getConfiguration().getAnalytics().getUrl(), false);
            }
        }
    }

    protected void postConfigurationCallback() {
        postOrQueueCallback(new QueuedCallback() {
            @Override
            public boolean shouldRun() {
                return mConfigurationListener != null;
            }

            @Override
            public void run() {
                mConfigurationListener.onConfigurationFetched(getConfiguration());
            }
        });
    }

    protected void postCancelCallback(final int requestCode) {
        postOrQueueCallback(new QueuedCallback() {
            @Override
            public boolean shouldRun() {
                return mCancelListener != null;
            }

            @Override
            public void run() {
                mCancelListener.onCancel(requestCode);
            }
        });
    }

    protected void postCallback(final PaymentMethodNonce paymentMethodNonce) {
        mCachedPaymentMethodNonces.add(0, paymentMethodNonce);

        postOrQueueCallback(new QueuedCallback() {
            @Override
            public boolean shouldRun() {
                return mPaymentMethodNonceCreatedListener != null;
            }

            @Override
            public void run() {
                mPaymentMethodNonceCreatedListener.onPaymentMethodNonceCreated(paymentMethodNonce);
            }
        });
    }

    protected void postCallback(final UnionPayCapabilities capabilities) {
        postOrQueueCallback(new QueuedCallback() {
            @Override
            public boolean shouldRun() {
                return mUnionPayListener != null;
            }

            @Override
            public void run() {
                mUnionPayListener.onCapabilitiesFetched(capabilities);
            }
        });
    }

    protected void postUnionPayCallback(final String enrollmentId, final boolean smsCodeRequired) {
        postOrQueueCallback(new QueuedCallback() {
            @Override
            public boolean shouldRun() {
                return mUnionPayListener != null;
            }

            @Override
            public void run() {
                mUnionPayListener.onSmsCodeSent(enrollmentId, smsCodeRequired);
            }
        });
    }

    protected void postCallback(final BraintreePaymentResult result) {
        postOrQueueCallback(new QueuedCallback() {
            @Override
            public boolean shouldRun() {
                return mBraintreePaymentResultListener != null;
            }

            @Override
            public void run() {
                mBraintreePaymentResultListener.onBraintreePaymentResult(result);
            }
        });
    }

    protected void postAmericanExpressCallback(final AmericanExpressRewardsBalance rewardsBalance) {
        postOrQueueCallback(new QueuedCallback() {
            @Override
            public boolean shouldRun() {
                return mAmericanExpressListener != null;
            }

            @Override
            public void run() {
                mAmericanExpressListener.onRewardsBalanceFetched(rewardsBalance);
            }
        });
    }

    protected void postCallback(final List<PaymentMethodNonce> paymentMethodNonceList) {
        mCachedPaymentMethodNonces.clear();
        mCachedPaymentMethodNonces.addAll(paymentMethodNonceList);
        mHasFetchedPaymentMethodNonces = true;
        postOrQueueCallback(new QueuedCallback() {
            @Override
            public boolean shouldRun() {
                return mPaymentMethodNoncesUpdatedListener != null;
            }

            @Override
            public void run() {
                mPaymentMethodNoncesUpdatedListener.onPaymentMethodNoncesUpdated(paymentMethodNonceList);
            }
        });
    }

    protected void postPaymentMethodDeletedCallback(final PaymentMethodNonce paymentMethodNonce) {
        postOrQueueCallback(new QueuedCallback() {
            @Override
            public boolean shouldRun() {
                return mPaymentMethodNonceDeletedListener != null;
            }

            @Override
            public void run() {
                mPaymentMethodNonceDeletedListener.onPaymentMethodNonceDeleted(paymentMethodNonce);
            }
        });
    }

    protected void postCallback(final Exception error) {
        postOrQueueCallback(new QueuedCallback() {
            @Override
            public boolean shouldRun() {
                return mErrorListener != null;
            }

            @Override
            public void run() {
                mErrorListener.onError(error);
            }
        });
    }

    @VisibleForTesting
    protected void postOrQueueCallback(QueuedCallback callback) {
        if (!callback.shouldRun()) {
            mCallbackQueue.add(callback);
        } else {
            callback.run();
        }
    }

    @VisibleForTesting
    protected void flushCallbacks() {
        synchronized (mCallbackQueue) {
            Queue<QueuedCallback> queue = new ArrayDeque<>(mCallbackQueue);
            for (QueuedCallback callback : queue) {
                if (callback.shouldRun()) {
                    callback.run();
                    mCallbackQueue.remove(callback);
                }
            }
        }
    }

    @VisibleForTesting
    protected void fetchConfiguration() {
        if (getConfiguration() != null || ConfigurationManager.isFetchingConfiguration() || mAuthorization == null ||
                mHttpClient == null) {
            return;
        }

        if (mConfigurationRequestAttempts >= 3) {
            postCallback(new ConfigurationException("Configuration retry limit has been exceeded. Create a new " +
                    "BraintreeFragment and try again."));
            return;
        }

        mConfigurationRequestAttempts++;

        ConfigurationManager.getConfiguration(this, new ConfigurationListener() {
            @Override
            public void onConfigurationFetched(Configuration configuration) {
                setConfiguration(configuration);
                postConfigurationCallback();
                flushCallbacks();
            }
        }, new BraintreeResponseListener<Exception>() {
            @Override
            public void onResponse(final Exception e) {
                final ConfigurationException exception =
                        new ConfigurationException("Request for configuration has failed: " + e.getMessage() + ". " +
                                "Future requests will retry up to 3 times", e);
                postCallback(exception);
                postOrQueueCallback(new QueuedCallback() {
                    @Override
                    public boolean shouldRun() {
                        return mConfigurationErrorListener != null;
                    }

                    @Override
                    public void run() {
                        mConfigurationErrorListener.onResponse(exception);
                    }
                });
                flushCallbacks();
            }
        });
    }

    protected void setConfigurationErrorListener(BraintreeResponseListener<Exception> listener) {
        mConfigurationErrorListener = listener;
    }

    protected void waitForConfiguration(final ConfigurationListener listener) {
        fetchConfiguration();

        postOrQueueCallback(new QueuedCallback() {
            @Override
            public boolean shouldRun() {
                return getConfiguration() != null && isAdded();
            }

            @Override
            public void run() {
                listener.onConfigurationFetched(getConfiguration());
            }
        });
    }

    protected Authorization getAuthorization() {
        return mAuthorization;
    }

    protected Context getApplicationContext() {
        return mContext;
    }

    protected Configuration getConfiguration() {
        return mConfiguration;
    }

    protected void setConfiguration(Configuration configuration) {
        mConfiguration = configuration;
        getHttpClient().setBaseUrl(configuration.getClientApiUrl());

        if (configuration.getGraphQL().isEnabled()) {
            mGraphQLHttpClient = new BraintreeGraphQLHttpClient(configuration.getGraphQL().getUrl(),
                    mAuthorization.getBearer());
        }
    }

    protected BraintreeHttpClient getHttpClient() {
        return mHttpClient;
    }

    @Nullable
    protected BraintreeApiHttpClient getBraintreeApiHttpClient() {
        if (mBraintreeApiClient == null && getConfiguration() != null &&
                getConfiguration().getBraintreeApiConfiguration().isEnabled()) {
            mBraintreeApiClient = new BraintreeApiHttpClient(getConfiguration().getBraintreeApiConfiguration().getUrl(),
                    getConfiguration().getBraintreeApiConfiguration().getAccessToken());
        }

        return mBraintreeApiClient;
    }

    @Nullable
    protected BraintreeGraphQLHttpClient getGraphQLHttpClient() {
        return mGraphQLHttpClient;
    }

    protected String getSessionId() {
        return mSessionId;
    }

    protected String getIntegrationType() {
        return mIntegrationType;
    }

    /**
     * Obtain an instance of a {@link GoogleApiClient} that is connected or connecting to be used
     * for Android Pay. This instance will be automatically disconnected in
     * {@link BraintreeFragment#onStop()} and automatically connected in
     * {@link BraintreeFragment#onResume()}.
     * <p/>
     * Connection failed and connection suspended errors will be sent to
     * {@link BraintreeErrorListener#onError(Exception)}.
     *
     * @param listener {@link BraintreeResponseListener<GoogleApiClient>} to receive the
     *                 {@link GoogleApiClient} in
     *                 {@link BraintreeResponseListener<GoogleApiClient>#onResponse(GoogleApiClient)}.
     */
    public void getGoogleApiClient(final BraintreeResponseListener<GoogleApiClient> listener) {
        waitForConfiguration(new ConfigurationListener() {
            @Override
            public void onConfigurationFetched(Configuration configuration) {
                GoogleApiClient googleApiClient = getGoogleApiClient();
                if (googleApiClient != null) {
                    listener.onResponse(googleApiClient);
                }
            }
        });
    }

    protected GoogleApiClient getGoogleApiClient() {
        if (getActivity() == null) {
            postCallback(new GoogleApiClientException(ErrorType.NotAttachedToActivity, 1));
            return null;
        }

        if (mGoogleApiClient == null) {
            mGoogleApiClient = new GoogleApiClient.Builder(getActivity())
                    .addApi(Wallet.API, new Wallet.WalletOptions.Builder()
                            .setEnvironment(GooglePayment.getEnvironment(getConfiguration().getGooglePayment()))
                            .setTheme(WalletConstants.THEME_LIGHT)
                            .build())
                    .build();
        }

        if (!mGoogleApiClient.isConnected() && !mGoogleApiClient.isConnecting()) {
            mGoogleApiClient.registerConnectionCallbacks(new ConnectionCallbacks() {
                @Override
                public void onConnected(Bundle bundle) {}

                @Override
                public void onConnectionSuspended(int i) {
                    postCallback(new GoogleApiClientException(ErrorType.ConnectionSuspended, i));
                }
            });

            mGoogleApiClient.registerConnectionFailedListener(new OnConnectionFailedListener() {
                @Override
                public void onConnectionFailed(ConnectionResult connectionResult) {
                    postCallback(new GoogleApiClientException(ErrorType.ConnectionFailed, connectionResult.getErrorCode()));
                }
            });

            mGoogleApiClient.connect();
        }

        return mGoogleApiClient;
    }
}


package com.braintreepayments.api.models;

import android.content.Context;

import com.braintreepayments.api.BraintreeFragment;
import com.braintreepayments.api.Json;
import com.braintreepayments.api.interfaces.BraintreeResponseListener;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.wallet.Wallet;

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

/**
 * Contains the remote Google Payment configuration for the Braintree SDK.
 */
public class GooglePaymentConfiguration {
    private static final String ENABLED_KEY = "enabled";
    private static final String GOOGLE_AUTHORIZATION_FINGERPRINT_KEY = "googleAuthorizationFingerprint";
    private static final String ENVIRONMENT_KEY = "environment";
    private static final String DISPLAY_NAME_KEY = "displayName";
    private static final String SUPPORTED_NETWORKS_KEY = "supportedNetworks";

    boolean mEnabled;
    String mGoogleAuthorizationFingerprint;
    String mEnvironment;
    String mDisplayName;
    String[] mSupportedNetworks;

    /**
     * Parse an {@link GooglePaymentConfiguration} from json.
     *
     * @param json The {@link JSONObject} to parse.
     * @return An {@link GooglePaymentConfiguration} instance with data that was able to be parsed from
     *         the {@link JSONObject}.
     */
    public static GooglePaymentConfiguration fromJson(JSONObject json) {
        if (json == null) {
            json = new JSONObject();
        }

        GooglePaymentConfiguration googlePaymentConfiguration = new GooglePaymentConfiguration();
        googlePaymentConfiguration.mEnabled = json.optBoolean(ENABLED_KEY, false);
        googlePaymentConfiguration.mGoogleAuthorizationFingerprint = Json.optString(json,
                GOOGLE_AUTHORIZATION_FINGERPRINT_KEY, null);
        googlePaymentConfiguration.mEnvironment = Json.optString(json, ENVIRONMENT_KEY, null);
        googlePaymentConfiguration.mDisplayName = Json.optString(json, DISPLAY_NAME_KEY, "");

        JSONArray supportedNetworks = json.optJSONArray(SUPPORTED_NETWORKS_KEY);
        if (supportedNetworks != null) {
            googlePaymentConfiguration.mSupportedNetworks = new String[supportedNetworks.length()];
            for (int i = 0; i < supportedNetworks.length(); i++) {
                try {
                    googlePaymentConfiguration.mSupportedNetworks[i] = supportedNetworks.getString(i);
                } catch (JSONException ignored) {}
            }
        } else {
            googlePaymentConfiguration.mSupportedNetworks = new String[0];
        }

        return googlePaymentConfiguration;
    }

    /**
     * @return {@code true} if Google Payment is enabled and supported in the current environment,
     *         {@code false} otherwise. Note: this value only pertains to the Braintree configuration, to check if
     *         the user has Google Payment setup use
     *         {@link com.braintreepayments.api.GooglePayment#isReadyToPay(BraintreeFragment, BraintreeResponseListener)}
     */
    public boolean isEnabled(Context context) {
        try {
            Class.forName(Wallet.class.getName());

            return mEnabled && GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) ==
                    ConnectionResult.SUCCESS;
        } catch (ClassNotFoundException | NoClassDefFoundError e) {
            return false;
        }
    }

    /**
     * @return the authorization fingerprint to use for Google Payment, only allows tokenizing Google Payment cards.
     */
    public String getGoogleAuthorizationFingerprint() {
        return mGoogleAuthorizationFingerprint;
    }

    /**
     * @return the current Google Payment environment.
     */
    public String getEnvironment() {
        return mEnvironment;
    }

    /**
     * @return the display name to show to the user.
     */
    public String getDisplayName() {
        return mDisplayName;
    }

    /**
     * @return a string array of supported card networks for Google Payment.
     */
    public String[] getSupportedNetworks() {
        return mSupportedNetworks;
    }
}