Similar to HEIC, AVIF files can also have smaller thumbnail images along with the main image in the container. So try to fetch that first instead of decoding the main image when creating a thumbnail. Test: ThumbnailUtils test still pass (with the new AVIF test). Bug: 141654151 Change-Id: I74afffe0e180edd425154daad30e07fcb0781ee6
580 lines
22 KiB
Java
580 lines
22 KiB
Java
/*
|
|
* Copyright (C) 2009 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 android.media;
|
|
|
|
import static android.media.MediaMetadataRetriever.METADATA_KEY_DURATION;
|
|
import static android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT;
|
|
import static android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH;
|
|
import static android.media.MediaMetadataRetriever.OPTION_CLOSEST_SYNC;
|
|
import static android.os.Environment.MEDIA_UNKNOWN;
|
|
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.compat.annotation.UnsupportedAppUsage;
|
|
import android.content.ContentResolver;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.BitmapFactory;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.ImageDecoder;
|
|
import android.graphics.ImageDecoder.ImageInfo;
|
|
import android.graphics.ImageDecoder.Source;
|
|
import android.graphics.Matrix;
|
|
import android.graphics.Rect;
|
|
import android.media.MediaMetadataRetriever.BitmapParams;
|
|
import android.net.Uri;
|
|
import android.os.Build;
|
|
import android.os.CancellationSignal;
|
|
import android.os.Environment;
|
|
import android.os.ParcelFileDescriptor;
|
|
import android.provider.MediaStore;
|
|
import android.util.Log;
|
|
import android.util.Size;
|
|
|
|
import com.android.internal.util.ArrayUtils;
|
|
|
|
import libcore.io.IoUtils;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.util.Arrays;
|
|
import java.util.Comparator;
|
|
import java.util.Objects;
|
|
import java.util.function.ToIntFunction;
|
|
|
|
/**
|
|
* Utilities for generating visual thumbnails from files.
|
|
*/
|
|
public class ThumbnailUtils {
|
|
private static final String TAG = "ThumbnailUtils";
|
|
|
|
/** @hide */
|
|
@Deprecated
|
|
@UnsupportedAppUsage
|
|
public static final int TARGET_SIZE_MICRO_THUMBNAIL = 96;
|
|
|
|
/* Options used internally. */
|
|
private static final int OPTIONS_NONE = 0x0;
|
|
private static final int OPTIONS_SCALE_UP = 0x1;
|
|
|
|
/**
|
|
* Constant used to indicate we should recycle the input in
|
|
* {@link #extractThumbnail(Bitmap, int, int, int)} unless the output is the input.
|
|
*/
|
|
public static final int OPTIONS_RECYCLE_INPUT = 0x2;
|
|
|
|
private static Size convertKind(int kind) {
|
|
return MediaStore.Images.Thumbnails.getKindSize(kind);
|
|
}
|
|
|
|
private static class Resizer implements ImageDecoder.OnHeaderDecodedListener {
|
|
private final Size size;
|
|
private final CancellationSignal signal;
|
|
|
|
public Resizer(Size size, CancellationSignal signal) {
|
|
this.size = size;
|
|
this.signal = signal;
|
|
}
|
|
|
|
@Override
|
|
public void onHeaderDecoded(ImageDecoder decoder, ImageInfo info, Source source) {
|
|
// One last-ditch check to see if we've been canceled.
|
|
if (signal != null) signal.throwIfCanceled();
|
|
|
|
// We don't know how clients will use the decoded data, so we have
|
|
// to default to the more flexible "software" option.
|
|
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
|
|
|
|
// We requested a rough thumbnail size, but the remote size may have
|
|
// returned something giant, so defensively scale down as needed.
|
|
final int widthSample = info.getSize().getWidth() / size.getWidth();
|
|
final int heightSample = info.getSize().getHeight() / size.getHeight();
|
|
final int sample = Math.max(widthSample, heightSample);
|
|
if (sample > 1) {
|
|
decoder.setTargetSampleSize(sample);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a thumbnail for given audio file.
|
|
*
|
|
* @param filePath The audio file.
|
|
* @param kind The desired thumbnail kind, such as
|
|
* {@link android.provider.MediaStore.Images.Thumbnails#MINI_KIND}.
|
|
* @deprecated Callers should migrate to using
|
|
* {@link #createAudioThumbnail(File, Size, CancellationSignal)},
|
|
* as it offers more control over resizing and cancellation.
|
|
*/
|
|
@Deprecated
|
|
public static @Nullable Bitmap createAudioThumbnail(@NonNull String filePath, int kind) {
|
|
try {
|
|
return createAudioThumbnail(new File(filePath), convertKind(kind), null);
|
|
} catch (IOException e) {
|
|
Log.w(TAG, e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a thumbnail for given audio file.
|
|
* <p>
|
|
* This method should only be used for files that you have direct access to;
|
|
* if you'd like to work with media hosted outside your app, consider using
|
|
* {@link ContentResolver#loadThumbnail(Uri, Size, CancellationSignal)}
|
|
* which enables remote providers to efficiently cache and invalidate
|
|
* thumbnails.
|
|
*
|
|
* @param file The audio file.
|
|
* @param size The desired thumbnail size.
|
|
* @throws IOException If any trouble was encountered while generating or
|
|
* loading the thumbnail, or if
|
|
* {@link CancellationSignal#cancel()} was invoked.
|
|
*/
|
|
public static @NonNull Bitmap createAudioThumbnail(@NonNull File file, @NonNull Size size,
|
|
@Nullable CancellationSignal signal) throws IOException {
|
|
// Checkpoint before going deeper
|
|
if (signal != null) signal.throwIfCanceled();
|
|
|
|
final Resizer resizer = new Resizer(size, signal);
|
|
try (MediaMetadataRetriever retriever = new MediaMetadataRetriever()) {
|
|
retriever.setDataSource(file.getAbsolutePath());
|
|
final byte[] raw = retriever.getEmbeddedPicture();
|
|
if (raw != null) {
|
|
return ImageDecoder.decodeBitmap(ImageDecoder.createSource(raw), resizer);
|
|
}
|
|
} catch (RuntimeException e) {
|
|
throw new IOException("Failed to create thumbnail", e);
|
|
}
|
|
|
|
// Only poke around for files on external storage
|
|
if (MEDIA_UNKNOWN.equals(Environment.getExternalStorageState(file))) {
|
|
throw new IOException("No embedded album art found");
|
|
}
|
|
|
|
// Ignore "Downloads" or top-level directories
|
|
final File parent = file.getParentFile();
|
|
final File grandParent = parent != null ? parent.getParentFile() : null;
|
|
if (parent != null
|
|
&& parent.getName().equals(Environment.DIRECTORY_DOWNLOADS)) {
|
|
throw new IOException("No thumbnails in Downloads directories");
|
|
}
|
|
if (grandParent != null
|
|
&& MEDIA_UNKNOWN.equals(Environment.getExternalStorageState(grandParent))) {
|
|
throw new IOException("No thumbnails in top-level directories");
|
|
}
|
|
|
|
// If no embedded image found, look around for best standalone file
|
|
final File[] found = ArrayUtils
|
|
.defeatNullable(file.getParentFile().listFiles((dir, name) -> {
|
|
final String lower = name.toLowerCase();
|
|
return (lower.endsWith(".jpg") || lower.endsWith(".png"));
|
|
}));
|
|
|
|
final ToIntFunction<File> score = (f) -> {
|
|
final String lower = f.getName().toLowerCase();
|
|
if (lower.equals("albumart.jpg")) return 4;
|
|
if (lower.startsWith("albumart") && lower.endsWith(".jpg")) return 3;
|
|
if (lower.contains("albumart") && lower.endsWith(".jpg")) return 2;
|
|
if (lower.endsWith(".jpg")) return 1;
|
|
return 0;
|
|
};
|
|
final Comparator<File> bestScore = (a, b) -> {
|
|
return score.applyAsInt(a) - score.applyAsInt(b);
|
|
};
|
|
|
|
final File bestFile = Arrays.asList(found).stream().max(bestScore).orElse(null);
|
|
if (bestFile == null) {
|
|
throw new IOException("No album art found");
|
|
}
|
|
|
|
// Checkpoint before going deeper
|
|
if (signal != null) signal.throwIfCanceled();
|
|
|
|
return ImageDecoder.decodeBitmap(ImageDecoder.createSource(bestFile), resizer);
|
|
}
|
|
|
|
/**
|
|
* Create a thumbnail for given image file.
|
|
*
|
|
* @param filePath The image file.
|
|
* @param kind The desired thumbnail kind, such as
|
|
* {@link android.provider.MediaStore.Images.Thumbnails#MINI_KIND}.
|
|
* @deprecated Callers should migrate to using
|
|
* {@link #createImageThumbnail(File, Size, CancellationSignal)},
|
|
* as it offers more control over resizing and cancellation.
|
|
*/
|
|
@Deprecated
|
|
public static @Nullable Bitmap createImageThumbnail(@NonNull String filePath, int kind) {
|
|
try {
|
|
return createImageThumbnail(new File(filePath), convertKind(kind), null);
|
|
} catch (IOException e) {
|
|
Log.w(TAG, e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a thumbnail for given image file.
|
|
* <p>
|
|
* This method should only be used for files that you have direct access to;
|
|
* if you'd like to work with media hosted outside your app, consider using
|
|
* {@link ContentResolver#loadThumbnail(Uri, Size, CancellationSignal)}
|
|
* which enables remote providers to efficiently cache and invalidate
|
|
* thumbnails.
|
|
*
|
|
* @param file The audio file.
|
|
* @param size The desired thumbnail size.
|
|
* @throws IOException If any trouble was encountered while generating or
|
|
* loading the thumbnail, or if
|
|
* {@link CancellationSignal#cancel()} was invoked.
|
|
*/
|
|
public static @NonNull Bitmap createImageThumbnail(@NonNull File file, @NonNull Size size,
|
|
@Nullable CancellationSignal signal) throws IOException {
|
|
// Checkpoint before going deeper
|
|
if (signal != null) signal.throwIfCanceled();
|
|
|
|
final Resizer resizer = new Resizer(size, signal);
|
|
final String mimeType = MediaFile.getMimeTypeForFile(file.getName());
|
|
Bitmap bitmap = null;
|
|
ExifInterface exif = null;
|
|
int orientation = 0;
|
|
|
|
// get orientation
|
|
if (MediaFile.isExifMimeType(mimeType)) {
|
|
exif = new ExifInterface(file);
|
|
switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0)) {
|
|
case ExifInterface.ORIENTATION_ROTATE_90:
|
|
orientation = 90;
|
|
break;
|
|
case ExifInterface.ORIENTATION_ROTATE_180:
|
|
orientation = 180;
|
|
break;
|
|
case ExifInterface.ORIENTATION_ROTATE_270:
|
|
orientation = 270;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (mimeType.equals("image/heif")
|
|
|| mimeType.equals("image/heif-sequence")
|
|
|| mimeType.equals("image/heic")
|
|
|| mimeType.equals("image/heic-sequence")
|
|
|| mimeType.equals("image/avif")) {
|
|
try (MediaMetadataRetriever retriever = new MediaMetadataRetriever()) {
|
|
retriever.setDataSource(file.getAbsolutePath());
|
|
bitmap = retriever.getThumbnailImageAtIndex(-1,
|
|
new MediaMetadataRetriever.BitmapParams(), size.getWidth(),
|
|
size.getWidth() * size.getHeight());
|
|
} catch (RuntimeException e) {
|
|
throw new IOException("Failed to create thumbnail", e);
|
|
}
|
|
}
|
|
|
|
if (bitmap == null && exif != null) {
|
|
final byte[] raw = exif.getThumbnailBytes();
|
|
if (raw != null) {
|
|
try {
|
|
bitmap = ImageDecoder.decodeBitmap(ImageDecoder.createSource(raw), resizer);
|
|
} catch (ImageDecoder.DecodeException e) {
|
|
Log.w(TAG, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Checkpoint before going deeper
|
|
if (signal != null) signal.throwIfCanceled();
|
|
|
|
if (bitmap == null) {
|
|
bitmap = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), resizer);
|
|
// Use ImageDecoder to do full file decoding, we don't need to handle the orientation
|
|
return bitmap;
|
|
}
|
|
|
|
// Transform the bitmap if the orientation of the image is not 0.
|
|
if (orientation != 0 && bitmap != null) {
|
|
final int width = bitmap.getWidth();
|
|
final int height = bitmap.getHeight();
|
|
|
|
final Matrix m = new Matrix();
|
|
m.setRotate(orientation, width / 2, height / 2);
|
|
bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, m, false);
|
|
}
|
|
|
|
return bitmap;
|
|
}
|
|
|
|
/**
|
|
* Create a thumbnail for given video file.
|
|
*
|
|
* @param filePath The video file.
|
|
* @param kind The desired thumbnail kind, such as
|
|
* {@link android.provider.MediaStore.Images.Thumbnails#MINI_KIND}.
|
|
* @deprecated Callers should migrate to using
|
|
* {@link #createVideoThumbnail(File, Size, CancellationSignal)},
|
|
* as it offers more control over resizing and cancellation.
|
|
*/
|
|
@Deprecated
|
|
public static @Nullable Bitmap createVideoThumbnail(@NonNull String filePath, int kind) {
|
|
try {
|
|
return createVideoThumbnail(new File(filePath), convertKind(kind), null);
|
|
} catch (IOException e) {
|
|
Log.w(TAG, e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a thumbnail for given video file.
|
|
* <p>
|
|
* This method should only be used for files that you have direct access to;
|
|
* if you'd like to work with media hosted outside your app, consider using
|
|
* {@link ContentResolver#loadThumbnail(Uri, Size, CancellationSignal)}
|
|
* which enables remote providers to efficiently cache and invalidate
|
|
* thumbnails.
|
|
*
|
|
* @param file The video file.
|
|
* @param size The desired thumbnail size.
|
|
* @throws IOException If any trouble was encountered while generating or
|
|
* loading the thumbnail, or if
|
|
* {@link CancellationSignal#cancel()} was invoked.
|
|
*/
|
|
public static @NonNull Bitmap createVideoThumbnail(@NonNull File file, @NonNull Size size,
|
|
@Nullable CancellationSignal signal) throws IOException {
|
|
// Checkpoint before going deeper
|
|
if (signal != null) signal.throwIfCanceled();
|
|
|
|
final Resizer resizer = new Resizer(size, signal);
|
|
try (MediaMetadataRetriever mmr = new MediaMetadataRetriever()) {
|
|
mmr.setDataSource(file.getAbsolutePath());
|
|
|
|
// Try to retrieve thumbnail from metadata
|
|
final byte[] raw = mmr.getEmbeddedPicture();
|
|
if (raw != null) {
|
|
return ImageDecoder.decodeBitmap(ImageDecoder.createSource(raw), resizer);
|
|
}
|
|
|
|
final BitmapParams params = new BitmapParams();
|
|
params.setPreferredConfig(Bitmap.Config.ARGB_8888);
|
|
|
|
final int width = Integer.parseInt(mmr.extractMetadata(METADATA_KEY_VIDEO_WIDTH));
|
|
final int height = Integer.parseInt(mmr.extractMetadata(METADATA_KEY_VIDEO_HEIGHT));
|
|
// Fall back to middle of video
|
|
// Note: METADATA_KEY_DURATION unit is in ms, not us.
|
|
final long thumbnailTimeUs =
|
|
Long.parseLong(mmr.extractMetadata(METADATA_KEY_DURATION)) * 1000 / 2;
|
|
|
|
// If we're okay with something larger than native format, just
|
|
// return a frame without up-scaling it
|
|
if (size.getWidth() > width && size.getHeight() > height) {
|
|
return Objects.requireNonNull(
|
|
mmr.getFrameAtTime(thumbnailTimeUs, OPTION_CLOSEST_SYNC, params));
|
|
} else {
|
|
return Objects.requireNonNull(
|
|
mmr.getScaledFrameAtTime(thumbnailTimeUs, OPTION_CLOSEST_SYNC,
|
|
size.getWidth(), size.getHeight(), params));
|
|
}
|
|
} catch (RuntimeException e) {
|
|
throw new IOException("Failed to create thumbnail", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a centered bitmap of the desired size.
|
|
*
|
|
* @param source original bitmap source
|
|
* @param width targeted width
|
|
* @param height targeted height
|
|
*/
|
|
public static Bitmap extractThumbnail(
|
|
Bitmap source, int width, int height) {
|
|
return extractThumbnail(source, width, height, OPTIONS_NONE);
|
|
}
|
|
|
|
/**
|
|
* Creates a centered bitmap of the desired size.
|
|
*
|
|
* @param source original bitmap source
|
|
* @param width targeted width
|
|
* @param height targeted height
|
|
* @param options options used during thumbnail extraction
|
|
*/
|
|
public static Bitmap extractThumbnail(
|
|
Bitmap source, int width, int height, int options) {
|
|
if (source == null) {
|
|
return null;
|
|
}
|
|
|
|
float scale;
|
|
if (source.getWidth() < source.getHeight()) {
|
|
scale = width / (float) source.getWidth();
|
|
} else {
|
|
scale = height / (float) source.getHeight();
|
|
}
|
|
Matrix matrix = new Matrix();
|
|
matrix.setScale(scale, scale);
|
|
Bitmap thumbnail = transform(matrix, source, width, height,
|
|
OPTIONS_SCALE_UP | options);
|
|
return thumbnail;
|
|
}
|
|
|
|
@Deprecated
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
|
|
private static int computeSampleSize(BitmapFactory.Options options,
|
|
int minSideLength, int maxNumOfPixels) {
|
|
return 1;
|
|
}
|
|
|
|
@Deprecated
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
|
|
private static int computeInitialSampleSize(BitmapFactory.Options options,
|
|
int minSideLength, int maxNumOfPixels) {
|
|
return 1;
|
|
}
|
|
|
|
@Deprecated
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
|
|
private static void closeSilently(ParcelFileDescriptor c) {
|
|
IoUtils.closeQuietly(c);
|
|
}
|
|
|
|
@Deprecated
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
|
|
private static ParcelFileDescriptor makeInputStream(
|
|
Uri uri, ContentResolver cr) {
|
|
try {
|
|
return cr.openFileDescriptor(uri, "r");
|
|
} catch (IOException ex) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transform source Bitmap to targeted width and height.
|
|
*/
|
|
@Deprecated
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
|
|
private static Bitmap transform(Matrix scaler,
|
|
Bitmap source,
|
|
int targetWidth,
|
|
int targetHeight,
|
|
int options) {
|
|
boolean scaleUp = (options & OPTIONS_SCALE_UP) != 0;
|
|
boolean recycle = (options & OPTIONS_RECYCLE_INPUT) != 0;
|
|
|
|
int deltaX = source.getWidth() - targetWidth;
|
|
int deltaY = source.getHeight() - targetHeight;
|
|
if (!scaleUp && (deltaX < 0 || deltaY < 0)) {
|
|
/*
|
|
* In this case the bitmap is smaller, at least in one dimension,
|
|
* than the target. Transform it by placing as much of the image
|
|
* as possible into the target and leaving the top/bottom or
|
|
* left/right (or both) black.
|
|
*/
|
|
Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight,
|
|
Bitmap.Config.ARGB_8888);
|
|
Canvas c = new Canvas(b2);
|
|
|
|
int deltaXHalf = Math.max(0, deltaX / 2);
|
|
int deltaYHalf = Math.max(0, deltaY / 2);
|
|
Rect src = new Rect(
|
|
deltaXHalf,
|
|
deltaYHalf,
|
|
deltaXHalf + Math.min(targetWidth, source.getWidth()),
|
|
deltaYHalf + Math.min(targetHeight, source.getHeight()));
|
|
int dstX = (targetWidth - src.width()) / 2;
|
|
int dstY = (targetHeight - src.height()) / 2;
|
|
Rect dst = new Rect(
|
|
dstX,
|
|
dstY,
|
|
targetWidth - dstX,
|
|
targetHeight - dstY);
|
|
c.drawBitmap(source, src, dst, null);
|
|
if (recycle) {
|
|
source.recycle();
|
|
}
|
|
c.setBitmap(null);
|
|
return b2;
|
|
}
|
|
float bitmapWidthF = source.getWidth();
|
|
float bitmapHeightF = source.getHeight();
|
|
|
|
float bitmapAspect = bitmapWidthF / bitmapHeightF;
|
|
float viewAspect = (float) targetWidth / targetHeight;
|
|
|
|
if (bitmapAspect > viewAspect) {
|
|
float scale = targetHeight / bitmapHeightF;
|
|
if (scale < .9F || scale > 1F) {
|
|
scaler.setScale(scale, scale);
|
|
} else {
|
|
scaler = null;
|
|
}
|
|
} else {
|
|
float scale = targetWidth / bitmapWidthF;
|
|
if (scale < .9F || scale > 1F) {
|
|
scaler.setScale(scale, scale);
|
|
} else {
|
|
scaler = null;
|
|
}
|
|
}
|
|
|
|
Bitmap b1;
|
|
if (scaler != null) {
|
|
// this is used for minithumb and crop, so we want to filter here.
|
|
b1 = Bitmap.createBitmap(source, 0, 0,
|
|
source.getWidth(), source.getHeight(), scaler, true);
|
|
} else {
|
|
b1 = source;
|
|
}
|
|
|
|
if (recycle && b1 != source) {
|
|
source.recycle();
|
|
}
|
|
|
|
int dx1 = Math.max(0, b1.getWidth() - targetWidth);
|
|
int dy1 = Math.max(0, b1.getHeight() - targetHeight);
|
|
|
|
Bitmap b2 = Bitmap.createBitmap(
|
|
b1,
|
|
dx1 / 2,
|
|
dy1 / 2,
|
|
targetWidth,
|
|
targetHeight);
|
|
|
|
if (b2 != b1) {
|
|
if (recycle || b1 != source) {
|
|
b1.recycle();
|
|
}
|
|
}
|
|
|
|
return b2;
|
|
}
|
|
|
|
@Deprecated
|
|
private static class SizedThumbnailBitmap {
|
|
public byte[] mThumbnailData;
|
|
public Bitmap mBitmap;
|
|
public int mThumbnailWidth;
|
|
public int mThumbnailHeight;
|
|
}
|
|
|
|
@Deprecated
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
|
|
private static void createThumbnailFromEXIF(String filePath, int targetSize,
|
|
int maxPixels, SizedThumbnailBitmap sizedThumbBitmap) {
|
|
}
|
|
}
|