/* * 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.ehviewer.gallery.gl.GalleryPageView; 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 final 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 final ThreadFactory mThreadFactory = new PriorityThreadFactory( SpiderWorker.class.getSimpleName(), Process.THREAD_PRIORITY_BACKGROUND); @Nullable private Thread mDecoderThread; private final Stack mDecodeRequestStack = new Stack<>(); private final AtomicInteger mDecodeIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX); private volatile AtomicReferenceArray mWorkers; private final Object mWorkerLock = new Object(); private final Object mPTokenLock = new Object(); private volatile SpiderInfo mSpiderInfo; private final 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 final AtomicInteger mDownloadedPages = new AtomicInteger(0); private final AtomicInteger mFinishedPages = new AtomicInteger(0); // Store page error private final ConcurrentHashMap mPageErrorMap = new ConcurrentHashMap<>(); // Store page download percent private final 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) { if (!mDecodeRequestStack.contains(index) && index != mDecodeIndex.get()) { 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); 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); if (skipHathKey != null) { url = url + "?nl=" + skipHathKey; } Log.d(TAG, url); 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, boolean force) { 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; // If it is force request, skip first image if (force && i == 0) { continue; } 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 { Log.d(TAG, "Start download image " + index); 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 if (Thread.currentThread().isInterrupted()) { interrupt = true; break; } Log.d(TAG, "Download image succeed " + index); // 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(); Log.d(TAG, "End download image " + index); } } // Remove download failed image mSpiderDen.remove(index); 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 Log.d(TAG, Thread.currentThread().getName() + " 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, force); } @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(); mDecodeIndex.lazySet(index); } // Check index valid if (index < 0 || index >= mPageStateArray.length) { mDecodeIndex.lazySet(GalleryPageView.INVALID_INDEX); notifyGetImageFailure(index, GetText.getString(R.string.error_out_of_range)); continue; } InputStreamPipe pipe = mSpiderDen.openInputStreamPipe(index); if (pipe == null) { mDecodeIndex.lazySet(GalleryPageView.INVALID_INDEX); notifyGetImageFailure(index, GetText.getString(R.string.error_not_found)); continue; } try { pipe.obtain(); Image image = Image.decode(pipe.open(), false); if (image != null) { mDecodeIndex.lazySet(GalleryPageView.INVALID_INDEX); notifyGetImageSuccess(index, image); } else { mDecodeIndex.lazySet(GalleryPageView.INVALID_INDEX); notifyGetImageFailure(index, GetText.getString(R.string.error_decoding_failed)); } } catch (IOException e) { mDecodeIndex.lazySet(GalleryPageView.INVALID_INDEX); 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); } }