/*
* Copyright 2015 Hippo Seven
*
* 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.
*/
package com.hippo.nimingban.client.ac;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.webkit.MimeTypeMap;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.hippo.io.FileInputStreamPipe;
import com.hippo.nimingban.NMBAppConfig;
import com.hippo.nimingban.client.CancelledException;
import com.hippo.nimingban.client.NMBException;
import com.hippo.nimingban.client.StringEscape;
import com.hippo.nimingban.client.ac.data.ACCdnPath;
import com.hippo.nimingban.client.ac.data.ACFeed;
import com.hippo.nimingban.client.ac.data.ACForumGroup;
import com.hippo.nimingban.client.ac.data.ACPost;
import com.hippo.nimingban.client.ac.data.ACPostStruct;
import com.hippo.nimingban.client.ac.data.ACReference;
import com.hippo.nimingban.client.ac.data.ACReplyStruct;
import com.hippo.nimingban.client.ac.data.ACSearchItem;
import com.hippo.nimingban.client.data.ACSite;
import com.hippo.nimingban.client.data.CommonPost;
import com.hippo.nimingban.client.data.DumpSite;
import com.hippo.nimingban.client.data.Post;
import com.hippo.nimingban.client.data.Reply;
import com.hippo.nimingban.util.BitmapUtils;
import com.hippo.yorozuya.IOUtils;
import com.hippo.yorozuya.StringUtils;
import com.hippo.yorozuya.io.InputStreamPipe;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import okhttp3.Call;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public final class ACEngine {
private ACEngine() {}
private static final String TAG = ACEngine.class.getSimpleName();
private static final MediaType MEDIA_TYPE_IMAGE_ALL = MediaType.parse("image/*");
private static final MediaType MEDIA_TYPE_IMAGE_JPEG = MediaType.parse("image/jpeg");
private static final String UNKNOWN = "Unknown";
private static void throwException(Call call, String body, Exception e) throws Exception {
if (call.isCanceled()) {
throw new CancelledException();
}
if (e instanceof NMBException) {
if (!UNKNOWN.equals(e.getMessage())) {
throw e;
}
}
if (TextUtils.isEmpty(body)) {
return;
}
try {
JSONObject jo = JSON.parseObject(body);
if (!jo.getBoolean("success")) {
throw new NMBException(ACSite.getInstance(), jo.getString("msg"));
}
} catch (Exception ee) {
// Ignore
}
Document doc = Jsoup.parse(body);
List<Element> elements = doc.getElementsByClass("error");
if (!elements.isEmpty()) {
throw new NMBException(ACSite.getInstance(), elements.get(0).text());
}
try {
throw new NMBException(ACSite.getInstance(), StringEscape.unescapeJson(body));
} catch (StringEscape.UnescapeException ee) {
// Ignore
}
}
public static Call prepareGetCookie(OkHttpClient okHttpClient) {
String url = ACUrl.API_GET_COOKIE;
Log.d(TAG, url);
Request request = new Request.Builder().url(url).build();
return okHttpClient.newCall(request);
}
public static Boolean doGetCookie(Call call) throws Exception {
String body = null;
try {
Response response = call.execute();
body = response.body().string();
if (!"\"ok\"".equals(body)) {
throw new NMBException(ACSite.getInstance(), UNKNOWN);
}
return true;
} catch (Exception e) {
throwException(call, body, e);
throw e;
}
}
public static Call prepareGetCdnPath(OkHttpClient okHttpClient) {
String url = ACUrl.API_GET_CDN_PATH;
Log.d(TAG, url);
Request request = new Request.Builder().url(url).build();
return okHttpClient.newCall(request);
}
public static List<ACCdnPath> doGetCdnPath(Call call) throws Exception {
String body = null;
try {
Response response = call.execute();
body = response.body().string();
return JSON.parseArray(body, ACCdnPath.class);
} catch (Exception e) {
throwException(call, body, e);
throw e;
}
}
public static Call prepareGetCommonPosts(OkHttpClient okHttpClient) {
String url = ACUrl.API_COMMON_POSTS;
Log.d(TAG, url);
Request request = new Request.Builder().url(url).build();
return okHttpClient.newCall(request);
}
public static List<CommonPost> doGetCommonPosts(Call call) throws Exception {
String body = null;
try {
Response response = call.execute();
body = response.body().string();
List<CommonPost> result = JSON.parseArray(body, CommonPost.class);
if (result == null) {
throw new NMBException(ACSite.getInstance(), "Can't parse json when getForumList");
}
return result;
} catch (Exception e) {
throwException(call, body, e);
throw e;
}
}
public static Call prepareGetForumList(OkHttpClient okHttpClient) {
String url = ACUrl.API_GET_FORUM_LIST;
Log.d(TAG, url);
Request request = new Request.Builder().url(url).build();
return okHttpClient.newCall(request);
}
public static List<ACForumGroup> doGetForumList(Call call) throws Exception {
String body = null;
try {
Response response = call.execute();
body = response.body().string();
List<ACForumGroup> result = JSON.parseArray(body, ACForumGroup.class);
if (result == null) {
throw new NMBException(ACSite.getInstance(), "Can't parse json when getForumList");
}
return result;
} catch (Exception e) {
throwException(call, body, e);
throw e;
}
}
public static Call prepareGetPostList(OkHttpClient okHttpClient, String id, int page) {
String url = ACUrl.getPostListUrl(id, page);
Log.d(TAG, url);
Request request = new Request.Builder().url(url).build();
return okHttpClient.newCall(request);
}
public static List<Post> doGetPostList(Call call) throws Exception {
String body = null;
try {
Response response = call.execute();
body = response.body().string();
List<ACPost> acPosts = JSON.parseArray(body, ACPost.class);
if (acPosts == null) {
throw new NMBException(ACSite.getInstance(), "Can't parse json when getPostList");
}
List<Post> result = new ArrayList<>(acPosts.size());
for (ACPost acPost : acPosts) {
if (acPost != null) {
acPost.generateSelfAndReplies(ACSite.getInstance());
result.add(acPost);
}
}
return result;
} catch (Exception e) {
throwException(call, body, e);
throw e;
}
}
public static Call prepareGetPost(OkHttpClient okHttpClient, String id, int page) {
String url = ACUrl.getPostUrl(id, page);
Log.d(TAG, url);
Request request = new Request.Builder().url(url).build();
return okHttpClient.newCall(request);
}
public static Pair<Post, List<Reply>> doGetPost(Call call) throws Exception {
String body = null;
try {
Response response = call.execute();
body = response.body().string();
ACPost acPost = JSON.parseObject(body, ACPost.class);
if (acPost == null) {
throw new NMBException(ACSite.getInstance(), "Can't parse json when getPost");
}
acPost.generateSelfAndReplies(ACSite.getInstance());
return new Pair<Post, List<Reply>>(acPost, new ArrayList<Reply>(acPost.replys));
} catch (Exception e) {
throwException(call, body, e);
throw e;
}
}
public static Call prepareGetReference(OkHttpClient okHttpClient, String id) {
String url = ACUrl.getReferenceUrl(id);
Log.d(TAG, url);
Request request = new Request.Builder().url(url).build();
return okHttpClient.newCall(request);
}
public static Reply doGetReference(Call call) throws Exception {
String body = null;
try {
Response response = call.execute();
body = response.body().string();
ACReference reference = new ACReference();
Document doc = Jsoup.parse(body, ACUrl.HOST + "/");
List<Element> elements = doc.getAllElements();
for (Element element : elements) {
String className = element.className();
if ("h-threads-item-reply h-threads-item-ref".equals(className)) {
reference.id = element.attr("data-threads-id");
} else if ("h-threads-img-a".equals(className)) {
reference.image = element.attr("href");
} else if ("h-threads-img".equals(className)) {
reference.thumb = element.attr("src");
} else if ("h-threads-info-title".equals(className)) {
reference.title = element.text();
} else if ("h-threads-info-email".equals(className)) {
// TODO email or user ?
reference.user = element.text();
} else if ("h-threads-info-createdat".equals(className)) {
reference.time = element.text();
} else if ("h-threads-info-uid".equals(className)) {
String user = element.text();
if (user.startsWith("ID:")) {
reference.userId = user.substring(3);
} else {
reference.userId = user;
}
reference.admin = element.childNodeSize() > 1;
} else if ("h-threads-info-id".equals(className)) {
String href = element.attr("href");
if (href.startsWith("/t/")) {
int index = href.indexOf('?');
if (index >= 0) {
reference.postId = href.substring(3, index);
} else {
reference.postId = href.substring(3);
}
}
} else if ("h-threads-content".equals(className)) {
reference.content = element.html();
}
}
reference.generate(ACSite.getInstance());
return reference;
} catch (Exception e) {
throwException(call, body, e);
throw e;
}
}
public static Call prepareReply(OkHttpClient okHttpClient, ACReplyStruct struct) throws Exception {
MultipartBody.Builder builder = new MultipartBody.Builder();
builder.setType(MultipartBody.FORM);
builder.addPart(
Headers.of("Content-Disposition", "form-data; name=\"name\""),
RequestBody.create(null, StringUtils.avoidNull(struct.name)));
builder.addPart(
Headers.of("Content-Disposition", "form-data; name=\"email\""),
RequestBody.create(null, StringUtils.avoidNull(struct.email)));
builder.addPart(
Headers.of("Content-Disposition", "form-data; name=\"title\""),
RequestBody.create(null, StringUtils.avoidNull(struct.title)));
builder.addPart(
Headers.of("Content-Disposition", "form-data; name=\"content\""),
RequestBody.create(null, StringUtils.avoidNull(struct.content)));
builder.addPart(
Headers.of("Content-Disposition", "form-data; name=\"resto\""),
RequestBody.create(null, StringUtils.avoidNull(struct.resto)));
if (struct.water) {
builder.addPart(
Headers.of("Content-Disposition", "form-data; name=\"water\""),
RequestBody.create(null, "true"));
}
if (struct.image != null) {
final byte[] bytes;
File file = compressBitmap(struct.image, struct.imageType);
final String imageType;
final InputStreamPipe imagePipe;
if (file == null) {
// Origin image
imageType = struct.imageType;
imagePipe = struct.image;
} else {
// Compressed image
// gif or jpeg
imageType = "image/gif".equals(struct.imageType) ? "image/gif" : "image/jpeg";
imagePipe = new FileInputStreamPipe(file);
}
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(imageType);
if (TextUtils.isEmpty(extension)) {
extension = "jpg";
}
final String filename = "a." + extension;
MediaType mediaType = MediaType.parse(imageType);
if (mediaType == null) {
mediaType = MEDIA_TYPE_IMAGE_ALL;
}
try {
imagePipe.obtain();
bytes = IOUtils.getAllByte(imagePipe.open());
} finally {
imagePipe.close();
imagePipe.release();
}
builder.addPart(
Headers.of("Content-Disposition", "form-data; name=\"image\"; filename=\"" + filename + "\""),
RequestBody.create(mediaType, bytes));
}
String url = ACUrl.API_REPLY;
Log.d(TAG, url);
Request request = new Request.Builder()
.url(url)
.post(builder.build())
.build();
return okHttpClient.newCall(request);
}
public static Void doReply(Call call) throws Exception {
String body = null;
try {
Response response = call.execute();
body = response.body().string();
try {
JSONObject jo = JSON.parseObject(body);
if (jo.getBoolean("success")) {
return null;
} else {
throw new NMBException(ACSite.getInstance(), jo.getString("msg"));
}
} catch (Exception e) {
if (body.contains("class=\"success\"")) {
return null;
} else {
throw e;
}
}
} catch (Exception e) {
throwException(call, body, e);
throw e;
}
}
public static Call prepareGetFeed(OkHttpClient okHttpClient, String uuid, int page) {
String url = ACUrl.getFeedUrl(uuid, page);
Log.d(TAG, url);
Request request = new Request.Builder().url(url).build();
return okHttpClient.newCall(request);
}
public static List<Post> doGetFeed(Call call) throws Exception {
String body = null;
try {
Response response = call.execute();
body = response.body().string();
List<ACFeed> acFeeds = JSON.parseArray(body, ACFeed.class);
if (acFeeds == null) {
throw new NMBException(ACSite.getInstance(), "Can't parse json when getPostList");
}
List<Post> result = new ArrayList<>(acFeeds.size());
for (ACFeed feed : acFeeds) {
if (feed != null) {
feed.generate(ACSite.getInstance());
result.add(feed);
}
}
return result;
} catch (Exception e) {
throwException(call, body, e);
throw e;
}
}
public static Call prepareAddFeed(OkHttpClient okHttpClient, String uuid, String tid) {
String url = ACUrl.getAddFeedUrl(uuid, tid);
Log.d(TAG, url);
Request request = new Request.Builder().url(url).build();
return okHttpClient.newCall(request);
}
public static Void doAddFeed(Call call) throws Exception {
String body = null;
try {
Response response = call.execute();
body = response.body().string();
if (body.equals("\"\\u8ba2\\u9605\\u5927\\u6210\\u529f\\u2192_\\u2192\"")) {
return null;
} else {
throw new NMBException(ACSite.getInstance(), UNKNOWN);
}
} catch (Exception e) {
throwException(call, body, e);
throw e;
}
}
public static Call prepareDelFeed(OkHttpClient okHttpClient, String uuid, String tid) {
String url = ACUrl.getDelFeedUrl(uuid, tid);
Log.d(TAG, url);
Request request = new Request.Builder().url(url).build();
return okHttpClient.newCall(request);
}
public static Void doDelFeed(Call call) throws Exception {
String body = null;
try {
Response response = call.execute();
body = response.body().string();
if (body.equals("\"\\u53d6\\u6d88\\u8ba2\\u9605\\u6210\\u529f!\"")) {
return null;
} else {
throw new NMBException(ACSite.getInstance(), UNKNOWN);
}
} catch (Exception e) {
throwException(call, body, e);
throw e;
}
}
public static Call prepareCreatePost(OkHttpClient okHttpClient, ACPostStruct struct) throws Exception {
MultipartBody.Builder builder = new MultipartBody.Builder();
builder.setType(MultipartBody.FORM);
builder.addPart(
Headers.of("Content-Disposition", "form-data; name=\"name\""),
RequestBody.create(null, StringUtils.avoidNull(struct.name)));
builder.addPart(
Headers.of("Content-Disposition", "form-data; name=\"email\""),
RequestBody.create(null, StringUtils.avoidNull(struct.email)));
builder.addPart(
Headers.of("Content-Disposition", "form-data; name=\"title\""),
RequestBody.create(null, StringUtils.avoidNull(struct.title)));
builder.addPart(
Headers.of("Content-Disposition", "form-data; name=\"content\""),
RequestBody.create(null, StringUtils.avoidNull(struct.content)));
builder.addPart(
Headers.of("Content-Disposition", "form-data; name=\"fid\""),
RequestBody.create(null, StringUtils.avoidNull(struct.fid)));
if (struct.water) {
builder.addPart(
Headers.of("Content-Disposition", "form-data; name=\"water\""),
RequestBody.create(null, "true"));
}
if (struct.image != null) {
final byte[] bytes;
File file = compressBitmap(struct.image, struct.imageType);
final String imageType;
final InputStreamPipe imagePipe;
if (file == null) {
// Origin image
imageType = struct.imageType;
imagePipe = struct.image;
} else {
// Compressed image
// gif or jpeg
imageType = "image/gif".equals(struct.imageType) ? "image/gif" : "image/jpeg";
imagePipe = new FileInputStreamPipe(file);
}
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(imageType);
if (TextUtils.isEmpty(extension)) {
extension = "jpg";
}
final String filename = "a." + extension;
MediaType mediaType = MediaType.parse(imageType);
if (mediaType == null) {
mediaType = MEDIA_TYPE_IMAGE_ALL;
}
try {
imagePipe.obtain();
bytes = IOUtils.getAllByte(imagePipe.open());
} finally {
imagePipe.close();
imagePipe.release();
}
builder.addPart(
Headers.of("Content-Disposition", "form-data; name=\"image\"; filename=\"" + filename + "\""),
RequestBody.create(mediaType, bytes));
}
String url = ACUrl.API_CREATE_POST;
Log.d(TAG, url);
Request request = new Request.Builder()
.url(url)
.post(builder.build())
.build();
return okHttpClient.newCall(request);
}
public static Void doCreatePost(Call call) throws Exception {
String body = null;
try {
Response response = call.execute();
body = response.body().string();
try {
JSONObject jo = JSON.parseObject(body);
if (jo.getBoolean("success")) {
return null;
} else {
throw new NMBException(ACSite.getInstance(), jo.getString("msg"));
}
} catch (Exception e) {
if (body.contains("class=\"success\"")) {
return null;
} else {
throw e;
}
}
} catch (Exception e) {
throwException(call, body, e);
throw e;
}
}
private static final long MAX_IMAGE_SIZE = 2000 * 1024;
private static int getBitmapWidth(File file) {
InputStream is = null;
try {
is = new FileInputStream(file);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
return options.outWidth;
} catch (FileNotFoundException e) {
return 0;
} finally {
IOUtils.closeQuietly(is);
}
}
private static boolean compressGifsicle(File input, File output) throws IOException {
final String gifsicleFilename;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
gifsicleFilename = "libgifsicle_executable.so";
} else {
gifsicleFilename = "libgifsicle_executable_legacy.so";
}
final File gifsicle = new File(NMBAppConfig.getNativeLibDir(), gifsicleFilename);
if (!gifsicle.canExecute()) {
return false;
}
float scale = (float) Math.sqrt((float) MAX_IMAGE_SIZE / (float) input.length());
int width = (int) (getBitmapWidth(input) * scale);
if (width <= 0) {
return false;
}
final int offset = width / 5;
for (int i = 0; i < 5 && width > 0; i++, width -= offset) {
String cmd = String.format(Locale.US, "%s --resize-width %d --output %s %s",
gifsicle.getPath(), width, output.getPath(), input.getPath());
String[] envp = { "LD_LIBRARY_PATH=" + NMBAppConfig.getNativeLibDir() };
Process process = Runtime.getRuntime().exec(cmd, envp);
try {
if (process.waitFor() != 0) {
return false;
}
if (output.length() < MAX_IMAGE_SIZE) {
return true;
}
} catch (InterruptedException e) {
e.printStackTrace();
return false;
}
}
return false;
}
/**
* @return null for not changed
*/
public static File compressBitmap(InputStreamPipe isp, String imageType) throws Exception {
OutputStream os = null;
try {
isp.obtain();
File temp = NMBAppConfig.createTempFile();
if (temp == null) {
throw new NMBException(DumpSite.getInstance(), "Can't create temp file");
}
os = new FileOutputStream(temp);
IOUtils.copy(isp.open(), os);
isp.close();
os.close();
long size = temp.length();
if (size < MAX_IMAGE_SIZE) {
temp.delete();
return null;
}
if ("image/gif".equals(imageType)) {
File output = NMBAppConfig.createTempFile();
if (output == null) {
throw new NMBException(DumpSite.getInstance(), "Can't create temp file");
}
if (compressGifsicle(temp, output)) {
return output;
} else {
throw new NMBException(DumpSite.getInstance(), "Can't compress gif");
}
} else {
int[] sampleScaleArray = new int[1];
BitmapUtils.decodeStream(new FileInputStreamPipe(temp), -1, -1, -1, true, true, sampleScaleArray);
int sampleScale = sampleScaleArray[0];
if (sampleScale < 1) {
throw new NMBException(DumpSite.getInstance(), "Can't get bitmap size");
}
BitmapFactory.Options options = new BitmapFactory.Options();
int i = (int) (Math.log(sampleScale) / Math.log(2));
while (true) {
options.inSampleSize = (int) Math.pow(2, i);
Bitmap bitmap = null;
try {
bitmap = BitmapFactory.decodeStream(isp.open(), null, options);
} catch (OutOfMemoryError e) {
// Ignore
}
if (bitmap == null) {
throw new NMBException(ACSite.getInstance(), "Can't decode bitmap");
}
isp.close();
os = new FileOutputStream(temp);
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, os);
os.close();
bitmap.recycle();
size = temp.length();
if (size < MAX_IMAGE_SIZE) {
return temp;
}
i++;
}
}
} finally {
isp.close();
isp.release();
IOUtils.closeQuietly(os);
}
}
public static Call prepareSearch(OkHttpClient okHttpClient, String keyword, int page) throws UnsupportedEncodingException {
String url = ACUrl.getSearchUrl(keyword, page);
Log.d(TAG, url);
Request request = new Request.Builder().url(url).build();
return okHttpClient.newCall(request);
}
public static List<ACSearchItem> doSearch(Call call) throws Exception {
String body = null;
try {
Response response = call.execute();
body = response.body().string();
JSONArray ja = JSON.parseObject(body).getJSONObject("hits").getJSONArray("hits");
List<ACSearchItem> result = new ArrayList<>();
for (int i = 0, n = ja.size(); i < n; i++) {
JSONObject jo = ja.getJSONObject(i);
ACSearchItem item = jo.getObject("_source", ACSearchItem.class);
item.id = jo.getString("_id");
item.generate(ACSite.getInstance());
result.add(item);
}
return result;
} catch (Exception e) {
throwException(call, body, e);
throw e;
}
}
}