diff --git a/keeplibrary/.gitignore b/keeplibrary/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/keeplibrary/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/keeplibrary/build.gradle b/keeplibrary/build.gradle
new file mode 100644
index 0000000..7da0bd6
--- /dev/null
+++ b/keeplibrary/build.gradle
@@ -0,0 +1,39 @@
+plugins {
+ id 'com.android.library'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ compileSdk 33
+
+ defaultConfig {
+ minSdk 19
+ targetSdk 32
+
+ consumerProguardFiles 'consumer-rules.pro'
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ lint {
+ abortOnError false
+ }
+}
+
+dependencies {
+ implementation fileTree(include: ['*.jar'], dir: 'libs')
+ implementation 'androidx.annotation:annotation:1.6.0'
+ implementation 'androidx.core:core:1.10.1'
+}
diff --git a/keeplibrary/consumer-rules.pro b/keeplibrary/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/keeplibrary/proguard-rules.pro b/keeplibrary/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/keeplibrary/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
diff --git a/keeplibrary/src/main/AndroidManifest.xml b/keeplibrary/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..77c8619
--- /dev/null
+++ b/keeplibrary/src/main/AndroidManifest.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/keeplibrary/src/main/aidl/com/fanjun/keeplive/service/GuardAidl.aidl b/keeplibrary/src/main/aidl/com/fanjun/keeplive/service/GuardAidl.aidl
new file mode 100644
index 0000000..f2bb854
--- /dev/null
+++ b/keeplibrary/src/main/aidl/com/fanjun/keeplive/service/GuardAidl.aidl
@@ -0,0 +1,6 @@
+package com.fanjun.keeplive.service;
+
+interface GuardAidl {
+ //相互唤醒服务
+ void wakeUp(String title, String discription, int iconRes);
+}
diff --git a/keeplibrary/src/main/java/com/fanjun/keeplive/KeepLive.java b/keeplibrary/src/main/java/com/fanjun/keeplive/KeepLive.java
new file mode 100644
index 0000000..9fb7bf6
--- /dev/null
+++ b/keeplibrary/src/main/java/com/fanjun/keeplive/KeepLive.java
@@ -0,0 +1,103 @@
+package com.fanjun.keeplive;
+
+import android.app.ActivityManager;
+import android.app.Application;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+
+import com.fanjun.keeplive.config.ForegroundNotification;
+import com.fanjun.keeplive.config.KeepLiveService;
+import com.fanjun.keeplive.service.JobHandlerService;
+import com.fanjun.keeplive.service.LocalService;
+import com.fanjun.keeplive.service.RemoteService;
+
+import java.util.List;
+
+/**
+ * 保活工具
+ */
+public final class KeepLive {
+ /**
+ * 运行模式
+ */
+ public static enum RunMode {
+ /**
+ * 省电模式
+ * 省电一些,但保活效果会差一点
+ */
+ ENERGY,
+ /**
+ * 流氓模式
+ * 相对耗电,但可造就不死之身
+ */
+ ROGUE
+ }
+
+ public static ForegroundNotification foregroundNotification = null;
+ public static KeepLiveService keepLiveService = null;
+ public static RunMode runMode = null;
+ public static boolean useSilenceMusice = true;
+
+ /**
+ * 启动保活
+ *
+ * @param application your application
+ * @param foregroundNotification 前台服务 必须要,安卓8.0后必须有前台通知才能正常启动Service
+ * @param keepLiveService 保活业务
+ */
+ public static void startWork(@NonNull Application application, @NonNull RunMode runMode, @NonNull ForegroundNotification foregroundNotification, @NonNull KeepLiveService keepLiveService) {
+ if (isMain(application)) {
+ KeepLive.foregroundNotification = foregroundNotification;
+ KeepLive.keepLiveService = keepLiveService;
+ KeepLive.runMode = runMode;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ //启动定时器,在定时器中启动本地服务和守护进程
+ Intent intent = new Intent(application, JobHandlerService.class);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ application.startForegroundService(intent);
+ } else {
+ application.startService(intent);
+ }
+ } else {
+ //启动本地服务
+ Intent localIntent = new Intent(application, LocalService.class);
+ //启动守护进程
+ Intent guardIntent = new Intent(application, RemoteService.class);
+ application.startService(localIntent);
+ application.startService(guardIntent);
+ }
+ }
+ }
+
+ /**
+ * 是否启用无声音乐
+ * 如不设置,则默认启用
+ * @param enable
+ */
+ public static void useSilenceMusice(boolean enable){
+ KeepLive.useSilenceMusice = enable;
+ }
+
+ private static boolean isMain(Application application) {
+ int pid = android.os.Process.myPid();
+ String processName = "";
+ ActivityManager mActivityManager = (ActivityManager) application.getSystemService(Context.ACTIVITY_SERVICE);
+ List runningAppProcessInfos = mActivityManager.getRunningAppProcesses();
+ if (runningAppProcessInfos != null) {
+ for (ActivityManager.RunningAppProcessInfo appProcess : mActivityManager.getRunningAppProcesses()) {
+ if (appProcess.pid == pid) {
+ processName = appProcess.processName;
+ break;
+ }
+ }
+ String packageName = application.getPackageName();
+ if (processName.equals(packageName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/keeplibrary/src/main/java/com/fanjun/keeplive/activity/OnePixelActivity.java b/keeplibrary/src/main/java/com/fanjun/keeplive/activity/OnePixelActivity.java
new file mode 100644
index 0000000..78bd971
--- /dev/null
+++ b/keeplibrary/src/main/java/com/fanjun/keeplive/activity/OnePixelActivity.java
@@ -0,0 +1,40 @@
+package com.fanjun.keeplive.activity;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.view.Gravity;
+import android.view.Window;
+import android.view.WindowManager;
+public final class OnePixelActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ //设定一像素的activity
+ Window window = getWindow();
+ window.setGravity(Gravity.START | Gravity.TOP);
+ WindowManager.LayoutParams params = window.getAttributes();
+ params.x = 0;
+ params.y = 0;
+ params.height = 1;
+ params.width = 1;
+ window.setAttributes(params);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ checkScreenOn("onResume");
+ }
+ private void checkScreenOn(String methodName) {
+ try{
+ PowerManager pm = (PowerManager) getApplicationContext().getSystemService(Context.POWER_SERVICE);
+ boolean isScreenOn = pm.isScreenOn();
+ if (isScreenOn) {
+ finish();
+ }
+ }catch (Exception e){}
+ }
+}
diff --git a/keeplibrary/src/main/java/com/fanjun/keeplive/config/ForegroundNotification.java b/keeplibrary/src/main/java/com/fanjun/keeplive/config/ForegroundNotification.java
new file mode 100644
index 0000000..6afc3dd
--- /dev/null
+++ b/keeplibrary/src/main/java/com/fanjun/keeplive/config/ForegroundNotification.java
@@ -0,0 +1,91 @@
+package com.fanjun.keeplive.config;
+
+
+import androidx.annotation.NonNull;
+
+import java.io.Serializable;
+
+/**
+ * 默认前台服务样式
+ */
+public class ForegroundNotification implements Serializable {
+ private String title;
+ private String description;
+ private int iconRes;
+ private ForegroundNotificationClickListener foregroundNotificationClickListener;
+ private ForegroundNotification(){
+
+ }
+ public ForegroundNotification(String title, String description, int iconRes, ForegroundNotificationClickListener foregroundNotificationClickListener) {
+ this.title = title;
+ this.description = description;
+ this.iconRes = iconRes;
+ this.foregroundNotificationClickListener = foregroundNotificationClickListener;
+ }
+
+ public ForegroundNotification(String title, String description, int iconRes) {
+ this.title = title;
+ this.description = description;
+ this.iconRes = iconRes;
+ }
+
+ /**
+ * 初始化
+ * @return ForegroundNotification
+ */
+ public static ForegroundNotification ini(){
+ return new ForegroundNotification();
+ }
+ /**
+ * 设置标题
+ * @param title 标题
+ * @return ForegroundNotification
+ */
+ public ForegroundNotification title(@NonNull String title){
+ this.title = title;
+ return this;
+ }
+ /**
+ * 设置副标题
+ * @param description 副标题
+ * @return ForegroundNotification
+ */
+ public ForegroundNotification description(@NonNull String description){
+ this.description = description;
+ return this;
+ }
+ /**
+ * 设置图标
+ * @param iconRes 图标
+ * @return ForegroundNotification
+ */
+ public ForegroundNotification icon(@NonNull int iconRes){
+ this.iconRes = iconRes;
+ return this;
+ }
+ /**
+ * 设置前台通知点击事件
+ * @param foregroundNotificationClickListener 前台通知点击回调
+ * @return ForegroundNotification
+ */
+ public ForegroundNotification foregroundNotificationClickListener(@NonNull ForegroundNotificationClickListener foregroundNotificationClickListener){
+ this.foregroundNotificationClickListener = foregroundNotificationClickListener;
+ return this;
+ }
+
+ public String getTitle() {
+ return title==null?"":title;
+ }
+
+ public String getDescription() {
+ return description==null?"":description;
+ }
+
+ public int getIconRes() {
+ return iconRes;
+ }
+
+ public ForegroundNotificationClickListener getForegroundNotificationClickListener() {
+ return foregroundNotificationClickListener;
+ }
+}
diff --git a/keeplibrary/src/main/java/com/fanjun/keeplive/config/ForegroundNotificationClickListener.java b/keeplibrary/src/main/java/com/fanjun/keeplive/config/ForegroundNotificationClickListener.java
new file mode 100644
index 0000000..5ec9567
--- /dev/null
+++ b/keeplibrary/src/main/java/com/fanjun/keeplive/config/ForegroundNotificationClickListener.java
@@ -0,0 +1,11 @@
+package com.fanjun.keeplive.config;
+
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * 前台服务通知点击事件
+ */
+public interface ForegroundNotificationClickListener {
+ void foregroundNotificationClick(Context context, Intent intent);
+}
diff --git a/keeplibrary/src/main/java/com/fanjun/keeplive/config/KeepLiveService.java b/keeplibrary/src/main/java/com/fanjun/keeplive/config/KeepLiveService.java
new file mode 100644
index 0000000..0b7dc4f
--- /dev/null
+++ b/keeplibrary/src/main/java/com/fanjun/keeplive/config/KeepLiveService.java
@@ -0,0 +1,18 @@
+package com.fanjun.keeplive.config;
+
+/**
+ * 需要保活的服务
+ */
+public interface KeepLiveService {
+ /**
+ * 运行中
+ * 由于服务可能会多次自动启动,该方法可能重复调用
+ */
+ void onWorking();
+
+ /**
+ * 服务终止
+ * 由于服务可能会被多次终止,该方法可能重复调用,需同onWorking配套使用,如注册和注销
+ */
+ void onStop();
+}
diff --git a/keeplibrary/src/main/java/com/fanjun/keeplive/config/NotificationUtils.java b/keeplibrary/src/main/java/com/fanjun/keeplive/config/NotificationUtils.java
new file mode 100644
index 0000000..1523f9d
--- /dev/null
+++ b/keeplibrary/src/main/java/com/fanjun/keeplive/config/NotificationUtils.java
@@ -0,0 +1,102 @@
+package com.fanjun.keeplive.config;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.core.app.NotificationCompat;
+
+
+public class NotificationUtils extends ContextWrapper {
+ private NotificationManager manager;
+ private String id;
+ private String name;
+ private Context context;
+ private NotificationChannel channel;
+
+ private NotificationUtils(Context context) {
+ super(context);
+ this.context = context;
+ id = context.getPackageName();
+ name = context.getPackageName();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public void createNotificationChannel() {
+ if (channel == null) {
+ channel = new NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH);
+ channel.enableVibration(false);
+ channel.enableLights(false);
+ channel.enableVibration(false);
+ channel.setVibrationPattern(new long[]{0});
+ channel.setSound(null, null);
+ getManager().createNotificationChannel(channel);
+ }
+ }
+
+ private NotificationManager getManager() {
+ if (manager == null) {
+ manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ }
+ return manager;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public Notification.Builder getChannelNotification(String title, String content, int icon, Intent intent) {
+ //PendingIntent.FLAG_UPDATE_CURRENT 这个类型才能传值
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, getPendingIntent());
+ return new Notification.Builder(context, id)
+ .setContentTitle(title)
+ .setContentText(content)
+ .setSmallIcon(icon)
+ .setAutoCancel(true)
+ .setContentIntent(pendingIntent);
+ }
+
+ private int getPendingIntent() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_IMMUTABLE : PendingIntent.FLAG_UPDATE_CURRENT;
+ }
+
+ public NotificationCompat.Builder getNotification_25(String title, String content, int icon, Intent intent) {
+ //PendingIntent.FLAG_UPDATE_CURRENT 这个类型才能传值
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, getPendingIntent());
+ return new NotificationCompat.Builder(context, id)
+ .setContentTitle(title)
+ .setContentText(content)
+ .setSmallIcon(icon)
+ .setAutoCancel(true)
+ .setVibrate(new long[]{0})
+ .setContentIntent(pendingIntent);
+ }
+
+ public static void sendNotification(@NonNull Context context, @NonNull String title, @NonNull String content, @NonNull int icon, @NonNull Intent intent) {
+ NotificationUtils notificationUtils = new NotificationUtils(context);
+ Notification notification = null;
+ if (Build.VERSION.SDK_INT >= 26) {
+ notificationUtils.createNotificationChannel();
+ notification = notificationUtils.getChannelNotification(title, content, icon, intent).build();
+ } else {
+ notification = notificationUtils.getNotification_25(title, content, icon, intent).build();
+ }
+ notificationUtils.getManager().notify(new java.util.Random().nextInt(10000), notification);
+ }
+
+ public static Notification createNotification(@NonNull Context context, @NonNull String title, @NonNull String content, @NonNull int icon, @NonNull Intent intent) {
+ NotificationUtils notificationUtils = new NotificationUtils(context);
+ Notification notification = null;
+ if (Build.VERSION.SDK_INT >= 26) {
+ notificationUtils.createNotificationChannel();
+ notification = notificationUtils.getChannelNotification(title, content, icon,intent).build();
+ } else {
+ notification = notificationUtils.getNotification_25(title, content, icon,intent).build();
+ }
+ return notification;
+ }
+}
diff --git a/keeplibrary/src/main/java/com/fanjun/keeplive/receiver/NotificationClickReceiver.java b/keeplibrary/src/main/java/com/fanjun/keeplive/receiver/NotificationClickReceiver.java
new file mode 100644
index 0000000..3abc652
--- /dev/null
+++ b/keeplibrary/src/main/java/com/fanjun/keeplive/receiver/NotificationClickReceiver.java
@@ -0,0 +1,22 @@
+package com.fanjun.keeplive.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.fanjun.keeplive.KeepLive;
+
+public final class NotificationClickReceiver extends BroadcastReceiver {
+ public final static String CLICK_NOTIFICATION = "CLICK_NOTIFICATION";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(NotificationClickReceiver.CLICK_NOTIFICATION)) {
+ if (KeepLive.foregroundNotification != null) {
+ if (KeepLive.foregroundNotification.getForegroundNotificationClickListener() != null) {
+ KeepLive.foregroundNotification.getForegroundNotificationClickListener().foregroundNotificationClick(context, intent);
+ }
+ }
+ }
+ }
+}
diff --git a/keeplibrary/src/main/java/com/fanjun/keeplive/receiver/OnepxReceiver.java b/keeplibrary/src/main/java/com/fanjun/keeplive/receiver/OnepxReceiver.java
new file mode 100644
index 0000000..a90e3e3
--- /dev/null
+++ b/keeplibrary/src/main/java/com/fanjun/keeplive/receiver/OnepxReceiver.java
@@ -0,0 +1,48 @@
+package com.fanjun.keeplive.receiver;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Looper;
+
+import com.fanjun.keeplive.activity.OnePixelActivity;
+
+public final class OnepxReceiver extends BroadcastReceiver {
+ android.os.Handler mHander;
+ boolean screenOn = true;
+
+ public OnepxReceiver() {
+ mHander = new android.os.Handler(Looper.getMainLooper());
+ }
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) { //屏幕关闭的时候接受到广播
+ screenOn = false;
+ mHander.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if(!screenOn){
+ Intent intent2 = new Intent(context, OnePixelActivity.class);
+ intent2.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent2, 0);
+ try {
+ pendingIntent.send();
+ /*} catch (PendingIntent.CanceledException e) {*/
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ },1000);
+ //通知屏幕已关闭,开始播放无声音乐
+ context.sendBroadcast(new Intent("_ACTION_SCREEN_OFF"));
+ } else if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) { //屏幕打开的时候发送广播 结束一像素
+ screenOn = true;
+ //通知屏幕已点亮,停止播放无声音乐
+ context.sendBroadcast(new Intent("_ACTION_SCREEN_ON"));
+ }
+ }
+}
diff --git a/keeplibrary/src/main/java/com/fanjun/keeplive/service/HideForegroundService.java b/keeplibrary/src/main/java/com/fanjun/keeplive/service/HideForegroundService.java
new file mode 100644
index 0000000..243b6ee
--- /dev/null
+++ b/keeplibrary/src/main/java/com/fanjun/keeplive/service/HideForegroundService.java
@@ -0,0 +1,48 @@
+package com.fanjun.keeplive.service;
+
+import android.app.Notification;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+
+import com.fanjun.keeplive.KeepLive;
+import com.fanjun.keeplive.config.NotificationUtils;
+import com.fanjun.keeplive.receiver.NotificationClickReceiver;
+
+/**
+ * 隐藏前台服务通知
+ */
+public class HideForegroundService extends Service {
+ private android.os.Handler handler;
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ startForeground();
+ if (handler == null){
+ handler = new Handler();
+ }
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ stopForeground(true);
+ stopSelf();
+ }
+ }, 2000);
+ return START_NOT_STICKY;
+ }
+
+
+ private void startForeground() {
+ if (KeepLive.foregroundNotification != null) {
+ Intent intent = new Intent(getApplicationContext(), NotificationClickReceiver.class);
+ intent.setAction(NotificationClickReceiver.CLICK_NOTIFICATION);
+ Notification notification = NotificationUtils.createNotification(this, KeepLive.foregroundNotification.getTitle(), KeepLive.foregroundNotification.getDescription(), KeepLive.foregroundNotification.getIconRes(), intent);
+ startForeground(13691, notification);
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+}
diff --git a/keeplibrary/src/main/java/com/fanjun/keeplive/service/JobHandlerService.java b/keeplibrary/src/main/java/com/fanjun/keeplive/service/JobHandlerService.java
new file mode 100644
index 0000000..356174d
--- /dev/null
+++ b/keeplibrary/src/main/java/com/fanjun/keeplive/service/JobHandlerService.java
@@ -0,0 +1,88 @@
+//
+// Source code recreated from a .class file by IntelliJ IDEA
+// (powered by Fernflower decompiler)
+//
+
+package com.fanjun.keeplive.service;
+
+import android.app.Notification;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.app.job.JobInfo.Builder;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build.VERSION;
+
+import androidx.annotation.RequiresApi;
+
+import com.fanjun.keeplive.KeepLive;
+import com.fanjun.keeplive.config.NotificationUtils;
+import com.fanjun.keeplive.receiver.NotificationClickReceiver;
+import com.fanjun.keeplive.utils.ServiceUtils;
+
+@RequiresApi(
+ api = 21
+)
+public final class JobHandlerService extends JobService {
+ private JobScheduler mJobScheduler;
+ private int jobId = 100;
+
+ public JobHandlerService() {
+ }
+
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ this.startService(this);
+ if (VERSION.SDK_INT >= 21) {
+ this.mJobScheduler = (JobScheduler) this.getSystemService("jobscheduler");
+ this.mJobScheduler.cancel(this.jobId);
+ Builder builder = new Builder(this.jobId, new ComponentName(this.getPackageName(), JobHandlerService.class.getName()));
+ if (VERSION.SDK_INT >= 24) {
+ builder.setMinimumLatency(30000L);
+ builder.setOverrideDeadline(30000L);
+ builder.setMinimumLatency(30000L);
+ builder.setBackoffCriteria(30000L, 0);
+ } else {
+ builder.setPeriodic(30000L);
+ }
+
+ builder.setRequiredNetworkType(1);
+ builder.setPersisted(true);
+ this.mJobScheduler.schedule(builder.build());
+ }
+
+ return 1;
+ }
+
+ private void startService(Context context) {
+ Intent localIntent;
+ if (VERSION.SDK_INT >= 26 && KeepLive.foregroundNotification != null) {
+ localIntent = new Intent(this.getApplicationContext(), NotificationClickReceiver.class);
+ localIntent.setAction("CLICK_NOTIFICATION");
+ Notification notification = NotificationUtils.createNotification(this, KeepLive.foregroundNotification.getTitle(), KeepLive.foregroundNotification.getDescription(), KeepLive.foregroundNotification.getIconRes(), localIntent);
+ this.startForeground(13691, notification);
+ }
+
+ localIntent = new Intent(context, LocalService.class);
+ Intent guardIntent = new Intent(context, RemoteService.class);
+ this.startService(localIntent);
+ this.startService(guardIntent);
+ }
+
+ public boolean onStartJob(JobParameters jobParameters) {
+ if (!ServiceUtils.isServiceRunning(this.getApplicationContext(), "com.fanjun.keeplive.service.LocalService") || !ServiceUtils.isRunningTaskExist(this.getApplicationContext(), this.getPackageName() + ":remote")) {
+ this.startService(this);
+ }
+
+ return false;
+ }
+
+ public boolean onStopJob(JobParameters jobParameters) {
+ if (!ServiceUtils.isServiceRunning(this.getApplicationContext(), "com.fanjun.keeplive.service.LocalService") || !ServiceUtils.isRunningTaskExist(this.getApplicationContext(), this.getPackageName() + ":remote")) {
+ this.startService(this);
+ }
+
+ return false;
+ }
+}
diff --git a/keeplibrary/src/main/java/com/fanjun/keeplive/service/LocalService.java b/keeplibrary/src/main/java/com/fanjun/keeplive/service/LocalService.java
new file mode 100644
index 0000000..b7bf2d0
--- /dev/null
+++ b/keeplibrary/src/main/java/com/fanjun/keeplive/service/LocalService.java
@@ -0,0 +1,220 @@
+package com.fanjun.keeplive.service;
+
+import android.app.Notification;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.media.MediaPlayer;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.RemoteException;
+
+import com.fanjun.keeplive.KeepLive;
+import com.fanjun.keeplive.R;
+import com.fanjun.keeplive.config.NotificationUtils;
+import com.fanjun.keeplive.receiver.NotificationClickReceiver;
+import com.fanjun.keeplive.receiver.OnepxReceiver;
+import com.fanjun.keeplive.utils.ServiceUtils;
+
+public final class LocalService extends Service {
+ private OnepxReceiver mOnepxReceiver;
+ private ScreenStateReceiver screenStateReceiver;
+ private boolean isPause = true;//控制暂停
+ private MediaPlayer mediaPlayer;
+ private MyBilder mBilder;
+ private android.os.Handler handler;
+ private boolean mIsBoundRemoteService ;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ if (mBilder == null) {
+ mBilder = new MyBilder();
+ }
+ PowerManager pm = (PowerManager) getApplicationContext().getSystemService(Context.POWER_SERVICE);
+ isPause = pm.isScreenOn();
+ if (handler == null) {
+ handler = new Handler();
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBilder;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (KeepLive.useSilenceMusice){
+ //播放无声音乐
+ if (mediaPlayer == null) {
+ mediaPlayer = MediaPlayer.create(this, R.raw.novioce);
+ if (mediaPlayer!= null){
+ mediaPlayer.setVolume(0f, 0f);
+ mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(MediaPlayer mediaPlayer) {
+ if (!isPause) {
+ if (KeepLive.runMode == KeepLive.RunMode.ROGUE) {
+ play();
+ } else {
+ if (handler != null) {
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ play();
+ }
+ }, 5000);
+ }
+ }
+ }
+ }
+ });
+ mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+ @Override
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ return false;
+ }
+ });
+ play();
+ }
+ }
+ }
+ //像素保活
+ if (mOnepxReceiver == null) {
+ mOnepxReceiver = new OnepxReceiver();
+ }
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction("android.intent.action.SCREEN_OFF");
+ intentFilter.addAction("android.intent.action.SCREEN_ON");
+ registerReceiver(mOnepxReceiver, intentFilter);
+ //屏幕点亮状态监听,用于单独控制音乐播放
+ if (screenStateReceiver == null) {
+ screenStateReceiver = new ScreenStateReceiver();
+ }
+ IntentFilter intentFilter2 = new IntentFilter();
+ intentFilter2.addAction("_ACTION_SCREEN_OFF");
+ intentFilter2.addAction("_ACTION_SCREEN_ON");
+ registerReceiver(screenStateReceiver, intentFilter2);
+ //启用前台服务,提升优先级
+ if (KeepLive.foregroundNotification != null) {
+ Intent intent2 = new Intent(getApplicationContext(), NotificationClickReceiver.class);
+ intent2.setAction(NotificationClickReceiver.CLICK_NOTIFICATION);
+ Notification notification = NotificationUtils.createNotification(this, KeepLive.foregroundNotification.getTitle(), KeepLive.foregroundNotification.getDescription(), KeepLive.foregroundNotification.getIconRes(), intent2);
+ startForeground(13691, notification);
+ }
+ //绑定守护进程
+ try {
+ Intent intent3 = new Intent(this, RemoteService.class);
+ mIsBoundRemoteService = this.bindService(intent3, connection, Context.BIND_ABOVE_CLIENT);
+ } catch (Exception e) {
+ }
+ //隐藏服务通知
+ try {
+ if(Build.VERSION.SDK_INT < 25){
+ startService(new Intent(this, HideForegroundService.class));
+ }
+ } catch (Exception e) {
+ }
+ if (KeepLive.keepLiveService != null) {
+ KeepLive.keepLiveService.onWorking();
+ }
+ return START_STICKY;
+ }
+
+ private void play() {
+ if (KeepLive.useSilenceMusice){
+ if (mediaPlayer != null && !mediaPlayer.isPlaying()) {
+ mediaPlayer.start();
+ }
+ }
+ }
+
+ private void pause() {
+ if (KeepLive.useSilenceMusice){
+ if (mediaPlayer != null && mediaPlayer.isPlaying()) {
+ mediaPlayer.pause();
+ }
+ }
+ }
+
+ private class ScreenStateReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ if (intent.getAction().equals("_ACTION_SCREEN_OFF")) {
+ isPause = false;
+ play();
+ } else if (intent.getAction().equals("_ACTION_SCREEN_ON")) {
+ isPause = true;
+ pause();
+ }
+ }
+ }
+
+ private final class MyBilder extends GuardAidl.Stub {
+
+ @Override
+ public void wakeUp(String title, String discription, int iconRes) throws RemoteException {
+
+ }
+ }
+
+ private ServiceConnection connection = new ServiceConnection() {
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (ServiceUtils.isServiceRunning(getApplicationContext(), "com.fanjun.keeplive.service.LocalService")){
+ Intent remoteService = new Intent(LocalService.this,
+ RemoteService.class);
+ LocalService.this.startService(remoteService);
+ Intent intent = new Intent(LocalService.this, RemoteService.class);
+ mIsBoundRemoteService = LocalService.this.bindService(intent, connection,
+ Context.BIND_ABOVE_CLIENT);
+ }
+ PowerManager pm = (PowerManager) getApplicationContext().getSystemService(Context.POWER_SERVICE);
+ boolean isScreenOn = pm.isScreenOn();
+ if (isScreenOn) {
+ sendBroadcast(new Intent("_ACTION_SCREEN_ON"));
+ } else {
+ sendBroadcast(new Intent("_ACTION_SCREEN_OFF"));
+ }
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ try {
+ if (mBilder != null && KeepLive.foregroundNotification != null) {
+ GuardAidl guardAidl = GuardAidl.Stub.asInterface(service);
+ guardAidl.wakeUp(KeepLive.foregroundNotification.getTitle(), KeepLive.foregroundNotification.getDescription(), KeepLive.foregroundNotification.getIconRes());
+ }
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+ };
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (connection != null){
+ try {
+ if (mIsBoundRemoteService){
+ unbindService(connection);
+ }
+ }catch (Exception e){}
+ }
+ try {
+ unregisterReceiver(mOnepxReceiver);
+ unregisterReceiver(screenStateReceiver);
+ }catch (Exception e){}
+ if (KeepLive.keepLiveService != null) {
+ KeepLive.keepLiveService.onStop();
+ }
+ }
+}
diff --git a/keeplibrary/src/main/java/com/fanjun/keeplive/service/RemoteService.java b/keeplibrary/src/main/java/com/fanjun/keeplive/service/RemoteService.java
new file mode 100644
index 0000000..9a7a6c4
--- /dev/null
+++ b/keeplibrary/src/main/java/com/fanjun/keeplive/service/RemoteService.java
@@ -0,0 +1,98 @@
+package com.fanjun.keeplive.service;
+
+import android.app.Notification;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.RemoteException;
+
+import com.fanjun.keeplive.config.NotificationUtils;
+import com.fanjun.keeplive.receiver.NotificationClickReceiver;
+import com.fanjun.keeplive.utils.ServiceUtils;
+
+/**
+ * 守护进程
+ */
+@SuppressWarnings(value={"unchecked", "deprecation"})
+public final class RemoteService extends Service {
+ private MyBilder mBilder;
+ private boolean mIsBoundLocalService ;
+
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ if (mBilder == null){
+ mBilder = new MyBilder();
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBilder;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ try {
+ mIsBoundLocalService = this.bindService(new Intent(RemoteService.this, LocalService.class),
+ connection, Context.BIND_ABOVE_CLIENT);
+ }catch (Exception e){
+ }
+ return START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (connection != null){
+ try {
+ if (mIsBoundLocalService){
+ unbindService(connection);
+ }
+ }catch (Exception e){}
+ }
+ }
+ private final class MyBilder extends GuardAidl.Stub {
+
+ @Override
+ public void wakeUp(String title, String discription, int iconRes) throws RemoteException {
+ if(Build.VERSION.SDK_INT < 25){
+ Intent intent2 = new Intent(getApplicationContext(), NotificationClickReceiver.class);
+ intent2.setAction(NotificationClickReceiver.CLICK_NOTIFICATION);
+ Notification notification = NotificationUtils.createNotification(RemoteService.this, title, discription, iconRes, intent2);
+ RemoteService.this.startForeground(13691, notification);
+ }
+ }
+
+ }
+
+ private final ServiceConnection connection = new ServiceConnection() {
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (ServiceUtils.isRunningTaskExist(getApplicationContext(), getPackageName() + ":remote")){
+ Intent localService = new Intent(RemoteService.this,
+ LocalService.class);
+ RemoteService.this.startService(localService);
+ mIsBoundLocalService = RemoteService.this.bindService(new Intent(RemoteService.this,
+ LocalService.class), connection, Context.BIND_ABOVE_CLIENT);
+ }
+ PowerManager pm = (PowerManager) RemoteService.this.getSystemService(Context.POWER_SERVICE);
+ boolean isScreenOn = pm.isScreenOn();
+ if (isScreenOn){
+ sendBroadcast(new Intent("_ACTION_SCREEN_ON"));
+ }else{
+ sendBroadcast(new Intent("_ACTION_SCREEN_OFF"));
+ }
+ }
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ }
+ };
+
+}
diff --git a/keeplibrary/src/main/java/com/fanjun/keeplive/utils/ServiceUtils.java b/keeplibrary/src/main/java/com/fanjun/keeplive/utils/ServiceUtils.java
new file mode 100644
index 0000000..b40cf8c
--- /dev/null
+++ b/keeplibrary/src/main/java/com/fanjun/keeplive/utils/ServiceUtils.java
@@ -0,0 +1,39 @@
+package com.fanjun.keeplive.utils;
+
+import android.app.ActivityManager;
+import android.content.Context;
+
+import java.util.Iterator;
+import java.util.List;
+
+public class ServiceUtils {
+ public static boolean isServiceRunning(Context ctx, String className) {
+ boolean isRunning = false;
+ ActivityManager activityManager = (ActivityManager) ctx
+ .getSystemService(Context.ACTIVITY_SERVICE);
+ List servicesList = activityManager
+ .getRunningServices(Integer.MAX_VALUE);
+ if (servicesList != null) {
+ Iterator l = servicesList.iterator();
+ while (l.hasNext()) {
+ ActivityManager.RunningServiceInfo si = l.next();
+ if (className.equals(si.service.getClassName())) {
+ isRunning = true;
+ }
+ }
+ }
+ return isRunning;
+ }
+ public static boolean isRunningTaskExist(Context context, String processName) {
+ ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ List processList = am.getRunningAppProcesses();
+ if (processList != null){
+ for (ActivityManager.RunningAppProcessInfo info : processList) {
+ if (info.processName.equals(processName)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/keeplibrary/src/main/res/raw/novioce.wav b/keeplibrary/src/main/res/raw/novioce.wav
new file mode 100644
index 0000000..519eee6
Binary files /dev/null and b/keeplibrary/src/main/res/raw/novioce.wav differ
diff --git a/keeplibrary/src/main/res/values/strings.xml b/keeplibrary/src/main/res/values/strings.xml
new file mode 100644
index 0000000..8542005
--- /dev/null
+++ b/keeplibrary/src/main/res/values/strings.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/keeplibrary/src/main/res/values/styles.xml b/keeplibrary/src/main/res/values/styles.xml
new file mode 100644
index 0000000..d8f491b
--- /dev/null
+++ b/keeplibrary/src/main/res/values/styles.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/ocr_ui/.gitignore b/ocr_ui/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/ocr_ui/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/ocr_ui/build.gradle b/ocr_ui/build.gradle
new file mode 100644
index 0000000..f65056c
--- /dev/null
+++ b/ocr_ui/build.gradle
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion rootProject.ext.android["compileSdkVersion"]
+// compileSdkVersion 27
+// buildToolsVersion "27.0.2"
+
+ defaultConfig {
+ minSdkVersion rootProject.ext.android["minSdkVersion"]
+ targetSdkVersion rootProject.ext.android["targetSdkVersion"]
+ versionCode rootProject.ext.android["versionCode"]
+ versionName rootProject.ext.android["versionName"]
+
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
+ exclude group: 'com.android.support', module: 'support-annotations'
+ })
+
+ implementation 'androidx.appcompat:appcompat:1.3.0'
+// implementation 'com.android.support:appcompat-v7:27.1.1'
+ testImplementation 'junit:junit:4.12'
+ implementation files('libs/license.jar')
+}
diff --git a/ocr_ui/libs/license.jar b/ocr_ui/libs/license.jar
new file mode 100644
index 0000000..57a2fa4
Binary files /dev/null and b/ocr_ui/libs/license.jar differ
diff --git a/ocr_ui/proguard-rules.pro b/ocr_ui/proguard-rules.pro
new file mode 100644
index 0000000..ce30132
--- /dev/null
+++ b/ocr_ui/proguard-rules.pro
@@ -0,0 +1,18 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/baidu/IDE/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# 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 *;
+#}
+-dontwarn com.baidu.ocr.**
\ No newline at end of file
diff --git a/ocr_ui/src/main/AndroidManifest.xml b/ocr_ui/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..cc4a6b0
--- /dev/null
+++ b/ocr_ui/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ocr_ui/src/main/assets/models/integrity_model_secret.bin b/ocr_ui/src/main/assets/models/integrity_model_secret.bin
new file mode 100644
index 0000000..31dc46c
Binary files /dev/null and b/ocr_ui/src/main/assets/models/integrity_model_secret.bin differ
diff --git a/ocr_ui/src/main/assets/models/quality_model_secret.bin b/ocr_ui/src/main/assets/models/quality_model_secret.bin
new file mode 100644
index 0000000..9e9b7bc
Binary files /dev/null and b/ocr_ui/src/main/assets/models/quality_model_secret.bin differ
diff --git a/ocr_ui/src/main/java/com/baidu/idcardquality/IDcardQualityProcess.java b/ocr_ui/src/main/java/com/baidu/idcardquality/IDcardQualityProcess.java
new file mode 100644
index 0000000..7e00d46
--- /dev/null
+++ b/ocr_ui/src/main/java/com/baidu/idcardquality/IDcardQualityProcess.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.baidu.idcardquality;
+
+import android.content.res.AssetManager;
+import android.graphics.Bitmap;
+
+import com.baidu.idl.authority.AlgorithmOnMainThreadException;
+import com.baidu.idl.authority.IDLAuthorityException;
+import com.baidu.idl.license.License;
+import com.baidu.idl.util.UIThread;
+
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+public class IDcardQualityProcess {
+ final ReentrantReadWriteLock nativeModelLock = new ReentrantReadWriteLock();
+ private static IDcardQualityProcess mInstance;
+ private static String tokenString;
+ private static int mAuthorityStatus;
+ private static Throwable loadNativeException = null;
+ private static volatile boolean hasReleased;
+
+ public IDcardQualityProcess() {
+ }
+
+ public static synchronized IDcardQualityProcess getInstance() {
+ if (null == mInstance) {
+ mInstance = new IDcardQualityProcess();
+ }
+
+ return mInstance;
+ }
+
+ public native byte[] convertRGBImage(int[] colors, int width, int height);
+
+ public native int idcardQualityModelInit(AssetManager var1, String var2);
+
+ public native int idcardQualityCaptchaRelease();
+
+ public native int idcardQualityProcess(byte[] var1, int var2, int var3, boolean var4, int var5);
+
+ public static synchronized int init(String token) throws AlgorithmOnMainThreadException, IDLAuthorityException {
+ if (UIThread.isUITread()) {
+ throw new AlgorithmOnMainThreadException();
+ } else {
+ tokenString = token;
+
+ try {
+ mAuthorityStatus = License.getInstance().init(tokenString);
+ } catch (Exception var2) {
+ var2.printStackTrace();
+ }
+
+ return mAuthorityStatus;
+ }
+ }
+
+ public int idcardQualityInit(AssetManager assetManager, String modelPath) {
+ if (mAuthorityStatus == 0) {
+ hasReleased = false;
+ nativeModelLock.writeLock().lock();
+ int status = this.idcardQualityModelInit(assetManager, modelPath);
+ nativeModelLock.writeLock().unlock();
+ return status;
+ } else {
+ return mAuthorityStatus;
+ }
+ }
+
+ public int idcardQualityRelease() {
+ if (mAuthorityStatus == 0) {
+ hasReleased = true;
+ nativeModelLock.writeLock().lock();
+ this.idcardQualityCaptchaRelease();
+ nativeModelLock.writeLock().unlock();
+ return 0;
+ } else {
+ return mAuthorityStatus;
+ }
+ }
+
+ public int idcardQualityDetectionImg(Bitmap img, boolean isfont) {
+ int status;
+ if (mAuthorityStatus == 0) {
+ if (hasReleased) {
+ return -1;
+ }
+ int imgHeight = img.getHeight();
+ int imgWidth = img.getWidth();
+ byte[] imageData = this.getRGBImageData(img);
+ nativeModelLock.readLock().lock();
+ status = this.idcardQualityProcess(imageData, imgHeight, imgWidth, isfont, 3);
+ nativeModelLock.readLock().unlock();
+ return status;
+ } else {
+ return mAuthorityStatus;
+ }
+ }
+
+ public static Throwable getLoadSoException() {
+ return loadNativeException;
+ }
+
+ public byte[] getRGBImageData(Bitmap img) {
+ int imgWidth = img.getWidth();
+ int imgHeight = img.getHeight();
+ int[] pixels = new int[imgWidth * imgHeight];
+ img.getPixels(pixels, 0, imgWidth, 0, 0, imgWidth, imgHeight);
+ byte[] imageData = convertRGBImage(pixels, imgWidth, imgHeight);
+ return imageData;
+ }
+
+ public void releaseModel() {
+ this.idcardQualityRelease();
+ }
+
+ static {
+ try {
+ System.loadLibrary("idl_license");
+ System.loadLibrary("idcard_quality.1.1.1");
+ } catch (Throwable e) {
+ loadNativeException = e;
+ }
+ mInstance = null;
+ mAuthorityStatus = 256;
+ }
+}
diff --git a/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/Camera1Control.java b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/Camera1Control.java
new file mode 100644
index 0000000..bf1e9eb
--- /dev/null
+++ b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/Camera1Control.java
@@ -0,0 +1,573 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.baidu.ocr.ui.camera;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.ImageFormat;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.graphics.YuvImage;
+import android.hardware.Camera;
+import android.view.TextureView;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import androidx.core.app.ActivityCompat;
+
+/**
+ * 5.0以下相机API的封装。
+ */
+@SuppressWarnings("deprecation")
+public class Camera1Control implements ICameraControl {
+
+ private int displayOrientation = 0;
+ private int cameraId = 0;
+ private int flashMode;
+ private AtomicBoolean takingPicture = new AtomicBoolean(false);
+ private AtomicBoolean abortingScan = new AtomicBoolean(false);
+ private Context context;
+ private Camera camera;
+
+ private Camera.Parameters parameters;
+ private PermissionCallback permissionCallback;
+ private Rect previewFrame = new Rect();
+
+ private PreviewView previewView;
+ private View displayView;
+ private int rotation = 0;
+ private OnDetectPictureCallback detectCallback;
+ private int previewFrameCount = 0;
+ private Camera.Size optSize;
+
+ /*
+ * 非扫描模式
+ */
+ private final int MODEL_NOSCAN = 0;
+
+ /*
+ * 本地质量控制扫描模式
+ */
+ private final int MODEL_SCAN = 1;
+
+ private int detectType = MODEL_NOSCAN;
+
+ public int getCameraRotation() {
+ return rotation;
+ }
+
+ public AtomicBoolean getAbortingScan() {
+ return abortingScan;
+ }
+
+ @Override
+ public void setDetectCallback(OnDetectPictureCallback callback) {
+ detectType = MODEL_SCAN;
+ detectCallback = callback;
+ }
+
+ private void onRequestDetect(byte[] data) {
+ // 相机已经关闭
+ if (camera == null || data == null || optSize == null) {
+ return;
+ }
+
+ YuvImage img = new YuvImage(data, ImageFormat.NV21, optSize.width, optSize.height, null);
+ ByteArrayOutputStream os = null;
+ try {
+ os = new ByteArrayOutputStream(data.length);
+ img.compressToJpeg(new Rect(0, 0, optSize.width, optSize.height), 80, os);
+ byte[] jpeg = os.toByteArray();
+ int status = detectCallback.onDetect(jpeg, getCameraRotation());
+
+ if (status == 0) {
+ clearPreviewCallback();
+ }
+ } catch (OutOfMemoryError e) {
+ // 内存溢出则取消当次操作
+ } finally {
+ try {
+ os.close();
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ @Override
+ public void setDisplayOrientation(@CameraView.Orientation int displayOrientation) {
+ this.displayOrientation = displayOrientation;
+ switch (displayOrientation) {
+ case CameraView.ORIENTATION_PORTRAIT:
+ rotation = 90;
+ break;
+ case CameraView.ORIENTATION_HORIZONTAL:
+ rotation = 0;
+ break;
+ case CameraView.ORIENTATION_INVERT:
+ rotation = 180;
+ break;
+ default:
+ rotation = 0;
+ }
+ previewView.requestLayout();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void refreshPermission() {
+ startPreview(true);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setFlashMode(@FlashMode int flashMode) {
+ if (this.flashMode == flashMode) {
+ return;
+ }
+ this.flashMode = flashMode;
+ updateFlashMode(flashMode);
+ }
+
+ @Override
+ public int getFlashMode() {
+ return flashMode;
+ }
+
+ @Override
+ public void start() {
+ startPreview(false);
+ }
+
+ @Override
+ public void stop() {
+ if (camera != null) {
+ camera.setPreviewCallback(null);
+ stopPreview();
+ // 避免同步代码,为了先设置null后release
+ Camera tempC = camera;
+ camera = null;
+ tempC.release();
+ camera = null;
+ buffer = null;
+ }
+ }
+
+ private void stopPreview() {
+ if (camera != null) {
+ camera.stopPreview();
+ }
+ }
+
+ @Override
+ public void pause() {
+ if (camera != null) {
+ stopPreview();
+ }
+ setFlashMode(FLASH_MODE_OFF);
+ }
+
+ @Override
+ public void resume() {
+ takingPicture.set(false);
+ if (camera == null) {
+ openCamera();
+ } else {
+ previewView.textureView.setSurfaceTextureListener(surfaceTextureListener);
+ if (previewView.textureView.isAvailable()) {
+ startPreview(false);
+ }
+ }
+ }
+
+ @Override
+ public View getDisplayView() {
+ return displayView;
+ }
+
+ @Override
+ public void takePicture(final OnTakePictureCallback onTakePictureCallback) {
+ if (takingPicture.get()) {
+ return;
+ }
+ switch (displayOrientation) {
+ case CameraView.ORIENTATION_PORTRAIT:
+ parameters.setRotation(90);
+ break;
+ case CameraView.ORIENTATION_HORIZONTAL:
+ parameters.setRotation(0);
+ break;
+ case CameraView.ORIENTATION_INVERT:
+ parameters.setRotation(180);
+ break;
+ }
+ try {
+ Camera.Size picSize = getOptimalSize(camera.getParameters().getSupportedPictureSizes());
+ parameters.setPictureSize(picSize.width, picSize.height);
+ camera.setParameters(parameters);
+ takingPicture.set(true);
+ cancelAutoFocus();
+ CameraThreadPool.execute(new Runnable() {
+ @Override
+ public void run() {
+ camera.takePicture(null, null, new Camera.PictureCallback() {
+ @Override
+ public void onPictureTaken(byte[] data, Camera camera) {
+ startPreview(false);
+ takingPicture.set(false);
+ if (onTakePictureCallback != null) {
+ onTakePictureCallback.onPictureTaken(data);
+ }
+ }
+ });
+ }
+ });
+
+ } catch (RuntimeException e) {
+ e.printStackTrace();
+ startPreview(false);;
+ takingPicture.set(false);
+ }
+ }
+
+ @Override
+ public void setPermissionCallback(PermissionCallback callback) {
+ this.permissionCallback = callback;
+ }
+
+ public Camera1Control(Context context) {
+ this.context = context;
+ previewView = new PreviewView(context);
+ openCamera();
+ }
+
+ private void openCamera() {
+ setupDisplayView();
+ }
+
+ private void setupDisplayView() {
+ final TextureView textureView = new TextureView(context);
+ previewView.textureView = textureView;
+ previewView.setTextureView(textureView);
+ displayView = previewView;
+ textureView.setSurfaceTextureListener(surfaceTextureListener);
+ }
+
+ private SurfaceTexture surfaceCache;
+
+ private byte[] buffer = null;
+
+ private void setPreviewCallbackImpl() {
+ if (buffer == null) {
+ buffer = new byte[displayView.getWidth()
+ * displayView.getHeight() * ImageFormat.getBitsPerPixel(ImageFormat.NV21) / 8];
+ }
+ if (camera != null && detectType == MODEL_SCAN) {
+ camera.addCallbackBuffer(buffer);
+ camera.setPreviewCallback(previewCallback);
+ }
+ }
+
+ private void clearPreviewCallback() {
+ if (camera != null && detectType == MODEL_SCAN) {
+ camera.setPreviewCallback(null);
+ stopPreview();
+ }
+ }
+
+ Camera.PreviewCallback previewCallback = new Camera.PreviewCallback() {
+ @Override
+ public void onPreviewFrame(final byte[] data, Camera camera) {
+ // 扫描成功阻止打开新线程处理
+ if (abortingScan.get()) {
+ return;
+ }
+ // 节流
+ if (previewFrameCount++ % 5 != 0) {
+ return;
+ }
+
+ // 在某些机型和某项项目中,某些帧的data的数据不符合nv21的格式,需要过滤,否则后续处理会导致crash
+ if (data.length != parameters.getPreviewSize().width * parameters.getPreviewSize().height * 1.5) {
+ return;
+ }
+
+ camera.addCallbackBuffer(buffer);
+
+ CameraThreadPool.execute(new Runnable() {
+ @Override
+ public void run() {
+ Camera1Control.this.onRequestDetect(data);
+ }
+ });
+ }
+ };
+
+
+ private void initCamera() {
+ try {
+ if (camera == null) {
+ Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
+ for (int i = 0; i < Camera.getNumberOfCameras(); i++) {
+ Camera.getCameraInfo(i, cameraInfo);
+ if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
+ cameraId = i;
+ }
+ }
+ try {
+ camera = Camera.open(cameraId);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ startPreview(true);
+ return;
+ }
+
+ }
+ if (parameters == null) {
+ parameters = camera.getParameters();
+ parameters.setPreviewFormat(ImageFormat.NV21);
+ }
+ opPreviewSize(previewView.getWidth(), previewView.getHeight());
+ camera.setPreviewTexture(surfaceCache);
+ setPreviewCallbackImpl();
+ startPreview(false);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private TextureView.SurfaceTextureListener surfaceTextureListener = new TextureView.SurfaceTextureListener() {
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+ surfaceCache = surface;
+ initCamera();
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+ opPreviewSize(previewView.getWidth(), previewView.getHeight());
+ startPreview(false);
+ setPreviewCallbackImpl();
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+ return false;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+ setPreviewCallbackImpl();
+ }
+ };
+
+ // 开启预览
+ private void startPreview(boolean checkPermission) {
+ if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
+ != PackageManager.PERMISSION_GRANTED) {
+ if (checkPermission && permissionCallback != null) {
+ permissionCallback.onRequestPermission();
+ }
+ return;
+ }
+ if (camera == null) {
+ initCamera();
+ } else {
+ camera.startPreview();
+ startAutoFocus();
+ }
+ }
+
+ private void cancelAutoFocus() {
+ camera.cancelAutoFocus();
+ CameraThreadPool.cancelAutoFocusTimer();
+ }
+
+ private void startAutoFocus() {
+ CameraThreadPool.createAutoFocusTimerTask(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (Camera1Control.this) {
+ if (camera != null && !takingPicture.get()) {
+ try {
+ camera.autoFocus(new Camera.AutoFocusCallback() {
+ @Override
+ public void onAutoFocus(boolean success, Camera camera) {
+ }
+ });
+ } catch (Throwable e) {
+ // startPreview是异步实现,可能在某些机器上前几次调用会autofocus failß
+ }
+ }
+ }
+ }
+ });
+ }
+
+ private void opPreviewSize(int width, @SuppressWarnings("unused") int height) {
+
+ if (parameters != null && camera != null && width > 0) {
+ optSize = getOptimalSize(camera.getParameters().getSupportedPreviewSizes());
+ parameters.setPreviewSize(optSize.width, optSize.height);
+ previewView.setRatio(1.0f * optSize.width / optSize.height);
+
+ camera.setDisplayOrientation(getSurfaceOrientation());
+ stopPreview();
+ try {
+ camera.setParameters(parameters);
+ } catch (RuntimeException e) {
+ e.printStackTrace();
+
+ }
+ }
+ }
+
+ private Camera.Size getOptimalSize(List sizes) {
+ int width = previewView.textureView.getWidth();
+ int height = previewView.textureView.getHeight();
+ Camera.Size pictureSize = sizes.get(0);
+
+ List candidates = new ArrayList<>();
+
+ for (Camera.Size size : sizes) {
+ if (size.width >= width && size.height >= height && size.width * height == size.height * width) {
+ // 比例相同
+ candidates.add(size);
+ } else if (size.height >= width && size.width >= height && size.width * width == size.height * height) {
+ // 反比例
+ candidates.add(size);
+ }
+ }
+ if (!candidates.isEmpty()) {
+ return Collections.min(candidates, sizeComparator);
+ }
+
+ for (Camera.Size size : sizes) {
+ if (size.width > width && size.height > height) {
+ return size;
+ }
+ }
+
+ return pictureSize;
+ }
+
+ private Comparator sizeComparator = new Comparator() {
+ @Override
+ public int compare(Camera.Size lhs, Camera.Size rhs) {
+ return Long.signum((long) lhs.width * lhs.height - (long) rhs.width * rhs.height);
+ }
+ };
+
+ private void updateFlashMode(int flashMode) {
+ switch (flashMode) {
+ case FLASH_MODE_TORCH:
+ parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
+ break;
+ case FLASH_MODE_OFF:
+ parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
+ break;
+ case ICameraControl.FLASH_MODE_AUTO:
+ parameters.setFlashMode(Camera.Parameters.FLASH_MODE_AUTO);
+ break;
+ default:
+ parameters.setFlashMode(Camera.Parameters.FLASH_MODE_AUTO);
+ break;
+ }
+ camera.setParameters(parameters);
+ }
+
+ private int getSurfaceOrientation() {
+ @CameraView.Orientation
+ int orientation = displayOrientation;
+ switch (orientation) {
+ case CameraView.ORIENTATION_PORTRAIT:
+ return 90;
+ case CameraView.ORIENTATION_HORIZONTAL:
+ return 0;
+ case CameraView.ORIENTATION_INVERT:
+ return 180;
+ default:
+ return 90;
+ }
+ }
+
+ /**
+ * 有些相机匹配不到完美的比例。比如。我们的layout是4:3的。预览只有16:9
+ * 的,如果直接显示图片会拉伸,变形。缩放的话,又有黑边。所以我们采取的策略
+ * 是,等比例放大。这样预览的有一部分会超出屏幕。拍照后再进行裁剪处理。
+ */
+ private class PreviewView extends FrameLayout {
+
+ private TextureView textureView;
+
+ private float ratio = 0.75f;
+
+ void setTextureView(TextureView textureView) {
+ this.textureView = textureView;
+ removeAllViews();
+ addView(textureView);
+ }
+
+ void setRatio(float ratio) {
+ this.ratio = ratio;
+ requestLayout();
+ relayout(getWidth(), getHeight());
+ }
+
+ public PreviewView(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ relayout(w, h);
+ }
+
+ private void relayout(int w, int h) {
+ int width = w;
+ int height = h;
+ if (w < h) {
+ // 垂直模式,高度固定。
+ height = (int) (width * ratio);
+ } else {
+ // 水平模式,宽度固定。
+ width = (int) (height * ratio);
+ }
+
+ int l = (getWidth() - width) / 2;
+ int t = (getHeight() - height) / 2;
+
+ previewFrame.left = l;
+ previewFrame.top = t;
+ previewFrame.right = l + width;
+ previewFrame.bottom = t + height;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ textureView.layout(previewFrame.left, previewFrame.top, previewFrame.right, previewFrame.bottom);
+ }
+ }
+
+ @Override
+ public Rect getPreviewFrame() {
+ return previewFrame;
+ }
+}
diff --git a/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/Camera2Control.java b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/Camera2Control.java
new file mode 100644
index 0000000..a6db0d0
--- /dev/null
+++ b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/Camera2Control.java
@@ -0,0 +1,698 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+
+package com.baidu.ocr.ui.camera;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.ImageFormat;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.media.Image;
+import android.media.ImageReader;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import android.util.Size;
+import android.util.SparseIntArray;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+public class Camera2Control implements ICameraControl {
+ @Override
+ public void setDetectCallback(OnDetectPictureCallback callback) {
+ // TODO 暂时只用camera
+ }
+
+ @Override
+ public AtomicBoolean getAbortingScan() {
+ // TODO 暂时只用camera
+ return null;
+ }
+
+ /**
+ * Conversion from screen rotation to JPEG orientation.
+ */
+ private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
+ private static final int MAX_PREVIEW_SIZE = 2048;
+
+ static {
+ ORIENTATIONS.append(Surface.ROTATION_0, 90);
+ ORIENTATIONS.append(Surface.ROTATION_90, 0);
+ ORIENTATIONS.append(Surface.ROTATION_180, 270);
+ ORIENTATIONS.append(Surface.ROTATION_270, 180);
+ }
+
+ private static final int STATE_PREVIEW = 0;
+ private static final int STATE_WAITING_FOR_LOCK = 1;
+ private static final int STATE_WAITING_FOR_CAPTURE = 2;
+ private static final int STATE_CAPTURING = 3;
+ private static final int STATE_PICTURE_TAKEN = 4;
+
+ private static final int MAX_PREVIEW_WIDTH = 1920;
+ private static final int MAX_PREVIEW_HEIGHT = 1080;
+
+ private int flashMode;
+ private int orientation = 0;
+ private int state = STATE_PREVIEW;
+
+ private Context context;
+ private OnTakePictureCallback onTakePictureCallback;
+ private PermissionCallback permissionCallback;
+
+ private String cameraId;
+ private TextureView textureView;
+ private Size previewSize;
+ private Rect previewFrame = new Rect();
+
+ private HandlerThread backgroundThread;
+ private Handler backgroundHandler;
+ private ImageReader imageReader;
+ private CameraCaptureSession captureSession;
+ private CameraDevice cameraDevice;
+
+ private CaptureRequest.Builder previewRequestBuilder;
+ private CaptureRequest previewRequest;
+
+ private Semaphore cameraLock = new Semaphore(1);
+ private int sensorOrientation;
+
+ @Override
+ public void start() {
+ startBackgroundThread();
+ if (textureView.isAvailable()) {
+ openCamera(textureView.getWidth(), textureView.getHeight());
+ textureView.setSurfaceTextureListener(surfaceTextureListener);
+ } else {
+ textureView.setSurfaceTextureListener(surfaceTextureListener);
+ }
+ }
+
+ @Override
+ public void stop() {
+ textureView.setSurfaceTextureListener(null);
+ closeCamera();
+ stopBackgroundThread();
+ }
+
+ @Override
+ public void pause() {
+ setFlashMode(FLASH_MODE_OFF);
+ }
+
+ @Override
+ public void resume() {
+ state = STATE_PREVIEW;
+ }
+
+ @Override
+ public View getDisplayView() {
+ return textureView;
+ }
+
+ @Override
+ public Rect getPreviewFrame() {
+ return previewFrame;
+ }
+
+ @Override
+ public void takePicture(OnTakePictureCallback callback) {
+ this.onTakePictureCallback = callback;
+ // 拍照第一步,对焦
+ lockFocus();
+ }
+
+ @Override
+ public void setPermissionCallback(PermissionCallback callback) {
+ this.permissionCallback = callback;
+ }
+
+ @Override
+ public void setDisplayOrientation(@CameraView.Orientation int displayOrientation) {
+ this.orientation = displayOrientation / 90;
+ }
+
+ @Override
+ public void refreshPermission() {
+ openCamera(textureView.getWidth(), textureView.getHeight());
+ }
+
+ @Override
+ public void setFlashMode(@FlashMode int flashMode) {
+ if (this.flashMode == flashMode) {
+ return;
+ }
+ this.flashMode = flashMode;
+ try {
+ previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+ updateFlashMode(flashMode, previewRequestBuilder);
+ previewRequest = previewRequestBuilder.build();
+ captureSession.setRepeatingRequest(previewRequest, captureCallback, backgroundHandler);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public int getFlashMode() {
+ return flashMode;
+ }
+
+ public Camera2Control(Context activity) {
+ this.context = activity;
+ textureView = new TextureView(activity);
+ }
+
+ private final TextureView.SurfaceTextureListener surfaceTextureListener =
+ new TextureView.SurfaceTextureListener() {
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height) {
+ openCamera(width, height);
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, int height) {
+ configureTransform(width, height);
+ previewFrame.left = 0;
+ previewFrame.top = 0;
+ previewFrame.right = width;
+ previewFrame.bottom = height;
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) {
+ stop();
+ return true;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture texture) {
+ }
+ };
+
+ private void openCamera(int width, int height) {
+ // 6.0+的系统需要检查系统权限 。
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
+ != PackageManager.PERMISSION_GRANTED) {
+ requestCameraPermission();
+ return;
+ }
+ setUpCameraOutputs(width, height);
+ configureTransform(width, height);
+ CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+ try {
+ if (!cameraLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
+ throw new RuntimeException("Time out waiting to lock camera opening.");
+ }
+ manager.openCamera(cameraId, deviceStateCallback, backgroundHandler);
+ } catch (CameraAccessException e) {
+ e.printStackTrace();
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
+ }
+ }
+
+ private final CameraDevice.StateCallback deviceStateCallback = new CameraDevice.StateCallback() {
+ @Override
+ public void onOpened(@NonNull CameraDevice cameraDevice) {
+ cameraLock.release();
+ Camera2Control.this.cameraDevice = cameraDevice;
+ createCameraPreviewSession();
+ }
+
+ @Override
+ public void onDisconnected(@NonNull CameraDevice cameraDevice) {
+ cameraLock.release();
+ cameraDevice.close();
+ Camera2Control.this.cameraDevice = null;
+ }
+
+ @Override
+ public void onError(@NonNull CameraDevice cameraDevice, int error) {
+ cameraLock.release();
+ cameraDevice.close();
+ Camera2Control.this.cameraDevice = null;
+ }
+ };
+
+ private void createCameraPreviewSession() {
+ try {
+ SurfaceTexture texture = textureView.getSurfaceTexture();
+ assert texture != null;
+ texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());
+ Surface surface = new Surface(texture);
+ previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+ previewRequestBuilder.addTarget(surface);
+ updateFlashMode(this.flashMode, previewRequestBuilder);
+ cameraDevice.createCaptureSession(Arrays.asList(surface, imageReader.getSurface()),
+ new CameraCaptureSession.StateCallback() {
+
+ @Override
+ public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
+ // The camera is already closed
+ if (null == cameraDevice) {
+ return;
+ }
+ captureSession = cameraCaptureSession;
+ try {
+ previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+ previewRequest = previewRequestBuilder.build();
+ captureSession.setRepeatingRequest(previewRequest,
+ captureCallback, backgroundHandler);
+ } catch (CameraAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onConfigureFailed(
+ @NonNull CameraCaptureSession cameraCaptureSession) {
+ // TODO
+ }
+ }, null
+ );
+ } catch (CameraAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private final ImageReader.OnImageAvailableListener onImageAvailableListener =
+ new ImageReader.OnImageAvailableListener() {
+ @Override
+ public void onImageAvailable(ImageReader reader) {
+ if (onTakePictureCallback != null) {
+ Image image = reader.acquireNextImage();
+ ByteBuffer buffer = image.getPlanes()[0].getBuffer();
+ byte[] bytes = new byte[buffer.remaining()];
+ buffer.get(bytes);
+
+ image.close();
+ onTakePictureCallback.onPictureTaken(bytes);
+ }
+ }
+ };
+
+ private CameraCaptureSession.CaptureCallback captureCallback =
+ new CameraCaptureSession.CaptureCallback() {
+ private void process(CaptureResult result) {
+ switch (state) {
+ case STATE_PREVIEW: {
+ break;
+ }
+ case STATE_WAITING_FOR_LOCK: {
+ Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
+ if (afState == null) {
+ captureStillPicture();
+ return;
+ }
+ switch (afState) {
+ case CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED:
+ case CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED:
+ case CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED: {
+ Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
+ if (aeState == null
+ || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
+ captureStillPicture();
+ } else {
+ runPreCaptureSequence();
+ }
+ break;
+ }
+ default:
+ captureStillPicture();
+ }
+ break;
+ }
+ case STATE_WAITING_FOR_CAPTURE: {
+ Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
+ if (aeState == null
+ || aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE
+ || aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) {
+ state = STATE_CAPTURING;
+ } else {
+ if (aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
+ captureStillPicture();
+ }
+ }
+ break;
+ }
+ case STATE_CAPTURING: {
+ Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
+ if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) {
+ captureStillPicture();
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ @Override
+ public void onCaptureProgressed(@NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ @NonNull CaptureResult partialResult) {
+ process(partialResult);
+ }
+
+ @Override
+ public void onCaptureCompleted(@NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ @NonNull TotalCaptureResult result) {
+ process(result);
+ }
+
+ };
+
+ private Size getOptimalSize(Size[] choices, int textureViewWidth,
+ int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio) {
+ List bigEnough = new ArrayList<>();
+ List notBigEnough = new ArrayList<>();
+ int w = aspectRatio.getWidth();
+ int h = aspectRatio.getHeight();
+ for (Size option : choices) {
+ if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight
+ && option.getHeight() == option.getWidth() * h / w) {
+ if (option.getWidth() >= textureViewWidth
+ && option.getHeight() >= textureViewHeight) {
+ bigEnough.add(option);
+ } else {
+ notBigEnough.add(option);
+ }
+ }
+ }
+
+ // Pick the smallest of those big enough. If there is no one big enough, pick the
+ // largest of those not big enough.
+ if (bigEnough.size() > 0) {
+ return Collections.min(bigEnough, sizeComparator);
+ }
+
+ for (Size option : choices) {
+ if (option.getWidth() >= maxWidth && option.getHeight() >= maxHeight) {
+ return option;
+ }
+ }
+
+ if (notBigEnough.size() > 0) {
+ return Collections.max(notBigEnough, sizeComparator);
+ }
+
+ return choices[0];
+ }
+
+ private Comparator sizeComparator = new Comparator() {
+ @Override
+ public int compare(Size lhs, Size rhs) {
+ return Long.signum((long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight());
+ }
+ };
+
+ private void requestCameraPermission() {
+ if (permissionCallback != null) {
+ permissionCallback.onRequestPermission();
+ }
+ }
+
+ @SuppressWarnings("SuspiciousNameCombination")
+ private void setUpCameraOutputs(int width, int height) {
+ CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+ try {
+ for (String cameraId : manager.getCameraIdList()) {
+ CameraCharacteristics characteristics =
+ manager.getCameraCharacteristics(cameraId);
+
+ Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
+ if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) {
+ continue;
+ }
+
+ StreamConfigurationMap map = characteristics.get(
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+ if (map == null) {
+ continue;
+ }
+
+ // int preferredPictureSize = (int) (Math.max(textureView.getWidth(), textureView
+ // .getHeight()) * 1.5);
+ // preferredPictureSize = Math.min(preferredPictureSize, 2560);
+
+ WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ Point screenSize = new Point();
+ windowManager.getDefaultDisplay().getSize(screenSize);
+ int maxImageSize = Math.max(MAX_PREVIEW_SIZE, screenSize.y * 3 / 2);
+
+ Size size = getOptimalSize(map.getOutputSizes(ImageFormat.JPEG), textureView.getWidth(),
+ textureView.getHeight(), maxImageSize, maxImageSize, new Size(4, 3));
+
+ imageReader = ImageReader.newInstance(size.getWidth(), size.getHeight(),
+ ImageFormat.JPEG, 1);
+ imageReader.setOnImageAvailableListener(
+ onImageAvailableListener, backgroundHandler);
+
+ int displayRotation = orientation;
+ // noinspection ConstantConditions
+ sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
+ boolean swappedDimensions = false;
+ switch (displayRotation) {
+ case Surface.ROTATION_0:
+ case Surface.ROTATION_180:
+ if (sensorOrientation == 90 || sensorOrientation == 270) {
+ swappedDimensions = true;
+ }
+ break;
+ case Surface.ROTATION_90:
+ case Surface.ROTATION_270:
+ if (sensorOrientation == 0 || sensorOrientation == 180) {
+ swappedDimensions = true;
+ }
+ break;
+ default:
+ }
+
+ int rotatedPreviewWidth = width;
+ int rotatedPreviewHeight = height;
+ int maxPreviewWidth = screenSize.x;
+ int maxPreviewHeight = screenSize.y;
+
+ if (swappedDimensions) {
+ rotatedPreviewWidth = height;
+ rotatedPreviewHeight = width;
+ maxPreviewWidth = screenSize.y;
+ maxPreviewHeight = screenSize.x;
+ }
+
+ maxPreviewWidth = Math.min(maxPreviewWidth, MAX_PREVIEW_WIDTH);
+ maxPreviewHeight = Math.min(maxPreviewHeight, MAX_PREVIEW_HEIGHT);
+
+ previewSize = getOptimalSize(map.getOutputSizes(SurfaceTexture.class),
+ rotatedPreviewWidth, rotatedPreviewHeight, maxPreviewWidth,
+ maxPreviewHeight, size);
+ this.cameraId = cameraId;
+ return;
+ }
+ } catch (CameraAccessException | NullPointerException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void closeCamera() {
+ try {
+ cameraLock.acquire();
+ if (null != captureSession) {
+ captureSession.close();
+ captureSession = null;
+ }
+ if (null != cameraDevice) {
+ cameraDevice.close();
+ cameraDevice = null;
+ }
+ if (null != imageReader) {
+ imageReader.close();
+ imageReader = null;
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while trying to lock camera closing.", e);
+ } finally {
+ cameraLock.release();
+ }
+ }
+
+ private void startBackgroundThread() {
+ backgroundThread = new HandlerThread("ocr_camera");
+ backgroundThread.start();
+ backgroundHandler = new Handler(backgroundThread.getLooper());
+ }
+
+ private void stopBackgroundThread() {
+ if (backgroundThread != null) {
+ backgroundThread.quitSafely();
+ backgroundThread = null;
+ backgroundHandler = null;
+ }
+ }
+
+ private void configureTransform(int viewWidth, int viewHeight) {
+ if (null == textureView || null == previewSize || null == context) {
+ return;
+ }
+ int rotation = orientation;
+ Matrix matrix = new Matrix();
+ RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
+ RectF bufferRect = new RectF(0, 0, previewSize.getHeight(), previewSize.getWidth());
+ float centerX = viewRect.centerX();
+ float centerY = viewRect.centerY();
+ if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) {
+ bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY());
+ matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL);
+ float scale = Math.max(
+ (float) viewHeight / previewSize.getHeight(),
+ (float) viewWidth / previewSize.getWidth());
+ matrix.postScale(scale, scale, centerX, centerY);
+ matrix.postRotate(90 * (rotation - 2), centerX, centerY);
+ } else if (Surface.ROTATION_180 == rotation) {
+ matrix.postRotate(180, centerX, centerY);
+ }
+ textureView.setTransform(matrix);
+ }
+
+ // 拍照前,先对焦
+ private void lockFocus() {
+ if (captureSession != null && state == STATE_PREVIEW) {
+ try {
+ previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
+ CameraMetadata.CONTROL_AF_TRIGGER_START);
+ state = STATE_WAITING_FOR_LOCK;
+ captureSession.capture(previewRequestBuilder.build(), captureCallback,
+ backgroundHandler);
+ } catch (CameraAccessException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void runPreCaptureSequence() {
+ try {
+ previewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
+ state = STATE_WAITING_FOR_CAPTURE;
+ captureSession.capture(previewRequestBuilder.build(), captureCallback,
+ backgroundHandler);
+ } catch (CameraAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ // 拍照session
+ private void captureStillPicture() {
+ try {
+ if (null == context || null == cameraDevice) {
+ return;
+ }
+ final CaptureRequest.Builder captureBuilder =
+ cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
+ captureBuilder.addTarget(imageReader.getSurface());
+ captureBuilder.set(CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+
+ captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(orientation));
+ updateFlashMode(this.flashMode, captureBuilder);
+ CameraCaptureSession.CaptureCallback captureCallback =
+ new CameraCaptureSession.CaptureCallback() {
+ @Override
+ public void onCaptureCompleted(@NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ @NonNull TotalCaptureResult result) {
+ unlockFocus();
+ }
+
+ @Override
+ public void onCaptureFailed(@NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ @NonNull CaptureFailure failure) {
+ super.onCaptureFailed(session, request, failure);
+ }
+ };
+
+ // 停止预览
+ captureSession.stopRepeating();
+ captureSession.capture(captureBuilder.build(), captureCallback, backgroundHandler);
+ state = STATE_PICTURE_TAKEN;
+ // unlockFocus();
+ } catch (CameraAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private int getOrientation(int rotation) {
+ return (ORIENTATIONS.get(rotation) + sensorOrientation + 270) % 360;
+ }
+
+ // 停止对焦
+ private void unlockFocus() {
+ try {
+ previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
+ CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
+ captureSession.capture(previewRequestBuilder.build(), captureCallback,
+ backgroundHandler);
+ state = STATE_PREVIEW;
+ // 预览
+ captureSession.setRepeatingRequest(previewRequest, captureCallback,
+ backgroundHandler);
+ textureView.setSurfaceTextureListener(surfaceTextureListener);
+ } catch (CameraAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void updateFlashMode(@FlashMode int flashMode, CaptureRequest.Builder builder) {
+ switch (flashMode) {
+ case FLASH_MODE_TORCH:
+ builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH);
+ break;
+ case FLASH_MODE_OFF:
+ builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF);
+ break;
+ case ICameraControl.FLASH_MODE_AUTO:
+ default:
+ builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_SINGLE);
+ break;
+ }
+ }
+}
diff --git a/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/CameraActivity.java b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/CameraActivity.java
new file mode 100644
index 0000000..a9aa3cf
--- /dev/null
+++ b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/CameraActivity.java
@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.baidu.ocr.ui.camera;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+import com.baidu.idcardquality.IDcardQualityProcess;
+import com.baidu.ocr.ui.R;
+import com.baidu.ocr.ui.crop.CropView;
+import com.baidu.ocr.ui.crop.FrameOverlayView;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.MediaStore;
+import android.view.Surface;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.Toast;
+
+import androidx.core.app.ActivityCompat;
+
+public class CameraActivity extends Activity {
+
+ public static final String KEY_OUTPUT_FILE_PATH = "outputFilePath";
+ public static final String KEY_CONTENT_TYPE = "contentType";
+ public static final String KEY_NATIVE_TOKEN = "nativeToken";
+ public static final String KEY_NATIVE_ENABLE = "nativeEnable";
+ public static final String KEY_NATIVE_MANUAL = "nativeEnableManual";
+
+ public static final String CONTENT_TYPE_GENERAL = "general";
+ public static final String CONTENT_TYPE_ID_CARD_FRONT = "IDCardFront";
+ public static final String CONTENT_TYPE_ID_CARD_BACK = "IDCardBack";
+ public static final String CONTENT_TYPE_BANK_CARD = "bankCard";
+ public static final String CONTENT_TYPE_PASSPORT = "passport";
+
+ private static final int REQUEST_CODE_PICK_IMAGE = 100;
+ private static final int PERMISSIONS_REQUEST_CAMERA = 800;
+ private static final int PERMISSIONS_EXTERNAL_STORAGE = 801;
+
+ private File outputFile;
+ private String contentType;
+ private Handler handler = new Handler();
+
+ private boolean isNativeEnable;
+ private boolean isNativeManual;
+
+ private OCRCameraLayout takePictureContainer;
+ private OCRCameraLayout cropContainer;
+ private OCRCameraLayout confirmResultContainer;
+ private ImageView lightButton;
+ private CameraView cameraView;
+ private ImageView displayImageView;
+ private CropView cropView;
+ private FrameOverlayView overlayView;
+ private MaskView cropMaskView;
+ private ImageView takePhotoBtn;
+ private PermissionCallback permissionCallback = new PermissionCallback() {
+ @Override
+ public boolean onRequestPermission() {
+ ActivityCompat.requestPermissions(CameraActivity.this,
+ new String[] {Manifest.permission.CAMERA},
+ PERMISSIONS_REQUEST_CAMERA);
+ return false;
+ }
+ };
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.bd_ocr_activity_camera);
+
+ takePictureContainer = (OCRCameraLayout) findViewById(R.id.take_picture_container);
+ confirmResultContainer = (OCRCameraLayout) findViewById(R.id.confirm_result_container);
+
+ cameraView = (CameraView) findViewById(R.id.camera_view);
+ cameraView.getCameraControl().setPermissionCallback(permissionCallback);
+ lightButton = (ImageView) findViewById(R.id.light_button);
+ lightButton.setOnClickListener(lightButtonOnClickListener);
+ takePhotoBtn = (ImageView) findViewById(R.id.take_photo_button);
+ findViewById(R.id.album_button).setOnClickListener(albumButtonOnClickListener);
+ takePhotoBtn.setOnClickListener(takeButtonOnClickListener);
+
+ // confirm result;
+ displayImageView = (ImageView) findViewById(R.id.display_image_view);
+ confirmResultContainer.findViewById(R.id.confirm_button).setOnClickListener(confirmButtonOnClickListener);
+ confirmResultContainer.findViewById(R.id.cancel_button).setOnClickListener(confirmCancelButtonOnClickListener);
+ findViewById(R.id.rotate_button).setOnClickListener(rotateButtonOnClickListener);
+
+ cropView = (CropView) findViewById(R.id.crop_view);
+ cropContainer = (OCRCameraLayout) findViewById(R.id.crop_container);
+ overlayView = (FrameOverlayView) findViewById(R.id.overlay_view);
+ cropContainer.findViewById(R.id.confirm_button).setOnClickListener(cropConfirmButtonListener);
+ cropMaskView = (MaskView) cropContainer.findViewById(R.id.crop_mask_view);
+ cropContainer.findViewById(R.id.cancel_button).setOnClickListener(cropCancelButtonListener);
+
+ setOrientation(getResources().getConfiguration());
+ initParams();
+
+ cameraView.setAutoPictureCallback(autoTakePictureCallback);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ cameraView.stop();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ cameraView.start();
+ }
+
+ private void initParams() {
+ String outputPath = getIntent().getStringExtra(KEY_OUTPUT_FILE_PATH);
+ final String token = getIntent().getStringExtra(KEY_NATIVE_TOKEN);
+ isNativeEnable = getIntent().getBooleanExtra(KEY_NATIVE_ENABLE, true);
+ isNativeManual = getIntent().getBooleanExtra(KEY_NATIVE_MANUAL, false);
+
+ if (token == null && !isNativeManual) {
+ isNativeEnable = false;
+ }
+
+ if (outputPath != null) {
+ outputFile = new File(outputPath);
+ }
+
+ contentType = getIntent().getStringExtra(KEY_CONTENT_TYPE);
+ if (contentType == null) {
+ contentType = CONTENT_TYPE_GENERAL;
+ }
+ int maskType;
+ switch (contentType) {
+ case CONTENT_TYPE_ID_CARD_FRONT:
+ maskType = MaskView.MASK_TYPE_ID_CARD_FRONT;
+ overlayView.setVisibility(View.INVISIBLE);
+ if (isNativeEnable) {
+ takePhotoBtn.setVisibility(View.INVISIBLE);
+ }
+ break;
+ case CONTENT_TYPE_ID_CARD_BACK:
+ maskType = MaskView.MASK_TYPE_ID_CARD_BACK;
+ overlayView.setVisibility(View.INVISIBLE);
+ if (isNativeEnable) {
+ takePhotoBtn.setVisibility(View.INVISIBLE);
+ }
+ break;
+ case CONTENT_TYPE_BANK_CARD:
+ maskType = MaskView.MASK_TYPE_BANK_CARD;
+ overlayView.setVisibility(View.INVISIBLE);
+ break;
+ case CONTENT_TYPE_PASSPORT:
+ maskType = MaskView.MASK_TYPE_PASSPORT;
+ overlayView.setVisibility(View.INVISIBLE);
+ break;
+ case CONTENT_TYPE_GENERAL:
+ default:
+ maskType = MaskView.MASK_TYPE_NONE;
+ cropMaskView.setVisibility(View.INVISIBLE);
+ break;
+ }
+
+ // 身份证本地能力初始化
+ if (maskType == MaskView.MASK_TYPE_ID_CARD_FRONT || maskType == MaskView.MASK_TYPE_ID_CARD_BACK) {
+ if (isNativeEnable && !isNativeManual) {
+ initNative(token);
+ }
+ }
+ cameraView.setEnableScan(isNativeEnable);
+ cameraView.setMaskType(maskType, this);
+ cropMaskView.setMaskType(maskType);
+ }
+
+ private void initNative(final String token) {
+ CameraNativeHelper.init(CameraActivity.this, token,
+ new CameraNativeHelper.CameraNativeInitCallback() {
+ @Override
+ public void onError(int errorCode, Throwable e) {
+ cameraView.setInitNativeStatus(errorCode);
+ }
+ });
+ }
+
+ private void showTakePicture() {
+ cameraView.getCameraControl().resume();
+ updateFlashMode();
+ takePictureContainer.setVisibility(View.VISIBLE);
+ confirmResultContainer.setVisibility(View.INVISIBLE);
+ cropContainer.setVisibility(View.INVISIBLE);
+ }
+
+ private void showCrop() {
+ cameraView.getCameraControl().pause();
+ updateFlashMode();
+ takePictureContainer.setVisibility(View.INVISIBLE);
+ confirmResultContainer.setVisibility(View.INVISIBLE);
+ cropContainer.setVisibility(View.VISIBLE);
+ }
+
+ private void showResultConfirm() {
+ cameraView.getCameraControl().pause();
+ updateFlashMode();
+ takePictureContainer.setVisibility(View.INVISIBLE);
+ confirmResultContainer.setVisibility(View.VISIBLE);
+ cropContainer.setVisibility(View.INVISIBLE);
+ }
+
+ // take photo;
+ private void updateFlashMode() {
+ int flashMode = cameraView.getCameraControl().getFlashMode();
+ if (flashMode == ICameraControl.FLASH_MODE_TORCH) {
+ lightButton.setImageResource(R.drawable.bd_ocr_light_on);
+ } else {
+ lightButton.setImageResource(R.drawable.bd_ocr_light_off);
+ }
+ }
+
+ private View.OnClickListener albumButtonOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.READ_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ ActivityCompat.requestPermissions(CameraActivity.this,
+ new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
+ PERMISSIONS_EXTERNAL_STORAGE);
+ return;
+ }
+ }
+ Intent intent = new Intent(Intent.ACTION_PICK);
+ intent.setType("image/*");
+ startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE);
+ }
+ };
+
+ private View.OnClickListener lightButtonOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (cameraView.getCameraControl().getFlashMode() == ICameraControl.FLASH_MODE_OFF) {
+ cameraView.getCameraControl().setFlashMode(ICameraControl.FLASH_MODE_TORCH);
+ } else {
+ cameraView.getCameraControl().setFlashMode(ICameraControl.FLASH_MODE_OFF);
+ }
+ updateFlashMode();
+ }
+ };
+
+ private View.OnClickListener takeButtonOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ cameraView.takePicture(outputFile, takePictureCallback);
+ }
+ };
+
+ private CameraView.OnTakePictureCallback autoTakePictureCallback = new CameraView.OnTakePictureCallback() {
+ @Override
+ public void onPictureTaken(final Bitmap bitmap) {
+ CameraThreadPool.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream);
+ bitmap.recycle();
+ fileOutputStream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ Intent intent = new Intent();
+ intent.putExtra(CameraActivity.KEY_CONTENT_TYPE, contentType);
+ setResult(Activity.RESULT_OK, intent);
+ finish();
+ }
+ });
+ }
+ };
+
+ private CameraView.OnTakePictureCallback takePictureCallback = new CameraView.OnTakePictureCallback() {
+ @Override
+ public void onPictureTaken(final Bitmap bitmap) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ takePictureContainer.setVisibility(View.INVISIBLE);
+ if (cropMaskView.getMaskType() == MaskView.MASK_TYPE_NONE) {
+ cropView.setFilePath(outputFile.getAbsolutePath());
+ showCrop();
+ } else if (cropMaskView.getMaskType() == MaskView.MASK_TYPE_BANK_CARD) {
+ cropView.setFilePath(outputFile.getAbsolutePath());
+ cropMaskView.setVisibility(View.INVISIBLE);
+ overlayView.setVisibility(View.VISIBLE);
+ overlayView.setTypeWide();
+ showCrop();
+ } else {
+ displayImageView.setImageBitmap(bitmap);
+ showResultConfirm();
+ }
+ }
+ });
+ }
+ };
+
+ private View.OnClickListener cropCancelButtonListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // 释放 cropView中的bitmap;
+ cropView.setFilePath(null);
+ showTakePicture();
+ }
+ };
+
+ private View.OnClickListener cropConfirmButtonListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int maskType = cropMaskView.getMaskType();
+ Rect rect;
+ switch (maskType) {
+ case MaskView.MASK_TYPE_BANK_CARD:
+ case MaskView.MASK_TYPE_ID_CARD_BACK:
+ case MaskView.MASK_TYPE_ID_CARD_FRONT:
+ rect = cropMaskView.getFrameRect();
+ break;
+ case MaskView.MASK_TYPE_NONE:
+ default:
+ rect = overlayView.getFrameRect();
+ break;
+ }
+ Bitmap cropped = cropView.crop(rect);
+ displayImageView.setImageBitmap(cropped);
+ cropAndConfirm();
+ }
+ };
+
+ private void cropAndConfirm() {
+ cameraView.getCameraControl().pause();
+ updateFlashMode();
+ doConfirmResult();
+ }
+
+ private void doConfirmResult() {
+ CameraThreadPool.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
+ Bitmap bitmap = ((BitmapDrawable) displayImageView.getDrawable()).getBitmap();
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream);
+ fileOutputStream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ Intent intent = new Intent();
+ intent.putExtra(CameraActivity.KEY_CONTENT_TYPE, contentType);
+ setResult(Activity.RESULT_OK, intent);
+ finish();
+ }
+ });
+ }
+
+ private View.OnClickListener confirmButtonOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ doConfirmResult();
+ }
+ };
+
+ private View.OnClickListener confirmCancelButtonOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ displayImageView.setImageBitmap(null);
+ showTakePicture();
+ }
+ };
+
+ private View.OnClickListener rotateButtonOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ cropView.rotate(90);
+ }
+ };
+
+ private String getRealPathFromURI(Uri contentURI) {
+ String result;
+ Cursor cursor = null;
+ try {
+ cursor = getContentResolver().query(contentURI, null, null, null, null);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ 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;
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ setOrientation(newConfig);
+ }
+
+ private void setOrientation(Configuration newConfig) {
+ int rotation = getWindowManager().getDefaultDisplay().getRotation();
+ int orientation;
+ int cameraViewOrientation = CameraView.ORIENTATION_PORTRAIT;
+ switch (newConfig.orientation) {
+ case Configuration.ORIENTATION_PORTRAIT:
+ cameraViewOrientation = CameraView.ORIENTATION_PORTRAIT;
+ orientation = OCRCameraLayout.ORIENTATION_PORTRAIT;
+ break;
+ case Configuration.ORIENTATION_LANDSCAPE:
+ orientation = OCRCameraLayout.ORIENTATION_HORIZONTAL;
+ if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) {
+ cameraViewOrientation = CameraView.ORIENTATION_HORIZONTAL;
+ } else {
+ cameraViewOrientation = CameraView.ORIENTATION_INVERT;
+ }
+ break;
+ default:
+ orientation = OCRCameraLayout.ORIENTATION_PORTRAIT;
+ cameraView.setOrientation(CameraView.ORIENTATION_PORTRAIT);
+ break;
+ }
+ takePictureContainer.setOrientation(orientation);
+ cameraView.setOrientation(cameraViewOrientation);
+ cropContainer.setOrientation(orientation);
+ confirmResultContainer.setOrientation(orientation);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == REQUEST_CODE_PICK_IMAGE) {
+ if (resultCode == Activity.RESULT_OK) {
+ Uri uri = data.getData();
+ cropView.setFilePath(getRealPathFromURI(uri));
+ showCrop();
+ } else {
+ cameraView.getCameraControl().resume();
+ }
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions,
+ int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ switch (requestCode) {
+ case PERMISSIONS_REQUEST_CAMERA: {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ cameraView.getCameraControl().refreshPermission();
+ } else {
+ Toast.makeText(getApplicationContext(), R.string.camera_permission_required, Toast.LENGTH_LONG)
+ .show();
+ }
+ break;
+ }
+ case PERMISSIONS_EXTERNAL_STORAGE:
+ default:
+ break;
+ }
+ }
+
+ /**
+ * 做一些收尾工作
+ *
+ */
+ private void doClear() {
+ CameraThreadPool.cancelAutoFocusTimer();
+ if (isNativeEnable && !isNativeManual) {
+ IDcardQualityProcess.getInstance().releaseModel();
+ }
+ }
+
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ this.doClear();
+ }
+}
diff --git a/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/CameraNativeHelper.java b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/CameraNativeHelper.java
new file mode 100644
index 0000000..29a6d74
--- /dev/null
+++ b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/CameraNativeHelper.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2018 Baidu, Inc. All Rights Reserved.
+ */
+package com.baidu.ocr.ui.camera;
+
+import android.content.Context;
+
+import com.baidu.idcardquality.IDcardQualityProcess;
+
+/**
+ * Created by ruanshimin on 2018/1/23.
+ */
+
+public class CameraNativeHelper {
+
+ public interface CameraNativeInitCallback {
+ /**
+ * 加载本地库异常回调
+ *
+ * @param errorCode 错误代码
+ * @param e 如果加载so异常则会有异常对象传入
+ */
+ void onError(int errorCode, Throwable e);
+ }
+
+ public static void init(final Context ctx, final String token, final CameraNativeInitCallback cb) {
+ CameraThreadPool.execute(new Runnable() {
+ @Override
+ public void run() {
+ int status;
+ // 加载本地so失败, 异常返回getloadSoException
+ if (IDcardQualityProcess.getLoadSoException() != null) {
+ status = CameraView.NATIVE_SOLOAD_FAIL;
+ cb.onError(status, IDcardQualityProcess.getLoadSoException());
+ return;
+ }
+ // 授权状态
+ int authStatus = IDcardQualityProcess.init(token);
+ if (authStatus != 0) {
+ cb.onError(CameraView.NATIVE_AUTH_FAIL, null);
+ return;
+ }
+
+ // 加载模型状态
+ int initModelStatus = IDcardQualityProcess.getInstance()
+ .idcardQualityInit(ctx.getAssets(),
+ "models");
+
+ if (initModelStatus != 0) {
+ cb.onError(CameraView.NATIVE_INIT_FAIL, null);
+ }
+ }
+ });
+ }
+
+ public static void release() {
+ IDcardQualityProcess.getInstance().releaseModel();
+ }
+}
diff --git a/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/CameraThreadPool.java b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/CameraThreadPool.java
new file mode 100644
index 0000000..b1d97ff
--- /dev/null
+++ b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/CameraThreadPool.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.baidu.ocr.ui.camera;
+
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class CameraThreadPool {
+
+ static Timer timerFocus = null;
+
+ /*
+ * 对焦频率
+ */
+ static final long cameraScanInterval = 2000;
+
+ /*
+ * 线程池大小
+ */
+ private static int poolCount = Runtime.getRuntime().availableProcessors();
+
+ private static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(poolCount);
+
+ /**
+ * 给线程池添加任务
+ * @param runnable 任务
+ */
+ public static void execute(Runnable runnable) {
+ fixedThreadPool.execute(runnable);
+ }
+
+ /**
+ * 创建一个定时对焦的timer任务
+ * @param runnable 对焦代码
+ * @return Timer Timer对象,用来终止自动对焦
+ */
+ public static Timer createAutoFocusTimerTask(final Runnable runnable) {
+ if (timerFocus != null) {
+ return timerFocus;
+ }
+ timerFocus = new Timer();
+ TimerTask task = new TimerTask() {
+ @Override
+ public void run() {
+ runnable.run();
+ }
+ };
+ timerFocus.scheduleAtFixedRate(task, 0, cameraScanInterval);
+ return timerFocus;
+ }
+
+ /**
+ * 终止自动对焦任务,实际调用了cancel方法并且清空对象
+ * 但是无法终止执行中的任务,需额外处理
+ *
+ */
+ public static void cancelAutoFocusTimer() {
+ if (timerFocus != null) {
+ timerFocus.cancel();
+ timerFocus = null;
+ }
+ }
+}
diff --git a/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/CameraView.java b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/CameraView.java
new file mode 100644
index 0000000..0951a83
--- /dev/null
+++ b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/CameraView.java
@@ -0,0 +1,651 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.baidu.ocr.ui.camera;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+import com.baidu.idcardquality.IDcardQualityProcess;
+import com.baidu.ocr.ui.R;
+import com.baidu.ocr.ui.util.DimensionUtil;
+import com.baidu.ocr.ui.util.ImageUtil;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Color;
+import android.graphics.ImageFormat;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.media.ImageReader;
+import android.os.Handler;
+import android.os.Looper;
+
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.IntDef;
+
+/**
+ * 负责,相机的管理。同时提供,裁剪遮罩功能。
+ */
+public class CameraView extends FrameLayout {
+
+ private int maskType;
+
+ /**
+ * 照相回调
+ */
+ interface OnTakePictureCallback {
+ void onPictureTaken(Bitmap bitmap);
+ }
+
+ /**
+ * 垂直方向 {@link #setOrientation(int)}
+ */
+ public static final int ORIENTATION_PORTRAIT = 0;
+ /**
+ * 水平方向 {@link #setOrientation(int)}
+ */
+ public static final int ORIENTATION_HORIZONTAL = 90;
+ /**
+ * 水平翻转方向 {@link #setOrientation(int)}
+ */
+ public static final int ORIENTATION_INVERT = 270;
+
+ /**
+ * 本地模型授权,加载成功
+ */
+ public static final int NATIVE_AUTH_INIT_SUCCESS = 0;
+
+ /**
+ * 本地模型授权,缺少SO
+ */
+ public static final int NATIVE_SOLOAD_FAIL = 10;
+
+ /**
+ * 本地模型授权,授权失败,token异常
+ */
+ public static final int NATIVE_AUTH_FAIL = 11;
+
+ /**
+ * 本地模型授权,模型加载失败
+ */
+ public static final int NATIVE_INIT_FAIL = 12;
+
+
+ /**
+ * 是否已经通过本地质量控制扫描
+ */
+ private final int SCAN_SUCCESS = 0;
+
+ public void setInitNativeStatus(int initNativeStatus) {
+ this.initNativeStatus = initNativeStatus;
+ }
+
+ /**
+ * 本地检测初始化,模型加载标识
+ */
+ private int initNativeStatus = NATIVE_AUTH_INIT_SUCCESS;
+
+ @IntDef({ORIENTATION_PORTRAIT, ORIENTATION_HORIZONTAL, ORIENTATION_INVERT})
+ public @interface Orientation {
+
+ }
+
+ private CameraViewTakePictureCallback cameraViewTakePictureCallback = new CameraViewTakePictureCallback();
+
+ private ICameraControl cameraControl;
+
+ /**
+ * 相机预览View
+ */
+ private View displayView;
+ /**
+ * 身份证,银行卡,等裁剪用的遮罩
+ */
+ private MaskView maskView;
+
+ /**
+ * 用于显示提示证 "请对齐身份证正面" 之类的背景
+ */
+ private ImageView hintView;
+
+ /**
+ * 用于显示提示证 "请对齐身份证正面" 之类的文字
+ */
+ private TextView hintViewText;
+
+ /**
+ * 提示文案容器
+ */
+ private LinearLayout hintViewTextWrapper;
+
+ /**
+ * 是否是本地质量控制扫描
+ */
+ private boolean isEnableScan;
+
+ public void setEnableScan(boolean enableScan) {
+ isEnableScan = enableScan;
+ }
+
+ /**
+ * UI线程的handler
+ */
+ Handler uiHandler = new Handler(Looper.getMainLooper());
+
+ public ICameraControl getCameraControl() {
+ return cameraControl;
+ }
+
+ public void setOrientation(@Orientation int orientation) {
+ cameraControl.setDisplayOrientation(orientation);
+ }
+ public CameraView(Context context) {
+ super(context);
+ init();
+ }
+
+ public CameraView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public CameraView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ public void start() {
+ cameraControl.start();
+ setKeepScreenOn(true);
+ }
+
+ public void stop() {
+ cameraControl.stop();
+ setKeepScreenOn(false);
+ }
+
+ public void takePicture(final File file, final OnTakePictureCallback callback) {
+ cameraViewTakePictureCallback.file = file;
+ cameraViewTakePictureCallback.callback = callback;
+ cameraControl.takePicture(cameraViewTakePictureCallback);
+ }
+
+ private OnTakePictureCallback autoPictureCallback;
+
+ public void setAutoPictureCallback(OnTakePictureCallback callback) {
+ autoPictureCallback = callback;
+ }
+
+ public void setMaskType(@MaskView.MaskType int maskType, final Context ctx) {
+ maskView.setMaskType(maskType);
+
+ maskView.setVisibility(VISIBLE);
+ hintView.setVisibility(VISIBLE);
+
+ int hintResourceId = R.drawable.bd_ocr_hint_align_id_card;
+ this.maskType = maskType;
+ boolean isNeedSetImage = true;
+ switch (maskType) {
+ case MaskView.MASK_TYPE_ID_CARD_FRONT:
+ hintResourceId = R.drawable.bd_ocr_round_corner;
+ isNeedSetImage = false;
+ break;
+ case MaskView.MASK_TYPE_ID_CARD_BACK:
+ isNeedSetImage = false;
+ hintResourceId = R.drawable.bd_ocr_round_corner;
+ break;
+ case MaskView.MASK_TYPE_BANK_CARD:
+ hintResourceId = R.drawable.bd_ocr_hint_align_bank_card;
+ break;
+ case MaskView.MASK_TYPE_PASSPORT:
+ hintView.setVisibility(INVISIBLE);
+ break;
+ case MaskView.MASK_TYPE_NONE:
+ default:
+ maskView.setVisibility(INVISIBLE);
+ hintView.setVisibility(INVISIBLE);
+ break;
+ }
+
+ if (isNeedSetImage) {
+ hintView.setImageResource(hintResourceId);
+ hintViewTextWrapper.setVisibility(INVISIBLE);
+ }
+
+ if (maskType == MaskView.MASK_TYPE_ID_CARD_FRONT && isEnableScan) {
+ cameraControl.setDetectCallback(new ICameraControl.OnDetectPictureCallback() {
+ @Override
+ public int onDetect(byte[] data, int rotation) {
+ return detect(data, rotation);
+ }
+ });
+ }
+
+ if (maskType == MaskView.MASK_TYPE_ID_CARD_BACK && isEnableScan) {
+ cameraControl.setDetectCallback(new ICameraControl.OnDetectPictureCallback() {
+ @Override
+ public int onDetect(byte[] data, int rotation) {
+ return detect(data, rotation);
+ }
+ });
+ }
+ }
+
+ private int detect(byte[] data, final int rotation) {
+ if (initNativeStatus != NATIVE_AUTH_INIT_SUCCESS) {
+ showTipMessage(initNativeStatus);
+ return 1;
+ }
+ // 扫描成功阻止多余的操作
+ if (cameraControl.getAbortingScan().get()) {
+ return 0;
+ }
+
+ Rect previewFrame = cameraControl.getPreviewFrame();
+
+ if (maskView.getWidth() == 0 || maskView.getHeight() == 0
+ || previewFrame.width() == 0 || previewFrame.height() == 0) {
+ return 0;
+ }
+
+ // BitmapRegionDecoder不会将整个图片加载到内存。
+ BitmapRegionDecoder decoder = null;
+ try {
+ decoder = BitmapRegionDecoder.newInstance(data, 0, data.length, true);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ int width = rotation % 180 == 0 ? decoder.getWidth() : decoder.getHeight();
+ int height = rotation % 180 == 0 ? decoder.getHeight() : decoder.getWidth();
+
+ Rect frameRect = maskView.getFrameRectExtend();
+
+ int left = width * frameRect.left / maskView.getWidth();
+ int top = height * frameRect.top / maskView.getHeight();
+ int right = width * frameRect.right / maskView.getWidth();
+ int bottom = height * frameRect.bottom / maskView.getHeight();
+
+ // 高度大于图片
+ if (previewFrame.top < 0) {
+ // 宽度对齐。
+ int adjustedPreviewHeight = previewFrame.height() * getWidth() / previewFrame.width();
+ int topInFrame = ((adjustedPreviewHeight - frameRect.height()) / 2)
+ * getWidth() / previewFrame.width();
+ int bottomInFrame = ((adjustedPreviewHeight + frameRect.height()) / 2) * getWidth()
+ / previewFrame.width();
+
+ // 等比例投射到照片当中。
+ top = topInFrame * height / previewFrame.height();
+ bottom = bottomInFrame * height / previewFrame.height();
+ } else {
+ // 宽度大于图片
+ if (previewFrame.left < 0) {
+ // 高度对齐
+ int adjustedPreviewWidth = previewFrame.width() * getHeight() / previewFrame.height();
+ int leftInFrame = ((adjustedPreviewWidth - maskView.getFrameRect().width()) / 2) * getHeight()
+ / previewFrame.height();
+ int rightInFrame = ((adjustedPreviewWidth + maskView.getFrameRect().width()) / 2) * getHeight()
+ / previewFrame.height();
+
+ // 等比例投射到照片当中。
+ left = leftInFrame * width / previewFrame.width();
+ right = rightInFrame * width / previewFrame.width();
+ }
+ }
+
+ Rect region = new Rect();
+ region.left = left;
+ region.top = top;
+ region.right = right;
+ region.bottom = bottom;
+
+ // 90度或者270度旋转
+ if (rotation % 180 == 90) {
+ int x = decoder.getWidth() / 2;
+ int y = decoder.getHeight() / 2;
+
+ int rotatedWidth = region.height();
+ int rotated = region.width();
+
+ // 计算,裁剪框旋转后的坐标
+ region.left = x - rotatedWidth / 2;
+ region.top = y - rotated / 2;
+ region.right = x + rotatedWidth / 2;
+ region.bottom = y + rotated / 2;
+ region.sort();
+ }
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+
+ // 最大图片大小。
+ int maxPreviewImageSize = 2560;
+ int size = Math.min(decoder.getWidth(), decoder.getHeight());
+ size = Math.min(size, maxPreviewImageSize);
+
+ options.inSampleSize = ImageUtil.calculateInSampleSize(options, size, size);
+ options.inScaled = true;
+ options.inDensity = Math.max(options.outWidth, options.outHeight);
+ options.inTargetDensity = size;
+ options.inPreferredConfig = Bitmap.Config.RGB_565;
+ Bitmap bitmap = decoder.decodeRegion(region, options);
+ if (rotation != 0) {
+ // 只能是裁剪完之后再旋转了。有没有别的更好的方案呢?
+ Matrix matrix = new Matrix();
+ matrix.postRotate(rotation);
+ Bitmap rotatedBitmap = Bitmap.createBitmap(
+ bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
+ if (bitmap != rotatedBitmap) {
+ // 有时候 createBitmap会复用对象
+ bitmap.recycle();
+ }
+ bitmap = rotatedBitmap;
+ }
+
+ final int status;
+
+ // 调用本地质量控制请求
+ switch (maskType) {
+ case MaskView.MASK_TYPE_ID_CARD_FRONT:
+ status = IDcardQualityProcess.getInstance().idcardQualityDetectionImg(bitmap, true);
+ break;
+ case MaskView.MASK_TYPE_ID_CARD_BACK:
+ status = IDcardQualityProcess.getInstance().idcardQualityDetectionImg(bitmap, false);
+ break;
+ default:
+ status = 1;
+ }
+
+ // 当有某个扫描处理线程调用成功后,阻止其他线程继续调用本地控制代码
+ if (status == SCAN_SUCCESS) {
+ // 扫描成功阻止多线程同时回调
+ if (!cameraControl.getAbortingScan().compareAndSet(false, true)) {
+ bitmap.recycle();
+ return 0;
+ }
+ autoPictureCallback.onPictureTaken(bitmap);
+ }
+
+ showTipMessage(status);
+
+ return status;
+ }
+
+ private void showTipMessage(final int status) {
+ // 提示tip文字变化
+ uiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (status == 0) {
+ hintViewText.setVisibility(View.INVISIBLE);
+ } else if (!cameraControl.getAbortingScan().get()) {
+ hintViewText.setVisibility(View.VISIBLE);
+ hintViewText.setText(getScanMessage(status));
+ }
+ }
+ });
+ }
+
+ private String getScanMessage(int status) {
+ String message;
+ switch (status) {
+ case 0:
+ message = "";
+ break;
+ case 2:
+ message = "身份证模糊,请重新尝试";
+ break;
+ case 3:
+ message = "身份证反光,请重新尝试";
+ break;
+ case 4:
+ message = "请将身份证前后反转再进行识别";
+ break;
+ case 5:
+ message = "请拿稳镜头和身份证";
+ break;
+ case 6:
+ message = "请将镜头靠近身份证";
+ break;
+ case 7:
+ message = "请将身份证完整置于取景框内";
+ break;
+ case NATIVE_AUTH_FAIL:
+ message = "本地质量控制授权失败";
+ break;
+ case NATIVE_INIT_FAIL:
+ message = "本地模型加载失败";
+ break;
+ case NATIVE_SOLOAD_FAIL:
+ message = "本地SO库加载失败";
+ break;
+ case 1:
+ default:
+ message = "请将身份证置于取景框内";
+ }
+
+
+ return message;
+ }
+
+ private void init() {
+// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+// cameraControl = new Camera2Control(getContext());
+// } else {
+//
+// }
+ cameraControl = new Camera1Control(getContext());
+
+ displayView = cameraControl.getDisplayView();
+ addView(displayView);
+
+ maskView = new MaskView(getContext());
+ addView(maskView);
+
+ hintView = new ImageView(getContext());
+ addView(hintView);
+
+ hintViewTextWrapper = new LinearLayout(getContext());
+ hintViewTextWrapper.setOrientation(LinearLayout.VERTICAL);
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
+ DimensionUtil.dpToPx(25));
+
+ lp.gravity = Gravity.CENTER;
+ hintViewText = new TextView(getContext());
+ hintViewText.setBackgroundResource(R.drawable.bd_ocr_round_corner);
+ hintViewText.setAlpha(0.5f);
+ hintViewText.setPadding(DimensionUtil.dpToPx(10), 0, DimensionUtil.dpToPx(10), 0);
+ hintViewTextWrapper.addView(hintViewText, lp);
+
+
+ hintViewText.setGravity(Gravity.CENTER);
+ hintViewText.setTextColor(Color.WHITE);
+ hintViewText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
+ hintViewText.setText(getScanMessage(-1));
+
+
+ addView(hintViewTextWrapper, lp);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ displayView.layout(left, 0, right, bottom - top);
+ maskView.layout(left, 0, right, bottom - top);
+
+ int hintViewWidth = DimensionUtil.dpToPx(250);
+ int hintViewHeight = DimensionUtil.dpToPx(25);
+
+ int hintViewLeft = (getWidth() - hintViewWidth) / 2;
+ int hintViewTop = maskView.getFrameRect().bottom + DimensionUtil.dpToPx(16);
+
+ hintViewTextWrapper.layout(hintViewLeft, hintViewTop,
+ hintViewLeft + hintViewWidth, hintViewTop + hintViewHeight);
+
+ hintView.layout(hintViewLeft, hintViewTop,
+ hintViewLeft + hintViewWidth, hintViewTop + hintViewHeight);
+ }
+
+ /**
+ * 拍摄后的照片。需要进行裁剪。有些手机(比如三星)不会对照片数据进行旋转,而是将旋转角度写入EXIF信息当中,
+ * 所以需要做旋转处理。
+ *
+ * @param outputFile 写入照片的文件。
+ * @param data 原始照片数据。
+ * @param rotation 照片exif中的旋转角度。
+ *
+ * @return 裁剪好的bitmap。
+ */
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ private Bitmap crop(File outputFile, byte[] data, int rotation) {
+ try {
+ Rect previewFrame = cameraControl.getPreviewFrame();
+
+ if (maskView.getWidth() == 0 || maskView.getHeight() == 0
+ || previewFrame.width() == 0 || previewFrame.height() == 0) {
+ return null;
+ }
+
+ // BitmapRegionDecoder不会将整个图片加载到内存。
+ BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(data, 0, data.length, true);
+
+
+
+ int width = rotation % 180 == 0 ? decoder.getWidth() : decoder.getHeight();
+ int height = rotation % 180 == 0 ? decoder.getHeight() : decoder.getWidth();
+
+ Rect frameRect = maskView.getFrameRect();
+
+ int left = width * frameRect.left / maskView.getWidth();
+ int top = height * frameRect.top / maskView.getHeight();
+ int right = width * frameRect.right / maskView.getWidth();
+ int bottom = height * frameRect.bottom / maskView.getHeight();
+
+ // 高度大于图片
+ if (previewFrame.top < 0) {
+ // 宽度对齐。
+ int adjustedPreviewHeight = previewFrame.height() * getWidth() / previewFrame.width();
+ int topInFrame = ((adjustedPreviewHeight - frameRect.height()) / 2)
+ * getWidth() / previewFrame.width();
+ int bottomInFrame = ((adjustedPreviewHeight + frameRect.height()) / 2) * getWidth()
+ / previewFrame.width();
+
+ // 等比例投射到照片当中。
+ top = topInFrame * height / previewFrame.height();
+ bottom = bottomInFrame * height / previewFrame.height();
+ } else {
+ // 宽度大于图片
+ if (previewFrame.left < 0) {
+ // 高度对齐
+ int adjustedPreviewWidth = previewFrame.width() * getHeight() / previewFrame.height();
+ int leftInFrame = ((adjustedPreviewWidth - maskView.getFrameRect().width()) / 2) * getHeight()
+ / previewFrame.height();
+ int rightInFrame = ((adjustedPreviewWidth + maskView.getFrameRect().width()) / 2) * getHeight()
+ / previewFrame.height();
+
+ // 等比例投射到照片当中。
+ left = leftInFrame * width / previewFrame.width();
+ right = rightInFrame * width / previewFrame.width();
+ }
+ }
+
+ Rect region = new Rect();
+ region.left = left;
+ region.top = top;
+ region.right = right;
+ region.bottom = bottom;
+
+ // 90度或者270度旋转
+ if (rotation % 180 == 90) {
+ int x = decoder.getWidth() / 2;
+ int y = decoder.getHeight() / 2;
+
+ int rotatedWidth = region.height();
+ int rotated = region.width();
+
+ // 计算,裁剪框旋转后的坐标
+ region.left = x - rotatedWidth / 2;
+ region.top = y - rotated / 2;
+ region.right = x + rotatedWidth / 2;
+ region.bottom = y + rotated / 2;
+ region.sort();
+ }
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+
+ // 最大图片大小。
+ int maxPreviewImageSize = 2560;
+ int size = Math.min(decoder.getWidth(), decoder.getHeight());
+ size = Math.min(size, maxPreviewImageSize);
+
+ options.inSampleSize = ImageUtil.calculateInSampleSize(options, size, size);
+ options.inScaled = true;
+ options.inDensity = Math.max(options.outWidth, options.outHeight);
+ options.inTargetDensity = size;
+ options.inPreferredConfig = Bitmap.Config.RGB_565;
+ Bitmap bitmap = decoder.decodeRegion(region, options);
+
+ if (rotation != 0) {
+ // 只能是裁剪完之后再旋转了。有没有别的更好的方案呢?
+ Matrix matrix = new Matrix();
+ matrix.postRotate(rotation);
+ Bitmap rotatedBitmap = Bitmap.createBitmap(
+ bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
+ if (bitmap != rotatedBitmap) {
+ // 有时候 createBitmap会复用对象
+ bitmap.recycle();
+ }
+ bitmap = rotatedBitmap;
+ }
+
+ try {
+ if (!outputFile.exists()) {
+ outputFile.createNewFile();
+ }
+ FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fileOutputStream);
+ fileOutputStream.flush();
+ fileOutputStream.close();
+ return bitmap;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public void release() {
+ IDcardQualityProcess.getInstance().releaseModel();
+ }
+
+ private class CameraViewTakePictureCallback implements ICameraControl.OnTakePictureCallback {
+
+ private File file;
+ private OnTakePictureCallback callback;
+
+ @Override
+ public void onPictureTaken(final byte[] data) {
+ CameraThreadPool.execute(new Runnable() {
+ @Override
+ public void run() {
+ final int rotation = ImageUtil.getOrientation(data);
+ Bitmap bitmap = crop(file, data, rotation);
+ callback.onPictureTaken(bitmap);
+ }
+ });
+ }
+ }
+}
diff --git a/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/ICameraControl.java b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/ICameraControl.java
new file mode 100644
index 0000000..30d5bdb
--- /dev/null
+++ b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/ICameraControl.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.baidu.ocr.ui.camera;
+
+import android.graphics.Rect;
+
+import android.view.View;
+
+import androidx.annotation.IntDef;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Android 5.0 相机的API发生很大的变化。些类屏蔽掉了 api的变化。相机的操作和功能,抽象剥离出来。
+ */
+public interface ICameraControl {
+
+ /**
+ * 闪光灯关 {@link #setFlashMode(int)}
+ */
+ int FLASH_MODE_OFF = 0;
+ /**
+ * 闪光灯开 {@link #setFlashMode(int)}
+ */
+ int FLASH_MODE_TORCH = 1;
+ /**
+ * 闪光灯自动 {@link #setFlashMode(int)}
+ */
+ int FLASH_MODE_AUTO = 2;
+
+ @IntDef({FLASH_MODE_TORCH, FLASH_MODE_OFF, FLASH_MODE_AUTO})
+ @interface FlashMode {
+
+ }
+
+ /**
+ * 照相回调。
+ */
+ interface OnTakePictureCallback {
+ void onPictureTaken(byte[] data);
+ }
+
+ /**
+ * 设置本地质量控制回调,如果不设置则视为不扫描调用本地质量控制代码。
+ */
+ void setDetectCallback(OnDetectPictureCallback callback);
+
+ /**
+ * 预览回调
+ */
+ interface OnDetectPictureCallback {
+ int onDetect(byte[] data, int rotation);
+ }
+
+ /**
+ * 打开相机。
+ */
+ void start();
+
+ /**
+ * 关闭相机
+ */
+ void stop();
+
+ void pause();
+
+ void resume();
+
+ /**
+ * 相机对应的预览视图。
+ * @return 预览视图
+ */
+ View getDisplayView();
+
+ /**
+ * 看到的预览可能不是照片的全部。返回预览视图的全貌。
+ * @return 预览视图frame;
+ */
+ Rect getPreviewFrame();
+
+ /**
+ * 拍照。结果在回调中获取。
+ * @param callback 拍照结果回调
+ */
+ void takePicture(OnTakePictureCallback callback);
+
+ /**
+ * 设置权限回调,当手机没有拍照权限时,可在回调中获取。
+ * @param callback 权限回调
+ */
+ void setPermissionCallback(PermissionCallback callback);
+
+ /**
+ * 设置水平方向
+ * @param displayOrientation 参数值见 {@link com.baidu.ocr.ui.camera.CameraView.Orientation}
+ */
+ void setDisplayOrientation(@CameraView.Orientation int displayOrientation);
+
+ /**
+ * 获取到拍照权限时,调用些函数以继续。
+ */
+ void refreshPermission();
+
+ /**
+ * 获取已经扫描成功,处理中
+ */
+ AtomicBoolean getAbortingScan();
+
+ /**
+ * 设置闪光灯状态。
+ * @param flashMode {@link #FLASH_MODE_TORCH,#FLASH_MODE_OFF,#FLASH_MODE_AUTO}
+ */
+ void setFlashMode(@FlashMode int flashMode);
+
+ /**
+ * 获取当前闪光灯状态
+ * @return 当前闪光灯状态 参见 {@link #setFlashMode(int)}
+ */
+ @FlashMode
+ int getFlashMode();
+}
diff --git a/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/MaskView.java b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/MaskView.java
new file mode 100644
index 0000000..a0b1425
--- /dev/null
+++ b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/MaskView.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.baidu.ocr.ui.camera;
+
+import java.io.File;
+
+import com.baidu.ocr.ui.R;
+
+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.graphics.drawable.Drawable;
+import android.os.Build;
+
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RequiresApi;
+import androidx.core.content.res.ResourcesCompat;
+
+@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;
+ public static final int MASK_TYPE_PASSPORT = 21;
+
+ @IntDef({MASK_TYPE_NONE, MASK_TYPE_ID_CARD_FRONT, MASK_TYPE_ID_CARD_BACK, MASK_TYPE_BANK_CARD,
+ MASK_TYPE_PASSPORT})
+ @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 Rect framePassport = new Rect();
+
+ private Drawable locatorDrawable;
+
+ public Rect getFrameRect() {
+ if (maskType == MASK_TYPE_NONE) {
+ return new Rect(0, 0, getWidth(), getHeight());
+ } else if (maskType == MASK_TYPE_PASSPORT) {
+ return new Rect(framePassport);
+ } 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_PASSPORT:
+ locatorDrawable = ResourcesCompat.getDrawable(getResources(),
+ R.drawable.bd_ocr_passport_locator, null);
+ break;
+ case MASK_TYPE_BANK_CARD:
+ break;
+ case MASK_TYPE_NONE:
+ default:
+ break;
+ }
+ invalidate();
+ }
+
+ public int getMaskType() {
+ return maskType;
+ }
+
+ public void setOrientation(@CameraView.Orientation 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) {
+ if (maskType != MASK_TYPE_PASSPORT) {
+ 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;
+ } else {
+ float ratio = 0.9f;
+
+ int width = (int) (w * ratio);
+ int height = width * 330 / 470;
+
+ int left = (w - width) / 2;
+ int top = (h - height) / 2;
+ int right = width + left;
+ int bottom = height + top;
+
+ framePassport.left = left;
+ framePassport.top = top;
+ framePassport.right = right;
+ framePassport.bottom = bottom;
+ }
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ Rect frame = this.frame;
+ if (maskType == MASK_TYPE_PASSPORT) {
+ frame = framePassport;
+ }
+
+ 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));
+ } else if (maskType == MASK_TYPE_PASSPORT) {
+ locatorDrawable.setBounds(
+ (int) (left + 30f / 1006 * width),
+ (int) (top + (20f / 632) * height),
+ (int) (left + (303f / 1006) * width),
+ (int) (top + (416f / 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) {
+
+ }
+}
diff --git a/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/OCRCameraLayout.java b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/OCRCameraLayout.java
new file mode 100644
index 0000000..b9aa9e0
--- /dev/null
+++ b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/OCRCameraLayout.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.baidu.ocr.ui.camera;
+
+import com.baidu.ocr.ui.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+public class OCRCameraLayout extends FrameLayout {
+
+ public static int ORIENTATION_PORTRAIT = 0;
+ public static int ORIENTATION_HORIZONTAL = 1;
+
+ private int orientation = ORIENTATION_PORTRAIT;
+ private View contentView;
+ private View centerView;
+ private View leftDownView;
+ private View rightUpView;
+
+ private int contentViewId;
+ private int centerViewId;
+ private int leftDownViewId;
+ private int rightUpViewId;
+
+ public void setOrientation(int orientation) {
+ if (this.orientation == orientation) {
+ return;
+ }
+ this.orientation = orientation;
+ requestLayout();
+ }
+
+ public OCRCameraLayout(Context context) {
+ super(context);
+ }
+
+ public OCRCameraLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ parseAttrs(attrs);
+ }
+
+ public OCRCameraLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ parseAttrs(attrs);
+ }
+
+ {
+ setWillNotDraw(false);
+ }
+
+ private void parseAttrs(AttributeSet attrs) {
+ TypedArray a = getContext().getTheme().obtainStyledAttributes(
+ attrs,
+ R.styleable.OCRCameraLayout,
+ 0, 0);
+ try {
+ contentViewId = a.getResourceId(R.styleable.OCRCameraLayout_contentView, -1);
+ centerViewId = a.getResourceId(R.styleable.OCRCameraLayout_centerView, -1);
+ leftDownViewId = a.getResourceId(R.styleable.OCRCameraLayout_leftDownView, -1);
+ rightUpViewId = a.getResourceId(R.styleable.OCRCameraLayout_rightUpView, -1);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ contentView = findViewById(contentViewId);
+ if (centerViewId != -1) {
+ centerView = findViewById(centerViewId);
+ }
+ leftDownView = findViewById(leftDownViewId);
+ rightUpView = findViewById(rightUpViewId);
+ }
+
+ private Rect backgroundRect = new Rect();
+ private Paint paint = new Paint();
+
+ {
+ paint.setStyle(Paint.Style.FILL);
+ paint.setColor(Color.argb(83, 0, 0, 0));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int width = getWidth();
+ int height = getHeight();
+ int left;
+ int top;
+
+ ViewGroup.MarginLayoutParams leftDownViewLayoutParams = (MarginLayoutParams) leftDownView.getLayoutParams();
+ ViewGroup.MarginLayoutParams rightUpViewLayoutParams = (MarginLayoutParams) rightUpView.getLayoutParams();
+ if (r < b) {
+ int contentHeight = width * 4 / 3;
+ int heightLeft = height - contentHeight;
+ contentView.layout(l, t, r, contentHeight);
+
+ backgroundRect.left = 0;
+ backgroundRect.top = contentHeight;
+ backgroundRect.right = width;
+ backgroundRect.bottom = height;
+
+ // layout centerView;
+ if (centerView != null) {
+ left = (width - centerView.getMeasuredWidth()) / 2;
+ top = contentHeight + (heightLeft - centerView.getMeasuredHeight()) / 2;
+ centerView
+ .layout(left, top, left + centerView.getMeasuredWidth(), top + centerView.getMeasuredHeight());
+ }
+ // layout leftDownView
+
+ left = leftDownViewLayoutParams.leftMargin;
+ top = contentHeight + (heightLeft - leftDownView.getMeasuredHeight()) / 2;
+ leftDownView
+ .layout(left, top, left + leftDownView.getMeasuredWidth(), top + leftDownView.getMeasuredHeight());
+ // layout rightUpView
+ left = width - rightUpView.getMeasuredWidth() - rightUpViewLayoutParams.rightMargin;
+ top = contentHeight + (heightLeft - rightUpView.getMeasuredHeight()) / 2;
+ rightUpView.layout(left, top, left + rightUpView.getMeasuredWidth(), top + rightUpView.getMeasuredHeight());
+ } else {
+ int contentWidth = height * 4 / 3;
+ int widthLeft = width - contentWidth;
+ contentView.layout(l, t, contentWidth, height);
+
+ backgroundRect.left = contentWidth;
+ backgroundRect.top = 0;
+ backgroundRect.right = width;
+ backgroundRect.bottom = height;
+
+ // layout centerView
+ if (centerView != null) {
+ left = contentWidth + (widthLeft - centerView.getMeasuredWidth()) / 2;
+ top = (height - centerView.getMeasuredHeight()) / 2;
+ centerView
+ .layout(left, top, left + centerView.getMeasuredWidth(), top + centerView.getMeasuredHeight());
+ }
+ // layout leftDownView
+ left = contentWidth + (widthLeft - leftDownView.getMeasuredWidth()) / 2;
+ top = height - leftDownView.getMeasuredHeight() - leftDownViewLayoutParams.bottomMargin;
+ leftDownView
+ .layout(left, top, left + leftDownView.getMeasuredWidth(), top + leftDownView.getMeasuredHeight());
+ // layout rightUpView
+ left = contentWidth + (widthLeft - rightUpView.getMeasuredWidth()) / 2;
+
+ top = rightUpViewLayoutParams.topMargin;
+ rightUpView.layout(left, top, left + rightUpView.getMeasuredWidth(), top + rightUpView.getMeasuredHeight());
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ canvas.drawRect(backgroundRect, paint);
+ }
+}
diff --git a/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/OCRFrameLayout.java b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/OCRFrameLayout.java
new file mode 100644
index 0000000..b17d7d9
--- /dev/null
+++ b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/OCRFrameLayout.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.baidu.ocr.ui.camera;
+
+
+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));
+ }
+}
diff --git a/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/PermissionCallback.java b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/PermissionCallback.java
new file mode 100644
index 0000000..c45a837
--- /dev/null
+++ b/ocr_ui/src/main/java/com/baidu/ocr/ui/camera/PermissionCallback.java
@@ -0,0 +1,8 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.baidu.ocr.ui.camera;
+
+public interface PermissionCallback {
+ boolean onRequestPermission();
+}
diff --git a/ocr_ui/src/main/java/com/baidu/ocr/ui/crop/CropView.java b/ocr_ui/src/main/java/com/baidu/ocr/ui/crop/CropView.java
new file mode 100644
index 0000000..53dc75f
--- /dev/null
+++ b/ocr_ui/src/main/java/com/baidu/ocr/ui/crop/CropView.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.baidu.ocr.ui.crop;
+
+import java.io.IOException;
+
+import com.baidu.ocr.ui.util.ImageUtil;
+
+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;
+
+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) {
+ if (this.bitmap == null) {
+ return;
+ }
+ 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;
+ }
+}
diff --git a/ocr_ui/src/main/java/com/baidu/ocr/ui/crop/FrameOverlayView.java b/ocr_ui/src/main/java/com/baidu/ocr/ui/crop/FrameOverlayView.java
new file mode 100644
index 0000000..8d000a8
--- /dev/null
+++ b/ocr_ui/src/main/java/com/baidu/ocr/ui/crop/FrameOverlayView.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.baidu.ocr.ui.crop;
+
+
+import com.baidu.ocr.ui.util.DimensionUtil;
+
+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;
+
+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;
+ }
+}
diff --git a/ocr_ui/src/main/java/com/baidu/ocr/ui/util/DimensionUtil.java b/ocr_ui/src/main/java/com/baidu/ocr/ui/util/DimensionUtil.java
new file mode 100644
index 0000000..42fd59c
--- /dev/null
+++ b/ocr_ui/src/main/java/com/baidu/ocr/ui/util/DimensionUtil.java
@@ -0,0 +1,14 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.baidu.ocr.ui.util;
+
+import android.content.res.Resources;
+
+public class DimensionUtil {
+
+ public static int dpToPx(int dp) {
+ return (int) (dp * Resources.getSystem().getDisplayMetrics().density);
+ }
+
+}
diff --git a/ocr_ui/src/main/java/com/baidu/ocr/ui/util/ImageUtil.java b/ocr_ui/src/main/java/com/baidu/ocr/ui/util/ImageUtil.java
new file mode 100644
index 0000000..98d69d6
--- /dev/null
+++ b/ocr_ui/src/main/java/com/baidu/ocr/ui/util/ImageUtil.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2017 Baidu, Inc. All Rights Reserved.
+ */
+package com.baidu.ocr.ui.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;
+ }
+}
diff --git a/ocr_ui/src/main/jniLibs/arm64-v8a/libidcard_quality.1.1.1.so b/ocr_ui/src/main/jniLibs/arm64-v8a/libidcard_quality.1.1.1.so
new file mode 100644
index 0000000..9d96062
Binary files /dev/null and b/ocr_ui/src/main/jniLibs/arm64-v8a/libidcard_quality.1.1.1.so differ
diff --git a/ocr_ui/src/main/jniLibs/arm64-v8a/libidl_license.so b/ocr_ui/src/main/jniLibs/arm64-v8a/libidl_license.so
new file mode 100644
index 0000000..23739c7
Binary files /dev/null and b/ocr_ui/src/main/jniLibs/arm64-v8a/libidl_license.so differ
diff --git a/ocr_ui/src/main/jniLibs/armeabi-v7a/libidcard_quality.1.1.1.so b/ocr_ui/src/main/jniLibs/armeabi-v7a/libidcard_quality.1.1.1.so
new file mode 100644
index 0000000..8a2ad1d
Binary files /dev/null and b/ocr_ui/src/main/jniLibs/armeabi-v7a/libidcard_quality.1.1.1.so differ
diff --git a/ocr_ui/src/main/jniLibs/armeabi-v7a/libidl_license.so b/ocr_ui/src/main/jniLibs/armeabi-v7a/libidl_license.so
new file mode 100644
index 0000000..52d48e8
Binary files /dev/null and b/ocr_ui/src/main/jniLibs/armeabi-v7a/libidl_license.so differ
diff --git a/ocr_ui/src/main/jniLibs/armeabi/libidcard_quality.1.1.1.so b/ocr_ui/src/main/jniLibs/armeabi/libidcard_quality.1.1.1.so
new file mode 100644
index 0000000..a3147ca
Binary files /dev/null and b/ocr_ui/src/main/jniLibs/armeabi/libidcard_quality.1.1.1.so differ
diff --git a/ocr_ui/src/main/jniLibs/armeabi/libidl_license.so b/ocr_ui/src/main/jniLibs/armeabi/libidl_license.so
new file mode 100644
index 0000000..0305834
Binary files /dev/null and b/ocr_ui/src/main/jniLibs/armeabi/libidl_license.so differ
diff --git a/ocr_ui/src/main/jniLibs/x86/libidcard_quality.1.1.1.so b/ocr_ui/src/main/jniLibs/x86/libidcard_quality.1.1.1.so
new file mode 100644
index 0000000..6644d9a
Binary files /dev/null and b/ocr_ui/src/main/jniLibs/x86/libidcard_quality.1.1.1.so differ
diff --git a/ocr_ui/src/main/jniLibs/x86/libidl_license.so b/ocr_ui/src/main/jniLibs/x86/libidl_license.so
new file mode 100644
index 0000000..ac680a4
Binary files /dev/null and b/ocr_ui/src/main/jniLibs/x86/libidl_license.so differ
diff --git a/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_cancel.png b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_cancel.png
new file mode 100644
index 0000000..9a4c575
Binary files /dev/null and b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_cancel.png differ
diff --git a/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_close.png b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_close.png
new file mode 100644
index 0000000..080f00f
Binary files /dev/null and b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_close.png differ
diff --git a/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_confirm.png b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_confirm.png
new file mode 100644
index 0000000..e01cc7c
Binary files /dev/null and b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_confirm.png differ
diff --git a/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_gallery.png b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_gallery.png
new file mode 100644
index 0000000..3749366
Binary files /dev/null and b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_gallery.png differ
diff --git a/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_hint_align_bank_card.png b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_hint_align_bank_card.png
new file mode 100644
index 0000000..9a73728
Binary files /dev/null and b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_hint_align_bank_card.png differ
diff --git a/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_hint_align_id_card.png b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_hint_align_id_card.png
new file mode 100644
index 0000000..b90d6b4
Binary files /dev/null and b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_hint_align_id_card.png differ
diff --git a/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_hint_align_id_card_back.png b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_hint_align_id_card_back.png
new file mode 100644
index 0000000..9f4a642
Binary files /dev/null and b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_hint_align_id_card_back.png differ
diff --git a/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_id_card_locator_back.png b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_id_card_locator_back.png
new file mode 100644
index 0000000..a38c86e
Binary files /dev/null and b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_id_card_locator_back.png differ
diff --git a/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_id_card_locator_front.png b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_id_card_locator_front.png
new file mode 100644
index 0000000..255b665
Binary files /dev/null and b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_id_card_locator_front.png differ
diff --git a/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_light_off.png b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_light_off.png
new file mode 100644
index 0000000..c74408d
Binary files /dev/null and b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_light_off.png differ
diff --git a/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_light_on.png b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_light_on.png
new file mode 100644
index 0000000..9cbf8ba
Binary files /dev/null and b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_light_on.png differ
diff --git a/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_passport_locator.png b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_passport_locator.png
new file mode 100644
index 0000000..3f31101
Binary files /dev/null and b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_passport_locator.png differ
diff --git a/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_reset.png b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_reset.png
new file mode 100644
index 0000000..1be3678
Binary files /dev/null and b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_reset.png differ
diff --git a/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_rotate.png b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_rotate.png
new file mode 100644
index 0000000..9cb08a6
Binary files /dev/null and b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_rotate.png differ
diff --git a/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_round_corner.xml b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_round_corner.xml
new file mode 100644
index 0000000..74d6ca4
--- /dev/null
+++ b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_round_corner.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_take_photo_highlight.png b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_take_photo_highlight.png
new file mode 100644
index 0000000..d30c4ab
Binary files /dev/null and b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_take_photo_highlight.png differ
diff --git a/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_take_photo_normal.png b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_take_photo_normal.png
new file mode 100644
index 0000000..eb83dd0
Binary files /dev/null and b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_take_photo_normal.png differ
diff --git a/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_take_photo_selector.xml b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_take_photo_selector.xml
new file mode 100644
index 0000000..1230626
--- /dev/null
+++ b/ocr_ui/src/main/res/drawable-xhdpi/bd_ocr_take_photo_selector.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ocr_ui/src/main/res/layout/bd_ocr_activity_camera.xml b/ocr_ui/src/main/res/layout/bd_ocr_activity_camera.xml
new file mode 100644
index 0000000..9445247
--- /dev/null
+++ b/ocr_ui/src/main/res/layout/bd_ocr_activity_camera.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ocr_ui/src/main/res/layout/bd_ocr_confirm_result.xml b/ocr_ui/src/main/res/layout/bd_ocr_confirm_result.xml
new file mode 100644
index 0000000..0f18a36
--- /dev/null
+++ b/ocr_ui/src/main/res/layout/bd_ocr_confirm_result.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ocr_ui/src/main/res/layout/bd_ocr_crop.xml b/ocr_ui/src/main/res/layout/bd_ocr_crop.xml
new file mode 100644
index 0000000..d9c1542
--- /dev/null
+++ b/ocr_ui/src/main/res/layout/bd_ocr_crop.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ocr_ui/src/main/res/layout/bd_ocr_take_picture.xml b/ocr_ui/src/main/res/layout/bd_ocr_take_picture.xml
new file mode 100644
index 0000000..0f36324
--- /dev/null
+++ b/ocr_ui/src/main/res/layout/bd_ocr_take_picture.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ocr_ui/src/main/res/mipmap-hdpi/ic_launcher.png b/ocr_ui/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
Binary files /dev/null and b/ocr_ui/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/ocr_ui/src/main/res/mipmap-mdpi/ic_launcher.png b/ocr_ui/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
Binary files /dev/null and b/ocr_ui/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/ocr_ui/src/main/res/mipmap-xhdpi/ic_launcher.png b/ocr_ui/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
Binary files /dev/null and b/ocr_ui/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/ocr_ui/src/main/res/mipmap-xxhdpi/ic_launcher.png b/ocr_ui/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
Binary files /dev/null and b/ocr_ui/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/ocr_ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/ocr_ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..aee44e1
Binary files /dev/null and b/ocr_ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/ocr_ui/src/main/res/values/bd_ocr_dimensions.xml b/ocr_ui/src/main/res/values/bd_ocr_dimensions.xml
new file mode 100644
index 0000000..6d2ecdf
--- /dev/null
+++ b/ocr_ui/src/main/res/values/bd_ocr_dimensions.xml
@@ -0,0 +1,10 @@
+
+
+
+ 18dp
+ 18dp
+ 16dp
+ 16dp
+
diff --git a/ocr_ui/src/main/res/values/bd_ocr_widgets.xml b/ocr_ui/src/main/res/values/bd_ocr_widgets.xml
new file mode 100644
index 0000000..6df54d2
--- /dev/null
+++ b/ocr_ui/src/main/res/values/bd_ocr_widgets.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/ocr_ui/src/main/res/values/colors.xml b/ocr_ui/src/main/res/values/colors.xml
new file mode 100644
index 0000000..13a7544
--- /dev/null
+++ b/ocr_ui/src/main/res/values/colors.xml
@@ -0,0 +1,9 @@
+
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/ocr_ui/src/main/res/values/strings.xml b/ocr_ui/src/main/res/values/strings.xml
new file mode 100644
index 0000000..5f2896d
--- /dev/null
+++ b/ocr_ui/src/main/res/values/strings.xml
@@ -0,0 +1,7 @@
+
+
+ OCR_UI
+ 本功能需要相机权限!
+
diff --git a/ocr_ui/src/main/res/values/styles.xml b/ocr_ui/src/main/res/values/styles.xml
new file mode 100644
index 0000000..55a8ffb
--- /dev/null
+++ b/ocr_ui/src/main/res/values/styles.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+