diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..43c9551aa --- /dev/null +++ b/app/src/debug/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher.png b/app/src/debug/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..0888c280c Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher.png b/app/src/debug/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..6cfdc067c Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..a58924f4f Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..eb519efbc Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..36a86e91d Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/java/com/hippo/ehviewer/AppConfig.java b/app/src/main/java/com/hippo/ehviewer/AppConfig.java new file mode 100644 index 000000000..038ba91c6 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/AppConfig.java @@ -0,0 +1,71 @@ +/* + * 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; + +import android.content.Context; +import android.os.Environment; +import android.support.annotation.Nullable; + +import com.hippo.yorozuya.FileUtils; + +import java.io.File; + +public class AppConfig { + + private static final String APP_DIRNAME = "EhViewer"; + + private static final String SPIDER_INFO = "spider_info"; + private static final String DOWNLOAD = "download"; + + private static Context sContext; + + public static void initialize(Context context) { + sContext = context.getApplicationContext(); + } + + public static File getSpiderInfoCacheDir() { + File dir = new File(sContext.getCacheDir(), SPIDER_INFO); + return FileUtils.ensureDirectory(dir) ? dir : null; + } + + @Nullable + public static File getExternalAppDir() { + if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { + File dir = new File(Environment.getExternalStorageDirectory(), APP_DIRNAME); + return FileUtils.ensureDirectory(dir) ? dir : null; + } + return null; + } + + /** + * mkdirs and get + */ + @Nullable + public static File getDirInAppDir(String filename) { + File appFolder = getExternalAppDir(); + if (appFolder != null) { + File dir = new File(appFolder, filename); + return FileUtils.ensureDirectory(dir) ? dir : null; + } + return null; + } + + @Nullable + public static File getDefaultDownloadDir() { + return getDirInAppDir(DOWNLOAD); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/EhApplication.java b/app/src/main/java/com/hippo/ehviewer/EhApplication.java index 678c6cf7b..38ae696ff 100644 --- a/app/src/main/java/com/hippo/ehviewer/EhApplication.java +++ b/app/src/main/java/com/hippo/ehviewer/EhApplication.java @@ -30,6 +30,7 @@ import com.hippo.ehviewer.client.EhClient; import com.hippo.ehviewer.client.EhCookieStore; import com.hippo.ehviewer.client.data.GalleryDetail; import com.hippo.ehviewer.client.data.LargePreviewSet; +import com.hippo.ehviewer.spider.SpiderDen; import com.hippo.network.StatusCodeException; import com.hippo.okhttp.CookieDB; import com.hippo.scene.SceneApplication; @@ -73,6 +74,8 @@ public class EhApplication extends SceneApplication { Settings.initialize(this); ReadableTime.initialize(this); Html.initialize(this); + AppConfig.initialize(this); + SpiderDen.initialize(this); if (DEBUG_NATIVE_MEMORY) { debugNativeMemory(); diff --git a/app/src/main/java/com/hippo/ehviewer/Settings.java b/app/src/main/java/com/hippo/ehviewer/Settings.java index df7a60f8e..e980b47d6 100644 --- a/app/src/main/java/com/hippo/ehviewer/Settings.java +++ b/app/src/main/java/com/hippo/ehviewer/Settings.java @@ -18,9 +18,13 @@ package com.hippo.ehviewer; import android.content.Context; import android.content.SharedPreferences; +import android.net.Uri; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import com.hippo.ehviewer.client.EhConfig; +import com.hippo.unifile.UniFile; import com.hippo.yorozuya.NumberUtils; public class Settings { @@ -121,4 +125,36 @@ public class Settings { public static boolean getShowJpnTitle() { return getBoolean(KEY_SHOW_JPN_TITLE, DEFAULT_SHOW_JPN_TITLE); } + + public static final String KEY_DOWNLOAD_SAVE_SCHEME = "image_scheme"; + public static final String KEY_DOWNLOAD_SAVE_AUTHORITY = "image_authority"; + public static final String KEY_DOWNLOAD_SAVE_PATH = "image_path"; + public static final String KEY_DOWNLOAD_SAVE_QUERY = "image_query"; + public static final String KEY_DOWNLOAD_SAVE_FRAGMENT = "image_fragment"; + + @Nullable + public static UniFile getDownloadLocation() { + UniFile dir = null; + try { + Uri.Builder builder = new Uri.Builder(); + builder.scheme(getString(KEY_DOWNLOAD_SAVE_SCHEME, null)); + builder.encodedAuthority(getString(KEY_DOWNLOAD_SAVE_AUTHORITY, null)); + builder.encodedPath(getString(KEY_DOWNLOAD_SAVE_PATH, null)); + builder.encodedQuery(getString(KEY_DOWNLOAD_SAVE_QUERY, null)); + builder.encodedFragment(getString(KEY_DOWNLOAD_SAVE_FRAGMENT, null)); + dir = UniFile.fromUri(sContext, builder.build()); + } catch (Exception e) { + // Ignore + } + return dir != null ? dir : UniFile.fromFile(AppConfig.getDefaultDownloadDir()); + } + + public static void putImageSaveLocation(@NonNull UniFile location) { + Uri uri = location.getUri(); + putString(KEY_DOWNLOAD_SAVE_SCHEME, uri.getScheme()); + putString(KEY_DOWNLOAD_SAVE_AUTHORITY, uri.getEncodedAuthority()); + putString(KEY_DOWNLOAD_SAVE_PATH, uri.getEncodedPath()); + putString(KEY_DOWNLOAD_SAVE_QUERY, uri.getEncodedQuery()); + putString(KEY_DOWNLOAD_SAVE_FRAGMENT, uri.getEncodedFragment()); + } } 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 8c6437e75..5bedf388d 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhEngine.java +++ b/app/src/main/java/com/hippo/ehviewer/client/EhEngine.java @@ -39,6 +39,7 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.net.MalformedURLException; import java.util.ArrayList; import java.util.List; @@ -87,7 +88,7 @@ public class EhEngine { } public static Call prepareSignIn(OkHttpClient okHttpClient, - EhConfig ehConfig, String username, String password) { + EhConfig ehConfig, String username, String password) throws MalformedURLException { FormBody.Builder builder = new FormBody.Builder() .add("UserName", username) .add("PassWord", password) @@ -120,7 +121,7 @@ public class EhEngine { } public static Call prepareGetGalleryList(OkHttpClient okHttpClient, - EhConfig ehConfig, String url) { + EhConfig ehConfig, String url) throws MalformedURLException { Log.d(TAG, url); Request request = new EhRequestBuilder(url, ehConfig).build(); return okHttpClient.newCall(request); @@ -232,7 +233,7 @@ public class EhEngine { } public static Call prepareGetGalleryDetail(OkHttpClient okHttpClient, - EhConfig ehConfig, String url) { + EhConfig ehConfig, String url) throws MalformedURLException { Log.d(TAG, url); Request request = new EhRequestBuilder(url, ehConfig).build(); return okHttpClient.newCall(request); @@ -255,7 +256,7 @@ public class EhEngine { } public static Call prepareGetLargePreviewSet(OkHttpClient okHttpClient, - EhConfig ehConfig, String url) { + EhConfig ehConfig, String url) throws MalformedURLException { Log.d(TAG, url); Request request = new EhRequestBuilder(url, ehConfig).build(); return okHttpClient.newCall(request); @@ -279,7 +280,7 @@ public class EhEngine { // rating 0.0 - 0.5 public static Call prepareRateGallery(OkHttpClient okHttpClient, - EhConfig ehConfig, int gid, String token, float rating) throws JSONException { + EhConfig ehConfig, int gid, String token, float rating) throws JSONException, MalformedURLException { final JSONObject json = new JSONObject(); json.put("method", "rategallery"); json.put("apiuid", APIUID); @@ -316,7 +317,7 @@ public class EhEngine { } public static Call prepareCommentGallery(OkHttpClient okHttpClient, - EhConfig ehConfig, String url, String comment) { + EhConfig ehConfig, String url, String comment) throws MalformedURLException { FormBody.Builder builder = new FormBody.Builder() .add("commenttext", comment) .add("postcomment", "Post New"); @@ -354,7 +355,7 @@ public class EhEngine { * } */ public static Call prepareGetGalleryToken(OkHttpClient okHttpClient, - EhConfig ehConfig, int gid, String gtoken, int page) throws JSONException { + EhConfig ehConfig, int gid, String gtoken, int page) throws JSONException, MalformedURLException { JSONObject json = new JSONObject() .put("method", "gtoken") .put("pagelist", new JSONArray().put( diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhRequestBuilder.java b/app/src/main/java/com/hippo/ehviewer/client/EhRequestBuilder.java index ec681f78b..754cc789e 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhRequestBuilder.java +++ b/app/src/main/java/com/hippo/ehviewer/client/EhRequestBuilder.java @@ -19,13 +19,15 @@ package com.hippo.ehviewer.client; import com.hippo.ehviewer.Settings; import com.hippo.okhttp.ChromeRequestBuilder; +import java.net.MalformedURLException; + public class EhRequestBuilder extends ChromeRequestBuilder { - public EhRequestBuilder(String url) { + public EhRequestBuilder(String url) throws MalformedURLException { this(url, Settings.getEhConfig()); } - public EhRequestBuilder(String url, EhConfig ehConfig) { + public EhRequestBuilder(String url, EhConfig ehConfig) throws MalformedURLException { super(url); tag(ehConfig); } 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 8064f0507..436c98c38 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhUrl.java +++ b/app/src/main/java/com/hippo/ehviewer/client/EhUrl.java @@ -46,4 +46,8 @@ public class EhUrl { } return builder.build(); } + + public static String getPageUrl(int gid, int index, String pToken) { + return HOST_EX + "s/" + pToken + '/' + gid + '-' + (index + 1); + } } diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhUrlOpener.java b/app/src/main/java/com/hippo/ehviewer/client/EhUrlOpener.java index 52ad36f20..65698fb68 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhUrlOpener.java +++ b/app/src/main/java/com/hippo/ehviewer/client/EhUrlOpener.java @@ -25,7 +25,7 @@ import android.util.Log; import com.hippo.ehviewer.client.data.ListUrlBuilder; import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser; import com.hippo.ehviewer.client.parser.GalleryListUrlParser; -import com.hippo.ehviewer.client.parser.GalleryUrlParser; +import com.hippo.ehviewer.client.parser.GalleryPageUrlParser; import com.hippo.ehviewer.ui.MainActivity; import com.hippo.ehviewer.ui.scene.GalleryDetailScene; import com.hippo.ehviewer.ui.scene.GalleryListScene; @@ -68,7 +68,7 @@ public class EhUrlOpener { return true; } - GalleryUrlParser.Result result2 = GalleryUrlParser.parse(url); + GalleryPageUrlParser.Result result2 = GalleryPageUrlParser.parse(url); if (result2 != null) { Bundle args = new Bundle(); args.putString(ProgressScene.KEY_ACTION, ProgressScene.ACTION_GALLERY_TOKEN); diff --git a/app/src/main/java/com/hippo/ehviewer/client/data/GalleryDetail.java b/app/src/main/java/com/hippo/ehviewer/client/data/GalleryDetail.java index 9dfcb7070..b399066ed 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/data/GalleryDetail.java +++ b/app/src/main/java/com/hippo/ehviewer/client/data/GalleryDetail.java @@ -17,6 +17,9 @@ package com.hippo.ehviewer.client.data; import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Arrays; public class GalleryDetail extends GalleryInfo { @@ -78,17 +81,29 @@ public class GalleryDetail extends GalleryInfo { this.favoredTimes = in.readInt(); this.isFavored = in.readByte() != 0; this.ratedTimes = in.readInt(); - this.tags = in.createTypedArray(GalleryTagGroup.CREATOR); - this.comments = in.createTypedArray(GalleryComment.CREATOR); + Parcelable[] array = in.readParcelableArray(GalleryTagGroup.class.getClassLoader()); + if (array != null) { + this.tags = Arrays.copyOf(array, array.length, GalleryTagGroup[].class); + } else { + this.tags = null; + } + array = in.readParcelableArray(GalleryComment.class.getClassLoader()); + if (array != null) { + this.comments = Arrays.copyOf(array, array.length, GalleryComment[].class); + } else { + this.comments = null; + } this.previewPages = in.readInt(); this.previewSet = in.readParcelable(LargePreviewSet.class.getClassLoader()); } public static final Creator CREATOR = new Creator() { + @Override public GalleryDetail createFromParcel(Parcel source) { return new GalleryDetail(source); } + @Override public GalleryDetail[] newArray(int size) { return new GalleryDetail[size]; } diff --git a/app/src/main/java/com/hippo/ehviewer/client/data/GalleryTagGroup.java b/app/src/main/java/com/hippo/ehviewer/client/data/GalleryTagGroup.java index c38fd71dd..02f7d247a 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/data/GalleryTagGroup.java +++ b/app/src/main/java/com/hippo/ehviewer/client/data/GalleryTagGroup.java @@ -24,7 +24,7 @@ import java.util.ArrayList; public class GalleryTagGroup implements Parcelable { public String groupName; - private ArrayList mTagList = new ArrayList<>(); + private ArrayList mTagList; public void addTag(String tag) { mTagList.add(tag); @@ -50,6 +50,7 @@ public class GalleryTagGroup implements Parcelable { } public GalleryTagGroup() { + mTagList = new ArrayList<>(); } protected GalleryTagGroup(Parcel in) { @@ -58,10 +59,12 @@ public class GalleryTagGroup implements Parcelable { } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override public GalleryTagGroup createFromParcel(Parcel source) { return new GalleryTagGroup(source); } + @Override public GalleryTagGroup[] newArray(int size) { return new GalleryTagGroup[size]; } diff --git a/app/src/main/java/com/hippo/ehviewer/client/data/LargePreviewSet.java b/app/src/main/java/com/hippo/ehviewer/client/data/LargePreviewSet.java index 6bf14a191..18dbafcdb 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/data/LargePreviewSet.java +++ b/app/src/main/java/com/hippo/ehviewer/client/data/LargePreviewSet.java @@ -25,9 +25,9 @@ import java.util.ArrayList; public class LargePreviewSet implements Parcelable { - private IntList mIndexList = new IntList(); - private ArrayList mImageUrlList = new ArrayList<>(); - private ArrayList mPageUrlList = new ArrayList<>(); + private IntList mIndexList; + private ArrayList mImageUrlList; + private ArrayList mPageUrlList; public int size() { return mImageUrlList.size(); @@ -58,26 +58,30 @@ public class LargePreviewSet implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - this.mIndexList.writeToParcel(dest, flags); - dest.writeParcelable(this.mIndexList, 0); + dest.writeParcelable(this.mIndexList, flags); dest.writeStringList(this.mImageUrlList); dest.writeStringList(this.mPageUrlList); } public LargePreviewSet() { + mIndexList = new IntList(); + mImageUrlList = new ArrayList<>(); + mPageUrlList = new ArrayList<>(); } protected LargePreviewSet(Parcel in) { - this.mIndexList = IntList.CREATOR.createFromParcel(in); + this.mIndexList = in.readParcelable(IntList.class.getClassLoader()); this.mImageUrlList = in.createStringArrayList(); this.mPageUrlList = in.createStringArrayList(); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override public LargePreviewSet createFromParcel(Parcel source) { return new LargePreviewSet(source); } + @Override public LargePreviewSet[] newArray(int size) { return new LargePreviewSet[size]; } diff --git a/app/src/main/java/com/hippo/ehviewer/client/data/NormalPreviewSet.java b/app/src/main/java/com/hippo/ehviewer/client/data/NormalPreviewSet.java new file mode 100644 index 000000000..84fffe7f7 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/client/data/NormalPreviewSet.java @@ -0,0 +1,55 @@ +/* + * 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.client.data; + +import com.hippo.yorozuya.IntList; + +import java.util.ArrayList; + +public class NormalPreviewSet { + + private IntList mIndexList = new IntList(); + private ArrayList mImageUrlList = new ArrayList<>(); + private IntList mXOffsetList = new IntList(); + private IntList mYOffsetList = new IntList(); + private IntList mWidthList = new IntList(); + private IntList mHeightList = new IntList(); + private ArrayList mPageUrlList = new ArrayList<>(); + + public void addItem(int index, String imageUrl, int xOffset, int yOffset, int width, + int height, String pageUrl) { + mIndexList.add(index); + mImageUrlList.add(imageUrl); + mXOffsetList.add(xOffset); + mYOffsetList.add(yOffset); + mWidthList.add(width); + mHeightList.add(height); + mPageUrlList.add(pageUrl); + } + + public int size() { + return mIndexList.size(); + } + + public int getIndexAt(int index) { + return mIndexList.get(index); + } + + public String getPageUrlAt(int index) { + return mPageUrlList.get(index); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/client/exception/Image509Exception.java b/app/src/main/java/com/hippo/ehviewer/client/exception/Image509Exception.java new file mode 100644 index 000000000..495722598 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/client/exception/Image509Exception.java @@ -0,0 +1,24 @@ +/* + * 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.client.exception; + +public class Image509Exception extends EhException { + + public Image509Exception() { + super("509"); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailParser.java b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailParser.java index 1848f30bc..05e19435f 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailParser.java +++ b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailParser.java @@ -16,22 +16,18 @@ package com.hippo.ehviewer.client.parser; -import android.util.Log; - import com.hippo.ehviewer.client.EhUtils; import com.hippo.ehviewer.client.data.GalleryComment; import com.hippo.ehviewer.client.data.GalleryDetail; import com.hippo.ehviewer.client.data.GalleryTagGroup; import com.hippo.ehviewer.client.data.LargePreviewSet; +import com.hippo.ehviewer.client.data.NormalPreviewSet; import com.hippo.ehviewer.client.exception.EhException; import com.hippo.ehviewer.client.exception.OffensiveException; import com.hippo.ehviewer.client.exception.ParseException; import com.hippo.ehviewer.client.exception.PiningException; import com.hippo.yorozuya.NumberUtils; -import java.io.File; -import java.io.FileOutputStream; -import java.io.OutputStream; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; @@ -74,9 +70,11 @@ public class GalleryDetailParser { + ".+?" + "([\\d,]+)" // 18 ratedTimes + "" - + "]*>([^<>]+)" // 19 rating "Average: x.xx" or "Not Yet Rated" - + ".+?" - + "]*>(.+?)", Pattern.DOTALL); // 20 isFavored "Favorite Gallery" for favorite + + "]*>([^<>]+)", // 19 rating "Average: x.xx" or "Not Yet Rated" + Pattern.DOTALL); + + private static final Pattern IS_FAVORED_PATTERN = Pattern.compile("]*>(.+?)"); // isFavored "Favorite Gallery" for favorite + private static final Pattern TAG_GROUP_PATTERN = Pattern.compile("]+>([\\w\\s]+):(?:]+>]+>[\\w\\s]+)+"); private static final Pattern TAG_PATTERN = Pattern.compile("]+>]+>([\\w\\s]+)"); @@ -85,7 +83,7 @@ public class GalleryDetailParser { public static final Pattern PAGES_PATTERN = Pattern.compile("]*>Length:]*>([\\d,]+) pages"); public static final Pattern PREVIEW_PAGES_PATTERN = Pattern.compile("]+>]+>([\\d,]+)]+>(?:]+>)?>(?:)?"); - private static final Pattern NORMAL_PREVIEW_PATTERN = Pattern.compile("]*class=\"gdtm\"[^<>]*>]*width:(\\d+)[^<>]*height:(\\d+)[^<>]*\\((.+?)\\)[^<>]*-(\\d+)px[^<>]*>]*href=\"(.+?)\"[^<>]*>"); + private static final Pattern NORMAL_PREVIEW_PATTERN = Pattern.compile("
]*>]*width:(\\d+)[^<>]*height:(\\d+)[^<>]*\\((.+?)\\)[^<>]*-(\\d+)px[^<>]*>]*href=\"(.+?)\"[^<>]*>\"([\\d,]+)\"");\"([\\d,]+)\".+?src=\"(.+?)\""); list = new LinkedList<>(); @@ -246,18 +244,7 @@ public class GalleryDetailParser { } } - public static LargePreviewSet parsePreview(String body) throws EhException { - return parseLargePreview(body); - /* - if (body.contains("
]+>= size()) { - return RESULT_ERROR; - } else { - synchronized (mRequests) { - mRequests.add(index); - mRequests.notify(); - } - return RESULT_WAIT; + public void request(int index) { + synchronized (mRequests) { + mRequests.add(index); + mRequests.notify(); } } @@ -108,7 +96,6 @@ public class DirGalleryProvider extends GalleryProvider implements Runnable { if (files == null) { mSize = STATE_ERROR; mError = GetText.getString(R.string.error_not_folder_path); - mBgThread = null; // Notify to to show error notifyDataChanged(); @@ -139,6 +126,12 @@ public class DirGalleryProvider extends GalleryProvider implements Runnable { index = mRequests.pop(); } + // Check index valid + if (index < 0 || index >= files.length) { + notifyPageFailed(index, GetText.getString(R.string.error_out_of_range)); + continue; + } + try { InputStream is = new FileInputStream(new File(mDir, files[index])); Image image = Image.decode(is, false); @@ -152,9 +145,6 @@ public class DirGalleryProvider extends GalleryProvider implements Runnable { } } - // Clear - mBgThread = null; - Log.i(TAG, "ImageDecoder end"); } diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/EhGalleryProvider.java b/app/src/main/java/com/hippo/ehviewer/gallery/EhGalleryProvider.java index c5baac195..f24a99a09 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/EhGalleryProvider.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/EhGalleryProvider.java @@ -16,30 +16,107 @@ package com.hippo.ehviewer.gallery; -public class EhGalleryProvider extends GalleryProvider { +import android.content.Context; +import android.support.annotation.Nullable; + +import com.hippo.ehviewer.client.data.GalleryInfo; +import com.hippo.ehviewer.spider.SpiderQueen; +import com.hippo.image.Image; + +public class EhGalleryProvider extends GalleryProvider implements SpiderQueen.OnSpiderListener { + + private Context mContext; + private GalleryInfo mGalleryInfo; + @Nullable + private SpiderQueen mSpiderQueen; + + public EhGalleryProvider(Context context, GalleryInfo galleryInfo) { + mContext = context; + mGalleryInfo = galleryInfo; + } @Override public void start() { + super.start(); + mSpiderQueen = SpiderQueen.obtainSpiderQueen(mContext, mGalleryInfo, SpiderQueen.MODE_READ); + mSpiderQueen.addOnSpiderListener(this); } @Override public void stop() { + super.stop(); + if (mSpiderQueen != null) { + mSpiderQueen.removeOnSpiderListener(this); + SpiderQueen.releaseSpiderQueen(mSpiderQueen, SpiderQueen.MODE_READ); + } } @Override public int size() { - return 0; + if (mSpiderQueen != null) { + return mSpiderQueen.size(); + } else { + return GalleryProvider.STATE_ERROR; + } } @Override - public int request(int index) { - return 0; + public void request(int index) { + if (mSpiderQueen != null) { + Object object = mSpiderQueen.request(index); + if (object instanceof Float) { + notifyPagePercent(index, (Float) object); + } else if (object instanceof String) { + notifyPageFailed(index, (String) object); + } + } } @Override public String getError() { - return null; + if (mSpiderQueen != null) { + return mSpiderQueen.getError(); + } else { + return "Error"; // TODO + } + } + + @Override + public void onGetPages(int pages) { + notifyDataChanged(); + } + + @Override + public void onGet509(int index) { + // TODO + } + + @Override + public void onDownload(int index, long contentLength, long receivedSize, int bytesRead) { + if (contentLength > 0) { + notifyPagePercent(index, (float) receivedSize / contentLength); + } + } + + @Override + public void onSuccess(int index) { + notifyDataChanged(index); + } + + @Override + public void onFailure(int index, String error) { + notifyPageFailed(index, error); + } + + @Override + public void onGetImageSuccess(int index, Image image) { + notifyPageSucceed(index, image); + } + + @Override + public void onGetImageFailure(int index, String error) { + notifyPageFailed(index, error); } } diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/GalleryProvider.java b/app/src/main/java/com/hippo/ehviewer/gallery/GalleryProvider.java index 6bbf4a4c3..97c45720d 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/GalleryProvider.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/GalleryProvider.java @@ -18,28 +18,22 @@ package com.hippo.ehviewer.gallery; import android.os.Handler; import android.support.annotation.IntDef; +import android.support.annotation.UiThread; import com.hippo.image.Image; +import com.hippo.yorozuya.OSUtils; import com.hippo.yorozuya.Pool; import com.hippo.yorozuya.SimpleHandler; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.List; public abstract class GalleryProvider { - @IntDef({RESULT_WAIT, RESULT_ERROR}) - @Retention(RetentionPolicy.SOURCE) - public @interface Result {} - - public static final int RESULT_WAIT = 0; - public static final int RESULT_ERROR = 1; - public static final int STATE_WAIT = -1; public static final int STATE_ERROR = -2; + // With dot public static final String[] SUPPORT_IMAGE_EXTENSIONS = { ".jpg", // Joint Photographic Experts Group ".jpeg", @@ -47,34 +41,46 @@ public abstract class GalleryProvider { ".gif", // Graphics Interchange Format }; - private List mGalleryProviderListeners = new ArrayList<>(2); + private GalleryProviderListener mGalleryProviderListener; private Pool mNotifyTaskPool = new Pool<>(5); private Handler mHandler = SimpleHandler.getInstance(); - public abstract void start(); + private boolean mStarted = false; - public abstract void stop(); + @UiThread + public void start() { + OSUtils.checkMainLoop(); + + if (mStarted) { + throw new IllegalStateException("Can't start it twice"); + } + mStarted = true; + } + + @UiThread + public void stop() { + OSUtils.checkMainLoop(); + } /** * @return {@link #STATE_WAIT} for wait, 0 for empty */ public abstract int size(); - @Result - public abstract int request(int index); + public abstract void request(int index); public abstract String getError(); - public void addGalleryProviderListener(GalleryProviderListener listener) { - mGalleryProviderListeners.add(listener); - } - - public void removeGalleryProviderListener(GalleryProviderListener listener) { - mGalleryProviderListeners.remove(listener); + public void setGalleryProviderListener(GalleryProviderListener listener) { + mGalleryProviderListener = listener; } public void notifyDataChanged() { - notify(NotifyTask.TYPE_DATA_CHANGED, 0, 0.0f, null, null); + notify(NotifyTask.TYPE_DATA_CHANGED, -1, 0.0f, null, null); + } + + public void notifyDataChanged(int index) { + notify(NotifyTask.TYPE_DATA_CHANGED, index, 0.0f, null, null); } public void notifyPagePercent(int index, float percent) { @@ -90,9 +96,14 @@ public abstract class GalleryProvider { } private void notify(@NotifyTask.Type int type, int index, float percent, Image image, String error) { + GalleryProviderListener listener = mGalleryProviderListener; + if (listener == null) { + return; + } + NotifyTask task = mNotifyTaskPool.pop(); if (task == null) { - task = new NotifyTask(mGalleryProviderListeners, mNotifyTaskPool); + task = new NotifyTask(listener, mNotifyTaskPool); } task.setData(type, index, percent, image, error); mHandler.post(task); @@ -109,7 +120,7 @@ public abstract class GalleryProvider { public static final int TYPE_SUCCEED = 2; public static final int TYPE_FAILED = 3; - private List mGalleryProviderListeners; + private GalleryProviderListener mGalleryProviderListener; private Pool mPool; @Type @@ -119,8 +130,8 @@ public abstract class GalleryProvider { private Image mImage; private String mError; - public NotifyTask(List galleryProviderListeners, Pool pool) { - mGalleryProviderListeners = galleryProviderListeners; + public NotifyTask(GalleryProviderListener galleryProviderListener, Pool pool) { + mGalleryProviderListener = galleryProviderListener; mPool = pool; } @@ -136,24 +147,20 @@ public abstract class GalleryProvider { public void run() { switch (mType) { case TYPE_DATA_CHANGED: - for (int i = 0, n = mGalleryProviderListeners.size(); i < n; i++) { - mGalleryProviderListeners.get(i).onDataChanged(); + if (mIndex < 0) { + mGalleryProviderListener.onDataChanged(); + } else { + mGalleryProviderListener.onDataChanged(mIndex); } break; case TYPE_PERCENT: - for (int i = 0, n = mGalleryProviderListeners.size(); i < n; i++) { - mGalleryProviderListeners.get(i).onPagePercent(mIndex, mPercent); - } + mGalleryProviderListener.onPagePercent(mIndex, mPercent); break; case TYPE_SUCCEED: - for (int i = 0, n = mGalleryProviderListeners.size(); i < n; i++) { - mGalleryProviderListeners.get(i).onPageSucceed(mIndex, mImage); - } + mGalleryProviderListener.onPageSucceed(mIndex, mImage); break; case TYPE_FAILED: - for (int i = 0, n = mGalleryProviderListeners.size(); i < n; i++) { - mGalleryProviderListeners.get(i).onPageFailed(mIndex, mError); - } + mGalleryProviderListener.onPageFailed(mIndex, mError); break; } diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/GalleryProviderListener.java b/app/src/main/java/com/hippo/ehviewer/gallery/GalleryProviderListener.java index a2b3cd666..4febe9bf7 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/GalleryProviderListener.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/GalleryProviderListener.java @@ -27,4 +27,6 @@ public interface GalleryProviderListener { void onPageSucceed(int index, Image image); void onPageFailed(int index, String error); + + void onDataChanged(int index); } diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ZipGalleryProvider.java b/app/src/main/java/com/hippo/ehviewer/gallery/ZipGalleryProvider.java index eb6620428..2a84138c2 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/ZipGalleryProvider.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ZipGalleryProvider.java @@ -51,7 +51,6 @@ public class ZipGalleryProvider extends GalleryProvider implements Runnable { private Thread mBgThread; private volatile int mSize = STATE_WAIT; private String mError; - private boolean mStarted = false; public ZipGalleryProvider(File file) { mFile = file; @@ -59,26 +58,21 @@ public class ZipGalleryProvider extends GalleryProvider implements Runnable { @Override public void start() { - if (mStarted) { - throw new IllegalStateException("Can't start it twice"); - } - mStarted = true; + super.start(); - if (mBgThread == null || mBgThread.isInterrupted()) { - mBgThread = new PriorityThread(this, TAG + '-' + sIdGenerator.incrementAndGet(), - Process.THREAD_PRIORITY_BACKGROUND); - mBgThread.start(); - } + mBgThread = new PriorityThread(this, TAG + '-' + sIdGenerator.incrementAndGet(), + Process.THREAD_PRIORITY_BACKGROUND); + mBgThread.start(); } @Override public void stop() { - if (mBgThread == null || mBgThread.isInterrupted()) { - return; - } + super.stop(); - mBgThread.interrupt(); - mBgThread = null; + if (mBgThread != null) { + mBgThread.interrupt(); + mBgThread = null; + } } @Override @@ -87,15 +81,10 @@ public class ZipGalleryProvider extends GalleryProvider implements Runnable { } @Override - public int request(int index) { - if (index < 0 || index >= mSize) { - return RESULT_ERROR; - } else { - synchronized (mRequests) { - mRequests.add(index); - mRequests.notify(); - } - return RESULT_WAIT; + public void request(int index) { + synchronized (mRequests) { + mRequests.add(index); + mRequests.notify(); } } @@ -123,7 +112,6 @@ public class ZipGalleryProvider extends GalleryProvider implements Runnable { if (mError == null) { mError = GetText.getString(R.string.error_unknown); } - mBgThread = null; // Notify to to show error notifyDataChanged(); @@ -163,6 +151,12 @@ public class ZipGalleryProvider extends GalleryProvider implements Runnable { index = mRequests.pop(); } + // Check index valid + if (index < 0 || index >= filenames.size()) { + notifyPageFailed(index, GetText.getString(R.string.error_out_of_range)); + continue; + } + try { ZipEntry zipEntry = zipFile.getEntry(filenames.get(index)); if (zipEntry != null) { @@ -187,7 +181,6 @@ public class ZipGalleryProvider extends GalleryProvider implements Runnable { } catch (IOException e) { // Ignore } - mBgThread = null; Log.i(TAG, "ImageDecoder end"); } diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/gl/PagerLayoutManager.java b/app/src/main/java/com/hippo/ehviewer/gallery/gl/PagerLayoutManager.java index 9c46b0ee5..78b0561fe 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/gl/PagerLayoutManager.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/gl/PagerLayoutManager.java @@ -35,6 +35,7 @@ import com.hippo.yorozuya.MathUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +// TODO Not safe ! Ensure change component in render thread public class PagerLayoutManager extends GalleryView.LayoutManager { @IntDef({MODE_LEFT_TO_RIGHT, MODE_RIGHT_TO_LEFT}) diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/gl/ScrollLayoutManager.java b/app/src/main/java/com/hippo/ehviewer/gallery/gl/ScrollLayoutManager.java index aea2da7dc..3c014c715 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/gl/ScrollLayoutManager.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/gl/ScrollLayoutManager.java @@ -30,6 +30,7 @@ import com.hippo.yorozuya.AssertUtils; import java.util.Iterator; import java.util.LinkedList; +// TODO Not safe ! Ensure change component in render thread public class ScrollLayoutManager extends GalleryView.LayoutManager { private static final String TAG = ScrollLayoutManager.class.getSimpleName(); diff --git a/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.java b/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.java new file mode 100644 index 000000000..fd2e0fd63 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.java @@ -0,0 +1,296 @@ +/* + * 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.spider; + +import android.content.Context; +import android.graphics.BitmapFactory; +import android.support.annotation.Nullable; +import android.webkit.MimeTypeMap; + +import com.hippo.beerbelly.SimpleDiskCache; +import com.hippo.ehviewer.Settings; +import com.hippo.ehviewer.client.EhCacheKeyFactory; +import com.hippo.ehviewer.client.data.GalleryInfo; +import com.hippo.ehviewer.gallery.GalleryProvider; +import com.hippo.io.UniFileInputStreamPipe; +import com.hippo.io.UniFileOutputStreamPipe; +import com.hippo.unifile.UniFile; +import com.hippo.yorozuya.FileUtils; +import com.hippo.yorozuya.IOUtils; +import com.hippo.yorozuya.Utilities; +import com.hippo.yorozuya.io.InputStreamPipe; +import com.hippo.yorozuya.io.OutputStreamPipe; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Locale; + +public final class SpiderDen { + + private UniFile mDownloadDir; + private volatile int mMode = SpiderQueen.MODE_READ; + private final int mGid; + + @Nullable + private static SimpleDiskCache sCache; + + public static void initialize(Context context) { + try { + sCache = new SimpleDiskCache(new File(context.getCacheDir(), "image"), 160 * 1024 * 1024); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public SpiderDen(GalleryInfo galleryInfo) { + mGid = galleryInfo.gid; + UniFile dir = Settings.getDownloadLocation(); + if (dir != null) { + mDownloadDir = dir.subFile(FileUtils.sanitizeFilename(galleryInfo.gid + "-" + galleryInfo.title)); + } + } + + public void setMode(@SpiderQueen.Mode int mode) { + mMode = mode; + + if (mode == SpiderQueen.MODE_DOWNLOAD) { + ensureDownloadDir(); + } + } + + private boolean ensureDownloadDir() { + return mDownloadDir != null && mDownloadDir.ensureDir(); + } + + public UniFile getDownloadDir() { + return mDownloadDir != null && mDownloadDir.isDirectory() ? mDownloadDir : null; + } + + private boolean containInCache(int index) { + if (sCache == null) { + return false; + } + + String key = EhCacheKeyFactory.getImageKey(mGid, index); + return sCache.contain(key); + } + + /** + * @param extension with dot + */ + public static String generateImageFilename(int index, String extension) { + return String.format(Locale.US, "%08d%s", index + 1, extension); + } + + private static UniFile findImageFile(UniFile dir, int index) { + for (String extension : GalleryProvider.SUPPORT_IMAGE_EXTENSIONS) { + String filename = generateImageFilename(index, extension); + UniFile file = dir.findFile(filename); + if (file != null) { + return file; + } + } + return null; + } + + private boolean containInDownloadDir(int index) { + UniFile dir = getDownloadDir(); + if (dir == null) { + return false; + } + + // Find image file in download dir + return findImageFile(dir, index) != null; + } + + /** + * @param extension with dot + */ + private String fixExtension(String extension) { + if (Utilities.contain(GalleryProvider.SUPPORT_IMAGE_EXTENSIONS, extension)) { + return extension; + } else { + return GalleryProvider.SUPPORT_IMAGE_EXTENSIONS[0]; + } + } + + private boolean copyFromCacheToDownloadDir(int index) { + if (sCache == null) { + return false; + } + UniFile dir = getDownloadDir(); + if (dir == null) { + return false; + } + // Find image file in cache + String key = EhCacheKeyFactory.getImageKey(mGid, index); + InputStreamPipe pipe = sCache.getInputStreamPipe(key); + if (pipe == null) { + return false; + } + + OutputStream os = null; + try { + // Get extension + String extension; + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + pipe.obtain(); + BitmapFactory.decodeStream(pipe.open(), null, options); + pipe.close(); + extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(options.outMimeType); + if (extension != null) { + extension = '.' + extension; + } else { + return false; + } + // Fix extension + extension = fixExtension(extension); + // Copy from cache to download dir + UniFile file = dir.createFile(generateImageFilename(index, extension)); + if (file == null) { + return false; + } + os = file.openOutputStream(); + IOUtils.copy(pipe.open(), os); + return true; + } catch (IOException e) { + return false; + } finally { + IOUtils.closeQuietly(os); + pipe.close(); + pipe.release(); + } + } + + public boolean contain(int index) { + if (mMode == SpiderQueen.MODE_READ) { + return containInCache(index) || containInDownloadDir(index); + } else if (mMode == SpiderQueen.MODE_DOWNLOAD) { + return containInDownloadDir(index) || copyFromCacheToDownloadDir(index); + } else { + return false; + } + } + + private void removeFromCache(int index) { + if (sCache == null) { + return; + } + + String key = EhCacheKeyFactory.getImageKey(mGid, index); + sCache.remove(key); + } + + private void removeFromDownloadDir(int index) { + UniFile dir = getDownloadDir(); + if (dir == null) { + return; + } + + for (int i = 0, n = GalleryProvider.SUPPORT_IMAGE_EXTENSIONS.length; i < n; i++) { + String filename = generateImageFilename(index, GalleryProvider.SUPPORT_IMAGE_EXTENSIONS[i]); + UniFile file = dir.subFile(filename); + if (file != null) { + file.delete(); + } + } + } + + public void remove(int index) { + removeFromCache(index); + removeFromDownloadDir(index); + } + + private OutputStreamPipe openCacheOutputStreamPipe(int index) { + if (sCache == null) { + return null; + } + + String key = EhCacheKeyFactory.getImageKey(mGid, index); + return sCache.getOutputStreamPipe(key); + } + + /** + * @param extension without dot + */ + private OutputStreamPipe openDownloadOutputStreamPipe(int index, @Nullable String extension) { + UniFile dir = getDownloadDir(); + if (dir == null) { + return null; + } + + extension = fixExtension('.' + extension); + UniFile file = dir.createFile(generateImageFilename(index, extension)); + if (file != null) { + return new UniFileOutputStreamPipe(file); + } else { + return null; + } + } + + @Nullable + public OutputStreamPipe openOutputStreamPipe(int index, @Nullable String extension) { + if (mMode == SpiderQueen.MODE_READ) { + return openCacheOutputStreamPipe(index); + } else if (mMode == SpiderQueen.MODE_DOWNLOAD) { + return openDownloadOutputStreamPipe(index, extension); + } else { + return null; + } + } + + + private InputStreamPipe openCacheInputStreamPipe(int index) { + if (sCache == null) { + return null; + } + + String key = EhCacheKeyFactory.getImageKey(mGid, index); + return sCache.getInputStreamPipe(key); + } + + public InputStreamPipe openDownloadInputStreamPipe(int index) { + UniFile dir = getDownloadDir(); + if (dir == null) { + return null; + } + + UniFile file = findImageFile(dir, index); + if (file != null) { + return new UniFileInputStreamPipe(file); + } else { + return null; + } + } + + @Nullable + public InputStreamPipe openInputStreamPipe(int index) { + if (mMode == SpiderQueen.MODE_READ) { + InputStreamPipe pipe = openCacheInputStreamPipe(index); + if (pipe == null) { + pipe = openDownloadInputStreamPipe(index); + } + return pipe; + } else if (mMode == SpiderQueen.MODE_DOWNLOAD) { + return openDownloadInputStreamPipe(index); + } else { + return null; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/spider/SpiderInfo.java b/app/src/main/java/com/hippo/ehviewer/spider/SpiderInfo.java new file mode 100644 index 000000000..8f14958f1 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/spider/SpiderInfo.java @@ -0,0 +1,138 @@ +/* + * 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.spider; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; +import android.util.SparseArray; + +import com.hippo.unifile.UniFile; +import com.hippo.yorozuya.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +public class SpiderInfo { + + private static final String TAG = SpiderInfo.class.getSimpleName(); + + static final String TOKEN_FAILED = "failed"; + static final String TOKEN_WAIT = "wait"; + + int currentIndex = 0; + int gid = -1; + String token = null; + int pages = -1; + int previewPages = -1; + int previewPerPage = -1; + SparseArray pTokenMap = null; + + @SuppressWarnings("InfiniteLoopStatement") + public static SpiderInfo readFromUniFile(@Nullable UniFile file) { + if (file == null) { + return null; + } + + InputStream is = null; + SpiderInfo spiderInfo = null; + try { + is = file.openInputStream(); + spiderInfo = new SpiderInfo(); + // Current index + spiderInfo.currentIndex = Integer.parseInt(IOUtils.readAsciiLine(is), 16); + // Gid + spiderInfo.gid = Integer.parseInt(IOUtils.readAsciiLine(is)); + // Token + spiderInfo.token = IOUtils.readAsciiLine(is); + // Deprecated, mode, only ex is support now, MUST be 1 + if (1 != Integer.parseInt(IOUtils.readAsciiLine(is))) { + Log.w(TAG, "Not ex mode"); + return null; + } + // Preview pages + spiderInfo.previewPages = Integer.parseInt(IOUtils.readAsciiLine(is)); + // Preview pre page + spiderInfo.previewPerPage = Integer.parseInt(IOUtils.readAsciiLine(is)); + // Pages + spiderInfo.pages = Integer.parseInt(IOUtils.readAsciiLine(is)); + // PToken + spiderInfo.pTokenMap = new SparseArray<>(spiderInfo.pages); + while (true) { // EOFException will raise + String line = IOUtils.readAsciiLine(is); + int pos = line.indexOf(" "); + if (pos > 0 || pos < line.length() - 1) { + int index = Integer.parseInt(line.substring(0, pos)); + String pToken = line.substring(pos + 1); + spiderInfo.pTokenMap.put(index, pToken); + } else { + Log.e(TAG, "Can't parse index and pToken, index = " + pos); + } + } + } catch (IOException | NumberFormatException e) { + e.printStackTrace(); + } finally { + IOUtils.closeQuietly(is); + } + + if (spiderInfo == null || spiderInfo.gid == -1 || spiderInfo.token == null || + spiderInfo.previewPages == -1 || spiderInfo.previewPerPage == -1 || + spiderInfo.pages == -1 || spiderInfo.pTokenMap == null) { + return null; + } else { + return spiderInfo; + } + } + + public void write(@NonNull OutputStream os) { + OutputStreamWriter writer = null; + try { + writer = new OutputStreamWriter(os); + writer.write(String.format("%08x", currentIndex)); + writer.write("\n"); + writer.write(Integer.toString(gid)); + writer.write("\n"); + writer.write(token); + writer.write("\n"); + writer.write('1'); + writer.write("\n"); + writer.write(Integer.toString(previewPages)); + writer.write("\n"); + writer.write(Integer.toString(previewPerPage)); + writer.write("\n"); + writer.write(Integer.toString(pages)); + writer.write("\n"); + for (int i = 0; i < pTokenMap.size(); i++) { + if (TOKEN_FAILED.equals(pTokenMap.valueAt(i))) { + continue; + } + writer.write(Integer.toString(pTokenMap.keyAt(i))); + writer.write(" "); + writer.write(pTokenMap.valueAt(i)); + writer.write("\n"); + } + writer.flush(); + } catch (IOException e) { + // Ignore + } finally { + IOUtils.closeQuietly(writer); + IOUtils.closeQuietly(os); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java b/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java new file mode 100644 index 000000000..db6077400 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java @@ -0,0 +1,1082 @@ +/* + * 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.spider; + +import android.content.Context; +import android.os.Process; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.util.Log; +import android.util.SparseArray; +import android.webkit.MimeTypeMap; + +import com.hippo.ehviewer.AppConfig; +import com.hippo.ehviewer.EhApplication; +import com.hippo.ehviewer.GetText; +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.Settings; +import com.hippo.ehviewer.client.EhConfig; +import com.hippo.ehviewer.client.EhRequestBuilder; +import com.hippo.ehviewer.client.EhUrl; +import com.hippo.ehviewer.client.data.GalleryInfo; +import com.hippo.ehviewer.client.data.NormalPreviewSet; +import com.hippo.ehviewer.client.exception.Image509Exception; +import com.hippo.ehviewer.client.exception.ParseException; +import com.hippo.ehviewer.client.parser.GalleryDetailParser; +import com.hippo.ehviewer.client.parser.GalleryPageParser; +import com.hippo.ehviewer.client.parser.GalleryPageUrlParser; +import com.hippo.ehviewer.gallery.GalleryProvider; +import com.hippo.image.Image; +import com.hippo.unifile.UniFile; +import com.hippo.yorozuya.IOUtils; +import com.hippo.yorozuya.OSUtils; +import com.hippo.yorozuya.PriorityThread; +import com.hippo.yorozuya.PriorityThreadFactory; +import com.hippo.yorozuya.StringUtils; +import com.hippo.yorozuya.io.InputStreamPipe; +import com.hippo.yorozuya.io.OutputStreamPipe; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.Stack; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReferenceArray; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public class SpiderQueen implements Runnable { + + private static final String TAG = SpiderQueen.class.getSimpleName(); + private static final AtomicInteger sIdGenerator = new AtomicInteger(); + + @IntDef({MODE_READ, MODE_DOWNLOAD}) + @Retention(RetentionPolicy.SOURCE) + public @interface Mode {} + + @IntDef({STATE_NONE, STATE_DOWNLOADING, STATE_FINISHED, STATE_FAILED}) + @Retention(RetentionPolicy.SOURCE) + public @interface State {} + + public static final int MODE_READ = 0; + public static final int MODE_DOWNLOAD = 1; + + public static final int STATE_NONE = 0; + public static final int STATE_DOWNLOADING = 1; + public static final int STATE_FINISHED = 2; + public static final int STATE_FAILED = 3; + + private static final int NUMBER_SPIDER_WORKER = 3; + private static final int NUMBER_PRELOAD = 5; + + private static final String SPIDER_INFO_FILENAME = ".ehviewer"; + + private static final String[] URL_509_SUFFIX_ARRAY = { + "/509.gif", + "/509s.gif" + }; + + private static SparseArray sQueenMap = new SparseArray<>(); + + private final OkHttpClient mHttpClient; + private final GalleryInfo mGalleryInfo; + private volatile SpiderDen mSpiderDen; + + private int mReadReference = 0; + private int mDownloadReference = 0; + @Mode + private int mMode; + + // It mQueenThread is null, failed or stopped + @Nullable + private volatile Thread mQueenThread; + private final Object mQueenLock = new Object(); + private ThreadFactory mThreadFactory = new PriorityThreadFactory( + SpiderWorker.class.getSimpleName(), Process.THREAD_PRIORITY_BACKGROUND); + + @Nullable + private Thread mDecoderThread; + private final Stack mDecodeRequestStack = new Stack<>(); + + private volatile AtomicReferenceArray mWorkers; + private final Object mWorkerLock = new Object(); + + private final Object mPTokenLock = new Object(); + private volatile SpiderInfo mSpiderInfo; + private Queue mRequestPTokenQueue = new LinkedList<>(); + + private final Object mPageStateLock = new Object(); + private volatile int[] mPageStateArray; + + // Store request page. The index may be invalid + private final Queue mRequestPageQueue = new LinkedList<>(); + // Store preload page. The index may be invalid + private final Queue mRequestPageQueue2 = new LinkedList<>(); + // Store force request page. The index may be invalid + private final Queue mForceRequestPageQueue = new LinkedList<>(); + // For download, when it go to mPageStateArray.size(), done + private volatile int mDownloadPage = -1; + + private final Set mRequestPreviewPageSet = new HashSet<>(); + + private AtomicInteger mDownloadedPages = new AtomicInteger(0); + private AtomicInteger mFinishedPages = new AtomicInteger(0); + + // Store page error + private ConcurrentHashMap mPageErrorMap = new ConcurrentHashMap<>(); + // Store page download percent + private ConcurrentHashMap mPagePercentMap = new ConcurrentHashMap<>(); + + private final List mSpiderListeners = new ArrayList<>(); + + private SpiderQueen(EhApplication application, GalleryInfo galleryInfo) { + mHttpClient = EhApplication.getOkHttpClient(application); + mGalleryInfo = galleryInfo; + } + + public void addOnSpiderListener(OnSpiderListener listener) { + synchronized (mSpiderListeners) { + mSpiderListeners.add(listener); + } + } + + public void removeOnSpiderListener(OnSpiderListener listener) { + synchronized (mSpiderListeners) { + mSpiderListeners.remove(listener); + } + } + + private void notifyGetPages(int pages) { + synchronized (mSpiderListeners) { + for (OnSpiderListener listener : mSpiderListeners) { + listener.onGetPages(pages); + } + } + } + + private void notifyGet509(int index) { + synchronized (mSpiderListeners) { + for (OnSpiderListener listener : mSpiderListeners) { + listener.onGet509(index); + } + } + } + + private void notifyDownload(int index, long contentLength, long receivedSize, int bytesRead) { + synchronized (mSpiderListeners) { + for (OnSpiderListener listener : mSpiderListeners) { + listener.onDownload(index, contentLength, receivedSize, bytesRead); + } + } + } + + private void notifySuccess(int index) { + synchronized (mSpiderListeners) { + for (OnSpiderListener listener : mSpiderListeners) { + listener.onSuccess(index); + } + } + } + + private void notifyFailure(int index, String error) { + synchronized (mSpiderListeners) { + for (OnSpiderListener listener : mSpiderListeners) { + listener.onFailure(index, error); + } + } + } + + private void notifyGetImageSuccess(int index, Image image) { + synchronized (mSpiderListeners) { + for (OnSpiderListener listener : mSpiderListeners) { + listener.onGetImageSuccess(index, image); + } + } + } + + private void notifyGetImageFailure(int index, String error) { + synchronized (mSpiderListeners) { + for (OnSpiderListener listener : mSpiderListeners) { + listener.onGetImageFailure(index, error); + } + } + } + + @UiThread + public static SpiderQueen obtainSpiderQueen(@NonNull Context context, + @NonNull GalleryInfo galleryInfo, @Mode int mode) { + OSUtils.checkMainLoop(); + + SpiderQueen queen = sQueenMap.get(galleryInfo.gid); + if (queen == null) { + EhApplication application = (EhApplication) context.getApplicationContext(); + queen = new SpiderQueen(application, galleryInfo); + sQueenMap.put(galleryInfo.gid, queen); + // Set mode + queen.setMode(mode); + queen.start(); + } else { + // Set mode + queen.setMode(mode); + } + return queen; + } + + @UiThread + public static void releaseSpiderQueen(@NonNull SpiderQueen queen, @Mode int mode) { + OSUtils.checkMainLoop(); + + // Clear mode + queen.clearMode(mode); + + if (queen.mReadReference == 0 && queen.mDownloadReference == 0) { + // Stop and remove if there is no reference + queen.stop(); + sQueenMap.remove(queen.mGalleryInfo.gid); + } + } + + private void updateMode() { + if (mDownloadReference > 0) { + mMode = MODE_DOWNLOAD; + } else { + mMode = MODE_READ; + } + + SpiderDen spiderDen = mSpiderDen; + if (spiderDen != null) { + spiderDen.setMode(mMode); + } + + // Update download page + if (mMode == MODE_DOWNLOAD) { + mDownloadPage = 0; + } else { + mDownloadPage = -1; + } + } + + private void setMode(@Mode int mode) { + switch (mode) { + case MODE_READ: + mReadReference++; + break; + case MODE_DOWNLOAD: + mDownloadReference++; + break; + } + + if (mDownloadReference > 1) { + throw new IllegalStateException("mDownloadReference can't more than 0"); + } + + updateMode(); + } + + private void clearMode(@Mode int mode) { + switch (mode) { + case MODE_READ: + mReadReference--; + break; + case MODE_DOWNLOAD: + mDownloadReference--; + break; + } + + if (mReadReference < 0 || mDownloadReference < 0) { + throw new IllegalStateException("Mode reference < 0"); + } + + updateMode(); + } + + private void start() { + Thread queenThread = new PriorityThread(this, TAG + '-' + sIdGenerator.incrementAndGet(), + Process.THREAD_PRIORITY_BACKGROUND); + mQueenThread = queenThread; + queenThread.start(); + } + + private void stop() { + Thread queenThread = mQueenThread; + if (queenThread != null) { + queenThread.interrupt(); + mQueenThread = null; + } + } + + public int size() { + if (mQueenThread == null) { + return GalleryProvider.STATE_ERROR; + } else if (mPageStateArray == null) { + return GalleryProvider.STATE_WAIT; + } else { + return mPageStateArray.length; + } + } + + public String getError() { + // TODO + if (mQueenThread == null) { + return "Error"; + } else { + return null; + } + } + + public Object forceRequest(int index) { + return request(index, true); + } + + public Object request(int index) { + return request(index, false); + } + + /** + * @return + * String for error
+ * Float for download percent
+ * null for wait + */ + private Object request(int index, boolean force) { + if (mQueenThread == null) { + return null; + } + + // Get page state + int state = STATE_NONE; + synchronized (mPageStateLock) { + if (mPageStateArray != null && index >= 0 && index < mPageStateArray.length) { + state = mPageStateArray[index]; + } + } + + // Fix state for force + if (force && (state == STATE_FINISHED || state == STATE_FAILED)) { + state = STATE_NONE; + } + + switch (state) { + default: + case STATE_NONE: + // Add to request queue + synchronized (mRequestPageQueue) { + if (force) { + mForceRequestPageQueue.add(index); + } else { + mRequestPageQueue.add(index); + mRequestPageQueue2.clear(); + int[] pageStateArray = mPageStateArray; + int size; + if (pageStateArray != null) { + size = pageStateArray.length; + } else { + size = Integer.MAX_VALUE; + } + for (int i = index + 1, n = index + i + NUMBER_PRELOAD; i < n && i < size; i++) { + mRequestPageQueue2.add(i); + } + } + } + // Only ensure workers when get pages + if (mPageStateArray != null) { + ensureWorkers(); + } + return null; + case STATE_DOWNLOADING: + return mPagePercentMap.get(index); + case STATE_FAILED: + String error = mPageErrorMap.get(index); + if (error == null) { + error = GetText.getString(R.string.error_unknown); + } + return error; + case STATE_FINISHED: + synchronized (mDecodeRequestStack) { + mDecodeRequestStack.add(index); + mDecodeRequestStack.notify(); + } + return null; + } + } + + private void ensureWorkers() { + synchronized (mWorkerLock) { + if (mWorkers == null) { + mWorkers = new AtomicReferenceArray<>(NUMBER_SPIDER_WORKER); + } + + for (int i = 0, n = mWorkers.length(); i < n; i++) { + if (mWorkers.get(i) == null) { + SpiderWorker worker = new SpiderWorker(i); + Thread thread = mThreadFactory.newThread(worker); + mWorkers.lazySet(i, thread); + thread.start(); + } + } + } + } + + private SpiderInfo readSpiderInfoFromLocal() { + // Read from download dir + UniFile downloadDir = mSpiderDen.getDownloadDir(); + if (downloadDir != null) { + UniFile file = downloadDir.findFile(SPIDER_INFO_FILENAME); + + Log.d("TAG", "file = " + file); + + SpiderInfo spiderInfo = SpiderInfo.readFromUniFile(file); + if (spiderInfo != null && spiderInfo.gid == mGalleryInfo.gid && + spiderInfo.token.equals(mGalleryInfo.token)) { + return spiderInfo; + } + } + + // Read from cache + File dir = AppConfig.getSpiderInfoCacheDir(); + if (dir != null) { + UniFile file = UniFile.fromFile(new File(dir, Integer.toString(mGalleryInfo.gid))); + SpiderInfo spiderInfo = SpiderInfo.readFromUniFile(file); + if (spiderInfo != null && spiderInfo.gid == mGalleryInfo.gid && + spiderInfo.token.equals(mGalleryInfo.token)) { + return spiderInfo; + } + } + + return null; + } + + private SpiderInfo readSpiderInfoFromInternet(EhConfig config) { + try { + SpiderInfo spiderInfo = new SpiderInfo(); + spiderInfo.gid = mGalleryInfo.gid; + spiderInfo.token = mGalleryInfo.token; + + Request request = new EhRequestBuilder(EhUrl.getGalleryDetailUrl( + mGalleryInfo.gid, mGalleryInfo.token, 0, false), config).build(); + Response response = mHttpClient.newCall(request).execute(); + String body = response.body().string(); + + spiderInfo.pages = GalleryDetailParser.parsePages(body); + spiderInfo.previewPages = GalleryDetailParser.parsePreviewPages(body); + NormalPreviewSet previewSet = GalleryDetailParser.parseNormalPreviewSet(body); + spiderInfo.previewPerPage = previewSet.size(); + spiderInfo.pTokenMap = new SparseArray<>(spiderInfo.pages); + for (int i = 0, n = previewSet.size(); i < n; i++) { + GalleryPageUrlParser.Result result = GalleryPageUrlParser.parse(previewSet.getPageUrlAt(i)); + if (result != null) { + spiderInfo.pTokenMap.put(result.page, result.pToken); + } + } + + return spiderInfo; + } catch (Exception e) { + return null; + } + } + + private String getPTokenFromInternet(int index, EhConfig config) { + // Check previewIndex + int previewIndex = index / mSpiderInfo.previewPerPage; + synchronized (mRequestPreviewPageSet) { + if (mRequestPreviewPageSet.contains(previewIndex)) { + return SpiderInfo.TOKEN_WAIT; + } + mRequestPreviewPageSet.add(previewIndex); + } + + try { + String url = EhUrl.getGalleryDetailUrl( + mGalleryInfo.gid, mGalleryInfo.token, previewIndex, false); + Log.d(TAG, url); + Request request = new EhRequestBuilder(url, config).build(); + Response response = mHttpClient.newCall(request).execute(); + String body = response.body().string(); + NormalPreviewSet previewSet = GalleryDetailParser.parseNormalPreviewSet(body); + synchronized (mPTokenLock) { + for (int i = 0, n = previewSet.size(); i < n; i++) { + GalleryPageUrlParser.Result result = GalleryPageUrlParser.parse(previewSet.getPageUrlAt(i)); + if (result != null) { + mSpiderInfo.pTokenMap.put(result.page, result.pToken); + } + } + writeSpiderInfoToLocal(mSpiderInfo); + } + return mSpiderInfo.pTokenMap.get(index); + } catch (Exception e) { + return null; + } finally { + synchronized (mRequestPreviewPageSet) { + mRequestPreviewPageSet.remove(previewIndex); + } + } + } + + private void writeSpiderInfoToLocal(@NonNull SpiderInfo spiderInfo) { + // Write to download dir + UniFile downloadDir = mSpiderDen.getDownloadDir(); + if (downloadDir != null) { + UniFile file = downloadDir.createFile(SPIDER_INFO_FILENAME); + try { + spiderInfo.write(file.openOutputStream()); + } catch (Exception e) { + // Ignore + } + } + + // Read from cache + File dir = AppConfig.getSpiderInfoCacheDir(); + if (dir != null) { + try { + spiderInfo.write(new FileOutputStream(new File(dir, Integer.toString(mGalleryInfo.gid)))); + } catch (Exception e) { + // Ignore + } + } + } + + private void runInternal() { + mSpiderDen = new SpiderDen(mGalleryInfo); + mSpiderDen.setMode(mMode); + + // Get EhConfig + EhConfig config = Settings.getEhConfig().clone(); + config.previewSize = EhConfig.PREVIEW_SIZE_NORMAL; + config.setDirty(); + + // Read spider info + SpiderInfo spiderInfo = readSpiderInfoFromLocal(); + + // Check interrupted + if (Thread.currentThread().isInterrupted()) { + return; + } + + // Spider info from internet + if (spiderInfo == null) { + spiderInfo = readSpiderInfoFromInternet(config); + } + + // Error! Can't get spiderInfo + if (spiderInfo == null) { + return; + } + mSpiderInfo = spiderInfo; + + // Check interrupted + if (Thread.currentThread().isInterrupted()) { + return; + } + + // Write spider info to file + writeSpiderInfoToLocal(spiderInfo); + + // Check interrupted + if (Thread.currentThread().isInterrupted()) { + return; + } + + // Setup page state + synchronized (mPageStateLock) { + mPageStateArray = new int[spiderInfo.pages]; + } + + // Notify get pages + notifyGetPages(spiderInfo.pages); + + // Ensure worker + boolean startWorkers = false; + synchronized (mRequestPageQueue) { + if (!mForceRequestPageQueue.isEmpty() || !mRequestPageQueue.isEmpty() || !mRequestPageQueue2.isEmpty() || + mDownloadPage >= 0 && mDownloadPage < mPageStateArray.length) { + startWorkers = true; + } + } + if (startWorkers) { + ensureWorkers(); + } + + // Start spider decoder + Thread decoderThread = new PriorityThread(new SpiderDecoder(), "SpiderDecoder-" + sIdGenerator.incrementAndGet(), + Process.THREAD_PRIORITY_BACKGROUND); + mDecoderThread = decoderThread; + decoderThread.start(); + + // handle pToken request + while (!Thread.currentThread().isInterrupted()) { + Integer index; + synchronized (mPTokenLock) { + index = mRequestPTokenQueue.poll(); + } + + if (index == null) { + // No request index, wait here + synchronized (mQueenLock) { + try { + mQueenLock.wait(); + } catch (InterruptedException e) { + break; + } + } + continue; + } + + // Check it in spider info + String pToken; + synchronized (mPTokenLock) { + pToken = spiderInfo.pTokenMap.get(index); + } + if (pToken != null) { + // Get pToken from spider info, notify worker + synchronized (mWorkerLock) { + mWorkerLock.notifyAll(); + } + continue; + } + + // Get pToken from internet + pToken = getPTokenFromInternet(index, config); + + if (null == pToken || SpiderInfo.TOKEN_WAIT.equals(pToken)) { + // If failed, set the pToken "failed" + synchronized (mPTokenLock) { + mSpiderInfo.pTokenMap.put(index, SpiderInfo.TOKEN_FAILED); + } + } + + // Notify worker + if (!SpiderInfo.TOKEN_WAIT.equals(pToken)) { + synchronized (mWorkerLock) { + mWorkerLock.notifyAll(); + } + } + } + } + + @Override + public void run() { + Log.i(TAG, Thread.currentThread().getName() + ": start"); + + runInternal(); + + // Set mQueenThread null + mQueenThread = null; + + // Interrupt decoder + Thread decoderThread = mDecoderThread; + if (decoderThread != null) { + decoderThread.interrupt(); + } + + // Interrupt all workers + synchronized (mWorkerLock) { + if (mWorkers != null) { + for (int i = 0, n = mWorkers.length(); i < n; i++) { + Thread thread = mWorkers.get(i); + if (thread != null) { + thread.interrupt(); + mWorkers.set(i, null); + } + } + mWorkers = null; + } + } + + Log.i(TAG, Thread.currentThread().getName() + ": end"); + } + + private class SpiderWorker implements Runnable { + + private int mWorkerIndex; + private int mGid; + + public SpiderWorker(int workerIndex) { + mWorkerIndex = workerIndex; + mGid = mGalleryInfo.gid; + } + + private void updatePageState(int index, @State int state) { + updatePageState(index, state, null); + } + + private void updatePageState(int index, @State int state, String error) { + int oldState; + synchronized (mPageStateLock) { + oldState = mPageStateArray[index]; + mPageStateArray[index] = state; + } + + if (oldState == STATE_NONE && state != STATE_NONE) { + mDownloadedPages.incrementAndGet(); + } else if (oldState != STATE_NONE && state == STATE_NONE) { + mDownloadedPages.decrementAndGet(); + } + if (oldState != STATE_FINISHED && state == STATE_FINISHED) { + mFinishedPages.incrementAndGet(); + } else if (oldState == STATE_FINISHED && state != STATE_FINISHED) { + mFinishedPages.decrementAndGet(); + } + + // Clear + if (state == STATE_DOWNLOADING) { + mPageErrorMap.remove(index); + } else if (state == STATE_FINISHED || state == STATE_FAILED) { + mPagePercentMap.remove(index); + } + + // Notify listeners + if (state == STATE_FAILED) { + if (error == null) { + error = GetText.getString(R.string.error_unknown); + } + mPageErrorMap.put(index, error); + notifyFailure(index, error); + } else if (state == STATE_FINISHED) { + notifySuccess(index); + } + } + + private GalleryPageParser.Result getImageUrl(int gid, int index, String pToken, + String skipHathKey) throws IOException, ParseException, Image509Exception { + String url = EhUrl.getPageUrl(gid, index, pToken); + Log.d(TAG, url); + if (skipHathKey != null) { + url = url + "?nl=" + skipHathKey; + } + + Response response = mHttpClient.newCall(new EhRequestBuilder(url).build()).execute(); + String body = response.body().string(); + GalleryPageParser.Result result = GalleryPageParser.parse(body); + if (StringUtils.endsWith(result.imageUrl, URL_509_SUFFIX_ARRAY)) { + // Get 509 + // Notify listeners + notifyGet509(index); + throw new Image509Exception(); + } + + return result; + } + + // false for stop + private boolean downloadImage(int gid, int index, String pToken) { + String skipHathKey = null; + String imageUrl; + String error = null; + boolean interrupt = false; + + // Try twice + for (int i = 0; i < 2; i++) { + GalleryPageParser.Result result = null; + try { + result = getImageUrl(gid, index, pToken, skipHathKey); + } catch (MalformedURLException e) { + error = GetText.getString(R.string.error_invalid_url); + } catch (IOException e) { + error = GetText.getString(R.string.error_socket); + } catch (ParseException e) { + error = GetText.getString(R.string.error_parse_error); + } catch (Image509Exception e) { + error = GetText.getString(R.string.error_509); + } + if (result == null) { + // Get image url failed + break; + } + // Check interrupted + if (Thread.currentThread().isInterrupted()) { + interrupt = true; + break; + } + + imageUrl = result.imageUrl; + skipHathKey = result.skipHathKey; + + Log.d(TAG, imageUrl); + + // Start download image + OutputStreamPipe pipe = mSpiderDen.openOutputStreamPipe( + index, MimeTypeMap.getFileExtensionFromUrl(imageUrl)); + if (pipe == null) { + // Can't get pipe + error = GetText.getString(R.string.error_write_failed); + break; + } + + // Download image + InputStream is = null; + try { + Response response = mHttpClient.newCall(new EhRequestBuilder(imageUrl).build()).execute(); + ResponseBody responseBody = response.body(); + long contentLength = responseBody.contentLength(); + is = responseBody.byteStream(); + pipe.obtain(); + OutputStream os = pipe.open(); + + final byte data[] = new byte[1024 * 4]; + long receivedSize = 0; + + while (!Thread.currentThread().isInterrupted()) { + int bytesRead = is.read(data); + if (bytesRead == -1) { + break; + } + os.write(data, 0, bytesRead); + receivedSize += bytesRead; + // Update page percent + if (contentLength > 0) { + mPagePercentMap.put(index, (float) receivedSize / contentLength); + } + // Notify listener + notifyDownload(index, contentLength, receivedSize, bytesRead); + } + os.flush(); + + // Check interrupted, download may be stopped by interrupting + if (Thread.currentThread().isInterrupted()) { + interrupt = true; + mSpiderDen.remove(index); + break; + } + + // Download finished + updatePageState(index, STATE_FINISHED); + return true; + } catch (IOException e) { + error = GetText.getString(R.string.error_socket); + } finally { + IOUtils.closeQuietly(is); + pipe.close(); + pipe.release(); + } + } + + updatePageState(index, STATE_FAILED, error); + return !interrupt; + } + + // false for stop + private boolean runInternal() { + int size = mPageStateArray.length; + + // Get request index + int index; + // From force request + boolean force = false; + synchronized (mRequestPageQueue) { + if (!mForceRequestPageQueue.isEmpty()) { + index = mForceRequestPageQueue.remove(); + force = true; + } else if (!mRequestPageQueue.isEmpty()) { + index = mRequestPageQueue.remove(); + } else if (!mRequestPageQueue2.isEmpty()) { + index = mRequestPageQueue2.remove(); + } else if (mDownloadPage >= 0 && mDownloadPage < size) { + index = mDownloadPage; + mDownloadPage++; + } else { + // No index any more, stop + return false; + } + + // Check out of range + if (index < 0 || index >= size) { + // Invalid index + return true; + } + } + + synchronized (mPageStateLock) { + // Check the page state + int state = mPageStateArray[index]; + if (state == STATE_DOWNLOADING || (!force && (state == STATE_FINISHED || state == STATE_FAILED))) { + return true; + } + + // Set state downloading + updatePageState(index, STATE_DOWNLOADING); + } + + // Check exist for not force request + if (!force && mSpiderDen.contain(index)) { + updatePageState(index , STATE_FINISHED); + return true; + } + + // Clear TOKEN_FAILED for force request + if (force) { + synchronized (mPTokenLock) { + int i = mSpiderInfo.pTokenMap.indexOfKey(index); + String pToken = mSpiderInfo.pTokenMap.valueAt(i); + if (SpiderInfo.TOKEN_FAILED.equals(pToken)) { + mSpiderInfo.pTokenMap.setValueAt(i, null); + } + } + } + + String pToken = null; + // Get token + while (!Thread.currentThread().isInterrupted()) { + synchronized (mPTokenLock) { + pToken = mSpiderInfo.pTokenMap.get(index); + } + if (pToken == null) { + mRequestPTokenQueue.add(index); + // Notify Queen + synchronized (mQueenLock) { + mQueenLock.notify(); + } + // Wait + synchronized (mWorkerLock) { + try { + mWorkerLock.wait(); + } catch (InterruptedException e) { + // Interrupted + break; + } + } + } else { + break; + } + } + + if (pToken == null) { + // Interrupted + // Get token failed + updatePageState(index, STATE_FAILED, null); + return false; + } + + if (SpiderInfo.TOKEN_FAILED.equals(pToken)) { + // Get token failed + updatePageState(index, STATE_FAILED, GetText.getString(R.string.error_get_ptoken_error)); + return true; + } + + // Get image url + return downloadImage(mGid, index, pToken); + } + + @Override + @SuppressWarnings("StatementWithEmptyBody") + public void run() { + Log.i(TAG, Thread.currentThread().getName() + ": start"); + + while (!Thread.currentThread().isInterrupted() && runInternal()); + + // Clear in spider worker array + synchronized (mWorkerLock) { + if (mWorkers != null && mWorkerIndex < mWorkers.length() && + mWorkers.get(mWorkerIndex) == Thread.currentThread()) { + mWorkers.lazySet(mWorkerIndex, null); + } + } + + Log.i(TAG, Thread.currentThread().getName() + ": end"); + } + } + + private class SpiderDecoder implements Runnable { + + @Override + public void run() { + Log.i(TAG, Thread.currentThread().getName() + ": start"); + + while (!Thread.currentThread().isInterrupted()) { + int index; + synchronized (mDecodeRequestStack) { + if (mDecodeRequestStack.isEmpty()) { + try { + mDecodeRequestStack.wait(); + } catch (InterruptedException e) { + // Interrupted + break; + } + continue; + } + index = mDecodeRequestStack.pop(); + } + + // Check index valid + if (index < 0 || index >= mPageStateArray.length) { + notifyGetImageFailure(index, GetText.getString(R.string.error_out_of_range)); + continue; + } + + InputStreamPipe pipe = mSpiderDen.openInputStreamPipe(index); + if (pipe == null) { + notifyGetImageFailure(index, GetText.getString(R.string.error_not_found)); + continue; + } + + try { + pipe.obtain(); + Image image = Image.decode(pipe.open(), false); + if (image != null) { + notifyGetImageSuccess(index, image); + } else { + notifyGetImageFailure(index, GetText.getString(R.string.error_decoding_failed)); + } + } catch (IOException e) { + notifyGetImageFailure(index, GetText.getString(R.string.error_reading_failed)); + } finally { + pipe.close(); + pipe.release(); + } + } + + Log.i(TAG, Thread.currentThread().getName() + ": end"); + } + } + + public interface OnSpiderListener { + + void onGetPages(int pages); + + void onGet509(int index); + + /** + * @param contentLength -1 for unknown + */ + void onDownload(int index, long contentLength, long receivedSize, int bytesRead); + + void onSuccess(int index); + + void onFailure(int index, String error); + + void onGetImageSuccess(int index, Image image); + + void onGetImageFailure(int index, String error); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java index 1464c7164..65344665d 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java @@ -26,7 +26,9 @@ import android.util.SparseIntArray; import android.view.KeyEvent; import com.hippo.ehviewer.R; +import com.hippo.ehviewer.client.data.GalleryInfo; import com.hippo.ehviewer.gallery.DirGalleryProvider; +import com.hippo.ehviewer.gallery.EhGalleryProvider; import com.hippo.ehviewer.gallery.GalleryProvider; import com.hippo.ehviewer.gallery.GalleryProviderListener; import com.hippo.ehviewer.gallery.ZipGalleryProvider; @@ -49,13 +51,11 @@ public class GalleryActivity extends AppCompatActivity implements GalleryProvide public static final String KEY_ACTION = "action"; public static final String KEY_FILENAME = "filename"; - public static final String KEY_GID = "gid"; - public static final String KEY_TOKEN = "token"; + public static final String KEY_GALLERY_INFO = "gallery_info"; private String mAction; private String mFilename; - private int mGid; - private String mToken; + private GalleryInfo mGalleryInfo; @Nullable private GalleryView mGalleryView; @@ -83,7 +83,9 @@ public class GalleryActivity extends AppCompatActivity implements GalleryProvide mGalleryProvider = new ZipGalleryProvider(new File(mFilename)); } } else if (ACTION_EH.equals(mAction)) { - + if (mGalleryInfo != null) { + mGalleryProvider = new EhGalleryProvider(this, mGalleryInfo); + } } } @@ -95,16 +97,14 @@ public class GalleryActivity extends AppCompatActivity implements GalleryProvide mAction = intent.getAction(); mFilename = intent.getStringExtra(KEY_FILENAME); - mGid = intent.getIntExtra(KEY_GID, -1); - mToken = intent.getStringExtra(KEY_TOKEN); + mGalleryInfo = intent.getParcelableExtra(KEY_GALLERY_INFO); buildProvider(); } private void onRestore(@NonNull Bundle savedInstanceState) { mAction = savedInstanceState.getString(KEY_ACTION); mFilename = savedInstanceState.getString(KEY_FILENAME); - mGid = savedInstanceState.getInt(KEY_GID, -1); - mToken = savedInstanceState.getString(KEY_TOKEN); + mGalleryInfo = savedInstanceState.getParcelable(KEY_GALLERY_INFO); buildProvider(); } @@ -113,8 +113,9 @@ public class GalleryActivity extends AppCompatActivity implements GalleryProvide super.onSaveInstanceState(outState); outState.putString(KEY_ACTION, mAction); outState.putString(KEY_FILENAME, mFilename); - outState.putInt(KEY_GID, mGid); - outState.putString(KEY_TOKEN, mToken); + if (mGalleryInfo != null) { + outState.putParcelable(KEY_GALLERY_INFO, mGalleryInfo); + } } @Override @@ -131,7 +132,7 @@ public class GalleryActivity extends AppCompatActivity implements GalleryProvide finish(); return; } - mGalleryProvider.addGalleryProviderListener(this); + mGalleryProvider.setGalleryProviderListener(this); mGalleryProvider.start(); setContentView(R.layout.activity_gallery); @@ -161,7 +162,7 @@ public class GalleryActivity extends AppCompatActivity implements GalleryProvide mPageTextTexture = null; } if (mGalleryProvider != null) { - mGalleryProvider.removeGalleryProviderListener(this); + mGalleryProvider.setGalleryProviderListener(null); mGalleryProvider.stop(); mGalleryProvider = null; } @@ -299,6 +300,14 @@ public class GalleryActivity extends AppCompatActivity implements GalleryProvide } } + @Override + public void onDataChanged(int index) { + GalleryPageView page = findPageByIndex(index); + if (page != null && mGalleryProvider != null) { + mGalleryProvider.request(index); + } + } + private class GalleryPageIterator extends GalleryView.PageIterator { private IntIdGenerator mIdGenerator = new IntIdGenerator(); diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java index 953cfe5d7..00316aa19 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java @@ -871,8 +871,18 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene } else if (mDownload == v) { // TODO } else if (mRead == v) { - Intent intent = new Intent(getActivity(), GalleryActivity.class); - startActivity(intent); + GalleryInfo galleryInfo = null; + if (mGalleryInfo != null) { + galleryInfo = mGalleryInfo; + } else if (mGalleryDetail != null) { + galleryInfo = mGalleryDetail; + } + if (galleryInfo != null) { + Intent intent = new Intent(getActivity(), GalleryActivity.class); + intent.setAction(GalleryActivity.ACTION_EH); + intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, galleryInfo); + startActivity(intent); + } } else if (mInfo == v) { Bundle args = new Bundle(); args.putParcelable(GalleryInfoScene.KEY_GALLERY_DETAIL, mGalleryDetail); @@ -929,11 +939,12 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene @Override public void onBackPressed() { - if (mViewTransition.getShownViewIndex() == 0 && ApiHelper.SUPPORT_TRANSITION) { + if (mViewTransition.getShownViewIndex() == 0 && ApiHelper.SUPPORT_TRANSITION && mThumb.isShown()) { int[] location = new int[2]; mThumb.getLocationInWindow(location); // Only show transaction when thumb can be seen if (location[1] + mThumb.getHeight() > 0) { + setTransitionName(); finish(new EnterGalleryListTransaction(mThumb, mTitle, mUploader, mCategory)); return; } diff --git a/app/src/main/java/com/hippo/io/FileInputStreamPipe.java b/app/src/main/java/com/hippo/io/FileInputStreamPipe.java new file mode 100644 index 000000000..140b133ba --- /dev/null +++ b/app/src/main/java/com/hippo/io/FileInputStreamPipe.java @@ -0,0 +1,64 @@ +/* + * 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.io; + +import android.support.annotation.NonNull; + +import com.hippo.yorozuya.IOUtils; +import com.hippo.yorozuya.io.InputStreamPipe; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class FileInputStreamPipe implements InputStreamPipe { + + private File mFile; + private InputStream mIs; + + public FileInputStreamPipe(File file) { + mFile = file; + } + + @Override + public void obtain() { + // Empty + } + + @Override + public void release() { + // Empty + } + + @NonNull + @Override + public InputStream open() throws IOException { + if (mIs != null) { + throw new IllegalStateException("Please close it first"); + } + + mIs = new FileInputStream(mFile); + return mIs; + } + + @Override + public void close() { + IOUtils.closeQuietly(mIs); + mIs = null; + } +} diff --git a/app/src/main/java/com/hippo/io/FileOutputStreamPipe.java b/app/src/main/java/com/hippo/io/FileOutputStreamPipe.java new file mode 100644 index 000000000..498bcb20e --- /dev/null +++ b/app/src/main/java/com/hippo/io/FileOutputStreamPipe.java @@ -0,0 +1,64 @@ +/* + * 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.io; + +import android.support.annotation.NonNull; + +import com.hippo.yorozuya.IOUtils; +import com.hippo.yorozuya.io.OutputStreamPipe; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +public class FileOutputStreamPipe implements OutputStreamPipe { + + private File mFile; + private OutputStream mOs; + + public FileOutputStreamPipe(File file) { + mFile = file; + } + + @Override + public void obtain() { + // Empty + } + + @Override + public void release() { + // Empty + } + + @NonNull + @Override + public OutputStream open() throws IOException { + if (mOs != null) { + throw new IllegalStateException("Please close it first"); + } + + mOs = new FileOutputStream(mFile); + return mOs; + } + + @Override + public void close() { + IOUtils.closeQuietly(mOs); + mOs = null; + } +} diff --git a/app/src/main/java/com/hippo/io/UniFileInputStreamPipe.java b/app/src/main/java/com/hippo/io/UniFileInputStreamPipe.java new file mode 100644 index 000000000..b43616c71 --- /dev/null +++ b/app/src/main/java/com/hippo/io/UniFileInputStreamPipe.java @@ -0,0 +1,63 @@ +/* + * 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.io; + +import android.support.annotation.NonNull; + +import com.hippo.unifile.UniFile; +import com.hippo.yorozuya.IOUtils; +import com.hippo.yorozuya.io.InputStreamPipe; + +import java.io.IOException; +import java.io.InputStream; + +public class UniFileInputStreamPipe implements InputStreamPipe { + + private UniFile mFile; + private InputStream mIs; + + public UniFileInputStreamPipe(UniFile file) { + mFile = file; + } + + @Override + public void obtain() { + // Empty + } + + @Override + public void release() { + // Empty + } + + @NonNull + @Override + public InputStream open() throws IOException { + if (mIs != null) { + throw new IllegalStateException("Please close it first"); + } + + mIs = mFile.openInputStream(); + return mIs; + } + + @Override + public void close() { + IOUtils.closeQuietly(mIs); + mIs = null; + } +} diff --git a/app/src/main/java/com/hippo/io/UniFileOutputStreamPipe.java b/app/src/main/java/com/hippo/io/UniFileOutputStreamPipe.java new file mode 100644 index 000000000..3487b215e --- /dev/null +++ b/app/src/main/java/com/hippo/io/UniFileOutputStreamPipe.java @@ -0,0 +1,63 @@ +/* + * 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.io; + +import android.support.annotation.NonNull; + +import com.hippo.unifile.UniFile; +import com.hippo.yorozuya.IOUtils; +import com.hippo.yorozuya.io.OutputStreamPipe; + +import java.io.IOException; +import java.io.OutputStream; + +public class UniFileOutputStreamPipe implements OutputStreamPipe { + + private UniFile mUniFile; + + private OutputStream mOs; + + public UniFileOutputStreamPipe(UniFile uniFile) { + mUniFile = uniFile; + } + + @Override + public void obtain() { + // Empty + } + + @Override + public void release() { + // Empty + } + + @Override + public @NonNull OutputStream open() throws IOException { + if (mOs != null) { + throw new IllegalStateException("Please close it first"); + } + + mOs = mUniFile.openOutputStream(); + return mOs; + } + + @Override + public void close() { + IOUtils.closeQuietly(mOs); + mOs = null; + } +} diff --git a/app/src/main/java/com/hippo/okhttp/ChromeRequestBuilder.java b/app/src/main/java/com/hippo/okhttp/ChromeRequestBuilder.java index 83d06ff11..ab2c10086 100644 --- a/app/src/main/java/com/hippo/okhttp/ChromeRequestBuilder.java +++ b/app/src/main/java/com/hippo/okhttp/ChromeRequestBuilder.java @@ -26,16 +26,8 @@ public class ChromeRequestBuilder extends Request.Builder { private static final String CHROME_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.103 Safari/537.36"; - public ChromeRequestBuilder(String url) { - URL u; - try { - u = new URL(url); - } catch (MalformedURLException e) { - throw new IllegalStateException(e); - } - - url(u); - + public ChromeRequestBuilder(String url) throws MalformedURLException { + url(new URL(url)); addHeader("User-Agent", CHROME_USER_AGENT); } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0b4b4163b..79cf57bc9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -88,6 +88,12 @@ Empty Invalid zip file Reading Failed + Out of range + Write failed + Parse error + 509 + Invalid url + Get pToken error The content of this application is from the Internet. Some of it may do physical or mental harm to you. You have learnt the risks above and would like to undertake them. Do you want to use this application? Username cannot be an empty string @@ -205,7 +211,6 @@ Gallery Previews Go to - Out of range EH Show Japanese title diff --git a/beerbelly b/beerbelly index a20b74d0a..b7f79ca74 160000 --- a/beerbelly +++ b/beerbelly @@ -1 +1 @@ -Subproject commit a20b74d0aebd6065bd6b2a1c4b65e03105d676d1 +Subproject commit b7f79ca745bf8ab6c5ff2bca5cd77ce5b19a6d55 diff --git a/build.gradle b/build.gradle index ea9ecb6f2..481a2e27c 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.0.0-beta4' + classpath 'com.android.tools.build:gradle:2.0.0-beta5' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/unifile b/unifile index d439d8fc0..cefb69635 160000 --- a/unifile +++ b/unifile @@ -1 +1 @@ -Subproject commit d439d8fc058867a7ad9ae442d0cddf3518fd01bc +Subproject commit cefb696355a3e1dcc51985d23e28340a038bd595 diff --git a/yorozuya b/yorozuya index fe276a68f..f688e1315 160000 --- a/yorozuya +++ b/yorozuya @@ -1 +1 @@ -Subproject commit fe276a68f183cb3639fdd77a0f46e5a5b6fafccd +Subproject commit f688e1315f5992674aa89c6f5d1a12449c4ee104