diff --git a/app.gradle b/app.gradle index 677332579..6460b00a7 100644 --- a/app.gradle +++ b/app.gradle @@ -1,5 +1,6 @@ apply plugin: 'com.android.application' apply from: file("${project.rootDir}/common.gradle") +apply from: file("${project.rootDir}/list-dependencies.gradle") /** * Gets the full path of the proguard file specified by `name`. @@ -80,8 +81,14 @@ android { matchingFallbacks = ['release'] } + + } + + buildFeatures { + buildConfig = true } + compileSdkVersion COMPILE_SDK_VERSION useLibrary HTTP_LIBRARY @@ -103,8 +110,4 @@ android { lintOptions { abortOnError false } - - dexOptions { - preDexLibraries = Boolean.valueOf(System.getProperty("androidPreDex", "true")) - } } diff --git a/assets/clover.css b/assets/clover.css new file mode 100644 index 000000000..103ba9f03 --- /dev/null +++ b/assets/clover.css @@ -0,0 +1,13 @@ +.theme-dark { + --sidemenu-section-active-color: rgba(34, 136, 0, 0.5) !important; +} +html:not(.theme-dark) { + --sidemenu-section-active-color: rgba(34, 136, 0, 0.09) !important; +} +html:not(.theme-dark) .sideMenuPart[data-active] > .overview > a { + color: #1c6e00 !important; +} + +html:not(.theme-dark) .sideMenuPart[data-active] > .overview .navButtonContent::before { + background-color: var(--default-font-color); +} \ No newline at end of file diff --git a/assets/logo-icon.svg b/assets/logo-icon.svg new file mode 100644 index 000000000..48a97f13e --- /dev/null +++ b/assets/logo-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 28a2aa03d..1d288ca24 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import org.jetbrains.dokka.gradle.DokkaTaskPartial +import org.gradle.api.internal.project.DefaultProject buildscript { if (!project.hasProperty('androidBuild')) { def likelyAndroidBuild = file('../android-build') @@ -32,6 +33,9 @@ buildscript { } google() mavenCentral() + maven { + url "https://plugins.gradle.org/m2/" + } } dependencies { @@ -42,10 +46,74 @@ buildscript { } } +plugins { + id("org.jetbrains.dokka") version "1.9.20" +} + +repositories { + mavenCentral() +} + defaultTasks 'clean', 'install' // SonarQube static analysis configuration // Adding detekt tasks to all the sub-modules subprojects { + tasks.withType(DokkaTaskPartial.class).configureEach { + dependsOn compileReleaseAidl + moduleName.set(project.name) + moduleVersion.set(project.version.toString()) + + String config = """ + { + "customAssets": ["${file("assets/logo-icon.svg")}"], + "customStyleSheets": ["${file("assets/clover.css")}"] + } + """ + pluginsMapConfiguration.set([ + "org.jetbrains.dokka.base.DokkaBase": config + ]) + dokkaSourceSets { + named("main") { + // https://slack-chats.kotlinlang.org/t/484637/hi-all-i-m-trying-to-get-dokka-to-include-documentation-for-#3fae1eb4-6f24-4765-8803-687e6c5819be + sourceRoots.from(file("build/generated/aidl_source_output_dir/release/out")) + suppressGeneratedFiles.set(false) + sourceLink { + // FIXME: source linking just doesn't seem to work + localDirectory.set(rootDir) + remoteUrl.set(new URL("https://github.com/clover/clover-android-sdk/tree/master")) + remoteLineSuffix.set("#L") + } + externalDocumentationLink { + url.set(new URL("https://square.github.io/retrofit/2.x/retrofit/")) + } + externalDocumentationLink { + url.set(new URL("https://square.github.io/okhttp/3.x/okhttp/")) + } + if(project.name == "clover-android-sdk") includes.from("overview.md") + } + } + + dependencies { + dokkaPlugin('org.jetbrains.dokka:kotlin-as-java-plugin:1.9.20') + } + } +} +tasks.dokkaHtmlMultiModule { + moduleName.set("Clover Android SDK") + moduleVersion.set("r"+(project(":clover-android-sdk") as DefaultProject).evaluate().version) + + String config = """ +{ + "customAssets": ["${file("assets/logo-icon.svg")}"], + "customStyleSheets": ["${file("assets/clover.css")}"] +} +""" + pluginsMapConfiguration.set([ + "org.jetbrains.dokka.base.DokkaBase": config + ]) } +dependencies { + dokkaPlugin('org.jetbrains.dokka:kotlin-as-java-plugin:1.9.20') +} \ No newline at end of file diff --git a/clover-android-connector-sdk/build.gradle b/clover-android-connector-sdk/build.gradle index 5cfeb42be..09e98fac7 100644 --- a/clover-android-connector-sdk/build.gradle +++ b/clover-android-connector-sdk/build.gradle @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + /** * Copyright (C) 2016 Clover Network, Inc. * @@ -14,15 +16,45 @@ * limitations under the License. */ group = 'com.clover.sdk' -version = '306' +version = '316' apply from: file("${project.rootDir}/lib.gradle") +apply plugin: 'kotlin-android' +apply plugin: 'org.jetbrains.dokka' + +android { + + buildTypes.configureEach { + consumerProguardFiles 'proguard-rules.pro' + } + + buildFeatures { + aidl true + } + + namespace 'com.clover.android.connector.sdk' + + compileOptions { + sourceCompatibility = 8 + targetCompatibility = 8 + } + tasks.withType(KotlinCompile).configureEach { + kotlinOptions { + jvmTarget = '1.8' + } + } + +} dependencies { implementation project(':clover-android-sdk') implementation "androidx.annotation:annotation:$ANDROIDX_ANNOTATION_VERSION" - implementation "com.google.code.gson:gson:$GSON_VERSION" + implementation ("com.google.code.gson:gson") { + version { + strictly "$GSON_VERSION" + } + } } ext { diff --git a/clover-android-connector-sdk/src/main/AndroidManifest.xml b/clover-android-connector-sdk/src/main/AndroidManifest.xml index 1d22d6bb6..54dd6b7f8 100644 --- a/clover-android-connector-sdk/src/main/AndroidManifest.xml +++ b/clover-android-connector-sdk/src/main/AndroidManifest.xml @@ -16,7 +16,6 @@ --> diff --git a/clover-android-connector-sdk/src/main/java/com/clover/connector/sdk/v3/CardEntryMethods.java b/clover-android-connector-sdk/src/main/java/com/clover/connector/sdk/v3/CardEntryMethods.java index f7ec8f340..f02b7685b 100644 --- a/clover-android-connector-sdk/src/main/java/com/clover/connector/sdk/v3/CardEntryMethods.java +++ b/clover-android-connector-sdk/src/main/java/com/clover/connector/sdk/v3/CardEntryMethods.java @@ -19,7 +19,7 @@ public class CardEntryMethods { private static int KIOSK_CARD_ENTRY_METHODS = 1 << 15; - public static int CARD_ENTRY_METHOD_MAG_STRIPE = 0b0001 | 0b000100000000 | CardEntryMethods.KIOSK_CARD_ENTRY_METHODS; // 33026 + public static int CARD_ENTRY_METHOD_MAG_STRIPE = 0b0001 | 0b000100000000 | CardEntryMethods.KIOSK_CARD_ENTRY_METHODS; // 33025 public static int CARD_ENTRY_METHOD_ICC_CONTACT = 0b0010 | 0b001000000000 | CardEntryMethods.KIOSK_CARD_ENTRY_METHODS; // 33282 public static int CARD_ENTRY_METHOD_NFC_CONTACTLESS = 0b0100 | 0b010000000000 | CardEntryMethods.KIOSK_CARD_ENTRY_METHODS; // 33796 public static int CARD_ENTRY_METHOD_MANUAL = 0b1000 | 0b100000000000 | CardEntryMethods.KIOSK_CARD_ENTRY_METHODS; // 34824 @@ -27,11 +27,11 @@ public class CardEntryMethods { public static int DEFAULT = CardEntryMethods.CARD_ENTRY_METHOD_MAG_STRIPE | CardEntryMethods.CARD_ENTRY_METHOD_ICC_CONTACT | - CardEntryMethods.CARD_ENTRY_METHOD_NFC_CONTACTLESS; // | CARD_ENTRY_METHOD_MANUAL; + CardEntryMethods.CARD_ENTRY_METHOD_NFC_CONTACTLESS; // 34567; public static int ALL = CardEntryMethods.CARD_ENTRY_METHOD_MAG_STRIPE | CardEntryMethods.CARD_ENTRY_METHOD_ICC_CONTACT | CardEntryMethods.CARD_ENTRY_METHOD_NFC_CONTACTLESS | - CardEntryMethods.CARD_ENTRY_METHOD_MANUAL; + CardEntryMethods.CARD_ENTRY_METHOD_MANUAL; // 32768 } \ No newline at end of file diff --git a/clover-android-connector-sdk/src/main/java/com/clover/connector/sdk/v3/session/SessionConnector.java b/clover-android-connector-sdk/src/main/java/com/clover/connector/sdk/v3/session/SessionConnector.java index 3b648bba5..a5049d9b7 100644 --- a/clover-android-connector-sdk/src/main/java/com/clover/connector/sdk/v3/session/SessionConnector.java +++ b/clover-android-connector-sdk/src/main/java/com/clover/connector/sdk/v3/session/SessionConnector.java @@ -1,8 +1,6 @@ package com.clover.connector.sdk.v3.session; -import com.clover.sdk.v1.merchant.Merchant; import com.clover.sdk.v3.customers.CustomerInfo; -import com.clover.sdk.v3.employees.Employee; import com.clover.sdk.v3.order.DisplayOrder; import com.clover.sdk.v3.payments.Transaction; @@ -47,8 +45,6 @@ public class SessionConnector implements Serializable, SessionListener { public static final String QUERY_PARAMETER_VALUE = "value"; public static final String QUERY_PARAMETER_NAME = "name"; public static final String QUERY_PARAMETER_SRC = "src"; - public static final String BUNDLE_KEY_MERCHANT = "Merchant"; - public static final String BUNDLE_KEY_EMPLOYEE = "Employee"; public static final String BUNDLE_KEY_TYPE = "TYPE"; public static final String BUNDLE_KEY_DATA = "DATA"; public static final String BUNDLE_KEY_MESSAGE = "MESSAGE"; @@ -189,48 +185,6 @@ public void setDisplayOrder(DisplayOrder displayOrder, boolean isOrderModificati } } - public Map getProperties() { - Map properties = new HashMap<>(); - try { - if (connect()) { - try (Cursor cursor = sessionContentProviderClient.query(SessionContract.PROPERTIES_URI, null, null, null, null)) { - if (null != cursor && cursor.moveToFirst()) { - do { - String key = cursor.getString(0); - String value = cursor.getString(1); - properties.put(key, value); - } while (cursor.moveToNext()); - } - } - } - } catch (Exception e) { - Log.e(TAG, e.getMessage(), e); - } - return properties; - } - - public Merchant getMerchantInfo() { - try { - Log.d(TAG, "Calling getMerchant"); - Bundle result = sessionContentProviderClient.call(SessionContract.CALL_METHOD_GET_MERCHANT, null, null); - return result == null ? null : (Merchant) result.getParcelable(BUNDLE_KEY_MERCHANT); - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - public Employee getEmployee() { - try { - Log.d(TAG, "Calling getEmployee"); - Bundle result = sessionContentProviderClient.call(SessionContract.CALL_METHOD_GET_EMPLOYEE, null, null); - return result == null ? null : (Employee) result.getParcelable(BUNDLE_KEY_EMPLOYEE); - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - public void setProperty(String key, String value) { try { if (connect()) { diff --git a/clover-android-connector-sdk/src/main/java/com/clover/connector/sdk/v3/session/SessionContract.java b/clover-android-connector-sdk/src/main/java/com/clover/connector/sdk/v3/session/SessionContract.java index c01b7e7a8..8fa7da490 100644 --- a/clover-android-connector-sdk/src/main/java/com/clover/connector/sdk/v3/session/SessionContract.java +++ b/clover-android-connector-sdk/src/main/java/com/clover/connector/sdk/v3/session/SessionContract.java @@ -65,15 +65,10 @@ public class SessionContract { matcher.addURI(SessionContract.AUTHORITY, SessionContract.SESSION_EVENT + "/*", SessionContract.EVENT); } - public static final String CALL_METHOD_ON_EVENT = "onEvent"; public static final String CALL_METHOD_CLEAR_SESSION = "clearSession"; - public static final String CALL_METHOD_GET_MERCHANT = "getMerchant"; - public static final String CALL_METHOD_GET_EMPLOYEE = "getEmployee"; public static final String CALL_METHOD_SET_ORDER = "setOrder"; public static final String CALL_METHOD_SET_CUSTOMER_INFO = "setCustomerInfo"; public static final String CALL_METHOD_SET_PROPERTY = "setProperty"; public static final String CALL_METHOD_SET_TRANSACTION = "setTransaction"; - public static final String CALL_METHOD_ANNOUNCE_CUSTOMER_PROVIDED_DATA = "announceCustomerProvidedData"; - } \ No newline at end of file diff --git a/clover-android-connector-sdk/src/main/res/values-pt-rBR/strings.xml b/clover-android-connector-sdk/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 000000000..c0dabfd31 --- /dev/null +++ b/clover-android-connector-sdk/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,5 @@ + + + Transação manual + Autorização + diff --git a/clover-android-loyalty-kit/build.gradle b/clover-android-loyalty-kit/build.gradle index 9dee6f5c6..e2850c11e 100644 --- a/clover-android-loyalty-kit/build.gradle +++ b/clover-android-loyalty-kit/build.gradle @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + /** * Copyright (C) 2016 Clover Network, Inc. * @@ -14,14 +16,44 @@ * limitations under the License. */ group = 'com.clover.sdk' -version = '306' +version = '316' apply from: file("${project.rootDir}/lib.gradle") +apply plugin: 'kotlin-android' +apply plugin: 'org.jetbrains.dokka' + +android { + + buildTypes.configureEach { + consumerProguardFiles 'proguard-rules.pro' + } + + buildFeatures { + aidl true + } + + namespace "com.clover.android.loyalty" + + compileOptions { + sourceCompatibility = 8 + targetCompatibility = 8 + } + tasks.withType(KotlinCompile).configureEach { + kotlinOptions { + jvmTarget = '1.8' + } + } +} dependencies { implementation project(':clover-android-sdk') implementation "androidx.annotation:annotation:$ANDROIDX_ANNOTATION_VERSION" + implementation ("com.google.code.gson:gson") { + version { + strictly "$GSON_VERSION" + } + } } ext{ diff --git a/clover-android-loyalty-kit/src/main/AndroidManifest.xml b/clover-android-loyalty-kit/src/main/AndroidManifest.xml index f3ff53606..0100f4aa1 100644 --- a/clover-android-loyalty-kit/src/main/AndroidManifest.xml +++ b/clover-android-loyalty-kit/src/main/AndroidManifest.xml @@ -17,7 +17,6 @@ diff --git a/clover-android-loyalty-kit/src/main/java/com/clover/loyalty/activity/helper/CloverCFPLoyaltyHelper.java b/clover-android-loyalty-kit/src/main/java/com/clover/loyalty/activity/helper/CloverCFPLoyaltyHelper.java new file mode 100644 index 000000000..87d3378cd --- /dev/null +++ b/clover-android-loyalty-kit/src/main/java/com/clover/loyalty/activity/helper/CloverCFPLoyaltyHelper.java @@ -0,0 +1,857 @@ +package com.clover.loyalty.activity.helper; + +import static android.app.Activity.RESULT_OK; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.IInterface; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.clover.sdk.cfp.activity.CFPConstants; +import com.clover.sdk.cfp.activity.helper.CloverCFPActivityHelper; +import com.clover.sdk.cfp.activity.helper.CloverCFPCommsHelper; +import com.clover.sdk.cfp.connector.session.CFPSessionConnector; +import com.clover.sdk.cfp.connector.session.CFPSessionListener; +import com.clover.loyalty.ILoyaltyDataService; +import com.clover.loyalty.LoyaltyConnector; +import com.clover.loyalty.LoyaltyDataTypes; +import com.clover.sdk.v1.ServiceConnector; +import com.clover.sdk.v3.customers.CustomerInfo; +import com.clover.sdk.v3.loyalty.LoyaltyDataConfig; +import com.clover.sdk.v3.order.DisplayOrder; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +/** + * Used by an Activity on the customer facing side to allow loyalty and customer information to be passed between + * the customer and merchant facing sides. + *

+ * Allows an integrator to use whatever base class they want to use for their Activity. + * + * @since 4.1.0 + */ +public class CloverCFPLoyaltyHelper { + /** + * Interface implemented on the customer facing side. + *

+ * This extends an interface which adds: Interface implemented on the customer facing side to: + *

    + *
  • Via onMessage(), receive messages from the merchant facing side, or
  • + *
  • Via sendMessage(), send messages back to the merchant facing side
  • + *
+ * + * @since 4.1.0 + */ + public interface LoyaltyListener { + + /** + * Message sent from the merchant facing side. + * + * @param payload - most likely a string JSON payload + * @since 4.0.0 + */ + void onMessage(String payload); + + /** + * Sends a message back to the merchant facing side. + * + * @param payload - most likely a string JSON payload + * @throws Exception - thrown if the action id for messages sent from the Custom Activity is null + * @since 4.0.0 + */ + @SuppressWarnings("unused") + void sendMessage(String payload) throws Exception; + + /** + * When the implementing class connects, it will receive the current list of LoyaltyDataConfigs and current + * customerInfo. + * + * @param configs - the list of loyalty data configs + * @param customerInfo - customer info + * @since 4.1.0 + */ + void onLoyaltyDataLoaded(List configs, CustomerInfo customerInfo); + + /** + * Notified when the state of a loyalty service changes. + * + * @param type - something like: "EMAIL", "PHONE", "CLEAR" + * @param state - a string that describes whether the service is "up" or "down". + * @since 4.1.0 + */ + void onLoyaltyServiceStateChanged(String type, String state); + } + + public static final String LOG_TAG = CloverCFPLoyaltyHelper.class.getSimpleName(); + + private final Gson gson = new GsonBuilder().serializeNulls().create(); + + protected WeakReference activityWeakReference; + private CFPSessionConnector sessionConnector; + private LoyaltyConnector loyaltyConnector; + + // Private session listener that forwards session events to the "externalSessionListener" - the session listener + // passed in on the constructor for this class. + private CFPSessionListener internalSessionListener; + + // We use this flag to determine if dispose() has been called. Once dispose() is called, there's no point to handling + // "in-flight" events. + private boolean isStopping = false; + + private List loyaltyDataConfigList = new ArrayList<>(); + + Map typeToConfig = new HashMap<>(); + + private CustomerInfo customerInfo; + private DisplayOrder displayOrder; + + private WeakReference weakReferenceLoyaltyListener; + private final WeakReference weakReferenceSessionListener; + private final Map loyaltyServiceStateChangeReceiver = new HashMap<>(); + private final CloverCFPActivityHelper cloverCFPActivityHelper; + private final CloverCFPCommsHelper cloverCFPCommsHelper; + + Executor executor = Executors.newSingleThreadExecutor(); + + /** + * Main constructor. + *

+ * Example usage: + * + *

+   *   public class CloverLoyaltyCustomActivity extends Activity
+   *                                            implements SessionListener, CloverCFPLoyaltyHelper.LoyaltyListener {
+   *       private CloverCFPLoyaltyHelper cloverCFPLoyaltyHelper;
+   *       ...
+   *       protected void onCreate(Bundle savedInstanceState) {
+   *           ...
+   *           cloverCFPLoyaltyHelper = new CloverCFPLoyaltyHelper(this, this, this);
+   *           ...
+   *       }
+   *       ...
+   *   }
+   * 
+ * + * @param activity - a reference to the activity that is using this helper class. + * @param loyaltyListener - a client that receives the onLoyaltyDataLoaded(), onLoyaltyServiceStateChanged(), onMessage() + * events, and sends an event via sendMessage(). + * @param externalSessionListener - a client that will receive onSessionDataChanged and onSessionEvent events. + * @since 4.1.0 + */ + public CloverCFPLoyaltyHelper(final Activity activity, final LoyaltyListener loyaltyListener, final CFPSessionListener externalSessionListener) { + activityWeakReference = new WeakReference<>(activity); + weakReferenceLoyaltyListener = new WeakReference<>(loyaltyListener); + weakReferenceSessionListener = new WeakReference<>(externalSessionListener); + + sessionConnector = new CFPSessionConnector(activity); + this.internalSessionListener = new CFPSessionListener() { + /** + * Data can be any type of string such as: + *
+       *   {
+       *   "customer": {
+       *     "firstName": "John Smith",
+       *     "phoneNumbers": {
+       *       "elements": [
+       *         {
+       *           "phoneNumber": "(855) 853-8340"
+       *         }
+       *       ]
+       *     },
+       *     "id": "10"
+       *   },
+       *   "externalId": "44XBAPX69H",
+       *   "extras": {
+       *     "POINTS": "42",
+       *     "OFFERS": "[{\"cost\":10,\"description\":\"\",\"id\":\"R1911\",\"item\":{\"id\":\"R1911\",\"name\":\"Small Drink\",\"price\":0,\"taxable\":true,\"tippable\":true},\"label\":\"Free small drink\"},{\"cost\":50,\"description\":\"\",\"discount\":{\"_amountOff\":500,\"_percentageOff\":0.0,\"name\":\"$5 Off\"},\"id\":\"2J411\",\"label\":\"$5 off\"},{\"cost\":40,\"description\":\"\",\"discount\":{\"_amountOff\":0,\"_percentageOff\":0.1,\"name\":\"10% Off\"},\"id\":\"P4610\",\"label\":\"10% Off\"}]"
+       *   },
+       *   "displayString": "Welcome back John Smith"
+       * }
+       * 
+ * @param type - any type of string (ex. "com.clover.extra.CUSTOMER_INFO") + * @param data - most likely a JSON serialized string (See above). + */ + @Override + public void onSessionDataChanged(String type, Object data) { + // "externalSessionListener" is the session listener passed in on the constructor for this class. + CFPSessionListener externalSessionListener = weakReferenceSessionListener.get(); + if (externalSessionListener != null) { + externalSessionListener.onSessionDataChanged(type, data); + } + } + + @Override + public void onSessionEvent(String type, String data) { + // "externalSessionListener" is the session listener passed in on the constructor for this class. + CFPSessionListener externalSessionListener = weakReferenceSessionListener.get(); + if (externalSessionListener != null) { + externalSessionListener.onSessionEvent(type, data); + } + } + }; + sessionConnector.addSessionListener(internalSessionListener); + cloverCFPActivityHelper = new CloverCFPActivityHelper(activity); + cloverCFPCommsHelper = new CloverCFPCommsHelper(activity, activity.getIntent(), new CloverCFPCommsHelper.MessageListener() { + /** + * Message sent from the merchant facing side. This anonymous inner class is forwarding the method request to + * a private method on the CloverCFPLoyaltyHelper class which then forwards it to a listener class. + * + * @param payload - most likely a string JSON payload + */ + @Override + public void onMessage(String payload) { + CloverCFPLoyaltyHelper.this.onMessage(payload); + } + }); + + // should also be able to get these from onLoyaltyDataLoaded call back + Intent intent = activity.getIntent(); + setCustomerInfo(intent.getParcelableExtra(CFPConstants.CUSTOMER_INFO_EXTRA)); + displayOrder = intent.getParcelableExtra(CFPConstants.DISPLAY_ORDER_EXTRA); + + initializeLoyaltyConnector(); + } + + /** + * Use this method to know if dispose() has been called on this class. + * + * @return true, if dispose() has been called. + * @since 4.1.0 + */ + @SuppressWarnings("unused") + public boolean isStopping() { + return isStopping; + } + + /** + * @since 4.1.0 + */ + @Nullable + @SuppressWarnings("unused") + public CustomerInfo getCustomerInfo() { + return customerInfo; + } + + /** + * @since 4.1.0 + */ + public void setCustomerInfo(@Nullable CustomerInfo customerInfo) { + this.customerInfo = customerInfo; + } + + /** + * @since 4.1.0 + */ + @SuppressWarnings("unused") + public DisplayOrder getDisplayOrder() { + return displayOrder; + } + + /** + * @since 4.1.0 + */ + @SuppressWarnings("unused") + public void setDisplayOrder(DisplayOrder displayOrder) { + this.displayOrder = displayOrder; + } + + /** + * @since 4.1.0 + */ + @SuppressWarnings("unused") + public String getOrderTotal() { + return hasOrder() ? displayOrder.getTotal() : ""; + } + + /** + * @since 4.1.0 + */ + public boolean hasOrder() { + return (displayOrder != null); + } + + private void initializeLoyaltyConnector() { + if (loyaltyConnector == null) { + Activity activity = getActivity(); + if (activity == null) { + Log.w(LOG_TAG, "Context is null!"); + return; + } + + ServiceConnector.OnServiceConnectedListener client = new ServiceConnector.OnServiceConnectedListener() { + @Override + public void onServiceConnected(ServiceConnector serviceConnector) { + Log.d(LOG_TAG, "Connected!"); + executor.execute(() -> { + try { + // Get desired LoyaltyDataConfigs and pass them to the onLoyaltyDataLoaded + final List desiredDataConfig = loyaltyConnector.getDesiredDataConfig(); + + new Handler(activity.getMainLooper()).post(() -> onLoyaltyDataLoaded(desiredDataConfig, sessionConnector.getCustomerInfo())); + } catch (Exception e) { + Log.e(LOG_TAG, "Error getting desired configs", e); + } + }); + } + + @Override + public void onServiceDisconnected(ServiceConnector serviceConnector) { + Log.d(LOG_TAG, "Disconnected!"); + } + }; + loyaltyConnector = new LoyaltyConnector(activity, null, client) { + @Override + public void onBindingDied(ComponentName name) { + Log.d(LOG_TAG, name + " onBindingDied"); + } + }; + if (!loyaltyConnector.connect()) { + Log.d(LOG_TAG, "LoyaltyConnector failed to connect."); + loyaltyConnector = null; + } + } + } + + @SuppressWarnings("unused") + public void startLoyaltyServices() { + if (isStopping) { // onDestroy will set this flag to true. + return; + } + } + + /** + * Mirrors the Activity Lifecycle method (@see Activity#onStart()) + * Consumers of this class should call this method in their activity's "onStart()" method: + *
+   *   public class CloverLoyaltyCustomActivity extends Activity {
+   *       private CloverCFPLoyaltyHelper cloverCFPLoyaltyHelper;
+   *       ...
+   *       protected void onStart() {
+   *           super.onStart();
+   *           cloverCFPLoyaltyHelper.onStart();
+   *       }
+   *       ...
+   *   }
+   * 
+ * + * @since 4.1.0 + */ + @SuppressWarnings("unused") + public void onStart() { + Log.d(LOG_TAG, "Activity lifecycle being started"); + } + + /** + * Mirrors the Activity Lifecycle method (@see Activity#onStop()) + * Consumers of this class should call this method in their activity's "onStop()" method: + *
+   *   public class CloverLoyaltyCustomActivity extends Activity {
+   *       private CloverCFPLoyaltyHelper cloverCFPLoyaltyHelper;
+   *       ...
+   *       protected void onStop() {
+   *           super.onStop();
+   *           cloverCFPLoyaltyHelper.onStop();
+   *       }
+   *       ...
+   *   }
+   * 
+ * + * @since 4.1.0 + */ + @SuppressWarnings("unused") + public void onStop() { + Log.d(LOG_TAG, "Activity lifecycle being stopped"); + } + + /** + * Given a config type, returns the corresponding LoyaltyDataConfig. + *

+ * Type will resemble something like: "EMAIL", "PHONE", "CLEAR" + * + * @param type - a string representing the LoyaltyDataConfig key + * @return null, if none exists + * @see LoyaltyDataTypes + * @since 4.1.0 + */ + @Nullable + @SuppressWarnings("unused") + public LoyaltyDataConfig getLoyaltyDataConfig(@Nullable String type) { + if (typeToConfig == null || type == null) { + return null; + } + + return typeToConfig.get(type); + } + + /** + * This method is called by the ServiceConnector.OnServiceConnectedListener#onServiceConnected when it receives + * a connection notification. + *

+ * Consumers of this class who want to receive the onLoyaltyDataLoaded() event will want to implement the + * CloverCFPLoyaltyHelper.LoyaltyListener interface: + *

+   *   public class CloverLoyaltyCustomActivity extends Activity implements CloverCFPLoyaltyHelper.LoyaltyListener {
+   *       private CloverCFPLoyaltyHelper cloverCFPLoyaltyHelper;
+   *       ...
+   *       public void onLoyaltyDataLoaded(List loyaltyDataConfigList, CustomerInfo customerInfo) {
+   *           // Handle it here...
+   *           // Use it to update various UI widgets...maybe like the Customer Panel, etc.
+   *       }
+   *       ...
+   *   }
+   * 
+ * + * @param configs - from the LoyaltyConnector - a list of the desired loyalty data configs + * @param customerInfo - from the SessionConnector + * @since 4.1.0 + */ + final protected void onLoyaltyDataLoaded(List configs, CustomerInfo customerInfo) { + Log.d(LOG_TAG, "onLoyaltyDataLoaded " + loyaltyDataConfigList); + + for (LoyaltyDataConfig config : configs) { + typeToConfig.put(config.getType(), config); + } + + loyaltyDataConfigList = configs; + // Here is where we give integrators the ability to also handle the onLoyaltyDataLoaded event too. + LoyaltyListener loyaltyListener = weakReferenceLoyaltyListener.get(); + if (loyaltyListener != null) { + loyaltyListener.onLoyaltyDataLoaded(configs, customerInfo); + } + } + + /** + * Message sent from the merchant facing side. This method just forwards the request to the LoyaltyListener class. + *

+ * It is called by the CloverCFPCommsHelper#onMessage() method from within the CloverCFPLoyaltyHelper's constructor. + *

+ * Consumers of this class who want to receive the onLoyaltyDataLoaded() event will want to implement the + * ICloverCFP interface: + *

+   *   public class CloverLoyaltyCustomActivity extends Activity implements CloverCFPLoyaltyHelper.LoyaltyListener {
+   *       private CloverCFPLoyaltyHelper cloverCFPLoyaltyHelper;
+   *       ...
+   *       //Use this method to receive a message from the merchant side
+   *       public void onMessage(String payload) {
+   *          // Handle it here..
+   *       }
+   *
+   *       //Use this method to send a message back to the merchant side
+   *       public void sendMessage(String payload) throws Exception {
+   *           cloverCFPLoyaltyHelper.sendMessage(payload);
+   *       }
+   *       ...
+   *   }
+   * 
+ * + * @param payload - most likely a string JSON payload + * @since 4.1.0 + */ + private void onMessage(String payload) { + // Here is where we give integrators the ability to handle the onMessage event too. + LoyaltyListener loyaltyListener = weakReferenceLoyaltyListener.get(); + if (loyaltyListener != null) { + loyaltyListener.onMessage(payload); + } + } + + /** + * Send a message back to the POS. Used by Activities that desire to send a message back to the merchant facing + * side. + * + * @param payload - mostly likely a JSON serialized string. + * @throws Exception - thrown when the action id for messages sent from the Custom Activity have not been set + * @see #onMessage(String) javadoc + * @since 4.1.0 + */ + @SuppressWarnings("unused") + public void sendMessage(String payload) throws Exception { + cloverCFPCommsHelper.sendMessage(payload); + } + + /** + * Cleans up the sessionConnector, loyalty connector and + *

+ * Consumers of this class will want to call this method in their Activity's onDestroy method: + * + *

+   *   public class CloverLoyaltyCustomActivity extends Activity {
+   *       private CloverCFPLoyaltyHelper cloverCFPLoyaltyHelper;
+   *       ...
+   *       protected void onDestroy() {
+   *           // Tell the helper to clean up everything
+   *           cloverCFPLoyaltyHelper.dispose();
+   *           super.onDestroy();
+   *       }
+   *       ...
+   *   }
+   * 
+ * + * @since 4.1.0 + */ + public void dispose() { + Log.d(LOG_TAG, "Activity lifecycle dispose() is being called"); + + isStopping = true; + + if (sessionConnector != null) { + boolean result = sessionConnector.removeSessionListener(internalSessionListener); + Log.d(LOG_TAG, String.format("Removing CloverCFPLoyaltyHelper as a session listener: %s", result)); + sessionConnector.disconnect(); + } + + if (loyaltyConnector != null) { + loyaltyConnector.disconnect(); + } + + internalSessionListener = null; + sessionConnector = null; + loyaltyConnector = null; + + Activity activity = getActivity(); + if (activity != null) { + for (BroadcastReceiver receiver : loyaltyServiceStateChangeReceiver.values()) { + activity.unregisterReceiver(receiver); + } + } else { + Log.w(LOG_TAG, "context is null!"); + } + + if (weakReferenceLoyaltyListener != null) { + weakReferenceLoyaltyListener = null; + } + + loyaltyServiceStateChangeReceiver.clear(); + + cloverCFPCommsHelper.dispose(); + cloverCFPActivityHelper.dispose(); + } + + /** + * @since 4.1.0 + */ + protected final Map getLoyaltyServiceStateChangeReceiver() { + return loyaltyServiceStateChangeReceiver; + } + + /** + * @since 4.1.0 + */ + protected final Activity getActivity() { + return (activityWeakReference != null) ? activityWeakReference.get() : null; + } + + /** + * @since 4.1.0 + */ + protected final LoyaltyConnector getLoyaltyConnector() { + return loyaltyConnector; + } + + /** + * Finish the activity setting RESULT_OK with a null payload. + *

+ * Here's an example of using this method: + * + *

+   *   public class CloverLoyaltyCustomActivity extends Activity {
+   *       private CloverCFPLoyaltyHelper cloverCFPLoyaltyHelper;
+   *       ...
+   *       public void onMessage(String payload) {
+   *           // this is a custom message that will finish the activity if a "finish" message is sent
+   *           // by the pos
+   *           if ("finish".equals(payload)) {
+   *               cloverCFPLoyaltyHelper.finishActivity();
+   *           } else {
+   *               ...
+   *           }
+   *       }
+   *       ...
+   *   }
+   * 
+ * + * @since 4.1.0 + */ + @SuppressWarnings("unused") + public final void finishActivity() { + cloverCFPActivityHelper.setResultAndFinish(RESULT_OK, null); + } + + /** + * @param type the LoyaltyServiceType to start + * @since 4.1.0 + * @deprecated - use com.clover.cfp.activity.CloverCFPLoyaltyHelper#start(java.lang.String, java.util.Map, java.lang.String) + * with start(type, null, null) for all services + * + */ + public void start(final String type) { + String config = null; + start(type, null, config); + } + + + /** + * use to start a loyalty data service. + * + * @param type - will look like: "EMAIL", "PHONE", "CLEAR" + * @param dataExtrasIn - + * @param config - Usually a JSON serialized string + * @since 4.1.0 + */ + public void start(final String type, @Nullable final Map dataExtrasIn, final String config) { + callConnector(new StartRunnable(this, type, dataExtrasIn, config)); + } + + private static class StartRunnable implements Runnable { + private final WeakReference cloverCFPLoyaltyHelperWeakReference; + private final String type; + private final Map dataExtrasIn; + private final String config; + + StartRunnable(CloverCFPLoyaltyHelper cloverCFPLoyaltyHelper, final String type, final Map dataExtrasIn, final String config) { + this.cloverCFPLoyaltyHelperWeakReference = new WeakReference<>(cloverCFPLoyaltyHelper); + this.type = type; + this.dataExtrasIn = dataExtrasIn; + this.config = config; + } + + @Override + public void run() { + try { + Log.d(LOG_TAG, String.format("Calling connector.start(%s)", type)); + + CloverCFPLoyaltyHelper cloverCFPLoyaltyHelper = cloverCFPLoyaltyHelperWeakReference.get(); + if (cloverCFPLoyaltyHelper == null) { + Log.d(LOG_TAG, "Unable to start b/c CloverCFPLoyaltyHelper was null!"); + return; + } + + String key = ILoyaltyDataService.Util.getServiceStateEventAction(type); + if (cloverCFPLoyaltyHelper.getLoyaltyServiceStateChangeReceiver().get(key) == null) { + BroadcastReceiver statusReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String state = intent.getStringExtra(ILoyaltyDataService.LOYALTY_SERVICE_STATE_EVENT); + cloverCFPLoyaltyHelper.onLoyaltyServiceStateChanged(type, state); + } + }; + + Context context = cloverCFPLoyaltyHelper.getActivity(); + if (context != null) { + context.registerReceiver(statusReceiver, new IntentFilter(key)); + } + cloverCFPLoyaltyHelper.getLoyaltyServiceStateChangeReceiver().put(key, statusReceiver); + } + Map dataExtras = cloverCFPLoyaltyHelper.addToLoyaltyServiceExtras(dataExtrasIn); + dataExtras.put( + "com.clover.payment.executor.secure.EXTRA_PROXY_PROVIDER", // todo: This constant needs to be in ONE place + "com.clover.payment.builder.pay"); + + LoyaltyConnector loyaltyConnector = cloverCFPLoyaltyHelper.getLoyaltyConnector(); + if (loyaltyConnector != null) { + loyaltyConnector.startLoyaltyService(type, dataExtras, config); + } else { + Log.w(LOG_TAG, "Unable to start Loyalty Service because it is null!"); + } + } catch (Exception e) { + Log.e(LOG_TAG, String.format("Error when starting service of type %s", type), e); + } + } + } + + /** + * Override this to get updates to loyalty data service. + * + * @param type - something like: "EMAIL", "PHONE", "CLEAR" + * @param state - a string that looks like: "com.clover.loyalty.service.state.running" or "com.clover.loyalty.service.state.running" + * @since 4.1.0 + */ + final protected void onLoyaltyServiceStateChanged(String type, String state) { + LoyaltyListener loyaltyListener = weakReferenceLoyaltyListener.get(); + if (loyaltyListener != null) { + loyaltyListener.onLoyaltyServiceStateChanged(type, state); + } + } + + /** + * Used to stop a loyalty data service + * + * @param type - + * @since 4.1.0 + */ + public void stop(final String type) { + final Runnable runLater = new StopRunnable(loyaltyConnector, type); + callConnector(runLater); + } + + private static class StopRunnable implements Runnable { + private final WeakReference loyaltyConnectorWeakReference; + private final String type; + + StopRunnable(LoyaltyConnector loyaltyConnector, String type) { + this.loyaltyConnectorWeakReference = new WeakReference<>(loyaltyConnector); + this.type = type; + } + + @Override + public void run() { + try { + LoyaltyConnector loyaltyConnector = loyaltyConnectorWeakReference.get(); + if (loyaltyConnector == null) { + Log.w(LOG_TAG, "Unable to stop the Loyalty Service because it is null!"); + return; + } + Log.d(LOG_TAG, String.format("Calling connector.stop(%s)", type)); + loyaltyConnector.stopLoyaltyService(type); + } catch (Exception e) { + Log.e(LOG_TAG, "Ow!", e); + } + } + } + + /** + * Used by custom activities to announce loyalty data, collected by the custom activity + * and put it in the loyalty platform. + * + * @param loyaltyDataConfig - the corresponding data config for the given data + * @param data - mostly likely a JSON serialized string. + * @since 4.1.0 + */ + public void announceCustomerProvidedData(final LoyaltyDataConfig loyaltyDataConfig, final String data) { + executor.execute(() -> callConnector(new AnnounceDataRunnable(loyaltyConnector, loyaltyDataConfig, data))); + } + + private static class AnnounceDataRunnable implements Runnable { + private final LoyaltyConnector loyaltyConnector; + private final LoyaltyDataConfig loyaltyDataConfig; + private final String data; + + AnnounceDataRunnable(LoyaltyConnector loyaltyConnector, LoyaltyDataConfig loyaltyDataConfig, String data) { + this.loyaltyConnector = loyaltyConnector; + this.loyaltyDataConfig = loyaltyDataConfig; + this.data = data; + } + + @Override + public void run() { + try { + Log.d(LOG_TAG, "Calling connector.announceCustomerProvidedData"); + loyaltyConnector.announceCustomerProvidedData(loyaltyDataConfig, data); + } catch (Exception e) { + Log.e(LOG_TAG, "Ow!", e); + } + } + } + + /** + * Add information to the map of extra information in the VasSettings + * + * @param map - the possibly null map of extras + * @return the non null map of extra data + *

+ * see com.clover.remote.terminal.kiosk.RemoteTerminalKioskActivity#addToVasExtras(java.util.Map, com.clover.sdk.v3.customers.CustomerInfo) + */ + private Map addToLoyaltyServiceExtras(Map map) { + DisplayOrder displayOrder = sessionConnector.getDisplayOrder(); + return + ILoyaltyDataService.Util.addToLoyaltyServiceExtras(map, sessionConnector.getCustomerInfo(), displayOrder != null ? displayOrder.getId() : null); + } + + private void callConnector(final Runnable runLater) { + if (loyaltyConnector == null) { + Context context = activityWeakReference.get(); + + if (context == null) { + return; + } + + ServiceConnector.OnServiceConnectedListener client = new ServiceConnector.OnServiceConnectedListener() { + Runnable delayedRun = runLater; + + @Override + public void onServiceConnected(ServiceConnector connector) { + Runnable tempDelayedRun = delayedRun; + if (tempDelayedRun != null) { + delayedRun = null; + // We could thread it, but we are already in a thread...? + Log.d(LOG_TAG, "Calling delayedRun.run!"); + executor.execute(tempDelayedRun); + } + Log.d(LOG_TAG, "Connected!"); + } + + @Override + public void onServiceDisconnected(ServiceConnector connector) { + Log.d(LOG_TAG, "Disconnected!"); + } + }; + loyaltyConnector = new LoyaltyConnector(context, null, client) { + @Override + public void onBindingDied(ComponentName name) { + Log.d(LOG_TAG, name + " onBindingDied"); + } + }; + if (!loyaltyConnector.connect()) { + Log.d(LOG_TAG, "Connect failed!!!!"); + loyaltyConnector = null; + } + } else { + // LoyaltyConnector is already initialized, just call the runnable. + executor.execute(runLater); + } + } + + /** + * used to force stop a loyalty data service + * + * @param type - the type of loyalty + * @param forceStop - true to force stop. + * @since 4.1.0 + */ + public void stop(final String type, boolean forceStop) { + callConnector(new ForceStopRunnable(loyaltyConnector, type, forceStop)); + } + + private static class ForceStopRunnable implements Runnable { + + private final LoyaltyConnector loyaltyConnector; + private final String type; + private final boolean forceStop; + + public ForceStopRunnable(LoyaltyConnector connector, String type, boolean forceStop) { + this.loyaltyConnector = connector; + this.type = type; + this.forceStop = forceStop; + } + + @Override + public void run() { + try { + Log.d(LOG_TAG, String.format("Calling connector.stop(%s)", type)); + //For now, we will send over if we are forcing stop or not. We can fill this config map with + //whatever we need later. + Map config = new HashMap<>(); + config.put(ILoyaltyDataService.LOYALTY_SERVICE_STATE_EVENT_NOT_RUNNING_FORCE, String.valueOf(forceStop)); + loyaltyConnector.stopLoyaltyService(type, config); + } catch (Exception e) { + Log.e(LOG_TAG, "Ow!", e); + } + } + } +} + diff --git a/clover-android-sdk-examples/build.gradle b/clover-android-sdk-examples/build.gradle index cd2094ccf..36ad73b67 100644 --- a/clover-android-sdk-examples/build.gradle +++ b/clover-android-sdk-examples/build.gradle @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + /** * Copyright (C) 2016 Clover Network, Inc. * @@ -14,10 +16,11 @@ * limitations under the License. */ group = 'com.clover.sdk' -version = '306' +version = '316' apply from: file("${project.rootDir}/app.gradle") apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' repositories { mavenLocal() @@ -27,7 +30,7 @@ repositories { android { buildTypes { all { - minifyEnabled true +// minifyEnabled true proguardFiles getDefaultProguardFile( 'proguard-android-optimize.txt'), 'proguard-rules.pro' @@ -41,6 +44,7 @@ android { compileSdkVersion COMPILE_SDK_VERSION defaultConfig { + multiDexEnabled true minSdkVersion MIN_SDK_VERSION targetSdkVersion TARGET_SDK_VERSION } @@ -50,9 +54,27 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility = 8 + targetCompatibility = 8 + } + tasks.withType(KotlinCompile).configureEach { + kotlinOptions { + jvmTarget = '1.8' + } } + + + buildTypes.configureEach { + consumerProguardFiles 'proguard-rules.pro' + } + + buildFeatures { + aidl true + } + + + namespace "com.clover.android.sdk.examples" + } dependencies { @@ -63,7 +85,14 @@ dependencies { implementation "androidx.annotation:annotation:$ANDROIDX_ANNOTATION_VERSION" implementation "androidx.recyclerview:recyclerview:$ANDROIDX_RECYCLERVIEW_VERSION" implementation "androidx.cardview:cardview:$ANDROIDX_CARDVIEW_VERSION" - implementation "androidx.core:core-ktx:1.3.2" - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1" + implementation "androidx.core:core-ktx:$ANDROIDX_CORE_VERSION" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$ANDROIDX_LIFECYCLE_EXTENSION_VERSION" implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$KOTLIN_COROUTINES_VERSION") + + def room_version = "2.6.1" + implementation("androidx.room:room-common:$room_version") + implementation("androidx.room:room-runtime:$room_version") + annotationProcessor("androidx.room:room-compiler:$room_version") + // To use Kotlin annotation processing tool (kapt) + kapt("androidx.room:room-compiler:$room_version") } diff --git a/clover-android-sdk-examples/src/main/AndroidManifest.xml b/clover-android-sdk-examples/src/main/AndroidManifest.xml index 24e3b618b..ca208a940 100644 --- a/clover-android-sdk-examples/src/main/AndroidManifest.xml +++ b/clover-android-sdk-examples/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ @@ -201,5 +200,18 @@ xmlns:tools="http://schemas.android.com/tools" android:name="com.clover.android.sdk.examples.ReceiptRegistrationTestProvider" android:authorities="com.clover.android.sdk.examples.receipt" android:exported="true" /> + + + + + diff --git a/clover-android-sdk-examples/src/main/java/com/clover/android/sdk/examples/CustomReceiptProviderTest.kt b/clover-android-sdk-examples/src/main/java/com/clover/android/sdk/examples/CustomReceiptProviderTest.kt new file mode 100644 index 000000000..314bda668 --- /dev/null +++ b/clover-android-sdk-examples/src/main/java/com/clover/android/sdk/examples/CustomReceiptProviderTest.kt @@ -0,0 +1,368 @@ +package com.clover.android.sdk.examples + +import android.accounts.Account +import android.content.ContentProvider +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context.MODE_PRIVATE +import android.database.Cursor +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.os.Bundle +import android.os.IInterface +import android.os.ParcelFileDescriptor +import android.os.ParcelFileDescriptor.AutoCloseOutputStream +import android.provider.BaseColumns +import android.util.Log +import androidx.room.ColumnInfo +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.Room +import androidx.room.RoomDatabase +import com.clover.sdk.util.CloverAccount +import com.clover.sdk.v1.ServiceConnector +import com.clover.sdk.v1.ServiceConnector.OnServiceConnectedListener +import com.clover.sdk.v1.printer.Printer +import com.clover.sdk.v1.printer.PrinterConnector +import com.clover.sdk.v1.printer.ReceiptContentContract +import com.clover.sdk.v1.printer.job.BalanceInquiryPrintJob +import com.clover.sdk.v1.printer.job.GiftCardPrintJob +import com.clover.sdk.v1.printer.job.PrintJob +import com.clover.sdk.v1.printer.job.StaticBillPrintJob +import com.clover.sdk.v1.printer.job.StaticCreditPrintJob +import com.clover.sdk.v1.printer.job.StaticGiftReceiptPrintJob +import com.clover.sdk.v1.printer.job.StaticLabelPrintJob +import com.clover.sdk.v1.printer.job.StaticOrderPrintJob +import com.clover.sdk.v1.printer.job.StaticPaymentPrintJob +import com.clover.sdk.v1.printer.job.StaticRefundPrintJob +import com.clover.sdk.v1.printer.job.TextPrintJob +import com.clover.sdk.v1.printer.job.TokenRequestBasedPrintJob +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.ByteArrayOutputStream +import java.io.FileNotFoundException +import java.io.IOException + +class CustomReceiptProviderTest : ContentProvider(), OnServiceConnectedListener, CoroutineScope by MainScope() { + + private var printer: Printer? = null + private var printerConnector: PrinterConnector? = null + private var account: Account? = null + private var supportedReceiptWidth: Int? = null + private var selectedFileResId = R.drawable.test_receipt_extra_large + + companion object { + const val AUTHORITY = "com.clover.android.sdk.examples.receipt.custom" + const val CONTENT_URI = "content://$AUTHORITY/" + + /** The name of the ID column. */ + const val COLUMN_ID = BaseColumns._ID + + /** The name of image bitmap column. */ + const val COLUMN_NAME = "imageBitmap" + + const val DATABASE_NAME = "receipt_data" + const val TABLE_NAME = "receipt_bitmaps" + const val SEGMENT_URI = "segment_uri" + lateinit var database: AppDatabase + + const val SHARED_PREFS = "customReceiptProviderPrefs" + const val N_CHUNKS = "nChunks" + const val SELECTED_FILE_RES_ID = "selectedFileResId" + const val MAX_RECEIPT_HEIGHT = 2048 + const val TAG = "CustomReceiptProviderTest" + } + + @Entity(tableName = TABLE_NAME) + data class ReceiptUriEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(index = true, name = COLUMN_ID) + val id: Long, + @ColumnInfo(name = COLUMN_NAME) val imageBitmap: ByteArray + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ReceiptUriEntity + + if (id != other.id) return false + if (!imageBitmap.contentEquals(other.imageBitmap)) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + imageBitmap.contentHashCode() + return result + } + } + + @Database(entities = [ReceiptUriEntity::class], version = 1) + abstract class AppDatabase : RoomDatabase() { + abstract fun receiptDao(): ReceiptDao + } + + @Dao + interface ReceiptDao { + @Insert + fun insertBitmap(receiptUri: ReceiptUriEntity): Long + + @Query("SELECT * FROM $TABLE_NAME WHERE $COLUMN_ID = :id") + fun selectById(id: Long): Cursor? + } + + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? { + val cursor: Cursor? = database.receiptDao().selectById(ContentUris.parseId(uri)) + cursor?.setNotificationUri(context?.contentResolver, uri) + return cursor + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + values?.let { + val receiptUriEntity = ReceiptUriEntity(0, values.getAsByteArray(SEGMENT_URI)) + val id = database.receiptDao().insertBitmap(receiptUriEntity) + context?.contentResolver?.notifyChange(uri, null); + return ContentUris.withAppendedId(uri, id); + } ?: run { + return null + } + } + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int { + throw UnsupportedOperationException("Not implemented") + } + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { + throw UnsupportedOperationException("Not implemented") + } + + override fun getType(uri: Uri): String? { + throw UnsupportedOperationException("Not implemented") + } + + override fun onCreate(): Boolean { + account = CloverAccount.getAccount(context) + account?.let { connect() } + + return kotlin.runCatching { + database = context?.let { + Room.databaseBuilder( + it, + AppDatabase::class.java, + DATABASE_NAME + ).build() + }!! + true + }.getOrElse { + Log.e(TAG, "Exception occurred! Couldn't create database!", it) + false + } + } + + @Throws(FileNotFoundException::class) + override fun openFile(contentUri: Uri, mode: String): ParcelFileDescriptor { + val bitmap = getReceiptSegmentBitmap(contentUri) + val rescaledBitmap = if (selectedFileResId == R.drawable.test_receipt_extra_large && bitmap != null && supportedReceiptWidth != null) { + // WARNING: Generate the receipt bitmap with width = supportedReceiptWidth and height up to + // CustomReceiptProviderTest.MAX_RECEIPT_HEIGHT. Instead of generating a receipt bitmap + // matching the supportedReceiptWidth, for testing purpose this app resizes the test bitmap + // resource to supportedReceiptWidth x supportedReceiptWidth + Bitmap.createScaledBitmap(bitmap, supportedReceiptWidth!!, supportedReceiptWidth!!, false) + } else { + getReceiptSegmentBitmap(contentUri) + } + + return openPipeHelper( + contentUri, "*/*", null, rescaledBitmap + ) { output: ParcelFileDescriptor, uri: Uri, mimeType: String?, opts: Bundle?, args: Bitmap? -> + try { + AutoCloseOutputStream(output).use { + rescaledBitmap?.compress(Bitmap.CompressFormat.PNG, 100, it) + } + } catch (e: IOException) { + e.printStackTrace() + } + } + } + + private fun connect() { + disconnect() + if (account != null) { + printerConnector = PrinterConnector(context, account, this) + printerConnector?.connect() + } + } + + private fun disconnect() { + if (printerConnector != null) { + printerConnector?.disconnect() + printerConnector = null + } + } + + private fun getReceiptSegmentBitmap(contentUri: Uri): Bitmap? { + val cursor = query(contentUri, null, null, null, null) + var bitmap: Bitmap? = null + + cursor?.let { + it.moveToFirst() + val bitmapData = it.getBlob(it.getColumnIndex(COLUMN_NAME)) + val opts = BitmapFactory.Options() + opts.inPreferredConfig = Bitmap.Config.RGB_565 + bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.size, opts) + it.close() + } + + return bitmap + } + + override fun call(method: String, arg: String?, extras: Bundle?): Bundle { + val result = Bundle() + val sharedPrefs = context?.getSharedPreferences(SHARED_PREFS, MODE_PRIVATE) + val nChunksToSend = sharedPrefs?.getInt(N_CHUNKS, 3) ?: 3 + selectedFileResId = sharedPrefs?.getInt(SELECTED_FILE_RES_ID, R.drawable.test_receipt_extra_large) + ?: R.drawable.test_receipt_extra_large + + if (method == ReceiptContentContract.METHOD_GET_RECEIPT_CONTENT_URIS) { + extras?.let { + // set PrintJob as classloader + it.classLoader = PrintJob::class.java.classLoader + val printJob: PrintJob? + when (it.getParcelable(ReceiptContentContract.EXTRA_PRINT_JOB)) { + is StaticBillPrintJob -> { + printJob = + (it.getParcelable(ReceiptContentContract.EXTRA_PRINT_JOB)) as StaticBillPrintJob + Log.i(TAG, "StaticBillPrintJob: $printJob") + } + + is StaticCreditPrintJob -> { + printJob = + (it.getParcelable(ReceiptContentContract.EXTRA_PRINT_JOB)) as StaticCreditPrintJob + Log.i(TAG, "StaticCreditPrintJob: $printJob") + } + + is StaticPaymentPrintJob -> { + printJob = + (it.getParcelable(ReceiptContentContract.EXTRA_PRINT_JOB)) as StaticPaymentPrintJob + Log.i(TAG, "isRefundReceipt?: ${(printJob.flags and PrintJob.FLAG_REFUND) == PrintJob.FLAG_REFUND}") + Log.i(TAG, "StaticPaymentPrintJob: $printJob") + } + + is StaticRefundPrintJob -> { + printJob = + (it.getParcelable(ReceiptContentContract.EXTRA_PRINT_JOB)) as StaticRefundPrintJob + Log.i(TAG, "StaticRefundPrintJob: $printJob") + } + + is StaticGiftReceiptPrintJob -> { + printJob = + (it.getParcelable(ReceiptContentContract.EXTRA_PRINT_JOB)) as StaticGiftReceiptPrintJob + Log.i(TAG, "StaticGiftReceiptPrintJob: $printJob") + } + + is TextPrintJob -> { + printJob = + (it.getParcelable(ReceiptContentContract.EXTRA_PRINT_JOB)) as TextPrintJob + Log.i(TAG, "TextPrintJob: $printJob") + } + + is StaticOrderPrintJob -> { + printJob = + (it.getParcelable(ReceiptContentContract.EXTRA_PRINT_JOB)) as StaticOrderPrintJob + Log.i(TAG, "StaticOrderPrintJob: $printJob") + } + + is GiftCardPrintJob -> { + printJob = + (it.getParcelable(ReceiptContentContract.EXTRA_PRINT_JOB)) as GiftCardPrintJob + Log.i(TAG, "GiftCardPrintJob: $printJob") + } + + is BalanceInquiryPrintJob -> { + printJob = + (it.getParcelable(ReceiptContentContract.EXTRA_PRINT_JOB)) as BalanceInquiryPrintJob + Log.i(TAG, "BalanceInquiryPrintJob: $printJob") + } + + is TokenRequestBasedPrintJob -> { + printJob = + (it.getParcelable(ReceiptContentContract.EXTRA_PRINT_JOB)) as TokenRequestBasedPrintJob + Log.i(TAG, "TokenRequestBasedPrintJob: $printJob") + } + + is StaticLabelPrintJob -> { + printJob = + (it.getParcelable(ReceiptContentContract.EXTRA_PRINT_JOB)) as StaticLabelPrintJob + Log.i(TAG, "StaticLabelPrintJob: $printJob") + } + } + + // Parse Printer object + printer = it.getParcelable(ReceiptContentContract.EXTRA_PRINTER) + Log.i(TAG, "Printer: $printer") + if (printer != null) { + launch { + withContext(Dispatchers.IO) { + // Fetch supported width for given printer + supportedReceiptWidth = printerConnector?.getPrinterTypeDetails(printer)?.numDotsWidth + Log.i(TAG, "Printer supportedWidth: $supportedReceiptWidth") + } + } + } + + val bitmapUri = storeInCP(selectedFileResId) + Log.d(TAG, "bitmapUri: $bitmapUri") + + result.putParcelableArrayList( + ReceiptContentContract.EXTRA_RECEIPT_CONTENT_URIS, + ArrayList(List(nChunksToSend){bitmapUri}) + ) + } + } + return result + } + + private fun storeInCP(res: Int): Uri? { + val values = ContentValues() + + val stream = ByteArrayOutputStream() + val b: Bitmap = BitmapFactory.decodeResource(this.context?.resources, res) + b.compress(Bitmap.CompressFormat.PNG, 0, stream) + val blob = stream.toByteArray() + + values.put(SEGMENT_URI, blob) + + return context?.contentResolver?.insert( + Uri.parse("$CONTENT_URI$TABLE_NAME"), values + ) + } + + override fun onServiceConnected(connector: ServiceConnector?) { + Log.i(TAG, "service connected: $connector") + } + + override fun onServiceDisconnected(connector: ServiceConnector?) { + Log.i(TAG, "service disconnected: $connector") + } +} diff --git a/clover-android-sdk-examples/src/main/java/com/clover/android/sdk/examples/CustomReceiptProviderTestActivity.kt b/clover-android-sdk-examples/src/main/java/com/clover/android/sdk/examples/CustomReceiptProviderTestActivity.kt new file mode 100644 index 000000000..a7a0c4d85 --- /dev/null +++ b/clover-android-sdk-examples/src/main/java/com/clover/android/sdk/examples/CustomReceiptProviderTestActivity.kt @@ -0,0 +1,103 @@ +package com.clover.android.sdk.examples + +import android.app.Activity +import android.content.ComponentName +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Button +import android.widget.EditText +import android.widget.Spinner +import android.widget.Toast +import com.clover.android.sdk.examples.CustomReceiptProviderTest.Companion.N_CHUNKS +import com.clover.android.sdk.examples.CustomReceiptProviderTest.Companion.SELECTED_FILE_RES_ID + +class CustomReceiptProviderTestActivity : Activity() { + + lateinit var enableBtn: Button + lateinit var disableBtn: Button + lateinit var save: Button + lateinit var numberOfBitmapChunks: EditText + lateinit var testReceiptSizeSelector: Spinner + lateinit var sharedPrefs: SharedPreferences + var selectedFileResId: Int = R.drawable.test_receipt_extra_large + private val receiptRes = ArrayList( + listOf( + R.drawable.test_receipt_extra_large, + R.drawable.test_receipt_small, + R.drawable.test_receipt_medium, + R.drawable.test_receipt_large + ) + ) + + companion object { + const val TAG = "CustomReceiptProviderTestActivity" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_custom_receipt_provider_test) + + sharedPrefs = getSharedPreferences(CustomReceiptProviderTest.SHARED_PREFS, MODE_PRIVATE) + + enableBtn = findViewById(R.id.enable_provider) + disableBtn = findViewById(R.id.disable_provider) + numberOfBitmapChunks = findViewById(R.id.number_of_bitmap_chunks) + numberOfBitmapChunks.setText(sharedPrefs.getInt(N_CHUNKS, 3).toString()) + testReceiptSizeSelector = findViewById(R.id.test_receipt_size_selector) + save = findViewById(R.id.save_number_of_bitmap_chunks) + + ArrayAdapter.createFromResource( + this, + R.array.test_receipt_sizes_array, + android.R.layout.simple_spinner_item + ).also { adapter -> + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + testReceiptSizeSelector.adapter = adapter + } + + testReceiptSizeSelector.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) { + selectedFileResId = receiptRes[pos] + Log.d(TAG, "Selected file: ${resources.getResourceEntryName(selectedFileResId)}") + } + + override fun onNothingSelected(parent: AdapterView<*>) { + return + } + } + + save.setOnClickListener { + val numberOfBitmapChunks = numberOfBitmapChunks.text.toString() + val nChunks = if (numberOfBitmapChunks.isEmpty()) 3 else numberOfBitmapChunks.toInt() + if (numberOfBitmapChunks.isEmpty() || nChunks < 0 || nChunks > 20) { + Toast.makeText(this, "Entered number of chunks are not between 0 and 20. " + + "Testing with default 3 chunks", Toast.LENGTH_LONG).show() + sharedPrefs.edit().putInt(N_CHUNKS, 3).apply() + } else { + sharedPrefs.edit().putInt(N_CHUNKS, nChunks).apply() + } + sharedPrefs.edit().putInt(SELECTED_FILE_RES_ID, selectedFileResId).apply() + } + + enableBtn.setOnClickListener { + val conProvCN = ComponentName(this, + "com.clover.android.sdk.examples.CustomReceiptProviderTest") + val pm: PackageManager = this.packageManager + pm.setComponentEnabledSetting(conProvCN, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0) + } + + disableBtn.setOnClickListener { + val conProvCN = ComponentName(this, + "com.clover.android.sdk.examples.CustomReceiptProviderTest") + val pm: PackageManager = this.packageManager + pm.setComponentEnabledSetting(conProvCN, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 0) + } + } +} \ No newline at end of file diff --git a/clover-android-sdk-examples/src/main/java/com/clover/android/sdk/examples/InventoryTestActivity.java b/clover-android-sdk-examples/src/main/java/com/clover/android/sdk/examples/InventoryTestActivity.java index fb315ab24..ec1d73e79 100644 --- a/clover-android-sdk-examples/src/main/java/com/clover/android/sdk/examples/InventoryTestActivity.java +++ b/clover-android-sdk-examples/src/main/java/com/clover/android/sdk/examples/InventoryTestActivity.java @@ -814,6 +814,12 @@ public Item getItem(String itemId, ResultStatus resultStatus) throws RemoteExcep return getResult(Item.class, uri, "item", resultStatus); } + @Override + public Item getPosMenuItem(String itemId, String menuId, ResultStatus resultStatus) throws RemoteException { + String uri = "/v2/merchant/" + merchantId + "/inventory/items/" + itemId; + return getResult(Item.class, uri, "item", resultStatus); + } + @Override public Item getItemWithCategories(String itemId, ResultStatus resultStatus) throws RemoteException { throw new UnsupportedOperationException("getItemWithCategories() not supported through web service API"); diff --git a/clover-android-sdk-examples/src/main/java/com/clover/android/sdk/examples/PrintJobsTestActivity.java b/clover-android-sdk-examples/src/main/java/com/clover/android/sdk/examples/PrintJobsTestActivity.java deleted file mode 100644 index 139e225ac..000000000 --- a/clover-android-sdk-examples/src/main/java/com/clover/android/sdk/examples/PrintJobsTestActivity.java +++ /dev/null @@ -1,205 +0,0 @@ -/** - * Copyright (C) 2016 Clover Network, Inc. - * - * 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.clover.android.sdk.examples; - -import android.accounts.Account; -import android.app.Activity; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.text.TextUtils; -import android.util.Pair; -import android.view.View; -import android.widget.Button; -import android.widget.EditText; -import android.widget.TextView; -import com.clover.sdk.util.CloverAccount; -import com.clover.sdk.v1.printer.Category; -import com.clover.sdk.v1.printer.Printer; -import com.clover.sdk.v1.printer.PrinterConnector; -import com.clover.sdk.v1.printer.job.PrintJob; -import com.clover.sdk.v1.printer.job.PrintJobsConnector; -import com.clover.sdk.v1.printer.job.PrintJobsContract; -import com.clover.sdk.v1.printer.job.TestReceiptPrintJob; - -import java.util.Arrays; -import java.util.List; - -public class PrintJobsTestActivity extends Activity { - private static final String TAG = PrintJobsTestActivity.class.getSimpleName(); - - private static final Handler uiHandler = new Handler(Looper.getMainLooper()); - - private Account account; - private PrinterConnector printerConnector; - - private EditText editPrinterId; - private Button buttonPrint; - private TextView textId; - - private Button buttonGetPrintJobIds; - private TextView textPrintJobIds; - - private HandlerThread handlerThread = new HandlerThread(this.getClass().getName()); - private Handler handler; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_printjobs_test); - - handlerThread.start(); - handler = new Handler(handlerThread.getLooper()); - - account = CloverAccount.getAccount(this); - printerConnector = new PrinterConnector(this, account, null); - fillPrinterId(); - - textId = (TextView) findViewById(R.id.text_id); - - editPrinterId = (EditText) findViewById(R.id.edit_printer_id); - buttonPrint = (Button) findViewById(R.id.button_print); - buttonPrint.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - new AsyncTask() { - @Override - protected String doInBackground(Void... voids) { - try { - Printer p = getPrinter(); - PrintJob printJob = new TestReceiptPrintJob.Builder().build(); - return new PrintJobsConnector(PrintJobsTestActivity.this).print(p, printJob); - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - @Override - protected void onPostExecute(String id) { - if (id == null) { - return; - } - textId.setText(id); - monitorState(); - } - }.execute(); - } - }); - - textPrintJobIds = (TextView) findViewById(R.id.text_printjob_ids); - } - - private void monitorState() { - handler.postDelayed(new Runnable() { - @Override - public void run() { - PrintJobsConnector connector = new PrintJobsConnector(PrintJobsTestActivity.this); - - final List inQueueIds = connector.getPrintJobIds(PrintJobsContract.STATE_IN_QUEUE); - final List printingIds = connector.getPrintJobIds(PrintJobsContract.STATE_PRINTING); - final List doneIds = connector.getPrintJobIds(PrintJobsContract.STATE_DONE); - final List errorIds = connector.getPrintJobIds(PrintJobsContract.STATE_ERROR); - - final StringBuilder printJobIds = new StringBuilder(); - - List>> pairs = Arrays.asList( - Pair.create("In queue: ", inQueueIds), - Pair.create("Printing: ", printingIds), - Pair.create("Done: ", doneIds), - Pair.create("Error: ", errorIds) - ); - for (Pair> pair: pairs) { - printJobIds.append(pair.first); - for (String id: pair.second) { - printJobIds.append(id).append(", "); - } - if (!pair.second.isEmpty()) { - printJobIds.delete(printJobIds.length() - 2, printJobIds.length()); - } - printJobIds.append("\n\n"); - } - uiHandler.post(new Runnable() { - @Override - public void run() { - textPrintJobIds.setText(printJobIds.toString()); - } - }); - - monitorState(); - } - }, 100); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - - uiHandler.removeCallbacksAndMessages(null); - handler.removeCallbacksAndMessages(null); - - if (printerConnector != null) { - printerConnector.disconnect(); - printerConnector = null; - } - } - - private void fillPrinterId() { - new AsyncTask>() { - @Override - protected List doInBackground(Void... params) { - try { - return printerConnector.getPrinters(Category.RECEIPT); - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - @Override - protected void onPostExecute(List printers) { - if (printers != null && !printers.isEmpty()) { - editPrinterId.setText(printers.get(0).getUuid()); - } - } - }.execute(); - } - - private Printer getPrinter() { - String id = editPrinterId.getText().toString(); - if (TextUtils.isEmpty(id)) { - try { - List printers = printerConnector.getPrinters(Category.RECEIPT); - if (printers != null && !printers.isEmpty()) { - return printers.get(0); - } - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - try { - return printerConnector.getPrinter(id); - } catch (Exception e) { - e.printStackTrace(); - } - return null; - - } -} diff --git a/clover-android-sdk-examples/src/main/res/drawable/test_receipt_extra_large.png b/clover-android-sdk-examples/src/main/res/drawable/test_receipt_extra_large.png new file mode 100644 index 000000000..78e082b25 Binary files /dev/null and b/clover-android-sdk-examples/src/main/res/drawable/test_receipt_extra_large.png differ diff --git a/clover-android-sdk-examples/src/main/res/drawable/test_receipt_large.jpg b/clover-android-sdk-examples/src/main/res/drawable/test_receipt_large.jpg new file mode 100644 index 000000000..8f90d73a9 Binary files /dev/null and b/clover-android-sdk-examples/src/main/res/drawable/test_receipt_large.jpg differ diff --git a/clover-android-sdk-examples/src/main/res/drawable/test_receipt_medium.jpg b/clover-android-sdk-examples/src/main/res/drawable/test_receipt_medium.jpg new file mode 100644 index 000000000..7c902e803 Binary files /dev/null and b/clover-android-sdk-examples/src/main/res/drawable/test_receipt_medium.jpg differ diff --git a/clover-android-sdk-examples/src/main/res/drawable/test_receipt_small.jpg b/clover-android-sdk-examples/src/main/res/drawable/test_receipt_small.jpg new file mode 100644 index 000000000..270692712 Binary files /dev/null and b/clover-android-sdk-examples/src/main/res/drawable/test_receipt_small.jpg differ diff --git a/clover-android-sdk-examples/src/main/res/layout/activity_custom_receipt_provider_test.xml b/clover-android-sdk-examples/src/main/res/layout/activity_custom_receipt_provider_test.xml new file mode 100644 index 000000000..f1cca769b --- /dev/null +++ b/clover-android-sdk-examples/src/main/res/layout/activity_custom_receipt_provider_test.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + +