diff --git a/app/src/main/java/com/hippo/ehviewer/AppConfig.java b/app/src/main/java/com/hippo/ehviewer/AppConfig.java index e2efaf13c..5dd05e0bf 100644 --- a/app/src/main/java/com/hippo/ehviewer/AppConfig.java +++ b/app/src/main/java/com/hippo/ehviewer/AppConfig.java @@ -113,4 +113,20 @@ public class AppConfig { public static File createExternalTempFile() { return FileUtils.createTempFile(getExternalTempDir(), null); } + + @Nullable + public static File getTempDir() { + File dir = sContext.getCacheDir(); + File file; + if (null != dir && FileUtils.ensureDirectory(file = new File(dir, TEMP))) { + return file; + } else { + return null; + } + } + + @Nullable + public static File createTempFile() { + return FileUtils.createTempFile(getTempDir(), null); + } } diff --git a/app/src/main/java/com/hippo/ehviewer/EhApplication.java b/app/src/main/java/com/hippo/ehviewer/EhApplication.java index d9a99bf98..d957b2a78 100644 --- a/app/src/main/java/com/hippo/ehviewer/EhApplication.java +++ b/app/src/main/java/com/hippo/ehviewer/EhApplication.java @@ -43,6 +43,7 @@ import com.hippo.network.StatusCodeException; import com.hippo.okhttp.CookieDB; import com.hippo.scene.SceneApplication; import com.hippo.text.Html; +import com.hippo.util.BitmapUtils; import com.hippo.util.ReadableTime; import com.hippo.yorozuya.FileUtils; import com.hippo.yorozuya.SimpleHandler; @@ -97,6 +98,7 @@ public class EhApplication extends SceneApplication implements Thread.UncaughtEx SpiderDen.initialize(this); EhDB.initialize(this); EhEngine.initialize(); + BitmapUtils.initialize(this); if (EhDB.needMerge()) { EhDB.mergeOldDB(this); diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhClient.java b/app/src/main/java/com/hippo/ehviewer/client/EhClient.java index e40c1af02..e2521d19d 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhClient.java +++ b/app/src/main/java/com/hippo/ehviewer/client/EhClient.java @@ -25,6 +25,7 @@ import com.hippo.ehviewer.client.exception.CancelledException; import com.hippo.yorozuya.PriorityThreadFactory; import com.hippo.yorozuya.SimpleHandler; +import java.io.File; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -57,6 +58,7 @@ public class EhClient { public static final int METHOD_GET_WHATS_HOT = 13; public static final int METHOD_GET_PROFILE = 14; public static final int METHOD_VOTE_COMMENT = 15; + public static final int METHOD_IMAGE_SEARCH = 16; private final ThreadPoolExecutor mRequestThreadPool; private final OkHttpClient mOkHttpClient; @@ -180,6 +182,8 @@ public class EhClient { return EhEngine.getProfile(this, mOkHttpClient); case METHOD_VOTE_COMMENT: return EhEngine.voteComment(this, mOkHttpClient, (Long) params[0], (String) params[1], (Long) params[2], (String) params[3], (Long) params[4], (Integer) params[5]); + case METHOD_IMAGE_SEARCH: + return EhEngine.imageSearch(this, mOkHttpClient, (File) params[0], (Boolean) params[1], (Boolean) params[2], (Boolean) params[3]); default: return new IllegalStateException("Can't detect method " + mMethod); } diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhEngine.java b/app/src/main/java/com/hippo/ehviewer/client/EhEngine.java index aaed07040..1454f8271 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhEngine.java +++ b/app/src/main/java/com/hippo/ehviewer/client/EhEngine.java @@ -64,6 +64,7 @@ import okhttp3.Call; import okhttp3.FormBody; import okhttp3.Headers; import okhttp3.MediaType; +import okhttp3.MultipartBody; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; @@ -77,7 +78,8 @@ public class EhEngine { private static final String SAD_PANDA_TYPE = "image/gif"; private static final String SAD_PANDA_LENGTH = "9615"; - public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + public static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json; charset=utf-8"); + private static final MediaType MEDIA_TYPE_JPEG = MediaType.parse("image/jpeg"); public static EhFilter sEhFilter; @@ -260,7 +262,7 @@ public class EhEngine { String url = EhUrl.getApiUrl(); Log.d(TAG, url); Request request = new EhRequestBuilder(url) - .post(RequestBody.create(JSON, json.toString())) + .post(RequestBody.create(MEDIA_TYPE_JSON, json.toString())) .build(); Call call = okHttpClient.newCall(request); @@ -342,7 +344,7 @@ public class EhEngine { json.put("gid", gid); json.put("token", token); json.put("rating", (int) Math.ceil(rating * 2)); - final RequestBody requestBody = RequestBody.create(JSON, json.toString()); + final RequestBody requestBody = RequestBody.create(MEDIA_TYPE_JSON, json.toString()); String url = EhUrl.getApiUrl(); Log.d(TAG, url); Request request = new EhRequestBuilder(url, task.getEhConfig()) @@ -404,7 +406,7 @@ public class EhEngine { .put("method", "gtoken") .put("pagelist", new JSONArray().put( new JSONArray().put(gid).put(gtoken).put(page + 1))); - final RequestBody requestBody = RequestBody.create(JSON, json.toString()); + final RequestBody requestBody = RequestBody.create(MEDIA_TYPE_JSON, json.toString()); String url = EhUrl.getApiUrl(); Log.d(TAG, url); Request request = new EhRequestBuilder(url, task.getEhConfig()) @@ -682,7 +684,7 @@ public class EhEngine { json.put("token", token); json.put("comment_id", commentId); json.put("comment_vote", commentVote); - final RequestBody requestBody = RequestBody.create(JSON, json.toString()); + final RequestBody requestBody = RequestBody.create(MEDIA_TYPE_JSON, json.toString()); String url = EhUrl.getApiUrl(); Log.d(TAG, url); Request request = new EhRequestBuilder(url, task.getEhConfig()) @@ -707,4 +709,94 @@ public class EhEngine { throw e; } } + + /** + * @param image Must be jpeg + */ + public static GalleryListParser.Result imageSearch(EhClient.Task task, OkHttpClient okHttpClient, + File image, boolean uss, boolean osc, boolean se) throws Exception { + MultipartBody.Builder builder = new MultipartBody.Builder(); + builder.setType(MultipartBody.FORM); + builder.addPart( + Headers.of("Content-Disposition", "form-data; name=\"sfile\"; filename=\"a.jpg\""), + RequestBody.create(MEDIA_TYPE_JPEG, image) + ); + if (uss) { + builder.addPart( + Headers.of("Content-Disposition", "form-data; name=\"fs_similar\""), + RequestBody.create(null, "on") + ); + } + if (osc) { + builder.addPart( + Headers.of("Content-Disposition", "form-data; name=\"fs_covers\""), + RequestBody.create(null, "on") + ); + } + if (se) { + builder.addPart( + Headers.of("Content-Disposition", "form-data; name=\"fs_exp\""), + RequestBody.create(null, "on") + ); + } + builder.addPart( + Headers.of("Content-Disposition", "form-data; name=\"f_sfile\""), + RequestBody.create(null, "File Search") + ); + String url = EhUrl.getImageSearchUrl(); + Log.d(TAG, url); + Request request = new EhRequestBuilder(url, task.getEhConfig()) + .post(builder.build()) + .build(); + Call call = okHttpClient.newCall(request); + + // Put call + task.setCall(call); + + String body = null; + Headers headers = null; + GalleryListParser.Result result; + int code = -1; + try { + Response response = call.execute(); + + Log.d(TAG, "" + response.request().url().toString()); + + code = response.code(); + headers = response.headers(); + body = response.body().string(); + result = GalleryListParser.parse(body); + } catch (Exception e) { + throwException(call, code, headers, body, e); + throw e; + } + + // Filter title and uploader + List list = result.galleryInfoList; + for (int i = 0, n = list.size(); i < n; i++) { + GalleryInfo info = list.get(i); + if (!sEhFilter.filterTitle(info) || !sEhFilter.filterUploader(info)) { + list.remove(i); + i--; + n--; + } + } + + if (list.size() > 0 && (Settings.getShowJpnTitle() || sEhFilter.needCallApi())) { + // Fill by api + fillGalleryListByApi(task, okHttpClient, list); + + // Filter tag + for (int i = 0, n = list.size(); i < n; i++) { + GalleryInfo info = list.get(i); + if (!sEhFilter.filterTag(info) || !sEhFilter.filterTagNamespace(info)) { + list.remove(i); + i--; + n--; + } + } + } + + return result; + } } diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhUrl.java b/app/src/main/java/com/hippo/ehviewer/client/EhUrl.java index a7cee7cf5..180661173 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhUrl.java +++ b/app/src/main/java/com/hippo/ehviewer/client/EhUrl.java @@ -16,6 +16,8 @@ package com.hippo.ehviewer.client; +import android.support.annotation.NonNull; + import com.hippo.ehviewer.Settings; import com.hippo.network.UrlBuilder; @@ -99,4 +101,15 @@ public class EhUrl { public static String getTagDefinitionUrl(String tag) { return "https://ehwiki.org/wiki/" + tag.replace(' ', '_'); } + + @NonNull + public static String getImageSearchUrl() { + switch (Settings.getGallerySite()) { + default: + case SITE_G: + return "http://ul.e-hentai.org/image_lookup.php"; + case SITE_EX: + return "http://ul.exhentai.org/image_lookup.php"; + } + } } diff --git a/app/src/main/java/com/hippo/ehviewer/client/data/ListUrlBuilder.java b/app/src/main/java/com/hippo/ehviewer/client/data/ListUrlBuilder.java index e0d79dc22..d4d996214 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/data/ListUrlBuilder.java +++ b/app/src/main/java/com/hippo/ehviewer/client/data/ListUrlBuilder.java @@ -62,6 +62,11 @@ public class ListUrlBuilder implements Cloneable, Parcelable { private int mAdvanceSearch = -1; private int mMinRating = -1; + private String mImagePath; + private boolean mUseSimilarityScan; + private boolean mOnlySearchCovers; + private boolean mShowExpunged; + /** * Make this ListUrlBuilder point to homepage */ @@ -72,6 +77,10 @@ public class ListUrlBuilder implements Cloneable, Parcelable { mKeyword = null; mAdvanceSearch = -1; mMinRating = -1; + mImagePath = null; + mUseSimilarityScan = false; + mOnlySearchCovers = false; + mShowExpunged = false; } @Override @@ -132,6 +141,38 @@ public class ListUrlBuilder implements Cloneable, Parcelable { mMinRating = minRating; } + public String getImagePath() { + return mImagePath; + } + + public void setImagePath(String imagePath) { + mImagePath = imagePath; + } + + public boolean isUseSimilarityScan() { + return mUseSimilarityScan; + } + + public void setUseSimilarityScan(boolean useSimilarityScan) { + mUseSimilarityScan = useSimilarityScan; + } + + public boolean isOnlySearchCovers() { + return mOnlySearchCovers; + } + + public void setOnlySearchCovers(boolean onlySearchCovers) { + mOnlySearchCovers = onlySearchCovers; + } + + public boolean isShowExpunged() { + return mShowExpunged; + } + + public void setShowExpunged(boolean showExpunged) { + mShowExpunged = showExpunged; + } + /** * Make them the same * @param lub The template @@ -143,6 +184,10 @@ public class ListUrlBuilder implements Cloneable, Parcelable { mKeyword = lub.mKeyword; mAdvanceSearch = lub.mAdvanceSearch; mMinRating = lub.mMinRating; + mImagePath = lub.mImagePath; + mUseSimilarityScan = lub.mUseSimilarityScan; + mOnlySearchCovers = lub.mOnlySearchCovers; + mShowExpunged = lub.mShowExpunged; } public void set(QuickSearch q) { @@ -413,6 +458,8 @@ public class ListUrlBuilder implements Cloneable, Parcelable { } return sb.toString(); } + case MODE_IMAGE_SEARCH: + return EhUrl.getImageSearchUrl(); } } @@ -429,22 +476,30 @@ public class ListUrlBuilder implements Cloneable, Parcelable { dest.writeString(this.mKeyword); dest.writeInt(this.mAdvanceSearch); dest.writeInt(this.mMinRating); + dest.writeString(this.mImagePath); + dest.writeByte(mUseSimilarityScan ? (byte) 1 : (byte) 0); + dest.writeByte(mOnlySearchCovers ? (byte) 1 : (byte) 0); + dest.writeByte(mShowExpunged ? (byte) 1 : (byte) 0); } - public ListUrlBuilder() {} + public ListUrlBuilder() { + } @SuppressWarnings("WrongConstant") protected ListUrlBuilder(Parcel in) { - mMode = in.readInt(); - mPageIndex = in.readInt(); - mCategory = in.readInt(); - mKeyword = in.readString(); - mAdvanceSearch = in.readInt(); - mMinRating = in.readInt(); + this.mMode = in.readInt(); + this.mPageIndex = in.readInt(); + this.mCategory = in.readInt(); + this.mKeyword = in.readString(); + this.mAdvanceSearch = in.readInt(); + this.mMinRating = in.readInt(); + this.mImagePath = in.readString(); + this.mUseSimilarityScan = in.readByte() != 0; + this.mOnlySearchCovers = in.readByte() != 0; + this.mShowExpunged = in.readByte() != 0; } - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - + public static final Creator CREATOR = new Creator() { @Override public ListUrlBuilder createFromParcel(Parcel source) { return new ListUrlBuilder(source); diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java index bfb0b55b9..41e188a4a 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java @@ -20,6 +20,7 @@ import android.animation.Animator; import android.app.Activity; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.content.res.Resources; import android.graphics.Point; import android.graphics.drawable.Drawable; @@ -68,6 +69,7 @@ import com.hippo.ehviewer.client.EhUrl; import com.hippo.ehviewer.client.EhUtils; import com.hippo.ehviewer.client.data.GalleryInfo; import com.hippo.ehviewer.client.data.ListUrlBuilder; +import com.hippo.ehviewer.client.exception.EhException; import com.hippo.ehviewer.client.parser.GalleryListParser; import com.hippo.ehviewer.dao.QuickSearch; import com.hippo.ehviewer.ui.CommonOperations; @@ -90,6 +92,7 @@ import com.hippo.yorozuya.MathUtils; import com.hippo.yorozuya.SimpleAnimatorListener; import com.hippo.yorozuya.ViewUtils; +import java.io.File; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; @@ -106,6 +109,8 @@ public final class GalleryListScene extends BaseScene private static final int BACK_PRESSED_INTERVAL = 2000; + public final static int REQUEST_CODE_SELECT_IMAGE = 0; + public final static String KEY_ACTION = "action"; public final static String ACTION_HOMEPAGE = "action_homepage"; public final static String ACTION_WHATS_HOT = "action_whats_hot"; @@ -1111,7 +1116,12 @@ public final class GalleryListScene extends BaseScene startScene(new Announcer(GalleryDetailScene.class).setArgs(args)); return; } else { - mSearchLayout.formatListUrlBuilder(mUrlBuilder, query); + try { + mSearchLayout.formatListUrlBuilder(mUrlBuilder, query); + } catch (EhException e) { + showTip(e.getMessage(), LENGTH_LONG); + return; + } } } else { mUrlBuilder.reset(); @@ -1177,6 +1187,15 @@ public final class GalleryListScene extends BaseScene } } + @Override + public void onSelectImage() { + Intent intent = new Intent(); + intent.setType("image/*"); + intent.setAction(Intent.ACTION_GET_CONTENT); + startActivityForResult(Intent.createChooser(intent, + getString(R.string.select_image)), REQUEST_CODE_SELECT_IMAGE); + } + // SearchBarMover.Helper @Override public boolean isValidView(RecyclerView recyclerView) { @@ -1207,6 +1226,17 @@ public final class GalleryListScene extends BaseScene scene.startScene(new Announcer(GalleryListScene.class).setArgs(args)); } + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (REQUEST_CODE_SELECT_IMAGE == requestCode) { + if (Activity.RESULT_OK == resultCode && null != mSearchLayout && null != data) { + mSearchLayout.setImageUri(data.getData()); + } + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + private class GalleryListAdapter extends GalleryAdapter { public GalleryListAdapter(@NonNull LayoutInflater inflater, @@ -1243,6 +1273,14 @@ public final class GalleryListScene extends BaseScene activity.getStageId(), getTag(), taskId)); request.setArgs(); mClient.execute(request); + } else if (ListUrlBuilder.MODE_IMAGE_SEARCH == mUrlBuilder.getMode()) { + EhRequest request = new EhRequest(); + request.setMethod(EhClient.METHOD_IMAGE_SEARCH); + request.setCallback(new GetGalleryListListener(getContext(), + activity.getStageId(), getTag(), taskId)); + request.setArgs(new File(mUrlBuilder.getImagePath()), mUrlBuilder.isUseSimilarityScan(), + mUrlBuilder.isOnlySearchCovers(), mUrlBuilder.isShowExpunged()); + mClient.execute(request); } else { String url = mUrlBuilder.build(); EhRequest request = new EhRequest(); diff --git a/app/src/main/java/com/hippo/ehviewer/widget/ImageSearchLayout.java b/app/src/main/java/com/hippo/ehviewer/widget/ImageSearchLayout.java new file mode 100644 index 000000000..46bb355ec --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/ImageSearchLayout.java @@ -0,0 +1,238 @@ +/* + * Copyright 2016 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.ehviewer.widget; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.AbsSavedState; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import com.hippo.ehviewer.AppConfig; +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.client.data.ListUrlBuilder; +import com.hippo.ehviewer.client.exception.EhException; +import com.hippo.io.UniFileInputStreamPipe; +import com.hippo.unifile.UniFile; +import com.hippo.util.BitmapUtils; +import com.hippo.yorozuya.IOUtils; +import com.hippo.yorozuya.ViewUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; + +public final class ImageSearchLayout extends LinearLayout implements View.OnClickListener { + + private static final String TAG = ImageSearchLayout.class.getSimpleName(); + + private ImageView mPreview; + private View mSelectImage; + private CheckBox mSearchUSS; + private CheckBox mSearchOSC; + private CheckBox mSearchSE; + + private Helper mHelper; + + @Nullable + private String mImagePath; + + public ImageSearchLayout(Context context) { + super(context); + init(context); + } + + public ImageSearchLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public ImageSearchLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + @SuppressWarnings("deprecation") + public void init(Context context) { + setOrientation(VERTICAL); + setDividerDrawable(context.getResources().getDrawable(R.drawable.spacer_keyline)); + setShowDividers(SHOW_DIVIDER_MIDDLE); + LayoutInflater.from(context).inflate(R.layout.widget_image_search, this); + + mPreview = (ImageView) ViewUtils.$$(this, R.id.preview); + mSelectImage = ViewUtils.$$(this, R.id.select_image); + mSearchUSS = (CheckBox) ViewUtils.$$(this, R.id.search_uss); + mSearchOSC = (CheckBox) ViewUtils.$$(this, R.id.search_osc); + mSearchSE = (CheckBox) ViewUtils.$$(this, R.id.search_se); + + mSelectImage.setOnClickListener(this); + } + + public void setHelper(Helper helper) { + mHelper = helper; + } + + @Override + public void onClick(View v) { + if (v == mSelectImage) { + if (null != mHelper) { + mHelper.onSelectImage(); + } + } + } + + public void setImageUri(@Nullable Uri imageUri) { + if (null == imageUri) { + return; + } + + Context context = getContext(); + UniFile file = UniFile.fromUri(context, imageUri); + try { + int maxSize = context.getResources().getDimensionPixelOffset(R.dimen.image_search_max_size); + Bitmap bitmap = BitmapUtils.decodeStream(new UniFileInputStreamPipe(file), maxSize, maxSize); + if (null == bitmap) { + return; + } + File temp = AppConfig.createTempFile(); + if (null == temp) { + return; + } + + // TODO ehentai image search is bad when I'm writing this line. + // Re-compress image will make image search failed. + OutputStream os = null; + try { + os = new FileOutputStream(temp); + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os); + mImagePath = temp.getPath(); + mPreview.setImageBitmap(bitmap); + mPreview.setVisibility(VISIBLE); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } finally { + IOUtils.closeQuietly(os); + } + } catch (OutOfMemoryError e) { + Log.e(TAG, "Out of memory"); + } + } + + private void setImagePath(@Nullable String imagePath) { + if (null == imagePath) { + return; + } + + InputStream is = null; + try { + is = new FileInputStream(imagePath); + Bitmap bitmap = BitmapFactory.decodeStream(is); + if (null == bitmap) { + return; + } + mImagePath = imagePath; + mPreview.setImageBitmap(bitmap); + mPreview.setVisibility(VISIBLE); + } catch (FileNotFoundException e) { + // Ignore + } finally { + IOUtils.closeQuietly(is); + } + } + + public void formatListUrlBuilder(ListUrlBuilder builder) throws EhException { + if (null == mImagePath) { + throw new EhException(getContext().getString(R.string.select_image_first)); + } + + builder.setImagePath(mImagePath); + builder.setUseSimilarityScan(mSearchUSS.isChecked()); + builder.setOnlySearchCovers(mSearchOSC.isChecked()); + builder.setShowExpunged(mSearchSE.isChecked()); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.imagePath = mImagePath; + return ss; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + setImagePath(ss.imagePath); + } + + private static class SavedState extends AbsSavedState { + + String imagePath; + + /** + * Constructor called from {@link ImageSearchLayout#onSaveInstanceState()} + */ + protected SavedState(Parcelable superState) { + super(superState); + } + + /** + * Constructor called from {@link #CREATOR} + */ + protected SavedState(Parcel source) { + super(source); + imagePath = source.readString(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeString(imagePath); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + public interface Helper { + void onSelectImage(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SearchLayout.java b/app/src/main/java/com/hippo/ehviewer/widget/SearchLayout.java index d2c916f6d..6e3010cea 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/SearchLayout.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/SearchLayout.java @@ -16,8 +16,10 @@ package com.hippo.ehviewer.widget; +import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Resources; +import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.IntDef; @@ -39,6 +41,7 @@ import android.widget.TextView; import com.hippo.easyrecyclerview.EasyRecyclerView; import com.hippo.ehviewer.R; import com.hippo.ehviewer.client.data.ListUrlBuilder; +import com.hippo.ehviewer.client.exception.EhException; import com.hippo.rippleold.RippleSalon; import com.hippo.widget.RadioGridGroup; import com.hippo.yorozuya.ViewUtils; @@ -47,9 +50,9 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; public class SearchLayout extends EasyRecyclerView implements CompoundButton.OnCheckedChangeListener, - View.OnClickListener { + View.OnClickListener, ImageSearchLayout.Helper { - @IntDef({SEARCH_MODE_NORMAL, SEARCH_MODE_IMAGE, SEARCH_MODE_SPECIFY}) + @IntDef({SEARCH_MODE_NORMAL, SEARCH_MODE_IMAGE}) @Retention(RetentionPolicy.SOURCE) private @interface SearchMode {} @@ -59,22 +62,19 @@ public class SearchLayout extends EasyRecyclerView implements CompoundButton.OnC public static final int SEARCH_MODE_NORMAL = 0; public static final int SEARCH_MODE_IMAGE = 1; - public static final int SEARCH_MODE_SPECIFY = 2; private static final int ITEM_TYPE_NORMAL = 0; private static final int ITEM_TYPE_NORMAL_ADVANCE = 1; private static final int ITEM_TYPE_IMAGE = 2; - private static final int ITEM_TYPE_SPECIFY = 3; - private static final int ITEM_TYPE_ACTION = 4; + private static final int ITEM_TYPE_ACTION = 3; private static final int[] SEARCH_ITEM_COUNT_ARRAY = { - 3, 2, 2 + 3, 2 }; private static final int[][] SEARCH_ITEM_TYPE = { {ITEM_TYPE_NORMAL, ITEM_TYPE_NORMAL_ADVANCE, ITEM_TYPE_ACTION}, // SEARCH_MODE_NORMAL {ITEM_TYPE_IMAGE, ITEM_TYPE_ACTION}, // SEARCH_MODE_IMAGE - {ITEM_TYPE_SPECIFY, ITEM_TYPE_ACTION} // SEARCH_MODE_SPECIFY }; private LayoutInflater mInflater; @@ -86,13 +86,15 @@ public class SearchLayout extends EasyRecyclerView implements CompoundButton.OnC private CategoryTable mCategoryTable; private RadioGridGroup mNormalSearchMode; private ImageView mNormalSearchModeHelp; - private View mSearchTagHelp; private SwitchCompat mEnableAdvanceSwitch; private View mAdvanceView; private AdvanceSearchTable mTableAdvanceSearch; + private ImageSearchLayout mImageView; + private View mActionView; + private TextView mAction; private LinearLayoutManager mLayoutManager; private SearchAdapter mAdapter; @@ -109,26 +111,23 @@ public class SearchLayout extends EasyRecyclerView implements CompoundButton.OnC init(context); } + @SuppressLint("InflateParams") private void init(Context context) { Resources resources = context.getResources(); mInflater = LayoutInflater.from(context); mLayoutManager = new LinearLayoutManager(context); mAdapter = new SearchAdapter(); - //mAnimator = new SearchItemAnimator(this); setLayoutManager(mLayoutManager); setAdapter(mAdapter); setHasFixedSize(true); setClipToPadding(false); - //setItemAnimator(mAnimator); int paddingH = resources.getDimensionPixelOffset(R.dimen.list_content_margin_h); int paddingV = resources.getDimensionPixelOffset(R.dimen.list_content_margin_v); setPadding(paddingV, paddingH, paddingV, paddingH); - FrameLayout frameLayout = new FrameLayout(context); - // Create normal view - View normalView = mInflater.inflate(R.layout.search_normal, frameLayout, false); + View normalView = mInflater.inflate(R.layout.search_normal, null); mNormalView = normalView; mCategoryTable = (CategoryTable) normalView.findViewById(R.id.search_category_table); mNormalSearchMode = (RadioGridGroup) normalView.findViewById(R.id.normal_search_mode); @@ -140,11 +139,17 @@ public class SearchLayout extends EasyRecyclerView implements CompoundButton.OnC mEnableAdvanceSwitch.setSwitchPadding(resources.getDimensionPixelSize(R.dimen.switch_padding)); // Create advance view - mAdvanceView = mInflater.inflate(R.layout.search_advance, frameLayout, false); + mAdvanceView = mInflater.inflate(R.layout.search_advance, null); mTableAdvanceSearch = (AdvanceSearchTable) mAdvanceView.findViewById(R.id.search_advance_search_table); + // Create image view + mImageView = (ImageSearchLayout) mInflater.inflate(R.layout.search_image, null); + mImageView.setHelper(this); + // Create action view - mActionView = new View(context);// TODO mInflater.inflate(R.layout.search_action, frameLayout, false); + mActionView = mInflater.inflate(R.layout.search_action, null); + mAction = (TextView) mActionView.findViewById(R.id.action); + mAction.setOnClickListener(this); } public void setHelper(Helper helper) { @@ -155,12 +160,24 @@ public class SearchLayout extends EasyRecyclerView implements CompoundButton.OnC mLayoutManager.scrollToPositionWithOffset(0, 0); } + public void setImageUri(Uri imageUri) { + mImageView.setImageUri(imageUri); + } + + @Override + public void onSelectImage() { + if (mHelper != null) { + mHelper.onSelectImage(); + } + } + @Override protected void dispatchSaveInstanceState(@NonNull SparseArray container) { super.dispatchSaveInstanceState(container); mNormalView.saveHierarchyState(container); mAdvanceView.saveHierarchyState(container); + mImageView.saveHierarchyState(container); mActionView.saveHierarchyState(container); } @@ -170,6 +187,7 @@ public class SearchLayout extends EasyRecyclerView implements CompoundButton.OnC mNormalView.restoreHierarchyState(container); mAdvanceView.restoreHierarchyState(container); + mImageView.restoreHierarchyState(container); mActionView.restoreHierarchyState(container); } @@ -214,29 +232,35 @@ public class SearchLayout extends EasyRecyclerView implements CompoundButton.OnC return R.id.search_specify_gallery == mNormalSearchMode.getCheckedRadioButtonId(); } - // TODO image search - public void formatListUrlBuilder(ListUrlBuilder urlBuilder, String query) { + public void formatListUrlBuilder(ListUrlBuilder urlBuilder, String query) throws EhException { urlBuilder.reset(); - int nsMode = mNormalSearchMode.getCheckedRadioButtonId(); - switch (nsMode) { - default: - case R.id.search_normal_search: - urlBuilder.setMode(ListUrlBuilder.MODE_NORMAL); + switch (mSearchMode) { + case SEARCH_MODE_NORMAL: + int nsMode = mNormalSearchMode.getCheckedRadioButtonId(); + switch (nsMode) { + default: + case R.id.search_normal_search: + urlBuilder.setMode(ListUrlBuilder.MODE_NORMAL); + break; + case R.id.search_specify_uploader: + urlBuilder.setMode(ListUrlBuilder.MODE_UPLOADER); + break; + case R.id.search_specify_tag: + urlBuilder.setMode(ListUrlBuilder.MODE_TAG); + break; + } + urlBuilder.setKeyword(query); + urlBuilder.setCategory(mCategoryTable.getCategory()); + if (mEnableAdvance) { + urlBuilder.setAdvanceSearch(mTableAdvanceSearch.getAdvanceSearch()); + urlBuilder.setMinRating(mTableAdvanceSearch.getMinRating()); + } break; - case R.id.search_specify_uploader: - urlBuilder.setMode(ListUrlBuilder.MODE_UPLOADER); + case SEARCH_MODE_IMAGE: + urlBuilder.setMode(ListUrlBuilder.MODE_IMAGE_SEARCH); + mImageView.formatListUrlBuilder(urlBuilder); break; - case R.id.search_specify_tag: - urlBuilder.setMode(ListUrlBuilder.MODE_TAG); - break; - } - - urlBuilder.setKeyword(query); - urlBuilder.setCategory(mCategoryTable.getCategory()); - if (mEnableAdvance) { - urlBuilder.setAdvanceSearch(mTableAdvanceSearch.getAdvanceSearch()); - urlBuilder.setMinRating(mTableAdvanceSearch.getMinRating()); } } @@ -263,7 +287,7 @@ public class SearchLayout extends EasyRecyclerView implements CompoundButton.OnC int oldItemCount = mAdapter.getItemCount(); mSearchMode++; - if (mSearchMode > SEARCH_MODE_SPECIFY) { + if (mSearchMode > SEARCH_MODE_IMAGE) { mSearchMode = SEARCH_MODE_NORMAL; } @@ -272,6 +296,19 @@ public class SearchLayout extends EasyRecyclerView implements CompoundButton.OnC mAdapter.notifyItemRangeRemoved(0, oldItemCount - 1); mAdapter.notifyItemRangeInserted(0, newItemCount - 1); + // Update action text + int resId; + switch (mSearchMode) { + default: + case SEARCH_MODE_NORMAL: + resId = R.string.image_search; + break; + case SEARCH_MODE_IMAGE: + resId = R.string.keyword_search; + break; + } + mAction.setText(resId); + if (mHelper != null) { mHelper.onChangeSearchMode(); } @@ -283,6 +320,8 @@ public class SearchLayout extends EasyRecyclerView implements CompoundButton.OnC new AlertDialog.Builder(getContext()) .setMessage(R.string.search_tip) .show(); + } else if (mAction == v) { + toggleSearchMode(); } } @@ -321,6 +360,17 @@ public class SearchLayout extends EasyRecyclerView implements CompoundButton.OnC mActionView.setLayoutParams(new RecyclerView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + int resId; + switch (mSearchMode) { + default: + case SEARCH_MODE_NORMAL: + resId = R.string.image_search; + break; + case SEARCH_MODE_IMAGE: + resId = R.string.keyword_search; + break; + } + mAction.setText(resId); view = mActionView; } else { view = mInflater.inflate(R.layout.search_category, parent, false); @@ -337,6 +387,11 @@ public class SearchLayout extends EasyRecyclerView implements CompoundButton.OnC ViewUtils.removeFromParent(mAdvanceView); content.addView(mAdvanceView); break; + case ITEM_TYPE_IMAGE: + title.setText(R.string.search_image); + ViewUtils.removeFromParent(mImageView); + content.addView(mImageView); + break; } } @@ -351,5 +406,6 @@ public class SearchLayout extends EasyRecyclerView implements CompoundButton.OnC public interface Helper { void onChangeSearchMode(); + void onSelectImage(); } } diff --git a/app/src/main/java/com/hippo/util/BitmapUtils.java b/app/src/main/java/com/hippo/util/BitmapUtils.java new file mode 100644 index 000000000..43d45e3aa --- /dev/null +++ b/app/src/main/java/com/hippo/util/BitmapUtils.java @@ -0,0 +1,129 @@ +/* + * 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.util; + +import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.support.annotation.NonNull; + +import com.hippo.yorozuya.MathUtils; +import com.hippo.yorozuya.io.InputStreamPipe; + +import java.io.IOException; + +public class BitmapUtils { + + public static Context sContext; + + public static void initialize(Context context) { + sContext = context.getApplicationContext(); + } + + public static long availableMemory() { + final Runtime runtime = Runtime.getRuntime(); + final long used = runtime.totalMemory() - runtime.freeMemory(); + + final ActivityManager activityManager = (ActivityManager) sContext. + getSystemService(Context.ACTIVITY_SERVICE); + final long total = activityManager.getMemoryClass() * 1024 * 1024; + + return total - used; + } + + public static Bitmap decodeStream(@NonNull InputStreamPipe isp, int maxWidth, int maxHeight, + int pixels, boolean checkMemory, boolean justCalc, int[] sampleSize) { + try { + isp.obtain(); + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(isp.open(), null, options); + isp.close(); + + int width = options.outWidth; + int height = options.outHeight; + if (width <= 0 || height <= 0) { + if (sampleSize != null && sampleSize.length >= 1) { + sampleSize[0] = -1; + } + return null; + } + + int scaleW = 1; + int scaleH = 1; + int scaleP = 1; + int scaleM = 1; + if (maxWidth > 0 && width > maxWidth) { + scaleW = MathUtils.ceilDivide(width, maxWidth); + } + if (maxHeight > 0 && height > maxHeight) { + scaleH = MathUtils.ceilDivide(height, maxHeight); + } + if (pixels > 0 && width * height > pixels) { + scaleP = (int) Math.ceil(Math.sqrt(width * height / (float) pixels)); + } + if (checkMemory) { + long m = availableMemory() - 5 * 1024 * 1024; // Leave 5m + if (m < 0) { + if (sampleSize != null && sampleSize.length >= 1) { + sampleSize[0] = -1; + } + return null; + } + if (width * height * 3 > m) { + scaleM = (int) Math.ceil(Math.sqrt(width * height * 3 / (float) m)); + } + } + options.inSampleSize = MathUtils.nextPowerOf2(MathUtils.max(scaleW, scaleH, scaleP, scaleM, 1)); + if (sampleSize != null && sampleSize.length >= 1) { + sampleSize[0] = options.inSampleSize; + } + + options.inJustDecodeBounds = false; + + if (justCalc) { + return null; + } else { + try { + return BitmapFactory.decodeStream(isp.open(), null, options); + } catch (OutOfMemoryError e) { + if (sampleSize != null && sampleSize.length >= 1) { + sampleSize[0] = -1; + } + return null; + } + } + } catch (IOException e) { + if (sampleSize != null && sampleSize.length >= 1) { + sampleSize[0] = -1; + } + return null; + } finally { + isp.close(); + isp.release(); + } + } + + /** + * @return null or the bitmap + */ + public static Bitmap decodeStream(@NonNull InputStreamPipe isp, int maxWidth, int maxHeight) { + return decodeStream(isp, maxWidth, maxHeight, -1, false, false, null); + } +} diff --git a/app/src/main/res/layout/search_action.xml b/app/src/main/res/layout/search_action.xml index dfc859e02..b87a64d1e 100644 --- a/app/src/main/res/layout/search_action.xml +++ b/app/src/main/res/layout/search_action.xml @@ -13,27 +13,26 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - + style="?attr/buttonBarStyle"> - + android:textColor="?attr/colorPrimary" + android:textStyle="bold" + style="?attr/buttonBarPositiveButtonStyle"/> - + android:layout_weight="1"/> - + diff --git a/app/src/main/res/layout/search_advance.xml b/app/src/main/res/layout/search_advance.xml index 82bb9b607..cfe7a0a4c 100644 --- a/app/src/main/res/layout/search_advance.xml +++ b/app/src/main/res/layout/search_advance.xml @@ -13,8 +13,9 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> + + android:layout_height="wrap_content"/> diff --git a/app/src/main/res/layout/search_image.xml b/app/src/main/res/layout/search_image.xml new file mode 100644 index 000000000..acb01206b --- /dev/null +++ b/app/src/main/res/layout/search_image.xml @@ -0,0 +1,22 @@ + + + + diff --git a/app/src/main/res/layout/widget_image_search.xml b/app/src/main/res/layout/widget_image_search.xml new file mode 100644 index 000000000..b3698e63e --- /dev/null +++ b/app/src/main/res/layout/widget_image_search.xml @@ -0,0 +1,86 @@ + + + + + + + +