Jelajahi Sumber

Merge branch 'developer' of https://gitlab.com/Arisono/SkWeiChat-Baidu into feature

# Conflicts:
#	WeiChat/version.properties
#	app_modular/appworks/src/main/AndroidManifest.xml
Arison 7 tahun lalu
induk
melakukan
32d5884541
100 mengubah file dengan 8485 tambahan dan 215 penghapusan
  1. 3 3
      WeiChat/version.properties
  2. 23 0
      app_core/common/src/main/java/com/common/ui/ImageUtil.java
  3. 2 0
      app_modular/appworks/build.gradle
  4. 426 180
      app_modular/appworks/src/main/java/com/uas/appworks/crm3_0/activity/ContactsAddActivity.java
  5. 0 3
      app_modular/appworks/src/main/java/com/uas/appworks/crm3_0/activity/ContactsListActivity.java
  6. 53 19
      app_modular/appworks/src/main/java/com/uas/appworks/crm3_0/fragment/ContactsListFragment.java
  7. 107 1
      app_modular/appworks/src/main/java/com/uas/appworks/crm3_0/model/ContactsBean.java
  8. 18 9
      app_modular/appworks/src/main/res/layout/activity_contacts_add.xml
  9. 1 0
      scancardlibrary/.gitignore
  10. 26 0
      scancardlibrary/build.gradle
  11. 21 0
      scancardlibrary/proguard-rules.pro
  12. 30 0
      scancardlibrary/src/main/AndroidManifest.xml
  13. 14 0
      scancardlibrary/src/main/java/com/riso/scancardlibrary/cutviews/util/DimensionUtil.java
  14. 157 0
      scancardlibrary/src/main/java/com/riso/scancardlibrary/cutviews/util/ImageUtil.java
  15. 323 0
      scancardlibrary/src/main/java/com/riso/scancardlibrary/cutviews/views/CropView.java
  16. 289 0
      scancardlibrary/src/main/java/com/riso/scancardlibrary/cutviews/views/FrameOverlayView.java
  17. 246 0
      scancardlibrary/src/main/java/com/riso/scancardlibrary/cutviews/views/MaskView.java
  18. 56 0
      scancardlibrary/src/main/java/com/riso/scancardlibrary/cutviews/views/OCRFrameLayout.java
  19. 16 0
      scancardlibrary/src/main/java/io/card/payment/CameraUnavailableException.java
  20. 1286 0
      scancardlibrary/src/main/java/io/card/payment/CardIOActivity.java
  21. 24 0
      scancardlibrary/src/main/java/io/card/payment/CardIONativeLibsConfig.java
  22. 671 0
      scancardlibrary/src/main/java/io/card/payment/CardScanner.java
  23. 342 0
      scancardlibrary/src/main/java/io/card/payment/CardType.java
  24. 194 0
      scancardlibrary/src/main/java/io/card/payment/CreditCard.java
  25. 155 0
      scancardlibrary/src/main/java/io/card/payment/CreditCardNumber.java
  26. 66 0
      scancardlibrary/src/main/java/io/card/payment/DetectionInfo.java
  27. 141 0
      scancardlibrary/src/main/java/io/card/payment/ExpiryValidator.java
  28. 63 0
      scancardlibrary/src/main/java/io/card/payment/FixedLengthValidator.java
  29. 77 0
      scancardlibrary/src/main/java/io/card/payment/Logo.java
  30. 23 0
      scancardlibrary/src/main/java/io/card/payment/MaxLengthValidator.java
  31. 54 0
      scancardlibrary/src/main/java/io/card/payment/NonEmptyValidator.java
  32. 505 0
      scancardlibrary/src/main/java/io/card/payment/OverlayView.java
  33. 104 0
      scancardlibrary/src/main/java/io/card/payment/Preview.java
  34. 17 0
      scancardlibrary/src/main/java/io/card/payment/StringHelper.java
  35. 130 0
      scancardlibrary/src/main/java/io/card/payment/Torch.java
  36. 313 0
      scancardlibrary/src/main/java/io/card/payment/Util.java
  37. 16 0
      scancardlibrary/src/main/java/io/card/payment/Validator.java
  38. 291 0
      scancardlibrary/src/main/java/io/card/payment/i18n/I18nManager.java
  39. 69 0
      scancardlibrary/src/main/java/io/card/payment/i18n/LocalizedStrings.java
  40. 36 0
      scancardlibrary/src/main/java/io/card/payment/i18n/StringKey.java
  41. 28 0
      scancardlibrary/src/main/java/io/card/payment/i18n/SupportedLocale.java
  42. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsAR.java
  43. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsDA.java
  44. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsDE.java
  45. 57 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsEN.java
  46. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsEN_AU.java
  47. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsEN_GB.java
  48. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsES.java
  49. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsES_MX.java
  50. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsFR.java
  51. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsHE.java
  52. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsIS.java
  53. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsIT.java
  54. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsJA.java
  55. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsKO.java
  56. 45 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsList.java
  57. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsMS.java
  58. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsNB.java
  59. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsNL.java
  60. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsPL.java
  61. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsPT.java
  62. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsPT_BR.java
  63. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsRU.java
  64. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsSV.java
  65. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsTH.java
  66. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsTR.java
  67. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsZH_HANS.java
  68. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsZH_HANT.java
  69. 56 0
      scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsZH_HANT_TW.java
  70. 133 0
      scancardlibrary/src/main/java/io/card/payment/ui/ActivityHelper.java
  71. 173 0
      scancardlibrary/src/main/java/io/card/payment/ui/Appearance.java
  72. 163 0
      scancardlibrary/src/main/java/io/card/payment/ui/ViewUtil.java
  73. TEMPAT SAMPAH
      scancardlibrary/src/main/jniLibs/armeabi-v7a/libcardioDecider.so
  74. TEMPAT SAMPAH
      scancardlibrary/src/main/jniLibs/armeabi-v7a/libcardioRecognizer.so
  75. TEMPAT SAMPAH
      scancardlibrary/src/main/jniLibs/armeabi-v7a/libopencv_core.so
  76. TEMPAT SAMPAH
      scancardlibrary/src/main/jniLibs/armeabi-v7a/libopencv_imgproc.so
  77. TEMPAT SAMPAH
      scancardlibrary/src/main/jniLibs/armeabi/libcardioDecider.so
  78. 5 0
      scancardlibrary/src/main/res/color/white_text_selector.xml
  79. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable-hdpi/cio_card_io_logo.png
  80. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable-hdpi/cio_ic_amex.png
  81. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable-hdpi/cio_ic_discover.png
  82. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable-hdpi/cio_ic_jcb.png
  83. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable-hdpi/cio_ic_mastercard.png
  84. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable-hdpi/cio_ic_paypal_monogram.png
  85. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable-hdpi/cio_ic_visa.png
  86. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable-hdpi/cio_paypal_logo.png
  87. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable-xhdpi/bd_ocr_cancel.png
  88. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable-xhdpi/bd_ocr_cancel_press.png
  89. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable-xhdpi/bd_ocr_confirm.png
  90. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable-xhdpi/bd_ocr_confirm_press.png
  91. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable-xhdpi/bd_ocr_rotate.png
  92. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable-xhdpi/bd_ocr_rotate_press.png
  93. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable-xhdpi/cio_card_io_logo.png
  94. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable-xhdpi/cio_ic_paypal_monogram.png
  95. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable-xhdpi/cio_paypal_logo.png
  96. 13 0
      scancardlibrary/src/main/res/drawable/bd_ocr_cancel_selector.xml
  97. 12 0
      scancardlibrary/src/main/res/drawable/bd_ocr_confirm_selector.xml
  98. 12 0
      scancardlibrary/src/main/res/drawable/bd_ocr_rotate_selector.xml
  99. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable/bd_ocr_take_photo_highlight.png
  100. TEMPAT SAMPAH
      scancardlibrary/src/main/res/drawable/bd_ocr_take_photo_normal.png

+ 3 - 3
WeiChat/version.properties

@@ -1,5 +1,5 @@
-#Fri Sep 21 17:55:30 CST 2018
-debugName=705
+#Wed Sep 26 17:47:09 CST 2018
+debugName=740
 versionName=644
-debugCode=705
+debugCode=740
 versionCode=184

+ 23 - 0
app_core/common/src/main/java/com/common/ui/ImageUtil.java

@@ -19,6 +19,8 @@ import android.text.Layout;
 import android.text.StaticLayout;
 import android.text.TextPaint;
 
+import com.common.file.FileUtils;
+
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -457,4 +459,25 @@ public class ImageUtil {
         }
         return bitmap;
     }
+
+    /**	 * 把batmap 转file	 * @param bitmap	 * @param filepath	 */	
+    public static File saveBitmapFile(Bitmap bitmap, String filepath) {
+        File file = new File(FileUtils.getSDRoot()+"/uu/");//将要保存图片的路径
+        if (!file.exists()){
+            file.mkdir();
+        }
+        File filePng = new File(filepath);
+     
+         try {
+             FileOutputStream fileOutputStream=  new FileOutputStream(filePng);
+             BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream);
+             bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos); 		
+             bos.flush(); 		
+             bos.close(); 
+         } catch (IOException e) { 		
+             e.printStackTrace(); 
+         } 	
+         return filePng;
+    }
+ 
 }

+ 2 - 0
app_modular/appworks/build.gradle

@@ -26,4 +26,6 @@ dependencies {
     compile project(':facesdk')
     compile project(path: ':appbooking')
     compile 'com.android.support:support-v4:26.+'
+
+    compile project(path: ':scancardlibrary')
 }

+ 426 - 180
app_modular/appworks/src/main/java/com/uas/appworks/crm3_0/activity/ContactsAddActivity.java

@@ -1,6 +1,9 @@
 package com.uas.appworks.crm3_0.activity;
 
 import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.os.Bundle;
 import android.text.Editable;
 import android.text.TextWatcher;
@@ -9,7 +12,6 @@ import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.AdapterView;
 import android.widget.BaseAdapter;
 import android.widget.EditText;
 import android.widget.ImageView;
@@ -18,19 +20,30 @@ import android.widget.RelativeLayout;
 import android.widget.ScrollView;
 import android.widget.TextView;
 
+import com.alibaba.fastjson.JSON;
 import com.common.LogUtil;
 import com.common.data.StringUtil;
+import com.common.file.FileUtils;
+import com.common.ui.ImageUtil;
 import com.core.app.MyApplication;
 import com.core.base.BaseActivity;
 import com.core.utils.ToastUtil;
 import com.core.widget.view.ListViewInScroller;
 import com.core.widget.view.SwitchView;
+import com.me.network.app.http.HttpClient;
+import com.me.network.app.http.Method;
+import com.me.network.app.http.rx.ResultListener;
+import com.me.network.app.http.rx.ResultSubscriber;
 import com.uas.appworks.R;
 import com.uas.appworks.crm3_0.inface.OnItemsButtonAddInface;
+import com.uas.appworks.crm3_0.model.ContactsBean;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
 
+import io.card.payment.CardIOActivity;
+
 
 /**
  * @desc:联系人新增+动态表单界面
@@ -38,6 +51,7 @@ import java.util.List;
  */
 public class ContactsAddActivity extends BaseActivity implements OnItemsButtonAddInface {
 
+    private static final String TAG = "ContactsAddActivity";
     ListViewInScroller mPhones;
     ListViewInScroller mTels;
     ListViewInScroller mEmails;
@@ -54,37 +68,44 @@ public class ContactsAddActivity extends BaseActivity implements OnItemsButtonAd
     RelativeLayout rlImages;
     TextView tvUpload;
     ImageView ivUpload;
-     LinearLayout llBasic;
-   TextView tvTitle;
+    LinearLayout llBasic;
+    TextView tvTitle;
     TextView tvSex;
-   TextView tvAge;
+    TextView tvAge;
     TextView tvPosition;
     TextView tvDepart;
     TextView tvNotes;
     TextView tvIsMarked;
-     SwitchView etIsMarked;
+    SwitchView etIsMarked;
     LinearLayout llBottom;
-   TextView tvUnmanger;
-     TextView tvManged;
-   TextView tvTimeout;
-   TextView tvTranstered;
-    int orderId=0;
-   boolean isMarked=true;
+    TextView tvUnmanger;
+    TextView tvManged;
+    TextView tvTimeout;
+    TextView tvTranstered;
+    int orderId = 0;
+    boolean isMarked = true;
+    private ImageView iv_header;
+    
+    private String imgUrl;
 
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_contacts_add);
-  
         initView();
-
         initData();
     }
-
-
+ 
+    ContactsBean model;
     private void initView() {
-        getSupportActionBar().setTitle("我的联系人");
+        super.setTitle("我的联系人");
+   
+        if (getIntent()!=null){
+           model=getIntent().getParcelableExtra("model");
+        }
+        LogUtil.d(TAG,"model:"+JSON.toJSONString(model));
+        
         mPhones = findViewById(R.id.lv_phones);
         mTels = findViewById(R.id.lv_tels);
         mEmails = findViewById(R.id.lv_emails);
@@ -102,95 +123,249 @@ public class ContactsAddActivity extends BaseActivity implements OnItemsButtonAd
         tvNotes = (TextView) findViewById(R.id.tv_notes);
         tvIsMarked = (TextView) findViewById(R.id.tv_isMarked);
         etIsMarked = (SwitchView) findViewById(R.id.et_isMarked);
-   
+
         llBottom = (LinearLayout) findViewById(R.id.ll_bottom);
         tvUnmanger = (TextView) findViewById(R.id.tv_unmanger);
         tvManged = (TextView) findViewById(R.id.tv_manged);
         tvTimeout = (TextView) findViewById(R.id.tv_timeout);
         tvTranstered = (TextView) findViewById(R.id.tv_transtered);
+        iv_header = findViewById(R.id.iv_header);
 
         etIsMarked.setChecked(true);
         etIsMarked.setOnCheckedChangeListener(new SwitchView.OnCheckedChangeListener() {
             @Override
             public void onCheckedChanged(View view, boolean isChecked) {
-                if (isChecked){
-                    isMarked=true;
-                }else{
-                    isMarked=false;
+                if (isChecked) {
+                    isMarked = true;
+                } else {
+                    isMarked = false;
                 }
             }
         });
 
-        mPhones.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+//        mPhones.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+//            @Override
+//            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
+//                LogUtil.d(TAG,"onItemClick()");
+//                ItemsContactLocalAddAdapter.ViewHolder holder = (ItemsContactLocalAddAdapter.ViewHolder) view.getTag();
+//                holder.tvAdd.setOnClickListener(new View.OnClickListener() {
+//                    @Override
+//                    public void onClick(View view) {
+//                     
+//                    }
+//                });
+//
+//            }
+//        });
+//
+//
+//        mTels.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+//            @Override
+//            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
+//                ItemsContactLocalAddAdapter.ViewHolder holder = (ItemsContactLocalAddAdapter.ViewHolder) view.getTag();
+//                holder.tvAdd.setOnClickListener(new View.OnClickListener() {
+//                    @Override
+//                    public void onClick(View view) {
+//                     
+//                    }
+//                });
+//
+//            }
+//        });
+//
+//
+//        mEmails.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+//            @Override
+//            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
+//                ItemsContactLocalAddAdapter.ViewHolder holder = (ItemsContactLocalAddAdapter.ViewHolder) view.getTag();
+//                holder.tvAdd.setOnClickListener(new View.OnClickListener() {
+//                    @Override
+//                    public void onClick(View view) {
+//                    
+//                    }
+//                });
+//            }
+//        });
+//
+//
+        tvUpload.setOnClickListener(new View.OnClickListener() {
             @Override
-            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
-                LogUtil.d(TAG,"onItemClick()");
-                ItemsContactLocalAddAdapter.ViewHolder holder = (ItemsContactLocalAddAdapter.ViewHolder) view.getTag();
-                holder.tvAdd.setOnClickListener(new View.OnClickListener() {
-                    @Override
-                    public void onClick(View view) {
-                     
-                    }
-                });
-
+            public void onClick(View view) {
+                CardIOActivity.activityStart(ContactsAddActivity.this, 0xFF00C5DC, 70);
             }
         });
 
+        //编辑状态
+        if (model!=null){
+//            getEtName().setKeyListener(null);
+           // getEtName().setFocusable(false);
+
+//            getEtSex().setKeyListener(null);
+           // getEtSex().setFocusable(false);
+//            getEtAge().setKeyListener(null);
+           // getEtAge().setFocusable(false);
+//            getEtPosition().setKeyListener(null);
+           // getEtPosition().setFocusable(false);
+//            getEtDepart().setKeyListener(null);
+           // getEtDepart().setFocusable(false);
+//            getEtNotes().setKeyListener(null);
+           // getEtNotes().setFocusable(false);
+            getEtName().setText(model.getName());
+            
+            if ("1".equals(model.getSex())){
+                getEtSex().setText("女");
+            }else{
+                getEtSex().setText("男");
+            }
+            getEtAge().setText(model.getAge()+"");
+            getEtPosition().setText(model.getPosition());
+            getEtDepart().setText(model.getDepartment());
+            getEtNotes().setText(model.getNotes());
+            if (model.getIsDMakers()==0){
+                etIsMarked.setChecked(false);
+            }else{
+                etIsMarked.setChecked(true);
+            }
+            
+            //解析手机号码,座机号码,邮件号
+            
+            String phones[]=model.getPhone().split("/");
+            String tels[]=model.getTel().split("/");
+            String emails[]=model.getEmail().split("/");
+            
+            pData.clear();
+            for (int i=0;i<phones.length;i++){
+                if (i==phones.length-1){
+                    ListItems items = new ListItems();
+                    items.setName("手机号");
+                    items.setHink("请输入");
+                    items.setValue(phones[i]);
+                    items.setAction("新增");
+                    pData.add(items);
+                }else{
+                    ListItems items = new ListItems();
+                    items.setName("手机号");
+                    items.setHink("请输入");
+                    items.setValue(phones[i]);
+                    items.setAction("删除");
+                    pData.add(items);
+                }
+            }
+            
+            
+             
+            tData.clear();
+            for (int i=0;i<tels.length;i++){
+                if(i==tels.length-1){
+                    ListItems items = new ListItems();
+                    items.setName("座机号");
+                    items.setHink("请输入");
+                    items.setValue(tels[i]);
+                    items.setAction("新增");
+                    tData.add(items);
+                }else{
+                    ListItems items = new ListItems();
+                    items.setName("座机号");
+                    items.setHink("请输入");
+                    items.setValue(tels[i]);
+                    items.setAction("删除");
+                    tData.add(items);
+                }
+              
+            }
 
-        mTels.setOnItemClickListener(new AdapterView.OnItemClickListener() {
-            @Override
-            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
-                ItemsContactLocalAddAdapter.ViewHolder holder = (ItemsContactLocalAddAdapter.ViewHolder) view.getTag();
-                holder.tvAdd.setOnClickListener(new View.OnClickListener() {
-                    @Override
-                    public void onClick(View view) {
-                     
-                    }
-                });
-
+             eData.clear();
+            for (int i=0;i<emails.length;i++){
+                if (i==emails.length-1){
+                    ListItems items = new ListItems();
+                    items.setName("Email");
+                    items.setHink("请输入");
+                    items.setValue(emails[i]);
+                    items.setAction("新增");
+                    eData.add(items);
+                }else{
+                    ListItems items = new ListItems();
+                    items.setName("Email");
+                    items.setHink("请输入");
+                    items.setValue(emails[i]);
+                    items.setAction("删除");
+                    eData.add(items);
+                }
+           
             }
-        });
 
+            
+            if (adapterPhone==null){
+                adapterPhone = new ItemsContactLocalAddAdapter(mContext, pData, this);
+                adapterPhone.setTagId(0);
+                mPhones.setAdapter(adapterPhone);
+            }else{
+                adapterPhone.notifyDataSetChanged();
+            }
+            if (adapterTel==null){
+                adapterTel = new ItemsContactLocalAddAdapter(mContext, tData, this);
+                adapterTel.setNeed(false);
+                adapterTel.setTagId(1);
+                mTels.setAdapter(adapterTel);
+            }else{
+                adapterTel.notifyDataSetChanged();
+            }
 
-        mEmails.setOnItemClickListener(new AdapterView.OnItemClickListener() {
-            @Override
-            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
-                ItemsContactLocalAddAdapter.ViewHolder holder = (ItemsContactLocalAddAdapter.ViewHolder) view.getTag();
-                holder.tvAdd.setOnClickListener(new View.OnClickListener() {
-                    @Override
-                    public void onClick(View view) {
-                    
-                    }
-                });
+            if (adapterEmails==null){
+                adapterEmails = new ItemsContactLocalAddAdapter(mContext, eData, this);
+                adapterEmails.setNeed(false);
+                adapterEmails.setTagId(2);
+                mEmails.setAdapter(adapterEmails);
+            }else{
+                adapterEmails.notifyDataSetChanged();
             }
-        });
 
+        }
     }
 
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (null != data && data.hasExtra(CardIOActivity.EXTRA_CAPTURED_CARD_IMAGE)) {
+            byte[] byteArrayExtra = data.getByteArrayExtra(CardIOActivity.EXTRA_CAPTURED_CARD_IMAGE);
+            int length = byteArrayExtra.length;
+            System.out.println("压缩前的大小====" + (length / 1024f / 1024f) + " M");
+//            tv.setText((length / 1024f / 1024f) + " M");
+            Bitmap bitmap = BitmapFactory.decodeByteArray(byteArrayExtra, 0, length);
+            iv_header.setImageBitmap(bitmap);
+            
+            tvUpload.setVisibility(View.INVISIBLE);
+            ivUpload.setVisibility(View.INVISIBLE);
+             File file=  ImageUtil.saveBitmapFile(bitmap, FileUtils.getSDRoot()+"/uu/linkheadtop.png");
+             upload(file);
+            //  bitmapBase64 = bitmapToBase64(bitmap);
+        }
+    }
 
-    private void initData() {
-        ListItems items = new ListItems();
-        items.setName("手机号");
-        items.setHink("请输入");
-
-        pData.add(items);
-
-        ListItems tItems = new ListItems();
-        tItems.setName("座机号");
-        tItems.setHink("请输入");
-        tData.add(tItems);
-
-        ListItems eTems = new ListItems();
-        eTems.setName("Email");
-        eTems.setHink("请输入");
-        eData.add(eTems);
 
-        adapterPhone = new ItemsContactLocalAddAdapter(mContext, pData,this);
+    private void initData() {
+         if (model==null){
+             ListItems items = new ListItems();
+             items.setName("手机号");
+             items.setHink("请输入");
+
+             pData.add(items);
+
+             ListItems tItems = new ListItems();
+             tItems.setName("座机号");
+             tItems.setHink("请输入");
+             tData.add(tItems);
+
+             ListItems eTems = new ListItems();
+             eTems.setName("Email");
+             eTems.setHink("请输入");
+             eData.add(eTems);
+         }
+        adapterPhone = new ItemsContactLocalAddAdapter(mContext, pData, this);
         adapterPhone.setTagId(0);
-        adapterTel = new ItemsContactLocalAddAdapter(mContext, tData,this);
+        adapterTel = new ItemsContactLocalAddAdapter(mContext, tData, this);
         adapterTel.setNeed(false);
         adapterTel.setTagId(1);
-        adapterEmails = new ItemsContactLocalAddAdapter(mContext, eData,this);
+        adapterEmails = new ItemsContactLocalAddAdapter(mContext, eData, this);
         adapterEmails.setNeed(false);
         adapterEmails.setTagId(2);
         mPhones.setAdapter(adapterPhone);
@@ -200,6 +375,24 @@ public class ContactsAddActivity extends BaseActivity implements OnItemsButtonAd
         mEmails.setAdapter(adapterEmails);
 
     }
+    
+    
+    private void upload(File file){
+        LogUtil.d(TAG,"file:"+file.getAbsolutePath());
+        HttpClient httpClient = new HttpClient.Builder("https://mobile.ubtob.com:8443/linkman/")
+                .isDebug(true)
+                .build();
+        httpClient.Api().uploads(new HttpClient.Builder("mobile/upload")
+        .add("file1",file)
+        .method(Method.POST)
+        .build(),new ResultSubscriber<>(new ResultListener<Object>() {
+            @Override
+            public void onResponse(Object s) {
+                LogUtil.d(TAG, JSON.toJSONString("result:"+s));
+              imgUrl=  JSON.parseObject(s.toString()).getString("data");
+            }
+        }));
+    }
 
 
     @Override
@@ -222,96 +415,118 @@ public class ContactsAddActivity extends BaseActivity implements OnItemsButtonAd
         }
         return true;
     }
-    
-    
-    
-    public void saveData(int orderId){
+
+
+    public void saveData( int Id) {
         //检查必填项
-        if (StringUtil.isEmpty(getEtName().getText().toString())){
+        if (StringUtil.isEmpty(getEtName().getText().toString())) {
             ToastMessage("请输入名字!");
             return;
         }
-        if (StringUtil.isEmpty(getEtSex().getText().toString())){
+        if (StringUtil.isEmpty(getEtSex().getText().toString())) {
             ToastMessage("请输入性别!");
             return;
         }
-        if (StringUtil.isEmpty(getEtAge().getText().toString())){
+        if (StringUtil.isEmpty(getEtAge().getText().toString())) {
             ToastMessage("请输入年龄!");
             return;
         }
-        
-        String phones="";
-        String tels="";
-        String emails="";
-        
-        for (int i=0;i<pData.size();i++){
-            if (!StringUtil.isEmpty(pData.get(i).getValue())){
-               if (i==pData.size()-1){
-                   phones=phones+pData.get(i).getValue();
-               }else{
-                   phones=phones+pData.get(i).getValue()+"/";
-               }
+
+        String phones = "";
+        String tels = "";
+        String emails = "";
+
+        for (int i = 0; i < pData.size(); i++) {
+            if (!StringUtil.isEmpty(pData.get(i).getValue())) {
+                if (i == pData.size() - 1) {
+                    phones = phones + pData.get(i).getValue();
+                } else {
+                    phones = phones + pData.get(i).getValue() + "/";
+                }
             }
         }
 
-        for (int i=0;i<tData.size();i++){
-            if (!StringUtil.isEmpty(tData.get(i).getValue())){
-                if (i==tData.size()-1){
-                    tels=tels+tData.get(i).getValue();
-                }else{
-                    tels=tels+tData.get(i).getValue()+"/";
+        for (int i = 0; i < tData.size(); i++) {
+            if (!StringUtil.isEmpty(tData.get(i).getValue())) {
+                if (i == tData.size() - 1) {
+                    tels = tels + tData.get(i).getValue();
+                } else {
+                    tels = tels + tData.get(i).getValue() + "/";
                 }
             }
         }
 
-        for (int i=0;i<eData.size();i++){
-            if (!StringUtil.isEmpty(eData.get(i).getValue())){
-                if (i==eData.size()-1){
-                    emails= emails+eData.get(i).getValue();
-                }else{
-                    emails= emails+eData.get(i).getValue()+"/";
+        for (int i = 0; i < eData.size(); i++) {
+            if (!StringUtil.isEmpty(eData.get(i).getValue())) {
+                if (i == eData.size() - 1) {
+                    emails = emails + eData.get(i).getValue();
+                } else {
+                    emails = emails + eData.get(i).getValue() + "/";
                 }
             }
         }
-        
-        int isMarks=0;
-        if (isMarked){
-            isMarks=1;
+
+        int isMarks = 0;
+        if (isMarked) {
+            isMarks = 1;
+        } else {
+            isMarks = 0;
+        }
+        int sexTag=0;
+        if ("男".equals(getEtSex().getText().toString())){
+            sexTag=0;
         }else{
-            isMarks=0;
+            sexTag=1;
         }
-        
-        String jsonData="{\"imid\":"+MyApplication.getInstance().mLoginUser.getUserId()+"," +
-                "\"companyName\":\""+"深圳市优软科技有限公司"+"\"," +
-                "\"name\":\""+getEtName().getText().toString()+"\"" +
-                ",\"sex\":"+getEtSex().getText().toString()+"," +
-                "\"age\":"+getEtAge().getText().toString()+"," +
-                "\"position\":\""+getEtPosition().getText().toString()+"\"," +
-                "\"department\":\""+getEtDepart().getText().toString()+"\"," +
+        String jsonData = "{\"imid\":" + MyApplication.getInstance().mLoginUser.getUserId() + "," +
+                "\"companyName\":\"" + "深圳市优软科技有限公司" + "\"," +
+                "\"name\":\"" + getEtName().getText().toString() + "\"" +
+                ",\"sex\":" + sexTag + "," +
+                "\"age\":" + getEtAge().getText().toString() + "," +
+                "\"position\":\"" + getEtPosition().getText().toString() + "\"," +
+                "\"department\":\"" + getEtDepart().getText().toString() + "\"," +
                 "\"brithday\":\"2018-05-06\"," +
-                "\"isDMakers\":"+isMarks+"," +
-                "\"notes\":\""+getEtNotes().getText().toString()+"\"," +
-                "\"phone\":\""+phones+"\"," +
-                "\"email\":\""+emails+"\"," +
-                "\"tel\":\""+tels+"\"," +
-                "\"imageUrl\":\"/files/gdfgfd.png\"" +
+                "\"isDMakers\":" + isMarks + "," +
+                "\"notes\":\"" + getEtNotes().getText().toString() + "\"," +
+                "\"phone\":\"" + phones + "\"," +
+                "\"email\":\"" + emails + "\"," +
+                "\"tel\":\"" + tels + "\"," +
+                "\"imageUrl\":\""+imgUrl+"\"" +
                 "}";
-        
-        LogUtil.d(TAG,"jsonData:"+jsonData);
-        
-        
-        
+
+        LogUtil.d(TAG, "jsonData:" + jsonData);
+        HttpClient httpClient = new HttpClient.Builder("https://mobile.ubtob.com:8443/linkman/")
+                .isDebug(true)
+                .build();
+        httpClient.Api().send(new HttpClient.Builder("mobile/contactAdd")
+        .add("jsonFile",jsonData)
+                .method(Method.POST)
+        .build(),new ResultSubscriber<Object>(new ResultListener<Object>() {
+
+            @Override
+            public void onResponse(Object o) {
+                LogUtil.d(TAG,JSON.toJSONString(o));
+               
+                try {
+                    orderId=JSON.parseObject(o.toString()).getInteger("data");
+                    ToastMessage("保存成功,单据ID:"+orderId);
+                }catch (Exception e){
+                    
+                }
+            }
+        }));
+
     }
 
     @Override
-    public void onItemsClick(View view, int id,int position,Object object) {
-        switch (id){
+    public void onItemsClick(View view, int id, int position, Object object) {
+        switch (id) {
             case 0:
-                ListItems model=(ListItems) object;
-                if ("删除".equals(model.getAction())){
+                ListItems model = (ListItems) object;
+                if ("删除".equals(model.getAction())) {
                     pData.remove(model);
-                    
-                }else{
+
+                } else {
                     //判别是否是删除操作,还是新增操作
                     ListItems items = new ListItems();
                     items.setName("手机号");
@@ -319,35 +534,71 @@ public class ContactsAddActivity extends BaseActivity implements OnItemsButtonAd
                     items.setAction("新增");
                     pData.add(items);
 
-                    for(int i=0;i<pData.size();i++){
-                        if (i==pData.size()-1){
+                    for (int i = 0; i < pData.size(); i++) {
+                        if (i == pData.size() - 1) {
                             pData.get(i).setAction("新增");
-                        }else{
+                        } else {
                             pData.get(i).setAction("删除");
 
                         }
                     }
-                  
+
                 }
                 adapterPhone.notifyDataSetChanged();
                 break;
             case 1:
-                ListItems tItems = new ListItems();
-                tItems.setName("座机号");
-                tItems.setHink("请输入");
-                tData.add(tItems);
+                ListItems tItems =  (ListItems) object;
+                if ("删除".equals(tItems.getAction())) {
+                    tData.remove(tItems);
+
+                } else {
+                    //判别是否是删除操作,还是新增操作
+                    ListItems items = new ListItems();
+                    items.setName("座机号");
+                    items.setHink("请输入");
+                    items.setAction("新增");
+                    tData.add(items);
+
+                    for (int i = 0; i <  tData.size(); i++) {
+                        if (i ==  tData.size() - 1) {
+                            tData.get(i).setAction("新增");
+                        } else {
+                            tData.get(i).setAction("删除");
+
+                        }
+                    }
+
+                }
 
                 adapterTel.notifyDataSetChanged();
                 break;
-                
-                
+
+
             case 2:
-                ListItems eTems = new ListItems();
-                eTems.setName("Email");
-                eTems.setHink("请输入");
-                eData.add(eTems);
+                ListItems eTems =  (ListItems) object;
+                if ("删除".equals( eTems.getAction())) {
+                    eData.remove( eTems);
+
+                } else {
+                    //判别是否是删除操作,还是新增操作
+                    ListItems items = new ListItems();
+                    items.setName("Email");
+                    items.setHink("请输入");
+                    items.setAction("新增");
+                    eData.add(items);
+
+                    for (int i = 0; i < eData.size(); i++) {
+                        if (i == eData.size() - 1) {
+                            eData.get(i).setAction("新增");
+                        } else {
+                            eData.get(i).setAction("删除");
+
+                        }
+                    }
+
+                }
                 adapterEmails.notifyDataSetChanged();
-                
+
                 break;
         }
     }
@@ -397,7 +648,7 @@ public class ContactsAddActivity extends BaseActivity implements OnItemsButtonAd
     public class ItemsContactLocalAddAdapter extends BaseAdapter {
 
         private List<ListItems> objects = new ArrayList<ListItems>();
-        
+
         private Context context;
         private LayoutInflater layoutInflater;
         private int tagId;
@@ -417,11 +668,11 @@ public class ContactsAddActivity extends BaseActivity implements OnItemsButtonAd
             this.objects = datas;
             this.layoutInflater = LayoutInflater.from(context);
         }
-        
-        public ItemsContactLocalAddAdapter(Context context, List<ListItems> datas,OnItemsButtonAddInface listener) {
+
+        public ItemsContactLocalAddAdapter(Context context, List<ListItems> datas, OnItemsButtonAddInface listener) {
             this.context = context;
             this.objects = datas;
-            this.onItemsButtonAddInface=listener;
+            this.onItemsButtonAddInface = listener;
             this.layoutInflater = LayoutInflater.from(context);
         }
 
@@ -462,7 +713,7 @@ public class ContactsAddActivity extends BaseActivity implements OnItemsButtonAd
                 convertView = layoutInflater.inflate(R.layout.items_contact_local_add, null);
                 convertView.setTag(new ViewHolder(convertView));
             }
-            initializeViews((ListItems) getItem(position), (ViewHolder) convertView.getTag(),position);
+            initializeViews((ListItems) getItem(position), (ViewHolder) convertView.getTag(), position);
             return convertView;
         }
 
@@ -470,37 +721,36 @@ public class ContactsAddActivity extends BaseActivity implements OnItemsButtonAd
             holder.tvName.setText(object.getName());
             holder.etName.setHint(object.getHink());
             holder.etName.setText(object.getValue());
-            
-            if ("删除".equals(object.getAction())){
+
+            if ("删除".equals(object.getAction())) {
                 holder.tvAdd.setText("删除");
-            }else{
+            } else {
                 holder.tvAdd.setText("新增");
             }
-            
+
             holder.tvAdd.setOnClickListener(new View.OnClickListener() {
                 @Override
                 public void onClick(View view) {
-                    ToastMessage("点击新增!");
-                    onItemsButtonAddInface.onItemsClick(view, tagId,position,object);
+                    onItemsButtonAddInface.onItemsClick(view, tagId, position, object);
                 }
             });
-            
+
             holder.etName.addTextChangedListener(new TextWatcher() {
                 @Override
                 public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
-                    
+
                 }
 
                 @Override
                 public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
-                   
-                   
-                   // pData.get(position).setValue(charSequence.toString());
+
+
+                    // pData.get(position).setValue(charSequence.toString());
                 }
 
                 @Override
                 public void afterTextChanged(Editable editable) {
-                        object.setValue(editable.toString());
+                    object.setValue(editable.toString());
                 }
             });
             if (isNeed) {
@@ -510,10 +760,7 @@ public class ContactsAddActivity extends BaseActivity implements OnItemsButtonAd
             }
         }
 
-        
-        
-        
-        
+
         protected class ViewHolder {
             private TextView tvName, tv_mark;
             private EditText etName;
@@ -528,30 +775,29 @@ public class ContactsAddActivity extends BaseActivity implements OnItemsButtonAd
         }
     }
 
-   
-    
-    private EditText getEtName(){
+
+    private EditText getEtName() {
         return (EditText) findViewById(R.id.et_name);
     }
 
-    private EditText getEtSex(){
+    private EditText getEtSex() {
         return (EditText) findViewById(R.id.et_sex);
     }
 
-    private EditText getEtAge(){
+    private EditText getEtAge() {
         return (EditText) findViewById(R.id.et_age);
     }
 
-    private EditText getEtPosition(){
+    private EditText getEtPosition() {
         return (EditText) findViewById(R.id.et_position);
     }
 
-    private EditText getEtDepart(){
+    private EditText getEtDepart() {
         return (EditText) findViewById(R.id.et_depart);
     }
 
-    private EditText getEtNotes(){
+    private EditText getEtNotes() {
         return (EditText) findViewById(R.id.et_notes);
     }
-    
+
 }

+ 0 - 3
app_modular/appworks/src/main/java/com/uas/appworks/crm3_0/activity/ContactsListActivity.java

@@ -210,15 +210,12 @@ public class ContactsListActivity extends BaseActivity implements  ContactsLocal
     private List<Map<String, Object>> getPopData() {
         List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
         Map<String, Object> map = new HashMap<>();
-        
         map = new HashMap<String, Object>();
         map.put("item_name", "新建我的联系人");
         list.add(map);
-
         map = new HashMap<String, Object>();
         map.put("item_name","新建客户联系人");
         list.add(map);
-
         return list;
     }
 }

+ 53 - 19
app_modular/appworks/src/main/java/com/uas/appworks/crm3_0/fragment/ContactsListFragment.java

@@ -15,9 +15,15 @@ import android.widget.TextView;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.TypeReference;
 import com.common.LogUtil;
+import com.common.data.StringUtil;
+import com.core.app.MyApplication;
 import com.core.utils.CommonUtil;
 import com.handmark.pulltorefresh.library.PullToRefreshBase;
 import com.handmark.pulltorefresh.library.PullToRefreshListView;
+import com.me.network.app.http.HttpClient;
+import com.me.network.app.http.Method;
+import com.me.network.app.http.rx.ResultListener;
+import com.me.network.app.http.rx.ResultSubscriber;
 import com.modular.apputils.listener.OnSmartHttpListener;
 import com.modular.apputils.network.Tags;
 import com.uas.appworks.R;
@@ -81,7 +87,11 @@ public class ContactsListFragment extends ViewPagerLazyFragment implements OnSma
         mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
             @Override
             public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
-                getActivity().startActivity(new Intent(getActivity(), ContactsAddActivity.class));
+                ItemContactsMeAdapter.ViewHolder viewHolder= (ItemContactsMeAdapter.ViewHolder) view.getTag();
+                 
+                getActivity().startActivity(new Intent(getActivity(), ContactsAddActivity.class)
+                .putExtra("model",viewHolder.bean));
+                
             }
         });
         
@@ -90,12 +100,32 @@ public class ContactsListFragment extends ViewPagerLazyFragment implements OnSma
     private void initData(){
         switch (tabItem){
             case 1:
+                //我的联系人-不分页
+                HttpClient httpClient = new HttpClient.Builder("https://mobile.ubtob.com:8443/linkman/")
+                        .isDebug(true)
+                        .build();
+                httpClient.Api().send(new HttpClient.Builder("mobile/contactList")
+                        .add("key","")
+                        .add("imid", MyApplication.getInstance().getLoginUserId())
+                        .method(Method.GET)
+                        .build(),new ResultSubscriber<Object>(new ResultListener<Object>() {
+
+                    @Override
+                    public void onResponse(Object o) {
+                        try {
+                            datas= JSON.parseObject(JSON.parseObject(o.toString()).getJSONArray("data").toJSONString()
+                                    ,new TypeReference<List<ContactsBean>>(){});
+                            adapter=new ItemContactsMeAdapter(getActivity(),datas);
+                            mListView.setAdapter(adapter);
+                            LogUtil.d(TAG,JSON.toJSONString(datas));
+                        }catch (Exception e){
+
+                        }
+                    }
+                }));
+                
                 String hex="{\"contacts\":[{\"companyName\":\"深圳市优软科技有限公司\",\"name\":\"张三\",\"sex\":\"男\",\"age\":25,\"position\":\"财务经理\",\"department\":\"财务部\",\"brithday\":\"1992-08-17\",\"phone\":\"13266699268\",\"isDMakers\":1,\"notes\":\"特殊客户\"},{\"companyName\":\"深圳市优软科技有限公司\",\"name\":\"张三\",\"sex\":\"男\",\"age\":25,\"position\":\"财务经理\",\"department\":\"财务部\",\"brithday\":\"1992-08-17\",\"phone\":\"13266699268\",\"isDMakers\":1,\"notes\":\"特殊客户\"}]}";
-                 datas= JSON.parseObject(JSON.parseObject(hex).getJSONArray("contacts").toJSONString()
-                        ,new TypeReference<List<ContactsBean>>(){});
-                adapter=new ItemContactsMeAdapter(getActivity(),datas);
-                mListView.setAdapter(adapter);
-                LogUtil.d(TAG,JSON.toJSONString(datas));
+               
                 break;
             case 2:
                 hex="{\"contacts\":[{\"companyName\":\"深圳市优软科技有限公司\",\"name\":\"张三\",\"sex\":\"男\",\"age\":25,\"position\":\"财务经理\",\"department\":\"财务部\",\"brithday\":\"1992-08-17\",\"phone\":\"13266699268\",\"isDMakers\":1,\"notes\":\"特殊客户\"},{\"companyName\":\"深圳市优软科技有限公司\",\"name\":\"张三\",\"sex\":\"男\",\"age\":25,\"position\":\"财务经理\",\"department\":\"财务部\",\"brithday\":\"1992-08-17\",\"phone\":\"13266699268\",\"isDMakers\":1,\"notes\":\"特殊客户\"}]}";
@@ -118,20 +148,20 @@ public class ContactsListFragment extends ViewPagerLazyFragment implements OnSma
     public void loadMore(){
         switch (tabItem){
             case 1:
-                String hex="{\"contacts\":[{\"companyName\":\"深圳市优软科技有限\",\"name\":\"张三\",\"sex\":\"男\",\"age\":25,\"position\":\"财务经理\",\"department\":\"财务部\",\"brithday\":\"1992-08-17\",\"phone\":\"13266699268\",\"isDMakers\":1,\"notes\":\"特殊客户\"},{\"companyName\":\"深圳市优软科司\",\"name\":\"张三\",\"sex\":\"男\",\"age\":25,\"position\":\"财务经理\",\"department\":\"财务部\",\"brithday\":\"1992-08-17\",\"phone\":\"13266699268\",\"isDMakers\":1,\"notes\":\"特殊客户\"}]}";
-                List<ContactsBean> datasNew= JSON.parseObject(JSON.parseObject(hex).getJSONArray("contacts").toJSONString()
-                        ,new TypeReference<List<ContactsBean>>(){});
-                datas.addAll(datasNew);
-                adapter.notifyDataSetChanged();
-                LogUtil.d(TAG,JSON.toJSONString(datas));
+//                String hex="{\"contacts\":[{\"companyName\":\"深圳市优软科技有限\",\"name\":\"张三\",\"sex\":\"男\",\"age\":25,\"position\":\"财务经理\",\"department\":\"财务部\",\"brithday\":\"1992-08-17\",\"phone\":\"13266699268\",\"isDMakers\":1,\"notes\":\"特殊客户\"},{\"companyName\":\"深圳市优软科司\",\"name\":\"张三\",\"sex\":\"男\",\"age\":25,\"position\":\"财务经理\",\"department\":\"财务部\",\"brithday\":\"1992-08-17\",\"phone\":\"13266699268\",\"isDMakers\":1,\"notes\":\"特殊客户\"}]}";
+//                List<ContactsBean> datasNew= JSON.parseObject(JSON.parseObject(hex).getJSONArray("contacts").toJSONString()
+//                        ,new TypeReference<List<ContactsBean>>(){});
+//                datas.addAll(datasNew);
+//                adapter.notifyDataSetChanged();
+//                LogUtil.d(TAG,JSON.toJSONString(datas));
                 break;
             case 2:
-                hex="{\"contacts\":[{\"companyName\":\"深圳市科技有限公司\",\"name\":\"张\",\"sex\":\"男\",\"age\":25,\"position\":\"财务经理\",\"department\":\"财务部\",\"brithday\":\"1992-08-17\",\"phone\":\"13266699268\",\"isDMakers\":1,\"notes\":\"特客户\"},{\"companyName\":\"深圳市优软科技有限公司\",\"name\":\"张三\",\"sex\":\"男\",\"age\":25,\"position\":\"财务经理\",\"department\":\"财务部\",\"brithday\":\"1992-08-17\",\"phone\":\"13266699268\",\"isDMakers\":1,\"notes\":\"特殊户\"}]}";
-                datasNew= JSON.parseObject(JSON.parseObject(hex).getJSONArray("contacts").toJSONString()
-                        ,new TypeReference<List<ContactsBean>>(){});
-                datas.addAll(datasNew);
-                adapter.notifyDataSetChanged();
-                LogUtil.d(TAG,JSON.toJSONString(datas));
+//                hex="{\"contacts\":[{\"companyName\":\"深圳市科技有限公司\",\"name\":\"张\",\"sex\":\"男\",\"age\":25,\"position\":\"财务经理\",\"department\":\"财务部\",\"brithday\":\"1992-08-17\",\"phone\":\"13266699268\",\"isDMakers\":1,\"notes\":\"特客户\"},{\"companyName\":\"深圳市优软科技有限公司\",\"name\":\"张三\",\"sex\":\"男\",\"age\":25,\"position\":\"财务经理\",\"department\":\"财务部\",\"brithday\":\"1992-08-17\",\"phone\":\"13266699268\",\"isDMakers\":1,\"notes\":\"特殊户\"}]}";
+//                datasNew= JSON.parseObject(JSON.parseObject(hex).getJSONArray("contacts").toJSONString()
+//                        ,new TypeReference<List<ContactsBean>>(){});
+//                datas.addAll(datasNew);
+//                adapter.notifyDataSetChanged();
+//                LogUtil.d(TAG,JSON.toJSONString(datas));
                 break;
         }
 
@@ -216,8 +246,11 @@ public class ContactsListFragment extends ViewPagerLazyFragment implements OnSma
         private void initializeViews(ContactsBean object, ViewHolder holder) {
          holder.tvName.setText(object.getName());
          holder.tvCompanyName.setText(object.getCompanyName());
-         holder.tvPhone.setText(object.getPhone());
+         if (!StringUtil.isEmpty(object.getPhone())){
+             holder.tvPhone.setText(object.getPhone().split("/")[0]);
+         }
          holder.tvPosition.setText(object.getPosition());
+         holder.bean=object;
         }
 
         protected class ViewHolder {
@@ -226,6 +259,7 @@ public class ContactsListFragment extends ViewPagerLazyFragment implements OnSma
             private TextView tvCompanyName;
             private TextView tvPhone;
             private ImageView ivIcon;
+            private ContactsBean bean;
 
             public ViewHolder(View view) {
                 tvName = (TextView) view.findViewById(R.id.tv_name);

+ 107 - 1
app_modular/appworks/src/main/java/com/uas/appworks/crm3_0/model/ContactsBean.java

@@ -4,14 +4,19 @@ package com.uas.appworks.crm3_0.model;
  * Created by Arison on 2018/9/18.
  */
 
+import android.os.Parcel;
+import android.os.Parcelable;
+
 /**
  * Auto-generated: 2018-09-17 17:51:27
  *
  * @author bejson.com (i@bejson.com)
  * @website http://www.bejson.com/java2pojo/
  */
-public class ContactsBean {
+public class ContactsBean implements Parcelable {
 
+    private int linkmanId;
+    private int imid;
     private String companyName;
     private String name;
     private String sex;
@@ -22,6 +27,9 @@ public class ContactsBean {
     private int isDMakers;
     private String notes;
     private String phone;
+    private String email;
+    private String tel;
+    private String imageUrl;
 
     public void setCompanyName(String companyName) {
         this.companyName = companyName;
@@ -93,4 +101,102 @@ public class ContactsBean {
     public void setPhone(String phone) {
         this.phone = phone;
     }
+
+    public int getLinkmanId() {
+        return linkmanId;
+    }
+
+    public void setLinkmanId(int linkmanId) {
+        this.linkmanId = linkmanId;
+    }
+
+    public int getImid() {
+        return imid;
+    }
+
+    public void setImid(int imid) {
+        this.imid = imid;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public String getTel() {
+        return tel;
+    }
+
+    public void setTel(String tel) {
+        this.tel = tel;
+    }
+
+    public String getImageUrl() {
+        return imageUrl;
+    }
+
+    public void setImageUrl(String imageUrl) {
+        this.imageUrl = imageUrl;
+    }
+
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(this.linkmanId);
+        dest.writeInt(this.imid);
+        dest.writeString(this.companyName);
+        dest.writeString(this.name);
+        dest.writeString(this.sex);
+        dest.writeInt(this.age);
+        dest.writeString(this.position);
+        dest.writeString(this.department);
+        dest.writeString(this.brithday);
+        dest.writeInt(this.isDMakers);
+        dest.writeString(this.notes);
+        dest.writeString(this.phone);
+        dest.writeString(this.email);
+        dest.writeString(this.tel);
+        dest.writeString(this.imageUrl);
+    }
+
+    public ContactsBean() {
+    }
+
+    protected ContactsBean(Parcel in) {
+        this.linkmanId = in.readInt();
+        this.imid = in.readInt();
+        this.companyName = in.readString();
+        this.name = in.readString();
+        this.sex = in.readString();
+        this.age = in.readInt();
+        this.position = in.readString();
+        this.department = in.readString();
+        this.brithday = in.readString();
+        this.isDMakers = in.readInt();
+        this.notes = in.readString();
+        this.phone = in.readString();
+        this.email = in.readString();
+        this.tel = in.readString();
+        this.imageUrl = in.readString();
+    }
+
+    public static final Parcelable.Creator<ContactsBean> CREATOR = new Parcelable.Creator<ContactsBean>() {
+        @Override
+        public ContactsBean createFromParcel(Parcel source) {
+            return new ContactsBean(source);
+        }
+
+        @Override
+        public ContactsBean[] newArray(int size) {
+            return new ContactsBean[size];
+        }
+    };
 }

+ 18 - 9
app_modular/appworks/src/main/res/layout/activity_contacts_add.xml

@@ -2,7 +2,9 @@
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:fitsSystemWindows="true"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent"
+    android:focusable="true"
+    android:focusableInTouchMode="true" >
     <ScrollView
         android:id="@+id/sv_content"
         android:layout_width="match_parent"
@@ -18,14 +20,14 @@
           <RelativeLayout
               android:id="@+id/rl_images"
               android:layout_width="match_parent"
-              android:layout_height="120dp">
-              <TextView
-                  android:id="@+id/tv_upload"
-                  android:layout_width="wrap_content"
-                  android:layout_height="wrap_content"
-                  android:layout_centerInParent="true"
-                  android:text="上传名片照"/>
-              
+              android:layout_height="200dp">
+              <ImageView
+                  android:id="@+id/iv_header"
+                  android:layout_width="match_parent"
+                  android:layout_height="match_parent"
+                  android:background="@color/transparent"
+                  android:layout_margin="@dimen/dp_10"
+                  android:src="@color/transparent"/>
               <ImageView
                   android:id="@+id/iv_upload"
                   android:layout_width="15dp"
@@ -34,6 +36,12 @@
                   android:layout_alignParentTop="true"
                   android:layout_alignParentRight="true"
                   android:src="@drawable/phone"/>
+              <TextView
+                  android:id="@+id/tv_upload"
+                  android:layout_width="wrap_content"
+                  android:layout_height="wrap_content"
+                  android:layout_centerInParent="true"
+                  android:text="上传名片照"/>
           </RelativeLayout>
           <LinearLayout
               android:id="@+id/ll_basic"
@@ -139,6 +147,7 @@
                       android:layout_marginLeft="1dp"
                       android:layout_marginTop="12dp"
                       android:layout_toRightOf="@id/tv_age"
+                      android:visibility="gone"
                       android:text="*"
                       android:textColor="@color/red" />
                   <EditText

+ 1 - 0
scancardlibrary/.gitignore

@@ -0,0 +1 @@
+/build

+ 26 - 0
scancardlibrary/build.gradle

@@ -0,0 +1,26 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion rootProject.ext.android.compileSdkVersion
+    buildToolsVersion rootProject.ext.android.buildToolsVersion
+    defaultConfig {
+        minSdkVersion rootProject.ext.android.minSdkVersion
+        targetSdkVersion rootProject.ext.android.targetSdkVersion
+        versionCode rootProject.ext.android.versionCode
+        versionName rootProject.ext.android.versionName
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+
+dependencies {
+    compile fileTree(include: ['*.jar'], dir: 'libs')
+    testCompile deps.junit
+    compile deps.appcompatV7
+
+}

+ 21 - 0
scancardlibrary/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 30 - 0
scancardlibrary/src/main/AndroidManifest.xml

@@ -0,0 +1,30 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.riso.scancardlibrary">
+
+    <uses-permission android:name="android.permission.CAMERA"/>
+    <uses-permission android:name="android.permission.VIBRATE"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+
+    <uses-feature
+        android:name="android.hardware.camera"
+        android:required="false"/>
+    <uses-feature
+        android:name="android.hardware.camera.autofocus"
+        android:required="false"/>
+
+
+    <application
+        android:allowBackup="true"
+        android:supportsRtl="true"
+        >
+
+        <activity
+            android:name="io.card.payment.CardIOActivity"
+            android:configChanges="keyboardHidden|orientation"
+            android:screenOrientation="portrait"/>
+    </application>
+
+
+</manifest>

+ 14 - 0
scancardlibrary/src/main/java/com/riso/scancardlibrary/cutviews/util/DimensionUtil.java

@@ -0,0 +1,14 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.riso.scancardlibrary.cutviews.util;
+
+import android.content.res.Resources;
+
+public class DimensionUtil {
+
+    public static int dpToPx(int dp) {
+        return (int) (dp * Resources.getSystem().getDisplayMetrics().density);
+    }
+
+}

+ 157 - 0
scancardlibrary/src/main/java/com/riso/scancardlibrary/cutviews/util/ImageUtil.java

@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.riso.scancardlibrary.cutviews.util;
+
+import android.graphics.BitmapFactory;
+import android.media.ExifInterface;
+import android.util.Log;
+
+public class ImageUtil {
+    private static final String TAG = "CameraExif";
+
+    public static int exifToDegrees(int exifOrientation) {
+        if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90) {
+            return 90;
+        } else if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_180) {
+            return 180;
+        } else if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) {
+            return 270;
+        }
+        return 0;
+    }
+
+    // Returns the degrees in clockwise. Values are 0, 90, 180, or 270.
+    public static int getOrientation(byte[] jpeg) {
+        if (jpeg == null) {
+            return 0;
+        }
+
+        int offset = 0;
+        int length = 0;
+
+        // ISO/IEC 10918-1:1993(E)
+        while (offset + 3 < jpeg.length && (jpeg[offset++] & 0xFF) == 0xFF) {
+            int marker = jpeg[offset] & 0xFF;
+
+            // Check if the marker is a padding.
+            if (marker == 0xFF) {
+                continue;
+            }
+            offset++;
+
+            // Check if the marker is SOI or TEM.
+            if (marker == 0xD8 || marker == 0x01) {
+                continue;
+            }
+            // Check if the marker is EOI or SOS.
+            if (marker == 0xD9 || marker == 0xDA) {
+                break;
+            }
+
+            // Get the length and check if it is reasonable.
+            length = pack(jpeg, offset, 2, false);
+            if (length < 2 || offset + length > jpeg.length) {
+                Log.e(TAG, "Invalid length");
+                return 0;
+            }
+
+            // Break if the marker is EXIF in APP1.
+            if (marker == 0xE1 && length >= 8
+                    && pack(jpeg, offset + 2, 4, false) == 0x45786966
+                    && pack(jpeg, offset + 6, 2, false) == 0) {
+                offset += 8;
+                length -= 8;
+                break;
+            }
+
+            // Skip other markers.
+            offset += length;
+            length = 0;
+        }
+
+        // JEITA CP-3451 Exif Version 2.2
+        if (length > 8) {
+            // Identify the byte order.
+            int tag = pack(jpeg, offset, 4, false);
+            if (tag != 0x49492A00 && tag != 0x4D4D002A) {
+                Log.e(TAG, "Invalid byte order");
+                return 0;
+            }
+            boolean littleEndian = (tag == 0x49492A00);
+
+            // Get the offset and check if it is reasonable.
+            int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;
+            if (count < 10 || count > length) {
+                Log.e(TAG, "Invalid offset");
+                return 0;
+            }
+            offset += count;
+            length -= count;
+
+            // Get the count and go through all the elements.
+            count = pack(jpeg, offset - 2, 2, littleEndian);
+            while (count-- > 0 && length >= 12) {
+                // Get the tag and check if it is orientation.
+                tag = pack(jpeg, offset, 2, littleEndian);
+                if (tag == 0x0112) {
+                    // We do not really care about type and count, do we?
+                    int orientation = pack(jpeg, offset + 8, 2, littleEndian);
+                    switch (orientation) {
+                        case 1:
+                            return 0;
+                        case 3:
+                            return 180;
+                        case 6:
+                            return 90;
+                        case 8:
+                            return 270;
+                        default:
+                            return 0;
+                    }
+                }
+                offset += 12;
+                length -= 12;
+            }
+        }
+
+        Log.i(TAG, "Orientation not found");
+        return 0;
+    }
+
+    private static int pack(byte[] bytes, int offset, int length,boolean littleEndian) {
+        int step = 1;
+        if (littleEndian) {
+            offset += length - 1;
+            step = -1;
+        }
+
+        int value = 0;
+        while (length-- > 0) {
+            value = (value << 8) | (bytes[offset] & 0xFF);
+            offset += step;
+        }
+        return value;
+    }
+
+    public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
+        // Raw height and width of image
+        final int height = options.outHeight;
+        final int width = options.outWidth;
+        int inSampleSize = 1;
+
+        if (height > reqHeight || width > reqWidth) {
+
+            final int halfHeight = height / 2;
+            final int halfWidth = width / 2;
+            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
+            // height and width larger than the requested height and width.
+            while ((halfHeight / inSampleSize) >= reqHeight
+                    && (halfWidth / inSampleSize) >= reqWidth) {
+                inSampleSize *= 2;
+            }
+        }
+
+        return inSampleSize;
+    }
+}

+ 323 - 0
scancardlibrary/src/main/java/com/riso/scancardlibrary/cutviews/views/CropView.java

@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.riso.scancardlibrary.cutviews.views;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.media.ExifInterface;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.riso.scancardlibrary.cutviews.util.ImageUtil;
+
+import java.io.IOException;
+
+
+public class CropView extends View {
+
+    public CropView(Context context) {
+        super(context);
+        init();
+    }
+
+    public CropView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    public CropView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init();
+    }
+
+    public void setFilePath(String path) {
+
+        if (this.bitmap != null && !this.bitmap.isRecycled()) {
+            this.bitmap.recycle();
+        }
+
+        if (path == null) {
+            return;
+        }
+
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inJustDecodeBounds = true;
+        Bitmap original = BitmapFactory.decodeFile(path, options);
+
+        try {
+            ExifInterface exif = new ExifInterface(path);
+            int rotation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
+            Matrix matrix = new Matrix();
+            int rotationInDegrees = ImageUtil.exifToDegrees(rotation);
+            if (rotation != 0f) {
+                matrix.preRotate(rotationInDegrees);
+            }
+
+            // 图片太大会导致内存泄露,所以在显示前对图片进行裁剪。
+            int maxPreviewImageSize = 2560;
+
+            int min = Math.min(options.outWidth, options.outHeight);
+            min = Math.min(min, maxPreviewImageSize);
+
+            WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
+            Point screenSize = new Point();
+            windowManager.getDefaultDisplay().getSize(screenSize);
+            min = Math.min(min, screenSize.x * 2 / 3);
+
+            options.inSampleSize = ImageUtil.calculateInSampleSize(options, min, min);
+            options.inScaled = true;
+            options.inDensity = options.outWidth;
+            options.inTargetDensity = min * options.inSampleSize;
+            options.inPreferredConfig = Bitmap.Config.RGB_565;
+
+            options.inJustDecodeBounds = false;
+            this.bitmap = BitmapFactory.decodeFile(path, options);
+        } catch (IOException e) {
+            e.printStackTrace();
+            this.bitmap = original;
+        } catch (NullPointerException e) {
+            e.printStackTrace();
+        }
+        setBitmap(this.bitmap);
+    }
+
+    private void setBitmap(Bitmap bitmap) {
+        this.bitmap = bitmap;
+        matrix.reset();
+        centerImage(getWidth(), getHeight());
+        rotation = 0;
+        invalidate();
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        centerImage(w, h);
+        invalidate();
+    }
+
+    public Bitmap crop(Rect frame) {
+        float scale = getScale();
+
+        float[] src = new float[] {frame.left, frame.top};
+        float[] desc = new float[] {0, 0};
+
+        Matrix invertedMatrix = new Matrix();
+        this.matrix.invert(invertedMatrix);
+        invertedMatrix.mapPoints(desc, src);
+
+        Matrix matrix = new Matrix();
+
+        int width = (int) (frame.width() / scale);
+        int height = (int) (frame.height() / scale);
+        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
+        Canvas canvas = new Canvas(bitmap);
+
+        Bitmap originalBitmap = this.bitmap;
+        matrix.postTranslate(-desc[0], -desc[1]);
+        canvas.drawBitmap(originalBitmap, matrix, null);
+        return bitmap;
+    }
+
+    public void setMinimumScale(float setMinimumScale) {
+        this.setMinimumScale = setMinimumScale;
+    }
+
+    public void setMaximumScale(float maximumScale) {
+        this.maximumScale = maximumScale;
+    }
+
+    private float setMinimumScale = 0.2f;
+    private float maximumScale = 4.0f;
+
+    private float[] matrixArray = new float[9];
+    private Matrix matrix = new Matrix();
+    private Bitmap bitmap;
+
+    private GestureDetector gestureDetector;
+
+    private ScaleGestureDetector scaleGestureDetector;
+    private ScaleGestureDetector.OnScaleGestureListener onScaleGestureListener =
+            new ScaleGestureDetector.OnScaleGestureListener() {
+                @Override
+                public boolean onScale(ScaleGestureDetector detector) {
+                    scale(detector);
+                    return true;
+                }
+
+                @Override
+                public boolean onScaleBegin(ScaleGestureDetector detector) {
+                    return true;
+                }
+
+                @Override
+                public void onScaleEnd(ScaleGestureDetector detector) {
+                    float scale = detector.getScaleFactor();
+                    matrix.postScale(scale, scale);
+                    invalidate();
+                }
+            };
+
+    private void init() {
+        scaleGestureDetector = new ScaleGestureDetector(getContext(), onScaleGestureListener);
+        gestureDetector = new GestureDetector(getContext(), new GestureDetector.OnGestureListener() {
+            @Override
+            public boolean onDown(MotionEvent e) {
+                return true;
+            }
+
+            @Override
+            public void onShowPress(MotionEvent e) {
+            }
+
+            @Override
+            public boolean onSingleTapUp(MotionEvent e) {
+                return false;
+            }
+
+            @Override
+            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+                translate(distanceX, distanceY);
+                return true;
+            }
+
+            @Override
+            public void onLongPress(MotionEvent e) {
+            }
+
+            @Override
+            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+                return false;
+            }
+        });
+    }
+
+    int rotation = 0;
+
+    public void rotate(int degrees) {
+        Matrix matrix = new Matrix();
+
+        int dx = this.bitmap.getWidth() / 2;
+        int dy = this.bitmap.getHeight() / 2;
+
+        matrix.postTranslate(-dx, -dy);
+        matrix.postRotate(degrees);
+        matrix.postTranslate(dy, dx);
+        Bitmap scaledBitmap = this.bitmap;
+        Bitmap rotatedBitmap = Bitmap.createBitmap(scaledBitmap.getHeight(), scaledBitmap.getWidth(),
+                Bitmap.Config.RGB_565);
+        Canvas canvas = new Canvas(rotatedBitmap);
+        canvas.drawBitmap(this.bitmap, matrix, null);
+        this.bitmap.recycle();
+        this.bitmap = rotatedBitmap;
+        centerImage(getWidth(), getHeight());
+        invalidate();
+    }
+
+    private void translate(float distanceX, float distanceY) {
+        matrix.getValues(matrixArray);
+        float left = matrixArray[Matrix.MTRANS_X];
+        float top = matrixArray[Matrix.MTRANS_Y];
+
+        Rect bound = getRestrictedBound();
+        if (bound != null) {
+            float scale = getScale();
+            float right = left + (int) (bitmap.getWidth() / scale);
+            float bottom = top + (int) (bitmap.getHeight() / scale);
+
+            if (left - distanceX > bound.left) {
+                distanceX = left - bound.left;
+            }
+            if (top - distanceY > bound.top) {
+                distanceY = top - bound.top;
+            }
+
+            if (distanceX > 0) {
+                if (right - distanceX < bound.right) {
+                    distanceX = right - bound.right;
+                }
+            }
+            if (distanceY > 0) {
+                if (bottom - distanceY < bound.bottom) {
+                    distanceY = bottom - bound.bottom;
+                }
+            }
+        }
+        matrix.postTranslate(-distanceX, -distanceY);
+        invalidate();
+    }
+
+    private void scale(ScaleGestureDetector detector) {
+        float scale = detector.getScaleFactor();
+        float currentScale = getScale();
+        if (currentScale * scale < setMinimumScale) {
+            scale = setMinimumScale / currentScale;
+        }
+        if (currentScale * scale > maximumScale) {
+            scale = maximumScale / currentScale;
+        }
+        matrix.postScale(scale, scale, detector.getFocusX(), detector.getFocusY());
+        invalidate();
+    }
+
+    private void centerImage(int width, int height) {
+        if (width <= 0 || height <= 0 || bitmap == null) {
+            return;
+        }
+        float widthRatio = 1.0f * height / this.bitmap.getHeight();
+        float heightRatio = 1.0f * width / this.bitmap.getWidth();
+
+        float ratio = Math.min(widthRatio, heightRatio);
+
+        float dx = (width - this.bitmap.getWidth()) / 2;
+        float dy = (height - this.bitmap.getHeight()) / 2;
+        matrix.setTranslate(0, 0);
+        matrix.setScale(ratio, ratio, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
+        matrix.postTranslate(dx, dy);
+        invalidate();
+    }
+
+    private float getScale() {
+        matrix.getValues(matrixArray);
+        float scale = matrixArray[Matrix.MSCALE_X];
+        if (Math.abs(scale) <= 0.1) {
+            scale = matrixArray[Matrix.MSKEW_X];
+        }
+        return Math.abs(scale);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        if (bitmap != null) {
+            canvas.drawBitmap(bitmap, matrix, null);
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        boolean result = scaleGestureDetector.onTouchEvent(event);
+        result = gestureDetector.onTouchEvent(event) || result;
+        return result || super.onTouchEvent(event);
+    }
+
+    private Rect restrictBound;
+
+    private Rect getRestrictedBound() {
+        return restrictBound;
+    }
+
+    public void setRestrictBound(Rect rect) {
+        this.restrictBound = rect;
+    }
+}

+ 289 - 0
scancardlibrary/src/main/java/com/riso/scancardlibrary/cutviews/views/FrameOverlayView.java

@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.riso.scancardlibrary.cutviews.views;
+
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.riso.scancardlibrary.cutviews.util.DimensionUtil;
+
+
+public class FrameOverlayView extends View {
+
+    interface OnFrameChangeListener {
+        void onFrameChange(RectF newFrame);
+    }
+
+    public Rect getFrameRect() {
+        Rect rect = new Rect();
+        rect.left = (int) frameRect.left;
+        rect.top = (int) frameRect.top;
+        rect.right = (int) frameRect.right;
+        rect.bottom = (int) frameRect.bottom;
+        return rect;
+    }
+
+    public FrameOverlayView(Context context) {
+        super(context);
+        init();
+    }
+
+    public FrameOverlayView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    public FrameOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init();
+    }
+
+    private GestureDetector.SimpleOnGestureListener onGestureListener = new GestureDetector.SimpleOnGestureListener() {
+        @Override
+        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+            translate(distanceX, distanceY);
+            return true;
+        }
+
+    };
+
+    private static final int CORNER_LEFT_TOP = 1;
+    private static final int CORNER_RIGHT_TOP = 2;
+    private static final int CORNER_RIGHT_BOTTOM = 3;
+    private static final int CORNER_LEFT_BOTTOM = 4;
+
+    private int currentCorner = -1;
+    int margin = 20;
+    int cornerLength = 100;
+    int cornerLineWidth = 6;
+
+    private int maskColor = Color.argb(180, 0, 0, 0);
+
+    private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private Paint eraser = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private GestureDetector gestureDetector;
+    private RectF touchRect = new RectF();
+    private RectF frameRect = new RectF();
+
+    private OnFrameChangeListener onFrameChangeListener;
+
+    {
+        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+        paint.setColor(Color.WHITE);
+        paint.setStyle(Paint.Style.STROKE);
+        paint.setStrokeWidth(6);
+
+        eraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+    }
+
+    public void setOnFrameChangeListener(OnFrameChangeListener onFrameChangeListener) {
+        this.onFrameChangeListener = onFrameChangeListener;
+    }
+
+    private void init() {
+        gestureDetector = new GestureDetector(getContext(), onGestureListener);
+        cornerLength = DimensionUtil.dpToPx(18);
+        cornerLineWidth = DimensionUtil.dpToPx(3);
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        resetFrameRect(w, h);
+    }
+
+    private void resetFrameRect(int w, int h) {
+        if (shapeType == 1) {
+            frameRect.left = (int) (w * 0.05);
+            frameRect.top = (int) (h * 0.25);
+        } else {
+            frameRect.left = (int) (w * 0.2);
+            frameRect.top = (int) (h * 0.2);
+        }
+        frameRect.right = w - frameRect.left;
+        frameRect.bottom = h - frameRect.top;
+    }
+
+    private int shapeType = 0;
+
+    public void setTypeWide() {
+        shapeType = 1;
+    }
+
+    private void translate(float x, float y) {
+        if (x > 0) {
+            // moving left;
+            if (frameRect.left - x < margin) {
+                x = frameRect.left - margin;
+            }
+        } else {
+            if (frameRect.right - x > getWidth() - margin) {
+                x = frameRect.right - getWidth() + margin;
+            }
+        }
+
+        if (y > 0) {
+            if (frameRect.top - y < margin) {
+                y = frameRect.top - margin;
+            }
+        } else {
+            if (frameRect.bottom - y > getHeight() - margin) {
+                y = frameRect.bottom - getHeight() + margin;
+            }
+        }
+        frameRect.offset(-x, -y);
+        invalidate();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        canvas.drawColor(maskColor);
+
+        paint.setStrokeWidth(DimensionUtil.dpToPx(1));
+        canvas.drawRect(frameRect, paint);
+        canvas.drawRect(frameRect, eraser);
+        drawCorners(canvas);
+    }
+
+    private void drawCorners(Canvas canvas) {
+        paint.setStrokeWidth(cornerLineWidth);
+        // left top
+        drawLine(canvas, frameRect.left - cornerLineWidth / 2, frameRect.top, cornerLength, 0);
+        drawLine(canvas, frameRect.left, frameRect.top, 0, cornerLength);
+
+        // right top
+        drawLine(canvas, frameRect.right + cornerLineWidth / 2, frameRect.top, -cornerLength, 0);
+        drawLine(canvas, frameRect.right, frameRect.top, 0, cornerLength);
+
+        // right bottom
+        drawLine(canvas, frameRect.right, frameRect.bottom, 0, -cornerLength);
+        drawLine(canvas, frameRect.right + cornerLineWidth / 2, frameRect.bottom, -cornerLength, 0);
+
+        // left bottom
+        drawLine(canvas, frameRect.left - cornerLineWidth / 2, frameRect.bottom, cornerLength, 0);
+        drawLine(canvas, frameRect.left, frameRect.bottom, 0, -cornerLength);
+    }
+
+    private void drawLine(Canvas canvas, float x, float y, int dx, int dy) {
+        canvas.drawLine(x, y, x + dx, y + dy, paint);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        boolean result = handleDown(event);
+        float ex = 60;
+        RectF rectExtend = new RectF(frameRect.left - ex, frameRect.top - ex,
+                frameRect.right + ex, frameRect.bottom + ex);
+        if (!result) {
+            if (rectExtend.contains(event.getX(), event.getY())) {
+                gestureDetector.onTouchEvent(event);
+                return true;
+            }
+        }
+        return result;
+    }
+
+    private boolean handleDown(MotionEvent event) {
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                currentCorner = -1;
+                break;
+            case MotionEvent.ACTION_DOWN: {
+                float radius = cornerLength;
+                touchRect.set(event.getX() - radius, event.getY() - radius, event.getX() + radius,
+                        event.getY() + radius);
+                if (touchRect.contains(frameRect.left, frameRect.top)) {
+                    currentCorner = CORNER_LEFT_TOP;
+                    return true;
+                }
+
+                if (touchRect.contains(frameRect.right, frameRect.top)) {
+                    currentCorner = CORNER_RIGHT_TOP;
+                    return true;
+                }
+
+                if (touchRect.contains(frameRect.right, frameRect.bottom)) {
+                    currentCorner = CORNER_RIGHT_BOTTOM;
+                    return true;
+                }
+
+                if (touchRect.contains(frameRect.left, frameRect.bottom)) {
+                    currentCorner = CORNER_LEFT_BOTTOM;
+                    return true;
+                }
+                return false;
+            }
+            case MotionEvent.ACTION_MOVE:
+                return handleScale(event);
+            default:
+
+        }
+        return false;
+    }
+
+    private boolean handleScale(MotionEvent event) {
+        switch (currentCorner) {
+            case CORNER_LEFT_TOP:
+                scaleTo(event.getX(), event.getY(), frameRect.right, frameRect.bottom);
+                return true;
+            case CORNER_RIGHT_TOP:
+                scaleTo(frameRect.left, event.getY(), event.getX(), frameRect.bottom);
+                return true;
+            case CORNER_RIGHT_BOTTOM:
+                scaleTo(frameRect.left, frameRect.top, event.getX(), event.getY());
+                return true;
+            case CORNER_LEFT_BOTTOM:
+                scaleTo(event.getX(), frameRect.top, frameRect.right, event.getY());
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    private void scaleTo(float left, float top, float right, float bottom) {
+        if (bottom - top < getMinimumFrameHeight()) {
+            top = frameRect.top;
+            bottom = frameRect.bottom;
+        }
+        if (right - left < getMinimumFrameWidth()) {
+            left = frameRect.left;
+            right = frameRect.right;
+        }
+        left = Math.max(margin, left);
+        top = Math.max(margin, top);
+        right = Math.min(getWidth() - margin, right);
+        bottom = Math.min(getHeight() - margin, bottom);
+
+        frameRect.set(left, top, right, bottom);
+        invalidate();
+    }
+
+    private void notifyFrameChange() {
+        if (onFrameChangeListener != null) {
+            onFrameChangeListener.onFrameChange(frameRect);
+        }
+    }
+
+    private float getMinimumFrameWidth() {
+        return 2.4f * cornerLength;
+    }
+
+    private float getMinimumFrameHeight() {
+        return 2.4f * cornerLength;
+    }
+}

+ 246 - 0
scancardlibrary/src/main/java/com/riso/scancardlibrary/cutviews/views/MaskView.java

@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.riso.scancardlibrary.cutviews.views;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.os.Build;
+import android.support.annotation.IntDef;
+import android.support.annotation.RequiresApi;
+import android.util.AttributeSet;
+import android.view.View;
+
+import java.io.File;
+
+@SuppressWarnings("unused")
+public class MaskView extends View {
+
+    public static final int MASK_TYPE_NONE = 0;
+    public static final int MASK_TYPE_ID_CARD_FRONT = 1;
+    public static final int MASK_TYPE_ID_CARD_BACK = 2;
+    public static final int MASK_TYPE_BANK_CARD = 11;
+
+    @IntDef({MASK_TYPE_NONE, MASK_TYPE_ID_CARD_FRONT, MASK_TYPE_ID_CARD_BACK, MASK_TYPE_BANK_CARD})
+    @interface MaskType {
+
+    }
+
+    public void setLineColor(int lineColor) {
+        this.lineColor = lineColor;
+    }
+
+    public void setMaskColor(int maskColor) {
+        this.maskColor = maskColor;
+    }
+
+    private int lineColor = Color.WHITE;
+
+    private int maskType = MASK_TYPE_ID_CARD_FRONT;
+
+    private int maskColor = Color.argb(100, 0, 0, 0);
+
+    private Paint eraser = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private Paint pen = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+    private Rect frame = new Rect();
+
+    //private Drawable locatorDrawable;
+
+    public Rect getFrameRect() {
+        if (maskType == MASK_TYPE_NONE) {
+            return new Rect(0, 0, getWidth(), getHeight());
+        } else {
+            return new Rect(frame);
+        }
+
+    }
+
+    public Rect getFrameRectExtend() {
+        Rect rc = new Rect(frame);
+        int widthExtend = (int) ((frame.right - frame.left) * 0.02f);
+        int heightExtend = (int) ((frame.bottom - frame.top) * 0.02f);
+        rc.left -= widthExtend;
+        rc.right += widthExtend;
+        rc.top -= heightExtend;
+        rc.bottom += heightExtend;
+        return rc;
+    }
+
+    public void setMaskType(@MaskType int maskType) {
+        this.maskType = maskType;
+        /*switch (maskType) {
+            case MASK_TYPE_ID_CARD_FRONT:
+                locatorDrawable = ResourcesCompat.getDrawable(getResources(),
+                        R.drawable.bd_ocr_id_card_locator_front, null);
+                break;
+            case MASK_TYPE_ID_CARD_BACK:
+                locatorDrawable = ResourcesCompat.getDrawable(getResources(),
+                        R.drawable.bd_ocr_id_card_locator_back, null);
+                break;
+            case MASK_TYPE_BANK_CARD:
+                break;
+            case MASK_TYPE_NONE:
+            default:
+                break;
+        }*/
+        invalidate();
+    }
+
+    public int getMaskType() {
+        return maskType;
+    }
+
+
+    /**
+     * ORIENTATION_PORTRAIT = 0;
+     * ORIENTATION_HORIZONTAL = 90;
+     * ORIENTATION_INVERT = 270;
+     *
+     * @param orientation
+     */
+    public void setOrientation(int orientation) {
+    }
+
+    public MaskView(Context context) {
+        super(context);
+        init();
+    }
+
+    public MaskView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    public MaskView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init();
+    }
+
+    private void init() {
+        ///locatorDrawable = ResourcesCompat.getDrawable(getResources(), R.drawable.bd_ocr_id_card_locator_front, null);
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        if (w > 0 && h > 0) {
+            float ratio = h > w ? 0.9f : 0.72f;
+
+            int width = (int) (w * ratio);
+            int height = width * 400 / 620;
+
+            int left = (w - width) / 2;
+            int top = (h - height) / 2;
+            int right = width + left;
+            int bottom = height + top;
+
+            frame.left = left;
+            frame.top = top;
+            frame.right = right;
+            frame.bottom = bottom;
+
+        }
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        int width = frame.width();
+        int height = frame.height();
+
+        int left = frame.left;
+        int top = frame.top;
+        int right = frame.right;
+        int bottom = frame.bottom;
+
+        canvas.drawColor(maskColor);
+        fillRectRound(left, top, right, bottom, 30, 30, false);
+        canvas.drawPath(path, pen);
+        canvas.drawPath(path, eraser);
+
+        /*if (maskType == MASK_TYPE_ID_CARD_FRONT) {
+            locatorDrawable.setBounds(
+                    (int) (left + 601f / 1006 * width),
+                    (int) (top + (110f / 632) * height),
+                    (int) (left + (963f / 1006) * width),
+                    (int) (top + (476f / 632) * height));
+        } else if (maskType == MASK_TYPE_ID_CARD_BACK) {
+            locatorDrawable.setBounds(
+                    (int) (left + 51f / 1006 * width),
+                    (int) (top + (48f / 632) * height),
+                    (int) (left + (250f / 1006) * width),
+                    (int) (top + (262f / 632) * height));
+        }
+        if (locatorDrawable != null) {
+            locatorDrawable.draw(canvas);
+        }*/
+    }
+
+    private Path path = new Path();
+
+    private Path fillRectRound(float left, float top, float right, float bottom, float rx, float ry, boolean
+            conformToOriginalPost) {
+
+        path.reset();
+        if (rx < 0) {
+            rx = 0;
+        }
+        if (ry < 0) {
+            ry = 0;
+        }
+        float width = right - left;
+        float height = bottom - top;
+        if (rx > width / 2) {
+            rx = width / 2;
+        }
+        if (ry > height / 2) {
+            ry = height / 2;
+        }
+        float widthMinusCorners = (width - (2 * rx));
+        float heightMinusCorners = (height - (2 * ry));
+
+        path.moveTo(right, top + ry);
+        path.rQuadTo(0, -ry, -rx, -ry);
+        path.rLineTo(-widthMinusCorners, 0);
+        path.rQuadTo(-rx, 0, -rx, ry);
+        path.rLineTo(0, heightMinusCorners);
+
+        if (conformToOriginalPost) {
+            path.rLineTo(0, ry);
+            path.rLineTo(width, 0);
+            path.rLineTo(0, -ry);
+        } else {
+            path.rQuadTo(0, ry, rx, ry);
+            path.rLineTo(widthMinusCorners, 0);
+            path.rQuadTo(rx, 0, rx, -ry);
+        }
+
+        path.rLineTo(0, -heightMinusCorners);
+        path.close();
+        return path;
+    }
+
+    {
+        // 硬件加速不支持,图层混合。
+        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+
+        pen.setColor(Color.WHITE);
+        pen.setStyle(Paint.Style.STROKE);
+        pen.setStrokeWidth(6);
+
+        eraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+    }
+
+    private void capture(File file) {
+
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/com/riso/scancardlibrary/cutviews/views/OCRFrameLayout.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.riso.scancardlibrary.cutviews.views;
+
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class OCRFrameLayout extends ViewGroup {
+
+    public OCRFrameLayout(Context context) {
+        super(context);
+    }
+
+    public OCRFrameLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        parseAttrs(attrs);
+    }
+
+    public OCRFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        parseAttrs(attrs);
+    }
+
+    private void parseAttrs(AttributeSet attrs) {
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View view = getChildAt(i);
+            view.layout(l, t, r, b);
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        int width;
+        int height;
+        int childCount = getChildCount();
+
+        width = MeasureSpec.getSize(widthMeasureSpec);
+        height = MeasureSpec.getSize(heightMeasureSpec);
+        for (int i = 0; i < childCount; i++) {
+            View view = getChildAt(i);
+            measureChild(view, MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec
+                    (height, MeasureSpec.EXACTLY));
+        }
+        setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec));
+    }
+}

+ 16 - 0
scancardlibrary/src/main/java/io/card/payment/CameraUnavailableException.java

@@ -0,0 +1,16 @@
+package io.card.payment;
+
+/* CameraUnavailableException.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+/**
+ * This exception is thrown when the camera cannot be opened for an unexpected reason. It should NOT
+ * be used if the hardware is known to be unacceptable.
+ *
+ * @version 1.0
+ */
+public class CameraUnavailableException extends RuntimeException {
+    private static final long serialVersionUID = 1L;
+
+}

+ 1286 - 0
scancardlibrary/src/main/java/io/card/payment/CardIOActivity.java

@@ -0,0 +1,1286 @@
+package io.card.payment;
+
+/* CardIOActivity.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+import android.Manifest;
+import android.app.ActionBar;
+import android.app.Activity;
+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.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Vibrator;
+import android.provider.MediaStore;
+import android.support.v4.app.ActivityCompat;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.Animation;
+import android.view.animation.RotateAnimation;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.Toast;
+
+import com.riso.scancardlibrary.BuildConfig;
+import com.riso.scancardlibrary.R;
+import com.riso.scancardlibrary.cutviews.views.CropView;
+import com.riso.scancardlibrary.cutviews.views.FrameOverlayView;
+import com.riso.scancardlibrary.cutviews.views.MaskView;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+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 com.riso.scancardlibrary.payment.i18n.StringKey;
+ * import com.riso.scancardlibrary.payment.i18n.SupportedLocale;
+ * This is the entry point {@link Activity} for a card.io client to use <a
+ * href="https://card.io">card.io</a>.
+ *
+ * @author 王黎聪
+ * @version 1.0
+ */
+public class CardIOActivity extends Activity implements View.OnClickListener {
+    /**
+     * Boolean extra. Optional. Defaults to <code>false</code>. 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 <code>false</code>. If
+     * set to <code>false</code>, expiry information will not be required.
+     * 是否 显示银行卡 过期时间   默认false
+     */
+    public static final String EXTRA_REQUIRE_EXPIRY = "io.card.payment.requireExpiry";
+
+    /**
+     * Boolean extra. Optional. Defaults to <code>true</code>. If
+     * set to <code>true</code>, and {@link #EXTRA_REQUIRE_EXPIRY} is <code>true</code>,
+     * an attempt to extract the expiry from the card image will be made.
+     * 是否 提取 过期时间  默认true
+     */
+    public static final String EXTRA_SCAN_EXPIRY = "io.card.payment.scanExpiry";
+
+    /**
+     * Integer extra. Optional. Defaults to <code>-1</code> (no blur). Privacy feature.
+     * How many of the Card number digits NOT to blur on the resulting image.
+     * Setting it to <code>4</code> will blur all digits except the last four.
+     * 是否 设置卡号 模糊  默认 4位   默认 false
+     */
+    public static final String EXTRA_UNBLUR_DIGITS = "io.card.payment.unblurDigits";
+
+    /**
+     * Boolean extra. Optional. Defaults to <code>false</code>. If set, the user will be prompted
+     * for the card CVV.
+     * 是否 提示用户 CVV 默认 false
+     */
+    public static final String EXTRA_REQUIRE_CVV = "io.card.payment.requireCVV";
+
+    /**
+     * Boolean extra. Optional. Defaults to <code>false</code>. If set, the user will be prompted
+     * for the card billing postal code.
+     * 是否 信用卡帐单的邮政编码  默认false
+     */
+    public static final String EXTRA_REQUIRE_POSTAL_CODE = "io.card.payment.requirePostalCode";
+
+    /**
+     * Boolean extra. Optional. Defaults to <code>false</code>. If set, the postal code will only collect numeric
+     * input. Set this if you know the <a href="https://en.wikipedia.org/wiki/Postal_code">expected country's
+     * postal code</a> has only numeric postal codes.
+     * 是否 信用卡帐单的邮政编码  默认false
+     */
+    public static final String EXTRA_RESTRICT_POSTAL_CODE_TO_NUMERIC_ONLY = "io.card.payment.restrictPostalCodeToNumericOnly";
+
+    /**
+     * Boolean extra. Optional. Defaults to <code>false</code>. If set, the user will be prompted
+     * for the cardholder name.
+     * 是否 将提示用户 持卡人的名字。  默认false
+     */
+    public static final String EXTRA_REQUIRE_CARDHOLDER_NAME = "io.card.payment.requireCardholderName";
+
+    /**
+     * Boolean extra. Optional. Defaults to <code>false</code>. If set, the card.io logo will be
+     * shown instead of the PayPal logo.
+     * 是否显示 用户 标志  默认 false
+     */
+    public static final String EXTRA_USE_CARDIO_LOGO = "io.card.payment.useCardIOLogo";
+
+    /**
+     * Parcelable extra containing {@link CreditCard}. The data intent returned to your {@link Activity}'s
+     * {@link Activity#onActivityResult(int, int, Intent)} will contain this extra if the resultCode is
+     * {@link #RESULT_CARD_INFO}.
+     */
+    public static final String EXTRA_SCAN_RESULT = "io.card.payment.scanResult";
+
+    /**
+     * Boolean extra indicating card was not scanned.
+     */
+    private static final String EXTRA_MANUAL_ENTRY_RESULT = "io.card.payment.manualEntryScanResult";
+
+    /**
+     * Boolean extra. Optional. Defaults to <code>false</code>. Removes the keyboard button from the
+     * scan screen.
+     * <br><br>
+     * If scanning is unavailable, the {@link Activity} result will be {@link #RESULT_SCAN_NOT_AVAILABLE}.
+     * 是否 从键盘上删除键盘按钮 扫描屏幕。   默认 false
+     */
+    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.
+     * <br><br>
+     * Can be specified as a language code ("en", "fr", "zh-Hans", etc.) or as a locale ("en_AU",
+     * "fr_FR", "zh-Hant_TW", etc.).
+     * <br><br>
+     * If the library does not contain localized strings for a specified locale, then will fall back
+     * to the language. E.g., "es_CO" -&gt; "es".
+     * <br><br>
+     * If the library does not contain localized strings for a specified language, then will fall
+     * back to American English.
+     * <br><br>
+     * 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".
+     * <br><br>
+     * These localizations are currently included:
+     * <br><br>
+     * 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. If this value is set to <code>true</code> the user will not be prompted to
+     * confirm their card number after processing.
+     * 卡号相关
+     */
+    public static final String EXTRA_SUPPRESS_CONFIRMATION = "io.card.payment.suppressConfirmation";
+
+    /**
+     * Boolean extra. Optional. Defaults to <code>false</code>. When set to <code>true</code> 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
+     * <code>true</code> the {@link CardIOActivity} will finish with a {@link #RESULT_SCAN_SUPPRESSED} result code.
+     * 一旦一张卡片图像被捕捉了,但在它之前
+     * 处理后,该值将决定是否继续按常规进行处理。如果该值为
+     */
+    public static final String EXTRA_SUPPRESS_SCAN = "io.card.payment.suppressScan";
+
+    /**
+     * 返回照片的质量
+     */
+    public static final String EXTRA_CRAD_QUALITY = "io.card.quality";
+
+    /**
+     * String extra. If {@link #EXTRA_RETURN_CARD_IMAGE} is set to <code>true</code>, the data intent passed to your
+     * {@link Activity} will have the card image stored as a JPEG formatted byte array in this extra.
+     * 额外的字符串。如果将@link extrareturncardimage设置为true/code,则将数据传递给您
+     * { @link android.app。活动将把卡片图像存储为一个JPEG格式的字节数组。
+     */
+    public static final String EXTRA_CAPTURED_CARD_IMAGE = "io.card.payment.capturedCardImage";
+
+    /**
+     * Boolean extra. Optional. If this value is set to <code>true</code> the card image will be passed as an
+     * extra in the data intent that is returned to your {@link Activity} using the
+     * {@link #EXTRA_CAPTURED_CARD_IMAGE} key.
+     */
+    public static final String EXTRA_RETURN_CARD_IMAGE = "io.card.payment.returnCardImage";
+
+    /**
+     * Integer extra. Optional. If this value is provided the view will be inflated and will overlay
+     * the camera during the scan process. The integer value must be the id of a valid layout
+     * resource.
+     * <p>
+     * 设置自定义布局
+     */
+    public static final String EXTRA_SCAN_OVERLAY_LAYOUT_ID = "io.card.payment.scanOverlayLayoutId";
+
+    /**
+     * Boolean extra. Optional. Use the PayPal icon in the ActionBar.
+     */
+    public static final String EXTRA_USE_PAYPAL_ACTIONBAR_ICON =
+            "io.card.payment.intentSenderIsPayPal";
+
+    /**
+     * Boolean extra. Optional. If this value is set to <code>true</code>, and the application has a theme,
+     * the theme for the card.io {@link 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.   仅用于测试。
+     */
+    static final String PRIVATE_EXTRA_CAMERA_BYPASS_TEST_MODE = "io.card.payment.cameraBypassTestMode";
+
+    private 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.
+     * <br><br>
+     * This error can be avoided in normal situations by checking
+     * {@link #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++;
+
+    /**
+     * result code indicating that confirmation was suppressed.
+     */
+    public static final int RESULT_CONFIRMATION_SUPPRESSED = lastResult++;
+
+    private static final String TAG = CardIOActivity.class.getSimpleName();
+
+    private static final int DEGREE_DELTA = 15;
+
+    private static final int ORIENTATION_PORTRAIT = 1;
+    private static final int ORIENTATION_PORTRAIT_UPSIDE_DOWN = 2;
+    private static final int ORIENTATION_LANDSCAPE_RIGHT = 3;
+    private static final int ORIENTATION_LANDSCAPE_LEFT = 4;
+
+    private static final int FRAME_ID = 1;
+    private static final int UIBAR_ID = 2;
+    private static final int KEY_BTN_ID = 3;
+
+    private static final String BUNDLE_WAITING_FOR_PERMISSION = "io.card.payment.waitingForPermission";
+
+    private static final float UIBAR_VERTICAL_MARGIN_DP = 15.0f;
+
+    private static final long[] VIBRATE_PATTERN = {0, 70, 10, 40};
+
+    private static final int TOAST_OFFSET_Y = -75;
+
+    private static final int DATA_ENTRY_REQUEST_ID = 10;
+    private static final int PERMISSION_REQUEST_ID = 11;
+
+    private static final int PERMISSIONS_EXTERNAL_STORAGE = 801;
+
+    /**
+     * 展示 闪光灯 和文案
+     */
+    private OverlayView mOverlay;
+    //横屏监听
+    //private OrientationEventListener orientationListener;
+
+    // (预览)由扫描仪来访问。不是最好的做法。
+    Preview mPreview;
+
+    private CreditCard mDetectedCard;
+    private Rect mGuideFrame;
+    private int mLastDegrees;
+    private int mFrameOrientation;
+    private boolean suppressManualEntry;
+    private boolean mDetectOnly;
+    private LinearLayout customOverlayLayout;
+    private boolean waitingForPermission;
+
+    private RelativeLayout mUIBar;
+    private boolean useApplicationTheme;
+
+    private CardScanner mCardScanner;
+
+    private boolean manualEntryFallbackOrForced = false;
+
+
+    private int quality = 100;
+
+
+    /**
+     * Static variable for the decorated card image. This is ugly, but works. Parceling and
+     * unparceling card image data to pass to the next {@link 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.
+     */
+    private View include_crop;
+    private CropView cropView;
+    private MaskView cropMaskView;
+    private FrameOverlayView overlayView;
+
+
+    public static void activityStart(Activity context, int guideColor, int bitmapQuality) {
+        Intent intent = new Intent(context, CardIOActivity.class)
+                .putExtra(CardIOActivity.EXTRA_NO_CAMERA, false)
+                .putExtra(CardIOActivity.EXTRA_REQUIRE_EXPIRY, false)
+                .putExtra(CardIOActivity.EXTRA_SCAN_EXPIRY, false)
+                .putExtra(CardIOActivity.EXTRA_HIDE_CARDIO_LOGO, true)
+                .putExtra(CardIOActivity.EXTRA_LANGUAGE_OR_LOCALE, "zh-Hans")
+                .putExtra(CardIOActivity.EXTRA_USE_PAYPAL_ACTIONBAR_ICON, false)
+                .putExtra(CardIOActivity.EXTRA_KEEP_APPLICATION_THEME, true)
+                .putExtra(CardIOActivity.EXTRA_GUIDE_COLOR, guideColor)
+                .putExtra(CardIOActivity.EXTRA_SUPPRESS_SCAN, true)
+                //设置图片质量
+                .putExtra(CardIOActivity.EXTRA_CRAD_QUALITY, bitmapQuality)
+                .putExtra(CardIOActivity.EXTRA_RETURN_CARD_IMAGE, true)
+                .putExtra(CardIOActivity.EXTRA_UNBLUR_DIGITS, 4);
+        context.startActivityForResult(intent, 100);
+    }
+
+
+    // ------------------------------------------------------------------------
+    // ACTIVITY LIFECYCLE
+    // ------------------------------------------------------------------------
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        final Intent clientData = getIntent();
+        //是否控制主题
+        useApplicationTheme = clientData.getBooleanExtra(CardIOActivity.EXTRA_KEEP_APPLICATION_THEME, false);
+        //设置主题
+        ActivityHelper.setActivityTheme(this, useApplicationTheme);
+        //设置语言
+        LocalizedStrings.setLanguage(clientData);
+
+        // Validate app's manifest is correct. 这个属性是 裁剪 关闭界面
+        mDetectOnly = clientData.getBooleanExtra(EXTRA_SUPPRESS_SCAN, false);
+
+        ResolveInfo resolveInfo;
+        String errorMsg;
+
+        // Check for DataEntryActivity's portrait orientation
+
+        // Check for CardIOActivity's orientation config in manifest
+        resolveInfo = getPackageManager().resolveActivity(clientData,
+                PackageManager.MATCH_DEFAULT_ONLY);
+        errorMsg = Util.manifestHasConfigChange(resolveInfo, CardIOActivity.class);
+        if (errorMsg != null) {
+            throw new RuntimeException(errorMsg); // Throw the actual exception from this class, for
+            // clarity.
+        }
+        //是否 从键盘上删除键盘按钮 扫描屏幕
+        suppressManualEntry = clientData.getBooleanExtra(EXTRA_SUPPRESS_MANUAL_ENTRY, false);
+        quality = clientData.getIntExtra(EXTRA_CRAD_QUALITY, 100);
+
+        if (savedInstanceState != null) {
+            waitingForPermission = savedInstanceState.getBoolean(BUNDLE_WAITING_FOR_PERMISSION);
+        }
+
+        if (clientData.getBooleanExtra(EXTRA_NO_CAMERA, false)) {
+            manualEntryFallbackOrForced = true;
+        } else if (!CardScanner.processorSupported()) {
+            manualEntryFallbackOrForced = true;
+        } else {
+            try {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                    if (!waitingForPermission) {
+                        if (checkSelfPermission(Manifest.permission.CAMERA) ==
+                                PackageManager.PERMISSION_DENIED) {
+                            String[] permissions = {Manifest.permission.CAMERA};
+                            waitingForPermission = true;
+                            requestPermissions(permissions, PERMISSION_REQUEST_ID);
+                            //finish();
+                        } else {
+                            checkCamera();
+                            android23AndAboveHandleCamera();
+                        }
+                    }
+                } else {
+                    checkCamera();
+                    android22AndBelowHandleCamera();
+                }
+            } catch (Exception e) {
+                handleGeneralExceptionError(e);
+            }
+        }
+
+    }
+
+    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
+            requestWindowFeature(Window.FEATURE_NO_TITLE);
+
+            showCameraScannerOverlay();
+        }
+    }
+
+    private void finishIfSuppressManualEntry() {
+        if (suppressManualEntry) {
+            setResultAndFinish(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(this, localizedError, Toast.LENGTH_LONG);
+            toast.setGravity(Gravity.CENTER, 0, TOAST_OFFSET_Y);
+            toast.show();
+            manualEntryFallbackOrForced = true;
+        }
+    }
+
+    public void showCameraScannerOverlay() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+            View decorView = getWindow().getDecorView();
+            // Hide the status bar.
+            int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
+            decorView.setSystemUiVisibility(uiOptions);
+            // Remember that you should never show the action bar if the
+            // status bar is hidden, so hide that too if necessary.
+            ActionBar actionBar = getActionBar();
+            if (null != actionBar) {
+                actionBar.hide();
+            }
+        }
+
+        try {
+            mGuideFrame = new Rect();
+
+            mFrameOrientation = ORIENTATION_PORTRAIT;
+
+            if (getIntent().getBooleanExtra(PRIVATE_EXTRA_CAMERA_BYPASS_TEST_MODE, false)) {
+                if (!this.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(new Object[]{this, mFrameOrientation});
+            } else {
+                mCardScanner = new CardScanner(this, mFrameOrientation);
+            }
+            mCardScanner.prepareScanner();
+            //设置UI
+            setMyPreviewLayout();
+            //横屏监听
+            /*orientationListener = new OrientationEventListener(this,
+                    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(this, localizedError, Toast.LENGTH_LONG);
+        toast.setGravity(Gravity.CENTER, 0, TOAST_OFFSET_Y);
+        toast.show();
+        manualEntryFallbackOrForced = true;
+    }
+
+    /**
+     * 横竖屏
+     *
+     * @param orientation 角度
+     */
+    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 < DEGREE_DELTA || orientation > 360 - DEGREE_DELTA) {
+            degrees = 0;
+            mFrameOrientation = ORIENTATION_PORTRAIT;
+        } else if (orientation > 90 - DEGREE_DELTA && orientation < 90 + DEGREE_DELTA) {
+            degrees = 90;
+            mFrameOrientation = ORIENTATION_LANDSCAPE_LEFT;
+        } else if (orientation > 180 - DEGREE_DELTA && orientation < 180 + DEGREE_DELTA) {
+            degrees = 180;
+            mFrameOrientation = ORIENTATION_PORTRAIT_UPSIDE_DOWN;
+        } else if (orientation > 270 - DEGREE_DELTA && orientation < 270 + DEGREE_DELTA) {
+            degrees = 270;
+            mFrameOrientation = ORIENTATION_LANDSCAPE_RIGHT;
+        }
+        if (degrees >= 0 && degrees != mLastDegrees) {
+            mCardScanner.setDeviceOrientation(mFrameOrientation);
+            setDeviceDegrees(degrees);
+            if (degrees == 90) {
+                rotateCustomOverlay(270);
+            } else if (degrees == 270) {
+                rotateCustomOverlay(90);
+            } else {
+                rotateCustomOverlay(degrees);
+            }
+        }
+    }
+
+    /**
+     * Suspend/resume camera preview as part of the {@link Activity} life cycle (side note: we reuse the
+     * same buffer for preview callbacks to greatly reduce the amount of required GC).
+     */
+    @Override
+    protected void onResume() {
+        super.onResume();
+
+        if (!waitingForPermission) {
+            if (manualEntryFallbackOrForced) {
+                if (suppressManualEntry) {
+                    finishIfSuppressManualEntry();
+                    return;
+                } else {
+                    nextActivity();
+                    return;
+                }
+            }
+
+            Util.logNativeMemoryStats();
+
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+            ActivityHelper.setFlagSecure(this);
+
+            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+            //激活横屏监听
+            //orientationListener.enable();
+
+            if (!restartPreview()) {
+                StringKey error = StringKey.ERROR_CAMERA_UNEXPECTED_FAIL;
+                showErrorMessage(LocalizedStrings.getString(error));
+                nextActivity();
+            } else {
+                // Turn flash off
+                setFlashOn(false);
+            }
+
+            doOrientationChange(mLastDegrees);
+        }
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+
+        outState.putBoolean(BUNDLE_WAITING_FOR_PERMISSION, waitingForPermission);
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        //停止横屏监听
+        /*if (orientationListener != null) {
+            orientationListener.disable();
+        }*/
+        setFlashOn(false);
+
+        if (mCardScanner != null) {
+            mCardScanner.pauseScanning();
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        mOverlay = null;
+        //停止横屏监听
+        /*if (orientationListener != null) {
+            orientationListener.disable();
+        }*/
+        setFlashOn(false);
+
+        if (mCardScanner != null) {
+            mCardScanner.endScanning();
+            mCardScanner = null;
+        }
+
+        super.onDestroy();
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String permissions[],
+                                           int[] grantResults) {
+        if (requestCode == PERMISSION_REQUEST_ID) {
+            waitingForPermission = false;
+            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                showCameraScannerOverlay();
+                onResume();
+            } else {
+                // show manual entry - handled in onResume()
+                manualEntryFallbackOrForced = true;
+                Toast.makeText(this, "请设置相机使用权限", Toast.LENGTH_LONG).show();
+            }
+        }
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+        if (requestCode == DATA_ENTRY_REQUEST_ID && null != data) {
+            //从相册回来 裁剪照片
+            if (resultCode == Activity.RESULT_OK) {
+
+                Uri uri = data.getData();
+                cropView.setFilePath(getRealPathFromURI(uri));
+                showCrop();
+            }
+        }
+    }
+
+    /**
+     * This {@link Activity} overrides back button handling to handle back presses properly given the
+     * various states this {@link Activity} can be in.
+     * <br><br>
+     * 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) {
+            if (null != include_crop && include_crop.getVisibility() == View.VISIBLE) {
+                onResume();
+                include_crop.setVisibility(View.GONE);
+            } else {
+                super.onBackPressed();
+            }
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // STATIC METHODS
+    // ------------------------------------------------------------------------
+
+    /**
+     * Determine if the device supports card scanning.
+     * <br><br>
+     * An ARM7 processor and Android SDK 8 or later are required. Additional checks for specific
+     * misbehaving devices may also be added.
+     *
+     * @return <code>true</code> if camera is supported. <code>false</code> 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();
+    }
+
+    /**
+     * Utility method for decoding card bitmap
+     *
+     * @param intent - intent received in {@link Activity#onActivityResult(int, int, Intent)}
+     * @return decoded bitmap or null
+     */
+    public static Bitmap getCapturedCardImage(Intent intent) {
+        if (intent == null || !intent.hasExtra(EXTRA_CAPTURED_CARD_IMAGE)) {
+            return null;
+        }
+
+        byte[] imageData = intent.getByteArrayExtra(EXTRA_CAPTURED_CARD_IMAGE);
+        ByteArrayInputStream inStream = new ByteArrayInputStream(imageData);
+        Bitmap result = BitmapFactory.decodeStream(inStream, null, new BitmapFactory.Options());
+        return result;
+    }
+
+    // end static
+
+    void onFirstFrame() {
+        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());
+    }
+
+    void onEdgeUpdate(DetectionInfo dInfo) {
+        mOverlay.setDetectionInfo(dInfo);
+    }
+
+    //拍照 并剪切
+    void onCardDetected(Bitmap detectedBitmap, DetectionInfo dInfo) {
+        try {
+            Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
+            vibrator.vibrate(VIBRATE_PATTERN, -1);
+        } catch (SecurityException e) {
+            Log.e(Util.PUBLIC_LOG_TAG,
+                    "Could not activate vibration feedback. Please add <uses-permission android:name=\"android.permission.VIBRATE\" /> to your application's manifest.");
+        } catch (Exception e) {
+            Log.w(Util.PUBLIC_LOG_TAG, "Exception while attempting to vibrate: ", e);
+        }
+
+        mCardScanner.pauseScanning();
+
+        if (dInfo.predicted()) {
+            mDetectedCard = dInfo.creditCard();
+            mOverlay.setDetectedCard(mDetectedCard);
+        }
+
+        float sf;
+        if (mFrameOrientation == ORIENTATION_PORTRAIT || mFrameOrientation == 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(detectedBitmap, 0, 0, detectedBitmap.getWidth(), detectedBitmap.getHeight(), m, false);
+        mOverlay.setBitmap(scaledCard);
+
+        if (mDetectOnly) {
+            //自动拍照 结果
+            if (mOverlay != null && mOverlay.getBitmap() != null) {
+                setIntent(mOverlay.getBitmap());
+            } else {
+                finish();
+            }
+        } else {
+            nextActivity();
+        }
+    }
+
+    void onCardDetected(Bitmap b, int scroll) {
+        Bitmap rectBitmap;
+        //保存图片到sdcard
+        if (null != b) {
+            if (scroll > 0) {
+                // 定义矩阵对象
+                Matrix matrix = new Matrix();
+                // 缩放原图
+                //matrix.postScale(3f, 3f);
+                // 向左旋转45度,参数为正则向右旋转
+                matrix.postRotate(scroll);
+                rectBitmap = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), matrix, false);
+                if (rectBitmap.isRecycled()) {
+                    rectBitmap.recycle();
+                    rectBitmap = null;
+                }
+            } else {
+                rectBitmap = b;
+            }
+            //手动拍照的 结果
+
+            Matrix m = new Matrix();
+            m.postScale(3f, 3f);
+            setIntent(Bitmap.createBitmap(rectBitmap, 26, 185, CardScanner.CREDIT_CARD_TARGET_WIDTH, CardScanner.CREDIT_CARD_TARGET_HEIGHT, m, false));
+            if (!b.isRecycled()) {
+                b.recycle();
+                b = null;
+            }
+        } else {
+            finish();
+        }
+
+    }
+
+
+    //todo   去 下一个 页面使用     这个方法用不上
+    private void nextActivity() {
+        finish();
+    }
+
+
+    /**
+     * 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(CardIOActivity.this, msgStr, Toast.LENGTH_LONG);
+        toast.show();
+    }
+
+    private boolean restartPreview() {
+        mDetectedCard = null;
+        assert mPreview != null;
+        boolean success = mCardScanner.resumeScanning(mPreview.getSurfaceHolder());
+        /*if (success) {
+            mUIBar.setVisibility(View.VISIBLE);
+        }*/
+
+        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 表面视图y偏移量调整    控制边框的上下偏移
+        mGuideFrame.top += sv.getTop();
+        mGuideFrame.bottom += sv.getTop();
+        mOverlay.setGuideAndRotation(mGuideFrame, degrees);
+        mLastDegrees = degrees;
+    }
+
+    // Called by OverlayView
+    void toggleFlash() {
+        setFlashOn(!mCardScanner.isFlashOn());
+    }
+
+    //设置 闪光的 开关
+    void setFlashOn(boolean b) {
+        boolean success = (mPreview != null && mOverlay != null && mCardScanner.setFlashOn(b));
+        if (success) {
+            mOverlay.setTorchOn(b);
+        }
+    }
+
+    /**
+     * 触发自动对焦
+     */
+    void triggerAutoFocus() {
+        mCardScanner.triggerAutoFocus(true);
+    }
+
+
+    /**
+     * 临时方法
+     *
+     * @param bitmap
+     * @param filePath
+     * @param fileName
+     * @return
+     */
+    public String saveBitmapToGallery(Bitmap bitmap, String filePath, String fileName) {
+        FileOutputStream fileOutputStream = null;
+        try {
+            File file = new File(filePath, fileName);
+            if (!file.getParentFile().exists()) {
+                file.getParentFile().mkdirs();
+            }
+            fileOutputStream = new FileOutputStream(file);
+            bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fileOutputStream);
+            fileOutputStream.flush();
+            galleryAddPic(CardIOActivity.this, file.getAbsolutePath());
+            return file.getAbsolutePath();
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                fileOutputStream.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 保存图片后,更新相册
+     *
+     * @param context           上下文
+     * @param mCurrentPhotoPath 图路径
+     */
+    public static void galleryAddPic(Context context, String mCurrentPhotoPath) {
+        Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+        File f = new File(mCurrentPhotoPath);
+        Uri contentUri = Uri.fromFile(f);
+        mediaScanIntent.setData(contentUri);
+        context.sendBroadcast(mediaScanIntent);
+    }
+
+
+    /**
+     * 自定义 主布局   ===================
+     */
+    private void setMyPreviewLayout() {
+
+        // top level container  顶层容器 背景容器
+        View layout_card_io_main = View.inflate(this, R.layout.layout_card_io_main, null);
+        //取消按钮的点击事件
+        layout_card_io_main.findViewById(R.id.tv_cancel).setOnClickListener(this);
+        //相册按钮的点击事件
+        layout_card_io_main.findViewById(R.id.tv_photo).setOnClickListener(this);
+        //拍照按钮的点击事件
+        layout_card_io_main.findViewById(R.id.take_photo_button).setOnClickListener(this);
+
+
+        //相机层
+        FrameLayout mMainLayout = layout_card_io_main.findViewById(R.id.fl_camera_main);
+        //控制上下的背景颜色
+        //相机 主界面
+        FrameLayout previewFrame = new FrameLayout(this);
+        previewFrame.setId(FRAME_ID);
+        //相机界面
+        mPreview = new Preview(this, null, mCardScanner.mPreviewWidth, mCardScanner.mPreviewHeight);
+        mPreview.setLayoutParams(new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, Gravity.TOP));
+        previewFrame.addView(mPreview);
+
+        mOverlay = new OverlayView(this, null, Util.deviceSupportsTorch(this));
+        mOverlay.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+        if (getIntent() != null) {
+            //设置展示logo
+            boolean useCardIOLogo = getIntent().getBooleanExtra(EXTRA_USE_CARDIO_LOGO, false);
+            mOverlay.setUseCardIOLogo(useCardIOLogo);
+
+            int color = getIntent().getIntExtra(EXTRA_GUIDE_COLOR, 0);
+
+            if (color != 0) {
+                // force 100% opaque guide colors.
+                int alphaRemovedColor = color | 0xFF000000;
+                mOverlay.setGuideColor(alphaRemovedColor);
+            } else {
+                // default to greeeeeen
+                mOverlay.setGuideColor(Color.GREEN);
+            }
+            //设置是否 隐藏logo
+            boolean hideCardIOLogo = getIntent().getBooleanExtra(EXTRA_HIDE_CARDIO_LOGO, false);
+            mOverlay.setHideCardIOLogo(hideCardIOLogo);
+            //设置 中间提示文案
+            String scanInstructions = getIntent().getStringExtra(EXTRA_SCAN_INSTRUCTIONS);
+            if (scanInstructions != null) {
+                mOverlay.setScanInstructions(scanInstructions);
+            }
+        }
+        //添加 相机覆盖 图标等
+        previewFrame.addView(mOverlay);
+        RelativeLayout.LayoutParams previewParams = new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+        previewParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
+        previewParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);
+        previewParams.addRule(RelativeLayout.ABOVE, UIBAR_ID);
+        //添加相机
+        mMainLayout.addView(previewFrame, previewParams);
+
+        include_crop = layout_card_io_main.findViewById(R.id.card_include_crop);
+        cropView = include_crop.findViewById(R.id.crop_view);
+        cropMaskView = include_crop.findViewById(R.id.crop_mask_view);
+        overlayView = include_crop.findViewById(R.id.overlay_view);
+        include_crop.findViewById(R.id.button_rotate).setOnClickListener(this);
+        include_crop.findViewById(R.id.button_confirm).setOnClickListener(this);
+        include_crop.findViewById(R.id.button_cancel).setOnClickListener(this);
+        this.setContentView(layout_card_io_main);
+    }
+
+    private void rotateCustomOverlay(float degrees) {
+        if (customOverlayLayout != null) {
+            float pivotX = customOverlayLayout.getWidth() / 2;
+            float pivotY = customOverlayLayout.getHeight() / 2;
+
+            Animation an = new RotateAnimation(0, degrees, pivotX, pivotY);
+            an.setDuration(0);
+            an.setRepeatCount(0);
+            an.setFillAfter(true);
+
+            customOverlayLayout.setAnimation(an);
+        }
+    }
+
+
+    /**
+     * 将结果 bitmap转换成数组
+     * <p>
+     * <p>
+     * <p>
+     * 精华
+     * 继承 CardIOActivity.class
+     * 重写 setIntent(Bitmap)
+     * <p>
+     * 获取到的 bitmap 会 传入 setIntent(bitmap)
+     *
+     * @param bitmap
+     */
+    public void setIntent(Bitmap bitmap) {
+        Log.e("====", "bitmap===" + bitmap);
+        if (null != bitmap) {
+            setResultAndFinish(RESULT_SCAN_SUPPRESSED, new Intent().putExtra(CardIOActivity.EXTRA_CAPTURED_CARD_IMAGE, bitmapToArray(bitmap, quality)));
+        } else {
+            finish();
+        }
+    }
+
+    /**
+     * 将bitmap 转换成 byte[]
+     *
+     * @param bitmap   图片
+     * @param qualityM 质量
+     * @return
+     */
+    public byte[] bitmapToArray(Bitmap bitmap, int qualityM) {
+        try {
+        ByteArrayOutputStream scaledCardBytes = new ByteArrayOutputStream();
+        bitmap.compress(Bitmap.CompressFormat.JPEG, qualityM, scaledCardBytes);
+        byte[] bytes = scaledCardBytes.toByteArray();
+            scaledCardBytes.close();
+        return bytes;
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return new byte[0];
+    }
+
+    /**
+     * 返回结果 并关闭
+     *
+     * @param resultCode
+     * @param data
+     */
+    private void setResultAndFinish(final int resultCode, final Intent data) {
+        setResult(resultCode, data);
+        finish();
+    }
+
+    // for torch test
+    public Rect getTorchRect() {
+        if (mOverlay == null) {
+            return null;
+        }
+        return mOverlay.getTorchRect();
+    }
+
+    /**
+     * 后添加的  获取 相册 图片路径的
+     *
+     * @param contentURI
+     * @return
+     */
+    private String getRealPathFromURI(Uri contentURI) {
+        String result;
+        Cursor cursor = getContentResolver().query(contentURI, null, null, null, null);
+        if (cursor == null) {
+            result = contentURI.getPath();
+        } else {
+            cursor.moveToFirst();
+            int idx = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
+            result = cursor.getString(idx);
+            cursor.close();
+        }
+        return result;
+    }
+
+
+    private void showCrop() {
+        onPause();
+        include_crop.setVisibility(View.VISIBLE);
+        cropMaskView.setVisibility(View.VISIBLE);
+        overlayView.setVisibility(View.INVISIBLE);
+        overlayView.setTypeWide();
+    }
+
+
+    @Override
+    public void onClick(View v) {
+        int id = v.getId();
+        if (id == R.id.tv_cancel) {
+            onBackPressed();
+
+            //点击相册
+        } else if (id == R.id.tv_photo) {
+            if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+                    ActivityCompat.requestPermissions(CardIOActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, PERMISSIONS_EXTERNAL_STORAGE);
+                    Toast.makeText(CardIOActivity.this, "请设置读取相册权限", Toast.LENGTH_SHORT).show();
+                    return;
+                }
+            }
+            Intent intent = new Intent(Intent.ACTION_PICK);
+            intent.setType("image/*");
+            startActivityForResult(intent, DATA_ENTRY_REQUEST_ID);
+
+            //点击拍照按钮
+        } else if (id == R.id.take_photo_button) {
+            mCardScanner.isTakePhoto = true;
+
+        } else if (id == R.id.button_rotate) {
+            cropView.rotate(90);
+
+        } else if (id == R.id.button_confirm) {// 从图片选择直接 裁剪
+            int maskType = cropMaskView.getMaskType();
+            Rect rect;
+            switch (maskType) {
+                case MaskView.MASK_TYPE_BANK_CARD:
+                    rect = cropMaskView.getFrameRect();
+                    break;
+                default:
+                    rect = overlayView.getFrameRect();
+                    Log.e("=====", rect.width() + "======" + rect.height());
+                    break;
+            }
+            Bitmap cropBitmap = cropView.crop(rect);
+            if (null != cropBitmap) {
+                // 计算缩放比例.
+                int scaleWidth = cropBitmap.getWidth();
+                int scaleHeight = cropBitmap.getHeight();
+                // 取得想要缩放的matrix参数.
+                Matrix matrix = new Matrix();
+                matrix.postScale(1296.0f / scaleWidth, 968.0f / scaleHeight);
+                // 得到新的图片.
+                setIntent(Bitmap.createBitmap(cropBitmap, 0, 0, scaleWidth, scaleHeight, matrix, true));
+            } else {
+                setIntent(cropBitmap);
+            }
+
+        } else if (id == R.id.button_cancel) {
+            onResume();
+            include_crop.setVisibility(View.GONE);
+
+        }
+    }
+
+
+    /*mCardScanner.mCamera.takePicture(null, null, new android.hardware.Camera.PictureCallback() {
+                    @Override
+                    public void onPictureTaken(byte[] data, android.hardware.Camera camera) {
+                        camera.stopPreview();
+                        Bitmap b = null;
+                        if (null != data) {
+                            System.out.println("===========111  data==="+data.length);
+                            b = BitmapFactory.decodeByteArray(data, 0, data.length);//data是字节数据,将其解析成位图
+
+                        }
+                        //保存图片到sdcard
+                        if (null != b) {
+                            System.out.println("==================b  w    " + b.getWidth());
+                            System.out.println("==================b  h    " + b.getHeight());
+                            // 定义矩阵对象
+                            Matrix matrix = new Matrix();
+                            // 缩放原图
+                            matrix.postScale(1f, 1f);
+                            // 向左旋转45度,参数为正则向右旋转
+                            matrix.postRotate(90);
+                            //bmp.getWidth(), 500分别表示重绘后的位图宽高  //y + height must be <= bitmap.height()
+                            Bitmap rectBitmap = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), matrix, true);
+
+                            //Bitmap rectBitmap = Bitmap.createBitmap(b, 40, 40, 1280, 960);
+                            //todo 保存  bitmap
+                            String imgUrl = saveBitmapToGallery(rectBitmap, getCacheDir().getAbsolutePath(), "/temp.jpg");
+
+                            System.out.println("imgUrl======================" + imgUrl);
+                            *//*if (!b.isRecycled()) {
+                                b.recycle();
+                                b = null;
+                            }*//*
+                            if (rectBitmap.isRecycled()) {
+                                rectBitmap.recycle();
+                                rectBitmap = null;
+                            }
+                            setResult(111);
+                        }
+                        finish();
+                    }
+                });*/
+}

+ 24 - 0
scancardlibrary/src/main/java/io/card/payment/CardIONativeLibsConfig.java

@@ -0,0 +1,24 @@
+package io.card.payment;
+
+/* CardIONativeLibsConfig.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+/**
+ * A config class that is used to specify an alternative search path for the native libraries.
+ *
+ * Its primary use case is for when the libraries are loaded over the network instead of being
+ * shipped with the APK and are stored in the app's data folder.
+ */
+public class CardIONativeLibsConfig {
+
+    private static String alternativeLibsPath;
+
+    public static void init(String path) {
+        alternativeLibsPath = path;
+    }
+
+    static String getAlternativeLibsPath() {
+        return alternativeLibsPath;
+    }
+}

+ 671 - 0
scancardlibrary/src/main/java/io/card/payment/CardScanner.java

@@ -0,0 +1,671 @@
+package io.card.payment;
+
+/* CardScanner.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.ImageFormat;
+import android.graphics.Rect;
+import android.graphics.YuvImage;
+import android.hardware.Camera;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.Size;
+import android.os.Build;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.WindowManager;
+
+import com.riso.scancardlibrary.BuildConfig;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.List;
+
+/**
+ * 封装了核心图像扫描。
+ * <p/>
+ * As of 7/20/12, the flow should be:
+ * <p/>
+ * 1. CardIOActivity sets up the CardScanner, Preview and Overlay. 2. As each frame is received &
+ * processed by the scanner, the scanner notifies the activity of any relevant changes. (e.g. edges
+ * detected, scan complete etc.) 3. CardIOActivity passes on the information to the preview and
+ * overlay, which can then update themselves as needed. 4. Once a result is reported, CardIOActivty
+ * closes the scanner and launches the next activity.
+ * <p/>
+ * HOWEVER, at the moment, the CardScanner is directly communicating with the Preview.
+ */
+public class CardScanner implements Camera.PreviewCallback, Camera.AutoFocusCallback, SurfaceHolder.Callback {
+
+    private static final String TAG = CardScanner.class.getSimpleName();
+
+    private static final float MIN_FOCUS_SCORE = 6; // TODO - parameterize this
+    // value based on phone? or
+    // change focus behavior?
+
+    private static final int DEFAULT_UNBLUR_DIGITS = -1; // no blur per default
+
+    private static final int CAMERA_CONNECT_TIMEOUT = 5000;
+    private static final int CAMERA_CONNECT_RETRY_INTERVAL = 50;
+
+    static final int ORIENTATION_PORTRAIT = 1;
+
+    // these values MUST match those in dmz_constants.h
+    static final int CREDIT_CARD_TARGET_WIDTH = 428; // kCreditCardTargetWidth
+    static final int CREDIT_CARD_TARGET_HEIGHT = 270; // kCreditCardTargetHeight
+
+    //--------------------- NATIVE------------------------
+    //判断 是不是  ARM架构处理器扩展结构
+    public static native boolean nUseNeon();
+
+    /*//判断 是不是  NVIDIA英伟达-Tegra图睿
+    public static native boolean nUseTegra();
+
+    //判断 是不是 X86
+    public static native boolean nUseX86();*/
+
+    private native void nSetup(boolean shouldDetectOnly, float minFocusScore);
+
+    private native void nSetup(boolean shouldDetectOnly, float minFocusScore, int unBlur);
+
+    private native void nResetAnalytics();
+
+    //控制 旋转的
+    private native void nGetGuideFrame(int orientation, int previewWidth, int previewHeight, Rect r);
+
+    //试试扫描计算 是否 对齐
+    private native void nScanFrame(byte[] data, int frameWidth, int frameHeight, int orientation,
+                                   DetectionInfo dinfo, Bitmap resultBitmap, boolean scanExpiry);
+
+    private native void nCleanup();
+
+    //--------------------- NATIVE------------------------
+
+    private Bitmap detectedBitmap;
+
+    private static boolean manualFallbackForError;
+
+    // member data
+    protected WeakReference<CardIOActivity> mScanActivityRef;
+    private boolean mSuppressScan = false;
+    private boolean mScanExpiry;
+    /**
+     * 拍照 按钮
+     */
+    public boolean isTakePhoto;
+    private int mUnblurDigits = DEFAULT_UNBLUR_DIGITS;
+
+    // read by CardIOActivity to set up Preview
+    final int mPreviewWidth = 640;
+    final int mPreviewHeight = 480;
+
+    private int mFrameOrientation = ORIENTATION_PORTRAIT;
+
+    private boolean mFirstPreviewFrame = true;
+    private long captureStart;
+    private long mAutoFocusStartedAt;
+    private long mAutoFocusCompletedAt;
+
+    public Camera mCamera;
+    private byte[] mPreviewBuffer;
+
+    // accessed by test harness subclass.
+    protected boolean useCamera = true;
+
+    private boolean isSurfaceValid;
+
+
+    // ------------------------------------------------------------------------
+    // STATIC INITIALIZATION
+    // ------------------------------------------------------------------------
+
+    static {
+        Log.i(Util.PUBLIC_LOG_TAG, "card.io " + BuildConfig.VERSION_NAME);
+
+        try {
+            //判断 用户是什么架构的  CPU
+            loadLibrary("cardioDecider");
+            Log.d(Util.PUBLIC_LOG_TAG, "Loaded card.io decider library.");
+            Log.d(Util.PUBLIC_LOG_TAG, "    nUseNeon(): " + nUseNeon());
+            //Log.d(Util.PUBLIC_LOG_TAG, "    nUseTegra():" + nUseTegra());
+            //Log.d(Util.PUBLIC_LOG_TAG, "    nUseX86():  " + nUseX86());
+
+            if (usesSupportedProcessorArch()) {
+                loadLibrary("opencv_core");
+                Log.d(Util.PUBLIC_LOG_TAG, "Loaded opencv core library");
+                loadLibrary("opencv_imgproc");
+                Log.d(Util.PUBLIC_LOG_TAG, "Loaded opencv imgproc library");
+            }
+            if (nUseNeon()) {
+                //ARM架构处理器扩展结构
+                loadLibrary("cardioRecognizer");
+                Log.i(Util.PUBLIC_LOG_TAG, "Loaded card.io NEON library");
+            }  else {
+                Log.w(Util.PUBLIC_LOG_TAG,
+                        "unsupported processor - card.io scanning requires ARMv7 or x86 architecture");
+                manualFallbackForError = true;
+            }
+        } catch (UnsatisfiedLinkError e) {
+            String error = "Failed to load native library: " + e.getMessage();
+            Log.e(Util.PUBLIC_LOG_TAG, error);
+            manualFallbackForError = true;
+        }
+    }
+
+    /**
+     * 自定义loadLibrary方法,它首先尝试从内置的libs中加载库
+     * 如果目录失败,则尝试使用备用的libs路径。
+     * <p>
+     * No checks are performed to ensure that the native libraries match the cardIO library version.
+     * This needs to be handled by the consuming application.
+     */
+    private static void loadLibrary(String libraryName) throws UnsatisfiedLinkError {
+        try {
+            System.loadLibrary(libraryName);
+        } catch (UnsatisfiedLinkError e) {
+            String altLibsPath = CardIONativeLibsConfig.getAlternativeLibsPath();
+            if (altLibsPath == null || altLibsPath.length() == 0) {
+                throw e;
+            }
+            if (!File.separator.equals(altLibsPath.charAt(altLibsPath.length() - 1))) {
+                altLibsPath += File.separator;
+            }
+            String fullPath = altLibsPath + Build.CPU_ABI + File.separator +
+                    System.mapLibraryName(libraryName);
+            Log.d(Util.PUBLIC_LOG_TAG, "loadLibrary failed for library " + libraryName + ". Trying " + fullPath);
+            // If we couldn't find the library in the normal places and we have an additional
+            // search path, try loading from there.
+            System.load(fullPath);
+        }
+    }
+
+    private static boolean usesSupportedProcessorArch() {
+        return nUseNeon() ;
+    }
+
+    static boolean processorSupported() {
+        return (!manualFallbackForError && (usesSupportedProcessorArch()));
+    }
+
+    CardScanner(CardIOActivity scanActivity, int currentFrameOrientation) {
+        Intent scanIntent = scanActivity.getIntent();
+        if (scanIntent != null) {
+            mSuppressScan = scanIntent.getBooleanExtra(CardIOActivity.EXTRA_SUPPRESS_SCAN, false);
+            mScanExpiry = scanIntent.getBooleanExtra(CardIOActivity.EXTRA_REQUIRE_EXPIRY, false)
+                    && scanIntent.getBooleanExtra(CardIOActivity.EXTRA_SCAN_EXPIRY, true);
+            mUnblurDigits = scanIntent.getIntExtra(CardIOActivity.EXTRA_UNBLUR_DIGITS, DEFAULT_UNBLUR_DIGITS);
+        }
+        mScanActivityRef = new WeakReference<>(scanActivity);
+        mFrameOrientation = currentFrameOrientation;
+        nSetup(mSuppressScan, MIN_FOCUS_SCORE, mUnblurDigits);
+    }
+
+    /**
+     * 连接或重新连接到相机。如果失败了,就再睡一遍。
+     * Returns <code>true</code> if successful,
+     * <code>false</code> if maxTimeout passes.
+     */
+    private Camera connectToCamera(int checkInterval, int maxTimeout) {
+        long start = System.currentTimeMillis();
+        if (useCamera) {
+            do {
+                try {
+                    // Camera.open() will open the back-facing camera. Front cameras are not
+                    // attempted.
+                    return Camera.open();
+                } catch (RuntimeException e) {
+                    try {
+                        Log.w(Util.PUBLIC_LOG_TAG,
+                                "Wasn't able to connect to camera service. Waiting and trying again...");
+                        Thread.sleep(checkInterval);
+                    } catch (InterruptedException e1) {
+                        Log.e(Util.PUBLIC_LOG_TAG, "Interrupted while waiting for camera", e1);
+                    }
+                } catch (Exception e) {
+                    Log.e(Util.PUBLIC_LOG_TAG, "Unexpected exception. Please report it as a GitHub issue", e);
+                    maxTimeout = 0;
+                }
+
+            } while (System.currentTimeMillis() - start < maxTimeout);
+        }
+
+        return null;
+    }
+
+    /**
+     * 准备相机
+     */
+    void prepareScanner() {
+        mFirstPreviewFrame = true;
+        mAutoFocusStartedAt = 0;
+        mAutoFocusCompletedAt = 0;
+
+        if (useCamera && mCamera == null) {
+            mCamera = connectToCamera(CAMERA_CONNECT_RETRY_INTERVAL, CAMERA_CONNECT_TIMEOUT);
+            if (mCamera == null) {
+                Log.e(Util.PUBLIC_LOG_TAG, "prepare scanner couldn't connect to camera!");
+                return;
+            }
+
+            setCameraDisplayOrientation(mCamera);
+
+            Parameters parameters = mCamera.getParameters();
+
+            List<Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
+            if (supportedPreviewSizes != null) {
+                Size previewSize = null;
+                for (Size s : supportedPreviewSizes) {
+                    if (s.width == 640 && s.height == 480) {
+                        previewSize = s;
+                        break;
+                    }
+                }
+                if (previewSize == null) {
+                    previewSize = supportedPreviewSizes.get(0);
+
+                    previewSize.width = mPreviewWidth;
+                    previewSize.height = mPreviewHeight;
+                }
+            }
+
+            parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
+
+            mCamera.setParameters(parameters);
+        }
+
+        if (detectedBitmap == null) {
+            detectedBitmap = Bitmap.createBitmap(CREDIT_CARD_TARGET_WIDTH,
+                    CREDIT_CARD_TARGET_HEIGHT, Bitmap.Config.ARGB_8888);
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    boolean resumeScanning(SurfaceHolder holder) {
+        if (mCamera == null) {
+            prepareScanner();
+        }
+
+        if (useCamera && mCamera == null) {
+            return false;
+        }
+
+        assert holder != null;
+
+        if (useCamera && mPreviewBuffer == null) {
+            Parameters parameters = mCamera.getParameters();
+            int previewFormat = parameters.getPreviewFormat();
+            int bytesPerPixel = ImageFormat.getBitsPerPixel(previewFormat) / 8;
+            int bufferSize = mPreviewWidth * mPreviewHeight * bytesPerPixel * 3;
+
+            mPreviewBuffer = new byte[bufferSize];
+            mCamera.addCallbackBuffer(mPreviewBuffer);
+        }
+
+        holder.addCallback(this);
+        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+        if (useCamera) {
+            mCamera.setPreviewCallbackWithBuffer(this);
+        }
+
+        if (isSurfaceValid) {
+            makePreviewGo(holder);
+        }
+
+        // 把闪光灯关掉
+        setFlashOn(false);
+        captureStart = System.currentTimeMillis();
+
+        nResetAnalytics();
+
+        return true;
+    }
+
+    public void pauseScanning() {
+        setFlashOn(false);
+        // 因为相机对象是共享资源,这很重要 ,在活动暂停时释放它。
+        if (mCamera != null) {
+            try {
+                mCamera.stopPreview();
+                mCamera.setPreviewDisplay(null);
+            } catch (IOException e) {
+                Log.w(Util.PUBLIC_LOG_TAG, "can't stop preview display", e);
+            }
+            mCamera.setPreviewCallback(null);
+            mCamera.release();
+            mPreviewBuffer = null;
+            mCamera = null;
+        }
+    }
+
+    public void endScanning() {
+        if (mCamera != null) {
+            pauseScanning();
+        }
+        nCleanup();
+
+        mPreviewBuffer = null;
+    }
+
+    /*
+     * --------------------------- SurfaceHolder callbacks
+     */
+
+    private boolean makePreviewGo(SurfaceHolder holder) {
+        // method name from http://www.youtube.com/watch?v=-WmGvYDLsj4
+        assert holder != null;
+        assert holder.getSurface() != null;
+        mFirstPreviewFrame = true;
+
+        if (useCamera) {
+            try {
+                mCamera.setPreviewDisplay(holder);
+            } catch (IOException e) {
+                return false;
+            }
+            try {
+                mCamera.startPreview();
+                mCamera.autoFocus(this);
+            } catch (RuntimeException e) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+    //=================================SurfaceHolder 三个回调, 创建(surfaceCreated)改变(surfaceChanged)销毁(surfaceDestroyed)
+    /*
+     * @see android.view.SurfaceHolder.Callback#surfaceCreated(android.view.SurfaceHolder )
+     */
+    @Override
+    public void surfaceCreated(SurfaceHolder holder) {
+        // The Surface has been created, acquire the camera and tell it where to draw.
+        if (mCamera != null || !useCamera) {
+            isSurfaceValid = true;
+            makePreviewGo(holder);
+        } else {
+            Log.wtf(Util.PUBLIC_LOG_TAG, "CardScanner.surfaceCreated() - camera is null!");
+            return;
+        }
+    }
+
+    /*
+     * @see android.view.SurfaceHolder.Callback#surfaceChanged(android.view.SurfaceHolder , int,
+     * int, int)
+     */
+    @Override
+    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+    }
+
+    /*
+     * @see android.view.SurfaceHolder.Callback#surfaceDestroyed(android.view. SurfaceHolder)
+     */
+    @Override
+    public void surfaceDestroyed(SurfaceHolder holder) {
+        if (mCamera != null) {
+            try {
+                mCamera.stopPreview();
+            } catch (Exception e) {
+                Log.e(Util.PUBLIC_LOG_TAG, "error stopping camera", e);
+            }
+        }
+        isSurfaceValid = false;
+    }
+
+    //=================================SurfaceHolder 三个回调, 创建(surfaceCreated)改变(surfaceChanged)销毁(surfaceDestroyed)
+
+    /**
+     * Handles processing of each frame.
+     * <p/>
+     * This method is called by Android, never directly by application code.
+     */
+    private static boolean processingInProgress = false;
+
+
+    //TODO 因为在时时扫描 , 加个 boolean  来停止扫描并截图
+    @Override
+    public void onPreviewFrame(byte[] data, Camera camera) {
+        if (data == null) {
+            return;
+        }
+        if (processingInProgress) {
+            // return frame buffer to pool 返回帧缓冲池
+            if (camera != null) {
+                camera.addCallbackBuffer(data);
+            }
+            return;
+        }
+        processingInProgress = true;
+
+        // TODO: eliminate this foolishness and measure/layout properly.
+        if (mFirstPreviewFrame) {
+            mFirstPreviewFrame = false;
+            mFrameOrientation = ORIENTATION_PORTRAIT;
+            mScanActivityRef.get().onFirstFrame();
+        }
+        DetectionInfo dInfo = new DetectionInfo();
+
+        /** pika **/
+        nScanFrame(data, mPreviewWidth, mPreviewHeight, mFrameOrientation, dInfo, detectedBitmap, mScanExpiry);
+
+        boolean sufficientFocus = (dInfo.focusScore >= MIN_FOCUS_SCORE);
+        //添加了 一个 Boolean值 停止扫描 并 拍照
+        if (isTakePhoto) {
+            pauseScanning();
+            YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, mPreviewWidth, mPreviewHeight, null);
+            ByteArrayOutputStream outPut = new ByteArrayOutputStream();
+            yuvImage.compressToJpeg(new Rect(0,0,mPreviewWidth, mPreviewHeight), 100, outPut);
+            byte[] jpgData = outPut.toByteArray();
+
+            mScanActivityRef.get().onCardDetected(BitmapFactory.decodeByteArray(jpgData, 0, jpgData.length),setCameraDisplayOrientation1());
+            try {
+                outPut.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        } else if (!sufficientFocus) {
+            triggerAutoFocus(false);
+        } else if (dInfo.predicted() || (mSuppressScan && dInfo.detected())) {
+            mScanActivityRef.get().onCardDetected(detectedBitmap, dInfo);
+        }
+        // give the image buffer back to the camera, AFTER we're done reading the image.
+        //在我们完成图像后,将图像缓冲区返回给相机。
+        if (camera != null) {
+            camera.addCallbackBuffer(data);
+        }
+        processingInProgress = false;
+
+    }
+
+    void onEdgeUpdate(DetectionInfo dInfo) {
+        mScanActivityRef.get().onEdgeUpdate(dInfo);
+    }
+
+    Rect getGuideFrame(int orientation, int previewWidth, int previewHeight) {
+        Rect r = null;
+        if (processorSupported()) {
+            r = new Rect();
+            nGetGuideFrame(orientation, previewWidth, previewHeight, r);
+        }
+        return r;
+    }
+
+    Rect getGuideFrame() {
+        return getGuideFrame(mFrameOrientation, mPreviewHeight, mPreviewWidth);
+    }
+
+    Rect getGuideFrame(int width, int height) {
+        return getGuideFrame(mFrameOrientation, width, height);
+    }
+
+    void setDeviceOrientation(int orientation) {
+        mFrameOrientation = orientation;
+    }
+
+    int getDeviceOrientation() {
+        return mFrameOrientation;
+    }
+
+    /*Map<String, Object> getAnalytics() {
+        HashMap<String, Object> analytics = new HashMap<String, Object>(11);
+
+        analytics.put("num_frames_scanned", Integer.valueOf(nGetNumFramesScanned()));
+        analytics.put("num_frames_skipped", Integer.valueOf(numFramesSkipped));
+
+        analytics.put("elapsed_time", Double.valueOf((System.currentTimeMillis() - captureStart) / 1000));
+
+        analytics.put("num_manual_refocusings", Integer.valueOf(numManualRefocus));
+        analytics.put("num_auto_triggered_refocusings", Integer.valueOf(numAutoRefocus));
+        analytics.put("num_manual_torch_changes", Integer.valueOf(numManualTorchChange));
+        return analytics;
+    }*/
+
+    // ------------------------------------------------------------------------
+    // CAMERA CONTROL & CALLBACKS
+    // ------------------------------------------------------------------------
+
+    /**
+     * Invoked when autoFocus is complete
+     * <p/>
+     * This method is called by Android, never directly by application code.
+     */
+    @Override
+    public void onAutoFocus(boolean success, Camera camera) {
+        mAutoFocusCompletedAt = System.currentTimeMillis();
+    }
+
+    /**
+     * True if autoFocus is in progress
+     */
+    boolean isAutoFocusing() {
+        return mAutoFocusCompletedAt < mAutoFocusStartedAt;
+    }
+
+    void toggleFlash() {
+        setFlashOn(!isFlashOn());
+    }
+
+    // ------------------------------------------------------------------------
+    // MISC CAMERA CONTROL
+    // ------------------------------------------------------------------------
+
+    /**
+     * 告诉预览的摄像头,以触发自动对焦。
+     *
+     * @param isManual callback for when autofocus is complete
+     */
+    void triggerAutoFocus(boolean isManual) {
+        if (useCamera && !isAutoFocusing()) {
+            try {
+                mAutoFocusStartedAt = System.currentTimeMillis();
+                mCamera.autoFocus(this);
+
+            } catch (RuntimeException e) {
+                Log.w(TAG, "could not trigger auto focus: " + e);
+            }
+        }
+    }
+
+    /**
+     * 检查一下闪光灯是否开了。
+     *
+     * @return state of the flash.
+     */
+    public boolean isFlashOn() {
+        if (!useCamera) {
+            return false;
+        }
+        Parameters params = mCamera.getParameters();
+        return params.getFlashMode().equals(Parameters.FLASH_MODE_TORCH);
+    }
+
+    /**
+     * 设置闪光灯 开关
+     *
+     * @param b desired flash state
+     * @return <code>true</code> if successful
+     */
+    public boolean setFlashOn(boolean b) {
+        if (mCamera != null) {
+            try {
+                Parameters params = mCamera.getParameters();
+                params.setFlashMode(b ? Parameters.FLASH_MODE_TORCH : Parameters.FLASH_MODE_OFF);
+                mCamera.setParameters(params);
+                return true;
+            } catch (RuntimeException e) {
+                Log.w(TAG, "Could not set flash mode: " + e);
+            }
+        }
+        return false;
+    }
+
+    private void setCameraDisplayOrientation(Camera mCamera) {
+        int result;
+
+        /* check API level. If upper API level 21, re-calculate orientation. */
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            Camera.CameraInfo info = new Camera.CameraInfo();
+            Camera.getCameraInfo(0, info);
+            int degrees = getRotationalOffset();
+            int cameraOrientation = info.orientation;
+            result = (cameraOrientation - degrees + 360) % 360;
+        } else {
+            /* if API level is lower than 21, use the default value */
+            result = 90;
+        }
+
+        /*set display orientation*/
+        mCamera.setDisplayOrientation(result);
+    }
+
+
+
+    private int setCameraDisplayOrientation1() {
+        int result;
+
+        /* check API level. If upper API level 21, re-calculate orientation. */
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            Camera.CameraInfo info = new Camera.CameraInfo();
+            Camera.getCameraInfo(0, info);
+            int degrees = getRotationalOffset();
+            int cameraOrientation = info.orientation;
+            result = (cameraOrientation - degrees + 360) % 360;
+        } else {
+            /* if API level is lower than 21, use the default value */
+            result = 90;
+        }
+
+       return result;
+    }
+
+    /**
+     * @see <a
+     * href="http://stackoverflow.com/questions/12216148/android-screen-orientation-differs-between-devices">SO
+     * post</a>
+     */
+    int getRotationalOffset() {
+        final int rotationOffset;
+        // Check "normal" screen orientation and adjust accordingly
+        int naturalOrientation = ((WindowManager) mScanActivityRef.get().getSystemService(Context.WINDOW_SERVICE))
+                .getDefaultDisplay().getRotation();
+        if (naturalOrientation == Surface.ROTATION_0) {
+            rotationOffset = 0;
+        } else if (naturalOrientation == Surface.ROTATION_90) {
+            rotationOffset = 90;
+        } else if (naturalOrientation == Surface.ROTATION_180) {
+            rotationOffset = 180;
+        } else if (naturalOrientation == Surface.ROTATION_270) {
+            rotationOffset = 270;
+        } else {
+            // just hope for the best (shouldn't happen)
+            rotationOffset = 0;
+        }
+        return rotationOffset;
+    }
+}

+ 342 - 0
scancardlibrary/src/main/java/io/card/payment/CardType.java

@@ -0,0 +1,342 @@
+package io.card.payment;
+
+/* CardType.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import com.riso.scancardlibrary.R;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map.Entry;
+
+import io.card.payment.i18n.LocalizedStrings;
+import io.card.payment.i18n.StringKey;
+
+/**
+ * Enumerates each supported card type. see http://en.wikipedia.org/wiki/Bank_card_number for more
+ * details.
+ *
+ * @version 1.0
+ */
+public enum CardType {
+    /**
+     * American Express cards start in 34 or 37
+     */
+    AMEX("AmEx"),
+    /**
+     * Diners Club
+     */
+    DINERSCLUB("DinersClub"),
+    /**
+     * Discover starts with 6x for some values of x.
+     */
+    DISCOVER("Discover"),
+    /**
+     * JCB (see http://www.jcbusa.com/) cards start with 35
+     */
+    JCB("JCB"),
+    /**
+     * Mastercard starts with 51-55
+     */
+    MASTERCARD("MasterCard"),
+    /**
+     * Visa starts with 4
+     */
+    VISA("Visa"),
+    /**
+     * Maestro
+     */
+    MAESTRO("Maestro"),
+    /**
+     * Unknown card type.
+     */
+    UNKNOWN("Unknown"),
+    /**
+     * Not enough information given.
+     * <br><br>
+     * More digits are required to know the card type. (e.g. all we have is a 3, so we don't know if
+     * it's JCB or AmEx)
+     */
+    INSUFFICIENT_DIGITS("More digits required");
+
+    public final String name;
+
+    private static int minDigits = 1;
+
+    private CardType(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+
+    /**
+     * Convenience method to return a CardType string (e.g. "Visa", "American Express", "JCB",
+     * "Maestro", "MasterCard", or "Discover") suitable for display. This string will be translated
+     * into the language specified. See {@link CardIOActivity#EXTRA_LANGUAGE_OR_LOCALE} for a
+     * detailed explanation of languageOrLocale.
+     *
+     * @param languageOrLocale See {@link CardIOActivity#EXTRA_LANGUAGE_OR_LOCALE}.
+     * @return the display name of the card
+     */
+    public String getDisplayName(String languageOrLocale) {
+        switch (this) {
+            case AMEX:
+                return LocalizedStrings.getString(StringKey.CARDTYPE_AMERICANEXPRESS, languageOrLocale);
+            case DINERSCLUB:
+            case DISCOVER:
+                return LocalizedStrings.getString(StringKey.CARDTYPE_DISCOVER, languageOrLocale);
+            case JCB:
+                return LocalizedStrings.getString(StringKey.CARDTYPE_JCB, languageOrLocale);
+            case MASTERCARD:
+                return LocalizedStrings.getString(StringKey.CARDTYPE_MASTERCARD, languageOrLocale);
+            case MAESTRO:
+                return LocalizedStrings.getString(StringKey.CARDTYPE_MAESTRO, languageOrLocale);
+            case VISA:
+                return LocalizedStrings.getString(StringKey.CARDTYPE_VISA, languageOrLocale);
+            default:
+                break;
+        }
+
+        return null;
+    }
+
+    /**
+     * @return 15 for AmEx, -1 for unknown, 16 for others.
+     */
+    public int numberLength() {
+        int result;
+        switch (this) {
+            case AMEX:
+                result = 15;
+                break;
+            case JCB:
+            case MASTERCARD:
+            case MAESTRO:
+            case VISA:
+            case DISCOVER:
+                result = 16;
+                break;
+            case DINERSCLUB:
+                result = 14;
+                break;
+            case INSUFFICIENT_DIGITS:
+                // this represents the maximum number of digits before we can know the card type
+                result = minDigits;
+                break;
+            case UNKNOWN:
+            default:
+                result = -1;
+                break;
+        }
+        return result;
+    }
+
+    /**
+     * @return 4 for Amex, 3 for others, -1 for unknown
+     */
+    public int cvvLength() {
+        int result;
+        switch (this) {
+            case AMEX:
+                result = 4;
+                break;
+            case JCB:
+            case MASTERCARD:
+            case MAESTRO:
+            case VISA:
+            case DISCOVER:
+            case DINERSCLUB:
+                result = 3;
+                break;
+            case UNKNOWN:
+            default:
+                result = -1;
+                break;
+        }
+
+        return result;
+    }
+
+    /**
+     * Returns the {@link Bitmap} of the card logo (e.g. Visa, MC, etc.), if known. Otherwise, returns null.
+     * <br><br>
+     * Returned bitmap is suitable for display with a masked card number, for example, to indicate a user's chosen
+     * card.
+     *
+     * @param context The application context for retrieving the image density
+     * @return the bitmap icon of the card for display
+     */
+    public Bitmap imageBitmap(Context context) {
+        int cardImageResource = -1;
+        switch (this) {
+            case AMEX: {
+                cardImageResource = R.drawable.cio_ic_amex;
+                break;
+            }
+            case VISA: {
+                cardImageResource = R.drawable.cio_ic_visa;
+                break;
+            }
+            case MASTERCARD: {
+                cardImageResource = R.drawable.cio_ic_mastercard;
+                break;
+            }
+            case DISCOVER:
+            case DINERSCLUB: {
+                cardImageResource = R.drawable.cio_ic_discover;
+                break;
+            }
+            case JCB: {
+                cardImageResource = R.drawable.cio_ic_jcb;
+                break;
+            }
+            default: {
+                // do not use generic image by default, if it's not one of the above, it's not
+                // valid, or it's maestro
+                break;
+            }
+        }
+
+        if (cardImageResource != -1) {
+            return BitmapFactory.decodeResource(context.getResources(), cardImageResource);
+        }
+
+        return null;
+    }
+
+    /**
+     * Determine if a number matches a prefix interval
+     *
+     * @param number credit card number
+     * @param intervalStart prefix (e.g. "4") or prefix interval start (e.g. "51")
+     * @param intervalEnd prefix interval end (e.g. "55") or null for non-intervals
+     * @return -1 for insufficient digits, 0 for no, 1 for yes.
+     */
+    private static boolean isNumberInInterval(String number, String intervalStart,
+                                              String intervalEnd) {
+        int numCompareStart = Math.min(number.length(), intervalStart.length());
+        int numCompareEnd = Math.min(number.length(), intervalEnd.length());
+
+        if (Integer.parseInt(number.substring(0, numCompareStart)) < Integer.parseInt(intervalStart
+                .substring(0, numCompareStart))) {
+            // number is too low
+            return false;
+        } else if (Integer.parseInt(number.substring(0, numCompareEnd)) > Integer
+                .parseInt(intervalEnd.substring(0, numCompareEnd))) {
+            // number is too high
+            return false;
+        }
+
+        return true;
+    }
+
+    private static HashMap<Pair<String, String>, CardType> intervalLookup;
+
+    static {
+        // initialize
+        intervalLookup = new HashMap<>();
+        intervalLookup.put(getNewPair("2221", "2720"), CardType.MASTERCARD);    // MasterCard 2-series
+        intervalLookup.put(getNewPair("300", "305"), CardType.DINERSCLUB);      // Diners Club (Discover)
+        intervalLookup.put(getNewPair("309", null), CardType.DINERSCLUB);       // Diners Club (Discover)
+        intervalLookup.put(getNewPair("34", null), CardType.AMEX);              // AmEx
+        intervalLookup.put(getNewPair("3528", "3589"), CardType.JCB);           // JCB
+        intervalLookup.put(getNewPair("36", null), CardType.DINERSCLUB);        // Diners Club (Discover)
+        intervalLookup.put(getNewPair("37", null), CardType.AMEX);              // AmEx
+        intervalLookup.put(getNewPair("38", "39"), CardType.DINERSCLUB);        // Diners Club (Discover)
+        intervalLookup.put(getNewPair("4", null), CardType.VISA);               // Visa
+        intervalLookup.put(getNewPair("50", null), CardType.MAESTRO);           // Maestro
+        intervalLookup.put(getNewPair("51", "55"), CardType.MASTERCARD);        // MasterCard
+        intervalLookup.put(getNewPair("56", "59"), CardType.MAESTRO);           // Maestro
+        intervalLookup.put(getNewPair("6011", null), CardType.DISCOVER);        // Discover
+        intervalLookup.put(getNewPair("61", null), CardType.MAESTRO);           // Maestro
+        intervalLookup.put(getNewPair("62", null), CardType.DISCOVER);          // China UnionPay (Discover)
+        intervalLookup.put(getNewPair("63", null), CardType.MAESTRO);           // Maestro
+        intervalLookup.put(getNewPair("644", "649"), CardType.DISCOVER);        // Discover
+        intervalLookup.put(getNewPair("65", null), CardType.DISCOVER);          // Discover
+        intervalLookup.put(getNewPair("66", "69"), CardType.MAESTRO);           // Maestro
+        intervalLookup.put(getNewPair("88", null), CardType.DISCOVER);          // China UnionPay (Discover)
+
+        for (Entry<Pair<String, String>, CardType> entry : getIntervalLookup().entrySet()) {
+            minDigits = Math.max(minDigits, entry.getKey().first.length());
+            if (entry.getKey().second != null) {
+                minDigits = Math.max(minDigits, entry.getKey().second.length());
+            }
+        }
+    }
+
+    private static HashMap<Pair<String, String>, CardType> getIntervalLookup() {
+        return intervalLookup;
+    }
+
+    private static Pair<String, String> getNewPair(String intervalStart, String intervalEnd) {
+        if (intervalEnd == null) {
+            // set intervalEnd to intervalStart before creating the Pair object, because apparently
+            // Pair.hashCode() can't handle nulls on some devices/versions. WTF.
+            intervalEnd = intervalStart;
+        }
+        return new Pair<String, String>(intervalStart, intervalEnd);
+    }
+
+    /**
+     * Infer the card type from a string.
+     *
+     * @param typeStr The String value of this enum
+     * @return the matched real type
+     */
+    public static CardType fromString(String typeStr) {
+        if (typeStr == null) {
+            return CardType.UNKNOWN;
+        }
+
+        for (CardType type : CardType.values()) {
+            if (type == CardType.UNKNOWN || type == CardType.INSUFFICIENT_DIGITS) {
+                continue;
+            }
+
+            if (typeStr.equalsIgnoreCase(type.toString())) {
+                return type;
+            }
+        }
+        return CardType.UNKNOWN;
+    }
+
+    /**
+     * Infer the CardType from the number string. See http://en.wikipedia.org/wiki/Bank_card_number
+     * for these ranges (last checked: 19 Feb 2013)
+     *
+     * @param numStr A string containing only the card number.
+     * @return the inferred card type
+     */
+    public static CardType fromCardNumber(String numStr) {
+        if (TextUtils.isEmpty(numStr)) {
+            return CardType.UNKNOWN;
+        }
+
+        HashSet<CardType> possibleCardTypes = new HashSet<CardType>();
+        for (Entry<Pair<String, String>, CardType> entry : getIntervalLookup().entrySet()) {
+            boolean isPossibleCard = isNumberInInterval(numStr, entry.getKey().first,
+                    entry.getKey().second);
+            if (isPossibleCard) {
+                possibleCardTypes.add(entry.getValue());
+            }
+        }
+
+        if (possibleCardTypes.size() > 1) {
+            return CardType.INSUFFICIENT_DIGITS;
+        } else if (possibleCardTypes.size() == 1) {
+            return possibleCardTypes.iterator().next();
+        } else {
+            return CardType.UNKNOWN;
+        }
+    }
+}

+ 194 - 0
scancardlibrary/src/main/java/io/card/payment/CreditCard.java

@@ -0,0 +1,194 @@
+package io.card.payment;
+
+/* CreditCard.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.UUID;
+
+/**
+ * Describes a credit card.
+ *
+ * @version 2.0
+ */
+public class CreditCard implements Parcelable {
+
+    /**
+     * Number of years into the future that a card expiration date is considered to be valid.
+     */
+    public static final int EXPIRY_MAX_FUTURE_YEARS = 15;
+
+    private static final String TAG = CreditCard.class.getSimpleName();
+
+    /**
+     * 15 or 16 digit card number. All numbers, no spaces.
+     */
+    public String cardNumber;
+
+    /**
+     * Month in two digit natural form. {January=1, ..., December=12}
+     */
+    public int expiryMonth = 0;
+
+    /**
+     * Four digit year
+     */
+    public int expiryYear = 0;
+
+    /**
+     * Three or four character security code.
+     */
+    public String cvv;
+
+    /**
+     * Billing postal code for card.
+     */
+    public String postalCode;
+
+    /**
+     * Cardholder name.
+     */
+    public String cardholderName;
+
+    // these should NOT be public
+    String scanId;
+    boolean flipped = false;
+    int yoff;
+    int[] xoff;
+
+    // constructors
+    public CreditCard() {
+        xoff = new int[16];
+        scanId = UUID.randomUUID().toString();
+    }
+
+    public CreditCard(String number, int month, int year, String code, String postalCode, String cardholderName) {
+        this.cardNumber = number;
+        this.expiryMonth = month;
+        this.expiryYear = year;
+        this.cvv = code;
+        this.postalCode = postalCode;
+        this.cardholderName = cardholderName;
+    }
+
+    // parcelable
+    private CreditCard(Parcel src) {
+        cardNumber = src.readString();
+        expiryMonth = src.readInt();
+        expiryYear = src.readInt();
+        cvv = src.readString();
+        postalCode = src.readString();
+        cardholderName = src.readString();
+        scanId = src.readString();
+        yoff = src.readInt();
+        xoff = src.createIntArray();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public final void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(cardNumber);
+        dest.writeInt(expiryMonth);
+        dest.writeInt(expiryYear);
+        dest.writeString(cvv);
+        dest.writeString(postalCode);
+        dest.writeString(cardholderName);
+        dest.writeString(scanId);
+        dest.writeInt(yoff);
+        dest.writeIntArray(xoff);
+    }
+
+    public static final Creator<CreditCard> CREATOR = new Creator<CreditCard>() {
+
+        @Override
+        public CreditCard createFromParcel(Parcel source) {
+            return new CreditCard(source);
+        }
+
+        @Override
+        public CreditCard[] newArray(int size) {
+            return new CreditCard[size];
+        }
+    };
+
+    /**
+     * @return The last four digits of the card number
+     */
+    public String getLastFourDigitsOfCardNumber() {
+        if (cardNumber != null) {
+            int available = Math.min(4, cardNumber.length());
+            return cardNumber.substring(cardNumber.length() - available);
+        } else {
+            return "";
+        }
+    }
+
+    /**
+     * @return The card number string consisting of all but the last four digits replaced with
+     * bullet ('&#8226;').
+     */
+    public String getRedactedCardNumber() {
+        if (cardNumber != null) {
+            String redacted = "";
+            if (cardNumber.length() > 4) {
+                redacted += String.format("%" + (cardNumber.length() - 4) + "s", "").replace(' ',
+                        '\u2022');
+            }
+            redacted += getLastFourDigitsOfCardNumber();
+            return CreditCardNumber.formatString(redacted, false,
+                    CardType.fromCardNumber(cardNumber));
+        } else {
+            return "";
+        }
+    }
+
+    /**
+     * @return The type of card, detected from the number
+     */
+    public CardType getCardType() {
+        return CardType.fromCardNumber(cardNumber);
+    }
+
+    /**
+     * @return A string suitable for display, with spaces inserted for readability.
+     */
+    public String getFormattedCardNumber() {
+        return CreditCardNumber.formatString(cardNumber);
+    }
+
+    /**
+     * @return <code>true</code> indicates a current, valid date.
+     */
+    public boolean isExpiryValid() {
+        return CreditCardNumber.isDateValid(expiryMonth, expiryYear);
+    }
+
+    /**
+     * @return a string suitable for writing to a log. Should not be displayed to the user.
+     */
+    @Override
+    public String toString() {
+        String s = "{" + getCardType() + ": " + getRedactedCardNumber();
+        if (expiryMonth > 0 || expiryYear > 0) {
+            s += "  expiry:" + expiryMonth + "/" + expiryYear;
+        }
+        if (postalCode != null) {
+            s += "  postalCode:" + postalCode;
+        }
+        if (cardholderName != null) {
+            s += "  cardholderName:" + cardholderName;
+        }
+        if (cvv != null) {
+            s += "  cvvLength:" + ((cvv != null) ? cvv.length() : 0);
+        }
+        s += "}";
+        return s;
+    }
+}

+ 155 - 0
scancardlibrary/src/main/java/io/card/payment/CreditCardNumber.java

@@ -0,0 +1,155 @@
+package io.card.payment;
+
+/* CreditCardNumber.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+import java.text.CharacterIterator;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.text.StringCharacterIterator;
+import java.util.Calendar;
+import java.util.Date;
+
+class CreditCardNumber {
+
+    /**
+     * Checks if the given string represents a number that passes the Luhn Checksum which all valid
+     * CCs will pass.
+     *
+     * @param number
+     * @return true if the number does pass the checksum, else false
+     */
+    public static boolean passesLuhnChecksum(String number) {
+        int even = 0;
+        int sum = 0;
+
+        final int[][] sums = { { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, { 0, 2, 4, 6, 8, 1, 3, 5, 7, 9 } };
+
+        CharacterIterator iter = new StringCharacterIterator(number);
+        for (char c = iter.last(); c != CharacterIterator.DONE; c = iter.previous()) {
+            if (!Character.isDigit(c)) {
+                return false;
+            }
+            int cInt = c - '0';
+            sum += sums[even++ & 0x1][cInt];
+        }
+        return sum % 10 == 0;
+    }
+
+    /**
+     * @param numStr the String of numbers to view
+     * @return null if numStr is not formattable by the known formatting rules
+     */
+
+    public static String formatString(String numStr) {
+        return formatString(numStr, true, null);
+    }
+
+    public static String formatString(String numStr, boolean filterDigits, CardType type) {
+        String digits;
+        if (filterDigits) {
+            digits = StringHelper.getDigitsOnlyString(numStr);
+        } else {
+            digits = numStr;
+        }
+        if (type == null) {
+            type = CardType.fromCardNumber(digits);
+        }
+        int numLen = type.numberLength();
+        if (digits.length() == numLen) {
+            if (numLen == 16) {
+                return formatSixteenString(digits);
+            } else if (numLen == 15) {
+                return formatFifteenString(digits);
+            }
+        }
+        return numStr; // at the worst case, pass back what was given
+    }
+
+    private static String formatFifteenString(String digits) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < 15; i++) {
+            if (i == 4 || i == 10) {
+                sb.append(' ');
+            }
+            sb.append(digits.charAt(i));
+        }
+        return sb.toString();
+    }
+
+    private static String formatSixteenString(String digits) {
+        StringBuilder sb = new StringBuilder();
+        {
+            for (int i = 0; i < 16; i++) {
+                if (i != 0 && i % 4 == 0) {
+                    sb.append(' ');// insert every 4th char, except at end
+                }
+                sb.append(digits.charAt(i));
+            }
+        }
+        return sb.toString();
+    }
+
+    public static boolean isDateValid(int expiryMonth, int expiryYear) {
+        if (expiryMonth < 1 || 12 < expiryMonth) {
+            return false;
+        }
+
+        Calendar now = Calendar.getInstance();
+        int thisYear = now.get(Calendar.YEAR);
+        int thisMonth = now.get(Calendar.MONTH) + 1;
+
+        if (expiryYear < thisYear) {
+            return false;
+        }
+        if (expiryYear == thisYear && expiryMonth < thisMonth) {
+            return false;
+        }
+        if (expiryYear > thisYear + CreditCard.EXPIRY_MAX_FUTURE_YEARS) {
+            return false;
+        }
+
+        return true;
+    }
+
+    public static boolean isDateValid(String dateString) {
+        String digitsOnly = StringHelper.getDigitsOnlyString(dateString);
+        SimpleDateFormat validDate = getDateFormatForLength(digitsOnly.length());
+        if (validDate == null) {
+            return false;
+        }
+        try {
+            validDate.setLenient(false);
+            Date enteredDate = validDate.parse(digitsOnly);
+            return isDateValid(enteredDate.getMonth() + 1, enteredDate.getYear() + 1900);
+        } catch (ParseException pe) {
+            return false;
+        }
+    }
+
+    public static SimpleDateFormat getDateFormatForLength(int len) {
+        if (len == 4) {
+            return new SimpleDateFormat("MMyy");
+        } else if (len == 6) {
+            return new SimpleDateFormat("MMyyyy");
+        } else {
+            return null;
+        }
+    }
+
+    public static Date getDateForString(String dateString) {
+        String digitsOnly = StringHelper.getDigitsOnlyString(dateString);
+        SimpleDateFormat validDate = getDateFormatForLength(digitsOnly.length());
+        if (validDate != null) {
+            try {
+                validDate.setLenient(false);
+                Date date = validDate.parse(digitsOnly);
+                return date;
+            } catch (ParseException pe) {
+                return null;
+            }
+        }
+        return null;
+    }
+}

+ 66 - 0
scancardlibrary/src/main/java/io/card/payment/DetectionInfo.java

@@ -0,0 +1,66 @@
+package io.card.payment;
+
+/* DetectionInfo.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+/**
+ * This class implements a data structure used to pass card detection details back and forth between
+ * java and native code/
+ */
+
+class DetectionInfo {
+    public boolean complete;
+    public boolean topEdge;
+    public boolean bottomEdge;
+    public boolean leftEdge;
+    public boolean rightEdge;
+    public float focusScore;
+    public int[] prediction;
+    public int expiry_month;
+    public int expiry_year;
+    public CreditCard detectedCard;
+
+    public DetectionInfo() {
+        complete = false;
+
+        prediction = new int[16];
+        prediction[0] = -1;
+        prediction[15] = -1;
+
+        detectedCard = new CreditCard();
+    }
+
+    ;
+
+    boolean sameEdgesAs(DetectionInfo other) {
+        return other.topEdge == this.topEdge && other.bottomEdge == this.bottomEdge
+                && other.leftEdge == this.leftEdge && other.rightEdge == this.rightEdge;
+    }
+
+    boolean detected() {
+        return (topEdge && bottomEdge && rightEdge && leftEdge);
+    }
+
+    boolean predicted() {
+        return complete;
+    }
+
+    CreditCard creditCard() {
+        String numberStr = new String();
+        for (int i = 0; i < 16 && 0 <= prediction[i] && prediction[i] < 10; i++) {
+            numberStr += String.valueOf(prediction[i]);
+        }
+        detectedCard.cardNumber = numberStr;
+
+        // set these regardless. They'll just be zeroes if not found.
+        detectedCard.expiryMonth = expiry_month;
+        detectedCard.expiryYear = expiry_year;
+        
+        return detectedCard;
+    }
+
+    int numVisibleEdges() {
+        return (topEdge ? 1 : 0) + (bottomEdge ? 1 : 0) + (leftEdge ? 1 : 0) + (rightEdge ? 1 : 0);
+    }
+}

+ 141 - 0
scancardlibrary/src/main/java/io/card/payment/ExpiryValidator.java

@@ -0,0 +1,141 @@
+package io.card.payment;
+
+/* ExpiryValidator.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+import android.text.Editable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+
+import java.util.Date;
+
+class ExpiryValidator implements Validator {
+
+    public int month;
+    public int year;
+
+    private boolean fullLength;
+
+    public ExpiryValidator() {}
+
+    public ExpiryValidator(int m, int y) {
+        month = m;
+        year = y;
+
+        fullLength = (month > 0 && year > 0);
+
+        // accept 2-digit years.
+        if (year < 2000) {
+            year += 2000;
+        }
+    }
+
+    @Override
+    public void afterTextChanged(Editable s) {
+        fullLength = (s.length() >= 5);
+
+        String dateStr = s.toString();
+        if (dateStr == null) {
+            return;
+        }
+
+        Date expiry = CreditCardNumber.getDateForString(dateStr);
+        if (expiry == null) {
+            return;
+        }
+
+        month = expiry.getMonth() + 1; // Java months are 0-11
+        year = expiry.getYear();
+
+        if (year < 1900) {
+            year += 1900;
+        }
+    }
+
+    @Override
+    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+        month = 0;
+        year = 0;
+        fullLength = false;
+    }
+
+    @Override
+    public void onTextChanged(CharSequence s, int start, int before, int count) {
+    }
+
+    @Override
+    public String getValue() {
+        return String.format("%02d/%02d", month, year % 100);
+    }
+
+    @Override
+    public boolean hasFullLength() {
+        return fullLength;
+    }
+
+    @Override
+    public boolean isValid() {
+        if (month < 1 || 12 < month) {
+            return false;
+        }
+
+        Date now = new Date();
+
+        if (year > 1900 + now.getYear() + 15) {
+            return false;
+        }
+
+        return (year > 1900 + now.getYear() || (year == 1900 + now.getYear() && month >= now
+                .getMonth() + 1));
+    }
+
+    @Override
+    public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
+                               int dend) {
+        // do all in place edits
+
+        SpannableStringBuilder result = new SpannableStringBuilder(source);
+
+        if (dstart == 0 && result.length() > 0
+                && ('1' < result.charAt(0) && result.charAt(0) <= '9')) {
+            result.insert(0, "0");
+            end++;
+        }
+
+        int replen = dend - dstart;
+        if (dstart - replen <= 2 && dstart + end - replen >= 2) {
+            int loc = 2 - dstart;
+            if (loc == end || (0 <= loc && loc < end && result.charAt(loc) != '/')) {
+                result.insert(loc, "/");
+                end++;
+            }
+        }
+
+        // look at what the updated string will be
+
+        String updated = new SpannableStringBuilder(dest).replace(dstart, dend, result, start, end)
+                .toString();
+
+        if (updated.length() >= 1) {
+            if (updated.charAt(0) < '0' || '1' < updated.charAt(0)) {
+                return "";
+            }
+        }
+
+        if (updated.length() >= 2) {
+            if (updated.charAt(0) != '0' && updated.charAt(1) > '2') {
+                return "";
+            }
+            if (updated.charAt(0) == '0' && updated.charAt(1) == '0') {
+                return "";
+            }
+        }
+
+        if (updated.length() > 5) {
+            return "";
+        }
+
+        return result;
+    }
+}

+ 63 - 0
scancardlibrary/src/main/java/io/card/payment/FixedLengthValidator.java

@@ -0,0 +1,63 @@
+package io.card.payment;
+
+/* FixedLengthValidator.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+import android.text.Editable;
+import android.text.Spanned;
+
+/**
+ * Validates that a field is exactly a certain length.
+ */
+class FixedLengthValidator implements Validator {
+    public int requiredLength;
+    private String value;
+
+    public FixedLengthValidator(int length) {
+        requiredLength = length;
+    }
+
+    @Override
+    public void afterTextChanged(Editable s) {
+        value = s.toString();
+    }
+
+    @Override
+    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+    }
+
+    @Override
+    public void onTextChanged(CharSequence s, int start, int before, int count) {
+    }
+
+    @Override
+    public String getValue() {
+        return value;
+    }
+
+    @Override
+    public boolean hasFullLength() {
+        return this.isValid();
+    }
+
+    @Override
+    public boolean isValid() {
+        if (value != null && value.length() == requiredLength) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
+                               int dend) {
+        if (end > 0 && dest.length() + dend - dstart + end > requiredLength) {
+            return "";
+        } else {
+            return null;
+        }
+    }
+
+}

+ 77 - 0
scancardlibrary/src/main/java/io/card/payment/Logo.java

@@ -0,0 +1,77 @@
+package io.card.payment;
+
+/* Logo.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import com.riso.scancardlibrary.R;
+
+
+// TODO - cache logo drawing as a bitmap and just draw that
+// TODO - get alpha overlay computation working properly with whites. should not look gray.
+
+class Logo {
+
+    private static final int ALPHA = 100;
+    private final Paint mPaint;
+
+    private Bitmap mLogo;
+    private boolean mUseCardIOLogo;
+    private final Context mContext;
+
+    public Logo(Context context) {
+        mPaint = new Paint();
+        mPaint.setAntiAlias(true);
+        mPaint.setAlpha(ALPHA);
+        mLogo = null;
+        mContext = context;
+    }
+
+    void loadLogo(boolean useCardIOLogo) {
+        if (mLogo != null && useCardIOLogo == mUseCardIOLogo) {
+            return; // no change, don't reload
+        }
+
+        mUseCardIOLogo = useCardIOLogo;
+        if (useCardIOLogo) {
+            mLogo = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.cio_card_io_logo);
+        } else {
+            mLogo = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.cio_paypal_logo);
+        }
+    }
+
+    public void draw(Canvas canvas, float maxWidth, float maxHeight) {
+        if (mLogo == null) {
+            loadLogo(false);
+        }
+
+        canvas.save();
+
+        float drawWidth, drawHeight;
+        float targetAspectRatio = (float) mLogo.getHeight() / mLogo.getWidth();
+        if ((maxHeight / maxWidth) < targetAspectRatio) {
+            drawHeight = maxHeight;
+            drawWidth = maxHeight / targetAspectRatio;
+        } else {
+            drawWidth = maxWidth;
+            drawHeight = maxWidth * targetAspectRatio;
+        }
+
+        float halfWidth = drawWidth / 2;
+        float halfHeight = drawHeight / 2;
+
+        canvas.drawBitmap(mLogo, new Rect(0, 0, mLogo.getWidth(), mLogo.getHeight()), new RectF(
+                -halfWidth, -halfHeight, halfWidth, halfHeight), mPaint);
+
+        canvas.restore();
+    }
+
+}

+ 23 - 0
scancardlibrary/src/main/java/io/card/payment/MaxLengthValidator.java

@@ -0,0 +1,23 @@
+package io.card.payment;
+
+/* MaxLengthValidator.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+/**
+ * Validates that a field is non-empty, and does not exceed a max value.
+ */
+class MaxLengthValidator extends NonEmptyValidator implements Validator {
+
+    private int maxLength;
+
+    MaxLengthValidator(int maxLength) {
+        this.maxLength = maxLength;
+    }
+
+    @Override
+    public boolean isValid() {
+        return super.isValid() && getValue().length() <= maxLength;
+    }
+
+}

+ 54 - 0
scancardlibrary/src/main/java/io/card/payment/NonEmptyValidator.java

@@ -0,0 +1,54 @@
+package io.card.payment;
+
+/* NonEmptyValidator.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+import android.text.Editable;
+import android.text.Spanned;
+
+/**
+ * Accepts all nonempty (after trimming) values.
+ */
+class NonEmptyValidator implements Validator {
+    private String value;
+
+    @Override
+    public void afterTextChanged(Editable s) {
+        value = s.toString().trim();
+    }
+
+    @Override
+    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+    }
+
+    @Override
+    public void onTextChanged(CharSequence s, int start, int before, int count) {
+    }
+
+    @Override
+    public String getValue() {
+        return value;
+    }
+
+    @Override
+    public boolean hasFullLength() {
+        return this.isValid();
+    }
+
+    @Override
+    public boolean isValid() {
+        if (value != null && value.length() > 0) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
+                               int dend) {
+        return null;
+    }
+
+}

+ 505 - 0
scancardlibrary/src/main/java/io/card/payment/OverlayView.java

@@ -0,0 +1,505 @@
+package io.card.payment;
+
+/* OverlayView.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.GradientDrawable.Orientation;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import java.lang.ref.WeakReference;
+
+import io.card.payment.i18n.LocalizedStrings;
+import io.card.payment.i18n.StringKey;
+
+/**
+ * This class implements a transparent overlay that is drawn over the raw camera capture frames.
+ * <p/>
+ * OverlayView draws the guide frame which indicates to the user where to hold the card. For debug
+ * builds, it also displays the frame rate and the focus score. Once a card is detected, the class
+ * displays a still image of the card.
+ * <p/>
+ * There are two stages of mark-up that are applied to the card image. When the image is first
+ * passed into this class, a negative rounded rectangle mask is used to block out the image
+ * background behind the rounded corners of the card. Then, a light gray rounded rectangle outline
+ * is drawn along the card edges.
+ * <p/>
+ * Once the digits are detected for the credit card number, those digits are drawn above the
+ * respective digits of the card.
+ * <p/>
+ * An instance of this class is created when the owning CardIOActivity is created. Its lifecycle is
+ * the same as that of the owning activity.
+ * <p/>
+ * <p/>
+ * A couple of technical notes:
+ * <p/>
+ * the drawing code is not optimized for performance. It re-computes values for each frame that
+ * could be cached instead (such as guide tick locations). However, I have measured performance for
+ * the app, including drawing performance, and drawing is not at all a bottleneck. So, fixing this
+ * seems like a low priority item.
+ * <p/>
+ * It is not clear whether the rotation animation currently implemented is really needed. We should
+ * figure out a strategy for this, and then change the code accordingly.
+ * <p/>
+ * To prevent race conditions & memory leaks, setting new card images happens in the context of the
+ * UI thread.
+ * <p/>
+ * The class has an instance float field displayScale that holds the value screen dimensions are
+ * scaled by. This field is used for instance to achieve consistent guide frame thickness
+ * independent of screen scale.
+ * <p/>
+ */
+class OverlayView extends View {
+    private static final String TAG = OverlayView.class.getSimpleName();
+
+    //提示文字 的大小
+    private static final float GUIDE_FONT_SIZE = 18.0f;
+    //提示文字的 padding
+    private static final float GUIDE_LINE_PADDING = 8.0f;
+
+    private static final float GUIDE_LINE_HEIGHT = GUIDE_FONT_SIZE + GUIDE_LINE_PADDING;
+    private static final float CARD_NUMBER_MARKUP_FONT_SIZE = GUIDE_FONT_SIZE + 2;
+
+    private static final Orientation[] GRADIENT_ORIENTATIONS = {Orientation.TOP_BOTTOM,
+            Orientation.LEFT_RIGHT, Orientation.BOTTOM_TOP, Orientation.RIGHT_LEFT};
+
+    /**
+     * 边界的 宽度
+     */
+    private static final int GUIDE_STROKE_WIDTH = 6;
+
+    private static final float CORNER_RADIUS_SIZE = 1 / 15.0f;
+
+    /**
+     * 控制闪光灯
+     */
+    private static final int TORCH_WIDTH = 70;
+    private static final int TORCH_HEIGHT = 50;
+
+    private static final int LOGO_MAX_WIDTH = 100;
+    private static final int LOGO_MAX_HEIGHT = TORCH_HEIGHT;
+
+    private static final int BUTTON_TOUCH_TOLERANCE = 20;
+
+    private final WeakReference<CardIOActivity> mScanActivityRef;
+    private DetectionInfo mDInfo;
+    private Bitmap mBitmap;
+    GradientDrawable mScanLineDrawable;
+    private Rect mGuide;
+    private CreditCard mDetectedCard;
+    private int mRotation;
+    private int mState;
+    /**
+     * 边界颜色
+     */
+    private int guideColor;
+
+    private boolean hideCardIOLogo;
+    /**
+     * 提示文案 '请将名片对准取景框'
+     */
+    private String scanInstructions;
+
+    // Keep paint objects around for high frequency methods to avoid re-allocating them.
+    private GradientDrawable mGradientDrawable;
+    private final Paint mGuidePaint;
+    private final Paint mLockedBackgroundPaint;
+    private Path mLockedBackgroundPath;
+    private Rect mCameraPreviewRect;
+    private final Torch mTorch;
+    private final Logo mLogo;
+    private Rect mTorchRect, mLogoRect;
+    private final boolean mShowTorch;
+    private int mRotationFlip;
+    private float mScale = 1;
+
+    public OverlayView(CardIOActivity captureActivity, AttributeSet attributeSet, boolean showTorch) {
+        super(captureActivity, attributeSet);
+
+        mShowTorch = showTorch;
+        mScanActivityRef = new WeakReference<CardIOActivity>(captureActivity);
+
+        mRotationFlip = 1;
+
+        // card.io is designed for an hdpi screen (density = 1.5);
+        mScale = getResources().getDisplayMetrics().density / 1.5f;
+        //创建 闪光按钮
+        mTorch = new Torch(TORCH_WIDTH * mScale, TORCH_HEIGHT * mScale,captureActivity);
+        mLogo = new Logo(captureActivity);
+
+        mGuidePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+        mLockedBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mLockedBackgroundPaint.clearShadowLayer();
+        mLockedBackgroundPaint.setStyle(Paint.Style.FILL);
+        mLockedBackgroundPaint.setColor(0xaa000000); // 75% black
+
+        scanInstructions = LocalizedStrings.getString(StringKey.SCAN_GUIDE);
+    }
+
+    public int getGuideColor() {
+        return guideColor;
+    }
+
+    public void setGuideColor(int color) {
+        guideColor = color;
+    }
+
+    public boolean getHideCardIOLogo() {
+        return hideCardIOLogo;
+    }
+
+    public void setHideCardIOLogo(boolean hide) {
+        hideCardIOLogo = hide;
+    }
+
+    public String getScanInstructions() {
+        return scanInstructions;
+    }
+
+    public void setScanInstructions(String scanInstructions) {
+        this.scanInstructions = scanInstructions;
+    }
+
+    // Public methods used by CardIOActivity
+    public void setGuideAndRotation(Rect rect, int rotation) {
+        mRotation = rotation;
+        mGuide = rect;
+        invalidate();
+
+        Point topEdgeUIOffset;
+        if (mRotation % 180 != 0) {
+            topEdgeUIOffset = new Point((int) (40 * mScale), (int) (60 * mScale));
+            mRotationFlip = -1;
+        } else {
+            topEdgeUIOffset = new Point((int) (60 * mScale), (int) (40 * mScale));
+            mRotationFlip = 1;
+        }
+        if (mCameraPreviewRect != null) {
+            Point torchPoint = new Point(mCameraPreviewRect.left + topEdgeUIOffset.x,
+                    mCameraPreviewRect.top + topEdgeUIOffset.y);
+
+            // mTorchRect used only for touch lookup, not layout
+            mTorchRect = Util.rectGivenCenter(torchPoint, (int) (TORCH_WIDTH * mScale),(int) (TORCH_HEIGHT * mScale));
+
+            // mLogoRect used only for touch lookup, not layout
+            Point logoPoint = new Point(mCameraPreviewRect.right - topEdgeUIOffset.x,
+                    mCameraPreviewRect.top + topEdgeUIOffset.y);
+            mLogoRect = Util.rectGivenCenter(logoPoint, (int) (LOGO_MAX_WIDTH * mScale),
+                    (int) (LOGO_MAX_HEIGHT * mScale));
+
+            int[] gradientColors = {Color.WHITE, Color.BLACK};
+            Orientation gradientOrientation = GRADIENT_ORIENTATIONS[(mRotation / 90) % 4];
+            mGradientDrawable = new GradientDrawable(gradientOrientation, gradientColors);
+            mGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
+            mGradientDrawable.setBounds(mGuide);
+            mGradientDrawable.setAlpha(50);
+
+            mLockedBackgroundPath = new Path();
+            mLockedBackgroundPath.addRect(new RectF(mCameraPreviewRect), Path.Direction.CW);
+            mLockedBackgroundPath.addRect(new RectF(mGuide), Path.Direction.CCW);
+        }
+    }
+
+    public void setBitmap(Bitmap bitmap) {
+        if (mBitmap != null) {
+            mBitmap.recycle();
+        }
+        mBitmap = bitmap;
+        if (mBitmap != null) {
+            decorateBitmap();
+        }
+    }
+
+    public Bitmap getBitmap() {
+        return mBitmap;
+    }
+
+    public void setDetectionInfo(DetectionInfo dinfo) {
+        if (mDInfo != null && !mDInfo.sameEdgesAs(dinfo)) {
+            invalidate();
+        }
+        this.mDInfo = dinfo;
+    }
+
+    public int getCardX() {
+        return mGuide.centerX() - mBitmap.getWidth() / 2;
+    }
+
+    public int getCardY() {
+        return mGuide.centerY() - mBitmap.getHeight() / 2;
+    }
+
+    public Bitmap getCardImage() {
+        if (mBitmap != null && !mBitmap.isRecycled()) {
+            return Bitmap.createBitmap(mBitmap, 0, 0, mBitmap.getWidth(), mBitmap.getHeight());
+        } else {
+            return null;
+        }
+    }
+
+    // Drawing methods
+    private Rect guideStrokeRect(int x1, int y1, int x2, int y2) {
+        Rect r;
+        int t2 = (int) (GUIDE_STROKE_WIDTH / 2 * mScale);
+        r = new Rect();
+
+        r.left = Math.min(x1, x2) - t2;
+        r.right = Math.max(x1, x2) + t2;
+
+        r.top = Math.min(y1, y2) - t2;
+        r.bottom = Math.max(y1, y2) + t2;
+
+        return r;
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+
+        if (mGuide == null || mCameraPreviewRect == null) {
+            return;
+        }
+        canvas.save();
+        int tickLength;
+
+        // Draw background rect
+
+        mGradientDrawable.draw(canvas);
+        //控制 角线的 长度
+        if ((mRotation == 0) || (mRotation == 180)) {
+            tickLength = (mGuide.bottom - mGuide.top) / 8;
+        } else {
+            tickLength = (mGuide.right - mGuide.left) / 8;
+        }
+
+        if (mDInfo != null && mDInfo.numVisibleEdges() == 4) {
+            // draw lock shadow.
+            canvas.drawPath(mLockedBackgroundPath, mLockedBackgroundPaint);
+        }
+
+        // Draw guide lines
+        mGuidePaint.clearShadowLayer();
+        mGuidePaint.setStyle(Paint.Style.FILL);
+        mGuidePaint.setColor(guideColor);
+
+        // top left
+        canvas.drawRect( guideStrokeRect(mGuide.left, mGuide.top, mGuide.left + tickLength, mGuide.top),
+                mGuidePaint);
+        canvas.drawRect( guideStrokeRect(mGuide.left, mGuide.top, mGuide.left, mGuide.top + tickLength),
+                mGuidePaint);
+
+        // top right
+        canvas.drawRect(
+                guideStrokeRect(mGuide.right, mGuide.top, mGuide.right - tickLength, mGuide.top),
+                mGuidePaint);
+        canvas.drawRect(
+                guideStrokeRect(mGuide.right, mGuide.top, mGuide.right, mGuide.top + tickLength),
+                mGuidePaint);
+
+        // bottom left
+        canvas.drawRect(
+                guideStrokeRect(mGuide.left, mGuide.bottom, mGuide.left + tickLength, mGuide.bottom),
+                mGuidePaint);
+        canvas.drawRect(
+                guideStrokeRect(mGuide.left, mGuide.bottom, mGuide.left, mGuide.bottom - tickLength),
+                mGuidePaint);
+
+        // bottom right
+        canvas.drawRect(
+                guideStrokeRect(mGuide.right, mGuide.bottom, mGuide.right - tickLength,
+                        mGuide.bottom), mGuidePaint);
+        canvas.drawRect(
+                guideStrokeRect(mGuide.right, mGuide.bottom, mGuide.right, mGuide.bottom
+                        - tickLength), mGuidePaint);
+
+        if (mDInfo != null) {
+            if (mDInfo.topEdge) {
+                canvas.drawRect(guideStrokeRect(mGuide.left, mGuide.top, mGuide.right, mGuide.top),
+                        mGuidePaint);
+            }
+            if (mDInfo.bottomEdge) {
+                canvas.drawRect(
+                        guideStrokeRect(mGuide.left, mGuide.bottom, mGuide.right, mGuide.bottom),
+                        mGuidePaint);
+            }
+            if (mDInfo.leftEdge) {
+                canvas.drawRect(
+                        guideStrokeRect(mGuide.left, mGuide.top, mGuide.left, mGuide.bottom),
+                        mGuidePaint);
+            }
+            if (mDInfo.rightEdge) {
+                canvas.drawRect(
+                        guideStrokeRect(mGuide.right, mGuide.top, mGuide.right, mGuide.bottom),
+                        mGuidePaint);
+            }
+
+            if (mDInfo.numVisibleEdges() < 3) {
+                // Draw guide text
+                // Set up paint attributes
+                float guideHeight = GUIDE_LINE_HEIGHT * mScale;
+                float guideFontSize = GUIDE_FONT_SIZE * mScale;
+
+                Util.setupTextPaintStyle(mGuidePaint);
+                mGuidePaint.setTextAlign(Align.CENTER);
+                mGuidePaint.setTextSize(guideFontSize);
+
+                // Translate and rotate text
+                canvas.translate(mGuide.left + mGuide.width() / 2, mGuide.top + mGuide.height() + (30 * mScale));
+                canvas.rotate(mRotationFlip * mRotation);
+
+                if (!TextUtils.isEmpty(scanInstructions)) {
+                    String[] lines = scanInstructions.split("\n");
+                    float y = -(((guideHeight * (lines.length - 1)) - guideFontSize) / 2) - 3;
+
+                    for (int i = 0; i < lines.length; i++) {
+                        canvas.drawText(lines[i], 0, y, mGuidePaint);
+                        y += guideHeight;
+                    }
+                }
+            }
+        }
+        canvas.restore();
+
+        // draw logo
+        if (!hideCardIOLogo) {
+            canvas.save();
+            canvas.translate(mLogoRect.exactCenterX(), mLogoRect.exactCenterY());
+            canvas.rotate(mRotationFlip * mRotation);
+            mLogo.draw(canvas, LOGO_MAX_WIDTH * mScale, LOGO_MAX_HEIGHT * mScale);
+            canvas.restore();
+        }
+
+        if (mShowTorch) {
+            // draw torch
+            canvas.save();
+            canvas.translate(mTorchRect.exactCenterX(), mTorchRect.exactCenterY());
+            canvas.rotate(mRotationFlip * mRotation);
+            mTorch.draw(canvas);
+            canvas.restore();
+        }
+    }
+
+    public void setDetectedCard(CreditCard creditCard) {
+        mDetectedCard = creditCard;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        try {
+            int action;
+            action = event.getAction() & MotionEvent.ACTION_MASK;
+            if (action == MotionEvent.ACTION_DOWN) {
+
+                Point p = new Point((int) event.getX(), (int) event.getY());
+                Rect r = Util.rectGivenCenter(p, BUTTON_TOUCH_TOLERANCE, BUTTON_TOUCH_TOLERANCE);
+                if (mShowTorch && mTorchRect != null && Rect.intersects(mTorchRect, r)) {
+                    mScanActivityRef.get().toggleFlash();
+                } else {
+                    mScanActivityRef.get().triggerAutoFocus();
+                }
+            }
+        } catch (NullPointerException e) {
+            // Un-reproducible NPE reported on device without flash where flash detected and flash
+            // button pressed (see https://github.com/paypal/PayPal-Android-SDK/issues/27)
+        }
+
+        return false;
+    }
+
+    /* create the card image with inside a rounded rect */
+    private void decorateBitmap() {
+
+        RectF roundedRect = new RectF(2, 2, mBitmap.getWidth() - 2, mBitmap.getHeight() - 2);
+        float cornerRadius = mBitmap.getHeight() * CORNER_RADIUS_SIZE;
+
+        // Alpha canvas with white rounded rect
+        Bitmap maskBitmap = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888);
+        Canvas maskCanvas = new Canvas(maskBitmap);
+        maskCanvas.drawColor(Color.TRANSPARENT);
+        Paint maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        maskPaint.setColor(Color.BLACK);
+        maskPaint.setStyle(Paint.Style.FILL);
+        maskCanvas.drawRoundRect(roundedRect, cornerRadius, cornerRadius, maskPaint);
+
+        Paint paint = new Paint();
+        paint.setFilterBitmap(false);
+
+        // Draw mask onto mBitmap
+        Canvas canvas = new Canvas(mBitmap);
+
+        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
+        canvas.drawBitmap(maskBitmap, 0, 0, paint);
+
+        // Now re-use the above bitmap to do a shadow.
+        paint.setXfermode(null);
+
+        maskBitmap.recycle();
+    }
+
+    // TODO - move this into RequestTask, so we just get back a card image ready to go
+    public void markupCard() {
+
+        if (mBitmap == null) {
+            return;
+        }
+
+        if (mDetectedCard.flipped) {
+            Matrix m = new Matrix();
+            m.setRotate(180, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2);
+
+            mBitmap = Bitmap.createBitmap(mBitmap, 0, 0, mBitmap.getWidth(), mBitmap.getHeight(),
+                    m, false);
+        }
+
+        Canvas bc = new Canvas(mBitmap);
+        Paint paint = new Paint();
+        Util.setupTextPaintStyle(paint);
+        paint.setTextSize(CARD_NUMBER_MARKUP_FONT_SIZE * mScale);
+
+        int len = mDetectedCard.cardNumber.length();
+        float sf = mBitmap.getWidth() / (float) CardScanner.CREDIT_CARD_TARGET_WIDTH;
+        int yOffset = (int) ((mDetectedCard.yoff * sf - 6));
+        for (int i = 0; i < len; i++) {
+            int xOffset = (int) (mDetectedCard.xoff[i] * sf);
+            bc.drawText("" + mDetectedCard.cardNumber.charAt(i), xOffset, yOffset, paint);
+        }
+    }
+
+    public boolean isAnimating() {
+        return (mState != 0);
+    }
+
+    public void setCameraPreviewRect(Rect rect) {
+        mCameraPreviewRect = rect;
+    }
+
+    public void setTorchOn(boolean b) {
+        mTorch.setOn(b);
+        invalidate();
+    }
+
+    public void setUseCardIOLogo(boolean useCardIOLogo) {
+        mLogo.loadLogo(useCardIOLogo);
+    }
+
+    // for test
+    public Rect getTorchRect() {
+        return mTorchRect;
+    }
+}

+ 104 - 0
scancardlibrary/src/main/java/io/card/payment/Preview.java

@@ -0,0 +1,104 @@
+package io.card.payment;
+
+/* Preview.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.ViewGroup;
+
+/**
+ * This class contains a SurfaceView, which is used to display the camera preview frames. It
+ * performs basic layout and life cycle tasks for the camera and camera previews.
+ * <p/>
+ * Technical note: display of camera preview frames will only work when there is a valid surface
+ * view to display those frames on. To that end, I have added a valid surface flag that is updated
+ * by surface view lifecycle callbacks. We only attempt (re-)start camera preview if there is a
+ * valid surface view to draw on.
+ */
+class Preview extends ViewGroup {
+    private static final String TAG = Preview.class.getSimpleName();
+
+    private int mPreviewWidth;
+    private int mPreviewHeight;
+
+    SurfaceView mSurfaceView;
+
+    public Preview(Context context, AttributeSet attributeSet, int previewWidth, int previewHeight) {
+        super(context, attributeSet);
+
+        // the preview size comes from the cardScanner (camera)
+        // need to swap width & height to account for implicit 90deg rotation
+        // which is part of cardScanner. see "mCamera.setDisplayOrientation(90);"
+        mPreviewWidth = previewHeight;
+        mPreviewHeight = previewWidth;
+
+        mSurfaceView = new SurfaceView(context);
+        addView(mSurfaceView);
+    }
+
+    public SurfaceView getSurfaceView() {
+        assert mSurfaceView != null;
+        return mSurfaceView;
+    }
+
+    SurfaceHolder getSurfaceHolder() {
+        SurfaceHolder holder = getSurfaceView().getHolder();
+        assert holder != null;
+        return holder;
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        canvas.drawARGB(255, 255, 0, 0);
+    }
+
+    // ------------------------------------------------------------------------
+    // LAYOUT METHODS
+    // ------------------------------------------------------------------------
+
+    // TODO - document
+    // Need a better explanation of why onMeasure is needed and how width/height are determined.
+    // Must the camera be set first via setCamera? What if mSupportedPreviewSizes == null?
+    // Why do we startPreview within this method if the surface is valid?
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // We purposely disregard child measurements because act as a
+        // wrapper to a SurfaceView that centers the camera preview instead
+        // of stretching it.
+        final int width = resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec);
+        final int height = resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec);
+
+        setMeasuredDimension(width, height);
+    }
+
+    // TODO - document
+    // What is the child surface? The camera preview image?
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        if (changed && getChildCount() > 0) {
+            assert mSurfaceView != null;
+
+            final int width = r - l;
+            final int height = b - t;
+
+            // Center the child SurfaceView within the parent, making sure that the preview is
+            // *always* fully contained on the device screen.
+            if (width * mPreviewHeight > height * mPreviewWidth) {
+                final int scaledChildWidth = mPreviewWidth * height / mPreviewHeight;
+                mSurfaceView.layout((width - scaledChildWidth) / 2, 0,
+                        (width + scaledChildWidth) / 2, height);
+            } else {
+                final int scaledChildHeight = mPreviewHeight * width / mPreviewWidth;
+                mSurfaceView.layout(0, (height - scaledChildHeight) / 2, width,
+                        (height + scaledChildHeight) / 2);
+            }
+        }
+    }
+
+}

+ 17 - 0
scancardlibrary/src/main/java/io/card/payment/StringHelper.java

@@ -0,0 +1,17 @@
+package io.card.payment;
+
+/* StringHelper.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+class StringHelper {
+    public static String getDigitsOnlyString(String numString) {
+        StringBuilder sb = new StringBuilder();
+        for (char c : numString.toCharArray()) {
+            if (Character.isDigit(c)) {
+                sb.append(c);
+            }
+        }
+        return sb.toString();
+    }
+}

+ 130 - 0
scancardlibrary/src/main/java/io/card/payment/Torch.java

@@ -0,0 +1,130 @@
+package io.card.payment;
+
+/* Torch.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ *
+ * 控制 闪光的UI
+ */
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+
+import com.riso.scancardlibrary.R;
+
+
+/**
+ * 闪光灯按钮
+ */
+class Torch {
+    private static final String TAG = Torch.class.getSimpleName();
+
+    private static final float CORNER_RADIUS = 5f;
+    private static final int BG_ALPHA = 96;
+    private Context context;
+
+    private boolean mOn;
+    private float mWidth;
+    private float mHeight;
+
+    public Torch(float width, float height, Context context) {
+        mOn = false;
+        mWidth = width;
+        mHeight = height;
+        this.context = context;
+    }
+
+    public void draw(Canvas canvas) {
+        canvas.save();
+        canvas.translate(-mWidth / 2, -mHeight / 2);
+        /*float cornerRadius = CORNER_RADIUS;
+        // Create border paint 创建一个画边线的笔
+        Paint borderPaint = new Paint();
+        borderPaint.setColor(Color.BLACK);
+        borderPaint.setStyle(Style.STROKE);
+        borderPaint.setAntiAlias(true);
+        borderPaint.setStrokeWidth(1.5f);
+        // Create fill paint  创建一个 填充的画笔
+        Paint fillPaint = new Paint();
+        fillPaint.setStyle(Style.FILL);
+        fillPaint.setColor(Color.WHITE);
+        if (mOn) {
+            fillPaint.setAlpha(BG_ALPHA * 2);
+        } else {
+            fillPaint.setAlpha(BG_ALPHA);
+        }
+
+        // Create the button itself
+        float[] outerRadii = new float[8];
+        Arrays.fill(outerRadii, cornerRadius);
+        RoundRectShape buttonShape = new RoundRectShape(outerRadii, null, null);
+        buttonShape.resize(mWidth, mHeight);
+
+        // Draw the button stroke and background 画按钮线和背景
+        buttonShape.draw(canvas, fillPaint);
+        buttonShape.draw(canvas, borderPaint);
+
+        // Create bolt fill paint
+        Paint boltPaint = new Paint();
+        boltPaint.setStyle(Style.FILL_AND_STROKE);
+        boltPaint.setAntiAlias(true);*/
+        //控制 闪电的颜色(闪光的按钮)
+        if (mOn) {
+            //开启的颜色
+            //boltPaint.setColor(Color.WHITE);
+            Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.icon_flash_light);
+            Rect mTopSrcRect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+            Rect mTopDestRect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+            canvas.drawBitmap(bitmap, mTopSrcRect, mTopDestRect, new Paint());
+
+        } else {
+            //关闭的颜色
+            //boltPaint.setColor(Color.BLACK);
+            Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.icon_flash);
+            Rect mTopSrcRect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+            Rect mTopDestRect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+            canvas.drawBitmap(bitmap, mTopSrcRect, mTopDestRect, new Paint());
+        }
+        // Draw the bolt itself   画闪电
+        /*Path boltPath = createBoltPath();
+        Matrix m = new Matrix();
+        float boltHeight = .8f * mHeight;
+        m.postScale(boltHeight, boltHeight);
+        boltPath.transform(m);
+        canvas.translate(mWidth / 2, mHeight / 2);
+        canvas.drawPath(boltPath, boltPaint);*/
+        canvas.restore();
+    }
+
+    public void setOn(boolean on) {
+        mOn = on;
+    }
+
+    /**
+     * @return Path of width height 1 and width 0.65
+     */
+
+    static private Path createBoltPath() {
+
+        Path p = new Path();
+        p.moveTo(10.0f, 0.0f); // top
+        p.lineTo(0.0f, 11.0f); // left
+        p.lineTo(6.0f, 11.0f); // left indent
+        p.lineTo(2.0f, 20.0f); // bottom
+        p.lineTo(13.0f, 8.0f); // right
+        p.lineTo(7.0f, 8.0f); // right indent
+        p.lineTo(10.0f, 0.0f); // top
+        p.setLastPoint(10.0f, 0.0f);
+        Matrix m = new Matrix();
+        m.postTranslate(-6.5f, -10.0f);
+        m.postScale(1 / 20.0f, 1 / 20.0f);
+        p.transform(m);
+        return p;
+    }
+
+}

+ 313 - 0
scancardlibrary/src/main/java/io/card/payment/Util.java

@@ -0,0 +1,313 @@
+package io.card.payment;
+
+/* Util.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+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.Paint;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.hardware.Camera;
+import android.media.ExifInterface;
+import android.os.Build;
+import android.os.Debug;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * This class has various static utility methods.
+ */
+
+class Util {
+    private static final String TAG = Util.class.getSimpleName();
+    private static final boolean TORCH_BLACK_LISTED = (Build.MODEL.equals("DROID2"));
+
+    public static final String PUBLIC_LOG_TAG = "card.io";
+
+    private static Boolean sHardwareSupported;
+
+    public static boolean deviceSupportsTorch(Context context) {
+        return !TORCH_BLACK_LISTED
+                && context.getPackageManager()
+                .hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH);
+    }
+
+    @SuppressWarnings("rawtypes")
+    public static String manifestHasConfigChange(ResolveInfo resolveInfo, Class activityClass) {
+        String error = null;
+        if (resolveInfo == null) {
+            error = String.format("Didn't find %s in the AndroidManifest.xml",
+                    activityClass.getName());
+        } else if (!Util.hasConfigFlag(resolveInfo.activityInfo.configChanges,
+                ActivityInfo.CONFIG_ORIENTATION)) {
+            error = activityClass.getName()
+                    + " requires attribute android:configChanges=\"orientation\"";
+        }
+        if (error != null) {
+            Log.e(Util.PUBLIC_LOG_TAG, error);
+        }
+        return error;
+    }
+
+    public static boolean hasConfigFlag(int config, int configFlag) {
+        return ((config & configFlag) == configFlag);
+    }
+
+    /* --- HARDWARE SUPPORT --- */
+
+    public static boolean hardwareSupported() {
+        if (sHardwareSupported == null) {
+            sHardwareSupported = hardwareSupportCheck();
+        }
+        return sHardwareSupported;
+    }
+
+    private static boolean hardwareSupportCheck() {
+        if (!CardScanner.processorSupported()) {
+            Log.w(PUBLIC_LOG_TAG, "- Processor type is not supported");
+            return false;
+        }
+
+        // Camera needs to open
+        Camera c = null;
+        try {
+            c = Camera.open();
+        } catch (RuntimeException e) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                return true;
+            } else {
+                Log.w(PUBLIC_LOG_TAG, "- Error opening camera: " + e);
+                throw new CameraUnavailableException();
+            }
+        }
+        if (c == null) {
+            Log.w(PUBLIC_LOG_TAG, "- No camera found");
+            return false;
+        } else {
+            List<Camera.Size> list = c.getParameters().getSupportedPreviewSizes();
+            c.release();
+
+            boolean supportsVGA = false;
+
+            for (Camera.Size s : list) {
+                if (s.width == 640 && s.height == 480) {
+                    supportsVGA = true;
+                    break;
+                }
+            }
+
+            if (!supportsVGA) {
+                Log.w(PUBLIC_LOG_TAG, "- Camera resolution is insufficient");
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public static String getNativeMemoryStats() {
+        return "(free/alloc'd/total)" + Debug.getNativeHeapFreeSize() + "/"
+                + Debug.getNativeHeapAllocatedSize() + "/" + Debug.getNativeHeapSize();
+    }
+
+    public static void logNativeMemoryStats() {
+        Log.d("MEMORY", "Native memory stats: " + getNativeMemoryStats());
+    }
+
+    static public Rect rectGivenCenter(Point center, int width, int height) {
+        return new Rect(center.x - width / 2, center.y - height / 2, center.x + width / 2, center.y
+                + height / 2);
+    }
+
+    static public void setupTextPaintStyle(Paint paint) {
+        paint.setColor(Color.WHITE);
+        //paint.setStyle(Paint.Style.FILL);
+        //paint.setTypeface(Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD));
+        paint.setAntiAlias(true);
+        float[] black = {0f, 0f, 0f};
+        paint.setShadowLayer(1.5f, 0.5f, 0f, Color.HSVToColor(200, black));
+    }
+
+    /**
+     * Writes {@link CardIOActivity#EXTRA_CAPTURED_CARD_IMAGE} to dataIntent if
+     * origIntent has {@link CardIOActivity#EXTRA_RETURN_CARD_IMAGE}.
+     * <p>
+     * 将图片 压缩成字节数组
+     *
+     * @param origIntent
+     * @param dataIntent
+     * @param mOverlay
+     */
+    static void writeCapturedCardImageIfNecessary(Intent origIntent, Intent dataIntent, OverlayView mOverlay) {
+        if (origIntent.getBooleanExtra(CardIOActivity.EXTRA_RETURN_CARD_IMAGE, false) && mOverlay != null && mOverlay.getBitmap() != null) {
+            ByteArrayOutputStream scaledCardBytes = new ByteArrayOutputStream();
+            mOverlay.getBitmap().compress(Bitmap.CompressFormat.JPEG, 100, scaledCardBytes);
+            dataIntent.putExtra(CardIOActivity.EXTRA_CAPTURED_CARD_IMAGE, scaledCardBytes.toByteArray());
+        }
+
+    }
+
+
+
+
+
+
+    /**
+     * 获取字节数组的角度
+     */
+    public static int getOrientation(byte[] jpeg) {
+        if (jpeg == null) {
+            return 0;
+        }
+
+        int offset = 0;
+        int length = 0;
+
+        // ISO/IEC 10918-1:1993(E)
+        while (offset + 3 < jpeg.length && (jpeg[offset++] & 0xFF) == 0xFF) {
+            int marker = jpeg[offset] & 0xFF;
+
+            // Check if the marker is a padding.
+            if (marker == 0xFF) {
+                continue;
+            }
+            offset++;
+
+            // Check if the marker is SOI or TEM.
+            if (marker == 0xD8 || marker == 0x01) {
+                continue;
+            }
+            // Check if the marker is EOI or SOS.
+            if (marker == 0xD9 || marker == 0xDA) {
+                break;
+            }
+
+            // Get the length and check if it is reasonable.
+            length = pack(jpeg, offset, 2, false);
+            if (length < 2 || offset + length > jpeg.length) {
+                Log.e(TAG, "Invalid length");
+                return 0;
+            }
+
+            // Break if the marker is EXIF in APP1.
+            if (marker == 0xE1 && length >= 8
+                    && pack(jpeg, offset + 2, 4, false) == 0x45786966
+                    && pack(jpeg, offset + 6, 2, false) == 0) {
+                offset += 8;
+                length -= 8;
+                break;
+            }
+
+            // Skip other markers.
+            offset += length;
+            length = 0;
+        }
+
+        // JEITA CP-3451 Exif Version 2.2
+        if (length > 8) {
+            // Identify the byte order.
+            int tag = pack(jpeg, offset, 4, false);
+            if (tag != 0x49492A00 && tag != 0x4D4D002A) {
+                Log.e(TAG, "Invalid byte order");
+                return 0;
+            }
+            boolean littleEndian = (tag == 0x49492A00);
+
+            // Get the offset and check if it is reasonable.
+            int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;
+            if (count < 10 || count > length) {
+                Log.e(TAG, "Invalid offset");
+                return 0;
+            }
+            offset += count;
+            length -= count;
+
+            // Get the count and go through all the elements.
+            count = pack(jpeg, offset - 2, 2, littleEndian);
+            while (count-- > 0 && length >= 12) {
+                // Get the tag and check if it is orientation.
+                tag = pack(jpeg, offset, 2, littleEndian);
+                if (tag == 0x0112) {
+                    // We do not really care about type and count, do we?
+                    int orientation = pack(jpeg, offset + 8, 2, littleEndian);
+                    switch (orientation) {
+                        case 1:
+                            return 0;
+                        case 3:
+                            return 180;
+                        case 6:
+                            return 90;
+                        case 8:
+                            return 270;
+                        default:
+                            return 0;
+                    }
+                }
+                offset += 12;
+                length -= 12;
+            }
+        }
+
+        Log.i(TAG, "Orientation not found");
+        return 0;
+    }
+
+    private static int pack(byte[] bytes, int offset, int length, boolean littleEndian) {
+        int step = 1;
+        if (littleEndian) {
+            offset += length - 1;
+            step = -1;
+        }
+
+        int value = 0;
+        while (length-- > 0) {
+            value = (value << 8) | (bytes[offset] & 0xFF);
+            offset += step;
+        }
+        return value;
+    }
+
+
+
+    /**
+     * 获取原始图片的角度(解决三星手机拍照后图片是横着的问题)
+     * @param path 图片的绝对路径
+     * @return 原始图片的角度
+     */
+    public static int getBitmapDegree(String path) {
+        int degree = 0;
+        try {
+            // 从指定路径下读取图片,并获取其EXIF信息
+            ExifInterface exifInterface = new ExifInterface(path);
+            // 获取图片的旋转信息
+            int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,
+                    ExifInterface.ORIENTATION_NORMAL);
+            Log.e("jxf", "orientation" + orientation);
+            switch (orientation) {
+                case ExifInterface.ORIENTATION_ROTATE_90:
+                    degree = 90;
+                    break;
+                case ExifInterface.ORIENTATION_ROTATE_180:
+                    degree = 180;
+                    break;
+                case ExifInterface.ORIENTATION_ROTATE_270:
+                    degree = 270;
+                    break;
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return degree;
+    }
+
+
+}

+ 16 - 0
scancardlibrary/src/main/java/io/card/payment/Validator.java

@@ -0,0 +1,16 @@
+package io.card.payment;
+
+/* Validator.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+import android.text.InputFilter;
+import android.text.TextWatcher;
+
+interface Validator extends TextWatcher, InputFilter {
+    String getValue();
+
+    boolean isValid();
+
+    boolean hasFullLength();
+}

+ 291 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/I18nManager.java

@@ -0,0 +1,291 @@
+package io.card.payment.i18n;
+
+/* i18nManager.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Manages the internationalization
+ *
+ * @author jbrateman
+ */
+
+public class I18nManager<E extends Enum<?>> {
+    private static final String TAG = I18nManager.class.getSimpleName();
+
+    /**
+     * Map of incorrect locales that we accept, and their target locale
+     */
+    private static final Map<String, String> SPECIAL_LOCALE_MAP = new HashMap<String, String>();
+
+    /**
+     * Set of all RTL locales
+     */
+    private static final Set<String> RIGHT_TO_LEFT_LOCALE_SET = new HashSet<String>();
+
+    static {
+        SPECIAL_LOCALE_MAP.put("zh_CN", "zh-Hans");
+        SPECIAL_LOCALE_MAP.put("zh_TW", "zh-Hant_TW");
+        SPECIAL_LOCALE_MAP.put("zh_HK", "zh-Hant");
+        SPECIAL_LOCALE_MAP.put("en_UK", "en_GB");
+        SPECIAL_LOCALE_MAP.put("en_IE", "en_GB");
+        /*
+         * Support Hebrew from phone settings (see
+         * http://code.google.com/p/android/issues/detail?id=3639)
+         */
+        SPECIAL_LOCALE_MAP.put("iw_IL", "he");
+        SPECIAL_LOCALE_MAP.put("no", "nb");
+
+        RIGHT_TO_LEFT_LOCALE_SET.add("he");
+        RIGHT_TO_LEFT_LOCALE_SET.add("ar");
+    }
+
+    private Map<String, SupportedLocale<E>> supportedLocales;
+    private SupportedLocale<E> currentLocale;
+    private Class<E> enumClazz;
+
+    public I18nManager(Class<E> enumClazz, List<SupportedLocale<E>> locales) {
+        this.supportedLocales = new LinkedHashMap<String, SupportedLocale<E>>();
+        this.enumClazz = enumClazz;
+
+        // add all supported locales
+        for (SupportedLocale<E> locale : locales) {
+            addLocale(locale);
+        }
+
+        // start off with defaults
+        setLanguage(null);
+    }
+
+    /**
+     * Prints out error messages for missing localizations of display messages
+     *
+     * @param localeName
+     */
+    private void logMissingLocalizations(String localeName) {
+        List<String> errorMessages = getMissingLocaleMessages(localeName);
+
+        for (String errorMessage : errorMessages) {
+            Log.i(TAG, errorMessage);
+        }
+    }
+
+    /**
+     * Returns a list of all missing localizations over all locales. Useful for
+     * testing.
+     *
+     * @return
+     */
+    public List<String> getMissingLocaleMessages() {
+        List<String> errorMessages = new ArrayList<String>();
+        for (String locale : supportedLocales.keySet()) {
+            errorMessages.addAll(getMissingLocaleMessages(locale));
+        }
+
+        return errorMessages;
+    }
+
+    /**
+     * Returns a list of all the missing localizations in the specific locale.
+     *
+     * @return
+     */
+    private List<String> getMissingLocaleMessages(String localeName) {
+        SupportedLocale<E> locale = supportedLocales.get(localeName);
+        List<String> errorMessages = new ArrayList<String>();
+
+        for (E key : enumClazz.getEnumConstants()) {
+            String prettyKeyValue = "[" + localeName + "," + key + "]";
+
+            if (null == locale.getAdaptedDisplay(key, null)) {
+                errorMessages.add("Missing " + prettyKeyValue);
+            }
+        }
+
+        return errorMessages;
+    }
+
+    /**
+     * Sets the locale to the given locale specifier if not null. If null or not
+     * found, fall back to defaults.
+     *
+     * @param localeSpecifier
+     */
+    public void setLanguage(String localeSpecifier) {
+        // reset current locale since we're trying to set it to something new.
+        currentLocale = null;
+
+        currentLocale = getLocaleFromSpecifier(localeSpecifier);
+
+        assert currentLocale != null;
+        Log.d(TAG, "setting locale to:" + currentLocale.getName());
+    }
+
+    /**
+     * Returns the SupportedLocale object corresponding to the provided
+     * localeSpecifier, if found. If not found, use phone settings, then default
+     * to English.
+     *
+     * @param localeSpecifier
+     * @return
+     */
+    public SupportedLocale<E> getLocaleFromSpecifier(String localeSpecifier) {
+        SupportedLocale<E> foundLocale = null;
+        if (null != localeSpecifier) {
+            foundLocale = lookupSupportedLocale(localeSpecifier);
+        }
+
+        if (null == foundLocale) {
+            // use default phone
+            String phoneLanguage = Locale.getDefault().toString();
+            Log.d(TAG, localeSpecifier + " not found.  Attempting to look for " + phoneLanguage);
+
+            foundLocale = lookupSupportedLocale(phoneLanguage);
+        }
+
+        if (null == foundLocale) {
+            // use english
+            Log.d(TAG, "defaulting to english");
+            foundLocale = supportedLocales.get("en");
+        }
+
+        assert foundLocale != null;
+        return foundLocale;
+    }
+
+    /**
+     * Attempts to look up the locale based on the specifier. These are pretty
+     * specific rules, so here's the summarized description from iOS:
+     * <p/>
+     * Can be specified as a language code ("en", "fr", "zh-Hans", etc.) or as a
+     * locale ("en_AU", "fr_FR", "zh-Hant_TW", etc.).
+     * <p/>
+     * If the library does not contain localized strings for a specified locale,
+     * then will fall back to the language.
+     * <p/>
+     * E.g., "es_CO" -> "es".
+     * <p/>
+     * If the library does not contain localized strings for a specified region,
+     * then will fall back to American English.
+     * <p/>
+     * 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.
+     * <p/>
+     * E.g., specifying "en" on a device set to "English" and "United Kingdom"
+     * will result in "en_GB".
+     *
+     * @param localeSpecifier
+     * @return
+     */
+    private SupportedLocale<E> lookupSupportedLocale(final String localeSpecifier) {
+        if (null == localeSpecifier || localeSpecifier.length() < 2) {
+            // not enough info provided
+            return null;
+        }
+
+        SupportedLocale<E> supportedLocale = null;
+
+        // special cases taken care of first
+        if (SPECIAL_LOCALE_MAP.containsKey(localeSpecifier)) {
+            String localeToUse = SPECIAL_LOCALE_MAP.get(localeSpecifier);
+            supportedLocale = supportedLocales.get(localeToUse);
+            Log.d(TAG, "Overriding locale specifier " + localeSpecifier + " with " + localeToUse);
+        }
+
+        // First try for <language>_<COUNTRY>:
+        if (null == supportedLocale) {
+            String language_country;
+            if (localeSpecifier.contains("_")) {
+                language_country = localeSpecifier;
+            } else {
+                // append country code to the current localeSpecifier. This handles the case where
+                // the locale is set to "en", and country is set to "GB", so we want to set language
+                // to "en_GB"
+                language_country = localeSpecifier + "_" + Locale.getDefault().getCountry();
+            }
+            supportedLocale = supportedLocales.get(language_country);
+        }
+
+        // Next, fall back to the exact requested locale (specifically handles zh-Hans case):
+        if (null == supportedLocale) {
+            supportedLocale = supportedLocales.get(localeSpecifier);
+        }
+
+        // Next, fall back to just the stripped off <language>:
+        if (null == supportedLocale) {
+            String languageCode = localeSpecifier.substring(0, 2);
+            supportedLocale = supportedLocales.get(languageCode);
+        }
+
+        return supportedLocale;
+    }
+
+    public String getString(E key) {
+        return getString(key, currentLocale);
+    }
+
+    public String getString(E key, SupportedLocale<E> localeToTranslate) {
+        String countryCode = Locale.getDefault().getCountry().toUpperCase(Locale.US);
+        String s = localeToTranslate.getAdaptedDisplay(key, countryCode);
+
+        if (s == null) {
+            String errorMessage =
+                    "Missing localized string for [" + currentLocale.getName() + ",Key."
+                            + key.toString() + "]";
+            Log.i(TAG, errorMessage);
+
+            // return what we have in the canonical "en"
+            // and if that's missing fake it
+            s = supportedLocales.get("en").getAdaptedDisplay(key, countryCode);
+        }
+
+        if (s == null) {
+            Log.i(TAG, "Missing localized string for [en,Key." + key.toString()
+                    + "], so defaulting to keyname");
+            s = key.toString();
+        }
+
+        return s;
+    }
+
+    public List<String> getSupportedLocales() {
+        return new ArrayList<String>(supportedLocales.keySet());
+    }
+
+    /**
+     * Adds a supported locale
+     *
+     * @param supportedLocale
+     */
+    private void addLocale(SupportedLocale<E> supportedLocale) {
+        String localeName = supportedLocale.getName();
+        if (null == localeName) {
+            throw new RuntimeException("Null localeName");
+        }
+        if (supportedLocales.containsKey(localeName)) {
+            throw new RuntimeException("Locale " + localeName + " already added");
+        }
+        supportedLocales.put(localeName, supportedLocale);
+
+        logMissingLocalizations(localeName);
+    }
+
+    /**
+     * Returns <code>true</code> if the current locale is right-to-left
+     */
+    public boolean isCurrentLocaleRightToLeftLang() {
+        return RIGHT_TO_LEFT_LOCALE_SET.contains(currentLocale.getName());
+    }
+}

+ 69 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/LocalizedStrings.java

@@ -0,0 +1,69 @@
+package io.card.payment.i18n;
+
+/* LocalizedStrings.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+import android.content.Intent;
+
+import java.util.Collection;
+
+import io.card.payment.CardIOActivity;
+import io.card.payment.i18n.locales.LocalizedStringsList;
+
+
+/**
+ * String constants available for localizing. Public class because of tests.
+ */
+public final class LocalizedStrings {
+    /**
+     * Returns the localized message to display for the given key.
+     *
+     * @param key - which UI string
+     * @return localized version of this string
+     */
+    public static String getString(StringKey key) {
+        return i18nManager.getString(key);
+    }
+
+    /**
+     * Returns the localized message to display for the given key, and given localization. This
+     * method is only for static use by implementing apps/libraries, and should only be used to
+     * override any previous locale that could have possibly been set (or cleared).
+     *
+     * @param key The UI string key
+     * @param languageOrLocale the target translation locale
+     * @return localized version of this string
+     */
+    public static String getString(StringKey key, String languageOrLocale) {
+        return i18nManager.getString(key, i18nManager.getLocaleFromSpecifier(languageOrLocale));
+    }
+
+    /**
+     * Sets the language for an activity. Should be called in onCreate, before any possible messages
+     * are generated.
+     *
+     * @param intent
+     */
+    public static void setLanguage(Intent intent) {
+        i18nManager.setLanguage(intent.getStringExtra(CardIOActivity.EXTRA_LANGUAGE_OR_LOCALE));
+    }
+
+    private static final I18nManager<StringKey> i18nManager;
+
+    static {
+        /**
+         * TODO optimize this for lazy loading. Should be relatively easy.
+         */
+        i18nManager = new I18nManager<>(StringKey.class, LocalizedStringsList.ALL_LOCALES);
+    }
+
+    /**
+     * Return the missing locale messages for testing
+     *
+     * @return
+     */
+    static Collection<String> getMissingLocaleMessages() {
+        return i18nManager.getMissingLocaleMessages();
+    }
+}

+ 36 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/StringKey.java

@@ -0,0 +1,36 @@
+package io.card.payment.i18n;
+
+/* StringKey.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+public enum StringKey {
+
+    // @formatter:off
+
+    // common between iOS and android
+    CANCEL,
+    CARDTYPE_AMERICANEXPRESS,
+    CARDTYPE_DISCOVER,
+    CARDTYPE_JCB,
+    CARDTYPE_MASTERCARD,
+    CARDTYPE_MAESTRO,
+    CARDTYPE_VISA,
+    DONE,
+    ENTRY_CVV,
+    ENTRY_POSTAL_CODE,
+    ENTRY_CARDHOLDER_NAME,
+    ENTRY_EXPIRES,
+    EXPIRES_PLACEHOLDER,
+    SCAN_GUIDE,
+
+    // android specific
+    KEYBOARD,
+    ENTRY_CARD_NUMBER,
+    MANUAL_ENTRY_TITLE,
+    ERROR_NO_DEVICE_SUPPORT,
+    ERROR_CAMERA_CONNECT_FAIL,
+    ERROR_CAMERA_UNEXPECTED_FAIL
+
+    // @formatter:on
+}

+ 28 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/SupportedLocale.java

@@ -0,0 +1,28 @@
+package io.card.payment.i18n;
+
+/* SupportedLocale.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+/**
+ * A locale supported by this particular sdk or library.
+ */
+public interface SupportedLocale<E extends Enum<?>> {
+    /**
+     * Returns the name of this locale
+     *
+     * @return
+     */
+    String getName();
+
+    /**
+     * Returns a country-adapted string translation of the given key. If no
+     * adaptation is available, or no country is provided, the default
+     * translation for the given key is returned.
+     *
+     * @param key
+     * @param country (Optional)
+     * @return
+     */
+    String getAdaptedDisplay(E key, String country);
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsAR.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.121072 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsAR implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "ar";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsAR() {
+        mDisplay.put(StringKey.CANCEL, "إلغاء");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express‏");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover‏");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB‏");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard‏");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa‏");
+        mDisplay.put(StringKey.DONE, "تم");
+        mDisplay.put(StringKey.ENTRY_CVV, "CVV‏");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "الرمز البريدي");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "اسم صاحب البطاقة");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "تاريخ انتهاء الصلاحية");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/YY‏");
+        mDisplay.put(StringKey.SCAN_GUIDE, "امسك البطاقة هنا.\n ستمسح تلقائيا.");
+        mDisplay.put(StringKey.KEYBOARD, "لوحة المفاتيح…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "رقم البطاقة");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "تفاصيل البطاقة");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "هذا الجهاز لا يمكنه استعمال الكاميرا لقراءة أرقام البطاقة.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "كاميرا الجهاز غير متاحة.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "الجهاز حدث به خطا غير متوقع عند فتح الكاميرا.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsDA.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.124677 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsDA implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "da";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsDA() {
+        mDisplay.put(StringKey.CANCEL, "Annuller");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "Udført");
+        mDisplay.put(StringKey.ENTRY_CVV, "Kontrolcifre");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "Postnummer");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Kortindehaverens navn");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Udløbsdato");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/ÅÅ");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Hold kortet her.\nDet scannes automatisk.");
+        mDisplay.put(StringKey.KEYBOARD, "Tastatur…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "Kortnummer");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Kortoplysninger");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "Denne enhed kan ikke anvende kameraet til at læse kortnumre.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "Enhed kamera ikke er tilgængelig.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "Enheden havde en uventet fejl under åbning af kamera.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsDE.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.126851 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsDE implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "de";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsDE() {
+        mDisplay.put(StringKey.CANCEL, "Abbrechen");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "Fertig");
+        mDisplay.put(StringKey.ENTRY_CVV, "Prüfnr.");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "PLZ");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Karteninhaber");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Gültig bis");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/JJ");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Kreditkarte hierhin halten.\nSie wird automatisch gelesen.");
+        mDisplay.put(StringKey.KEYBOARD, "Tastatur…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "Kartennummer");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Kreditkartendetails");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "Dieses Gerät kann mit der Kamera keine Kreditkartennummern lesen.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "Die Kamera ist nicht verfügbar.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "Beim Öffnen der Kamera ist ein unerwarteter Fehler aufgetreten.");
+
+        // no adapted_translations found
+    }
+}

+ 57 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsEN.java

@@ -0,0 +1,57 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.129094 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsEN implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "en";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsEN() {
+        mDisplay.put(StringKey.CANCEL, "Cancel");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_MAESTRO, "Maestro");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "Done");
+        mDisplay.put(StringKey.ENTRY_CVV, "CVV");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "Postal Code");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Cardholder Name");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Expires");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/YY");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Hold card here.\nIt will scan automatically.");
+        mDisplay.put(StringKey.KEYBOARD, "Keyboard…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "Card Number");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Card Details");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "This device cannot use the camera to read card numbers.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "Device camera is unavailable.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "The device had an unexpected error opening the camera.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsEN_AU.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.131398 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsEN_AU implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "en_AU";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsEN_AU() {
+        mDisplay.put(StringKey.CANCEL, "Cancel");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "Done");
+        mDisplay.put(StringKey.ENTRY_CVV, "CVV");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "Postcode");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Cardholder Name");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Expires");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/YY");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Hold card here.\nIt will scan automatically.");
+        mDisplay.put(StringKey.KEYBOARD, "Keyboard…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "Card Number");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Card Details");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "This device cannot use the camera to read card numbers.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "Device camera is unavailable.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "The device had an unexpected error opening the camera.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsEN_GB.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.133605 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsEN_GB implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "en_GB";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsEN_GB() {
+        mDisplay.put(StringKey.CANCEL, "Cancel");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "Done");
+        mDisplay.put(StringKey.ENTRY_CVV, "CVV");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "Postcode");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Cardholder Name");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Expires");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/YY");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Hold card here.\nIt will scan automatically.");
+        mDisplay.put(StringKey.KEYBOARD, "Keyboard…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "Card Number");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Card Details");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "This device cannot use the camera to read card numbers.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "Device camera is unavailable.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "The device had an unexpected error opening the camera.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsES.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.138046 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsES implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "es";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsES() {
+        mDisplay.put(StringKey.CANCEL, "Cancelar");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "Hecho");
+        mDisplay.put(StringKey.ENTRY_CVV, "CVV");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "Código postal");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Nombre del titular de la tarjeta");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Vence");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/AA");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Mantén la tarjeta aquí.\nSe escaneará automáticamente.");
+        mDisplay.put(StringKey.KEYBOARD, "Teclado…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "Número de tarjeta");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Detalles de la tarjeta");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "Este dispositivo no puede usar la cámara para leer números de tarjeta.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "La cámara del dispositivo no está disponible.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "Al abrir la cámara, el dispositivo ha experimentado un error inesperado.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsES_MX.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.140560 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsES_MX implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "es_MX";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsES_MX() {
+        mDisplay.put(StringKey.CANCEL, "Cancelar");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "Listo");
+        mDisplay.put(StringKey.ENTRY_CVV, "CVV");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "Código postal");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Nombre del titular");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Vence");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/AA");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Coloque la tarjeta aquí.\nSe escaneará automáticamente.");
+        mDisplay.put(StringKey.KEYBOARD, "Teclado…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "N.° de tarjeta");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Detalles de la tarjeta");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "Este dispositivo no puede usar la cámara para leer números de tarjeta.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "La cámara del dispositivo no está disponible.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "El dispositivo tuvo un error inesperado al abrir la cámara.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsFR.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.142847 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsFR implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "fr";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsFR() {
+        mDisplay.put(StringKey.CANCEL, "Annuler");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "OK");
+        mDisplay.put(StringKey.ENTRY_CVV, "Crypto.");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "Code postal");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Nom du titulaire de la carte");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Date d’expiration");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/AA");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Maintenez la carte à cet endroit.\nElle va être automatiquement scannée.");
+        mDisplay.put(StringKey.KEYBOARD, "Clavier…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "Nº de carte");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Carte");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "Cet appareil ne peut pas utiliser l’appareil photo pour lire les numéros de carte.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "L’appareil photo n’est pas disponible.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "Une erreur s’est produite en ouvrant l’appareil photo.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsHE.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.145036 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsHE implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "he";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsHE() {
+        mDisplay.put(StringKey.CANCEL, "ביטול");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "אמריקן אקספרס");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover‏");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB‏");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "מאסטרקארד");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "ויזה");
+        mDisplay.put(StringKey.DONE, "בוצע");
+        mDisplay.put(StringKey.ENTRY_CVV, "קוד אימות כרטיס");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "מיקוד");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "שם בעל הכרטיס");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "תאריך תפוגה");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/YY‏");
+        mDisplay.put(StringKey.SCAN_GUIDE, "החזק את הכרטיס כאן.\nהסריקה תתבצע באופן אוטומטי.");
+        mDisplay.put(StringKey.KEYBOARD, "מקלדת…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "מספר כרטיס");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "פרטי כרטיס");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "המכשיר אינו מסוגל להשתמש במצלמה לקריאת מספרי כרטיס.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "מצלמת המכשיר אינה זמינה.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "המכשיר נתקל בשגיאה בלתי צפויה בזמן הפעלת המצלמה.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsIS.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.147236 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsIS implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "is";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsIS() {
+        mDisplay.put(StringKey.CANCEL, "Hætta við");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "Lokið");
+        mDisplay.put(StringKey.ENTRY_CVV, "CVV");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "Póstnúmer");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Nafn Korthafa");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Rennur út");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/ÁÁ");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Haltu kortinu kyrru hér.\nÞað verður sjálvirkt skannað.");
+        mDisplay.put(StringKey.KEYBOARD, "Lyklaborð…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "Kortanúmar");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Kortaupplýsingar");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "Þetta tæki getur ekki notað myndavélina til að lesa af númer af kortinu.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "Ekki næst samband við myndavélina.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "Upp kom villa við að opna myndavélina..");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsIT.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.149443 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsIT implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "it";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsIT() {
+        mDisplay.put(StringKey.CANCEL, "Annulla");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "OK");
+        mDisplay.put(StringKey.ENTRY_CVV, "CVV");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "CAP");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Titolare della carta");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Scadenza");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/AA");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Inquadra la carta.\nLa scansione è automatica.");
+        mDisplay.put(StringKey.KEYBOARD, "Tastiera…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "Numero di carta");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Dati carta");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "La fotocamera non legge il numero di carta.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "Fotocamera non disponibile.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "Errore inatteso nell’apertura della fotocamera.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsJA.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.151634 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsJA implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "ja";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsJA() {
+        mDisplay.put(StringKey.CANCEL, "キャンセル");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "完了");
+        mDisplay.put(StringKey.ENTRY_CVV, "カード確認コード");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "郵便番号");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "カード保有者の名前");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "有効期限");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/YY");
+        mDisplay.put(StringKey.SCAN_GUIDE, "ここでカードをお持ちください。\n自動的にスキャンされます。");
+        mDisplay.put(StringKey.KEYBOARD, "キーボード…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "カード番号");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "カードの詳細");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "この端末ではカード番号の読込にカメラを使えません。");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "端末のカメラを使用できません。");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "カメラを起動中に予期しないエラーが発生しました。");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsKO.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.153840 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsKO implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "ko";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsKO() {
+        mDisplay.put(StringKey.CANCEL, "취소");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "완료");
+        mDisplay.put(StringKey.ENTRY_CVV, "CVV");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "우편번호");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "카드 소유자 이름");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "유효기간");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM / YY");
+        mDisplay.put(StringKey.SCAN_GUIDE, "카드를 여기에 갖다 대세요.\n자동으로 스캔됩니다.");
+        mDisplay.put(StringKey.KEYBOARD, "키보드…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "카드 번호");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "카드 세부정보");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "기기가 카메라를 이용한 카드 숫자 판독을 지원하지 않습니다.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "기기에서 카메라를 사용할 수 없습니다.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "기기에서 카메라를 여는 동안 예상치 못한 오류가 발생했습니다.");
+
+        // no adapted_translations found
+    }
+}

+ 45 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsList.java

@@ -0,0 +1,45 @@
+package io.card.payment.i18n.locales;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.185885 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsList {
+    public static final List<SupportedLocale<StringKey>> ALL_LOCALES = new ArrayList<>();
+
+    static {
+        ALL_LOCALES.add(new LocalizedStringsAR());
+        ALL_LOCALES.add(new LocalizedStringsDA());
+        ALL_LOCALES.add(new LocalizedStringsDE());
+        ALL_LOCALES.add(new LocalizedStringsEN());
+        ALL_LOCALES.add(new LocalizedStringsEN_AU());
+        ALL_LOCALES.add(new LocalizedStringsEN_GB());
+        ALL_LOCALES.add(new LocalizedStringsES());
+        ALL_LOCALES.add(new LocalizedStringsES_MX());
+        ALL_LOCALES.add(new LocalizedStringsFR());
+        ALL_LOCALES.add(new LocalizedStringsHE());
+        ALL_LOCALES.add(new LocalizedStringsIS());
+        ALL_LOCALES.add(new LocalizedStringsIT());
+        ALL_LOCALES.add(new LocalizedStringsJA());
+        ALL_LOCALES.add(new LocalizedStringsKO());
+        ALL_LOCALES.add(new LocalizedStringsMS());
+        ALL_LOCALES.add(new LocalizedStringsNB());
+        ALL_LOCALES.add(new LocalizedStringsNL());
+        ALL_LOCALES.add(new LocalizedStringsPL());
+        ALL_LOCALES.add(new LocalizedStringsPT());
+        ALL_LOCALES.add(new LocalizedStringsPT_BR());
+        ALL_LOCALES.add(new LocalizedStringsRU());
+        ALL_LOCALES.add(new LocalizedStringsSV());
+        ALL_LOCALES.add(new LocalizedStringsTH());
+        ALL_LOCALES.add(new LocalizedStringsTR());
+        ALL_LOCALES.add(new LocalizedStringsZH_HANS());
+        ALL_LOCALES.add(new LocalizedStringsZH_HANT());
+        ALL_LOCALES.add(new LocalizedStringsZH_HANT_TW());
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsMS.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.156058 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsMS implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "ms";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsMS() {
+        mDisplay.put(StringKey.CANCEL, "Batal");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "Selesai");
+        mDisplay.put(StringKey.ENTRY_CVV, "CVV");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "Poskod");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Nama Pemegang Kad");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Luput");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "BB/TT");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Pegang kad di sini.\nIa akan mengimbas secara automatik.");
+        mDisplay.put(StringKey.KEYBOARD, "Papan Kekunci…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "Nombor Kad");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Butiran Kad");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "Peranti ini tidak dapat menggunakan kamera untuk membaca nombor kad.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "Kamera peranti tidak tersedia.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "Peranti mengalami ralat tidak dijangka semasa membuka kamera.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsNB.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.158232 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsNB implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "nb";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsNB() {
+        mDisplay.put(StringKey.CANCEL, "Avbryt");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "Fullført");
+        mDisplay.put(StringKey.ENTRY_CVV, "CVV");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "Postnummer");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Kortinnehaverens navn");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Utløper");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/ÅÅ");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Hold kortet her.\nDet skannes automatisk.");
+        mDisplay.put(StringKey.KEYBOARD, "Tastatur …");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "Kortnummer");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Kortdetaljer");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "Denne enheten kan ikke bruke kameraet til å lese kortnumre.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "Kameraet er utilgjengelig.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "Det oppstod en uventet feil ved kameraoppstart.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsNL.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.160445 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsNL implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "nl";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsNL() {
+        mDisplay.put(StringKey.CANCEL, "Annuleren");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "Gereed");
+        mDisplay.put(StringKey.ENTRY_CVV, "CVV");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "Postcode");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Naam kaarthouder");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Vervaldatum");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/JJ");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Houd uw kaart hier.\nScannen gaat automatisch.");
+        mDisplay.put(StringKey.KEYBOARD, "Toetsenbord…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "Creditcardnummer");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Kaartgegevens");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "Met de camera van dit apparaat kunnen geen kaartnummers worden gelezen.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "Camera apparaat niet beschikbaar.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "Er is een onverwachte fout opgetreden bij het starten van de camera.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsPL.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.162694 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsPL implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "pl";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsPL() {
+        mDisplay.put(StringKey.CANCEL, "Anuluj");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "Gotowe");
+        mDisplay.put(StringKey.ENTRY_CVV, "Kod CVV2/CVC2");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "Kod pocztowy");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Imię i nazwisko posiadacza karty");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Wygasa");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/RR");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Przytrzymaj kartę tutaj.\nZostanie ona zeskanowana automatycznie.");
+        mDisplay.put(StringKey.KEYBOARD, "Klawiatura…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "Numer karty");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Dane karty");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "Na tym urządzeniu nie można odczytać numeru karty za pomocą aparatu.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "Aparat na tym urządzeniu jest niedostepny.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "Przy otwieraniu aparatu na tym urządzeniu wystąpił nieoczekiwany błąd.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsPT.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.164903 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsPT implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "pt";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsPT() {
+        mDisplay.put(StringKey.CANCEL, "Cancelar");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "Concluir");
+        mDisplay.put(StringKey.ENTRY_CVV, "CSC");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "Código postal");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Nome do titular do cartão");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Validade");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/AA");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Segure o cartão aqui.\nSerá lido automaticamente.");
+        mDisplay.put(StringKey.KEYBOARD, "Teclado…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "Número do cartão");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Detalhes do cartão");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "Este dispositivo não pode utilizar a câmara para ler números de cartões.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "A câmara do dispositivo não está disponível.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "Ocorreu um erro inesperado no dispositivo ao abrir a câmara.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsPT_BR.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.167102 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsPT_BR implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "pt_BR";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsPT_BR() {
+        mDisplay.put(StringKey.CANCEL, "Cancelar");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "Concluído");
+        mDisplay.put(StringKey.ENTRY_CVV, "CVV");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "CEP");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Nome do titular do cartão");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Vencimento");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/AA");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Posicionar cartão aqui.\nEle será digitalizado automaticamente.");
+        mDisplay.put(StringKey.KEYBOARD, "Teclado…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "Número do Cartão");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Dados do cartão");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "Este dispositivo não pode usar a câmera para ler números de cartão.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "A câmera do dispositivo não está disponível.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "O dispositivo sofreu um erro inesperado ao abrir a câmera.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsRU.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.169528 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsRU implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "ru";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsRU() {
+        mDisplay.put(StringKey.CANCEL, "Отмена");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "Готово");
+        mDisplay.put(StringKey.ENTRY_CVV, "Код безопасности");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "Индекс");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Имя и фамилия владельца");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Действ. до");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "ММ/ГГ");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Держите карту внутри рамки.\nОна будет считана автоматически.");
+        mDisplay.put(StringKey.KEYBOARD, "Клавиатура…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "Номер карты");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Ввести данные вручную");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "В данном устройстве нет опции считывания номера карты с помощью фотокамеры.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "Фотокамера устройства недоступна.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "Возникла незапланированная ошибка при открытии фотокамеры устройства.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsSV.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.172127 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsSV implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "sv";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsSV() {
+        mDisplay.put(StringKey.CANCEL, "Avbryt");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "Klart");
+        mDisplay.put(StringKey.ENTRY_CVV, "CVV");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "Postnummer");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Kortinnehavarens namn");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Går ut");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/ÅÅ");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Håll kortet här.\nDet skannas automatiskt.");
+        mDisplay.put(StringKey.KEYBOARD, "Tangentbord …");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "Kortnummer");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Kortinformation");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "Den här enheten kan inte använda kameran till att läsa kortnummer.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "Enhetens kamera är inte tillgänglig.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "Ett oväntat fel uppstod när enheten skulle öppna kameran.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsTH.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.174790 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsTH implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "th";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsTH() {
+        mDisplay.put(StringKey.CANCEL, "ยกเลิก");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "เสร็จแล้ว");
+        mDisplay.put(StringKey.ENTRY_CVV, "CVV");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "รหัสไปรษณีย์");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "ชื่อผู้ถือบัตร");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "หมดอายุ");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "ดด/ปป");
+        mDisplay.put(StringKey.SCAN_GUIDE, "ถือบัตรไว้ตรงนี้\nเครื่องจะสแกนโดยอัตโนมัติ");
+        mDisplay.put(StringKey.KEYBOARD, "คีย์บอร์ด…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "หมายเลขบัตร");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "รายละเอียดบัตร");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "อุปกรณ์ไม่สามารถใช้กล้องเพื่ออ่านหมายเลขบัตรได้");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "กล้องของอุปกรณ์ไม่พร้อมใช้งาน");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "อุปกรณ์พบข้อผิดพลาดขณะเปิดกล้อง");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsTR.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.177256 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsTR implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "tr";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsTR() {
+        mDisplay.put(StringKey.CANCEL, "İptal");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "Bitti");
+        mDisplay.put(StringKey.ENTRY_CVV, "CVV");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "Posta Kodu");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "Kart sahibinin adı");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "Son kullanma tarihi");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "AA/YY");
+        mDisplay.put(StringKey.SCAN_GUIDE, "Kartınızı buraya tutun.\nOtomatik olarak taranacaktır.");
+        mDisplay.put(StringKey.KEYBOARD, "Klavye…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "Kart Numarası");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "Kart Ayrıntıları");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "Bu cihazın kamerası kart rakamlarını okuyamaz.");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "Cihaz kamerası kullanılamıyor.");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "Cihaz kamerayı açarken beklenmedik bir hata verdi.");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsZH_HANS.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.179573 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsZH_HANS implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "zh-Hans";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsZH_HANS() {
+        mDisplay.put(StringKey.CANCEL, "取消");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "American Express");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "完成");
+        mDisplay.put(StringKey.ENTRY_CVV, "CVV");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "邮政编码");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "持卡人姓名");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "有效期限:");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "MM/YY");
+        mDisplay.put(StringKey.SCAN_GUIDE, "请将名片对准取景框");
+        mDisplay.put(StringKey.KEYBOARD, "键盘…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "卡号");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "卡详细信息");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "此设备无法使用摄像头读取卡号。");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "设备摄像头不可用。");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "设备打开摄像头时出现意外错误。");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsZH_HANT.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.181771 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsZH_HANT implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "zh-Hant";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsZH_HANT() {
+        mDisplay.put(StringKey.CANCEL, "取消");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "美國運通");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "完成");
+        mDisplay.put(StringKey.ENTRY_CVV, "CVV");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "郵遞區號");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "持卡人名稱");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "到期日");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "月 / 年");
+        mDisplay.put(StringKey.SCAN_GUIDE, "將信用卡置於此處。\n裝置會自動掃描。");
+        mDisplay.put(StringKey.KEYBOARD, "鍵盤…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "卡號");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "信用卡詳細資料");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "此裝置無法使用相機讀取信用卡卡號。");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "無法使用裝置的相機。");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "此裝置啟動相機時發生意外錯誤。");
+
+        // no adapted_translations found
+    }
+}

+ 56 - 0
scancardlibrary/src/main/java/io/card/payment/i18n/locales/LocalizedStringsZH_HANT_TW.java

@@ -0,0 +1,56 @@
+package io.card.payment.i18n.locales;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.card.payment.i18n.StringKey;
+import io.card.payment.i18n.SupportedLocale;
+
+// Class autogenerated!  Do not modify.
+// Generated on 2014-12-10 11:29:25.183955 via script:
+// /Users/twhipple/Documents/buffalo/lib-i18n/i18n/script/generate_android_i18n.py -java_src_path src/ -java_gen_path gen/ -stringkey_path io/card/payment/i18n/ -strings_path ../strings/projects/card.io/strings/ --strict
+
+public class LocalizedStringsZH_HANT_TW implements SupportedLocale<StringKey> {
+
+    private static Map<StringKey, String> mDisplay = new HashMap<>();
+    private static Map<String, String> mAdapted = new HashMap<>();
+
+    @Override
+    public String getName() {
+        return "zh-Hant_TW";
+    }
+
+    @Override
+    public String getAdaptedDisplay(StringKey key, String country) {
+        String adaptedKey = key.toString() + "|" + country;
+        if (mAdapted.containsKey(adaptedKey)) {
+            return mAdapted.get(adaptedKey);
+        } else {
+            return mDisplay.get(key);
+        }
+    }
+
+    public LocalizedStringsZH_HANT_TW() {
+        mDisplay.put(StringKey.CANCEL, "取消");
+        mDisplay.put(StringKey.CARDTYPE_AMERICANEXPRESS, "美國運通");
+        mDisplay.put(StringKey.CARDTYPE_DISCOVER, "Discover");
+        mDisplay.put(StringKey.CARDTYPE_JCB, "JCB");
+        mDisplay.put(StringKey.CARDTYPE_MASTERCARD, "MasterCard");
+        mDisplay.put(StringKey.CARDTYPE_VISA, "Visa");
+        mDisplay.put(StringKey.DONE, "完成");
+        mDisplay.put(StringKey.ENTRY_CVV, "信用卡驗證碼");
+        mDisplay.put(StringKey.ENTRY_POSTAL_CODE, "郵遞區號");
+        mDisplay.put(StringKey.ENTRY_CARDHOLDER_NAME, "持卡人姓名");
+        mDisplay.put(StringKey.ENTRY_EXPIRES, "到期日");
+        mDisplay.put(StringKey.EXPIRES_PLACEHOLDER, "月 / 年");
+        mDisplay.put(StringKey.SCAN_GUIDE, "將信用卡放在此處。\n系統將自動掃描。");
+        mDisplay.put(StringKey.KEYBOARD, "鍵盤…");
+        mDisplay.put(StringKey.ENTRY_CARD_NUMBER, "卡號");
+        mDisplay.put(StringKey.MANUAL_ENTRY_TITLE, "信用卡詳細資料");
+        mDisplay.put(StringKey.ERROR_NO_DEVICE_SUPPORT, "此裝置無法使用相機讀取卡號。");
+        mDisplay.put(StringKey.ERROR_CAMERA_CONNECT_FAIL, "無法使用相機。");
+        mDisplay.put(StringKey.ERROR_CAMERA_UNEXPECTED_FAIL, "啟動相機時發生意外的錯誤。");
+
+        // no adapted_translations found
+    }
+}

+ 133 - 0
scancardlibrary/src/main/java/io/card/payment/ui/ActivityHelper.java

@@ -0,0 +1,133 @@
+package io.card.payment.ui;
+
+/* ActivityHelper.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+import android.annotation.TargetApi;
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+public class ActivityHelper {
+    /**
+     * Request the ActionBar window feature if we are on a supported Android
+     * version. This should be called before the activity's setContentView.
+     *
+     * @param activity
+     */
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    public static void addActionBarIfSupported(Activity activity) {
+        if (actionBarSupported()) {
+            activity.requestWindowFeature(Window.FEATURE_ACTION_BAR);
+        }
+    }
+
+    /**
+     * Sets up the title, appearance, and behavior of the ActionBar if it is
+     * supported. Should be invoked after
+     * {@link #addActionBarIfSupported(Activity)}.
+     *
+     * @param activity
+     * @param titleTextView
+     * @param title
+     * @param titleTextViewPrefix
+     * @param icon
+     */
+    public static void setupActionBarIfSupported(Activity activity, TextView titleTextView,
+                                                 String title, String titleTextViewPrefix, Drawable icon) {
+        if (titleTextViewPrefix == null) {
+            titleTextViewPrefix = "";
+        }
+        activity.setTitle(titleTextViewPrefix + title);
+        if (actionBarSupported() && actionBarNonNull(activity)) {
+            setupActionBar(activity, title, icon);
+            if (titleTextView != null) {
+                titleTextView.setVisibility(View.GONE);
+            }
+        } else {
+            if (titleTextView != null) {
+                titleTextView.setText(title);
+            }
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    private static boolean actionBarNonNull(Activity activity) {
+        return activity.getActionBar() != null;
+    }
+
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    private static void setupActionBar(Activity activity, String title, Drawable icon) {
+        ActionBar bar = activity.getActionBar();
+
+        bar.setBackgroundDrawable(Appearance.ACTIONBAR_BACKGROUND);
+        bar.setTitle(title);
+
+        // Hack to set the actionbar text to white
+        int actionBarTitleId =
+                Resources.getSystem().getIdentifier("action_bar_title", "id", "android");
+        TextView actionBarTextView = (TextView) activity.findViewById(actionBarTitleId);
+        if (actionBarTextView != null) {
+            actionBarTextView.setTextColor(Color.WHITE);
+        }
+
+        bar.setDisplayHomeAsUpEnabled(false);
+        if (icon != null
+                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+            setActionBarHomeIcon(bar, icon);
+            // bar.setDisplayHomeAsUpEnabled(true);
+        } else {
+            bar.setDisplayShowHomeEnabled(false);
+        }
+    }
+
+    private static boolean actionBarSupported() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
+    }
+
+    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+    private static void setActionBarHomeIcon(ActionBar bar, Drawable icon) {
+        bar.setIcon(icon);
+    }
+
+    public static boolean holoSupported() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
+    }
+
+    /**
+     * Overrides whatever theme the activity currently has with either
+     * Theme_Holo_Light or Theme_Light, depending on OS support.
+     *
+     * @param activity
+     * @param useApplicationTheme
+     */
+
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    public static void setActivityTheme(Activity activity, boolean useApplicationTheme) {
+
+        if (useApplicationTheme && 0 != activity.getApplicationInfo().theme) {
+            activity.setTheme(activity.getApplicationInfo().theme);
+        } else if (holoSupported()) {
+            activity.setTheme(android.R.style.Theme_Holo_Light);
+        } else {
+            activity.setTheme(android.R.style.Theme_Light);
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    public static void setFlagSecure(Activity activity) {
+        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+            // Prevent screenshots; FLAG_SECURE is restricted to HoneyComb and above to prevent issues with some Samsung handsets
+            // Please see http://stackoverflow.com/questions/9822076/how-do-i-prevent-android-taking-a-screenshot-when-my-app-goes-to-the-background
+            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
+        }
+    }
+}

+ 173 - 0
scancardlibrary/src/main/java/io/card/payment/ui/Appearance.java

@@ -0,0 +1,173 @@
+package io.card.payment.ui;
+
+/* Appearance.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Paint.Style;
+import android.graphics.Typeface;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.StateListDrawable;
+import android.graphics.drawable.shapes.RectShape;
+
+/**
+ * Appearance constants and utilities
+ */
+public class Appearance {
+
+    // Margin + Padding
+
+    public static final String CONTAINER_MARGIN_HORIZONTAL = "16dip";
+    public static final String CONTAINER_MARGIN_VERTICAL = "20dip";
+
+    public static final String BASE_SPACING = "4dip";
+    public static final String VERTICAL_SPACING = "8dip";
+
+    public static final String BUTTON_HEIGHT = "54dip";
+    public static final String SMALL_BUTTON_HEIGHT = "42dip";
+
+    // States
+
+    public static final int[] BUTTON_STATE_PRESSED = new int[] {
+            android.R.attr.state_pressed, android.R.attr.state_enabled };
+    public static final int[] BUTTON_STATE_NORMAL = new int[] { android.R.attr.state_enabled };
+    public static final int[] BUTTON_STATE_DISABLED = new int[] { -android.R.attr.state_enabled };
+    public static final int[] BUTTON_STATE_FOCUSED = new int[] { android.R.attr.state_focused };
+
+    // Colors
+
+    public static final int PAY_BLUE_COLOR = Color.parseColor("#003087");
+    public static final int PAL_BLUE_COLOR = Color.parseColor("#009CDE");
+    public static final int PAL_BLUE_COLOR_OPACITY_66 = Color.parseColor("#aa009CDE");
+
+    // Background colors
+
+    public static final Drawable ACTIONBAR_BACKGROUND = new ColorDrawable(
+            Color.parseColor("#717074"));
+    public static final int DEFAULT_BACKGROUND_COLOR = Color.parseColor("#f5f5f5");
+
+    public static final int BUTTON_PRIMARY_NORMAL_COLOR = PAL_BLUE_COLOR;
+    public static final int BUTTON_PRIMARY_FOCUS_COLOR = PAL_BLUE_COLOR_OPACITY_66;
+    public static final int BUTTON_PRIMARY_PRESSED_COLOR = PAY_BLUE_COLOR;
+    public static final int BUTTON_PRIMARY_DISABLED_COLOR = Color.parseColor("#c5ddeb");
+
+    public static final int BUTTON_SECONDARY_NORMAL_COLOR = Color.parseColor("#717074");
+    public static final int BUTTON_SECONDARY_FOCUS_COLOR = Color.parseColor("#aa717074");
+    public static final int BUTTON_SECONDARY_PRESSED_COLOR = Color.parseColor("#5a5a5d");
+    public static final int BUTTON_SECONDARY_DISABLED_COLOR = Color.parseColor("#f5f5f5");
+
+    // Text colors
+
+    public static final int TEXT_COLOR_LIGHT = Color.parseColor("#515151"); // Style guide says
+    // #5e5e5d, but seems
+    // inconsistent
+    public static final int TEXT_COLOR_EDIT_TEXT = Color.DKGRAY;
+    public static final int TEXT_COLOR_EDIT_TEXT_HINT = Color.LTGRAY;
+    public static final int TEXT_COLOR_ERROR = Color.parseColor("#b32317");
+
+    public static final int TEXT_COLOR_LABEL = TEXT_COLOR_LIGHT;
+    public static final int TEXT_COLOR_BUTTON = Color.WHITE;
+
+    // Text sizes
+
+    public static final float TEXT_SIZE_BUTTON = 20.0f;
+    public static final float TEXT_SIZE_MEDIUM_BUTTON = 16.0f;
+    public static final float TEXT_SIZE_SMALL_BUTTON = 14.0f;
+
+    // Typefaces
+    public static final Typeface TYPEFACE_BUTTON = typefaceLight();
+
+    // Focus: For console support, focus is indicated by a box around clickable elements. Other
+    // elements must be padded by this amount for proper alignment. The width of the focus box is
+    // calculated at half of this padding amount.
+    public static final String FOCUS_BORDER_PADDING = "4dip";
+
+
+    public static Drawable buttonBackgroundPrimary(Context context) {
+        StateListDrawable d = new StateListDrawable();
+        d.addState(BUTTON_STATE_PRESSED, new ColorDrawable(BUTTON_PRIMARY_PRESSED_COLOR));
+        d.addState(BUTTON_STATE_DISABLED, new ColorDrawable(BUTTON_PRIMARY_DISABLED_COLOR));
+        d.addState(BUTTON_STATE_FOCUSED, buttonBackgroundPrimaryFocused(context));
+        d.addState(BUTTON_STATE_NORMAL, buttonBackgroundPrimaryNormal(context));
+        return d;
+    }
+
+    private static float getFocusBorderWidthPixels(Context context) {
+        float scale = context.getResources().getDisplayMetrics().density;
+        float adjustedwidth =
+                (ViewUtil.typedDimensionValueToPixels(FOCUS_BORDER_PADDING, context) / 2.0f)
+                        * scale;
+        return adjustedwidth;
+    }
+
+    private static Drawable buttonBackgroundPrimaryNormal(Context context) {
+        return buttonNormal(BUTTON_PRIMARY_NORMAL_COLOR, getFocusBorderWidthPixels(context));
+    }
+
+    private static Drawable buttonBackgroundPrimaryFocused(Context context) {
+        return buttonFocused(
+                BUTTON_PRIMARY_NORMAL_COLOR, BUTTON_PRIMARY_FOCUS_COLOR,
+                getFocusBorderWidthPixels(context));
+    }
+
+    public static Drawable buttonBackgroundSecondary(Context context) {
+        StateListDrawable d = new StateListDrawable();
+        d.addState(BUTTON_STATE_PRESSED, new ColorDrawable(BUTTON_SECONDARY_PRESSED_COLOR));
+        d.addState(BUTTON_STATE_DISABLED, new ColorDrawable(BUTTON_SECONDARY_DISABLED_COLOR));
+        d.addState(BUTTON_STATE_FOCUSED, buttonBackgroundSecondaryFocused(context));
+        d.addState(BUTTON_STATE_NORMAL, buttonBackgroundSecondaryNormal(context));
+        return d;
+    }
+
+    private static Drawable buttonBackgroundSecondaryNormal(Context context) {
+        return buttonNormal(BUTTON_SECONDARY_NORMAL_COLOR, getFocusBorderWidthPixels(context));
+    }
+
+    private static Drawable buttonBackgroundSecondaryFocused(Context context) {
+        return buttonFocused(
+                BUTTON_SECONDARY_NORMAL_COLOR, BUTTON_SECONDARY_FOCUS_COLOR,
+                getFocusBorderWidthPixels(context));
+    }
+
+    private static Drawable buttonNormal(int color, float width) {
+        Drawable[] layers = new Drawable[2];
+        layers[0] = new ColorDrawable(color);
+        ShapeDrawable s = new ShapeDrawable(new RectShape());
+        s.getPaint().setStrokeWidth(2 * width);
+        s.getPaint().setStyle(Style.STROKE);
+        s.getPaint().setColor(DEFAULT_BACKGROUND_COLOR);
+        layers[1] = s;
+        LayerDrawable ld = new LayerDrawable(layers);
+        return ld;
+    }
+
+    private static Drawable buttonFocused(int backgroundColor, int focusBoxColor,
+                                          float scaledBorderWidth) {
+        Drawable[] layers = new Drawable[3];
+        layers[0] = new ColorDrawable(backgroundColor);
+
+        ShapeDrawable s = new ShapeDrawable(new RectShape());
+        s.getPaint().setStrokeWidth(2 * scaledBorderWidth);
+        s.getPaint().setStyle(Style.STROKE);
+        s.getPaint().setColor(DEFAULT_BACKGROUND_COLOR);
+        layers[1] = s;
+
+        ShapeDrawable s2 = new ShapeDrawable(new RectShape());
+        s2.getPaint().setStrokeWidth(scaledBorderWidth);
+        s2.getPaint().setStyle(Style.STROKE);
+        s2.getPaint().setColor(focusBoxColor);
+        layers[2] = s2;
+
+        LayerDrawable ld = new LayerDrawable(layers);
+        return ld;
+    }
+
+    private static Typeface typefaceLight() {
+        return Typeface.create("sans-serif-light", Typeface.NORMAL);
+    }
+}

+ 163 - 0
scancardlibrary/src/main/java/io/card/payment/ui/ViewUtil.java

@@ -0,0 +1,163 @@
+package io.card.payment.ui;
+
+/* ViewUtil.java
+ * See the file "LICENSE.md" for the full license governing this code.
+ */
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.Button;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility methods for altering views.
+ */
+public class ViewUtil {
+    /**
+     * Wrapper to only use the deprecated {@link View#setBackgroundDrawable} on
+     * older systems.
+     *
+     * @param view
+     * @param drawable
+     */
+    @SuppressWarnings("deprecation")
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+    public static void setBackground(View view, Drawable drawable) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+            view.setBackground(drawable);
+        } else {
+            view.setBackgroundDrawable(drawable);
+        }
+    }
+
+    // DIMENSION HELPERS
+
+    // see also similar work: http://stackoverflow.com/a/11353603/306657
+    private static final Map<String, Integer> DIMENSION_STRING_CONSTANT =
+            initDimensionStringConstantMap();
+    static Pattern DIMENSION_VALUE_PATTERN = Pattern
+            .compile("^\\s*(\\d+(\\.\\d+)*)\\s*([a-zA-Z]+)\\s*$");
+
+    static Map<String, Integer> initDimensionStringConstantMap() {
+        Map<String, Integer> m = new HashMap<String, Integer>();
+        m.put("px", TypedValue.COMPLEX_UNIT_PX);
+        m.put("dip", TypedValue.COMPLEX_UNIT_DIP);
+        m.put("dp", TypedValue.COMPLEX_UNIT_DIP);
+        m.put("sp", TypedValue.COMPLEX_UNIT_SP);
+        m.put("pt", TypedValue.COMPLEX_UNIT_PT);
+        m.put("in", TypedValue.COMPLEX_UNIT_IN);
+        m.put("mm", TypedValue.COMPLEX_UNIT_MM);
+        return Collections.unmodifiableMap(m);
+    }
+
+    public static int typedDimensionValueToPixelsInt(String dimensionValueString, Context context) {
+        if (dimensionValueString == null) {
+            return 0;
+        } else {
+            return (int) typedDimensionValueToPixels(dimensionValueString, context);
+        }
+    }
+
+    static HashMap<String, Float> pxDimensionLookupTable = new HashMap<String, Float>();
+
+    @SuppressLint("DefaultLocale")
+    public static float typedDimensionValueToPixels(String dimensionValueString, Context context) {
+        if (dimensionValueString == null) {
+            return 0;
+        }
+        dimensionValueString = dimensionValueString.toLowerCase();
+        if (pxDimensionLookupTable.containsKey(dimensionValueString)) {
+            return pxDimensionLookupTable.get(dimensionValueString);
+        }
+        Matcher m = DIMENSION_VALUE_PATTERN.matcher(dimensionValueString);
+        if (!m.matches()) {
+            throw new NumberFormatException();
+        }
+        float value = Float.parseFloat(m.group(1));
+        String dimensionString = m.group(3).toLowerCase();
+        Integer unit = DIMENSION_STRING_CONSTANT.get(dimensionString);
+        if (unit == null) {
+            unit = TypedValue.COMPLEX_UNIT_DIP;
+        }
+        float ret =
+                TypedValue.applyDimension(unit, value, context.getResources().getDisplayMetrics());
+        pxDimensionLookupTable.put(dimensionValueString, ret);
+        return ret;
+    }
+
+    // ATTRIBUTE HELPERS
+
+    public static void setPadding(View view, String left, String top, String right, String bottom) {
+        Context context = view.getContext();
+        view.setPadding(
+                typedDimensionValueToPixelsInt(left, context),
+                typedDimensionValueToPixelsInt(top, context),
+                typedDimensionValueToPixelsInt(right, context),
+                typedDimensionValueToPixelsInt(bottom, context));
+    }
+
+    // LAYOUT PARAM HELPERS
+
+    /**
+     * Set margins for given view if its LayoutParams are MarginLayoutParams.
+     * Should be used after the view is already added to a layout.
+     *
+     * @param left
+     * @param top
+     * @param right
+     * @param bottom
+     * @category layout
+     */
+    public static void setMargins(View view, String left, String top, String right, String bottom) {
+        Context context = view.getContext();
+        LayoutParams params = view.getLayoutParams();
+        if (params instanceof ViewGroup.MarginLayoutParams) {
+            ((ViewGroup.MarginLayoutParams) params).setMargins(
+                    typedDimensionValueToPixelsInt(left, context),
+                    typedDimensionValueToPixelsInt(top, context),
+                    typedDimensionValueToPixelsInt(right, context),
+                    typedDimensionValueToPixelsInt(bottom, context));
+        }
+    }
+
+    public static void setDimensions(View view, int width, int height) {
+        LayoutParams params = view.getLayoutParams();
+        params.width = width;
+        params.height = height;
+    }
+
+    public static void styleAsButton(Button button, boolean primary, Context context, boolean useApplicationTheme) {
+        setDimensions(button, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+
+        button.setFocusable(true);
+        setPadding(button, "10dip", "0dip", "10dip", "0dip");
+
+        if(! useApplicationTheme) {
+
+            setBackground(
+                    button,
+                    primary ? Appearance.buttonBackgroundPrimary(context) : Appearance
+                            .buttonBackgroundSecondary(context));
+
+            button.setGravity(Gravity.CENTER);
+            button.setMinimumHeight(ViewUtil.typedDimensionValueToPixelsInt(
+                    Appearance.BUTTON_HEIGHT, context));
+            button.setTextColor(Appearance.TEXT_COLOR_BUTTON);
+            button.setTextSize(Appearance.TEXT_SIZE_BUTTON);
+            button.setTypeface(Appearance.TYPEFACE_BUTTON);
+        }
+    }
+}

TEMPAT SAMPAH
scancardlibrary/src/main/jniLibs/armeabi-v7a/libcardioDecider.so


TEMPAT SAMPAH
scancardlibrary/src/main/jniLibs/armeabi-v7a/libcardioRecognizer.so


TEMPAT SAMPAH
scancardlibrary/src/main/jniLibs/armeabi-v7a/libopencv_core.so


TEMPAT SAMPAH
scancardlibrary/src/main/jniLibs/armeabi-v7a/libopencv_imgproc.so


TEMPAT SAMPAH
scancardlibrary/src/main/jniLibs/armeabi/libcardioDecider.so


+ 5 - 0
scancardlibrary/src/main/res/color/white_text_selector.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+    <item android:state_pressed="true" android:color="@color/whitePress"></item>
+    <item android:color="@color/white"></item>
+</selector>

TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable-hdpi/cio_card_io_logo.png


TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable-hdpi/cio_ic_amex.png


TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable-hdpi/cio_ic_discover.png


TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable-hdpi/cio_ic_jcb.png


TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable-hdpi/cio_ic_mastercard.png


TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable-hdpi/cio_ic_paypal_monogram.png


TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable-hdpi/cio_ic_visa.png


TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable-hdpi/cio_paypal_logo.png


TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable-xhdpi/bd_ocr_cancel.png


TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable-xhdpi/bd_ocr_cancel_press.png


TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable-xhdpi/bd_ocr_confirm.png


TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable-xhdpi/bd_ocr_confirm_press.png


TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable-xhdpi/bd_ocr_rotate.png


TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable-xhdpi/bd_ocr_rotate_press.png


TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable-xhdpi/cio_card_io_logo.png


TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable-xhdpi/cio_ic_paypal_monogram.png


TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable-xhdpi/cio_paypal_logo.png


+ 13 - 0
scancardlibrary/src/main/res/drawable/bd_ocr_cancel_selector.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:constantSize="true"
+    >
+    <!-- 按压时 -->
+    <item android:drawable="@drawable/bd_ocr_cancel_press" android:state_pressed="true"/>
+    <!-- 被选中时 -->
+    <item android:drawable="@drawable/bd_ocr_cancel_press" android:state_selected="true"/>
+    <!-- 被激活时 -->
+    <item android:drawable="@drawable/bd_ocr_cancel_press" android:state_activated="true"/>
+    <!-- 默认时 -->
+    <item android:drawable="@drawable/bd_ocr_cancel"/>
+</selector>

+ 12 - 0
scancardlibrary/src/main/res/drawable/bd_ocr_confirm_selector.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          android:constantSize="true">
+    <!-- 按压时 -->
+    <item android:drawable="@drawable/bd_ocr_confirm_press" android:state_pressed="true"/>
+    <!-- 被选中时 -->
+    <item android:drawable="@drawable/bd_ocr_confirm_press" android:state_selected="true"/>
+    <!-- 被激活时 -->
+    <item android:drawable="@drawable/bd_ocr_confirm_press" android:state_activated="true"/>
+    <!-- 默认时 -->
+    <item android:drawable="@drawable/bd_ocr_confirm"/>
+</selector>

+ 12 - 0
scancardlibrary/src/main/res/drawable/bd_ocr_rotate_selector.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          android:constantSize="true">
+    <!-- 按压时 -->
+    <item android:drawable="@drawable/bd_ocr_rotate_press" android:state_pressed="true"/>
+    <!-- 被选中时 -->
+    <item android:drawable="@drawable/bd_ocr_rotate_press" android:state_selected="true"/>
+    <!-- 被激活时 -->
+    <item android:drawable="@drawable/bd_ocr_rotate_press" android:state_activated="true"/>
+    <!-- 默认时 -->
+    <item android:drawable="@drawable/bd_ocr_rotate"/>
+</selector>

TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable/bd_ocr_take_photo_highlight.png


TEMPAT SAMPAH
scancardlibrary/src/main/res/drawable/bd_ocr_take_photo_normal.png


Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini