/* * Copyright (C) 2014 SCVNGR, Inc. d/b/a LevelUp * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.scvngr.levelup.core.model.qr; import android.content.res.Resources; import android.content.res.TypedArray; import android.support.annotation.NonNull; import android.text.TextUtils; import com.scvngr.levelup.core.R; import com.scvngr.levelup.core.annotation.LevelUpApi; import com.scvngr.levelup.core.annotation.LevelUpApi.Contract; import com.scvngr.levelup.core.annotation.VisibleForTesting; import com.scvngr.levelup.core.annotation.VisibleForTesting.Visibility; import com.scvngr.levelup.core.model.tip.PercentageTip; import com.scvngr.levelup.core.model.tip.Tip; import com.scvngr.levelup.core.util.LogManager; import net.jcip.annotations.Immutable; /** * <p> * Class to represent a QR code. Note: this is not a representation of the web service model. * </p> * * <p> * The primary goal of this class is to enable the parsing of colors from the QR codes scanned by * the merchant. This does not validate codes, it attempts to determine code version for the sole * purpose of parsing colors. The server will always be the final validator of codes. * </p> * * <p> * Callers should not instantiate this class, they should just call * {@link LevelUpCode#parseColor(Resources, String)} and it will return the color parsed or the * default color. * </p> */ @Immutable @LevelUpApi(contract = Contract.DRAFT) public abstract class LevelUpCode { /** * The QR code data. */ @NonNull protected final String mData; /** * @param qrData the data read from the QR code */ protected LevelUpCode(@NonNull final String qrData) { mData = qrData; } /** * Parse the color parameter from this QR code. Try all supported versions of the QR code, * falling back to the default dock scan color if no color can be parsed. * * @param resources the resources to use to get color resources. * @param qrData the QR code string scanned. * @return the color the dock should display. */ public static int parseColor(@NonNull final Resources resources, @NonNull final String qrData) { int dockColor = PaymentPreferences.COLOR_UNKNOWN; if (!TextUtils.isEmpty(qrData)) { final LevelUpCode code = getFullPaymentTokenVersion(qrData); dockColor = code.getColor(resources); } if (PaymentPreferences.COLOR_UNKNOWN == dockColor) { // If we couldn't find anything, then use the default. dockColor = resources.getColor(R.color.dock_default_scan_color); } return dockColor; } /** * Encodes the payment preferences to the end of the QR code string passed. * * @param data the QR code data to append the preferences to. * @param color the color to encode. This value is between 0 and 9, indexing into a list of * preset colors. * @param tip the tip to encode. * @return the String with the payment preferences encoded into it. */ @NonNull public static String encodeLevelUpCode(@NonNull final String data, final int color, final Tip<?> tip) { String encodedQr = data; if (!TextUtils.isEmpty(data)) { final LevelUpCode code = getPaymentTokenVersion(data); int constrainedColor = color; if (color < 0) { constrainedColor = 0; } encodedQr = code.encodePaymentPreferences(constrainedColor, tip); } return encodedQr; } /** * Encodes the payment preferences to the end of the QR code string passed. * * @param data the QR code data to append the preferences to. * @param color the color to encode. This value is between 0 and 9, indexing into a list of * preset colors. * @param tipValue the tip value to encode. * @return the String with the payment preferences encoded into it. * @deprecated use {@link #encodeLevelUpCode(String, int, Tip)} instead. */ @NonNull @Deprecated public static String encodeLevelUpCode(@NonNull final String data, final int color, final int tipValue) { return encodeLevelUpCode(data, color, new PercentageTip(tipValue)); } /** * Subclasses must implement this to encode the payment preferences into the LevelUp code. * * @param color the color to encode. This is an index between 0 and 9, inclusive. * @param tip the tip to encode. * @return the full LevelUp code with the payment preferences encoded into it */ @NonNull /* package */abstract String encodePaymentPreferences(int color, Tip<?> tip); /** * Get the color from this LevelUp code. * * @param resources the resources to use to resolve the color. * @return the int color or {@link PaymentPreferences#COLOR_UNKNOWN} if none were found in QR * code. */ protected abstract int getColor(@NonNull final Resources resources); /** * Detect the version of the full LevelUp code (including tip and color) passed. * * @param data the data string scanned * @return {@link LevelUpCode} for the version detected. */ @NonNull @VisibleForTesting(visibility = Visibility.PRIVATE) /* package */static LevelUpCode getFullPaymentTokenVersion(@NonNull final String data) { return new PaymentTokenV2(data); } /** * Detect the version of the Payment Token portion of the QR code passed. * * @param data the data string scanned * @return {@link LevelUpCode} for the version detected. */ @NonNull @VisibleForTesting(visibility = Visibility.PRIVATE) /* package */static LevelUpCode getPaymentTokenVersion(@NonNull final String data) { return new PaymentTokenV2(data); } /** * @param position the position in the color array that was parsed from the QR code data. * @param resources Application resources. * @return The color decoded from {@code colorToDecode} or * {@link PaymentPreferences#COLOR_UNKNOWN} if {@code colorToDecode} couldn't be * decoded. */ protected static int decodeColor(final int position, @NonNull final Resources resources) { int color = PaymentPreferences.COLOR_UNKNOWN; String name = null; TypedArray array = null; try { array = resources.obtainTypedArray(R.array.levelup_dock_colors); if (0 <= position && array.length() > position) { name = resources.getResourceName(array.getResourceId(position, -1)); color = array.getColor(position, -1); } } finally { if (null != array) { array.recycle(); array = null; } } LogManager.d("Scanned color position=%d %s=%x", position, name, color); return color; } }