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 
17 package com.android.camera.data;
18 
19 import android.content.Context;
20 import android.graphics.Bitmap;
21 import android.net.Uri;
22 import android.opengl.EGL14;
23 import android.opengl.EGLConfig;
24 import android.opengl.EGLContext;
25 import android.opengl.EGLDisplay;
26 import android.opengl.EGLSurface;
27 import android.opengl.GLES20;
28 
29 import com.android.camera.debug.Log;
30 import com.android.camera.debug.Log.Tag;
31 import com.android.camera.util.Size;
32 import com.android.camera2.R;
33 import com.bumptech.glide.DrawableRequestBuilder;
34 import com.bumptech.glide.GenericRequestBuilder;
35 import com.bumptech.glide.Glide;
36 import com.bumptech.glide.RequestManager;
37 import com.bumptech.glide.load.Key;
38 import com.bumptech.glide.load.resource.bitmap.BitmapEncoder;
39 import com.bumptech.glide.load.resource.drawable.GlideDrawable;
40 import com.bumptech.glide.load.resource.gif.GifResourceEncoder;
41 import com.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapperResourceEncoder;
42 import com.bumptech.glide.load.resource.transcode.BitmapToGlideDrawableTranscoder;
43 
44 /**
45  * Manage common glide image requests for the camera filmstrip.
46  */
47 public 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;
getMaxImageDisplaySize()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 
GlideFilmstripManager(Context context)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      */
loadFull(Uri uri, Key key, Size original)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      */
loadScreen(Uri uri, Key key, Size original)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      */
loadMediaStoreThumb(Uri uri, Key key)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      */
loadTinyThumb(Uri uri, Key key)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      */
clampSize(Size original, double maxArea, Size maxSize)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 
computeFitWithinSize(Size original, Size maxSize)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      */
computeEglMaxTextureSize()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