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