Repository: MrFuFuFu/RxFace Branch: master Commit: f8753b8d76d1 Files: 53 Total size: 114.0 KB Directory structure: gitextract_nz7e2i3v/ ├── .gitignore ├── .idea/ │ ├── .name │ ├── compiler.xml │ ├── copyright/ │ │ └── profiles_settings.xml │ ├── gradle.xml │ ├── misc.xml │ ├── modules.xml │ ├── runConfigurations.xml │ └── vcs.xml ├── Face.iml ├── README.md ├── RxFace.iml ├── app/ │ ├── .gitignore │ ├── app.iml │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── mrfu/ │ │ └── face/ │ │ └── ApplicationTest.java │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── mrfu/ │ │ │ └── rxface/ │ │ │ ├── AppApplication.java │ │ │ ├── BaseActivity.java │ │ │ ├── MainActivity.java │ │ │ ├── business/ │ │ │ │ ├── Constants.java │ │ │ │ └── DealData.java │ │ │ ├── loader/ │ │ │ │ ├── ExecutorManager.java │ │ │ │ ├── FaceApi.java │ │ │ │ ├── SchedulersCompat.java │ │ │ │ ├── WebServiceException.java │ │ │ │ └── custom/ │ │ │ │ ├── AsciiTypeString.java │ │ │ │ ├── CustomMultipartTypedOutput.java │ │ │ │ └── CustomTypedByteArray.java │ │ │ └── models/ │ │ │ ├── BaseResponse.java │ │ │ ├── FaceResponse.java │ │ │ └── NeedDataEntity.java │ │ └── res/ │ │ ├── layout/ │ │ │ ├── activity_main.xml │ │ │ └── content_main.xml │ │ ├── menu/ │ │ │ └── menu_main.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── values-v21/ │ │ │ └── styles.xml │ │ └── values-w820dp/ │ │ └── dimens.xml │ └── test/ │ └── java/ │ └── mrfu/ │ └── face/ │ └── ExampleUnitTest.java ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── sdk_unuse/ │ ├── HttpRequests.java │ ├── MainActivity.java │ └── PostParameters.java └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures ================================================ FILE: .idea/.name ================================================ RxFace ================================================ FILE: .idea/compiler.xml ================================================ ================================================ FILE: .idea/copyright/profiles_settings.xml ================================================ ================================================ FILE: .idea/gradle.xml ================================================ ================================================ FILE: .idea/misc.xml ================================================ Android API 21 Platform ================================================ FILE: .idea/modules.xml ================================================ ================================================ FILE: .idea/runConfigurations.xml ================================================ ================================================ FILE: .idea/vcs.xml ================================================ ================================================ FILE: Face.iml ================================================ ================================================ FILE: README.md ================================================ RxFace ===================== 用 RxJava, Retrofit, Okhttp 处理人脸识别的简单用例 ## Overview 这是一个人脸识别的简单 Demo, 使用了 [FacePlusPlus](http://www.faceplusplus.com.cn/) 的接口。他们的`/detection/detect` 人脸识别接口可以使用普通的 `get` 也可以用 `post` 传递图片二进制流的形式。其中 `post` 的时候遇到了相当多的坑,下面会提。 该 demo 的网络请求库使用了 [Retrofit](https://github.com/square/retrofit) 并集成了 [OkHttp](https://github.com/square/okhttp),使用 [RxJava](https://github.com/ReactiveX/RxJava) 进行封装,方便以流的形式处理网络回调以及图片处理,View 的注入框架用了 [ButterKnife](https://github.com/JakeWharton/butterknife),图片加载使用 [Glide](https://github.com/bumptech/glide)。 ## Versions ### v1.1 1. 增加了 compose 复用 work thread 处理数据,然后在 main thread 处理结果的逻辑:你可以在这片文章看到更多:[RxWeekend——RxJava周末狂欢](http://www.jianshu.com/p/ce228f517586) 2. mSubscription.unsubscribe(); 3. 增加了Service 返回错误情况的处理 ### v1.0 ## Main difficulties 当直接使用 `get` 通过传图片 Url 拿到人脸识别数据的话是相当简单的,如下请求链接只要使用 `Retrofit` 的 `get` 请求的 `@QueryMap` 传递参数即可: [Get数据Demo](http://apicn.faceplusplus.com/v2/detection/detect?api_key=7cd1e10dc037bbe9e6db2813d6127475&api_secret=gruCjvStG159LCJutENBt6yzeLK_5ggX&url=http://imglife.gmw.cn/attachement/jpg/site2/20111014/002564a5d7d21002188831.jpg)。 主要存在的困难点是,当获取本地图片,再使用 `post` 传二进制图片数据时,`post` 要使用 `MultipartTypedOutput`,可参考 [stackoverflow](http://stackoverflow.com/questions/25249042/retrofit-multiple-images-attached-in-one-multipart-request/25260556#25260556) 的回答。然而,这样并没有结束,根据 FacePlusPlus 提供的 SDK Sample 里的 Httpurlconnection 的得到的 `post` 请求头是这样的: `[Content-Disposition: form-data; name="api_key", Content-Type: text/plain; charset=US-ASCII, Content-Transfer-Encoding: 8bit]` `[Content-Disposition: form-data; name="img"; filename="NoName", Content-Type: application/octet-stream, Content-Transfer-Encoding: binary]` 而使用 `Retrofit` 默认实现的话,我们这样来实现: ```java public static MultipartTypedOutput mulipartData(Bitmap bitmap, String boundary){ byte[] data = getBitmapByte(bitmap); MultipartTypedOutput multipartTypedOutput = new MultipartTypedOutput(); multipartTypedOutput.addPart("api_key", new TypedString(Constants.API_KEY)); multipartTypedOutput.addPart("api_secret", new TypedString(Constants.API_SECRET)); multipartTypedOutput.addPart("img", new TypedByteArray("application/octet-stream", data)); return multipartTypedOutput; } ``` 根据 Sample 的请求头,`RestAdapter` 的请求头参数我们这样设置来: ```java private RequestInterceptor mRequestInterceptor = new RequestInterceptor() { @Override public void intercept(RequestFacade request) { request.addHeader("connection", "keep-alive"); request.addHeader("Content-Type", "multipart/form-data; boundary="+ getBoundary() + "; charset=UTF-8"); } }; ``` 但是!!!它得到的String参数的头是这样的,这里没有贴出其他的差异, ```java Content-Disposition: form-data; name="api_key" Content-Type: text/plain; charset=UTF-8 Content-Length: 32 Content-Transfer-Encoding: 8bit ``` 所以需要重写三个类: `MultipartTypedOutput` 为 final 类,所以重写为 `CustomMultipartTypedOutput`,并使其构造函数,增加 boundary 的设置; `TypedString `默认的编码格式是`UTF-8`,所以重写为 `AsciiTypeString`类,使其编码格式改为 `US-ASCII`; `TypedByteArray` 默认的的 `fileName()` 方法返回的是 null,而当传图片数据时需要 fileName 为 "NoName",所以重写为 `CustomTypedByteArray` 类,设置其 fileName 为 "NoName"。 同时需要注意的是在设置 `RestAdapter` 的 header 时,其 boundary 一定要和 `CustomMultipartTypedOutput` 的 boundary 相同,否则服务端无法匹配的!(这个地方,一时没注意,被整了一个多小时才发现!!) 最后 body 的传参,这样来得到: ```java public static CustomMultipartTypedOutput mulipartData(Bitmap bitmap, String boundary){ byte[] data = getBitmapByte(bitmap); CustomMultipartTypedOutput multipartTypedOutput = new CustomMultipartTypedOutput(boundary); multipartTypedOutput.addPart("api_key", "8bit", new AsciiTypeString(Constants.API_KEY)); multipartTypedOutput.addPart("api_secret", "8bit", new AsciiTypeString(Constants.API_SECRET)); multipartTypedOutput.addPart("img", new CustomTypedByteArray("application/octet-stream", data)); return multipartTypedOutput; } ``` ## Preview ![image_screen](https://raw.githubusercontent.com/MrFuFuFu/RxFace/master/images/image_screen.png) ![movie_screen](https://raw.githubusercontent.com/MrFuFuFu/RxFace/master/images/movie_screen.gif) ## More about me * [MrFu-傅圆的个人博客](http://mrfu.me/) ## Acknowledgments * [Glide](https://github.com/bumptech/glide) -Glide * [Retrofit](https://github.com/square/retrofit) - Retrofit * [OkHttp](https://github.com/square/okhttp) - OkHttp * [RxJava](https://github.com/ReactiveX/RxJava) - RxJava * [ButterKnife](https://github.com/JakeWharton/butterknife) - ButterKnife License ============ Copyright 2015 MrFu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/MrFuFuFu/rxface/trend.png)](https://bitdeli.com/free "Bitdeli Badge") ================================================ FILE: RxFace.iml ================================================ ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/app.iml ================================================ ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' android { compileSdkVersion 23 buildToolsVersion "23.0.2" defaultConfig { applicationId "mrfu.face" minSdkVersion 14 targetSdkVersion 23 versionCode 3 versionName "1.1" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } lintOptions { abortOnError false } // compileOptions { // sourceCompatibility JavaVersion.VERSION_1_8 // targetCompatibility JavaVersion.VERSION_1_8 // } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.android.support:design:23.1.1' compile 'com.squareup.retrofit:retrofit:1.9.0' compile 'io.reactivex:rxjava:1.0.14' compile 'io.reactivex:rxandroid:1.0.1' compile 'com.squareup.okhttp:okhttp-urlconnection:2.0.0' compile 'com.squareup.okhttp:okhttp:2.0.0' compile 'com.jakewharton:butterknife:7.0.1' compile 'com.squareup.okhttp:okhttp-urlconnection:2.0.0' compile 'com.github.bumptech.glide:glide:3.6.0' } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/MrFu/Desktop/Android/adt-bundle-mac-x86_64-20131030/sdk/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 *; #} ================================================ FILE: app/src/androidTest/java/mrfu/face/ApplicationTest.java ================================================ package mrfu.face; import android.app.Application; import android.test.ApplicationTestCase; /** * Testing Fundamentals */ public class ApplicationTest extends ApplicationTestCase { public ApplicationTest() { super(Application.class); } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/mrfu/rxface/AppApplication.java ================================================ package mrfu.rxface; import android.app.Application; import android.content.Context; /** * Created by MrFu on 15/12/15. */ public class AppApplication extends Application { private static Context context; public static Context getContext() { return context; } @Override public void onCreate() { super.onCreate(); context = getApplicationContext(); } } ================================================ FILE: app/src/main/java/mrfu/rxface/BaseActivity.java ================================================ package mrfu.rxface; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import butterknife.ButterKnife; import rx.Subscription; import rx.subscriptions.Subscriptions; /** * Created by MrFu on 16/1/10. */ public class BaseActivity extends AppCompatActivity { protected Subscription mSubscription = Subscriptions.empty(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public void setContentView(int layoutResID) { super.setContentView(layoutResID); ButterKnife.bind(BaseActivity.this); } @Override protected void onDestroy() { super.onDestroy(); ButterKnife.unbind(BaseActivity.this); if (mSubscription != null && !mSubscription.isUnsubscribed()) mSubscription.unsubscribe(); } } ================================================ FILE: app/src/main/java/mrfu/rxface/MainActivity.java ================================================ package mrfu.rxface; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; import android.os.Bundle; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; import com.bumptech.glide.Glide; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutionException; import butterknife.Bind; import butterknife.OnClick; import mrfu.rxface.business.Constants; import mrfu.rxface.business.DealData; import mrfu.rxface.loader.FaceApi; import mrfu.rxface.models.FaceResponse; import mrfu.rxface.models.NeedDataEntity; import rx.Observable; import rx.Observer; import rx.android.schedulers.AndroidSchedulers; import rx.functions.Func1; import rx.schedulers.Schedulers; public class MainActivity extends BaseActivity { @Bind(R.id.iv_face_get) ImageView iv_face_get; @Bind(R.id.iv_face_post) ImageView iv_face_post; @Bind(R.id.btn_recongize_get) Button btn_recongize_get; @Bind(R.id.btn_recongize_post) Button btn_recongize_post; @Bind(R.id.tv_age_get) TextView tv_age_get; @Bind(R.id.tv_age_post) TextView tv_age_post; /** * 0:GET button * 1:POST button */ private int type = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); Glide.with(this).load(Constants.IMAGE_URL).fitCenter().into(iv_face_get); iv_face_post.setImageResource(R.drawable.jobs); } @OnClick(R.id.btn_recongize_get) public void btn_recongize_get(View view){ type = 0; Map options = new HashMap<>(); options.put("api_key", Constants.API_KEY); options.put("api_secret", Constants.API_SECRET); options.put("url", Constants.IMAGE_URL); mSubscription = FaceApi.getIns()//api_key={apiKey}&api_secret={apiSecret}&url={imgUrl} .getDataUrlGet(options) .observeOn(Schedulers.newThread()) .flatMap(new Func1>() { @Override public Observable call(FaceResponse faceResponse) { Bitmap bitmap = null; try { //java.lang.IllegalArgumentException: YOu must call this method on a background thread bitmap = Glide.with(MainActivity.this).load(Constants.IMAGE_URL).asBitmap().into(-1, -1).get(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } NeedDataEntity entity = new NeedDataEntity(); entity.bitmap = DealData.drawLineGetBitmap(faceResponse, bitmap); entity.displayStr = DealData.getDisplayInfo(faceResponse); return Observable.just(entity); } }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(setBitmapDataObserver); } @OnClick(R.id.btn_recongize_post) public void btn_recongize_post(View view){ type = 1; BitmapDrawable mDrawable = (BitmapDrawable) iv_face_post.getDrawable(); final Bitmap bitmap = mDrawable.getBitmap(); mSubscription = FaceApi.getIns() .getDataPost(DealData.mulipartData(bitmap, FaceApi.getIns().getBoundary())) .flatMap(new Func1>() { @Override public Observable call(FaceResponse faceResponse) { NeedDataEntity entity = new NeedDataEntity(); entity.bitmap = DealData.drawLineGetBitmap(faceResponse, bitmap); entity.displayStr = DealData.getDisplayInfo(faceResponse); return Observable.just(entity); } }) .subscribe(setBitmapDataObserver); } private Observer setBitmapDataObserver = new Observer() { @Override public void onNext(final NeedDataEntity needDataEntity) { if (needDataEntity == null){ return; } switch (type){ case 0: if (!TextUtils.isEmpty(needDataEntity.displayStr)){ tv_age_get.setText(needDataEntity.displayStr); } if (needDataEntity.bitmap != null){ iv_face_get.setImageBitmap(needDataEntity.bitmap); } break; case 1: if (!TextUtils.isEmpty(needDataEntity.displayStr)){ tv_age_post.setText(needDataEntity.displayStr); } if (needDataEntity.bitmap != null){ iv_face_post.setImageBitmap(needDataEntity.bitmap); } break; default: break; } } @Override public void onCompleted() { Log.i("MrFu", "onCompleted"); } @Override public void onError(final Throwable error) { error.printStackTrace(); } }; @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { Uri uri = Uri.parse("https://github.com/MrFuFuFu/RxFace"); Intent i = new Intent(Intent.ACTION_VIEW, uri); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(i); return true; } return super.onOptionsItemSelected(item); } } ================================================ FILE: app/src/main/java/mrfu/rxface/business/Constants.java ================================================ package mrfu.rxface.business; /** * Created by MrFu on 15/12/15. */ public class Constants { // public static final String FACE_SERVER_IP = "https://apicn.faceplusplus.com/v2"; public static final String FACE_SERVER_IP = "http://apicn.faceplusplus.com/v2"; public static final String API_KEY = "7cd1e10dc037bbe9e6db2813d6127475"; public static final String API_SECRET = "gruCjvStG159LCJutENBt6yzeLK_5ggX"; public static final String IMAGE_URL = "http://www.ipmm.cn/UploadImage/20130502/2013050209272791.jpg"; public static final int TIME_OUT = 30 * 1000; } ================================================ FILE: app/src/main/java/mrfu/rxface/business/DealData.java ================================================ package mrfu.rxface.business; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import java.io.ByteArrayOutputStream; import mrfu.rxface.loader.custom.AsciiTypeString; import mrfu.rxface.loader.custom.CustomMultipartTypedOutput; import mrfu.rxface.loader.custom.CustomTypedByteArray; import mrfu.rxface.models.FaceResponse; /** * Created by MrFu on 15/12/15. */ public class DealData { /** * 设置参数 * 使用 MultipartTypedOutput, 而不使用 Retrofit @Multipart 的 @Part 和 @PartMap 的形式 * http://stackoverflow.com/questions/25249042/retrofit-multiple-images-attached-in-one-multipart-request/25260556#25260556 * * @see CustomMultipartTypedOutput 重写 MultipartTypedOutput 使之接受 boundary 参数 * @see AsciiTypeString , 重写 TypedByteArray, 使其编码格式为 US-ASCII * @see CustomTypedByteArray , 重写 TypedByteArray 设置其 fileName 为 "NoName", * 以上参数格式和参数类型都必须指定,否则会返回对应的错误 * http://www.faceplusplus.com.cn/detection_detect/ * @param bitmap need upload image * @param boundary must same with http header boundary * @return http post body param */ public static CustomMultipartTypedOutput mulipartData(Bitmap bitmap, String boundary){ byte[] data = getBitmapByte(bitmap); CustomMultipartTypedOutput multipartTypedOutput = new CustomMultipartTypedOutput(boundary); multipartTypedOutput.addPart("api_key", "8bit", new AsciiTypeString(Constants.API_KEY)); multipartTypedOutput.addPart("api_secret", "8bit", new AsciiTypeString(Constants.API_SECRET)); multipartTypedOutput.addPart("img", new CustomTypedByteArray("application/octet-stream", data)); return multipartTypedOutput; } /** * convert bitmap to byte[] * @param bitmap need convert bitmap * @return byte[] */ private static byte[] getBitmapByte(Bitmap bitmap){ ByteArrayOutputStream stream = new ByteArrayOutputStream(); float scale = Math.min(1, Math.min(600f / bitmap.getWidth(), 600f / bitmap.getHeight())); Matrix matrix = new Matrix(); matrix.postScale(scale, scale); Bitmap imgSmall = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false); imgSmall.compress(Bitmap.CompressFormat.JPEG, 100, stream); return stream.toByteArray(); } /** * draw to include face bitmap * @param entity data * @param viewBitmap display bitmap * @return face bitmap */ public static Bitmap drawLineGetBitmap(FaceResponse entity, Bitmap viewBitmap){ //use the red paint Paint paint = new Paint(); paint.setColor(Color.RED); paint.setStrokeWidth(Math.max(viewBitmap.getWidth(), viewBitmap.getHeight()) / 100f); //create a new canvas Bitmap bitmap = Bitmap.createBitmap(viewBitmap.getWidth(), viewBitmap.getHeight(), viewBitmap.getConfig()); Canvas canvas = new Canvas(bitmap); canvas.drawBitmap(viewBitmap, new Matrix(), null); try { //find out all faces final int count = entity.face.size(); for (int i = 0; i < count; ++i) { float x, y, w, h; //get the center point x = (float) entity.face.get(i).position.center.x; y = (float) entity.face.get(i).position.center.y; //get face size w = (float) entity.face.get(i).position.width; h = (float) entity.face.get(i).position.height; //change percent value to the real size x = x / 100 * viewBitmap.getWidth(); w = w / 100 * viewBitmap.getWidth() * 0.7f; y = y / 100 * viewBitmap.getHeight(); h = h / 100 * viewBitmap.getHeight() * 0.7f; //draw the box to mark it out canvas.drawLine(x - w, y - h, x - w, y + h, paint); canvas.drawLine(x - w, y - h, x + w, y - h, paint); canvas.drawLine(x + w, y + h, x - w, y + h, paint); canvas.drawLine(x + w, y + h, x + w, y - h, paint); } //save new image viewBitmap = bitmap; return viewBitmap; }catch (Exception e){ e.printStackTrace(); } return null; } public static String getDisplayInfo(FaceResponse entity){ if (entity == null || entity.face == null || entity.face.size() == 0 || entity.face.get(0).attribute == null){ return ""; } int age = 0; if (entity.face.get(0).attribute.age != null){ age = entity.face.get(0).attribute.age.value; } String gender = ""; if (entity.face.get(0).attribute.gender != null){ gender = "male".equalsIgnoreCase(entity.face.get(0).attribute.gender.value) ? "男" : "女"; } return "年龄约 " + age + " 性别为 " + gender; } } ================================================ FILE: app/src/main/java/mrfu/rxface/loader/ExecutorManager.java ================================================ package mrfu.rxface.loader; import android.os.Build; import android.support.annotation.NonNull; import java.io.File; import java.io.FileFilter; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** * Created by Joker on 2015/8/24. */ public class ExecutorManager { public static final int DEVICE_INFO_UNKNOWN = 0; public static ExecutorService eventExecutor; //private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); private static final int CPU_COUNT = ExecutorManager.getCountOfCPU(); private static final int CORE_POOL_SIZE = CPU_COUNT + 1; private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; private static final int KEEP_ALIVE = 1; private static final BlockingQueue eventPoolWaitQueue = new LinkedBlockingQueue<>(128); private static final ThreadFactory eventThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); public Thread newThread(@NonNull Runnable r) { return new Thread(r, "eventAsyncAndBackground #" + mCount.getAndIncrement()); } }; private static final RejectedExecutionHandler eventHandler = new ThreadPoolExecutor.CallerRunsPolicy(); static { eventExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, eventPoolWaitQueue, eventThreadFactory, eventHandler); } /** * Linux中的设备都是以文件的形式存在,CPU也不例外,因此CPU的文件个数就等价与核数。 * Android的CPU 设备文件位于/sys/devices/system/cpu/目录,文件名的的格式为cpu\d+。 * * 引用:http://www.jianshu.com/p/f7add443cd32#,感谢 liangfeizc :) * https://github.com/facebook/device-year-class */ public static int getCountOfCPU() { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) { return 1; } int count; try { count = new File("/sys/devices/system/cpu/").listFiles(CPU_FILTER).length; } catch (SecurityException | NullPointerException e) { count = DEVICE_INFO_UNKNOWN; } return count; } private static final FileFilter CPU_FILTER = new FileFilter() { @Override public boolean accept(File pathname) { String path = pathname.getName(); if (path.startsWith("cpu")) { for (int i = 3; i < path.length(); i++) { if (path.charAt(i) < '0' || path.charAt(i) > '9') { return false; } } return true; } return false; } }; } ================================================ FILE: app/src/main/java/mrfu/rxface/loader/FaceApi.java ================================================ package mrfu.rxface.loader; import com.squareup.okhttp.OkHttpClient; import java.util.Map; import java.util.Random; import java.util.concurrent.TimeUnit; import mrfu.rxface.BuildConfig; import mrfu.rxface.business.Constants; import mrfu.rxface.loader.custom.CustomMultipartTypedOutput; import mrfu.rxface.models.FaceResponse; import retrofit.RequestInterceptor; import retrofit.RestAdapter; import retrofit.client.OkClient; import retrofit.http.Body; import retrofit.http.GET; import retrofit.http.POST; import retrofit.http.QueryMap; import rx.Observable; import rx.functions.Func1; /** * face api * Created by MrFu on 15/12/15. */ public class FaceApi { private static String mBoundry; private final static int boundaryLength = 32; private final static String boundaryAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"; public static FaceApi instance; public static FaceApi getIns() { if (null == instance) { synchronized (FaceApi.class) { if (null == instance) { instance = new FaceApi(); } } } return instance; } private final MrFuService mWebService; public FaceApi() { RestAdapter restAdapter = new RestAdapter.Builder() .setEndpoint(Constants.FACE_SERVER_IP) .setClient(new OkClient(new OkHttpClient())) .setLogLevel(BuildConfig.DEBUG ? RestAdapter.LogLevel.FULL : RestAdapter.LogLevel.NONE) .setRequestInterceptor(mRequestInterceptor) .build(); mWebService = restAdapter.create(MrFuService.class); mBoundry = setBoundary(); } private RequestInterceptor mRequestInterceptor = new RequestInterceptor() { @Override public void intercept(RequestFacade request) { request.addHeader("connection", "keep-alive"); request.addHeader("Content-Type", "multipart/form-data; boundary="+ getBoundary() + "; charset=UTF-8"); } }; public String getBoundary(){ return mBoundry; } /** * 设置 Content-Type 的 boundary,这里有个强坑: * header 的 boundary 和 CustomMultipartTypedOutput 的 boundary 必须相同!! * @return mBoundry */ private static String setBoundary() { StringBuilder sb = new StringBuilder(); Random random = new Random(); for (int i = 0; i < boundaryLength; ++i) sb.append(boundaryAlphabet.charAt(random.nextInt(boundaryAlphabet.length()))); return sb.toString(); } public interface MrFuService { @POST("/detection/detect") Observable uploadImagePost( @Body CustomMultipartTypedOutput listMultipartOutput ); //http://apicn.faceplusplus.com/v2/detection/detect?api_key=7cd1e10dc037bbe9e6db2813d6127475&api_secret=gruCjvStG159LCJutENBt6yzeLK_5ggX&url=http://imglife.gmw.cn/attachement/jpg/site2/20111014/002564a5d7d21002188831.jpg @GET("/detection/detect") Observable uploadImageUrlGet( @QueryMap Map options ); } public Observable getDataPost(CustomMultipartTypedOutput listMultipartOutput) { return mWebService.uploadImagePost(listMultipartOutput) .timeout(Constants.TIME_OUT, TimeUnit.MILLISECONDS) .concatMap(new Func1>() { @Override public Observable call(FaceResponse faceResponse) { return faceResponse.filterWebServiceErrors(); } }).compose(SchedulersCompat.applyExecutorSchedulers()); } public Observable getDataUrlGet(Map options) { return mWebService.uploadImageUrlGet(options) .timeout(Constants.TIME_OUT, TimeUnit.MILLISECONDS) .concatMap(new Func1>() { @Override public Observable call(FaceResponse faceResponse) { return faceResponse.filterWebServiceErrors(); } }).compose(SchedulersCompat.applyExecutorSchedulers());//http://www.jianshu.com/p/e9e03194199e } } ================================================ FILE: app/src/main/java/mrfu/rxface/loader/SchedulersCompat.java ================================================ package mrfu.rxface.loader; import rx.Observable; import rx.android.schedulers.AndroidSchedulers; import rx.schedulers.Schedulers; /** * 小鄧子:【译】避免打断链式结构:使用.compose( )操作符 http://www.jianshu.com/p/e9e03194199e * Created by Joker on 2015/8/10. */ public class SchedulersCompat { private static final Observable.Transformer computationTransformer = new Observable.Transformer() { @Override public Object call(Object observable) { return ((Observable) observable).subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()); } }; private static final Observable.Transformer ioTransformer = new Observable.Transformer() { @Override public Object call(Object observable) { return ((Observable) observable).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); } }; private static final Observable.Transformer newTransformer = new Observable.Transformer() { @Override public Object call(Object observable) { return ((Observable) observable).subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()); } }; private static final Observable.Transformer trampolineTransformer = new Observable.Transformer() { @Override public Object call(Object observable) { return ((Observable) observable).subscribeOn(Schedulers.trampoline()) .observeOn(AndroidSchedulers.mainThread()); } }; private static final Observable.Transformer executorTransformer = new Observable.Transformer() { @Override public Object call(Object observable) { return ((Observable) observable).subscribeOn(Schedulers.from(ExecutorManager.eventExecutor)) .observeOn(AndroidSchedulers.mainThread()); } }; /** * Don't break the chain: use RxJava's compose() operator */ public static Observable.Transformer applyComputationSchedulers() { return (Observable.Transformer) computationTransformer; } public static Observable.Transformer applyIoSchedulers() { return (Observable.Transformer) ioTransformer; } public static Observable.Transformer applyNewSchedulers() { return (Observable.Transformer) newTransformer; } public static Observable.Transformer applyTrampolineSchedulers() { return (Observable.Transformer) trampolineTransformer; } public static Observable.Transformer applyExecutorSchedulers() { return (Observable.Transformer) executorTransformer; } } ================================================ FILE: app/src/main/java/mrfu/rxface/loader/WebServiceException.java ================================================ package mrfu.rxface.loader; /** * Created by MrFu on 16/1/10. */ public class WebServiceException extends Exception { public WebServiceException(String detailMessage) { super(detailMessage); } } ================================================ FILE: app/src/main/java/mrfu/rxface/loader/custom/AsciiTypeString.java ================================================ package mrfu.rxface.loader.custom; import java.io.UnsupportedEncodingException; import retrofit.mime.TypedByteArray; /** * 重写 TypedByteArray, 使其编码格式为 US-ASCII * Created by MrFu on 15/12/16. */ public class AsciiTypeString extends TypedByteArray { public AsciiTypeString(String string) { super("text/plain; charset=US-ASCII", convertToBytes(string)); } private static byte[] convertToBytes(String string) { try { return string.getBytes("US-ASCII"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } @Override public String toString() { try { return "TypedString[" + new String(getBytes(), "US-ASCII") + "]"; } catch (UnsupportedEncodingException e) { throw new AssertionError("Must be able to decode US-ASCII"); } } } ================================================ FILE: app/src/main/java/mrfu/rxface/loader/custom/CustomMultipartTypedOutput.java ================================================ package mrfu.rxface.loader.custom; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.UUID; import retrofit.mime.TypedOutput; /** * 重写 MultipartTypedOutput 使之接受 boundary 参数 * Created by MrFu on 15/12/16. */ public class CustomMultipartTypedOutput implements TypedOutput { public static final String DEFAULT_TRANSFER_ENCODING = "binary"; private static final class MimePart { private final TypedOutput body; private final String name; private final String transferEncoding; private final boolean isFirst; private final String boundary; private byte[] partBoundary; private byte[] partHeader; private boolean isBuilt; public MimePart(String name, String transferEncoding, TypedOutput body, String boundary, boolean isFirst) { this.name = name; this.transferEncoding = transferEncoding; this.body = body; this.isFirst = isFirst; this.boundary = boundary; } public void writeTo(OutputStream out) throws IOException { build(); out.write(partBoundary); out.write(partHeader); body.writeTo(out); } public long size() { build(); if (body.length() > -1) { return body.length() + partBoundary.length + partHeader.length; } else { return -1; } } private void build() { if (isBuilt) return; partBoundary = buildBoundary(boundary, isFirst, false); partHeader = buildHeader(name, transferEncoding, body); isBuilt = true; } } private final List mimeParts = new LinkedList(); private final byte[] footer; private final String boundary; private long length; public CustomMultipartTypedOutput() { this(UUID.randomUUID().toString()); } public CustomMultipartTypedOutput(String boundary) { this.boundary = boundary; footer = buildBoundary(boundary, false, true); length = footer.length; } List getParts() throws IOException { List parts = new ArrayList(mimeParts.size()); for (MimePart part : mimeParts) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); part.writeTo(bos); parts.add(bos.toByteArray()); } return parts; } public void addPart(String name, TypedOutput body) { addPart(name, DEFAULT_TRANSFER_ENCODING, body); } public void addPart(String name, String transferEncoding, TypedOutput body) { if (name == null) { throw new NullPointerException("Part name must not be null."); } if (transferEncoding == null) { throw new NullPointerException("Transfer encoding must not be null."); } if (body == null) { throw new NullPointerException("Part body must not be null."); } MimePart part = new MimePart(name, transferEncoding, body, boundary, mimeParts.isEmpty()); mimeParts.add(part); long size = part.size(); if (size == -1) { length = -1; } else if (length != -1) { length += size; } } public int getPartCount() { return mimeParts.size(); } @Override public String fileName() { return null; } @Override public String mimeType() { return "multipart/form-data; boundary=" + boundary; } @Override public long length() { return length; } @Override public void writeTo(OutputStream out) throws IOException { for (MimePart part : mimeParts) { part.writeTo(out); } out.write(footer); } private static byte[] buildBoundary(String boundary, boolean first, boolean last) { try { // Pre-size for the last boundary, the worst case scenario. StringBuilder sb = new StringBuilder(boundary.length() + 8); if (!first) { sb.append("\r\n"); } sb.append("--"); sb.append(boundary); if (last) { sb.append("--"); } sb.append("\r\n"); return sb.toString().getBytes("UTF-8"); } catch (IOException ex) { throw new RuntimeException("Unable to write multipart boundary", ex); } } private static byte[] buildHeader(String name, String transferEncoding, TypedOutput value) { try { // Initial size estimate based on always-present strings and conservative value lengths. StringBuilder headers = new StringBuilder(128); headers.append("Content-Disposition: form-data; name=\""); headers.append(name); String fileName = value.fileName(); if (fileName != null) { headers.append("\"; filename=\""); headers.append(fileName); } headers.append("\"\r\nContent-Type: "); headers.append(value.mimeType()); long length = value.length(); if (length != -1) { headers.append("\r\nContent-Length: ").append(length); } headers.append("\r\nContent-Transfer-Encoding: "); headers.append(transferEncoding); headers.append("\r\n\r\n"); return headers.toString().getBytes("UTF-8"); } catch (IOException ex) { throw new RuntimeException("Unable to write multipart header", ex); } } } ================================================ FILE: app/src/main/java/mrfu/rxface/loader/custom/CustomTypedByteArray.java ================================================ package mrfu.rxface.loader.custom; import retrofit.mime.TypedByteArray; /** * 重写 TypedByteArray 设置其 fileName 为 "NoName", * Created by MrFu on 15/12/16. */ public class CustomTypedByteArray extends TypedByteArray { /** * Constructs a new typed byte array. Sets mimeType to {@code application/unknown} if absent. * * @param mimeType * @param bytes * @throws NullPointerException if bytes are null */ public CustomTypedByteArray(String mimeType, byte[] bytes) { super(mimeType, bytes); } @Override public String fileName() { return "NoName"; } } ================================================ FILE: app/src/main/java/mrfu/rxface/models/BaseResponse.java ================================================ package mrfu.rxface.models; import mrfu.rxface.loader.WebServiceException; import rx.Observable; /** * Created by MrFu on 16/1/10. */ public class BaseResponse { public Observable filterWebServiceErrors() { if (true){//judge result status is ok~~ return Observable.just(this); }else { return Observable.error(new WebServiceException("Service return Error message")); } //demo code just like blow, the common is a class object, you can deal the error code in here. // public class BaseResponse { // public Common common; // // public Observable filterWebServiceErrors() { // if (common == null){ // return Observable.error(new WebServiceException("啊喔,服务器除了点小问题")); // }else { // int code = Integer.parseInt(common.status); // switch (code){ // case Constants.RESULT_OK://正常 // return Observable.just(this); // case Constants.RESULT_NORMAL_UPDATE://普通升级 // case Constants.RESULT_FORCE_UPDATE://墙纸升级 // if (AppApplication.getInstance().updateModel == null) { // AppApplication.getInstance().updateModel = common.update; // AppApplication.getInstance().updateModel.code = code; // } // return Observable.just(this); // default://出错 // return Observable.error(new WebServiceException(BaseResponse.this.common.memo)); // } // } // } // } } } ================================================ FILE: app/src/main/java/mrfu/rxface/models/FaceResponse.java ================================================ package mrfu.rxface.models; import java.util.List; /** * Created by MrFu on 15/12/16. */ public class FaceResponse extends BaseResponse{ /** * face : [{"attribute":{"age":{"range":6,"value":18},"gender":{"confidence":99.9996,"value":"Male"},"race":{"confidence":99.8977,"value":"White"},"smiling":{"value":81.2229}},"face_id":"5bf244c54d5731974e25ee024b950cd3","position":{"center":{"x":47.833333,"y":49.036403},"eye_left":{"x":42.140333,"y":42.28394},"eye_right":{"x":53.658,"y":42.389936},"height":31.263383,"mouth_left":{"x":42.352167,"y":56.311991},"mouth_right":{"x":53.747833,"y":56.89379},"nose":{"x":47.392667,"y":51.794004},"width":24.333333},"tag":""},{"attribute":{"age":{"range":5,"value":24},"gender":{"confidence":99.5758,"value":"Male"},"race":{"confidence":99.94800000000001,"value":"White"},"smiling":{"value":93.0865}},"face_id":"092cb7660d3f8d115b8af331465624a1","position":{"center":{"x":83.833333,"y":52.462527},"eye_left":{"x":77.052667,"y":47.005353},"eye_right":{"x":87.198,"y":44.253961},"height":28.265525,"mouth_left":{"x":77.304167,"y":60.063169},"mouth_right":{"x":86.635167,"y":60.254176},"nose":{"x":86.9965,"y":54.840685},"width":22},"tag":""},{"attribute":{"age":{"range":11,"value":38},"gender":{"confidence":74.2956,"value":"Female"},"race":{"confidence":96.8155,"value":"White"},"smiling":{"value":86.556}},"face_id":"3c9d898f732c84d07d760f184bfec814","position":{"center":{"x":13,"y":52.248394},"eye_left":{"x":8.483217,"y":44.584797},"eye_right":{"x":18.669667,"y":46.517987},"height":27.837259,"mouth_left":{"x":8.44005,"y":60.162955},"mouth_right":{"x":17.914333,"y":60.197645},"nose":{"x":8.59085,"y":53.623769},"width":21.666667},"tag":""}] * img_height : 490 * img_id : f3f8c2826537ce51ca1995143e8b9289 * img_width : 629 * session_id : a7f871065a064bdfabe06de48189dcac * url : http://imglife.gmw.cn/attachement/jpg/site2/20111014/002564a5d7d21002188831.jpg */ public int img_height; public String img_id; public int img_width; public String session_id; public String url; /** * attribute : {"age":{"range":6,"value":18},"gender":{"confidence":99.9996,"value":"Male"},"race":{"confidence":99.8977,"value":"White"},"smiling":{"value":81.2229}} * face_id : 5bf244c54d5731974e25ee024b950cd3 * position : {"center":{"x":47.833333,"y":49.036403},"eye_left":{"x":42.140333,"y":42.28394},"eye_right":{"x":53.658,"y":42.389936},"height":31.263383,"mouth_left":{"x":42.352167,"y":56.311991},"mouth_right":{"x":53.747833,"y":56.89379},"nose":{"x":47.392667,"y":51.794004},"width":24.333333} * tag : */ public List face; public static class FaceEntity { /** * age : {"range":6,"value":18} * gender : {"confidence":99.9996,"value":"Male"} * race : {"confidence":99.8977,"value":"White"} * smiling : {"value":81.2229} */ public AttributeEntity attribute; public String face_id; /** * center : {"x":47.833333,"y":49.036403} * eye_left : {"x":42.140333,"y":42.28394} * eye_right : {"x":53.658,"y":42.389936} * height : 31.263383 * mouth_left : {"x":42.352167,"y":56.311991} * mouth_right : {"x":53.747833,"y":56.89379} * nose : {"x":47.392667,"y":51.794004} * width : 24.333333 */ public PositionEntity position; public String tag; public static class AttributeEntity { /** * range : 6 * value : 18 */ public AgeEntity age; /** * confidence : 99.9996 * value : Male */ public GenderEntity gender; /** * confidence : 99.8977 * value : White */ public RaceEntity race; /** * value : 81.2229 */ public SmilingEntity smiling; public static class AgeEntity { public int range; public int value; } public static class GenderEntity { public double confidence; public String value; } public static class RaceEntity { public double confidence; public String value; } public static class SmilingEntity { public double value; } } public static class PositionEntity { /** * x : 47.833333 * y : 49.036403 */ public CenterEntity center; /** * x : 42.140333 * y : 42.28394 */ public EyeLeftEntity eye_left; /** * x : 53.658 * y : 42.389936 */ public EyeRightEntity eye_right; public double height; /** * x : 42.352167 * y : 56.311991 */ public MouthLeftEntity mouth_left; /** * x : 53.747833 * y : 56.89379 */ public MouthRightEntity mouth_right; /** * x : 47.392667 * y : 51.794004 */ public NoseEntity nose; public double width; public static class CenterEntity { public double x; public double y; } public static class EyeLeftEntity { public double x; public double y; } public static class EyeRightEntity { public double x; public double y; } public static class MouthLeftEntity { public double x; public double y; } public static class MouthRightEntity { public double x; public double y; } public static class NoseEntity { public double x; public double y; } } } } ================================================ FILE: app/src/main/java/mrfu/rxface/models/NeedDataEntity.java ================================================ package mrfu.rxface.models; import android.graphics.Bitmap; /** * Created by MrFu on 15/12/16. */ public class NeedDataEntity { public Bitmap bitmap; public String displayStr; } ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/content_main.xml ================================================