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 @@ + + + + + + +