/*
|
* Copyright (C) 2015 The Android Open Source Project
|
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
*/
|
|
package com.android.camera.data;
|
|
import android.content.Context;
|
import android.graphics.Bitmap;
|
import android.net.Uri;
|
import android.opengl.EGL14;
|
import android.opengl.EGLConfig;
|
import android.opengl.EGLContext;
|
import android.opengl.EGLDisplay;
|
import android.opengl.EGLSurface;
|
import android.opengl.GLES20;
|
|
import com.android.camera.debug.Log;
|
import com.android.camera.debug.Log.Tag;
|
import com.android.camera.util.Size;
|
import com.android.camera2.R;
|
import com.bumptech.glide.DrawableRequestBuilder;
|
import com.bumptech.glide.GenericRequestBuilder;
|
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.RequestManager;
|
import com.bumptech.glide.load.Key;
|
import com.bumptech.glide.load.resource.bitmap.BitmapEncoder;
|
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
|
import com.bumptech.glide.load.resource.gif.GifResourceEncoder;
|
import com.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapperResourceEncoder;
|
import com.bumptech.glide.load.resource.transcode.BitmapToGlideDrawableTranscoder;
|
|
/**
|
* Manage common glide image requests for the camera filmstrip.
|
*/
|
public final class GlideFilmstripManager {
|
private static final Tag TAG = new Tag("GlideFlmMgr");
|
|
/** Default placeholder to display while images load */
|
public static final int DEFAULT_PLACEHOLDER_RESOURCE = R.color.photo_placeholder;
|
|
// This is the default GL texture size for K and below, it may be bigger,
|
// it should not be smaller than this.
|
private static final int DEFAULT_MAX_IMAGE_DISPLAY_SIZE = 2048;
|
|
// Some phones have massive GL_Texture sizes. Prevent images from doing
|
// overly large allocations by capping the texture size.
|
private static final int MAX_GL_TEXTURE_SIZE = 4096;
|
private static Size MAX_IMAGE_DISPLAY_SIZE;
|
public static Size getMaxImageDisplaySize() {
|
if (MAX_IMAGE_DISPLAY_SIZE == null) {
|
Integer size = computeEglMaxTextureSize();
|
if (size == null) {
|
// Fallback to the default 2048 if a size is not found.
|
MAX_IMAGE_DISPLAY_SIZE = new Size(DEFAULT_MAX_IMAGE_DISPLAY_SIZE,
|
DEFAULT_MAX_IMAGE_DISPLAY_SIZE);
|
} else if (size > MAX_GL_TEXTURE_SIZE) {
|
// Cap the display size to prevent Out of memory problems during
|
// pre-allocation of huge bitmaps.
|
MAX_IMAGE_DISPLAY_SIZE = new Size(MAX_GL_TEXTURE_SIZE, MAX_GL_TEXTURE_SIZE);
|
} else {
|
MAX_IMAGE_DISPLAY_SIZE = new Size(size, size);
|
}
|
}
|
|
return MAX_IMAGE_DISPLAY_SIZE;
|
}
|
|
public static final Size MEDIASTORE_THUMB_SIZE = new Size(512, 384);
|
public static final Size TINY_THUMB_SIZE = new Size(256, 256);
|
|
// Estimated memory bandwidth for N5 and N6 is about 500MB/s
|
// 500MBs * 1000000(Bytes per MB) / 4 (RGBA pixel) / 1000 (milli per S)
|
// Give a 20% margin for error and real conditions.
|
private static final int EST_PIXELS_PER_MILLI = 100000;
|
|
// Estimated number of bytes that can be used to usually display a thumbnail
|
// in under a frame at 60fps (16ms).
|
public static final int MAXIMUM_SMOOTH_PIXELS = EST_PIXELS_PER_MILLI * 10 /* millis */;
|
|
// Estimated number of bytes that can be used to generate a large thumbnail in under
|
// (about) 3 frames at 60fps (16ms).
|
public static final int MAXIMUM_FULL_RES_PIXELS = EST_PIXELS_PER_MILLI * 45 /* millis */;
|
public static final int JPEG_COMPRESS_QUALITY = 90;
|
|
private final GenericRequestBuilder<Uri, ?, ?, GlideDrawable> mTinyImageBuilder;
|
private final DrawableRequestBuilder<Uri> mLargeImageBuilder;
|
|
public GlideFilmstripManager(Context context) {
|
Glide glide = Glide.get(context);
|
BitmapEncoder bitmapEncoder = new BitmapEncoder(Bitmap.CompressFormat.JPEG,
|
JPEG_COMPRESS_QUALITY);
|
GifBitmapWrapperResourceEncoder drawableEncoder = new GifBitmapWrapperResourceEncoder(
|
bitmapEncoder,
|
new GifResourceEncoder(glide.getBitmapPool()));
|
RequestManager request = Glide.with(context);
|
|
mTinyImageBuilder = request
|
.fromMediaStore()
|
.asBitmap() // This prevents gifs from animating at tiny sizes.
|
.transcode(new BitmapToGlideDrawableTranscoder(context), GlideDrawable.class)
|
.fitCenter()
|
.placeholder(DEFAULT_PLACEHOLDER_RESOURCE)
|
.dontAnimate();
|
|
mLargeImageBuilder = request
|
.fromMediaStore()
|
.encoder(drawableEncoder)
|
.fitCenter()
|
.placeholder(DEFAULT_PLACEHOLDER_RESOURCE)
|
.dontAnimate();
|
}
|
|
/**
|
* Create a full size drawable request for a given width and height that is
|
* as large as we can reasonably load into a view without causing massive
|
* jank problems or blank frames due to overly large textures.
|
*/
|
public final DrawableRequestBuilder<Uri> loadFull(Uri uri, Key key, Size original) {
|
Size size = clampSize(original, MAXIMUM_FULL_RES_PIXELS, getMaxImageDisplaySize());
|
|
return mLargeImageBuilder
|
.clone()
|
.load(uri)
|
.signature(key)
|
.override(size.width(), size.height());
|
}
|
|
/**
|
* Create a full size drawable request for a given width and height that is
|
* smaller than loadFull, but is intended be large enough to fill the screen
|
* pixels.
|
*/
|
public DrawableRequestBuilder<Uri> loadScreen(Uri uri, Key key, Size original) {
|
Size size = clampSize(original, MAXIMUM_SMOOTH_PIXELS, getMaxImageDisplaySize());
|
return mLargeImageBuilder
|
.clone()
|
.load(uri)
|
.signature(key)
|
.override(size.width(), size.height());
|
}
|
|
/**
|
* Create a small thumbnail sized image that has the same bounds as the
|
* media store thumbnail images.
|
*
|
* If the Uri points at an animated gif, the gif will not play.
|
*/
|
public GenericRequestBuilder<Uri, ?, ?, GlideDrawable> loadMediaStoreThumb(Uri uri, Key key) {
|
Size size = clampSize(MEDIASTORE_THUMB_SIZE, MAXIMUM_SMOOTH_PIXELS, getMaxImageDisplaySize());
|
return mTinyImageBuilder
|
.clone()
|
.load(uri)
|
.signature(key)
|
// This attempts to ensure we load the cached media store version.
|
.override(size.width(), size.height());
|
}
|
|
/**
|
* Create very tiny thumbnail request that should complete as fast
|
* as possible.
|
*
|
* If the Uri points at an animated gif, the gif will not play.
|
*/
|
public GenericRequestBuilder<Uri, ?, ?, GlideDrawable> loadTinyThumb(Uri uri, Key key) {
|
Size size = clampSize(TINY_THUMB_SIZE, MAXIMUM_SMOOTH_PIXELS, getMaxImageDisplaySize());
|
return mTinyImageBuilder
|
.clone()
|
.load(uri)
|
.signature(key)
|
.override(size.width(), size.height());
|
}
|
|
/**
|
* Given a size, compute a value such that it will downscale the original size
|
* to fit within the maxSize bounding box and to be less than the provided area.
|
*
|
* This will never upscale sizes.
|
*/
|
private static Size clampSize(Size original, double maxArea, Size maxSize) {
|
if (original.getWidth() * original.getHeight() < maxArea &&
|
original.getWidth() < maxSize.getWidth() &&
|
original.getHeight() < maxSize.getHeight()) {
|
// In several cases, the size is smaller than the max, and the area is
|
// smaller than the max area.
|
return original;
|
}
|
|
// Compute a ratio that will keep the number of pixels in the image (hence,
|
// the number of bytes that can be copied into memory) under the maxArea.
|
double ratio = Math.min(Math.sqrt(maxArea / original.area()), 1.0f);
|
int width = (int) Math.round(original.width() * ratio);
|
int height = (int) Math.round(original.height() * ratio);
|
|
// If that ratio results in an image where the edge length is still too large,
|
// constrain based on max edge length instead.
|
if (width > maxSize.width() || height > maxSize.height()) {
|
return computeFitWithinSize(original, maxSize);
|
}
|
|
return new Size(width, height);
|
}
|
|
private static Size computeFitWithinSize(Size original, Size maxSize) {
|
double widthRatio = (double) maxSize.width() / original.width();
|
double heightRatio = (double) maxSize.height() / original.height();
|
|
double ratio = widthRatio > heightRatio ? heightRatio : widthRatio;
|
|
// This rounds and ensures that (even with rounding and int conversion)
|
// that the returned size is never larger than maxSize.
|
return new Size(
|
Math.min((int) Math.round(original.width() * ratio), maxSize.width()),
|
Math.min((int) Math.round(original.height() * ratio), maxSize.height()));
|
}
|
|
/**
|
* Ridiculous way to read the devices maximum texture size because no other
|
* way is provided.
|
*/
|
private static Integer computeEglMaxTextureSize() {
|
EGLDisplay eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
|
int[] majorMinor = new int[2];
|
EGL14.eglInitialize(eglDisplay, majorMinor, 0, majorMinor, 1);
|
|
int[] configAttr = {
|
EGL14.EGL_COLOR_BUFFER_TYPE, EGL14.EGL_RGB_BUFFER,
|
EGL14.EGL_LEVEL, 0,
|
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
|
EGL14.EGL_SURFACE_TYPE, EGL14.EGL_PBUFFER_BIT,
|
EGL14.EGL_NONE
|
};
|
EGLConfig[] eglConfigs = new EGLConfig[1];
|
int[] configCount = new int[1];
|
EGL14.eglChooseConfig(eglDisplay, configAttr, 0,
|
eglConfigs, 0, 1, configCount, 0);
|
|
if (configCount[0] == 0) {
|
Log.w(TAG, "No EGL configurations found!");
|
return null;
|
}
|
EGLConfig eglConfig = eglConfigs[0];
|
|
// Create a tiny surface
|
int[] eglSurfaceAttributes = {
|
EGL14.EGL_WIDTH, 64,
|
EGL14.EGL_HEIGHT, 64,
|
EGL14.EGL_NONE
|
};
|
//
|
EGLSurface eglSurface = EGL14.eglCreatePbufferSurface(eglDisplay, eglConfig,
|
eglSurfaceAttributes, 0);
|
|
int[] eglContextAttributes = {
|
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
|
EGL14.EGL_NONE
|
};
|
|
// Create an EGL context.
|
EGLContext eglContext = EGL14.eglCreateContext(eglDisplay, eglConfig, EGL14.EGL_NO_CONTEXT,
|
eglContextAttributes, 0);
|
EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);
|
|
// Actually read the Gl_MAX_TEXTURE_SIZE into the array.
|
int[] maxSize = new int[1];
|
GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxSize, 0);
|
int result = maxSize[0];
|
|
// Tear down the surface, context, and display.
|
EGL14.eglMakeCurrent(eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
|
EGL14.EGL_NO_CONTEXT);
|
EGL14.eglDestroySurface(eglDisplay, eglSurface);
|
EGL14.eglDestroyContext(eglDisplay, eglContext);
|
EGL14.eglTerminate(eglDisplay);
|
|
// Return the computed max size.
|
return result;
|
}
|
}
|