diff --git a/card.io/build.gradle b/card.io/build.gradle index 1c0d2184..4e1d1d2c 100644 --- a/card.io/build.gradle +++ b/card.io/build.gradle @@ -17,6 +17,7 @@ android { ndk { moduleName "cardioRecognizer" + abiFilters 'armeabi-v7a', 'arm64-v8a' } consumerProguardFiles 'proguard.cfg' @@ -34,10 +35,18 @@ android { } dependencies { - testCompile "junit:junit:4.12" - testCompile "org.robolectric:robolectric:3.1.2" + testImplementation "junit:junit:4.12" + testImplementation "org.robolectric:robolectric:3.1.2" } +task ndkClean(type: Delete) { + // remove unused archs from build cache + delete fileTree('.externalNativeBuild') { + exclude android.defaultConfig.ndk.abiFilters.collect { '/**' + it } + } +} +tasks.findByPath(':clean').dependsOn ndkClean + task javadocs(type: Javadoc) { source = android.sourceSets.main.java.srcDirs classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) diff --git a/card.io/src/main/java/io/card/payment/CardIOActivity.java b/card.io/src/main/java/io/card/payment/CardIOActivity.java index 961eb522..72aeb778 100644 --- a/card.io/src/main/java/io/card/payment/CardIOActivity.java +++ b/card.io/src/main/java/io/card/payment/CardIOActivity.java @@ -45,6 +45,8 @@ import io.card.payment.i18n.LocalizedStrings; import io.card.payment.i18n.StringKey; +import io.card.payment.interfaces.CardIOCameraControl; +import io.card.payment.interfaces.CardScanRecognition; import io.card.payment.ui.ActivityHelper; import io.card.payment.ui.Appearance; import io.card.payment.ui.ViewUtil; @@ -55,7 +57,7 @@ * * @version 1.0 */ -public final class CardIOActivity extends Activity { +public final class CardIOActivity extends Activity implements CardScanRecognition, CardIOCameraControl { /** * Boolean extra. Optional. Defaults to false. If set, the card will not be scanned * with the camera. @@ -462,7 +464,7 @@ private void showCameraScannerOverlay() { mCardScanner = (CardScanner) cons.newInstance(new Object[] { this, mFrameOrientation }); } else { - mCardScanner = new CardScanner(this, mFrameOrientation); + mCardScanner = new CardScanner(this, mFrameOrientation, false); } mCardScanner.prepareScanner(); @@ -733,11 +735,30 @@ void onFirstFrame() { onEdgeUpdate(new DetectionInfo()); } - void onEdgeUpdate(DetectionInfo dInfo) { + @Override + public void onEdgeUpdate(DetectionInfo dInfo) { mOverlay.setDetectionInfo(dInfo); } - void onCardDetected(Bitmap detectedBitmap, DetectionInfo dInfo) { + @Override + public Activity getActivity() { + return this; + } + + @Override + public void onFirstFrame(int orientation) { + SurfaceView sv = mPreview.getSurfaceView(); + if (mOverlay != null) { + mOverlay.setCameraPreviewRect(new Rect(sv.getLeft(), sv.getTop(), sv.getRight(), sv + .getBottom())); + } + mFrameOrientation = ORIENTATION_PORTRAIT; + setDeviceDegrees(0); + + onEdgeUpdate(new DetectionInfo()); + } + @Override + public void onCardDetected(Bitmap detectedBitmap, DetectionInfo dInfo) { try { Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); vibrator.vibrate(VIBRATE_PATTERN, -1); @@ -873,7 +894,8 @@ private void setDeviceDegrees(int degrees) { } // Called by OverlayView - void toggleFlash() { + @Override + public void toggleFlash() { setFlashOn(!mCardScanner.isFlashOn()); } @@ -883,8 +905,8 @@ void setFlashOn(boolean b) { mOverlay.setTorchOn(b); } } - - void triggerAutoFocus() { + @Override + public void triggerAutoFocus() { mCardScanner.triggerAutoFocus(true); } @@ -908,7 +930,7 @@ private void setPreviewLayout() { LayoutParams.MATCH_PARENT, Gravity.TOP)); previewFrame.addView(mPreview); - mOverlay = new OverlayView(this, null, Util.deviceSupportsTorch(this)); + mOverlay = new OverlayView(this, this, null, Util.deviceSupportsTorch(this)); mOverlay.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); if (getIntent() != null) { diff --git a/card.io/src/main/java/io/card/payment/CardIOConstants.java b/card.io/src/main/java/io/card/payment/CardIOConstants.java new file mode 100644 index 00000000..27abada5 --- /dev/null +++ b/card.io/src/main/java/io/card/payment/CardIOConstants.java @@ -0,0 +1,153 @@ +package io.card.payment; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.Color; + +/** + * Created by glaubermartins on 2018-04-04. + */ + +public class CardIOConstants { + + /** + * Boolean extra. Optional. Defaults to false. If set, the card will not be scanned + * with the camera. + */ + public static final String EXTRA_NO_CAMERA = "io.card.payment.noCamera"; + + /** + * Boolean extra. Optional. Defaults to false. If set, the user will be prompted + * for the cardholder name. + */ + public static final String EXTRA_REQUIRE_CARDHOLDER_NAME = "io.card.payment.requireCardholderName"; + + /** + * Boolean extra. Optional. Defaults to false. If set, the card.io logo will be + * shown instead of the PayPal logo. + */ + public static final String EXTRA_USE_CARDIO_LOGO = "io.card.payment.useCardIOLogo"; + + /** + * Boolean extra. Optional. Defaults to false. Removes the keyboard button from the + * scan screen. + *

+ * If scanning is unavailable, the {@link android.app.Activity} result will be {@link #RESULT_SCAN_NOT_AVAILABLE}. + */ + public static final String EXTRA_SUPPRESS_MANUAL_ENTRY = "io.card.payment.suppressManual"; + + /** + * String extra. Optional. The preferred language for all strings appearing in the user + * interface. If not set, or if set to null, defaults to the device's current language setting. + *

+ * Can be specified as a language code ("en", "fr", "zh-Hans", etc.) or as a locale ("en_AU", + * "fr_FR", "zh-Hant_TW", etc.). + *

+ * If the library does not contain localized strings for a specified locale, then will fall back + * to the language. E.g., "es_CO" -> "es". + *

+ * If the library does not contain localized strings for a specified language, then will fall + * back to American English. + *

+ * If you specify only a language code, and that code matches the device's currently preferred + * language, then the library will attempt to use the device's current region as well. E.g., + * specifying "en" on a device set to "English" and "United Kingdom" will result in "en_GB". + *

+ * These localizations are currently included: + *

+ * ar, da, de, en, en_AU, en_GB, es, es_MX, fr, he, is, it, ja, ko, ms, nb, nl, pl, pt, pt_BR, ru, + * sv, th, tr, zh-Hans, zh-Hant, zh-Hant_TW. + */ + public static final String EXTRA_LANGUAGE_OR_LOCALE = "io.card.payment.languageOrLocale"; + + /** + * Integer extra. Optional. Defaults to {@link Color#GREEN}. Changes the color of the guide overlay on the + * camera. + */ + public static final String EXTRA_GUIDE_COLOR = "io.card.payment.guideColor"; + + /** + * Boolean extra. Optional. Defaults to false. When set to true the card.io logo + * will not be shown overlaid on the camera. + */ + public static final String EXTRA_HIDE_CARDIO_LOGO = "io.card.payment.hideLogo"; + + /** + * String extra. Optional. Used to display instructions to the user while they are scanning + * their card. + */ + public static final String EXTRA_SCAN_INSTRUCTIONS = "io.card.payment.scanInstructions"; + + /** + * Boolean extra. Optional. Once a card image has been captured but before it has been + * processed, this value will determine whether to continue processing as usual. If the value is + * true the {@link CardIOActivity} will finish with a {@link #RESULT_SCAN_SUPPRESSED} result code. + */ + public static final String EXTRA_SUPPRESS_SCAN = "io.card.payment.suppressScan"; + + /** + * Boolean extra. Optional. If this value is set to true, and the application has a theme, + * the theme for the card.io {@link android.app.Activity}s will be set to the theme of the application. + */ + public static final String EXTRA_KEEP_APPLICATION_THEME = "io.card.payment.keepApplicationTheme"; + + + /** + * Boolean extra. Used for testing only. + */ + public static final String PRIVATE_EXTRA_CAMERA_BYPASS_TEST_MODE = "io.card.payment.cameraBypassTestMode"; + + public static int lastResult = 0xca8d10; // arbitrary. chosen to be well above + // Activity.RESULT_FIRST_USER. + /** + * result code supplied to {@link Activity#onActivityResult(int, int, Intent)} when a scan request completes. + */ + public static final int RESULT_CARD_INFO = lastResult++; + + /** + * result code supplied to {@link Activity#onActivityResult(int, int, Intent)} when the user presses the cancel + * button. + */ + public static final int RESULT_ENTRY_CANCELED = lastResult++; + + /** + * result code indicating that scan is not available. Only returned when + * {@link #EXTRA_SUPPRESS_MANUAL_ENTRY} is set and scanning is not available. + *

+ * This error can be avoided in normal situations by checking + * {@link CardIOFragment#canReadCardWithCamera()}. + */ + public static final int RESULT_SCAN_NOT_AVAILABLE = lastResult++; + + /** + * result code indicating that we only captured the card image. + */ + public static final int RESULT_SCAN_SUPPRESSED = lastResult++; + + public static final int DEGREE_DELTA = 15; + + public static final int ORIENTATION_PORTRAIT = 1; + public static final int ORIENTATION_PORTRAIT_UPSIDE_DOWN = 2; + public static final int ORIENTATION_LANDSCAPE_RIGHT = 3; + public static final int ORIENTATION_LANDSCAPE_LEFT = 4; + + public static final String BUNDLE_WAITING_FOR_PERMISSION = "io.card.payment.waitingForPermission"; + + public static final long[] VIBRATE_PATTERN = { 0, 70, 10, 40 }; + + public static final int TOAST_OFFSET_Y = -75; + + public static final int DATA_ENTRY_REQUEST_ID = 10; + public static final int PERMISSION_REQUEST_ID = 11; + + public static final String PORTRAIT_ORIENTATION_LOCK = "PORTRAIT_ORIENTATION_LOCK"; + + /** + * {@link CardIOFragment#getArguments()} + * + * Reference to the id of view which will be a holder for the card.io fragment + * */ + public static final String CARD_IO_VIEW = "CARD_IO_VIEW"; + public static final String CARD_EXPIRY = "CARD_EXPIRY"; + public static final String CARD_IO_OVERLAY_COLOUR = "CARD_IO_OVERLAY_COLOUR"; +} diff --git a/card.io/src/main/java/io/card/payment/CardIOFragment.java b/card.io/src/main/java/io/card/payment/CardIOFragment.java new file mode 100644 index 00000000..ecb7bb73 --- /dev/null +++ b/card.io/src/main/java/io/card/payment/CardIOFragment.java @@ -0,0 +1,665 @@ +package io.card.payment; + +import android.Manifest; +import android.app.Activity; +import android.app.Fragment; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.hardware.Camera; +import android.hardware.SensorManager; +import android.os.Bundle; +import android.os.Vibrator; +import android.util.Log; +import android.view.Gravity; +import android.view.OrientationEventListener; +import android.view.SurfaceView; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.RelativeLayout; +import android.widget.Toast; +import android.view.ViewGroup.LayoutParams; + +import java.lang.reflect.Constructor; +import java.util.Date; + +import io.card.payment.i18n.LocalizedStrings; +import io.card.payment.i18n.StringKey; +import io.card.payment.ui.ActivityHelper; +import io.card.payment.interfaces.*; + +/** + * + * Significant changes have been made in order to create CardIO as a Fragment. Primarely in {@link CardIOActivity}, {@link CardScanner}, {@link OverlayView} and {@link Preview} + * {@link CardIOFragment#takePictureOfCard()} has been created to facilitate in case the scan fails for any reason. Implementing {@link CardScanListener#onPictureTaken(byte[])} in the FragmentActivity/Activity should do the trick + * Essentially, all dependencies were turned into references to Interfaces (As sorted by n0m0r3pa1n at Github) so it became more flexible. Thus, it also makes it possible to handle callbacks in the FragmentActivity/Activity, whichever handles the CardIOFragment + * As a side note, bear in mind that some features are not working or for some reason it wasn't found out why by the time. For example, {@link CardScanner#setScanExpiry(boolean)} among other data from scanned CreditCards + * + * It might still need some cleanup + * + * How to create a fragment(cardIOViewHolder being a RelativeLayout which will hold Card.io SurfaceView): + * + * //arguments setup for Card.io + * Bundle args = new Bundle(); + * args.putBoolean(CardIOConstants.PORTRAIT_ORIENTATION_LOCK, true); + * args.putInt(CardIOConstants.CARD_IO_VIEW, cardIOViewHolder.getId()); + * args.putInt(CardIOConstants.CARD_IO_OVERLAY_COLOUR, null); + * args.putString(CardIOConstants.EXTRA_SCAN_INSTRUCTIONS, ""); + * args.putBoolean(CardIOConstants.CARD_EXPIRY, true); //not working + * + * + * //adding Card.io as fragment + * cardFragment = new CardIOFragment(); + * cardFragment.setArguments(args); + * + * FragmentManager fragManager = getFragmentManager(); + * FragmentTransaction fragTransaction = fragManager.beginTransaction(); + * fragTransaction.add(containerPreview.getId(), cardFragment); + * fragTransaction.commit(); + * + * Created by glaubermartins on 2018-03-21. + * + */ + +public class CardIOFragment extends Fragment implements CardScanRecognition, CardIOCameraControl, Camera.PictureCallback { + + private static final String TAG = CardIOActivity.class.getSimpleName(); + + private OverlayView mOverlay; + private OrientationEventListener orientationListener; + + // TODO: the preview is accessed by the scanner. Not the best practice. + private Preview mPreview; + + private CreditCard mDetectedCard; + private Rect mGuideFrame; + private int mLastDegrees; + private int mFrameOrientation; + private boolean suppressManualEntry; + private boolean mDetectOnly; + private boolean waitingForPermission; + + private boolean useApplicationTheme; + + private CardScanner mCardScanner; + + private boolean manualEntryFallbackOrForced = false; + + /** + * Static variable for the decorated card image. This is ugly, but works. Parceling and + * unparceling card image data to pass to the next {@link android.app.Activity} does not work because the image + * data + * is too big and causes a somewhat misleading exception. Compressing the image data yields a + * reduction to 30% of the original size, but still gives the same exception. An alternative + * would be to persist the image data in a file. That seems like a pretty horrible idea, as we + * would be persisting very sensitive data on the device. + */ + static Bitmap markedCardImage = null; + + private RelativeLayout previewFrame; + private Intent clientData; + private boolean isPortraitOrientationLocked = false, includeExpiry = false, hideCardIOLogo = true, useCardIOLogo = false; + private CardScanListener cardScanListener; + private int cardIOViewHolder, overlayGuideColour; + private String scanInstructions; + + public CardIOFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle bundle = getArguments(); + if (bundle != null) { + cardIOViewHolder = bundle.getInt(CardIOConstants.CARD_IO_VIEW); + isPortraitOrientationLocked = bundle.getBoolean(CardIOConstants.PORTRAIT_ORIENTATION_LOCK); + includeExpiry = bundle.getBoolean(CardIOConstants.CARD_EXPIRY); + overlayGuideColour = bundle.getInt(CardIOConstants.CARD_IO_OVERLAY_COLOUR); + hideCardIOLogo = bundle.getBoolean(CardIOConstants.EXTRA_HIDE_CARDIO_LOGO); + scanInstructions = bundle.getString(CardIOConstants.EXTRA_SCAN_INSTRUCTIONS); + useCardIOLogo = bundle.getBoolean(CardIOConstants.EXTRA_USE_CARDIO_LOGO); + } + + clientData = getActivity().getIntent(); + + useApplicationTheme = getActivity().getIntent().getBooleanExtra(CardIOConstants.EXTRA_KEEP_APPLICATION_THEME, false); + ActivityHelper.setActivityTheme(getActivity(), useApplicationTheme); + + LocalizedStrings.setLanguage(clientData); + + // Validate app's manifest is correct. + mDetectOnly = clientData.getBooleanExtra(CardIOConstants.EXTRA_SUPPRESS_SCAN, false); + + ResolveInfo resolveInfo; + String errorMsg; + + // Check for DataEntryActivity's portrait orientation + + // Check for CardIOActivity's orientation config in manifest + resolveInfo = getActivity().getPackageManager().resolveActivity(clientData, + PackageManager.MATCH_DEFAULT_ONLY); + errorMsg = Util.manifestHasConfigChange(resolveInfo, getActivity().getClass()); + if (errorMsg != null) { + throw new RuntimeException(errorMsg); // Throw the actual exception from this class, for + // clarity. + } + + suppressManualEntry = clientData.getBooleanExtra(CardIOConstants.EXTRA_SUPPRESS_MANUAL_ENTRY, false); + + + if (savedInstanceState != null) { + waitingForPermission = savedInstanceState.getBoolean(CardIOConstants.BUNDLE_WAITING_FOR_PERMISSION); + } + + if (clientData.getBooleanExtra(CardIOConstants.EXTRA_NO_CAMERA, false)) { + manualEntryFallbackOrForced = true; + } else if (!CardScanner.processorSupported()){ + manualEntryFallbackOrForced = true; + } else { + try { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + if (!waitingForPermission) { + if (getActivity().checkSelfPermission(Manifest.permission.CAMERA) == + PackageManager.PERMISSION_DENIED) { + String[] permissions = {Manifest.permission.CAMERA}; + waitingForPermission = true; + requestPermissions(permissions, CardIOConstants.PERMISSION_REQUEST_ID); + } else { + checkCamera(); + android23AndAboveHandleCamera(); + } + } + } else { + checkCamera(); + android22AndBelowHandleCamera(); + } + } catch (Exception e) { + handleGeneralExceptionError(e); + } + } + + } + + /** + * Suspend/resume camera preview as part of the {@link android.app.Activity} life cycle (side note: we reuse the + * same buffer for preview callbacks to greatly reduce the amount of required GC). + */ + @Override + public void onResume() { + super.onResume(); + + if (!waitingForPermission) { + if (manualEntryFallbackOrForced) { + if (suppressManualEntry) { + finishIfSuppressManualEntry(); + return; + } else { + return; + } + } + + Util.logNativeMemoryStats(); + + ActivityHelper.setFlagSecure(getActivity()); + + getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + if (!isPortraitOrientationLocked) + orientationListener.enable(); + + if (!restartPreview()) { + StringKey error = StringKey.ERROR_CAMERA_UNEXPECTED_FAIL; + showErrorMessage(LocalizedStrings.getString(error)); + cardScanListener.onCardScanFail(); + } + + if (!isPortraitOrientationLocked) + doOrientationChange(mLastDegrees); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putBoolean(CardIOConstants.BUNDLE_WAITING_FOR_PERMISSION, waitingForPermission); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof CardScanListener) + cardScanListener = (CardScanListener) context; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + if (activity instanceof CardScanListener) + cardScanListener = (CardScanListener) activity; + } + + @Override + public void onPause() { + super.onPause(); + + if (orientationListener != null) { + orientationListener.disable(); + } + + if (mCardScanner != null) { + mCardScanner.pauseScanning(); + } + } + + @Override + public void onDestroy() { + mOverlay = null; + + if (orientationListener != null) { + orientationListener.disable(); + } + + if (mCardScanner != null) { + mCardScanner.endScanning(); + mCardScanner = null; + } + + super.onDestroy(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, String permissions[], + int[] grantResults) { + if (requestCode == CardIOConstants.PERMISSION_REQUEST_ID) { + waitingForPermission = false; + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + showCameraScannerOverlay(); + } else { + // show manual entry - handled in onResume() + manualEntryFallbackOrForced = true; + } + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == CardIOConstants.DATA_ENTRY_REQUEST_ID) { + if (resultCode == CardIOConstants.RESULT_CARD_INFO || resultCode == CardIOConstants.RESULT_ENTRY_CANCELED + || manualEntryFallbackOrForced) { + setResultAndFinish(resultCode, data); + } + } + } + + private void android23AndAboveHandleCamera() { + if (manualEntryFallbackOrForced) { + finishIfSuppressManualEntry(); + } else { + // Guaranteed to be called in API 23+ + showCameraScannerOverlay(); + } + } + + + private void android22AndBelowHandleCamera() { + if (manualEntryFallbackOrForced) { + finishIfSuppressManualEntry(); + } else { + // guaranteed to be called in onCreate on API < 22, so it's ok that we're removing the window feature here + showCameraScannerOverlay(); + } + } + + private void finishIfSuppressManualEntry() { + if (suppressManualEntry) { + setResultAndFinish(CardIOConstants.RESULT_SCAN_NOT_AVAILABLE, null); + } + } + + private void checkCamera() { + try { + if (!Util.hardwareSupported()) { + StringKey errorKey = StringKey.ERROR_NO_DEVICE_SUPPORT; + String localizedError = LocalizedStrings.getString(errorKey); + Log.w(Util.PUBLIC_LOG_TAG, errorKey + ": " + localizedError); + manualEntryFallbackOrForced = true; + } + } catch (CameraUnavailableException e) { + StringKey errorKey = StringKey.ERROR_CAMERA_CONNECT_FAIL; + String localizedError = LocalizedStrings.getString(errorKey); + + Log.e(Util.PUBLIC_LOG_TAG, errorKey + ": " + localizedError); + Toast toast = Toast.makeText(getActivity(), localizedError, Toast.LENGTH_LONG); + toast.setGravity(Gravity.CENTER, 0, CardIOConstants.TOAST_OFFSET_Y); + toast.show(); + manualEntryFallbackOrForced = true; + } + } + + private void showCameraScannerOverlay() { + try { + mGuideFrame = new Rect(); + + mFrameOrientation = CardIOConstants.ORIENTATION_PORTRAIT; + + if (getActivity().getIntent().getBooleanExtra(CardIOConstants.PRIVATE_EXTRA_CAMERA_BYPASS_TEST_MODE, false)) { + if (!getActivity().getPackageName().contentEquals("io.card.development")) { + throw new IllegalStateException("Illegal access of private extra"); + } + // use reflection here so that the tester can be safely stripped for release + // builds. + Class testScannerClass = Class.forName("io.card.payment.CardScannerTester"); + Constructor cons = testScannerClass.getConstructor(this.getClass(), + Integer.TYPE); + mCardScanner = (CardScanner) cons.newInstance(this, + mFrameOrientation); + } else { + mCardScanner = new CardScanner(this, mFrameOrientation, false); + mCardScanner.setScanExpiry(includeExpiry); + } + mCardScanner.prepareScanner(); + + setPreviewLayout(); + + if (!isPortraitOrientationLocked){ + orientationListener = new OrientationEventListener(getActivity(), + SensorManager.SENSOR_DELAY_UI) { + @Override + public void onOrientationChanged(int orientation) { + doOrientationChange(orientation); + } + }; + } + + } catch (Exception e) { + handleGeneralExceptionError(e); + } + } + + private void handleGeneralExceptionError(Exception e) { + StringKey errorKey = StringKey.ERROR_CAMERA_UNEXPECTED_FAIL; + String localizedError = LocalizedStrings.getString(errorKey); + + Log.e(Util.PUBLIC_LOG_TAG, "Unknown exception, please post the stack trace as a GitHub issue", e); + Toast toast = Toast.makeText(getActivity(), localizedError, Toast.LENGTH_LONG); + toast.setGravity(Gravity.CENTER, 0, CardIOConstants.TOAST_OFFSET_Y); + toast.show(); + manualEntryFallbackOrForced = true; + } + + private void doOrientationChange(int orientation) { + if (orientation < 0 || mCardScanner == null) + return; + + orientation += mCardScanner.getRotationalOffset(); + + // Check if we have gone too far forward with + // rotation adjustment, keep the result between 0-360 + if (orientation > 360) { + orientation -= 360; + } + int degrees; + + degrees = -1; + + if (orientation < CardIOConstants.DEGREE_DELTA || orientation > 360 - CardIOConstants.DEGREE_DELTA) { + degrees = 0; + mFrameOrientation = CardIOConstants.ORIENTATION_PORTRAIT; + } else if (orientation > 90 - CardIOConstants.DEGREE_DELTA && orientation < 90 + CardIOConstants.DEGREE_DELTA) { + degrees = 90; + mFrameOrientation = CardIOConstants.ORIENTATION_LANDSCAPE_LEFT; + } else if (orientation > 180 - CardIOConstants.DEGREE_DELTA && orientation < 180 + CardIOConstants.DEGREE_DELTA) { + degrees = 180; + mFrameOrientation = CardIOConstants.ORIENTATION_PORTRAIT_UPSIDE_DOWN; + } else if (orientation > 270 - CardIOConstants.DEGREE_DELTA && orientation < 270 + CardIOConstants.DEGREE_DELTA) { + degrees = 270; + mFrameOrientation = CardIOConstants.ORIENTATION_LANDSCAPE_RIGHT; + } + if (degrees >= 0 && degrees != mLastDegrees) { + mCardScanner.setDeviceOrientation(mFrameOrientation); + setDeviceDegrees(degrees); + } + } + + /** + * This {@link android.app.Activity} overrides back button handling to handle back presses properly given the + * various states this {@link android.app.Activity} can be in. + *

+ * This method is called by Android, never directly by application code. + */ + @Override + public void onBackPressed() { + if (!manualEntryFallbackOrForced && mOverlay.isAnimating()) { + try { + restartPreview(); + } catch (RuntimeException re) { + Log.w(TAG, "*** could not return to preview: " + re); + } + } else if (mCardScanner != null) { + getActivity().onBackPressed(); + } + } + + // ------------------------------------------------------------------------ + // STATIC METHODS + // ------------------------------------------------------------------------ + + /** + * Determine if the device supports card scanning. + *

+ * An ARM7 processor and Android SDK 8 or later are required. Additional checks for specific + * misbehaving devices may also be added. + * + * @return true if camera is supported. false otherwise. + */ + public static boolean canReadCardWithCamera() { + try { + return Util.hardwareSupported(); + } catch (CameraUnavailableException e) { + return false; + } catch (RuntimeException e) { + Log.w(TAG, "RuntimeException accessing Util.hardwareSupported()"); + return false; + } + } + + /** + * Returns the String version of this SDK. Please include the return value of this method in any support requests. + * + * @return The String version of this SDK + */ + public static String sdkVersion() { + return BuildConfig.VERSION_NAME; + } + + /** + * @deprecated Always returns {@code new Date()}. + */ + @Deprecated + public static Date sdkBuildDate() { + return new Date(); + } + + // end static + + public void takePictureOfCard() { + mCardScanner.takePicture(this); + } + + /** + * Show an error message using toast. + */ + private void showErrorMessage(final String msgStr) { + Log.e(Util.PUBLIC_LOG_TAG, "error display: " + msgStr); + Toast toast = Toast.makeText(getActivity(), msgStr, Toast.LENGTH_LONG); + toast.show(); + } + + private boolean restartPreview() { + mDetectedCard = null; + assert mPreview != null; + boolean success = mCardScanner.resumeScanning(mPreview.getSurfaceHolder()); + + return success; + } + + private void setDeviceDegrees(int degrees) { + View sv; + + sv = mPreview.getSurfaceView(); + + if (sv == null) { + return; + } + + mGuideFrame = mCardScanner.getGuideFrame(sv.getWidth(), sv.getHeight()); + + // adjust for surface view y offset + mGuideFrame.top += sv.getTop(); + mGuideFrame.bottom += sv.getTop(); + mOverlay.setGuideAndRotation(mGuideFrame, degrees); + mLastDegrees = degrees; + + } + + /** + * Fragment setup for camera preview as well as CardScanner. CardIOViewHolder being the container for the CardIO camera as a fragment passed over from FragmentActivity/Activity. + * */ + private void setPreviewLayout() { + + //get top level container from parent FragmentActivity (not necessarily as it could be just an Activity) + previewFrame = (RelativeLayout) getActivity().findViewById(cardIOViewHolder); + + //setup SurfaceView + mPreview = new Preview(getActivity(), null, mCardScanner.mPreviewWidth, mCardScanner.mPreviewHeight); + mPreview.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + + //adds to the parent view + previewFrame.addView(mPreview); + + //setup scancard overlay view + mOverlay = new OverlayView(getActivity(), this,null, Util.deviceSupportsTorch(getActivity())); + mOverlay.setLayoutParams(new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + mOverlay.setUseCardIOLogo(useCardIOLogo); + + //int color = getActivity().getIntent().getIntExtra(CardIOConstants.EXTRA_GUIDE_COLOR, 0); + + if (overlayGuideColour != 0) { + // force 100% opaque guide colors. + int alphaRemovedColor = overlayGuideColour | 0xFF000000; + mOverlay.setGuideColor(alphaRemovedColor); + } else { + // default to greeeeen << geez, that guys is loud + mOverlay.setGuideColor(Color.GREEN); + } + + mOverlay.setHideCardIOLogo(hideCardIOLogo); + + if (scanInstructions != null) { + mOverlay.setScanInstructions(scanInstructions); + } + + previewFrame.addView(mOverlay); + + } + + private void setResultAndFinish(final int resultCode, final Intent data) { + getActivity().setResult(resultCode, data); + markedCardImage = null; + getActivity().finish(); + } + + @Override + public void onFirstFrame(int orientation) { + SurfaceView sv = mPreview.getSurfaceView(); + if (mOverlay != null) { + mOverlay.setCameraPreviewRect(new Rect(sv.getLeft(), sv.getTop(), sv.getRight(), sv.getBottom())); + } + mFrameOrientation = CardIOConstants.ORIENTATION_PORTRAIT; + setDeviceDegrees(0); + + onEdgeUpdate(new DetectionInfo()); + } + + @Override + public void onEdgeUpdate(DetectionInfo info) { + mOverlay.setDetectionInfo(info); + } + + @Override + public void onCardDetected(Bitmap bitmap, DetectionInfo info) { + try { + Vibrator vibrator = (Vibrator) getActivity().getSystemService(Context.VIBRATOR_SERVICE); + vibrator.vibrate(CardIOConstants.VIBRATE_PATTERN, -1); + } catch (SecurityException e) { + Log.e(Util.PUBLIC_LOG_TAG, + "Could not activate vibration feedback. Please add to your application's manifest."); + } catch (Exception e) { + Log.w(Util.PUBLIC_LOG_TAG, "Exception while attempting to vibrate: ", e); + } + + mCardScanner.pauseScanning(); + + if (info.predicted()) { + mDetectedCard = info.creditCard(); + mOverlay.setDetectedCard(mDetectedCard); + } + + float sf; + if (mFrameOrientation == CardIOConstants.ORIENTATION_PORTRAIT + || mFrameOrientation == CardIOConstants.ORIENTATION_PORTRAIT_UPSIDE_DOWN) { + sf = mGuideFrame.right / (float)CardScanner.CREDIT_CARD_TARGET_WIDTH * .95f; + } else { + sf = mGuideFrame.right / (float)CardScanner.CREDIT_CARD_TARGET_WIDTH * 1.15f; + } + + Matrix m = new Matrix(); + m.postScale(sf, sf); + + Bitmap scaledCard = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), + bitmap.getHeight(), m, false); + mOverlay.setBitmap(scaledCard); + + if (mDetectOnly) { + Intent dataIntent = new Intent(); + Util.writeCapturedCardImageIfNecessary(getActivity().getIntent(), dataIntent, mOverlay); + + setResultAndFinish(CardIOConstants.RESULT_SCAN_SUPPRESSED, dataIntent); + } else { + if (mDetectedCard != null) + cardScanListener.onCardScanSuccess(mDetectedCard, scaledCard); + else + cardScanListener.onCardScanFail(); + mDetectedCard = null; + } + } + + @Override + public void triggerAutoFocus() { + mCardScanner.triggerAutoFocus(true); + } + + @Override + public void toggleFlash() { + //not being used for fragment + } + + @Override + public void onPictureTaken(byte[] data, Camera camera) { + try { + if (data != null) + cardScanListener.onPictureTaken(data); + }catch (Exception e){ + e.printStackTrace(); + } + } +} diff --git a/card.io/src/main/java/io/card/payment/CardScanner.java b/card.io/src/main/java/io/card/payment/CardScanner.java index d724c3ae..f4b22c83 100644 --- a/card.io/src/main/java/io/card/payment/CardScanner.java +++ b/card.io/src/main/java/io/card/payment/CardScanner.java @@ -25,6 +25,8 @@ import java.util.List; import java.util.Map; +import io.card.payment.interfaces.CardScanRecognition; + /** * Encapsulates the core image scanning. *

@@ -38,7 +40,7 @@ *

* HOWEVER, at the moment, the CardScanner is directly communicating with the Preview. */ -class CardScanner implements Camera.PreviewCallback, Camera.AutoFocusCallback, +public class CardScanner implements Camera.PreviewCallback, Camera.AutoFocusCallback, SurfaceHolder.Callback { private static final String TAG = CardScanner.class.getSimpleName(); @@ -84,7 +86,7 @@ private native void nScanFrame(byte[] data, int frameWidth, int frameHeight, int private static boolean manualFallbackForError; // member data - protected WeakReference mScanActivityRef; + protected WeakReference mScanActivityRef; private boolean mSuppressScan = false; private boolean mScanExpiry; private int mUnblurDigits = DEFAULT_UNBLUR_DIGITS; @@ -189,7 +191,24 @@ static boolean processorSupported() { return (!manualFallbackForError && (usesSupportedProcessorArch())); } - CardScanner(CardIOActivity scanActivity, int currentFrameOrientation) { + /** + * Modified to support fragments. No longer coupled with CardIOActivity only (non-monogamous class) + */ + public CardScanner(CardScanRecognition cardScanRecognition, int currentFrameOrientation, boolean suppressScan) { + this.mSuppressScan = suppressScan; + + setScanExpiry(false); + setUnblurDigits(DEFAULT_UNBLUR_DIGITS); + + mScanActivityRef = new WeakReference<>(cardScanRecognition); + mFrameOrientation = currentFrameOrientation; + nSetup(mSuppressScan, MIN_FOCUS_SCORE, mUnblurDigits); + } + + /** + * Commented out due to integration for fragment. See constructor above + * */ + /*CardScanner(CardIOActivity scanActivity, int currentFrameOrientation) { Intent scanIntent = scanActivity.getIntent(); if (scanIntent != null) { mSuppressScan = scanIntent.getBooleanExtra(CardIOActivity.EXTRA_SUPPRESS_SCAN, false); @@ -200,7 +219,7 @@ static boolean processorSupported() { mScanActivityRef = new WeakReference<>(scanActivity); mFrameOrientation = currentFrameOrientation; nSetup(mSuppressScan, MIN_FOCUS_SCORE, mUnblurDigits); - } + }*/ /** * Connect or reconnect to camera. If fails, sleeps and tries again. Returns true if successful, @@ -442,7 +461,7 @@ public void onPreviewFrame(byte[] data, Camera camera) { if (mFirstPreviewFrame) { mFirstPreviewFrame = false; mFrameOrientation = ORIENTATION_PORTRAIT; - mScanActivityRef.get().onFirstFrame(); + mScanActivityRef.get().onFirstFrame(mFrameOrientation); } DetectionInfo dInfo = new DetectionInfo(); @@ -625,7 +644,7 @@ private void setCameraDisplayOrientation(Camera mCamera) { int getRotationalOffset() { final int rotationOffset; // Check "normal" screen orientation and adjust accordingly - int naturalOrientation = ((WindowManager) mScanActivityRef.get().getSystemService(Context.WINDOW_SERVICE)) + int naturalOrientation = ((WindowManager) mScanActivityRef.get().getActivity().getSystemService(Context.WINDOW_SERVICE)) .getDefaultDisplay().getRotation(); if (naturalOrientation == Surface.ROTATION_0) { rotationOffset = 0; @@ -641,4 +660,16 @@ int getRotationalOffset() { } return rotationOffset; } + + public void setScanExpiry(boolean scanExpiry){ + this.mScanExpiry = scanExpiry; + } + + public void setUnblurDigits(int unblurDigits){ + this.mUnblurDigits = unblurDigits; + } + + public void takePicture(Camera.PictureCallback callback){ + mCamera.takePicture(null, callback, callback); + } } diff --git a/card.io/src/main/java/io/card/payment/DetectionInfo.java b/card.io/src/main/java/io/card/payment/DetectionInfo.java index a23c5c7d..bf34438b 100644 --- a/card.io/src/main/java/io/card/payment/DetectionInfo.java +++ b/card.io/src/main/java/io/card/payment/DetectionInfo.java @@ -9,7 +9,7 @@ * java and native code/ */ -class DetectionInfo { +public class DetectionInfo { public boolean complete; public boolean topEdge; public boolean bottomEdge; diff --git a/card.io/src/main/java/io/card/payment/OverlayView.java b/card.io/src/main/java/io/card/payment/OverlayView.java index 59dc1e7b..9b8a0057 100644 --- a/card.io/src/main/java/io/card/payment/OverlayView.java +++ b/card.io/src/main/java/io/card/payment/OverlayView.java @@ -4,6 +4,7 @@ * See the file "LICENSE.md" for the full license governing this code. */ +import android.app.Activity; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; @@ -26,6 +27,7 @@ import io.card.payment.i18n.LocalizedStrings; import io.card.payment.i18n.StringKey; +import io.card.payment.interfaces.CardIOCameraControl; /** * This class implements a transparent overlay that is drawn over the raw camera capture frames. @@ -64,7 +66,7 @@ * independent of screen scale. *

*/ -class OverlayView extends View { +public class OverlayView extends View { private static final String TAG = OverlayView.class.getSimpleName(); private static final float GUIDE_FONT_SIZE = 26.0f; @@ -87,7 +89,7 @@ class OverlayView extends View { private static final int BUTTON_TOUCH_TOLERANCE = 20; - private final WeakReference mScanActivityRef; + private final WeakReference mScanActivityRef; private DetectionInfo mDInfo; private Bitmap mBitmap; GradientDrawable mScanLineDrawable; @@ -113,11 +115,11 @@ class OverlayView extends View { private int mRotationFlip; private float mScale = 1; - public OverlayView(CardIOActivity captureActivity, AttributeSet attributeSet, boolean showTorch) { - super(captureActivity, attributeSet); + public OverlayView(Activity activity, CardIOCameraControl cameraControl, AttributeSet attributeSet, boolean showTorch) { + super(activity, attributeSet); mShowTorch = showTorch; - mScanActivityRef = new WeakReference(captureActivity); + mScanActivityRef = new WeakReference(cameraControl); mRotationFlip = 1; @@ -125,7 +127,7 @@ public OverlayView(CardIOActivity captureActivity, AttributeSet attributeSet, bo mScale = getResources().getDisplayMetrics().density / 1.5f; mTorch = new Torch(TORCH_WIDTH * mScale, TORCH_HEIGHT * mScale); - mLogo = new Logo(captureActivity); + mLogo = new Logo(activity); mGuidePaint = new Paint(Paint.ANTI_ALIAS_FLAG); diff --git a/card.io/src/main/java/io/card/payment/interfaces/CardIOCameraControl.java b/card.io/src/main/java/io/card/payment/interfaces/CardIOCameraControl.java new file mode 100644 index 00000000..5a5a9b8d --- /dev/null +++ b/card.io/src/main/java/io/card/payment/interfaces/CardIOCameraControl.java @@ -0,0 +1,11 @@ +package io.card.payment.interfaces; + +/** + * Created by glaubermartins on 2018-03-23. + */ + +public interface CardIOCameraControl { + void triggerAutoFocus(); + void toggleFlash(); + void onBackPressed(); +} diff --git a/card.io/src/main/java/io/card/payment/interfaces/CardScanListener.java b/card.io/src/main/java/io/card/payment/interfaces/CardScanListener.java new file mode 100644 index 00000000..129dfe12 --- /dev/null +++ b/card.io/src/main/java/io/card/payment/interfaces/CardScanListener.java @@ -0,0 +1,16 @@ +package io.card.payment.interfaces; + +import android.graphics.Bitmap; + +import io.card.payment.CreditCard; + +/** + * Created by glaubermartins on 2018-03-29. + */ + +public interface CardScanListener { + + void onCardScanSuccess(CreditCard cc, Bitmap bitmap); + void onCardScanFail(); + void onPictureTaken(byte[] data); +} diff --git a/card.io/src/main/java/io/card/payment/interfaces/CardScanRecognition.java b/card.io/src/main/java/io/card/payment/interfaces/CardScanRecognition.java new file mode 100644 index 00000000..9cf30ea5 --- /dev/null +++ b/card.io/src/main/java/io/card/payment/interfaces/CardScanRecognition.java @@ -0,0 +1,20 @@ +package io.card.payment.interfaces; + +import android.app.Activity; +import android.graphics.Bitmap; + +import io.card.payment.DetectionInfo; + +/** + * Created by glaubermartins on 2018-03-22. + */ + +public interface CardScanRecognition { + + Activity getActivity(); + + void onFirstFrame(int orientation); + void onCardDetected(Bitmap bitmap, DetectionInfo info); + void onEdgeUpdate(DetectionInfo info); + +}