Files
EhViewer/app/src/main/java/com/hippo/ehviewer/gallery/SpiderQueen.java
T
2015-07-18 23:27:26 +08:00

584 lines
18 KiB
Java

/*
* 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.ehviewer.gallery;
import android.graphics.Bitmap;
import android.os.Process;
import com.hippo.ehviewer.client.EhConfig;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.GalleryPageParser;
import com.hippo.ehviewer.client.data.GalleryBase;
import com.hippo.ehviewer.network.EhHttpClient;
import com.hippo.ehviewer.network.EhHttpRequest;
import com.hippo.ehviewer.util.Settings;
import com.hippo.httpclient.HttpRequest;
import com.hippo.httpclient.HttpResponse;
import com.hippo.yorozuya.AutoExpandArray;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.PriorityThreadFactory;
import com.hippo.yorozuya.Say;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.locks.ReentrantLock;
public class SpiderQueen implements Runnable {
private static final String TAG = SpiderQueen.class.getSimpleName();
private static final String[] URL_509_SUFFIX_ARRAY = {
"/509.gif",
"/509s.gif"
};
public static final int PAGE_STATE_NONE = 0;
public static final int PAGE_STATE_DOWNLOADING = 1;
public static final int PAGE_STATE_SUCCEED = 2;
public static final int PAGE_STATE_FAILED = 3;
public EhHttpClient mHttpClient;
public File mSpiderInfoDir;
public GalleryBase mGalleryBase;
public ImageHandler mImageHandler;
public int mSource;
private ImageHandler.Mode mMode;
private SpiderListener mSpiderListener;
public SpiderInfo mSpiderInfo;
public int mNextIndex;
public int[] mPageStates;
public boolean mHasCheckIndexAgain = true;
private ThreadFactory mThreadFactory = new PriorityThreadFactory("SpiderWorker",
Process.THREAD_PRIORITY_BACKGROUND);
private SpiderWorker[] mSpiderWorkers;
private LinkedList<Integer> mRequestTokens = new LinkedList<>();
private Render mRender;
private final Object mQueenLock = new Object();
private final Object mWorkerLock = new Object();
private final Object mPageLock = new Object();
private final ReentrantLock mWorkerArrayLock = new ReentrantLock();
// Lock for mRequestTokens and mSpiderInfo.tokens
private final Object mTokenLock = new Object();
private boolean mStop = false;
public SpiderQueen(EhHttpClient httpClient, File spiderInfoDir,
GalleryBase galleryBase, ImageHandler.Mode mode, SpiderListener listener) {
mHttpClient = httpClient;
mSpiderInfoDir = spiderInfoDir;
mGalleryBase = galleryBase;
mSource = Settings.getEhSource();
mMode = mode;
mSpiderListener = listener;
}
public void setMode(ImageHandler.Mode mode) {
mMode = mode;
if (mImageHandler != null) {
mImageHandler.setMode(mode);
}
}
public void request(int index) {
if (mRender == null) {
mSpiderListener.onGetNone(index);
} else {
mRender.request(index);
}
}
public void releaseBitmap(Bitmap bitmap) {
if (mImageHandler != null) {
mImageHandler.releaseBitmap(bitmap);
}
}
@Override
public void run() {
Say.d(TAG, "Spider Queen starts");
EhConfig ehConfig = mHttpClient.getEhConfigClone();
GalleryBase gb = mGalleryBase;
SpiderInfo spiderInfo = null;
// 1. get spider info from dir
File spiderInfoFile = new File(mSpiderInfoDir, Integer.toString(gb.gid));
if (spiderInfoFile.exists()) {
try {
spiderInfo = SpiderInfo.read(new FileInputStream(spiderInfoFile));
if (gb.gid != spiderInfo.galleryBase.gid) {
spiderInfo = null;
throw new IllegalStateException("Gid not match in SpiderInfo file");
}
} catch (Exception e) {
FileUtils.delete(spiderInfoFile);
}
}
// Check stop
if (mStop) {
return;
}
// 2. if can't get spider info from dir, get it from internet
if (spiderInfo == null) {
// Use normal size to get more preview
ehConfig.previewSize = EhConfig.PREVIEW_SIZE_NORMAL;
ehConfig.setDirty();
EhHttpRequest request = new EhHttpRequest();
request.setEhConfig(ehConfig);
request.setUrl(EhUrl.getDetailUrl(mSource, gb.gid, gb.token, 0));
try {
HttpResponse response = mHttpClient.execute(request);
String body = response.getString();
SpiderParser.Result result = SpiderParser.parse(body, SpiderParser.REQUEST_ALL);
spiderInfo = new SpiderInfo();
spiderInfo.galleryBase = gb;
spiderInfo.pages = result.pages;
spiderInfo.previewSize = result.previewSize;
spiderInfo.previewPages = result.previewPages;
spiderInfo.previewCountPerPage = result.tokens.size();
spiderInfo.tokens = new AutoExpandArray<>(spiderInfo.pages);
} catch (Exception e) {
// TODO Add a listener to tell spider error
e.printStackTrace();
return;
} finally {
request.disconnect();
}
}
// Update EhConfig preview size
ehConfig.previewSize = spiderInfo.previewSize;
// Write to file
try {
spiderInfo.write(new FileOutputStream(spiderInfoFile));
} catch (IOException e) {
// Ignore
}
// Check stop
if (mStop) {
return;
}
// Listener
mSpiderListener.onGetPages(spiderInfo.pages);
// 3. prepare
mSpiderInfo = spiderInfo;
mNextIndex = 0;
mPageStates = new int[spiderInfo.pages];
Arrays.fill(mPageStates, PAGE_STATE_NONE);
mSpiderWorkers = new SpiderWorker[3];
mImageHandler = new ImageHandler(gb, spiderInfo.pages, mMode);
// Check stop
if (mStop) {
return;
}
// 4. Make Worker to do work
ensureWorker();
mRender = new Render();
Thread thread = new Thread(mRender, "Render");
thread.setPriority(Process.THREAD_PRIORITY_BACKGROUND);
thread.start();
// 4. Get tokens
while (!mStop) {
Integer index;
synchronized (mTokenLock) {
index = mRequestTokens.poll();
}
if (index == null) {
// Can't get request index
synchronized (mQueenLock) {
try {
mQueenLock.wait();
} catch (InterruptedException e) {
// Ignore
}
}
continue;
}
// Check if we has the token first
String token;
synchronized (mTokenLock) {
token = spiderInfo.tokens.get(index);
}
if (token == null) {
SpiderParser.Result result;
int previewIndex = index / spiderInfo.previewCountPerPage;
EhHttpRequest request = new EhHttpRequest();
request.setEhConfig(ehConfig);
request.setUrl(EhUrl.getDetailUrl(mSource, gb.gid, gb.token, previewIndex));
try {
HttpResponse response = mHttpClient.execute(request);
String body = response.getString();
result = SpiderParser.parse(body, SpiderParser.REQUEST_PREVIEW);
} catch (Exception e) {
// TODO Add a listener to tell spider error
e.printStackTrace();
break;
} finally {
request.disconnect();
}
synchronized (mTokenLock) {
AutoExpandArray<String> tokens = spiderInfo.tokens;
List<String> list = result.tokens;
int start = result.startIndex;
int end = start + list.size();
for (int i = start; i < end; i++) {
tokens.set(i, list.get(i - start));
}
}
// Write to file
try {
spiderInfo.write(new FileOutputStream(spiderInfoFile));
} catch (IOException e) {
// Ignore
}
}
// Notify
synchronized (mWorkerLock) {
mWorkerLock.notifyAll();
}
}
// 5. The queen dies, workers die, render dies
mWorkerArrayLock.lock();
for (SpiderWorker worker : mSpiderWorkers) {
if (worker != null) {
worker.mStop = true;
}
}
synchronized (mWorkerLock) {
mWorkerLock.notifyAll();
}
mWorkerArrayLock.unlock();
mRender.stop();
Say.d(TAG, "Spider dies");
}
public int pages() {
if (mSpiderInfo != null) {
return mSpiderInfo.pages;
} else {
return -1;
}
}
public void stop() {
mStop = true;
synchronized (mQueenLock) {
mQueenLock.notify();
}
}
private void ensureWorker() {
mWorkerArrayLock.lock();
int length = mSpiderWorkers.length;
for (int i = 0; i < length; i++) {
if (mSpiderWorkers[i] == null) {
SpiderWorker worker = new SpiderWorker(i);
mSpiderWorkers[i] = worker;
mThreadFactory.newThread(worker).start();
}
}
mWorkerArrayLock.unlock();
}
public void setNextIndex(int index) {
synchronized (mPageLock) {
if (mSpiderInfo != null && index < mSpiderInfo.pages) {
mNextIndex = index;
}
}
}
public String getTokenByIndex(int index) {
synchronized (mTokenLock) {
String token = mSpiderInfo.tokens.get(index);
if (token == null && !mRequestTokens.contains(index)) {
mRequestTokens.offer(index);
}
// Notify Queen
synchronized (mQueenLock) {
mQueenLock.notify();
}
return token;
}
}
private int getRequestIndex() {
synchronized (mPageLock) {
int length = mSpiderInfo.pages;
if (mNextIndex >= length) {
if (!mHasCheckIndexAgain) {
mHasCheckIndexAgain = true;
mNextIndex = 0;
}
}
while (mNextIndex < length && mPageStates[mNextIndex] != PAGE_STATE_NONE) {
mNextIndex++;
}
return mNextIndex >= length ? -1 : mNextIndex++;
}
}
class SpiderWorker implements Runnable {
private final String TAG = SpiderWorker.class.getSimpleName();
public int mIndex;
public boolean mStop = false;
public SpiderWorker(int index) {
mIndex = index;
}
private String getImageUrl(int gid, String token, int index) throws Exception {
HttpRequest request = new HttpRequest();
request.setUrl(EhUrl.getPageUrl(mSource, mGalleryBase.gid, token, index));
try {
HttpResponse response = mHttpClient.execute(request);
String body = response.getString();
String imageUrl = GalleryPageParser.parse(body);
for (String suffix : URL_509_SUFFIX_ARRAY) {
if (imageUrl.endsWith(suffix)) {
// Get 509 gif here
throw new Image509Expection();
}
}
return imageUrl;
} finally {
request.disconnect();
}
}
@Override
public void run() {
Say.d(TAG, "Spider worker starts");
GalleryBase gb = mGalleryBase;
ImageHandler handler = mImageHandler;
while (!mStop) {
// Get index
int index;
synchronized (mPageLock) {
index = getRequestIndex();
if (index == -1) {
// There is no more
break;
} else {
mPageStates[index] = PAGE_STATE_DOWNLOADING;
}
}
// Check contain
if (handler.contain(index)) {
// Find it, no need to download
synchronized (mPageLock) {
mPageStates[index] = PAGE_STATE_SUCCEED;
}
// Listener
mSpiderListener.onSpiderSucceed(index);
continue;
}
// Get token
String token;
for (;;) {
token = getTokenByIndex(index);
if (token == null) {
synchronized (mWorkerLock) {
try {
mWorkerLock.wait();
} catch (InterruptedException e) {
// Ignore
}
if (mStop) {
break;
}
}
} else {
break;
}
}
// For stop
if (mStop) {
break;
}
// Get image url
String imageUrl;
try {
imageUrl = getImageUrl(gb.gid, token, index);
} catch (Exception e) {
e.printStackTrace();
// TODO notify 509 error
synchronized (mPageLock) {
mPageStates[index] = PAGE_STATE_FAILED;
}
// Listener
mSpiderListener.onSpiderFailed(index);
continue;
}
// Download image
HttpRequest request = new HttpRequest();
request.setUrl(imageUrl);
try {
HttpResponse response = mHttpClient.execute(request);
handler.save(index, response);
synchronized (mPageLock) {
mPageStates[index] = PAGE_STATE_SUCCEED;
}
// Listener
mSpiderListener.onSpiderSucceed(index);
} catch (Exception e) {
synchronized (mPageLock) {
mPageStates[index] = PAGE_STATE_FAILED;
}
// Listener
mSpiderListener.onSpiderFailed(index);
} finally {
request.disconnect();
}
}
// TODO this lock is not right, we need to lock when before check it is over
mWorkerArrayLock.lock();
mSpiderWorkers[mIndex] = null;
mWorkerArrayLock.unlock();
Say.d(TAG, "Spider Worker dies");
}
}
// The runner to handle request
class Render implements Runnable {
private final String TAG = Render.class.getSimpleName();
private boolean mStop = false;
private final Object mRenderLock = new Object();
private BlockingQueue<Integer> mQueue = new LinkedBlockingQueue<>();
public void stop() {
mStop = true;
synchronized (mRenderLock) {
mRenderLock.notify();
}
}
public void request(int index) {
mQueue.offer(index);
synchronized (mRenderLock) {
mRenderLock.notify();
}
}
@Override
public void run() {
Say.d(TAG, "Render starts");
while (!mStop) {
Integer index = mQueue.poll();
if (index == null) {
synchronized (mRenderLock) {
try {
mRenderLock.wait();
} catch (InterruptedException e) {
// Ignore
}
}
continue;
}
int state;
synchronized (mPageLock) {
state = mPageStates[index];
}
switch (state) {
case PAGE_STATE_NONE:
mSpiderListener.onGetNone(index);
break;
case PAGE_STATE_DOWNLOADING:
mSpiderListener.onGetSpider(index, 0.5f); // TODO
break;
case PAGE_STATE_SUCCEED:
mSpiderListener.onGetBitmap(index, mImageHandler == null ? null : mImageHandler.getBitmap(index));
break;
default:
case PAGE_STATE_FAILED:
mSpiderListener.onGetFailed(index);
}
}
Say.d(TAG, "Render dies");
}
}
public interface SpiderListener {
void onGetPages(int pages);
void onSpiderPage(int index, float percent);
void onSpiderSucceed(int index);
void onSpiderFailed(int index);
void onGetNone(int index);
void onGetBitmap(int index, Bitmap bitmap);
void onGetSpider(int index, float percent);
void onGetFailed(int index);
}
}