diff --git a/app/src/main/java/com/hippo/ehviewer/AppConfig.java b/app/src/main/java/com/hippo/ehviewer/AppConfig.java index 5abf213a7..79cd5bb5c 100644 --- a/app/src/main/java/com/hippo/ehviewer/AppConfig.java +++ b/app/src/main/java/com/hippo/ehviewer/AppConfig.java @@ -31,9 +31,9 @@ public final class AppConfig { private static final String DOWNLOAD_DIRNAME = "download"; private static final String ARCHIVE_DIRNAME = "archive"; private static final String TORRENT_DIRNAME = "torrent"; + private static final String TEMP_DIRNAME = "temp"; - public static @Nullable - File getAppDir() { + public static @Nullable File getExternalAppDir() { if (Environment.getExternalStorageState() .equals(Environment.MEDIA_MOUNTED)) { return new File(Environment.getExternalStorageDirectory(), APP_DIRNAME); @@ -43,7 +43,7 @@ public final class AppConfig { } public static @Nullable File getFileInAppDir(String filename) { - File appFolder = getAppDir(); + File appFolder = getExternalAppDir(); if (appFolder != null) { return new File(appFolder, filename); } else { @@ -66,5 +66,18 @@ public final class AppConfig { public static @Nullable File getTorrentDir() { return getFileInAppDir(TORRENT_DIRNAME); } + + public static @Nullable File getTempDir() { + return getFileInAppDir(TEMP_DIRNAME); + } + + public static @Nullable File createTempFile() { + File dir = getTempDir(); + if (dir != null && (dir.isDirectory() || dir.mkdirs())) { + return new File(dir, Long.toString(System.currentTimeMillis())); + } else { + return null; + } + } } 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 9d3b4bc0e..8d5887818 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/GalleryProviderListener.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/GalleryProviderListener.java @@ -16,7 +16,6 @@ package com.hippo.ehviewer.gallery; -import android.graphics.Bitmap; import android.support.annotation.Nullable; public interface GalleryProviderListener { @@ -27,7 +26,7 @@ public interface GalleryProviderListener { void onGetSize(int size); - void onGetBitmap(int index, @Nullable Bitmap bitmap); + void onGetImage(int index, @Nullable Object obj); void onPagePercent(int index, float percent); diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/GallerySpider.java b/app/src/main/java/com/hippo/ehviewer/gallery/GallerySpider.java index 8492cc473..fe0047f27 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/GallerySpider.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/GallerySpider.java @@ -48,7 +48,8 @@ public class GallerySpider implements GalleryProvider, SpiderQueen.SpiderListene private ImageHandler.Mode mMode; private GalleryBase mGalleryBase; - private UniFile mDownloadDir; + private UniFile mDownloadDirParent; + private String mDownloadDirname; private ThreadFactory mThreadFactory = new PriorityThreadFactory("SpiderQueen", Process.THREAD_PRIORITY_BACKGROUND); private SpiderQueen mSpiderQueen; @@ -99,18 +100,14 @@ public class GallerySpider implements GalleryProvider, SpiderQueen.SpiderListene mGalleryBase = galleryBase; mMode = mode; - String dirname = DBUtils.getDirname(galleryBase.gid); - if (dirname == null) { - dirname = downloadDirname(galleryBase); - DBUtils.addDirname(galleryBase, dirname); + mDownloadDirname = DBUtils.getDirname(galleryBase.gid); + if (mDownloadDirname == null) { + mDownloadDirname = downloadDirname(galleryBase); + DBUtils.addDirname(galleryBase, mDownloadDirname); } - UniFile dir = Settings.getImageDownloadLocation(); - if (dir != null) { - mDownloadDir = dir.createDirectory(dirname); - } - - if (mDownloadDir == null) { + mDownloadDirParent = Settings.getImageDownloadLocation(); + if (mDownloadDirParent == null) { throw new IOException("Can't get download dir"); } } @@ -130,7 +127,7 @@ public class GallerySpider implements GalleryProvider, SpiderQueen.SpiderListene if (mSpiderQueen == null) { mSpiderQueen = new SpiderQueen(sHttpClient, sSpiderInfoDir, - mGalleryBase, mMode, mDownloadDir, this); + mGalleryBase, mMode, mDownloadDirParent, mDownloadDirname, this); mThreadFactory.newThread(mSpiderQueen).start(); } } @@ -214,6 +211,7 @@ public class GallerySpider implements GalleryProvider, SpiderQueen.SpiderListene } } + @Override public void releaseBitmap(Bitmap bitmap) { if (mSpiderQueen != null) { mSpiderQueen.releaseBitmap(bitmap); @@ -330,12 +328,12 @@ public class GallerySpider implements GalleryProvider, SpiderQueen.SpiderListene } @Override - public void onGetBitmap(final int index, final Bitmap bitmap) { + public void onGetImage(final int index, final Object obj) { SimpleHandler.getInstance().post(new Runnable() { @Override public void run() { for (GalleryProviderListener l : mGalleryProviderListeners) { - l.onGetBitmap(index, bitmap); + l.onGetImage(index, obj); } } }); diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ImageHandler.java b/app/src/main/java/com/hippo/ehviewer/gallery/ImageHandler.java index 34cb1ed48..79adddeba 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/ImageHandler.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ImageHandler.java @@ -18,26 +18,36 @@ package com.hippo.ehviewer.gallery; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.nfc.FormatException; import android.support.annotation.Nullable; import android.text.TextUtils; +import android.util.Pair; import android.webkit.MimeTypeMap; import com.hippo.beerbelly.SimpleDiskCache; import com.hippo.conaco.BitmapPool; +import com.hippo.ehviewer.AppConfig; import com.hippo.ehviewer.EhCacheKeyFactory; import com.hippo.ehviewer.client.data.GalleryBase; +import com.hippo.ehviewer.gallery.gifdecoder.ByteArrayPool; +import com.hippo.ehviewer.gallery.gifdecoder.GifBitmapProvider; +import com.hippo.ehviewer.gallery.gifdecoder.GifDecoder; +import com.hippo.ehviewer.gallery.gifdecoder.GifHeader; +import com.hippo.ehviewer.gallery.gifdecoder.GifHeaderParser; import com.hippo.httpclient.HttpClient; -import com.hippo.httpclient.HttpResponse; import com.hippo.io.UniFileInputStreamPipe; import com.hippo.io.UniFileOutputStreamPipe; import com.hippo.network.DownloadClient; import com.hippo.network.DownloadRequest; import com.hippo.unifile.UniFile; +import com.hippo.unifile.UniRandomReadFile; import com.hippo.yorozuya.FileUtils; import com.hippo.yorozuya.IOUtils; 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; @@ -45,6 +55,8 @@ import java.util.concurrent.atomic.AtomicReferenceArray; public class ImageHandler { + private static final String TAG = ImageHandler.class.getSimpleName(); + public static final String[] POSSIBLE_IMAGE_EXTENSIONS = new String[] { "jpg", "jpeg", "png", "gif" }; @@ -52,10 +64,13 @@ public class ImageHandler { private static SimpleDiskCache sDiskCache; private GalleryBase mGalleryBase; - private UniFile mDownloadDir; - private transient Mode mMode; + private UniFile mDownloadDirParent; + private String mDownloadDirname; + private @Nullable UniFile mDownloadDir; + private volatile Mode mMode; private AtomicReferenceArray mFilenames; private BitmapPool mBitmapPool = new BitmapPool(); + private GifBitmapProvider mGifBitmapProvider = new GifBitmapProvider(new BitmapPool(), new ByteArrayPool()); public enum Mode { READ, @@ -66,15 +81,23 @@ public class ImageHandler { sDiskCache = diskCache; } - ImageHandler(GalleryBase galleryBase, int pages, Mode mode, UniFile downloadDir) { + ImageHandler(GalleryBase galleryBase, int pages, Mode mode, UniFile downloadDirParent, String downloadDirname) { mGalleryBase = galleryBase; mMode = mode; mFilenames = new AtomicReferenceArray<>(pages); - mDownloadDir = downloadDir; + mDownloadDirParent = downloadDirParent; + mDownloadDirname = downloadDirname; + + if (mode == Mode.DOWNLOAD) { + mDownloadDir = mDownloadDirParent.createDirectory(mDownloadDirname); + } } public void setMode(Mode mode) { - mMode = mode; + if (mMode != mode) { + mMode = mode; + mDownloadDir = mDownloadDirParent.createDirectory(mDownloadDirname); + } // Let DownloadManager to set spiderObj's download to true } @@ -83,6 +106,10 @@ public class ImageHandler { } private String guessImageFilename(int index) { + if (mDownloadDir == null) { + return null; + } + for (String extension : POSSIBLE_IMAGE_EXTENSIONS) { String filename = getImageFilename(index, extension); if (mDownloadDir.findFile(filename) != null) { @@ -110,6 +137,10 @@ public class ImageHandler { } private InputStreamPipe getDownloadInputStreamPipe(String filename) { + if (mDownloadDir == null) { + return null; + } + UniFile uniFile = mDownloadDir.findFile(filename); if (uniFile != null) { return new UniFileInputStreamPipe(uniFile); @@ -124,6 +155,10 @@ public class ImageHandler { } private OutputStreamPipe getDownloadOutputStreamPipe(String filename) { + if (mDownloadDir == null) { + return null; + } + UniFile uniFile = mDownloadDir.createFile(filename); if (uniFile != null) { return new UniFileOutputStreamPipe(uniFile); @@ -149,7 +184,7 @@ public class ImageHandler { String filename = mFilenames.get(index); if (filename != null) { // Check it in dir - if (mDownloadDir.findFile(filename) != null) { + if (mDownloadDir != null && mDownloadDir.findFile(filename) != null) { return true; } } else { @@ -167,6 +202,11 @@ public class ImageHandler { return false; } + if (mDownloadDir == null) { + // Can't get download dir + return false; + } + // Copy image from cache to dir OutputStream os = null; try { @@ -216,23 +256,13 @@ public class ImageHandler { } } - private String getExtension(HttpResponse response, String defaultExtension) { - String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType( - response.getHeaderField("Content-Type")); - if (extension == null) { - extension = MimeTypeMap.getFileExtensionFromUrl(response.getUrl().toString()); - if (extension == null) { - extension = defaultExtension; - } - } - return extension; - } - public void save(HttpClient httpClient, String url, int index, SaveHelper helper) throws Exception { OutputStreamPipe osPipe = null; if (mMode == Mode.READ) { osPipe = getReadOutputStreamPipe(index); + } else if (mMode == Mode.DOWNLOAD && mDownloadDir == null) { + return; } if (helper.mCancel) { @@ -314,7 +344,10 @@ public class ImageHandler { } } - public Bitmap getBitmap(int index) { + /** + * @return + */ + public Object getImage(int index) { InputStreamPipe isPipe = null; if (mMode == Mode.READ) { isPipe = getReadInputStreamPipe(index); @@ -329,6 +362,7 @@ public class ImageHandler { return null; } + Bitmap bitmap = null; try { isPipe.obtain(); @@ -349,10 +383,53 @@ public class ImageHandler { options.inMutable = true; options.inSampleSize = 1; options.inBitmap = mBitmapPool.getInBitmap(options); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; is = isPipe.open(); - return BitmapFactory.decodeStream(is, null, options); + bitmap = BitmapFactory.decodeStream(is, null, options); + + if (bitmap != null && "image/gif".equals(options.outMimeType)) { + // It is gif + UniRandomReadFile randomReadFile; + if (mMode == Mode.READ) { + // Copy the file to + File tempFile = AppConfig.createTempFile(); + if (tempFile == null) { + throw new IOException("Can't get temp dir or can't create temp dir"); + } + isPipe.close(); + is = isPipe.open(); + IOUtils.copy(is, new FileOutputStream(tempFile)); + randomReadFile = new TempRandomReadFile(tempFile, UniRandomReadFile.fromFile(tempFile)); + } else if (mMode == Mode.DOWNLOAD) { + if (mDownloadDir == null) { + throw new IOException("Can't find download dir"); + } + UniFile uniFile = mDownloadDir.findFile(getImageFilename(index)); + if (uniFile == null) { + throw new IOException("Can't find image file"); + } + + isPipe.close(); + isPipe.release(); + randomReadFile = uniFile.createRandomReadFile(); + } else { + throw new IllegalStateException("Unknown state"); + } + + GifHeader gifHeader = new GifHeaderParser().setData(randomReadFile).parseHeader(); + if (gifHeader.getStatus() == GifDecoder.STATUS_OK) { + return new Pair<>(new GifDecoder(mGifBitmapProvider, gifHeader, randomReadFile), bitmap); + } else { + throw new FormatException("Can't decode gif"); + } + } else { + // It is not gif, just reture + return bitmap; + } } catch (Exception e) { + e.printStackTrace(); + releaseBitmap(bitmap); return null; } finally { isPipe.close(); @@ -377,4 +454,41 @@ public class ImageHandler { } } } + + public static class TempRandomReadFile extends UniRandomReadFile { + + private File mFile; + private UniRandomReadFile mRandomReadFile; + + public TempRandomReadFile(File file, UniRandomReadFile randomReadFile) { + mFile = file; + mRandomReadFile = randomReadFile; + } + + @Override + public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { + return mRandomReadFile.read(buffer, byteOffset, byteCount); + } + + @Override + public void seek(long offset) throws IOException { + mRandomReadFile.seek(offset); + } + + @Override + public long position() throws IOException { + return mRandomReadFile.position(); + } + + @Override + public long length() throws IOException { + return mRandomReadFile.length(); + } + + @Override + public void close() throws IOException { + mRandomReadFile.close(); + FileUtils.delete(mFile); + } + } } diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/SpiderQueen.java b/app/src/main/java/com/hippo/ehviewer/gallery/SpiderQueen.java index f8b45c5a5..2d4e2cd94 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/SpiderQueen.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/SpiderQueen.java @@ -18,6 +18,7 @@ package com.hippo.ehviewer.gallery; import android.graphics.Bitmap; import android.os.Process; +import android.util.Log; import com.hippo.ehviewer.client.EhConfig; import com.hippo.ehviewer.client.EhUrl; @@ -69,7 +70,8 @@ public class SpiderQueen implements Runnable { public EhHttpClient mHttpClient; public File mSpiderInfoDir; public GalleryBase mGalleryBase; - private UniFile mDownloadDir; + private UniFile mDownloadDirparent; + private String mDownloadDirname; public ImageHandler mImageHandler; public int mSource; private ImageHandler.Mode mMode; @@ -77,14 +79,14 @@ public class SpiderQueen implements Runnable { public SpiderInfo mSpiderInfo; public boolean mHasCheckIndexAgain = true; - public transient int mNextIndex; + public volatile int mNextIndex; public AtomicIntegerArray mPageStates; - private transient HttpRequest mCurrentHttpRequest; + private volatile HttpRequest mCurrentHttpRequest; - private transient boolean mRestart = false; - private transient boolean mRetry = false; - private transient boolean mStop = false; + private volatile boolean mRestart = false; + private volatile boolean mRetry = false; + private volatile boolean mStop = false; private ThreadFactory mThreadFactory = new PriorityThreadFactory("SpiderWorker", Process.THREAD_PRIORITY_BACKGROUND); @@ -106,13 +108,14 @@ public class SpiderQueen implements Runnable { private final Object mGetSizeLock = new Object(); public SpiderQueen(EhHttpClient httpClient, File spiderInfoDir, - GalleryBase galleryBase, ImageHandler.Mode mode, UniFile downloadDir, - SpiderListener listener) { + GalleryBase galleryBase, ImageHandler.Mode mode, UniFile downloadDirParent, + String downloadDirname, SpiderListener listener) { mHttpClient = httpClient; mSpiderInfoDir = spiderInfoDir; mGalleryBase = galleryBase; mMode = mode; - mDownloadDir = downloadDir; + mDownloadDirparent = downloadDirParent; + mDownloadDirname = downloadDirname; mSpiderListener = listener; mSource = Settings.getEhSource(); @@ -278,8 +281,14 @@ public class SpiderQueen implements Runnable { private SpiderInfo readSpiderInfoFromDownloadDir() { if (mMode == ImageHandler.Mode.DOWNLOAD) { + UniFile dir = mDownloadDirparent.findFile(mDownloadDirname); + if (dir == null || !dir.isDirectory()) { + // Can't get download dir + return null; + } + SpiderInfo spiderInfo; - UniFile file = mDownloadDir.findFile(SPIDER_FILENAME); + UniFile file = dir.findFile(SPIDER_FILENAME); if (file == null) { return null; } @@ -316,16 +325,28 @@ public class SpiderQueen implements Runnable { } private void writeSpiderInfoToDownloadDir(SpiderInfo spiderInfo) { + + Log.d("TAG", "writeSpiderInfoToDownloadDir"); + if (mMode == ImageHandler.Mode.DOWNLOAD) { + UniFile dir = mDownloadDirparent.findFile(mDownloadDirname); + if (dir == null || !dir.isDirectory()) { + // Can't get download dir + return; + } + OutputStream os = null; try { - UniFile file = mDownloadDir.findFile(SPIDER_FILENAME); + + Log.d("TAG", "do writeSpiderInfoToDownloadDir"); + + UniFile file = dir.findFile(SPIDER_FILENAME); if (file != null) { os = file.openOutputStream(); spiderInfo.write(os); } } catch (Exception e) { - // Ingore + e.printStackTrace(); } finally { IOUtils.closeQuietly(os); } @@ -558,7 +579,7 @@ public class SpiderQueen implements Runnable { mPageStates.lazySet(i, PAGE_STATE_NONE); } mSpiderWorkers = new SpiderWorker[3]; - mImageHandler = new ImageHandler(gb, spiderInfo.pages, mMode, mDownloadDir); + mImageHandler = new ImageHandler(gb, spiderInfo.pages, mMode, mDownloadDirparent, mDownloadDirname); // Check stop if (mStop) { return; @@ -662,8 +683,8 @@ public class SpiderQueen implements Runnable { public int mIndex; - private transient HttpRequest mCurrentHttpRequest; - private transient ImageHandler.SaveHelper mCurrentSaveHelper; + private volatile HttpRequest mCurrentHttpRequest; + private volatile ImageHandler.SaveHelper mCurrentSaveHelper; public boolean mStop = false; @@ -883,7 +904,7 @@ public class SpiderQueen implements Runnable { continue; } - mSpiderListener.onGetBitmap(index, mImageHandler == null ? null : mImageHandler.getBitmap(index)); + mSpiderListener.onGetImage(index, mImageHandler == null ? null : mImageHandler.getImage(index)); } Say.d(TAG, "Render dies"); @@ -912,6 +933,6 @@ public class SpiderQueen implements Runnable { void onSpiderFailed(int index, Exception e); - void onGetBitmap(int index, Bitmap bitmap); + void onGetImage(int index, Object obj); } } 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 c5fbb8a74..23fc750ec 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/ZipGalleryProvider.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ZipGalleryProvider.java @@ -30,13 +30,13 @@ public class ZipGalleryProvider implements GalleryProvider, Runnable { private static final String[] IMAGE_EXTENSIONS = {"jpg", "jpeg", "png", "gif"}; - private transient boolean mRestart = false; - private transient boolean mStop = false; + private volatile boolean mRestart = false; + private volatile boolean mStop = false; private List mGalleryProviderListeners = new ArrayList<>(); private ZipFile mZipFile; - private transient int mSize = -1; + private volatile int mSize = -1; private List mFilenames = new ArrayList<>(); private BlockingQueue mRequestQueue = new LinkedBlockingQueue<>(); private BitmapPool mBitmapPool = new BitmapPool(); @@ -143,7 +143,7 @@ public class ZipGalleryProvider implements GalleryProvider, Runnable { @Override public void run() { for (GalleryProviderListener l : mGalleryProviderListeners) { - l.onGetBitmap(index, bitmap); + l.onGetImage(index, bitmap); } } }); diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/gifdecoder/ByteArrayPool.java b/app/src/main/java/com/hippo/ehviewer/gallery/gifdecoder/ByteArrayPool.java new file mode 100644 index 000000000..abb1ac13b --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/gifdecoder/ByteArrayPool.java @@ -0,0 +1,73 @@ +/* + * 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.gifdecoder; + +import java.lang.ref.WeakReference; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; + +public class ByteArrayPool { + + private final Set> mReusableSet = new LinkedHashSet<>(); + + public synchronized void add(byte[] bytes) { + if (bytes != null) { + mReusableSet.add(new WeakReference<>(bytes)); + } + } + + public synchronized byte[] getAtLeast(int size) { + final Iterator> iterator = mReusableSet.iterator(); + byte[] item; + while (iterator.hasNext()) { + item = iterator.next().get(); + if (item != null) { + if (item.length >= size) { + // Remove from reusable set so it can't be used again. + iterator.remove(); + return item; + } + } else { + // Remove from the set if the reference has been cleared or + // it can't be used. + iterator.remove(); + } + } + return null; + } + + public synchronized byte[] getExactly(int size) { + final Iterator> iterator = mReusableSet.iterator(); + byte[] item; + while (iterator.hasNext()) { + item = iterator.next().get(); + if (item != null) { + if (item.length == size) { + // Remove from reusable set so it can't be used again. + iterator.remove(); + return item; + } + } else { + // Remove from the set if the reference has been cleared or + // it can't be used. + iterator.remove(); + } + } + return null; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/gifdecoder/GifBitmapProvider.java b/app/src/main/java/com/hippo/ehviewer/gallery/gifdecoder/GifBitmapProvider.java new file mode 100644 index 000000000..68b817531 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/gifdecoder/GifBitmapProvider.java @@ -0,0 +1,47 @@ +package com.hippo.ehviewer.gallery.gifdecoder; + +import android.graphics.Bitmap; +import android.support.annotation.NonNull; + +import com.hippo.conaco.BitmapPool; + +public class GifBitmapProvider implements GifDecoder.BitmapProvider { + + private BitmapPool mBitmapPool; + private ByteArrayPool mByteArrayPool; + + public GifBitmapProvider(BitmapPool bitmapPool, ByteArrayPool byteArrayPool) { + mBitmapPool = bitmapPool; + mByteArrayPool = byteArrayPool; + } + + @NonNull + @Override + public Bitmap obtain(int width, int height) { + Bitmap bitmap = mBitmapPool.getBitmap(width, height); + if (bitmap == null) { + bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + bitmap.setHasAlpha(true); + } + return bitmap; + } + + @Override + public void release(Bitmap bitmap) { + mBitmapPool.addReusableBitmap(bitmap); + } + + @Override + public byte[] obtainByteArray(int size) { + byte[] bytes = mByteArrayPool.getAtLeast(size); + if (bytes == null) { + bytes = new byte[size]; + } + return bytes; + } + + @Override + public void release(byte[] bytes) { + mByteArrayPool.add(bytes); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/gifdecoder/GifDecoder.java b/app/src/main/java/com/hippo/ehviewer/gallery/gifdecoder/GifDecoder.java new file mode 100644 index 000000000..e584b1c69 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/gifdecoder/GifDecoder.java @@ -0,0 +1,724 @@ +package com.hippo.ehviewer.gallery.gifdecoder; + +/** + * Copyright (c) 2013 Xcellent Creations, Inc. + *

+ * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + *

+ * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import android.graphics.Bitmap; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import com.hippo.unifile.UniRandomReadFile; +import com.hippo.yorozuya.IOUtils; + +import java.io.IOException; +import java.util.Arrays; + +/** + * Reads frame data from a GIF image source and decodes it into individual frames for animation + * purposes. Image data can be read from either and InputStream source or a byte[]. + *

+ * This class is optimized for running animations with the frames, there are no methods to get + * individual frame images, only to decode the next frame in the animation sequence. Instead, it + * lowers its memory footprint by only housing the minimum data necessary to decode the next frame + * in the animation sequence. + *

+ * The animation must be manually moved forward using {@link #advance()} before requesting the next + * frame. This method must also be called before you request the first frame or an error will + * occur. + *

+ * Implementation adapted from sample code published in Lyons. (2004). Java for + * Programmers, republished under the MIT Open Source License + */ +public class GifDecoder { + private static final String TAG = GifDecoder.class.getSimpleName(); + + /** + * File read status: No errors. + */ + public static final int STATUS_OK = 0; + /** + * File read status: Error decoding file (may be partially decoded). + */ + public static final int STATUS_FORMAT_ERROR = 1; + /** + * File read status: Unable to read source. + */ + public static final int STATUS_READ_ERROR = 2; + /** + * Unable to fully decode the current frame. + */ + public static final int STATUS_PARTIAL_DECODE = 3; + /** + * max decoder pixel stack size. + */ + private static final int MAX_STACK_SIZE = 4096; + + /** + * GIF Disposal Method meaning take no action. + */ + private static final int DISPOSAL_UNSPECIFIED = 0; + /** + * GIF Disposal Method meaning leave canvas from previous frame. + */ + private static final int DISPOSAL_NONE = 1; + /** + * GIF Disposal Method meaning clear canvas to background color. + */ + private static final int DISPOSAL_BACKGROUND = 2; + /** + * GIF Disposal Method meaning clear canvas to frame before last. + */ + private static final int DISPOSAL_PREVIOUS = 3; + + private static final int NULL_CODE = -1; + + private static final int INITIAL_FRAME_POINTER = -1; + + // Global File Header values and parsing flags. + // Active color table. + private int[] act; + + // Raw GIF data from input source. + private UniRandomReadFile rawData; + + // Raw data read working array. + private byte[] block; + + // Temporary buffer for block reading. Reads 16k chunks from the native buffer for processing, + // to greatly reduce JNI overhead. + private static final int WORK_BUFFER_SIZE = 16384; + @Nullable + private byte[] workBuffer; + private int workBufferSize = 0; + private int workBufferPosition = 0; + + // LZW decoder working arrays. + private short[] prefix; + private byte[] suffix; + private byte[] pixelStack; + private byte[] mainPixels; + private int[] mainScratch; + + private int framePointer; + private GifHeader header; + private BitmapProvider bitmapProvider; + private Bitmap previousImage; + private boolean savePrevious; + private int status; + private int sampleSize; + private int downsampledHeight; + private int downsampledWidth; + private boolean isFirstFrameTransparent; + + /** + * An interface that can be used to provide reused {@link Bitmap}s to avoid GCs + * from constantly allocating {@link Bitmap}s for every frame. + */ + public interface BitmapProvider { + /** + * Returns an {@link Bitmap} with exactly the given dimensions and config. + * + * @param width The width in pixels of the desired {@link android.graphics.Bitmap}. + * @param height The height in pixels of the desired {@link android.graphics.Bitmap}. + */ + @NonNull + Bitmap obtain(int width, int height); + + /** + * Releases the given Bitmap back to the pool. + */ + void release(Bitmap bitmap); + + /** + * Returns a byte array used for decoding and generating the frame bitmap. + * + * @param size the size of the byte array to obtain + */ + byte[] obtainByteArray(int size); + + /** + * Releases the given byte array back to the pool. + */ + void release(byte[] bytes); + + } + + public GifDecoder(BitmapProvider provider, GifHeader gifHeader, UniRandomReadFile rawData) { + this(provider, gifHeader, rawData, 1 /*sampleSize*/); + } + + public GifDecoder(BitmapProvider provider, GifHeader gifHeader, UniRandomReadFile rawData, int sampleSize) { + this.bitmapProvider = provider; + setData(gifHeader, rawData, sampleSize); + } + + public int getWidth() { + return header.width; + } + + public int getHeight() { + return header.height; + } + + /** + * Returns the current status of the decoder. + *

+ *

Status will update per frame to allow the caller to tell whether or not the current frame + * was decoded successfully and/or completely. Format and open failures persist across frames. + *

+ */ + public int getStatus() { + return status; + } + + /** + * Move the animation frame counter forward. + */ + public void advance() { + framePointer = (framePointer + 1) % header.frameCount; + } + + /** + * Gets display duration for specified frame. + * + * @param n int index of frame. + * @return delay in milliseconds. + */ + public int getDelay(int n) { + int delay = -1; + if ((n >= 0) && (n < header.frameCount)) { + delay = header.frames.get(n).delay; + } + return delay; + } + + /** + * Gets display duration for the upcoming frame in ms. + */ + public int getNextDelay() { + if (header.frameCount <= 0 || framePointer < 0) { + return 0; + } + + return getDelay(framePointer); + } + + /** + * Gets the number of frames read from file. + * + * @return frame count. + */ + public int getFrameCount() { + return header.frameCount; + } + + /** + * Gets the current index of the animation frame, or -1 if animation hasn't not yet started. + * + * @return frame index. + */ + public int getCurrentFrameIndex() { + return framePointer; + } + + /** + * Resets the frame pointer to before the 0th frame, as if we'd never used this decoder to + * decode any frames. + */ + public void resetFrameIndex() { + framePointer = INITIAL_FRAME_POINTER; + } + + /** + * Gets the "Netscape" iteration count, if any. A count of 0 means repeat indefinitely. + * + * @return iteration count if one was specified, else 1. + */ + public int getLoopCount() { + return header.loopCount; + } + + /** + * Get the next frame in the animation sequence. + * + * @return Bitmap representation of frame. + */ + public synchronized Bitmap getNextFrame() { + if (header.frameCount <= 0 || framePointer < 0) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "unable to decode frame, frameCount=" + header.frameCount + " framePointer=" + framePointer); + } + status = STATUS_FORMAT_ERROR; + } + if (status == STATUS_FORMAT_ERROR || status == STATUS_READ_ERROR) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Unable to decode frame, status=" + status); + } + return null; + } + status = STATUS_OK; + + GifFrame currentFrame = header.frames.get(framePointer); + GifFrame previousFrame = null; + int previousIndex = framePointer - 1; + if (previousIndex >= 0) { + previousFrame = header.frames.get(previousIndex); + } + + final int savedBgColor = header.bgColor; + + // Set the appropriate color table. + if (currentFrame.lct == null) { + act = header.gct; + } else { + act = currentFrame.lct; + if (header.bgIndex == currentFrame.transIndex) { + header.bgColor = 0; + } + } + + int save = 0; + if (currentFrame.transparency) { + save = act[currentFrame.transIndex]; + // Set transparent color if specified. + act[currentFrame.transIndex] = 0; + } + if (act == null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "No Valid Color Table"); + } + // No color table defined. + status = STATUS_FORMAT_ERROR; + return null; + } + + // Transfer pixel data to image. + Bitmap result = setPixels(currentFrame, previousFrame); + + // Reset the transparent pixel in the color table + if (currentFrame.transparency) { + act[currentFrame.transIndex] = save; + } + header.bgColor = savedBgColor; + + return result; + } + + public void clear() { + header = null; + mainPixels = null; + mainScratch = null; + if (previousImage != null) { + bitmapProvider.release(previousImage); + } + previousImage = null; + IOUtils.closeQuietly(rawData); + rawData = null; + isFirstFrameTransparent = false; + if (block != null) { + bitmapProvider.release(block); + } + if (workBuffer != null) { + bitmapProvider.release(workBuffer); + } + } + + public synchronized void setData(GifHeader header, UniRandomReadFile file, int sampleSize) { + if (sampleSize <= 0) { + throw new IllegalArgumentException("Sample size must be >=0, not: " + sampleSize); + } + // Make sure sample size is a power of 2. + sampleSize = Integer.highestOneBit(sampleSize); + this.status = STATUS_OK; + this.header = header; + isFirstFrameTransparent = false; + framePointer = INITIAL_FRAME_POINTER; + // Initialize the raw data buffer. + rawData = file; + + // No point in specially saving an old frame if we're never going to use it. + savePrevious = false; + for (GifFrame frame : header.frames) { + if (frame.dispose == DISPOSAL_PREVIOUS) { + savePrevious = true; + break; + } + } + + this.sampleSize = sampleSize; + // Now that we know the size, init scratch arrays. + // TODO: Find a way to avoid this entirely or at least downsample it + // (either should be possible). + mainPixels = new byte[header.width * header.height]; + mainScratch = new int[(header.width / sampleSize) * (header.height / sampleSize)]; + downsampledWidth = header.width / sampleSize; + downsampledHeight = header.height / sampleSize; + } + + /** + * Creates new frame image from current data (and previous frames as specified by their + * disposition codes). + */ + private Bitmap setPixels(GifFrame currentFrame, GifFrame previousFrame) { + // Final location of blended pixels. + final int[] dest = mainScratch; + + // fill in starting image contents based on last image's dispose code + if (previousFrame != null && previousFrame.dispose > DISPOSAL_UNSPECIFIED) { + // We don't need to do anything for DISPOSAL_NONE, if it has the correct pixels so will our + // mainScratch and therefore so will our dest array. + if (previousFrame.dispose == DISPOSAL_BACKGROUND) { + // Start with a canvas filled with the background color + int c = 0; + if (!currentFrame.transparency) { + c = header.bgColor; + } else if (framePointer == 0) { + // TODO: We should check and see if all individual pixels are replaced. If they are, the + // first frame isn't actually transparent. For now, it's simpler and safer to assume + // drawing a transparent background means the GIF contains transparency. + isFirstFrameTransparent = true; + } + Arrays.fill(dest, c); + } else if (previousFrame.dispose == DISPOSAL_PREVIOUS && previousImage != null) { + // Start with the previous frame + previousImage.getPixels(dest, 0, downsampledWidth, 0, 0, downsampledWidth, downsampledHeight); + } + } + + // Decode pixels for this frame into the global pixels[] scratch. + decodeBitmapData(currentFrame); + + int downsampledIH = currentFrame.ih / sampleSize; + int downsampledIY = currentFrame.iy / sampleSize; + int downsampledIW = currentFrame.iw / sampleSize; + int downsampledIX = currentFrame.ix / sampleSize; + // Copy each source line to the appropriate place in the destination. + int pass = 1; + int inc = 8; + int iline = 0; + boolean isFirstFrame = framePointer == 0; + for (int i = 0; i < downsampledIH; i++) { + int line = i; + if (currentFrame.interlace) { + if (iline >= downsampledIH) { + pass++; + switch (pass) { + case 2: + iline = 4; + break; + case 3: + iline = 2; + inc = 4; + break; + case 4: + iline = 1; + inc = 2; + break; + default: + break; + } + } + line = iline; + iline += inc; + } + line += downsampledIY; + if (line < downsampledHeight) { + int k = line * downsampledWidth; + // Start of line in dest. + int dx = k + downsampledIX; + // End of dest line. + int dlim = dx + downsampledIW; + if (k + downsampledWidth < dlim) { + // Past dest edge. + dlim = k + downsampledWidth; + } + // Start of line in source. + int sx = i * sampleSize * currentFrame.iw; + int maxPositionInSource = sx + ((dlim - dx) * sampleSize); + while (dx < dlim) { + // Map color and insert in destination. + int averageColor = averageColorsNear(sx, maxPositionInSource, currentFrame.iw); + if (averageColor != 0) { + dest[dx] = averageColor; + } else if (!isFirstFrameTransparent && isFirstFrame) { + isFirstFrameTransparent = true; + } + sx += sampleSize; + dx++; + } + } + } + + // Copy pixels into previous image + if (savePrevious && (currentFrame.dispose == DISPOSAL_UNSPECIFIED || currentFrame.dispose == DISPOSAL_NONE)) { + if (previousImage == null) { + previousImage = bitmapProvider.obtain(downsampledWidth, downsampledHeight); + } + previousImage.setPixels(dest, 0, downsampledWidth, 0, 0, downsampledWidth, downsampledHeight); + } + + // Set pixels for current image. + Bitmap result = bitmapProvider.obtain(downsampledWidth, downsampledHeight); + result.setPixels(dest, 0, downsampledWidth, 0, 0, downsampledWidth, downsampledHeight); + return result; + } + + private int averageColorsNear(int positionInMainPixels, int maxPositionInMainPixels, int currentFrameIw) { + int alphaSum = 0; + int redSum = 0; + int greenSum = 0; + int blueSum = 0; + + int totalAdded = 0; + // Find the pixels in the current row. + for (int i = positionInMainPixels; i < positionInMainPixels + sampleSize && i < mainPixels.length && i < maxPositionInMainPixels; i++) { + int currentColorIndex = ((int) mainPixels[i]) & 0xff; + int currentColor = act[currentColorIndex]; + if (currentColor != 0) { + alphaSum += currentColor >> 24 & 0x000000ff; + redSum += currentColor >> 16 & 0x000000ff; + greenSum += currentColor >> 8 & 0x000000ff; + blueSum += currentColor & 0x000000ff; + totalAdded++; + } + } + // Find the pixels in the next row. + for (int i = positionInMainPixels + currentFrameIw; i < positionInMainPixels + currentFrameIw + sampleSize && i < mainPixels.length && i < maxPositionInMainPixels; i++) { + int currentColorIndex = ((int) mainPixels[i]) & 0xff; + int currentColor = act[currentColorIndex]; + if (currentColor != 0) { + alphaSum += currentColor >> 24 & 0x000000ff; + redSum += currentColor >> 16 & 0x000000ff; + greenSum += currentColor >> 8 & 0x000000ff; + blueSum += currentColor & 0x000000ff; + totalAdded++; + } + } + if (totalAdded == 0) { + return 0; + } else { + return ((alphaSum / totalAdded) << 24) | ((redSum / totalAdded) << 16) | ((greenSum / totalAdded) << 8) | (blueSum / totalAdded); + } + } + + /** + * Decodes LZW image data into pixel array. Adapted from John Cristy's BitmapMagick. + */ + private void decodeBitmapData(GifFrame frame) { + workBufferSize = 0; + workBufferPosition = 0; + if (frame != null) { + // Jump to the frame start position. + try { + rawData.seek(frame.bufferFrameStart); + } catch (IOException e) { + e.printStackTrace(); // TODO what if get exception + } + } + + int npix = (frame == null) ? header.width * header.height : frame.iw * frame.ih; + int available, clear, codeMask, codeSize, endOfInformation, inCode, oldCode, bits, code, count, + i, datum, + dataSize, first, top, bi, pi; + + if (mainPixels == null || mainPixels.length < npix) { + // Allocate new pixel array. + mainPixels = new byte[npix]; + } + if (prefix == null) { + prefix = new short[MAX_STACK_SIZE]; + } + if (suffix == null) { + suffix = new byte[MAX_STACK_SIZE]; + } + if (pixelStack == null) { + pixelStack = new byte[MAX_STACK_SIZE + 1]; + } + + // Initialize GIF data stream decoder. + dataSize = readByte(); + clear = 1 << dataSize; + endOfInformation = clear + 1; + available = clear + 2; + oldCode = NULL_CODE; + codeSize = dataSize + 1; + codeMask = (1 << codeSize) - 1; + for (code = 0; code < clear; code++) { + // XXX ArrayIndexOutOfBoundsException. + prefix[code] = 0; + suffix[code] = (byte) code; + } + + // Decode GIF pixel stream. + datum = bits = count = first = top = pi = bi = 0; + for (i = 0; i < npix; ) { + // Load bytes until there are enough bits for a code. + if (count == 0) { + // Read a new data block. + count = readBlock(); + if (count <= 0) { + status = STATUS_PARTIAL_DECODE; + break; + } + bi = 0; + } + + datum += (((int) block[bi]) & 0xff) << bits; + bits += 8; + bi++; + count--; + + while (bits >= codeSize) { + // Get the next code. + code = datum & codeMask; + datum >>= codeSize; + bits -= codeSize; + + // Interpret the code. + if (code == clear) { + // Reset decoder. + codeSize = dataSize + 1; + codeMask = (1 << codeSize) - 1; + available = clear + 2; + oldCode = NULL_CODE; + continue; + } + + if (code > available) { + status = STATUS_PARTIAL_DECODE; + break; + } + + if (code == endOfInformation) { + break; + } + + if (oldCode == NULL_CODE) { + pixelStack[top++] = suffix[code]; + oldCode = code; + first = code; + continue; + } + inCode = code; + if (code >= available) { + pixelStack[top++] = (byte) first; + code = oldCode; + } + while (code >= clear) { + pixelStack[top++] = suffix[code]; + code = prefix[code]; + } + first = ((int) suffix[code]) & 0xff; + pixelStack[top++] = (byte) first; + + // Add a new string to the string table. + if (available < MAX_STACK_SIZE) { + prefix[available] = (short) oldCode; + suffix[available] = (byte) first; + available++; + if (((available & codeMask) == 0) && (available < MAX_STACK_SIZE)) { + codeSize++; + codeMask += available; + } + } + oldCode = inCode; + + while (top > 0) { + // Pop a pixel off the pixel stack. + mainPixels[pi++] = pixelStack[--top]; + i++; + } + } + } + + // Clear missing pixels. + for (i = pi; i < npix; i++) { + mainPixels[i] = 0; + } + } + + /** + * Reads the next chunk for the intermediate work buffer. + */ + private void readChunkIfNeeded() throws IOException { + if (workBufferSize > workBufferPosition) { + return; + } + if (workBuffer == null) { + workBuffer = bitmapProvider.obtainByteArray(WORK_BUFFER_SIZE); + } + workBufferPosition = 0; + workBufferSize = Math.min((int) rawData.remaining(), WORK_BUFFER_SIZE); + rawData.read(workBuffer, 0, workBufferSize); + } + + /** + * Reads a single byte from the input stream. + */ + private int readByte() { + try { + readChunkIfNeeded(); + return workBuffer[workBufferPosition++] & 0xFF; + } catch (Exception e) { + status = STATUS_FORMAT_ERROR; + return 0; + } + } + + /** + * Reads next variable length block from input. + * + * @return number of bytes stored in "buffer". + */ + private int readBlock() { + int blockSize = readByte(); + if (blockSize > 0) { + try { + if (block == null) { + block = bitmapProvider.obtainByteArray(255); + } + final int remaining = workBufferSize - workBufferPosition; + if (remaining >= blockSize) { + // Block can be read from the current work buffer. + System.arraycopy(workBuffer, workBufferPosition, block, 0, blockSize); + workBufferPosition += blockSize; + } else if (rawData.remaining() + remaining >= blockSize) { + // Block can be read in two passes. + System.arraycopy(workBuffer, workBufferPosition, block, 0, remaining); + workBufferPosition = workBufferSize; + readChunkIfNeeded(); + final int secondHalfRemaining = blockSize - remaining; + System.arraycopy(workBuffer, 0, block, remaining, secondHalfRemaining); + workBufferPosition += secondHalfRemaining; + } else { + status = STATUS_FORMAT_ERROR; + } + } catch (Exception e) { + Log.w(TAG, "Error Reading Block", e); + status = STATUS_FORMAT_ERROR; + } + } + return blockSize; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/gifdecoder/GifFrame.java b/app/src/main/java/com/hippo/ehviewer/gallery/gifdecoder/GifFrame.java new file mode 100644 index 000000000..89af6f0fb --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/gifdecoder/GifFrame.java @@ -0,0 +1,36 @@ +package com.hippo.ehviewer.gallery.gifdecoder; + +/** + * Inner model class housing metadata for each frame. + */ +class GifFrame { + int ix, iy, iw, ih; + /** + * Control Flag. + */ + boolean interlace; + /** + * Control Flag. + */ + boolean transparency; + /** + * Disposal Method. + */ + int dispose; + /** + * Transparency Index. + */ + int transIndex; + /** + * Delay, in ms, to next frame. + */ + int delay; + /** + * Index in the raw buffer where we need to start reading to decode. + */ + int bufferFrameStart; + /** + * Local Color Table. + */ + int[] lct; +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/gifdecoder/GifHeader.java b/app/src/main/java/com/hippo/ehviewer/gallery/gifdecoder/GifHeader.java new file mode 100644 index 000000000..4659efb3b --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/gifdecoder/GifHeader.java @@ -0,0 +1,58 @@ +package com.hippo.ehviewer.gallery.gifdecoder; + +import java.util.ArrayList; +import java.util.List; + +/** + * A header object containing the number of frames in an animated GIF image as well as basic + * metadata like width and height that can be used to decode each individual frame of the GIF. Can + * be shared by one or more {@link GifDecoder}s to play the same + * animated GIF in multiple views. + */ +public class GifHeader { + + int[] gct = null; + int status = GifDecoder.STATUS_OK; + int frameCount = 0; + + GifFrame currentFrame; + List frames = new ArrayList(); + // Logical screen size. + // Full image width. + int width; + // Full image height. + int height; + + // 1 : global color table flag. + boolean gctFlag; + // 2-4 : color resolution. + // 5 : gct sort flag. + // 6-8 : gct size. + int gctSize; + // Background color index. + int bgIndex; + // Pixel aspect ratio. + int pixelAspect; + //TODO: this is set both during reading the header and while decoding frames... + int bgColor; + int loopCount; + + public int getHeight() { + return height; + } + + public int getWidth() { + return width; + } + + public int getNumFrames() { + return frameCount; + } + + /** + * Global status code of GIF data parsing. + */ + public int getStatus() { + return status; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/gifdecoder/GifHeaderParser.java b/app/src/main/java/com/hippo/ehviewer/gallery/gifdecoder/GifHeaderParser.java new file mode 100644 index 000000000..65d679555 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/gifdecoder/GifHeaderParser.java @@ -0,0 +1,381 @@ +package com.hippo.ehviewer.gallery.gifdecoder; + +import android.util.Log; + +import com.hippo.unifile.UniRandomReadFile; + +import java.io.IOException; +import java.util.Arrays; + +/** + * A class responsible for creating {@link GifHeader}s from data + * representing animated gifs. + */ +public class GifHeaderParser { + public static final String TAG = "GifHeaderParser"; + + // The minimum frame delay in hundredths of a second. + static final int MIN_FRAME_DELAY = 3; + // The default frame delay in hundredths of a second for GIFs with frame delays less than the + // minimum. + static final int DEFAULT_FRAME_DELAY = 10; + + private static final int MAX_BLOCK_SIZE = 256; + // Raw data read working array. + private final byte[] block = new byte[MAX_BLOCK_SIZE]; + + private UniRandomReadFile rawData; + private GifHeader header; + private int blockSize = 0; + + private byte[] mTemp = new byte[2]; + + public GifHeaderParser setData(UniRandomReadFile data) { + reset(); + rawData = data; + return this; + } + + public void clear() { + rawData = null; + header = null; + } + + private void reset() { + rawData = null; + Arrays.fill(block, (byte) 0); + header = new GifHeader(); + blockSize = 0; + } + + public GifHeader parseHeader() { + if (rawData == null) { + throw new IllegalStateException("You must call setData() before parseHeader()"); + } + if (err()) { + return header; + } + + try { + readHeader(); + if (!err()) { + readContents(); + if (header.frameCount < 0) { + header.status = GifDecoder.STATUS_FORMAT_ERROR; + } + } + } catch (IOException e) { + e.printStackTrace(); + header.status = GifDecoder.STATUS_READ_ERROR; + } + + return header; + } + + /** + * Main file parser. Reads GIF content blocks. + */ + private void readContents() throws IOException { + // Read GIF file content blocks. + boolean done = false; + while (!(done || err())) { + int code = read(); + switch (code) { + // Image separator. + case 0x2C: + // The graphics control extension is optional, but will always come first if it exists. + // If one did + // exist, there will be a non-null current frame which we should use. However if one + // did not exist, + // the current frame will be null and we must create it here. See issue #134. + if (header.currentFrame == null) { + header.currentFrame = new GifFrame(); + } + readBitmap(); + break; + // Extension. + case 0x21: + code = read(); + switch (code) { + // Graphics control extension. + case 0xf9: + // Start a new frame. + header.currentFrame = new GifFrame(); + readGraphicControlExt(); + break; + // Application extension. + case 0xff: + readBlock(); + String app = ""; + for (int i = 0; i < 11; i++) { + app += (char) block[i]; + } + if (app.equals("NETSCAPE2.0")) { + readNetscapeExt(); + } else { + // Don't care. + skip(); + } + break; + // Comment extension. + case 0xfe: + skip(); + break; + // Plain text extension. + case 0x01: + skip(); + break; + // Uninteresting extension. + default: + skip(); + } + break; + // Terminator. + case 0x3b: + done = true; + break; + // Bad byte, but keep going and see what happens break; + case 0x00: + default: + header.status = GifDecoder.STATUS_FORMAT_ERROR; + break; + } + } + } + + /** + * Reads Graphics Control Extension values. + */ + private void readGraphicControlExt() throws IOException { + // Block size. + read(); + // Packed fields. + int packed = read(); + // Disposal method. + header.currentFrame.dispose = (packed & 0x1c) >> 2; + if (header.currentFrame.dispose == 0) { + // Elect to keep old image if discretionary. + header.currentFrame.dispose = 1; + } + header.currentFrame.transparency = (packed & 1) != 0; + // Delay in milliseconds. + int delayInHundredthsOfASecond = readShort(); + // TODO: consider allowing -1 to indicate show forever. + if (delayInHundredthsOfASecond < MIN_FRAME_DELAY) { + delayInHundredthsOfASecond = DEFAULT_FRAME_DELAY; + } + header.currentFrame.delay = delayInHundredthsOfASecond * 10; + // Transparent color index + header.currentFrame.transIndex = read(); + // Block terminator + read(); + } + + /** + * Reads next frame image. + */ + private void readBitmap() throws IOException { + // (sub)image position & size. + header.currentFrame.ix = readShort(); + header.currentFrame.iy = readShort(); + header.currentFrame.iw = readShort(); + header.currentFrame.ih = readShort(); + + int packed = read(); + // 1 - local color table flag interlace + boolean lctFlag = (packed & 0x80) != 0; + int lctSize = (int) Math.pow(2, (packed & 0x07) + 1); + // 3 - sort flag + // 4-5 - reserved lctSize = 2 << (packed & 7); // 6-8 - local color + // table size + header.currentFrame.interlace = (packed & 0x40) != 0; + if (lctFlag) { + // Read table. + header.currentFrame.lct = readColorTable(lctSize); + } else { + // No local color table. + header.currentFrame.lct = null; + } + + // Save this as the decoding position pointer. + header.currentFrame.bufferFrameStart = (int) rawData.position(); + + // False decode pixel data to advance buffer. + skipImageData(); + + if (err()) { + return; + } + + header.frameCount++; + // Add image to frame. + header.frames.add(header.currentFrame); + } + + /** + * Reads Netscape extension to obtain iteration count. + */ + private void readNetscapeExt() throws IOException { + do { + readBlock(); + if (block[0] == 1) { + // Loop count sub-block. + int b1 = ((int) block[1]) & 0xff; + int b2 = ((int) block[2]) & 0xff; + header.loopCount = (b2 << 8) | b1; + } + } while ((blockSize > 0) && !err()); + } + + + /** + * Reads GIF file header information. + */ + private void readHeader() throws IOException { + String id = ""; + for (int i = 0; i < 6; i++) { + id += (char) read(); + } + if (!id.startsWith("GIF")) { + header.status = GifDecoder.STATUS_FORMAT_ERROR; + return; + } + readLSD(); + if (header.gctFlag && !err()) { + header.gct = readColorTable(header.gctSize); + header.bgColor = header.gct[header.bgIndex]; + } + } + + /** + * Reads Logical Screen Descriptor. + */ + private void readLSD() throws IOException { + // Logical screen size. + header.width = readShort(); + header.height = readShort(); + // Packed fields + int packed = read(); + // 1 : global color table flag. + header.gctFlag = (packed & 0x80) != 0; + // 2-4 : color resolution. + // 5 : gct sort flag. + // 6-8 : gct size. + header.gctSize = 2 << (packed & 7); + // Background color index. + header.bgIndex = read(); + // Pixel aspect ratio + header.pixelAspect = read(); + } + + /** + * Reads color table as 256 RGB integer values. + * + * @param ncolors int number of colors to read. + * @return int array containing 256 colors (packed ARGB with full alpha). + */ + private int[] readColorTable(int ncolors) throws IOException { + int nbytes = 3 * ncolors; + int[] tab; + byte[] c = new byte[nbytes]; + + try { + rawData.read(c); + + // TODO: what bounds checks are we avoiding if we know the number of colors? + // Max size to avoid bounds checks. + tab = new int[MAX_BLOCK_SIZE]; + int i = 0; + int j = 0; + while (i < ncolors) { + int r = ((int) c[j++]) & 0xff; + int g = ((int) c[j++]) & 0xff; + int b = ((int) c[j++]) & 0xff; + tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b; + } + } catch (IOException e) { + Log.d(TAG, "Format Error Reading Color Table", e); + header.status = GifDecoder.STATUS_FORMAT_ERROR; + throw e; + } + + return tab; + } + + /** + * Skips LZW image data for a single frame to advance buffer. + */ + private void skipImageData() throws IOException { + // lzwMinCodeSize + read(); + // data sub-blocks + skip(); + } + + /** + * Skips variable length blocks up to and including next zero length block. + */ + private void skip() throws IOException { + int blockSize; + do { + blockSize = read(); + rawData.seek(rawData.position() + blockSize); + } while (blockSize > 0); + } + + /** + * Reads next variable length block from input. + * + * @return number of bytes stored in "buffer" + */ + private int readBlock() throws IOException { + blockSize = read(); + int n = 0; + if (blockSize > 0) { + int count = 0; + try { + while (n < blockSize) { + count = blockSize - n; + rawData.read(block, n, count); + + n += count; + } + } catch (Exception e) { + Log.d(TAG, "Error Reading Block n: " + n + " count: " + count + " blockSize: " + blockSize, e); + header.status = GifDecoder.STATUS_FORMAT_ERROR; + } + } + return n; + } + + /** + * Reads a single byte from the input stream. + */ + private int read() throws IOException { + try { + rawData.read(mTemp, 0, 1); + return mTemp[0] & 0xff; + } catch (IOException e) { + header.status = GifDecoder.STATUS_READ_ERROR; + throw e; + } + } + + /** + * Reads next 16-bit value, LSB first. + */ + private int readShort() throws IOException { + // Read 16-bit value. + try { + rawData.read(mTemp, 0, 2); + return (mTemp[1] << 8 & 0xff00) | (mTemp[0] & 0xff); + } catch (IOException e) { + header.status = GifDecoder.STATUS_READ_ERROR; + throw e; + } + } + + private boolean err() { + return header.status != GifDecoder.STATUS_OK; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GifTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GifTexture.java new file mode 100644 index 000000000..79fb537fc --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/GifTexture.java @@ -0,0 +1,146 @@ +/* + * 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.glrenderer; + +import android.graphics.Bitmap; +import android.support.annotation.NonNull; +import android.util.Log; + +import com.hippo.ehviewer.gallery.gifdecoder.GifDecoder; +import com.hippo.ehviewer.gallery.ui.GLRoot; + +public class GifTexture extends TiledTexture { + + private static final String TAG = GifTexture.class.getSimpleName(); + + private static TiledTexture.Uploader sUploader; + private static InfiniteThreadExecutor sThreadExecutor; + + private GLRoot mGLRoot; + private GifDecoder mGifDecoder; + private boolean mRunning = false; + + private GifDecodeTask mGifDecodeTask; + + public static void initialize(TiledTexture.Uploader uploader, InfiniteThreadExecutor threadExecutor) { + sUploader = uploader; + sThreadExecutor = threadExecutor; + } + + public static void uninitialize() { + sUploader = null; + sThreadExecutor = null; + } + + class GifDecodeTask implements Runnable { + + private long mLastTime = 0; + private volatile boolean mSleeping = false; + private volatile boolean mClear = false; + + public boolean isSleeping() { + return mSleeping; + } + + public void clearByMyself() { + mClear = true; + } + + @Override + public void run() { + GifDecoder gifDecoder = mGifDecoder; + + while (mRunning) { + gifDecoder.advance(); + Bitmap bitmap = gifDecoder.getNextFrame(); + if (bitmap == null) { + Log.d(TAG, "Can't get bitmap, GifDecoder state = " + gifDecoder.getStatus()); + mRunning = false; + break; + } + + setBitmap(bitmap); + sUploader.addTexture(GifTexture.this); + + long delay = gifDecoder.getNextDelay(); + long time = System.currentTimeMillis(); + if (mLastTime != 0) { + delay -= time - mLastTime; + } + mLastTime = time; + if (delay > 5) { + mSleeping = true; + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + // Ignore + } + mSleeping = false; + } + } + + if (mClear) { + gifDecoder.clear(); + } + } + } + + public GifTexture(@NonNull GifDecoder gifDecoder, @NonNull Bitmap bitmap, @NonNull GLRoot glRoot) { + super(bitmap); + + mGifDecoder = gifDecoder; + mGLRoot = glRoot; + start(); + } + + public void start() { + mRunning = true; + if (mGifDecodeTask == null) { + mGifDecodeTask = new GifDecodeTask(); + sThreadExecutor.execute(mGifDecodeTask); + } + } + + public void stop() { + mRunning = false; + mGifDecodeTask = null; + + mGifDecoder.resetFrameIndex(); + Bitmap bitmap = mGifDecoder.getNextFrame(); + if (bitmap != null) { + setBitmap(bitmap); + sUploader.addTexture(GifTexture.this); + } + } + + @Override + public void recycle() { + + Log.d(TAG, "recycle"); + + mRunning = false; + if (mGifDecodeTask.isSleeping()) { + mGifDecoder.clear(); + } else { + mGifDecodeTask.clearByMyself(); + } + mGifDecoder = null; + mGifDecodeTask = null; + + super.recycle(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/InfiniteThreadExecutor.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/InfiniteThreadExecutor.java new file mode 100644 index 000000000..48c5d7c24 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/InfiniteThreadExecutor.java @@ -0,0 +1,111 @@ +/* + * 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.glrenderer; + +import android.support.annotation.NonNull; +import android.util.Log; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public class InfiniteThreadExecutor implements Executor { + + private long mKeepAliveMillis; + private BlockingQueue mWorkQueue; + private ThreadFactory mThreadFactory; + private volatile int mThreadCount; + private volatile int mEmptyThreadCount; + + private final Lock mThreadLock = new ReentrantLock(); + private final Object mWaitLock = new Object(); + + public InfiniteThreadExecutor(long keepAliveMillis, + BlockingQueue workQueue, ThreadFactory threadFactory) { + mKeepAliveMillis = keepAliveMillis; + mWorkQueue = workQueue; + mThreadFactory = threadFactory; + } + + @Override + public void execute(@NonNull Runnable command) { + mThreadLock.lock(); + try { + mWorkQueue.add(command); + if (mEmptyThreadCount > 0) { + mEmptyThreadCount--; + synchronized (mWaitLock) { + mWaitLock.notify(); + } + } else { + mThreadFactory.newThread(new Task()).start(); + } + } catch (Exception e) { + // Ignore + } + mThreadLock.unlock(); + } + + public int getThreadCount() { + return mThreadCount; + } + + public class Task implements Runnable { + + @Override + public void run() { + Log.d("TAG", Thread.currentThread().getName() + " start"); + mThreadCount++; + + int loop = 0; + boolean end = false; + for (;;) { + Runnable command = mWorkQueue.poll(); + if (command == null) { + mEmptyThreadCount--; + end = true; + } + + if (loop != 0) { + mThreadLock.unlock(); + } + + if (end) { + break; + } + + command.run(); + + mEmptyThreadCount++; + synchronized (mWaitLock) { + try { + mWaitLock.wait(mKeepAliveMillis); + } catch (InterruptedException e) { + // Ignore + } + } + mThreadLock.lock(); + loop++; + } + + mThreadCount--; + Log.d("TAG", Thread.currentThread().getName() + " end"); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/TiledTexture.java b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/TiledTexture.java index 8ba834c40..36cd12569 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/TiledTexture.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/glrenderer/TiledTexture.java @@ -26,6 +26,7 @@ import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.os.SystemClock; import android.support.annotation.NonNull; +import android.util.Log; import com.hippo.ehviewer.gallery.ui.GLRoot; @@ -240,6 +241,32 @@ public class TiledTexture implements Texture { mTiles = list.toArray(new Tile[list.size()]); } + // Only for GifTexture + protected void setBitmap(@NonNull Bitmap bitmap) { + if (bitmap.getWidth() == mWidth && bitmap.getHeight() == mHeight) { + synchronized (mTiles) { + freeBitmap(); + mUploadIndex = 0; + mBitmap = bitmap; + for (int x = 0, w = mWidth, i = 0; x < w; x += CONTENT_SIZE) { + for (int y = 0, h = mHeight; y < h; y += CONTENT_SIZE, i++) { + Tile tile = mTiles[i]; + tile.offsetX = x; + tile.offsetY = y; + tile.bitmap = bitmap; + tile.setSize(Math.min(CONTENT_SIZE, mWidth - x), + Math.min(CONTENT_SIZE, mHeight - y)); + tile.invalidateContent(); + } + } + } + } else { + Log.d(TAG, "Only same size bitmap can be set, " + + "original is [" + mWidth + ", " + mHeight + "], " + + "bitmap is [" + bitmap.getWidth() + ", " + bitmap.getHeight() + "]"); + } + } + public boolean isReady() { return mUploadIndex == mTiles.length; } diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ui/GalleryView.java b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GalleryView.java index 32c103671..29ce55790 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/ui/GalleryView.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ui/GalleryView.java @@ -15,6 +15,7 @@ import com.hippo.yorozuya.MathUtils; import java.util.ArrayList; import java.util.List; +import java.util.Stack; public class GalleryView extends GLView implements GestureRecognizer.Listener { @@ -76,6 +77,8 @@ public class GalleryView extends GLView implements GestureRecognizer.Listener { private SmoothScroller mSmoothScroller; private SmoothScaler mSmoothScaler; + private Recycler mRecycler; + public enum Mode { NONE, // Just a progress view @@ -107,6 +110,7 @@ public class GalleryView extends GLView implements GestureRecognizer.Listener { mSmoothScroller.setInterpolator(SMOOTH_SCROLLER_INTERPOLATOR); mSmoothScaler = new SmoothScaler(); mSmoothScaler.setInterpolator(SMOOTH_SCALER_INTERPOLATOR); + mRecycler = new Recycler(); setBackgroundColor(GalleryUtils.intColorToFloatARGBArray(context.getResources().getColor(R.color.gallery_background))); } @@ -510,6 +514,7 @@ public class GalleryView extends GLView implements GestureRecognizer.Listener { */ + mLayoutOffset = 0; fill(); } @@ -519,10 +524,8 @@ public class GalleryView extends GLView implements GestureRecognizer.Listener { private void unbindGalleryPage(GalleryPageView view) { LayoutParams lp = (LayoutParams) view.getLayoutParams(); - int index = lp.index; - lp.index = NO_POSITION; - if (index != NO_POSITION) { - mAdapter.unbindPage(view, index); + if (lp.index != NO_POSITION) { + mAdapter.unbindPage(view, lp.index); } } @@ -530,10 +533,7 @@ public class GalleryView extends GLView implements GestureRecognizer.Listener { mPageMap.put(index, page); } - private void detachPage(int index) { - mPageMap.remove(index); - } - + // Set lp.index NO_POSITION private void detachPage(GalleryPageView page) { LayoutParams lp = (LayoutParams) page.getLayoutParams(); int index = lp.index; @@ -609,11 +609,11 @@ public class GalleryView extends GLView implements GestureRecognizer.Listener { if (i < count) { page = mAttachedPage.valueAt(i); // Unbind - detachPage(page); unbindGalleryPage(page); + detachPage(page); mUsedPage.add(page); } else { - page = mAdapter.createPage(); + page = mRecycler.obtain(); addComponent(page); } // Bind @@ -635,9 +635,10 @@ public class GalleryView extends GLView implements GestureRecognizer.Listener { if (mUsedPage.contains(page)) { continue; } - detachPage(page); unbindGalleryPage(page); + detachPage(page); removeComponent(page); + mRecycler.release(page); } mAttachedPage.clear(); mUsedPage.clear(); @@ -682,10 +683,12 @@ public class GalleryView extends GLView implements GestureRecognizer.Listener { GLView component = getComponent(i); removeComponentAt(i); - // Remove from map, unbind gallery page + // Remove from map, unbind gallery page, add to recycler if (component instanceof GalleryPageView) { - detachPage((GalleryPageView) component); - unbindGalleryPage((GalleryPageView) component); + GalleryPageView page = (GalleryPageView) component; + unbindGalleryPage(page); + detachPage(page); + mRecycler.release(page); } } @@ -800,7 +803,7 @@ public class GalleryView extends GLView implements GestureRecognizer.Listener { public abstract void unbindPage(GalleryPageView view, int index); } - public class SmoothScaler extends Animation { + class SmoothScaler extends Animation { private GalleryPageView mPage; private float mFocusX; @@ -833,7 +836,7 @@ public class GalleryView extends GLView implements GestureRecognizer.Listener { } } - public class SmoothScroller extends Animation { + class SmoothScroller extends Animation { private int mDx; private int mDy; @@ -858,4 +861,31 @@ public class GalleryView extends GLView implements GestureRecognizer.Listener { mLastX = x; } } + + class Recycler { + + private int mSize = 0; + + private Stack mStack = new Stack<>(); + + private GalleryPageView obtain() { + if (mSize != 0) { + mSize--; + return mStack.pop(); + } else { + return mAdapter.createPage(); + } + } + + public void release(GalleryPageView page) { + if (page == null) { + return; + } + + if (mSize < 3) { // 3 is max size + mSize++; + mStack.push(page); + } + } + } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/ContentActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/ContentActivity.java index bacd8f27a..c44f36899 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/ContentActivity.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/ContentActivity.java @@ -39,10 +39,10 @@ import com.hippo.scene.SceneManager; import com.hippo.scene.SimpleCurtain; import com.hippo.scene.StageActivity; import com.hippo.scene.StageLayout; -import com.hippo.yorozuya.Messenger; import com.hippo.vectorold.content.VectorContext; import com.hippo.widget.DrawerLayout; import com.hippo.widget.DrawerListView; +import com.hippo.yorozuya.Messenger; public final class ContentActivity extends StageActivity implements FitStageLayout.OnFitPaddingBottomListener, @@ -174,6 +174,41 @@ public final class ContentActivity extends StageActivity if (savedInstanceState == null) { startScene(GalleryListScene.class, null); } + + /* + + try { + + BitmapPool bitmapPool = new BitmapPool(); + ByteArrayPool byteArrayPool = new ByteArrayPool(); + GifBitmapProvider gifBitmapProvider = new GifBitmapProvider(bitmapPool, byteArrayPool); + + + File file = new File(Environment.getExternalStorageDirectory(), "00000001.gif"); + UniRandomReadFile randomReadFile = UniRandomReadFile.fromFile(file); + GifHeader gifHeader = new GifHeaderParser().setData(randomReadFile).parseHeader(); + if (gifHeader.getStatus() == GifDecoder.STATUS_OK) { + GifDecoder gifDecoder = new GifDecoder(gifBitmapProvider, gifHeader, randomReadFile); + + + for (int i = 0, n = gifDecoder.getFrameCount(); i < n * 2; i++) { + gifDecoder.advance(); + gifDecoder.getNextFrame().compress(Bitmap.CompressFormat.PNG, 100, new FileOutputStream(new File(Environment.getExternalStorageDirectory(), String.format("%08d", i) + ".png"))); + } + + + + } + + + + + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + */ + + } @Override 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 9f9d52177..d7d1cf976 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java @@ -22,7 +22,10 @@ import android.graphics.Bitmap; import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; +import android.os.Process; import android.support.annotation.Nullable; +import android.util.Log; +import android.util.Pair; import com.hippo.app.StatsActivity; import com.hippo.ehviewer.R; @@ -32,6 +35,9 @@ import com.hippo.ehviewer.gallery.GalleryProviderListener; import com.hippo.ehviewer.gallery.GallerySpider; import com.hippo.ehviewer.gallery.ImageHandler; import com.hippo.ehviewer.gallery.ZipGalleryProvider; +import com.hippo.ehviewer.gallery.gifdecoder.GifDecoder; +import com.hippo.ehviewer.gallery.glrenderer.GifTexture; +import com.hippo.ehviewer.gallery.glrenderer.InfiniteThreadExecutor; import com.hippo.ehviewer.gallery.glrenderer.TextTexture; import com.hippo.ehviewer.gallery.glrenderer.TiledTexture; import com.hippo.ehviewer.gallery.ui.GLRootView; @@ -40,9 +46,11 @@ import com.hippo.ehviewer.gallery.ui.GalleryPageView; import com.hippo.ehviewer.gallery.ui.GalleryView; import com.hippo.util.SystemUiHelper; import com.hippo.yorozuya.LayoutUtils; +import com.hippo.yorozuya.PriorityThreadFactory; import java.io.File; import java.io.IOException; +import java.util.concurrent.LinkedBlockingQueue; public class GalleryActivity extends StatsActivity implements TiledTexture.OnFreeBitmapListener, GalleryProviderListener { @@ -57,7 +65,9 @@ public class GalleryActivity extends StatsActivity implements TiledTexture.OnFre private Resources mResources; private TiledTexture.Uploader mUploader; + private InfiniteThreadExecutor mThreadExecutor; + private GLRootView mGLRootView; private GalleryView mGalleryView; private GalleryAdapter mAdapter; @@ -118,13 +128,15 @@ public class GalleryActivity extends StatsActivity implements TiledTexture.OnFre mSystemUiHelper.hide(); setContentView(R.layout.activity_gallery); - GLRootView glRootView = (GLRootView) findViewById(R.id.gl_root_view); - + mGLRootView = (GLRootView) findViewById(R.id.gl_root_view); mResources = getResources(); // Prepare TiledTexture.prepareResources(); TiledTexture.setOnFreeBitmapListener(this); - mUploader = new TiledTexture.Uploader(glRootView); + mUploader = new TiledTexture.Uploader(mGLRootView); + mThreadExecutor = new InfiniteThreadExecutor(3000, new LinkedBlockingQueue(), + new PriorityThreadFactory("GifDecode", Process.THREAD_PRIORITY_BACKGROUND)); + GifTexture.initialize(mUploader, mThreadExecutor); Typeface tf = Typeface.createFromAsset(mResources.getAssets(), "fonts/number.ttf"); mTextTexture = TextTexture.create(tf, mResources.getDimensionPixelSize(R.dimen.gallery_index_text), @@ -147,7 +159,7 @@ public class GalleryActivity extends StatsActivity implements TiledTexture.OnFre } mGalleryProvider.addGalleryProviderListener(this); - glRootView.setContentPane(mGalleryView); + mGLRootView.setContentPane(mGalleryView); } private void setMode(GalleryView.Mode mode) { @@ -162,6 +174,7 @@ public class GalleryActivity extends StatsActivity implements TiledTexture.OnFre if (!mDieYoung) { GalleryPageView.setTextTexture(null); mTextTexture.recycle(); + GifTexture.uninitialize(); TiledTexture.setOnFreeBitmapListener(null); TiledTexture.freeResources(); mGalleryProvider.removeGalleryProviderListener(this); @@ -173,8 +186,15 @@ public class GalleryActivity extends StatsActivity implements TiledTexture.OnFre } mGalleryProvider = null; + + // Free all TileTexture in GalleryView + for (int i = 0, n = mGalleryView.getComponentCount(); i < n; i++) { + GLView view = mGalleryView.getComponent(i); + if (view instanceof GalleryPageView) { + clearTiledTextureInPage((GalleryPageView) view); + } + } } - // TODO free all TileTexture in GalleryView } @Override @@ -209,19 +229,27 @@ public class GalleryActivity extends StatsActivity implements TiledTexture.OnFre } } + public void releaseImage(Object obj) { + if (mGalleryProvider != null && obj != null) { + if (obj instanceof Bitmap) { + mGalleryProvider.releaseBitmap((Bitmap) obj); + } else if (obj instanceof Pair) { + mGalleryProvider.releaseBitmap((Bitmap) ((Pair) obj).second); + } + } + } + @Override - public void onGetBitmap(int index, @Nullable Bitmap bitmap) { + public void onGetImage(int index, @Nullable Object obj) { GalleryPageView page = mGalleryView.getPage(index); if (page != null) { - if (bitmap == null) { - bindBadBitmap(page); + if (obj == null) { + bindBadImage(page); } else { - bindBitmap(page, bitmap); + bindImage(page, obj); } } else { - if (mGalleryProvider != null) { - mGalleryProvider.releaseBitmap(bitmap); - } + releaseImage(obj); } } @@ -255,19 +283,31 @@ public class GalleryActivity extends StatsActivity implements TiledTexture.OnFre if (tiledTexture != null) { view.mImageView.setTiledTexture(null); tiledTexture.recycle(); + } else { + Log.d("TAG", "tiledTexture == null"); } } - private void bindBadBitmap(GalleryPageView view) { + private void bindBadImage(GalleryPageView view) { clearTiledTextureInPage(view); view.mProgressView.setVisibility(GLView.INVISIBLE); view.mIndexView.setVisibility(GLView.VISIBLE); view.mIndexView.setText(".3"); } - private void bindBitmap(GalleryPageView view, Bitmap bitmap) { - TiledTexture tiledTexture = new TiledTexture(bitmap); - mUploader.addTexture(tiledTexture); + private void bindImage(GalleryPageView view, Object obj) { + TiledTexture tiledTexture; + if (obj instanceof Bitmap) { + tiledTexture = new TiledTexture((Bitmap) obj); + mUploader.addTexture(tiledTexture); + } else if (obj instanceof Pair) { + Pair pair = (Pair) obj; + tiledTexture = new GifTexture((GifDecoder) pair.first, (Bitmap) pair.second, mGLRootView); + mUploader.addTexture(tiledTexture); + } else { + bindBadImage(view); + return; + } clearTiledTextureInPage(view); view.mProgressView.setVisibility(GLView.INVISIBLE); diff --git a/beerbelly b/beerbelly index 47a38c0e4..2d6dcbcc8 160000 --- a/beerbelly +++ b/beerbelly @@ -1 +1 @@ -Subproject commit 47a38c0e4897c5d06366ab9c86ba575899df56aa +Subproject commit 2d6dcbcc8aa91eeceae51366178842e58bfd7477 diff --git a/conaco b/conaco index 54e8aabeb..ef916a783 160000 --- a/conaco +++ b/conaco @@ -1 +1 @@ -Subproject commit 54e8aabeb28701eb4e27e90dae8feda02af0f1de +Subproject commit ef916a783822c7bb3902321dc2492915a859951f diff --git a/unifile b/unifile index 29bf62258..797141f39 160000 --- a/unifile +++ b/unifile @@ -1 +1 @@ -Subproject commit 29bf6225863be2e1479096720fd506839545e79f +Subproject commit 797141f39ff5cf95c86aede2779929bba63c4373 diff --git a/yorozuya b/yorozuya index 8a6459d77..75f9410d6 160000 --- a/yorozuya +++ b/yorozuya @@ -1 +1 @@ -Subproject commit 8a6459d77e79db41f8485cc0892b5eb9a2cd4e4d +Subproject commit 75f9410d66feba9f8f19313c31518258801bf973