summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/ThumbnailHelper.java
blob: 3a701243166bfd4084f1d77facec8b41cde3d892 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko;

import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.util.ResourceDrawableUtils;
import org.mozilla.gecko.mozglue.DirectBufferAllocator;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.util.Log;

import java.nio.ByteBuffer;
import java.util.ArrayList;

/**
 * Helper class to generate thumbnails for tabs.
 * Internally, a queue of pending thumbnails is maintained in mPendingThumbnails.
 * The head of the queue is the thumbnail that is currently being processed; upon
 * completion of the current thumbnail the next one is automatically processed.
 * Changes to the thumbnail width are stashed in mPendingWidth and the change is
 * applied between thumbnail processing. This allows a single thumbnail buffer to
 * be used for all thumbnails.
 */
public final class ThumbnailHelper {
    private static final boolean DEBUG = false;
    private static final String LOGTAG = "GeckoThumbnailHelper";

    public static final float TABS_PANEL_THUMBNAIL_ASPECT_RATIO = 0.8333333f;
    public static final float TOP_SITES_THUMBNAIL_ASPECT_RATIO = 0.571428571f;  // this is a 4:7 ratio (as per UX decision)
    public static final float THUMBNAIL_ASPECT_RATIO;

    static {
      // As we only want to generate one thumbnail for each tab, we calculate the
      // largest aspect ratio required and create the thumbnail based off that.
      // Any views with a smaller aspect ratio will use a cropped version of the
      // same image.
      THUMBNAIL_ASPECT_RATIO = Math.max(TABS_PANEL_THUMBNAIL_ASPECT_RATIO, TOP_SITES_THUMBNAIL_ASPECT_RATIO);
    }

    public enum CachePolicy {
        STORE,
        NO_STORE
    }

    // static singleton stuff

    private static ThumbnailHelper sInstance;

    public static synchronized ThumbnailHelper getInstance() {
        if (sInstance == null) {
            sInstance = new ThumbnailHelper();
        }
        return sInstance;
    }

    // instance stuff

    private final ArrayList<Tab> mPendingThumbnails;    // synchronized access only
    private volatile int mPendingWidth;
    private int mWidth;
    private int mHeight;
    private ByteBuffer mBuffer;

    private ThumbnailHelper() {
        final Resources res = GeckoAppShell.getContext().getResources();

        mPendingThumbnails = new ArrayList<>();
        try {
            mPendingWidth = (int) res.getDimension(R.dimen.tab_thumbnail_width);
        } catch (Resources.NotFoundException nfe) {
        }
        mWidth = -1;
        mHeight = -1;
    }

    public void getAndProcessThumbnailFor(final int tabId, final ResourceDrawableUtils.BitmapLoader loader) {
        final Tab tab = Tabs.getInstance().getTab(tabId);
        if (tab != null) {
            getAndProcessThumbnailFor(tab, loader);
        }
    }

    public void getAndProcessThumbnailFor(final Tab tab, final ResourceDrawableUtils.BitmapLoader loader) {
        ResourceDrawableUtils.runOnBitmapFoundOnUiThread(loader, tab.getThumbnail());

        Tabs.registerOnTabsChangedListener(new Tabs.OnTabsChangedListener() {
                @Override
                public void onTabChanged(final Tab t, final Tabs.TabEvents msg, final String data) {
                    if (tab != t || msg != Tabs.TabEvents.THUMBNAIL) {
                        return;
                    }
                    Tabs.unregisterOnTabsChangedListener(this);
                    ResourceDrawableUtils.runOnBitmapFoundOnUiThread(loader, t.getThumbnail());
                }
            });
        getAndProcessThumbnailFor(tab);
    }

    public void getAndProcessThumbnailFor(Tab tab) {
        if (AboutPages.isAboutHome(tab.getURL()) || AboutPages.isAboutPrivateBrowsing(tab.getURL())) {
            tab.updateThumbnail(null, CachePolicy.NO_STORE);
            return;
        }

        synchronized (mPendingThumbnails) {
            if (mPendingThumbnails.lastIndexOf(tab) > 0) {
                // This tab is already in the queue, so don't add it again.
                // Note that if this tab is only at the *head* of the queue,
                // (i.e. mPendingThumbnails.lastIndexOf(tab) == 0) then we do
                // add it again because it may have already been thumbnailed
                // and now we need to do it again.
                return;
            }

            mPendingThumbnails.add(tab);
            if (mPendingThumbnails.size() > 1) {
                // Some thumbnail was already being processed, so wait
                // for that to be done.
                return;
            }

            requestThumbnailLocked(tab);
        }
    }

    public void setThumbnailWidth(int width) {
        // Check inverted for safety: Bug 803299 Comment 34.
        if (GeckoAppShell.getScreenDepth() == 24) {
            mPendingWidth = width;
        } else {
            // Bug 776906: on 16-bit screens we need to ensure an even width.
            mPendingWidth = (width + 1) & (~1);
        }
    }

    private void updateThumbnailSizeLocked() {
        // Apply any pending width updates.
        mWidth = mPendingWidth;
        mHeight = Math.round(mWidth * THUMBNAIL_ASPECT_RATIO);

        int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2;
        int capacity = mWidth * mHeight * pixelSize;
        if (DEBUG) {
            Log.d(LOGTAG, "Using new thumbnail size: " + capacity +
                          " (width " + mWidth + " - height " + mHeight + ")");
        }
        if (mBuffer == null || mBuffer.capacity() != capacity) {
            if (mBuffer != null) {
                mBuffer = DirectBufferAllocator.free(mBuffer);
            }
            try {
                mBuffer = DirectBufferAllocator.allocate(capacity);
            } catch (IllegalArgumentException iae) {
                Log.w(LOGTAG, iae.toString());
            } catch (OutOfMemoryError oom) {
                Log.w(LOGTAG, "Unable to allocate thumbnail buffer of capacity " + capacity);
            }
            // If we hit an error above, mBuffer will be pointing to null, so we are in a sane state.
        }
    }

    private void requestThumbnailLocked(Tab tab) {
        updateThumbnailSizeLocked();

        if (mBuffer == null) {
            // Buffer allocation may have failed. In this case we can't send the
            // event requesting the screenshot which means we won't get back a response
            // and so our queue will grow unboundedly. Handle this scenario by clearing
            // the queue (no point trying more thumbnailing right now since we're likely
            // low on memory). We will try again normally on the next call to
            // getAndProcessThumbnailFor which will hopefully be when we have more free memory.
            mPendingThumbnails.clear();
            return;
        }

        if (DEBUG) {
            Log.d(LOGTAG, "Sending thumbnail event: " + mWidth + ", " + mHeight);
        }
        requestThumbnailLocked(mBuffer, tab, tab.getId(), mWidth, mHeight);
    }

    @WrapForJNI(stubName = "RequestThumbnail", dispatchTo = "proxy")
    private static native void requestThumbnailLocked(ByteBuffer data, Tab tab, int tabId,
                                                      int width, int height);

    /* This method is invoked by JNI once the thumbnail data is ready. */
    @WrapForJNI(calledFrom = "gecko")
    private static void notifyThumbnail(final ByteBuffer data, final Tab tab,
                                        final boolean success, final boolean shouldStore) {
        final ThumbnailHelper helper = ThumbnailHelper.getInstance();
        if (success) {
            helper.handleThumbnailData(
                    tab, data, shouldStore ? CachePolicy.STORE : CachePolicy.NO_STORE);
        }
        helper.processNextThumbnail();
    }

    private void processNextThumbnail() {
        synchronized (mPendingThumbnails) {
            if (mPendingThumbnails.isEmpty()) {
                return;
            }

            mPendingThumbnails.remove(0);

            if (!mPendingThumbnails.isEmpty()) {
                requestThumbnailLocked(mPendingThumbnails.get(0));
            }
        }
    }

    private void handleThumbnailData(Tab tab, ByteBuffer data, CachePolicy cachePolicy) {
        if (DEBUG) {
            Log.d(LOGTAG, "handleThumbnailData: " + data.capacity());
        }
        if (data != mBuffer) {
            // This should never happen, but log it and recover gracefully
            Log.e(LOGTAG, "handleThumbnailData called with an unexpected ByteBuffer!");
        }

        processThumbnailData(tab, data, cachePolicy);
    }

    private void processThumbnailData(Tab tab, ByteBuffer data, CachePolicy cachePolicy) {
        Bitmap b = tab.getThumbnailBitmap(mWidth, mHeight);
        data.position(0);
        b.copyPixelsFromBuffer(data);
        setTabThumbnail(tab, b, null, cachePolicy);
    }

    private void setTabThumbnail(Tab tab, Bitmap bitmap, byte[] compressed, CachePolicy cachePolicy) {
        if (bitmap == null) {
            if (compressed == null) {
                Log.w(LOGTAG, "setTabThumbnail: one of bitmap or compressed must be non-null!");
                return;
            }
            bitmap = BitmapUtils.decodeByteArray(compressed);
        }
        tab.updateThumbnail(bitmap, cachePolicy);
    }
}