1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.camera.data;
18
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.net.Uri;
22import android.opengl.EGL14;
23import android.opengl.EGLConfig;
24import android.opengl.EGLContext;
25import android.opengl.EGLDisplay;
26import android.opengl.EGLSurface;
27import android.opengl.GLES20;
28
29import com.android.camera.debug.Log;
30import com.android.camera.debug.Log.Tag;
31import com.android.camera.util.Size;
32import com.android.camera2.R;
33import com.bumptech.glide.DrawableRequestBuilder;
34import com.bumptech.glide.GenericRequestBuilder;
35import com.bumptech.glide.Glide;
36import com.bumptech.glide.RequestManager;
37import com.bumptech.glide.load.Key;
38import com.bumptech.glide.load.resource.bitmap.BitmapEncoder;
39import com.bumptech.glide.load.resource.drawable.GlideDrawable;
40import com.bumptech.glide.load.resource.gif.GifResourceEncoder;
41import com.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapperResourceEncoder;
42import com.bumptech.glide.load.resource.transcode.BitmapToGlideDrawableTranscoder;
43
44/**
45 * Manage common glide image requests for the camera filmstrip.
46 */
47public final class GlideFilmstripManager {
48    private static final Tag TAG = new Tag("GlideFlmMgr");
49
50    /** Default placeholder to display while images load */
51    public static final int DEFAULT_PLACEHOLDER_RESOURCE = R.color.photo_placeholder;
52
53    // This is the default GL texture size for K and below, it may be bigger,
54    // it should not be smaller than this.
55    private static final int DEFAULT_MAX_IMAGE_DISPLAY_SIZE = 2048;
56
57    // Some phones have massive GL_Texture sizes. Prevent images from doing
58    // overly large allocations by capping the texture size.
59    private static final int MAX_GL_TEXTURE_SIZE = 4096;
60    private static Size MAX_IMAGE_DISPLAY_SIZE;
61    public static Size getMaxImageDisplaySize() {
62        if (MAX_IMAGE_DISPLAY_SIZE == null) {
63            Integer size = computeEglMaxTextureSize();
64            if (size == null) {
65                // Fallback to the default 2048 if a size is not found.
66                MAX_IMAGE_DISPLAY_SIZE = new Size(DEFAULT_MAX_IMAGE_DISPLAY_SIZE,
67                      DEFAULT_MAX_IMAGE_DISPLAY_SIZE);
68            } else if (size > MAX_GL_TEXTURE_SIZE) {
69                // Cap the display size to prevent Out of memory problems during
70                // pre-allocation of huge bitmaps.
71                MAX_IMAGE_DISPLAY_SIZE = new Size(MAX_GL_TEXTURE_SIZE, MAX_GL_TEXTURE_SIZE);
72            } else {
73                MAX_IMAGE_DISPLAY_SIZE = new Size(size, size);
74            }
75        }
76
77        return MAX_IMAGE_DISPLAY_SIZE;
78    }
79
80    public static final Size MEDIASTORE_THUMB_SIZE = new Size(512, 384);
81    public static final Size TINY_THUMB_SIZE = new Size(256, 256);
82
83    // Estimated memory bandwidth for N5 and N6 is about 500MB/s
84    // 500MBs * 1000000(Bytes per MB) / 4 (RGBA pixel) / 1000 (milli per S)
85    // Give a 20% margin for error and real conditions.
86    private static final int EST_PIXELS_PER_MILLI = 100000;
87
88    // Estimated number of bytes that can be used to usually display a thumbnail
89    // in under a frame at 60fps (16ms).
90    public static final int MAXIMUM_SMOOTH_PIXELS = EST_PIXELS_PER_MILLI * 10 /* millis */;
91
92    // Estimated number of bytes that can be used to generate a large thumbnail in under
93    // (about) 3 frames at 60fps (16ms).
94    public static final int MAXIMUM_FULL_RES_PIXELS = EST_PIXELS_PER_MILLI * 45 /* millis */;
95    public static final int JPEG_COMPRESS_QUALITY = 90;
96
97    private final GenericRequestBuilder<Uri, ?, ?, GlideDrawable> mTinyImageBuilder;
98    private final DrawableRequestBuilder<Uri> mLargeImageBuilder;
99
100    public GlideFilmstripManager(Context context) {
101        Glide glide = Glide.get(context);
102        BitmapEncoder bitmapEncoder = new BitmapEncoder(Bitmap.CompressFormat.JPEG,
103              JPEG_COMPRESS_QUALITY);
104        GifBitmapWrapperResourceEncoder drawableEncoder = new GifBitmapWrapperResourceEncoder(
105              bitmapEncoder,
106              new GifResourceEncoder(glide.getBitmapPool()));
107        RequestManager request = Glide.with(context);
108
109        mTinyImageBuilder = request
110              .fromMediaStore()
111              .asBitmap() // This prevents gifs from animating at tiny sizes.
112              .transcode(new BitmapToGlideDrawableTranscoder(context), GlideDrawable.class)
113              .fitCenter()
114              .placeholder(DEFAULT_PLACEHOLDER_RESOURCE)
115              .dontAnimate();
116
117        mLargeImageBuilder = request
118              .fromMediaStore()
119              .encoder(drawableEncoder)
120              .fitCenter()
121              .placeholder(DEFAULT_PLACEHOLDER_RESOURCE)
122              .dontAnimate();
123    }
124
125    /**
126     * Create a full size drawable request for a given width and height that is
127     * as large as we can reasonably load into a view without causing massive
128     * jank problems or blank frames due to overly large textures.
129     */
130    public final DrawableRequestBuilder<Uri> loadFull(Uri uri, Key key, Size original) {
131        Size size = clampSize(original, MAXIMUM_FULL_RES_PIXELS, getMaxImageDisplaySize());
132
133        return mLargeImageBuilder
134              .clone()
135              .load(uri)
136              .signature(key)
137              .override(size.width(), size.height());
138    }
139
140    /**
141     * Create a full size drawable request for a given width and height that is
142     * smaller than loadFull, but is intended be large enough to fill the screen
143     * pixels.
144     */
145    public DrawableRequestBuilder<Uri> loadScreen(Uri uri, Key key, Size original) {
146        Size size = clampSize(original, MAXIMUM_SMOOTH_PIXELS, getMaxImageDisplaySize());
147        return mLargeImageBuilder
148              .clone()
149              .load(uri)
150              .signature(key)
151              .override(size.width(), size.height());
152    }
153
154    /**
155     * Create a small thumbnail sized image that has the same bounds as the
156     * media store thumbnail images.
157     *
158     * If the Uri points at an animated gif, the gif will not play.
159     */
160    public GenericRequestBuilder<Uri, ?, ?, GlideDrawable> loadMediaStoreThumb(Uri uri, Key key) {
161        Size size = clampSize(MEDIASTORE_THUMB_SIZE, MAXIMUM_SMOOTH_PIXELS, getMaxImageDisplaySize());
162        return mTinyImageBuilder
163              .clone()
164              .load(uri)
165              .signature(key)
166                    // This attempts to ensure we load the cached media store version.
167              .override(size.width(), size.height());
168    }
169
170    /**
171     * Create very tiny thumbnail request that should complete as fast
172     * as possible.
173     *
174     * If the Uri points at an animated gif, the gif will not play.
175     */
176    public GenericRequestBuilder<Uri, ?, ?, GlideDrawable> loadTinyThumb(Uri uri, Key key) {
177        Size size = clampSize(TINY_THUMB_SIZE, MAXIMUM_SMOOTH_PIXELS,  getMaxImageDisplaySize());
178        return mTinyImageBuilder
179              .clone()
180              .load(uri)
181              .signature(key)
182              .override(size.width(), size.height());
183    }
184
185    /**
186     * Given a size, compute a value such that it will downscale the original size
187     * to fit within the maxSize bounding box and to be less than the provided area.
188     *
189     * This will never upscale sizes.
190     */
191    private static Size clampSize(Size original, double maxArea, Size maxSize) {
192        if (original.getWidth() * original.getHeight() < maxArea &&
193              original.getWidth() < maxSize.getWidth() &&
194              original.getHeight() < maxSize.getHeight()) {
195            // In several cases, the size is smaller than the max, and the area is
196            // smaller than the max area.
197            return original;
198        }
199
200        // Compute a ratio that will keep the number of pixels in the image (hence,
201        // the number of bytes that can be copied into memory) under the maxArea.
202        double ratio = Math.min(Math.sqrt(maxArea / original.area()), 1.0f);
203        int width = (int) Math.round(original.width() * ratio);
204        int height = (int) Math.round(original.height() * ratio);
205
206        // If that ratio results in an image where the edge length is still too large,
207        // constrain based on max edge length instead.
208        if (width > maxSize.width() || height > maxSize.height()) {
209            return computeFitWithinSize(original, maxSize);
210        }
211
212        return new Size(width, height);
213    }
214
215    private static Size computeFitWithinSize(Size original, Size maxSize) {
216        double widthRatio = (double) maxSize.width() / original.width();
217        double heightRatio = (double) maxSize.height() / original.height();
218
219        double ratio = widthRatio > heightRatio ? heightRatio : widthRatio;
220
221        // This rounds and ensures that (even with rounding and int conversion)
222        // that the returned size is never larger than maxSize.
223        return new Size(
224              Math.min((int) Math.round(original.width() * ratio), maxSize.width()),
225              Math.min((int) Math.round(original.height() * ratio), maxSize.height()));
226    }
227
228    /**
229     * Ridiculous way to read the devices maximum texture size because no other
230     * way is provided.
231     */
232    private static Integer computeEglMaxTextureSize() {
233        EGLDisplay eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
234        int[] majorMinor = new int[2];
235        EGL14.eglInitialize(eglDisplay, majorMinor, 0, majorMinor, 1);
236
237        int[] configAttr = {
238              EGL14.EGL_COLOR_BUFFER_TYPE, EGL14.EGL_RGB_BUFFER,
239              EGL14.EGL_LEVEL, 0,
240              EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
241              EGL14.EGL_SURFACE_TYPE, EGL14.EGL_PBUFFER_BIT,
242              EGL14.EGL_NONE
243        };
244        EGLConfig[] eglConfigs = new EGLConfig[1];
245        int[] configCount = new int[1];
246        EGL14.eglChooseConfig(eglDisplay, configAttr, 0,
247              eglConfigs, 0, 1, configCount, 0);
248
249        if (configCount[0] == 0) {
250            Log.w(TAG, "No EGL configurations found!");
251            return null;
252        }
253        EGLConfig eglConfig = eglConfigs[0];
254
255        // Create a tiny surface
256        int[] eglSurfaceAttributes = {
257              EGL14.EGL_WIDTH, 64,
258              EGL14.EGL_HEIGHT, 64,
259              EGL14.EGL_NONE
260        };
261        //
262        EGLSurface eglSurface = EGL14.eglCreatePbufferSurface(eglDisplay, eglConfig,
263              eglSurfaceAttributes, 0);
264
265        int[] eglContextAttributes = {
266              EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
267              EGL14.EGL_NONE
268        };
269
270        // Create an EGL context.
271        EGLContext eglContext = EGL14.eglCreateContext(eglDisplay, eglConfig, EGL14.EGL_NO_CONTEXT,
272              eglContextAttributes, 0);
273        EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);
274
275        // Actually read the Gl_MAX_TEXTURE_SIZE into the array.
276        int[] maxSize = new int[1];
277        GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxSize, 0);
278        int result = maxSize[0];
279
280        // Tear down the surface, context, and display.
281        EGL14.eglMakeCurrent(eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
282              EGL14.EGL_NO_CONTEXT);
283        EGL14.eglDestroySurface(eglDisplay, eglSurface);
284        EGL14.eglDestroyContext(eglDisplay, eglContext);
285        EGL14.eglTerminate(eglDisplay);
286
287        // Return the computed max size.
288        return result;
289    }
290}
291