mobile/android/base/ThumbnailHelper.java

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/base/ThumbnailHelper.java	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,217 @@
     1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
     1.5 + * This Source Code Form is subject to the terms of the Mozilla Public
     1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.8 +
     1.9 +package org.mozilla.gecko;
    1.10 +
    1.11 +import org.mozilla.gecko.db.BrowserDB;
    1.12 +import org.mozilla.gecko.gfx.BitmapUtils;
    1.13 +import org.mozilla.gecko.gfx.IntSize;
    1.14 +import org.mozilla.gecko.mozglue.DirectBufferAllocator;
    1.15 +import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI;
    1.16 +
    1.17 +import android.graphics.Bitmap;
    1.18 +import android.util.Log;
    1.19 +import android.content.res.Resources;
    1.20 +
    1.21 +import java.nio.ByteBuffer;
    1.22 +import java.util.LinkedList;
    1.23 +import java.util.concurrent.atomic.AtomicInteger;
    1.24 +
    1.25 +/**
    1.26 + * Helper class to generate thumbnails for tabs.
    1.27 + * Internally, a queue of pending thumbnails is maintained in mPendingThumbnails.
    1.28 + * The head of the queue is the thumbnail that is currently being processed; upon
    1.29 + * completion of the current thumbnail the next one is automatically processed.
    1.30 + * Changes to the thumbnail width are stashed in mPendingWidth and the change is
    1.31 + * applied between thumbnail processing. This allows a single thumbnail buffer to
    1.32 + * be used for all thumbnails.
    1.33 + */
    1.34 +public final class ThumbnailHelper {
    1.35 +    private static final String LOGTAG = "GeckoThumbnailHelper";
    1.36 +
    1.37 +    public static final float THUMBNAIL_ASPECT_RATIO = 0.571f;  // this is a 4:7 ratio (as per UX decision)
    1.38 +
    1.39 +    // static singleton stuff
    1.40 +
    1.41 +    private static ThumbnailHelper sInstance;
    1.42 +
    1.43 +    public static synchronized ThumbnailHelper getInstance() {
    1.44 +        if (sInstance == null) {
    1.45 +            sInstance = new ThumbnailHelper();
    1.46 +        }
    1.47 +        return sInstance;
    1.48 +    }
    1.49 +
    1.50 +    // instance stuff
    1.51 +
    1.52 +    private final LinkedList<Tab> mPendingThumbnails;    // synchronized access only
    1.53 +    private AtomicInteger mPendingWidth;
    1.54 +    private int mWidth;
    1.55 +    private int mHeight;
    1.56 +    private ByteBuffer mBuffer;
    1.57 +
    1.58 +    private ThumbnailHelper() {
    1.59 +        mPendingThumbnails = new LinkedList<Tab>();
    1.60 +        try {
    1.61 +            mPendingWidth = new AtomicInteger((int)GeckoAppShell.getContext().getResources().getDimension(R.dimen.tab_thumbnail_width));
    1.62 +        } catch (Resources.NotFoundException nfe) { mPendingWidth = new AtomicInteger(0); }
    1.63 +        mWidth = -1;
    1.64 +        mHeight = -1;
    1.65 +    }
    1.66 +
    1.67 +    public void getAndProcessThumbnailFor(Tab tab) {
    1.68 +        if (AboutPages.isAboutHome(tab.getURL())) {
    1.69 +            tab.updateThumbnail(null);
    1.70 +            return;
    1.71 +        }
    1.72 +
    1.73 +        if (tab.getState() == Tab.STATE_DELAYED) {
    1.74 +            String url = tab.getURL();
    1.75 +            if (url != null) {
    1.76 +                byte[] thumbnail = BrowserDB.getThumbnailForUrl(GeckoAppShell.getContext().getContentResolver(), url);
    1.77 +                if (thumbnail != null) {
    1.78 +                    setTabThumbnail(tab, null, thumbnail);
    1.79 +                }
    1.80 +            }
    1.81 +            return;
    1.82 +        }
    1.83 +
    1.84 +        synchronized (mPendingThumbnails) {
    1.85 +            if (mPendingThumbnails.lastIndexOf(tab) > 0) {
    1.86 +                // This tab is already in the queue, so don't add it again.
    1.87 +                // Note that if this tab is only at the *head* of the queue,
    1.88 +                // (i.e. mPendingThumbnails.lastIndexOf(tab) == 0) then we do
    1.89 +                // add it again because it may have already been thumbnailed
    1.90 +                // and now we need to do it again.
    1.91 +                return;
    1.92 +            }
    1.93 +
    1.94 +            mPendingThumbnails.add(tab);
    1.95 +            if (mPendingThumbnails.size() > 1) {
    1.96 +                // Some thumbnail was already being processed, so wait
    1.97 +                // for that to be done.
    1.98 +                return;
    1.99 +            }
   1.100 +        }
   1.101 +        requestThumbnailFor(tab);
   1.102 +    }
   1.103 +
   1.104 +    public void setThumbnailWidth(int width) {
   1.105 +        // Check inverted for safety: Bug 803299 Comment 34.
   1.106 +        if (GeckoAppShell.getScreenDepth() == 24) {
   1.107 +            mPendingWidth.set(width);
   1.108 +        } else {
   1.109 +            // Bug 776906: on 16-bit screens we need to ensure an even width.
   1.110 +            mPendingWidth.set((width & 1) == 0 ? width : width + 1);
   1.111 +        }
   1.112 +    }
   1.113 +
   1.114 +    private void updateThumbnailSize() {
   1.115 +        // Apply any pending width updates.
   1.116 +        mWidth = mPendingWidth.get();
   1.117 +
   1.118 +        mHeight = Math.round(mWidth * THUMBNAIL_ASPECT_RATIO);
   1.119 +
   1.120 +        int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2;
   1.121 +        int capacity = mWidth * mHeight * pixelSize;
   1.122 +        Log.d(LOGTAG, "Using new thumbnail size: " + capacity + " (width " + mWidth + ")");
   1.123 +        if (mBuffer == null || mBuffer.capacity() != capacity) {
   1.124 +            if (mBuffer != null) {
   1.125 +                mBuffer = DirectBufferAllocator.free(mBuffer);
   1.126 +            }
   1.127 +            try {
   1.128 +                mBuffer = DirectBufferAllocator.allocate(capacity);
   1.129 +            } catch (IllegalArgumentException iae) {
   1.130 +                Log.w(LOGTAG, iae.toString());
   1.131 +            } catch (OutOfMemoryError oom) {
   1.132 +                Log.w(LOGTAG, "Unable to allocate thumbnail buffer of capacity " + capacity);
   1.133 +            }
   1.134 +            // If we hit an error above, mBuffer will be pointing to null, so we are in a sane state.
   1.135 +        }
   1.136 +    }
   1.137 +
   1.138 +    private void requestThumbnailFor(Tab tab) {
   1.139 +        updateThumbnailSize();
   1.140 +
   1.141 +        if (mBuffer == null) {
   1.142 +            // Buffer allocation may have failed. In this case we can't send the
   1.143 +            // event requesting the screenshot which means we won't get back a response
   1.144 +            // and so our queue will grow unboundedly. Handle this scenario by clearing
   1.145 +            // the queue (no point trying more thumbnailing right now since we're likely
   1.146 +            // low on memory). We will try again normally on the next call to
   1.147 +            // getAndProcessThumbnailFor which will hopefully be when we have more free memory.
   1.148 +            synchronized (mPendingThumbnails) {
   1.149 +                mPendingThumbnails.clear();
   1.150 +            }
   1.151 +            return;
   1.152 +        }
   1.153 +
   1.154 +        Log.d(LOGTAG, "Sending thumbnail event: " + mWidth + ", " + mHeight);
   1.155 +        GeckoEvent e = GeckoEvent.createThumbnailEvent(tab.getId(), mWidth, mHeight, mBuffer);
   1.156 +        GeckoAppShell.sendEventToGecko(e);
   1.157 +    }
   1.158 +
   1.159 +    /* This method is invoked by JNI once the thumbnail data is ready. */
   1.160 +    @WrapElementForJNI(stubName = "SendThumbnail")
   1.161 +    public static void notifyThumbnail(ByteBuffer data, int tabId, boolean success) {
   1.162 +        Tab tab = Tabs.getInstance().getTab(tabId);
   1.163 +        ThumbnailHelper helper = ThumbnailHelper.getInstance();
   1.164 +        if (success && tab != null) {
   1.165 +            helper.handleThumbnailData(tab, data);
   1.166 +        }
   1.167 +        helper.processNextThumbnail(tab);
   1.168 +    }
   1.169 +
   1.170 +    private void processNextThumbnail(Tab tab) {
   1.171 +        Tab nextTab = null;
   1.172 +        synchronized (mPendingThumbnails) {
   1.173 +            if (tab != null && tab != mPendingThumbnails.peek()) {
   1.174 +                Log.e(LOGTAG, "handleThumbnailData called with unexpected tab's data!");
   1.175 +                // This should never happen, but recover gracefully by processing the
   1.176 +                // unexpected tab that we found in the queue
   1.177 +            } else {
   1.178 +                mPendingThumbnails.remove();
   1.179 +            }
   1.180 +            nextTab = mPendingThumbnails.peek();
   1.181 +        }
   1.182 +        if (nextTab != null) {
   1.183 +            requestThumbnailFor(nextTab);
   1.184 +        }
   1.185 +    }
   1.186 +
   1.187 +    private void handleThumbnailData(Tab tab, ByteBuffer data) {
   1.188 +        Log.d(LOGTAG, "handleThumbnailData: " + data.capacity());
   1.189 +        if (data != mBuffer) {
   1.190 +            // This should never happen, but log it and recover gracefully
   1.191 +            Log.e(LOGTAG, "handleThumbnailData called with an unexpected ByteBuffer!");
   1.192 +        }
   1.193 +
   1.194 +        if (shouldUpdateThumbnail(tab)) {
   1.195 +            processThumbnailData(tab, data);
   1.196 +        }
   1.197 +    }
   1.198 +
   1.199 +    private void processThumbnailData(Tab tab, ByteBuffer data) {
   1.200 +        Bitmap b = tab.getThumbnailBitmap(mWidth, mHeight);
   1.201 +        data.position(0);
   1.202 +        b.copyPixelsFromBuffer(data);
   1.203 +        setTabThumbnail(tab, b, null);
   1.204 +    }
   1.205 +
   1.206 +    private void setTabThumbnail(Tab tab, Bitmap bitmap, byte[] compressed) {
   1.207 +        if (bitmap == null) {
   1.208 +            if (compressed == null) {
   1.209 +                Log.w(LOGTAG, "setTabThumbnail: one of bitmap or compressed must be non-null!");
   1.210 +                return;
   1.211 +            }
   1.212 +            bitmap = BitmapUtils.decodeByteArray(compressed);
   1.213 +        }
   1.214 +        tab.updateThumbnail(bitmap);
   1.215 +    }
   1.216 +
   1.217 +    private boolean shouldUpdateThumbnail(Tab tab) {
   1.218 +        return (Tabs.getInstance().isSelectedTab(tab) || (GeckoAppShell.getGeckoInterface() != null && GeckoAppShell.getGeckoInterface().areTabsShown()));
   1.219 +    }
   1.220 +}

mercurial