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