diff options
Diffstat (limited to 'graphics/java/android')
28 files changed, 2985 insertions, 1180 deletions
diff --git a/graphics/java/android/graphics/BaseCanvas.java b/graphics/java/android/graphics/BaseCanvas.java index 1f339f7aaa54..07df0454362c 100644 --- a/graphics/java/android/graphics/BaseCanvas.java +++ b/graphics/java/android/graphics/BaseCanvas.java @@ -22,6 +22,7 @@ import android.annotation.Nullable; import android.annotation.Size; import android.graphics.Canvas.VertexMode; import android.text.GraphicsOperations; +import android.text.PrecomputedText; import android.text.SpannableString; import android.text.SpannedString; import android.text.TextUtils; @@ -375,7 +376,7 @@ public abstract class BaseCanvas { } throwIfHasHwBitmapInSwMode(paint); nDrawText(mNativeCanvasWrapper, text, index, count, x, y, paint.mBidiFlags, - paint.getNativeInstance(), paint.mNativeTypeface); + paint.getNativeInstance()); } public void drawText(@NonNull CharSequence text, int start, int end, float x, float y, @@ -387,7 +388,7 @@ public abstract class BaseCanvas { if (text instanceof String || text instanceof SpannedString || text instanceof SpannableString) { nDrawText(mNativeCanvasWrapper, text.toString(), start, end, x, y, - paint.mBidiFlags, paint.getNativeInstance(), paint.mNativeTypeface); + paint.mBidiFlags, paint.getNativeInstance()); } else if (text instanceof GraphicsOperations) { ((GraphicsOperations) text).drawText(this, start, end, x, y, paint); @@ -395,7 +396,7 @@ public abstract class BaseCanvas { char[] buf = TemporaryBuffer.obtain(end - start); TextUtils.getChars(text, start, end, buf, 0); nDrawText(mNativeCanvasWrapper, buf, 0, end - start, x, y, - paint.mBidiFlags, paint.getNativeInstance(), paint.mNativeTypeface); + paint.mBidiFlags, paint.getNativeInstance()); TemporaryBuffer.recycle(buf); } } @@ -403,7 +404,7 @@ public abstract class BaseCanvas { public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) { throwIfHasHwBitmapInSwMode(paint); nDrawText(mNativeCanvasWrapper, text, 0, text.length(), x, y, paint.mBidiFlags, - paint.getNativeInstance(), paint.mNativeTypeface); + paint.getNativeInstance()); } public void drawText(@NonNull String text, int start, int end, float x, float y, @@ -413,7 +414,7 @@ public abstract class BaseCanvas { } throwIfHasHwBitmapInSwMode(paint); nDrawText(mNativeCanvasWrapper, text, start, end, x, y, paint.mBidiFlags, - paint.getNativeInstance(), paint.mNativeTypeface); + paint.getNativeInstance()); } public void drawTextOnPath(@NonNull char[] text, int index, int count, @NonNull Path path, @@ -424,7 +425,7 @@ public abstract class BaseCanvas { throwIfHasHwBitmapInSwMode(paint); nDrawTextOnPath(mNativeCanvasWrapper, text, index, count, path.readOnlyNI(), hOffset, vOffset, - paint.mBidiFlags, paint.getNativeInstance(), paint.mNativeTypeface); + paint.mBidiFlags, paint.getNativeInstance()); } public void drawTextOnPath(@NonNull String text, @NonNull Path path, float hOffset, @@ -432,7 +433,7 @@ public abstract class BaseCanvas { if (text.length() > 0) { throwIfHasHwBitmapInSwMode(paint); nDrawTextOnPath(mNativeCanvasWrapper, text, path.readOnlyNI(), hOffset, vOffset, - paint.mBidiFlags, paint.getNativeInstance(), paint.mNativeTypeface); + paint.mBidiFlags, paint.getNativeInstance()); } } @@ -453,7 +454,8 @@ public abstract class BaseCanvas { throwIfHasHwBitmapInSwMode(paint); nDrawTextRun(mNativeCanvasWrapper, text, index, count, contextIndex, contextCount, - x, y, isRtl, paint.getNativeInstance(), paint.mNativeTypeface); + x, y, isRtl, paint.getNativeInstance(), 0 /* measured text */, + 0 /* measured text offset */); } public void drawTextRun(@NonNull CharSequence text, int start, int end, int contextStart, @@ -474,7 +476,7 @@ public abstract class BaseCanvas { if (text instanceof String || text instanceof SpannedString || text instanceof SpannableString) { nDrawTextRun(mNativeCanvasWrapper, text.toString(), start, end, contextStart, - contextEnd, x, y, isRtl, paint.getNativeInstance(), paint.mNativeTypeface); + contextEnd, x, y, isRtl, paint.getNativeInstance()); } else if (text instanceof GraphicsOperations) { ((GraphicsOperations) text).drawTextRun(this, start, end, contextStart, contextEnd, x, y, isRtl, paint); @@ -483,8 +485,20 @@ public abstract class BaseCanvas { int len = end - start; char[] buf = TemporaryBuffer.obtain(contextLen); TextUtils.getChars(text, contextStart, contextEnd, buf, 0); + long measuredTextPtr = 0; + int measuredTextOffset = 0; + if (text instanceof PrecomputedText) { + PrecomputedText mt = (PrecomputedText) text; + int paraIndex = mt.findParaIndex(start); + if (end <= mt.getParagraphEnd(paraIndex)) { + // Only suppor the same paragraph. + measuredTextPtr = mt.getMeasuredParagraph(paraIndex).getNativePtr(); + measuredTextOffset = start - mt.getParagraphStart(paraIndex); + } + } nDrawTextRun(mNativeCanvasWrapper, buf, start - contextStart, len, - 0, contextLen, x, y, isRtl, paint.getNativeInstance(), paint.mNativeTypeface); + 0, contextLen, x, y, isRtl, paint.getNativeInstance(), + measuredTextPtr, measuredTextOffset); TemporaryBuffer.recycle(buf); } } @@ -526,10 +540,19 @@ public abstract class BaseCanvas { return mAllowHwBitmapsInSwMode; } + /** + * @hide + */ + protected void onHwBitmapInSwMode() { + if (!mAllowHwBitmapsInSwMode) { + throw new IllegalArgumentException( + "Software rendering doesn't support hardware bitmaps"); + } + } + private void throwIfHwBitmapInSwMode(Bitmap bitmap) { - if (!mAllowHwBitmapsInSwMode && !isHardwareAccelerated() - && bitmap.getConfig() == Bitmap.Config.HARDWARE) { - throw new IllegalStateException("Software rendering doesn't support hardware bitmaps"); + if (!isHardwareAccelerated() && bitmap.getConfig() == Bitmap.Config.HARDWARE) { + onHwBitmapInSwMode(); } } @@ -614,23 +637,21 @@ public abstract class BaseCanvas { short[] indices, int indexOffset, int indexCount, long nativePaint); private static native void nDrawText(long nativeCanvas, char[] text, int index, int count, - float x, float y, int flags, long nativePaint, long nativeTypeface); + float x, float y, int flags, long nativePaint); private static native void nDrawText(long nativeCanvas, String text, int start, int end, - float x, float y, int flags, long nativePaint, long nativeTypeface); + float x, float y, int flags, long nativePaint); private static native void nDrawTextRun(long nativeCanvas, String text, int start, int end, - int contextStart, int contextEnd, float x, float y, boolean isRtl, long nativePaint, - long nativeTypeface); + int contextStart, int contextEnd, float x, float y, boolean isRtl, long nativePaint); private static native void nDrawTextRun(long nativeCanvas, char[] text, int start, int count, int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint, - long nativeTypeface); + long nativePrecomputedText, int measuredTextOffset); private static native void nDrawTextOnPath(long nativeCanvas, char[] text, int index, int count, - long nativePath, float hOffset, float vOffset, int bidiFlags, long nativePaint, - long nativeTypeface); + long nativePath, float hOffset, float vOffset, int bidiFlags, long nativePaint); private static native void nDrawTextOnPath(long nativeCanvas, String text, long nativePath, - float hOffset, float vOffset, int flags, long nativePaint, long nativeTypeface); + float hOffset, float vOffset, int flags, long nativePaint); } diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java index 57c75490ec47..44e7066c5c66 100644 --- a/graphics/java/android/graphics/Bitmap.java +++ b/graphics/java/android/graphics/Bitmap.java @@ -21,6 +21,7 @@ import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Size; +import android.annotation.WorkerThread; import android.content.res.ResourcesImpl; import android.os.Parcel; import android.os.Parcelable; @@ -28,6 +29,10 @@ import android.os.StrictMode; import android.os.Trace; import android.util.DisplayMetrics; import android.util.Log; +import android.view.DisplayListCanvas; +import android.view.RenderNode; +import android.view.ThreadedRenderer; + import libcore.util.NativeAllocationRegistry; import java.io.OutputStream; @@ -1170,6 +1175,82 @@ public final class Bitmap implements Parcelable { } /** + * Creates a Bitmap from the given {@link Picture} source of recorded drawing commands. + * + * Equivalent to calling {@link #createBitmap(Picture, int, int, Config)} with + * width and height the same as the Picture's width and height and a Config.HARDWARE + * config. + * + * @param source The recorded {@link Picture} of drawing commands that will be + * drawn into the returned Bitmap. + * @return An immutable bitmap with a HARDWARE config whose contents are created + * from the recorded drawing commands in the Picture source. + */ + public static @NonNull Bitmap createBitmap(@NonNull Picture source) { + return createBitmap(source, source.getWidth(), source.getHeight(), Config.HARDWARE); + } + + /** + * Creates a Bitmap from the given {@link Picture} source of recorded drawing commands. + * + * The bitmap will be immutable with the given width and height. If the width and height + * are not the same as the Picture's width & height, the Picture will be scaled to + * fit the given width and height. + * + * @param source The recorded {@link Picture} of drawing commands that will be + * drawn into the returned Bitmap. + * @param width The width of the bitmap to create. The picture's width will be + * scaled to match if necessary. + * @param height The height of the bitmap to create. The picture's height will be + * scaled to match if necessary. + * @param config The {@link Config} of the created bitmap. If this is null then + * the bitmap will be {@link Config#HARDWARE}. + * + * @return An immutable bitmap with a HARDWARE config whose contents are created + * from the recorded drawing commands in the Picture source. + */ + public static @NonNull Bitmap createBitmap(@NonNull Picture source, int width, int height, + @NonNull Config config) { + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("width & height must be > 0"); + } + if (config == null) { + throw new IllegalArgumentException("Config must not be null"); + } + if (source.requiresHardwareAcceleration() && config != Config.HARDWARE) { + StrictMode.noteSlowCall("GPU readback"); + } + if (config == Config.HARDWARE || source.requiresHardwareAcceleration()) { + final RenderNode node = RenderNode.create("BitmapTemporary", null); + node.setLeftTopRightBottom(0, 0, width, height); + node.setClipToBounds(false); + final DisplayListCanvas canvas = node.start(width, height); + if (source.getWidth() != width || source.getHeight() != height) { + canvas.scale(width / (float) source.getWidth(), + height / (float) source.getHeight()); + } + canvas.drawPicture(source); + node.end(canvas); + Bitmap bitmap = ThreadedRenderer.createHardwareBitmap(node, width, height); + if (config != Config.HARDWARE) { + bitmap = bitmap.copy(config, false); + } + return bitmap; + } else { + Bitmap bitmap = Bitmap.createBitmap(width, height, config); + Canvas canvas = new Canvas(bitmap); + if (source.getWidth() != width || source.getHeight() != height) { + canvas.scale(width / (float) source.getWidth(), + height / (float) source.getHeight()); + } + canvas.drawPicture(source); + canvas.setBitmap(null); + bitmap.makeImmutable(); + return bitmap; + } + } + + /** * Returns an optional array of private data, used by the UI system for * some bitmaps. Not intended to be called by applications. */ @@ -1233,6 +1314,7 @@ public final class Bitmap implements Parcelable { * @param stream The outputstream to write the compressed data. * @return true if successfully compressed to the specified stream. */ + @WorkerThread public boolean compress(CompressFormat format, int quality, OutputStream stream) { checkRecycled("Can't compress a recycled bitmap"); // do explicit check before calling the native method @@ -1257,6 +1339,12 @@ public final class Bitmap implements Parcelable { return mIsMutable; } + /** @hide */ + public final void makeImmutable() { + // todo mIsMutable = false; + // todo nMakeImmutable(); + } + /** * <p>Indicates whether pixels stored in this bitmaps are stored pre-multiplied. * When a pixel is pre-multiplied, the RGB components have been multiplied by diff --git a/graphics/java/android/graphics/BitmapFactory.java b/graphics/java/android/graphics/BitmapFactory.java index ffb39e339119..7ea35e73619a 100644 --- a/graphics/java/android/graphics/BitmapFactory.java +++ b/graphics/java/android/graphics/BitmapFactory.java @@ -18,6 +18,8 @@ package android.graphics; import static android.graphics.BitmapFactory.Options.validate; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.res.AssetManager; import android.content.res.Resources; import android.os.Trace; @@ -354,6 +356,7 @@ public class BitmapFactory { * decode, in the case of which a more accurate, but slightly slower, * IDCT method will be used instead. */ + @Deprecated public boolean inPreferQualityOverSpeed; /** @@ -412,6 +415,7 @@ public class BitmapFactory { * can check, inbetween the bounds decode and the image decode, to see * if the operation is canceled. */ + @Deprecated public boolean mCancel; /** @@ -426,6 +430,7 @@ public class BitmapFactory { * or if inJustDecodeBounds is true, will set outWidth/outHeight * to -1 */ + @Deprecated public void requestCancelDecode() { mCancel = true; } @@ -515,8 +520,9 @@ public class BitmapFactory { * is not {@link ColorSpace.Model#RGB RGB}, or if the specified color space's transfer * function is not an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve} */ - public static Bitmap decodeResourceStream(Resources res, TypedValue value, - InputStream is, Rect pad, Options opts) { + @Nullable + public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value, + @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) { validate(opts); if (opts == null) { opts = new Options(); @@ -704,7 +710,9 @@ public class BitmapFactory { * <code>is.mark(1024)</code> would be called. As of * {@link android.os.Build.VERSION_CODES#KITKAT}, this is no longer the case.</p> */ - public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) { + @Nullable + public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding, + @Nullable Options opts) { // we don't throw in this case, thus allowing the caller to only check // the cache, and not force the image to be decoded. if (is == null) { @@ -739,7 +747,8 @@ public class BitmapFactory { * Private helper function for decoding an InputStream natively. Buffers the input enough to * do a rewind as needed, and supplies temporary storage if necessary. is MUST NOT be null. */ - private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) { + private static Bitmap decodeStreamInternal(@NonNull InputStream is, + @Nullable Rect outPadding, @Nullable Options opts) { // ASSERT(is != null); byte [] tempStorage = null; if (opts != null) tempStorage = opts.inTempStorage; diff --git a/graphics/java/android/graphics/Canvas.java b/graphics/java/android/graphics/Canvas.java index 0301f2e6b555..d925441e3657 100644 --- a/graphics/java/android/graphics/Canvas.java +++ b/graphics/java/android/graphics/Canvas.java @@ -200,11 +200,6 @@ public class Canvas extends BaseCanvas { } /** @hide */ - public void setHighContrastText(boolean highContrastText) { - nSetHighContrastText(mNativeCanvasWrapper, highContrastText); - } - - /** @hide */ public void insertReorderBarrier() {} /** @hide */ @@ -1224,10 +1219,14 @@ public class Canvas extends BaseCanvas { nFreeTextLayoutCaches(); } + /** @hide */ + public static void setCompatibilityVersion(int apiLevel) { nSetCompatibilityVersion(apiLevel); } + private static native void nFreeCaches(); private static native void nFreeTextLayoutCaches(); private static native long nInitRaster(Bitmap bitmap); private static native long nGetNativeFinalizer(); + private static native void nSetCompatibilityVersion(int apiLevel); // ---------------- @FastNative ------------------- @@ -1242,8 +1241,6 @@ public class Canvas extends BaseCanvas { @CriticalNative private static native boolean nIsOpaque(long canvasHandle); @CriticalNative - private static native void nSetHighContrastText(long renderer, boolean highContrastText); - @CriticalNative private static native int nGetWidth(long canvasHandle); @CriticalNative private static native int nGetHeight(long canvasHandle); diff --git a/graphics/java/android/graphics/FontFamily.java b/graphics/java/android/graphics/FontFamily.java index d9a77e752823..e7cfcfdf7760 100644 --- a/graphics/java/android/graphics/FontFamily.java +++ b/graphics/java/android/graphics/FontFamily.java @@ -16,10 +16,12 @@ package android.graphics; +import android.annotation.Nullable; import android.content.res.AssetManager; import android.graphics.fonts.FontVariationAxis; -import android.text.FontConfig; +import android.text.TextUtils; import android.util.Log; + import dalvik.annotation.optimization.CriticalNative; import java.io.FileInputStream; @@ -48,8 +50,16 @@ public class FontFamily { mBuilderPtr = nInitBuilder(null, 0); } - public FontFamily(String lang, int variant) { - mBuilderPtr = nInitBuilder(lang, variant); + public FontFamily(@Nullable String[] langs, int variant) { + final String langsString; + if (langs == null || langs.length == 0) { + langsString = null; + } else if (langs.length == 1) { + langsString = langs[0]; + } else { + langsString = TextUtils.join(",", langs); + } + mBuilderPtr = nInitBuilder(langsString, variant); } /** @@ -150,39 +160,17 @@ public class FontFamily { isItalic); } - /** - * Allow creating unsupported FontFamily. - * - * For compatibility reasons, we still need to create a FontFamily object even if Minikin failed - * to find any usable 'cmap' table for some reasons, e.g. broken 'cmap' table, no 'cmap' table - * encoded with Unicode code points, etc. Without calling this method, the freeze() method will - * return null if Minikin fails to find any usable 'cmap' table. By calling this method, the - * freeze() won't fail and will create an empty FontFamily. This empty FontFamily is placed at - * the top of the fallback chain but is never used. if we don't create this empty FontFamily - * and put it at top, bad things (performance regressions, unexpected glyph selection) will - * happen. - */ - public void allowUnsupportedFont() { - if (mBuilderPtr == 0) { - throw new IllegalStateException("Unable to allow unsupported font."); - } - nAllowUnsupportedFont(mBuilderPtr); - } - // TODO: Remove once internal user stop using private API. private static boolean nAddFont(long builderPtr, ByteBuffer font, int ttcIndex) { return nAddFont(builderPtr, font, ttcIndex, -1, -1); } - private static native long nInitBuilder(String lang, int variant); + private static native long nInitBuilder(String langs, int variant); @CriticalNative private static native long nCreateFamily(long mBuilderPtr); @CriticalNative - private static native void nAllowUnsupportedFont(long builderPtr); - - @CriticalNative private static native void nAbort(long mBuilderPtr); @CriticalNative diff --git a/graphics/java/android/graphics/FontListParser.java b/graphics/java/android/graphics/FontListParser.java index 7c07a302dfe9..431d0e0eb7b4 100644 --- a/graphics/java/android/graphics/FontListParser.java +++ b/graphics/java/android/graphics/FontListParser.java @@ -16,16 +16,13 @@ package android.graphics; -import android.text.FontConfig; import android.graphics.fonts.FontVariationAxis; +import android.text.FontConfig; import android.util.Xml; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -import android.annotation.Nullable; -import com.android.internal.annotations.VisibleForTesting; - import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -74,13 +71,14 @@ public class FontListParser { private static FontConfig.Family readFamily(XmlPullParser parser) throws XmlPullParserException, IOException { - String name = parser.getAttributeValue(null, "name"); - String lang = parser.getAttributeValue(null, "lang"); - String variant = parser.getAttributeValue(null, "variant"); - List<FontConfig.Font> fonts = new ArrayList<FontConfig.Font>(); + final String name = parser.getAttributeValue(null, "name"); + final String lang = parser.getAttributeValue(null, "lang"); + final String[] langs = lang == null ? null : lang.split("\\s+"); + final String variant = parser.getAttributeValue(null, "variant"); + final List<FontConfig.Font> fonts = new ArrayList<FontConfig.Font>(); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() != XmlPullParser.START_TAG) continue; - String tag = parser.getName(); + final String tag = parser.getName(); if (tag.equals("font")) { fonts.add(readFont(parser)); } else { @@ -95,7 +93,7 @@ public class FontListParser { intVariant = FontConfig.Family.VARIANT_ELEGANT; } } - return new FontConfig.Family(name, fonts.toArray(new FontConfig.Font[fonts.size()]), lang, + return new FontConfig.Family(name, fonts.toArray(new FontConfig.Font[fonts.size()]), langs, intVariant); } @@ -111,6 +109,7 @@ public class FontListParser { String weightStr = parser.getAttributeValue(null, "weight"); int weight = weightStr == null ? 400 : Integer.parseInt(weightStr); boolean isItalic = "italic".equals(parser.getAttributeValue(null, "style")); + String fallbackFor = parser.getAttributeValue(null, "fallbackFor"); StringBuilder filename = new StringBuilder(); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() == XmlPullParser.TEXT) { @@ -126,7 +125,7 @@ public class FontListParser { } String sanitizedName = FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll(""); return new FontConfig.Font(sanitizedName, index, - axes.toArray(new FontVariationAxis[axes.size()]), weight, isItalic); + axes.toArray(new FontVariationAxis[axes.size()]), weight, isItalic, fallbackFor); } private static FontVariationAxis readAxis(XmlPullParser parser) diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java new file mode 100644 index 000000000000..ee7abc5bd254 --- /dev/null +++ b/graphics/java/android/graphics/ImageDecoder.java @@ -0,0 +1,1104 @@ +/* + * Copyright (C) 2017 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.graphics; + +import static android.system.OsConstants.SEEK_CUR; +import static android.system.OsConstants.SEEK_SET; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RawRes; +import android.content.ContentResolver; +import android.content.res.AssetFileDescriptor; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.graphics.drawable.AnimatedImageDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.NinePatchDrawable; +import android.net.Uri; +import android.util.Size; +import android.system.ErrnoException; +import android.system.Os; +import android.util.DisplayMetrics; +import android.util.TypedValue; + +import libcore.io.IoUtils; +import dalvik.system.CloseGuard; + +import java.nio.ByteBuffer; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ArrayIndexOutOfBoundsException; +import java.lang.AutoCloseable; +import java.lang.NullPointerException; +import java.lang.RuntimeException; +import java.lang.annotation.Retention; +import static java.lang.annotation.RetentionPolicy.SOURCE; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Class for decoding images as {@link Bitmap}s or {@link Drawable}s. + */ +public final class ImageDecoder implements AutoCloseable { + /** + * Source of the encoded image data. + */ + public static abstract class Source { + private Source() {} + + /* @hide */ + @Nullable + Resources getResources() { return null; } + + /* @hide */ + int getDensity() { return Bitmap.DENSITY_NONE; } + + /* @hide */ + final int computeDstDensity() { + Resources res = getResources(); + if (res == null) { + return Bitmap.getDefaultDensity(); + } + + return res.getDisplayMetrics().densityDpi; + } + + /* @hide */ + @NonNull + abstract ImageDecoder createImageDecoder() throws IOException; + }; + + private static class ByteArraySource extends Source { + ByteArraySource(@NonNull byte[] data, int offset, int length) { + mData = data; + mOffset = offset; + mLength = length; + }; + private final byte[] mData; + private final int mOffset; + private final int mLength; + + @Override + public ImageDecoder createImageDecoder() throws IOException { + return nCreate(mData, mOffset, mLength); + } + } + + private static class ByteBufferSource extends Source { + ByteBufferSource(@NonNull ByteBuffer buffer) { + mBuffer = buffer; + } + private final ByteBuffer mBuffer; + + @Override + public ImageDecoder createImageDecoder() throws IOException { + if (!mBuffer.isDirect() && mBuffer.hasArray()) { + int offset = mBuffer.arrayOffset() + mBuffer.position(); + int length = mBuffer.limit() - mBuffer.position(); + return nCreate(mBuffer.array(), offset, length); + } + return nCreate(mBuffer, mBuffer.position(), mBuffer.limit()); + } + } + + private static class ContentResolverSource extends Source { + ContentResolverSource(@NonNull ContentResolver resolver, @NonNull Uri uri, + @Nullable Resources res) { + mResolver = resolver; + mUri = uri; + mResources = res; + } + + private final ContentResolver mResolver; + private final Uri mUri; + private final Resources mResources; + + @Nullable + Resources getResources() { return mResources; } + + @Override + public ImageDecoder createImageDecoder() throws IOException { + AssetFileDescriptor assetFd = null; + try { + if (mUri.getScheme() == ContentResolver.SCHEME_CONTENT) { + assetFd = mResolver.openTypedAssetFileDescriptor(mUri, + "image/*", null); + } else { + assetFd = mResolver.openAssetFileDescriptor(mUri, "r"); + } + } catch (FileNotFoundException e) { + // Some images cannot be opened as AssetFileDescriptors (e.g. + // bmp, ico). Open them as InputStreams. + InputStream is = mResolver.openInputStream(mUri); + if (is == null) { + throw new FileNotFoundException(mUri.toString()); + } + + return createFromStream(is, true); + } + + final FileDescriptor fd = assetFd.getFileDescriptor(); + final long offset = assetFd.getStartOffset(); + + ImageDecoder decoder = null; + try { + try { + Os.lseek(fd, offset, SEEK_SET); + decoder = nCreate(fd); + } catch (ErrnoException e) { + decoder = createFromStream(new FileInputStream(fd), true); + } + } finally { + if (decoder == null) { + IoUtils.closeQuietly(assetFd); + } else { + decoder.mAssetFd = assetFd; + } + } + return decoder; + } + } + + @NonNull + private static ImageDecoder createFromFile(@NonNull File file) throws IOException { + FileInputStream stream = new FileInputStream(file); + FileDescriptor fd = stream.getFD(); + try { + Os.lseek(fd, 0, SEEK_CUR); + } catch (ErrnoException e) { + return createFromStream(stream, true); + } + + ImageDecoder decoder = null; + try { + decoder = nCreate(fd); + } finally { + if (decoder == null) { + IoUtils.closeQuietly(stream); + } else { + decoder.mInputStream = stream; + decoder.mOwnsInputStream = true; + } + } + return decoder; + } + + @NonNull + private static ImageDecoder createFromStream(@NonNull InputStream is, + boolean closeInputStream) throws IOException { + // Arbitrary size matches BitmapFactory. + byte[] storage = new byte[16 * 1024]; + ImageDecoder decoder = null; + try { + decoder = nCreate(is, storage); + } finally { + if (decoder == null) { + if (closeInputStream) { + IoUtils.closeQuietly(is); + } + } else { + decoder.mInputStream = is; + decoder.mOwnsInputStream = closeInputStream; + decoder.mTempStorage = storage; + } + } + + return decoder; + } + + /** + * For backwards compatibility, this does *not* close the InputStream. + */ + private static class InputStreamSource extends Source { + InputStreamSource(Resources res, InputStream is, int inputDensity) { + if (is == null) { + throw new IllegalArgumentException("The InputStream cannot be null"); + } + mResources = res; + mInputStream = is; + mInputDensity = res != null ? inputDensity : Bitmap.DENSITY_NONE; + } + + final Resources mResources; + InputStream mInputStream; + final int mInputDensity; + + @Override + public Resources getResources() { return mResources; } + + @Override + public int getDensity() { return mInputDensity; } + + @Override + public ImageDecoder createImageDecoder() throws IOException { + + synchronized (this) { + if (mInputStream == null) { + throw new IOException("Cannot reuse InputStreamSource"); + } + InputStream is = mInputStream; + mInputStream = null; + return createFromStream(is, false); + } + } + } + + private static class ResourceSource extends Source { + ResourceSource(@NonNull Resources res, int resId) { + mResources = res; + mResId = resId; + mResDensity = Bitmap.DENSITY_NONE; + } + + final Resources mResources; + final int mResId; + int mResDensity; + + @Override + public Resources getResources() { return mResources; } + + @Override + public int getDensity() { return mResDensity; } + + @Override + public ImageDecoder createImageDecoder() throws IOException { + // This is just used in order to access the underlying Asset and + // keep it alive. FIXME: Can we skip creating this object? + InputStream is = null; + ImageDecoder decoder = null; + TypedValue value = new TypedValue(); + try { + is = mResources.openRawResource(mResId, value); + + if (value.density == TypedValue.DENSITY_DEFAULT) { + mResDensity = DisplayMetrics.DENSITY_DEFAULT; + } else if (value.density != TypedValue.DENSITY_NONE) { + mResDensity = value.density; + } + + if (!(is instanceof AssetManager.AssetInputStream)) { + // This should never happen. + throw new RuntimeException("Resource is not an asset?"); + } + long asset = ((AssetManager.AssetInputStream) is).getNativeAsset(); + decoder = nCreate(asset); + } finally { + if (decoder == null) { + IoUtils.closeQuietly(is); + } else { + decoder.mInputStream = is; + decoder.mOwnsInputStream = true; + } + } + return decoder; + } + } + + private static class FileSource extends Source { + FileSource(@NonNull File file) { + mFile = file; + } + + private final File mFile; + + @Override + public ImageDecoder createImageDecoder() throws IOException { + return createFromFile(mFile); + } + } + + /** + * Contains information about the encoded image. + */ + public static class ImageInfo { + private final Size mSize; + private ImageDecoder mDecoder; + + private ImageInfo(@NonNull ImageDecoder decoder) { + mSize = new Size(decoder.mWidth, decoder.mHeight); + mDecoder = decoder; + } + + /** + * Size of the image, without scaling or cropping. + */ + @NonNull + public Size getSize() { + return mSize; + } + + /** + * The mimeType of the image. + */ + @NonNull + public String getMimeType() { + return mDecoder.getMimeType(); + } + + /** + * Whether the image is animated. + * + * <p>Calling {@link #decodeDrawable} will return an + * {@link AnimatedImageDrawable}.</p> + */ + public boolean isAnimated() { + return mDecoder.mAnimated; + } + }; + + /** + * Thrown if the provided data is incomplete. + */ + public static class IncompleteException extends IOException {}; + + /** + * Optional listener supplied to {@link #decodeDrawable} or + * {@link #decodeBitmap}. + */ + public static interface OnHeaderDecodedListener { + /** + * Called when the header is decoded and the size is known. + * + * @param decoder allows changing the default settings of the decode. + * @param info Information about the encoded image. + * @param source that created the decoder. + */ + public void onHeaderDecoded(@NonNull ImageDecoder decoder, + @NonNull ImageInfo info, @NonNull Source source); + + }; + + /** + * An Exception was thrown reading the {@link Source}. + */ + public static final int ERROR_SOURCE_EXCEPTION = 1; + + /** + * The encoded data was incomplete. + */ + public static final int ERROR_SOURCE_INCOMPLETE = 2; + + /** + * The encoded data contained an error. + */ + public static final int ERROR_SOURCE_ERROR = 3; + + @Retention(SOURCE) + @IntDef({ ERROR_SOURCE_EXCEPTION, ERROR_SOURCE_INCOMPLETE, ERROR_SOURCE_ERROR }) + public @interface Error {}; + + /** + * Optional listener supplied to the ImageDecoder. + * + * Without this listener, errors will throw {@link java.io.IOException}. + */ + public static interface OnPartialImageListener { + /** + * Called when there is only a partial image to display. + * + * If decoding is interrupted after having decoded a partial image, + * this listener lets the client know that and allows them to + * optionally finish the rest of the decode/creation process to create + * a partial {@link Drawable}/{@link Bitmap}. + * + * @param error indicating what interrupted the decode. + * @param source that had the error. + * @return True to create and return a {@link Drawable}/{@link Bitmap} + * with partial data. False (which is the default) to abort the + * decode and throw {@link java.io.IOException}. + */ + public boolean onPartialImage(@Error int error, @NonNull Source source); + }; + + // Fields + private long mNativePtr; + private final int mWidth; + private final int mHeight; + private final boolean mAnimated; + + private int mDesiredWidth; + private int mDesiredHeight; + private int mAllocator = ALLOCATOR_DEFAULT; + private boolean mRequireUnpremultiplied = false; + private boolean mMutable = false; + private boolean mPreferRamOverQuality = false; + private boolean mAsAlphaMask = false; + private Rect mCropRect; + private Source mSource; + + private PostProcessor mPostProcessor; + private OnPartialImageListener mOnPartialImageListener; + + // Objects for interacting with the input. + private InputStream mInputStream; + private boolean mOwnsInputStream; + private byte[] mTempStorage; + private AssetFileDescriptor mAssetFd; + private final AtomicBoolean mClosed = new AtomicBoolean(); + private final CloseGuard mCloseGuard = CloseGuard.get(); + + /** + * Private constructor called by JNI. {@link #close} must be + * called after decoding to delete native resources. + */ + @SuppressWarnings("unused") + private ImageDecoder(long nativePtr, int width, int height, + boolean animated) { + mNativePtr = nativePtr; + mWidth = width; + mHeight = height; + mDesiredWidth = width; + mDesiredHeight = height; + mAnimated = animated; + mCloseGuard.open("close"); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + + // Avoid closing these in finalizer. + mInputStream = null; + mAssetFd = null; + + close(); + } finally { + super.finalize(); + } + } + + /** + * Create a new {@link Source} from an asset. + * @hide + * + * @param res the {@link Resources} object containing the image data. + * @param resId resource ID of the image data. + * // FIXME: Can be an @DrawableRes? + * @return a new Source object, which can be passed to + * {@link #decodeDrawable} or {@link #decodeBitmap}. + */ + @NonNull + public static Source createSource(@NonNull Resources res, @RawRes int resId) + { + return new ResourceSource(res, resId); + } + + /** + * Create a new {@link Source} from a {@link android.net.Uri}. + * + * @param cr to retrieve from. + * @param uri of the image file. + * @return a new Source object, which can be passed to + * {@link #decodeDrawable} or {@link #decodeBitmap}. + */ + @NonNull + public static Source createSource(@NonNull ContentResolver cr, + @NonNull Uri uri) { + return new ContentResolverSource(cr, uri, null); + } + + /** + * Provide Resources for density scaling. + * + * @hide + */ + @NonNull + public static Source createSource(@NonNull ContentResolver cr, + @NonNull Uri uri, @Nullable Resources res) { + return new ContentResolverSource(cr, uri, res); + } + + /** + * Create a new {@link Source} from a byte array. + * + * @param data byte array of compressed image data. + * @param offset offset into data for where the decoder should begin + * parsing. + * @param length number of bytes, beginning at offset, to parse. + * @throws NullPointerException if data is null. + * @throws ArrayIndexOutOfBoundsException if offset and length are + * not within data. + * @hide + */ + @NonNull + public static Source createSource(@NonNull byte[] data, int offset, + int length) throws ArrayIndexOutOfBoundsException { + if (data == null) { + throw new NullPointerException("null byte[] in createSource!"); + } + if (offset < 0 || length < 0 || offset >= data.length || + offset + length > data.length) { + throw new ArrayIndexOutOfBoundsException( + "invalid offset/length!"); + } + return new ByteArraySource(data, offset, length); + } + + /** + * See {@link #createSource(byte[], int, int). + * @hide + */ + @NonNull + public static Source createSource(@NonNull byte[] data) { + return createSource(data, 0, data.length); + } + + /** + * Create a new {@link Source} from a {@link java.nio.ByteBuffer}. + * + * <p>The returned {@link Source} effectively takes ownership of the + * {@link java.nio.ByteBuffer}; i.e. no other code should modify it after + * this call.</p> + * + * Decoding will start from {@link java.nio.ByteBuffer#position()}. The + * position after decoding is undefined. + */ + @NonNull + public static Source createSource(@NonNull ByteBuffer buffer) { + return new ByteBufferSource(buffer); + } + + /** + * Internal API used to generate bitmaps for use by Drawables (i.e. BitmapDrawable) + * @hide + */ + public static Source createSource(Resources res, InputStream is) { + return new InputStreamSource(res, is, Bitmap.getDefaultDensity()); + } + + /** + * Internal API used to generate bitmaps for use by Drawables (i.e. BitmapDrawable) + * @hide + */ + public static Source createSource(Resources res, InputStream is, int density) { + return new InputStreamSource(res, is, density); + } + + /** + * Create a new {@link Source} from a {@link java.io.File}. + */ + @NonNull + public static Source createSource(@NonNull File file) { + return new FileSource(file); + } + + /** + * Return the width and height of a given sample size. + * + * <p>This takes an input that functions like + * {@link BitmapFactory.Options#inSampleSize}. It returns a width and + * height that can be acheived by sampling the encoded image. Other widths + * and heights may be supported, but will require an additional (internal) + * scaling step. Such internal scaling is *not* supported with + * {@link #setRequireUnpremultiplied} set to {@code true}.</p> + * + * @param sampleSize Sampling rate of the encoded image. + * @return {@link android.util.Size} of the width and height after + * sampling. + */ + @NonNull + public Size getSampledSize(int sampleSize) { + if (sampleSize <= 0) { + throw new IllegalArgumentException("sampleSize must be positive! " + + "provided " + sampleSize); + } + if (mNativePtr == 0) { + throw new IllegalStateException("ImageDecoder is closed!"); + } + + return nGetSampledSize(mNativePtr, sampleSize); + } + + // Modifiers + /** + * Resize the output to have the following size. + * + * @param width must be greater than 0. + * @param height must be greater than 0. + */ + public void setResize(int width, int height) { + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("Dimensions must be positive! " + + "provided (" + width + ", " + height + ")"); + } + + mDesiredWidth = width; + mDesiredHeight = height; + } + + /** + * Resize based on a sample size. + * + * <p>This has the same effect as passing the result of + * {@link #getSampledSize} to {@link #setResize(int, int)}.</p> + * + * @param sampleSize Sampling rate of the encoded image. + */ + public void setResize(int sampleSize) { + Size size = this.getSampledSize(sampleSize); + this.setResize(size.getWidth(), size.getHeight()); + } + + private boolean requestedResize() { + return mWidth != mDesiredWidth || mHeight != mDesiredHeight; + } + + // These need to stay in sync with ImageDecoder.cpp's Allocator enum. + /** + * Use the default allocation for the pixel memory. + * + * Will typically result in a {@link Bitmap.Config#HARDWARE} + * allocation, but may be software for small images. In addition, this will + * switch to software when HARDWARE is incompatible, e.g. + * {@link #setMutable}, {@link #setAsAlphaMask}. + */ + public static final int ALLOCATOR_DEFAULT = 0; + + /** + * Use a software allocation for the pixel memory. + * + * Useful for drawing to a software {@link Canvas} or for + * accessing the pixels on the final output. + */ + public static final int ALLOCATOR_SOFTWARE = 1; + + /** + * Use shared memory for the pixel memory. + * + * Useful for sharing across processes. + */ + public static final int ALLOCATOR_SHARED_MEMORY = 2; + + /** + * Require a {@link Bitmap.Config#HARDWARE} {@link Bitmap}. + * + * When this is combined with incompatible options, like + * {@link #setMutable} or {@link #setAsAlphaMask}, {@link #decodeDrawable} + * / {@link #decodeBitmap} will throw an + * {@link java.lang.IllegalStateException}. + */ + public static final int ALLOCATOR_HARDWARE = 3; + + /** @hide **/ + @Retention(SOURCE) + @IntDef({ ALLOCATOR_DEFAULT, ALLOCATOR_SOFTWARE, ALLOCATOR_SHARED_MEMORY, + ALLOCATOR_HARDWARE }) + public @interface Allocator {}; + + /** + * Choose the backing for the pixel memory. + * + * This is ignored for animated drawables. + * + * @param allocator Type of allocator to use. + */ + public void setAllocator(@Allocator int allocator) { + if (allocator < ALLOCATOR_DEFAULT || allocator > ALLOCATOR_HARDWARE) { + throw new IllegalArgumentException("invalid allocator " + allocator); + } + mAllocator = allocator; + } + + /** + * Specify whether the {@link Bitmap} should have unpremultiplied pixels. + * + * By default, ImageDecoder will create a {@link Bitmap} with + * premultiplied pixels, which is required for drawing with the + * {@link android.view.View} system (i.e. to a {@link Canvas}). Calling + * this method with a value of {@code true} will result in + * {@link #decodeBitmap} returning a {@link Bitmap} with unpremultiplied + * pixels. See {@link Bitmap#isPremultiplied}. This is incompatible with + * {@link #decodeDrawable}; attempting to decode an unpremultiplied + * {@link Drawable} will throw an {@link java.lang.IllegalStateException}. + */ + public void setRequireUnpremultiplied(boolean requireUnpremultiplied) { + mRequireUnpremultiplied = requireUnpremultiplied; + } + + /** + * Modify the image after decoding and scaling. + * + * <p>This allows adding effects prior to returning a {@link Drawable} or + * {@link Bitmap}. For a {@code Drawable} or an immutable {@code Bitmap}, + * this is the only way to process the image after decoding.</p> + * + * <p>If set on a nine-patch image, the nine-patch data is ignored.</p> + * + * <p>For an animated image, the drawing commands drawn on the + * {@link Canvas} will be recorded immediately and then applied to each + * frame.</p> + */ + public void setPostProcessor(@Nullable PostProcessor p) { + mPostProcessor = p; + } + + /** + * Set (replace) the {@link OnPartialImageListener} on this object. + * + * Will be called if there is an error in the input. Without one, a + * partial {@link Bitmap} will be created. + */ + public void setOnPartialImageListener(@Nullable OnPartialImageListener l) { + mOnPartialImageListener = l; + } + + /** + * Crop the output to {@code subset} of the (possibly) scaled image. + * + * <p>{@code subset} must be contained within the size set by + * {@link #setResize} or the bounds of the image if setResize was not + * called. Otherwise an {@link IllegalStateException} will be thrown by + * {@link #decodeDrawable}/{@link #decodeBitmap}.</p> + * + * <p>NOT intended as a replacement for + * {@link BitmapRegionDecoder#decodeRegion}. This supports all formats, + * but merely crops the output.</p> + */ + public void setCrop(@Nullable Rect subset) { + mCropRect = subset; + } + + /** + * Specify whether the {@link Bitmap} should be mutable. + * + * <p>By default, a {@link Bitmap} created will be immutable, but that can + * be changed with this call.</p> + * + * <p>Mutable Bitmaps are incompatible with {@link #ALLOCATOR_HARDWARE}, + * because {@link Bitmap.Config#HARDWARE} Bitmaps cannot be mutable. + * Attempting to combine them will throw an + * {@link java.lang.IllegalStateException}.</p> + * + * <p>Mutable Bitmaps are also incompatible with {@link #decodeDrawable}, + * which would require retrieving the Bitmap from the returned Drawable in + * order to modify. Attempting to decode a mutable {@link Drawable} will + * throw an {@link java.lang.IllegalStateException}.</p> + */ + public void setMutable(boolean mutable) { + mMutable = mutable; + } + + /** + * Specify whether to potentially save RAM at the expense of quality. + * + * Setting this to {@code true} may result in a {@link Bitmap} with a + * denser {@link Bitmap.Config}, depending on the image. For example, for + * an opaque {@link Bitmap}, this may result in a {@link Bitmap.Config} + * with no alpha information. + */ + public void setPreferRamOverQuality(boolean preferRamOverQuality) { + mPreferRamOverQuality = preferRamOverQuality; + } + + /** + * Specify whether to potentially treat the output as an alpha mask. + * + * <p>If this is set to {@code true} and the image is encoded in a format + * with only one channel, treat that channel as alpha. Otherwise this call has + * no effect.</p> + * + * <p>setAsAlphaMask is incompatible with {@link #ALLOCATOR_HARDWARE}. Trying to + * combine them will result in {@link #decodeDrawable}/ + * {@link #decodeBitmap} throwing an + * {@link java.lang.IllegalStateException}.</p> + */ + public void setAsAlphaMask(boolean asAlphaMask) { + mAsAlphaMask = asAlphaMask; + } + + @Override + public void close() { + mCloseGuard.close(); + if (!mClosed.compareAndSet(false, true)) { + return; + } + nClose(mNativePtr); + mNativePtr = 0; + + if (mOwnsInputStream) { + IoUtils.closeQuietly(mInputStream); + } + IoUtils.closeQuietly(mAssetFd); + + mInputStream = null; + mAssetFd = null; + mTempStorage = null; + } + + private void checkState() { + if (mNativePtr == 0) { + throw new IllegalStateException("Cannot use closed ImageDecoder!"); + } + + checkSubset(mDesiredWidth, mDesiredHeight, mCropRect); + + if (mAllocator == ALLOCATOR_HARDWARE) { + if (mMutable) { + throw new IllegalStateException("Cannot make mutable HARDWARE Bitmap!"); + } + if (mAsAlphaMask) { + throw new IllegalStateException("Cannot make HARDWARE Alpha mask Bitmap!"); + } + } + + if (mPostProcessor != null && mRequireUnpremultiplied) { + throw new IllegalStateException("Cannot draw to unpremultiplied pixels!"); + } + } + + private static void checkSubset(int width, int height, Rect r) { + if (r == null) { + return; + } + if (r.left < 0 || r.top < 0 || r.right > width || r.bottom > height) { + throw new IllegalStateException("Subset " + r + " not contained by " + + "scaled image bounds: (" + width + " x " + height + ")"); + } + } + + @NonNull + private Bitmap decodeBitmap() throws IOException { + checkState(); + // nDecodeBitmap calls onPartialImage only if mOnPartialImageListener + // exists + ImageDecoder partialImagePtr = mOnPartialImageListener == null ? null : this; + // nDecodeBitmap calls postProcessAndRelease only if mPostProcessor + // exists. + ImageDecoder postProcessPtr = mPostProcessor == null ? null : this; + return nDecodeBitmap(mNativePtr, partialImagePtr, + postProcessPtr, mDesiredWidth, mDesiredHeight, mCropRect, + mMutable, mAllocator, mRequireUnpremultiplied, + mPreferRamOverQuality, mAsAlphaMask); + + } + + private void callHeaderDecoded(@Nullable OnHeaderDecodedListener listener, + @NonNull Source src) { + if (listener != null) { + ImageInfo info = new ImageInfo(this); + try { + listener.onHeaderDecoded(this, info, src); + } finally { + info.mDecoder = null; + } + } + } + + /** + * Create a {@link Drawable} from a {@code Source}. + * + * @param src representing the encoded image. + * @param listener for learning the {@link ImageInfo} and changing any + * default settings on the {@code ImageDecoder}. If not {@code null}, + * this will be called on the same thread as {@code decodeDrawable} + * before that method returns. + * @return Drawable for displaying the image. + * @throws IOException if {@code src} is not found, is an unsupported + * format, or cannot be decoded for any reason. + */ + @NonNull + public static Drawable decodeDrawable(@NonNull Source src, + @Nullable OnHeaderDecodedListener listener) throws IOException { + try (ImageDecoder decoder = src.createImageDecoder()) { + decoder.mSource = src; + decoder.callHeaderDecoded(listener, src); + + if (decoder.mRequireUnpremultiplied) { + // Though this could be supported (ignored) for opaque images, + // it seems better to always report this error. + throw new IllegalStateException("Cannot decode a Drawable " + + "with unpremultiplied pixels!"); + } + + if (decoder.mMutable) { + throw new IllegalStateException("Cannot decode a mutable " + + "Drawable!"); + } + + // this call potentially manipulates the decoder so it must be performed prior to + // decoding the bitmap and after decode set the density on the resulting bitmap + final int srcDensity = computeDensity(src, decoder); + if (decoder.mAnimated) { + // AnimatedImageDrawable calls postProcessAndRelease only if + // mPostProcessor exists. + ImageDecoder postProcessPtr = decoder.mPostProcessor == null ? + null : decoder; + Drawable d = new AnimatedImageDrawable(decoder.mNativePtr, + postProcessPtr, decoder.mDesiredWidth, + decoder.mDesiredHeight, srcDensity, + src.computeDstDensity(), decoder.mCropRect, + decoder.mInputStream, decoder.mAssetFd); + // d has taken ownership of these objects. + decoder.mInputStream = null; + decoder.mAssetFd = null; + return d; + } + + Bitmap bm = decoder.decodeBitmap(); + bm.setDensity(srcDensity); + + Resources res = src.getResources(); + byte[] np = bm.getNinePatchChunk(); + if (np != null && NinePatch.isNinePatchChunk(np)) { + Rect opticalInsets = new Rect(); + bm.getOpticalInsets(opticalInsets); + Rect padding = new Rect(); + nGetPadding(decoder.mNativePtr, padding); + return new NinePatchDrawable(res, bm, np, padding, + opticalInsets, null); + } + + return new BitmapDrawable(res, bm); + } + } + + /** + * See {@link #decodeDrawable(Source, OnHeaderDecodedListener)}. + */ + @NonNull + public static Drawable decodeDrawable(@NonNull Source src) + throws IOException { + return decodeDrawable(src, null); + } + + /** + * Create a {@link Bitmap} from a {@code Source}. + * + * @param src representing the encoded image. + * @param listener for learning the {@link ImageInfo} and changing any + * default settings on the {@code ImageDecoder}. If not {@code null}, + * this will be called on the same thread as {@code decodeBitmap} + * before that method returns. + * @return Bitmap containing the image. + * @throws IOException if {@code src} is not found, is an unsupported + * format, or cannot be decoded for any reason. + */ + @NonNull + public static Bitmap decodeBitmap(@NonNull Source src, + @Nullable OnHeaderDecodedListener listener) throws IOException { + try (ImageDecoder decoder = src.createImageDecoder()) { + decoder.mSource = src; + decoder.callHeaderDecoded(listener, src); + + // this call potentially manipulates the decoder so it must be performed prior to + // decoding the bitmap + final int srcDensity = computeDensity(src, decoder); + Bitmap bm = decoder.decodeBitmap(); + bm.setDensity(srcDensity); + return bm; + } + } + + // This method may modify the decoder so it must be called prior to performing the decode + private static int computeDensity(@NonNull Source src, @NonNull ImageDecoder decoder) { + // if the caller changed the size then we treat the density as unknown + if (decoder.requestedResize()) { + return Bitmap.DENSITY_NONE; + } + + // Special stuff for compatibility mode: if the target density is not + // the same as the display density, but the resource -is- the same as + // the display density, then don't scale it down to the target density. + // This allows us to load the system's density-correct resources into + // an application in compatibility mode, without scaling those down + // to the compatibility density only to have them scaled back up when + // drawn to the screen. + Resources res = src.getResources(); + final int srcDensity = src.getDensity(); + if (res != null && res.getDisplayMetrics().noncompatDensityDpi == srcDensity) { + return srcDensity; + } + + // downscale the bitmap if the asset has a higher density than the default + final int dstDensity = src.computeDstDensity(); + if (srcDensity != Bitmap.DENSITY_NONE && srcDensity > dstDensity) { + float scale = (float) dstDensity / srcDensity; + int scaledWidth = (int) (decoder.mWidth * scale + 0.5f); + int scaledHeight = (int) (decoder.mHeight * scale + 0.5f); + decoder.setResize(scaledWidth, scaledHeight); + return dstDensity; + } + + return srcDensity; + } + + @NonNull + private String getMimeType() { + return nGetMimeType(mNativePtr); + } + + /** + * See {@link #decodeBitmap(Source, OnHeaderDecodedListener)}. + */ + @NonNull + public static Bitmap decodeBitmap(@NonNull Source src) throws IOException { + return decodeBitmap(src, null); + } + + /** + * Private method called by JNI. + */ + @SuppressWarnings("unused") + private int postProcessAndRelease(@NonNull Canvas canvas) { + try { + return mPostProcessor.onPostProcess(canvas); + } finally { + canvas.release(); + } + } + + /** + * Private method called by JNI. + */ + @SuppressWarnings("unused") + private boolean onPartialImage(@Error int error) { + return mOnPartialImageListener.onPartialImage(error, mSource); + } + + private static native ImageDecoder nCreate(long asset) throws IOException; + private static native ImageDecoder nCreate(ByteBuffer buffer, + int position, + int limit) throws IOException; + private static native ImageDecoder nCreate(byte[] data, int offset, + int length) throws IOException; + private static native ImageDecoder nCreate(InputStream is, byte[] storage); + // The fd must be seekable. + private static native ImageDecoder nCreate(FileDescriptor fd) throws IOException; + @NonNull + private static native Bitmap nDecodeBitmap(long nativePtr, + @Nullable ImageDecoder partialImageListener, + @Nullable ImageDecoder postProcessor, + int width, int height, + @Nullable Rect cropRect, boolean mutable, + int allocator, boolean requireUnpremul, + boolean preferRamOverQuality, boolean asAlphaMask) + throws IOException; + private static native Size nGetSampledSize(long nativePtr, + int sampleSize); + private static native void nGetPadding(long nativePtr, @NonNull Rect outRect); + private static native void nClose(long nativePtr); + private static native String nGetMimeType(long nativePtr); +} diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index aa9227c9bb08..42dac38affba 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -19,10 +19,8 @@ package android.graphics; import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Size; -import android.graphics.FontListParser; import android.graphics.fonts.FontVariationAxis; import android.os.LocaleList; -import android.text.FontConfig; import android.text.GraphicsOperations; import android.text.SpannableString; import android.text.SpannedString; @@ -33,14 +31,13 @@ import com.android.internal.annotations.GuardedBy; import dalvik.annotation.optimization.CriticalNative; import dalvik.annotation.optimization.FastNative; +import libcore.util.NativeAllocationRegistry; + import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Locale; -import libcore.util.NativeAllocationRegistry; - /** * The Paint class holds the style and color information about how to draw * geometries, text and bitmaps. @@ -60,11 +57,6 @@ public class Paint { Paint.class.getClassLoader(), nGetNativeFinalizer(), NATIVE_PAINT_SIZE); } - /** - * @hide - */ - public long mNativeTypeface; - private ColorFilter mColorFilter; private MaskFilter mMaskFilter; private PathEffect mPathEffect; @@ -93,7 +85,7 @@ public class Paint { * A map from a string representation of the LocaleList to Minikin's language list ID. */ @GuardedBy("sCacheLock") - private static final HashMap<String, Integer> sMinikinLangListIdCache = new HashMap<>(); + private static final HashMap<String, Integer> sMinikinLocaleListIdCache = new HashMap<>(); /** * @hide @@ -523,7 +515,6 @@ public class Paint { mShader = null; mNativeShader = 0; mTypeface = null; - mNativeTypeface = 0; mXfermode = null; mHasCompatScaling = false; @@ -566,7 +557,6 @@ public class Paint { mShader = paint.mShader; mNativeShader = paint.mNativeShader; mTypeface = paint.mTypeface; - mNativeTypeface = paint.mNativeTypeface; mXfermode = paint.mXfermode; mHasCompatScaling = paint.mHasCompatScaling; @@ -822,14 +812,14 @@ public class Paint { * @hide */ public float getUnderlinePosition() { - return nGetUnderlinePosition(mNativePaint, mNativeTypeface); + return nGetUnderlinePosition(mNativePaint); } /** * @hide */ public float getUnderlineThickness() { - return nGetUnderlineThickness(mNativePaint, mNativeTypeface); + return nGetUnderlineThickness(mNativePaint); } /** @@ -858,14 +848,14 @@ public class Paint { * @hide */ public float getStrikeThruPosition() { - return nGetStrikeThruPosition(mNativePaint, mNativeTypeface); + return nGetStrikeThruPosition(mNativePaint); } /** * @hide */ public float getStrikeThruThickness() { - return nGetStrikeThruThickness(mNativePaint, mNativeTypeface); + return nGetStrikeThruThickness(mNativePaint); } /** @@ -1274,13 +1264,9 @@ public class Paint { * @return typeface */ public Typeface setTypeface(Typeface typeface) { - long typefaceNative = 0; - if (typeface != null) { - typefaceNative = typeface.native_instance; - } + final long typefaceNative = typeface == null ? 0 : typeface.native_instance; nSetTypeface(mNativePaint, typefaceNative); mTypeface = typeface; - mNativeTypeface = typefaceNative; return typeface; } @@ -1456,16 +1442,16 @@ public class Paint { private void syncTextLocalesWithMinikin() { final String languageTags = mLocales.toLanguageTags(); - final Integer minikinLangListId; + final Integer minikinLocaleListId; synchronized (sCacheLock) { - minikinLangListId = sMinikinLangListIdCache.get(languageTags); - if (minikinLangListId == null) { + minikinLocaleListId = sMinikinLocaleListIdCache.get(languageTags); + if (minikinLocaleListId == null) { final int newID = nSetTextLocales(mNativePaint, languageTags); - sMinikinLangListIdCache.put(languageTags, newID); + sMinikinLocaleListIdCache.put(languageTags, newID); return; } } - nSetTextLocalesByMinikinLangListId(mNativePaint, minikinLangListId.intValue()); + nSetTextLocalesByMinikinLocaleListId(mNativePaint, minikinLocaleListId.intValue()); } /** @@ -1740,22 +1726,28 @@ public class Paint { * Return the distance above (negative) the baseline (ascent) based on the * current typeface and text size. * + * <p>Note that this is the ascent of the main typeface, and actual text rendered may need a + * larger ascent because fallback fonts may get used in rendering the text. + * * @return the distance above (negative) the baseline (ascent) based on the * current typeface and text size. */ public float ascent() { - return nAscent(mNativePaint, mNativeTypeface); + return nAscent(mNativePaint); } /** * Return the distance below (positive) the baseline (descent) based on the * current typeface and text size. * + * <p>Note that this is the descent of the main typeface, and actual text rendered may need a + * larger descent because fallback fonts may get used in rendering the text. + * * @return the distance below (positive) the baseline (descent) based on * the current typeface and text size. */ public float descent() { - return nDescent(mNativePaint, mNativeTypeface); + return nDescent(mNativePaint); } /** @@ -1794,12 +1786,15 @@ public class Paint { * settings for typeface, textSize, etc. If metrics is not null, return the * fontmetric values in it. * + * <p>Note that these are the values for the main typeface, and actual text rendered may need a + * larger set of values because fallback fonts may get used in rendering the text. + * * @param metrics If this object is not null, its fields are filled with * the appropriate values given the paint's text attributes. * @return the font's recommended interline spacing. */ public float getFontMetrics(FontMetrics metrics) { - return nGetFontMetrics(mNativePaint, mNativeTypeface, metrics); + return nGetFontMetrics(mNativePaint, metrics); } /** @@ -1855,10 +1850,13 @@ public class Paint { * and clipping. If you want more control over the rounding, call * getFontMetrics(). * + * <p>Note that these are the values for the main typeface, and actual text rendered may need a + * larger set of values because fallback fonts may get used in rendering the text. + * * @return the font's interline spacing. */ public int getFontMetricsInt(FontMetricsInt fmi) { - return nGetFontMetricsInt(mNativePaint, mNativeTypeface, fmi); + return nGetFontMetricsInt(mNativePaint, fmi); } public FontMetricsInt getFontMetricsInt() { @@ -1871,6 +1869,9 @@ public class Paint { * Return the recommend line spacing based on the current typeface and * text size. * + * <p>Note that this is the value for the main typeface, and actual text rendered may need a + * larger value because fallback fonts may get used in rendering the text. + * * @return recommend line spacing based on the current typeface and * text size. */ @@ -1898,14 +1899,14 @@ public class Paint { return 0f; } if (!mHasCompatScaling) { - return (float) Math.ceil(nGetTextAdvances(mNativePaint, mNativeTypeface, text, + return (float) Math.ceil(nGetTextAdvances(mNativePaint, text, index, count, index, count, mBidiFlags, null, 0)); } final float oldSize = getTextSize(); setTextSize(oldSize * mCompatScaling); - float w = nGetTextAdvances(mNativePaint, mNativeTypeface, text, index, count, index, - count, mBidiFlags, null, 0); + final float w = nGetTextAdvances(mNativePaint, text, index, count, index, count, + mBidiFlags, null, 0); setTextSize(oldSize); return (float) Math.ceil(w*mInvCompatScaling); } @@ -1930,13 +1931,13 @@ public class Paint { return 0f; } if (!mHasCompatScaling) { - return (float) Math.ceil(nGetTextAdvances(mNativePaint, mNativeTypeface, text, + return (float) Math.ceil(nGetTextAdvances(mNativePaint, text, start, end, start, end, mBidiFlags, null, 0)); } final float oldSize = getTextSize(); setTextSize(oldSize * mCompatScaling); - float w = nGetTextAdvances(mNativePaint, mNativeTypeface, text, start, end, start, - end, mBidiFlags, null, 0); + final float w = nGetTextAdvances(mNativePaint, text, start, end, start, end, mBidiFlags, + null, 0); setTextSize(oldSize); return (float) Math.ceil(w * mInvCompatScaling); } @@ -2019,14 +2020,14 @@ public class Paint { return 0; } if (!mHasCompatScaling) { - return nBreakText(mNativePaint, mNativeTypeface, text, index, count, maxWidth, - mBidiFlags, measuredWidth); + return nBreakText(mNativePaint, text, index, count, maxWidth, mBidiFlags, + measuredWidth); } final float oldSize = getTextSize(); setTextSize(oldSize * mCompatScaling); - int res = nBreakText(mNativePaint, mNativeTypeface, text, index, count, - maxWidth * mCompatScaling, mBidiFlags, measuredWidth); + final int res = nBreakText(mNativePaint, text, index, count, maxWidth * mCompatScaling, + mBidiFlags, measuredWidth); setTextSize(oldSize); if (measuredWidth != null) measuredWidth[0] *= mInvCompatScaling; return res; @@ -2107,14 +2108,14 @@ public class Paint { return 0; } if (!mHasCompatScaling) { - return nBreakText(mNativePaint, mNativeTypeface, text, measureForwards, + return nBreakText(mNativePaint, text, measureForwards, maxWidth, mBidiFlags, measuredWidth); } final float oldSize = getTextSize(); setTextSize(oldSize*mCompatScaling); - int res = nBreakText(mNativePaint, mNativeTypeface, text, measureForwards, - maxWidth*mCompatScaling, mBidiFlags, measuredWidth); + final int res = nBreakText(mNativePaint, text, measureForwards, maxWidth*mCompatScaling, + mBidiFlags, measuredWidth); setTextSize(oldSize); if (measuredWidth != null) measuredWidth[0] *= mInvCompatScaling; return res; @@ -2144,15 +2145,13 @@ public class Paint { return 0; } if (!mHasCompatScaling) { - nGetTextAdvances(mNativePaint, mNativeTypeface, text, index, count, index, count, - mBidiFlags, widths, 0); + nGetTextAdvances(mNativePaint, text, index, count, index, count, mBidiFlags, widths, 0); return count; } final float oldSize = getTextSize(); setTextSize(oldSize * mCompatScaling); - nGetTextAdvances(mNativePaint, mNativeTypeface, text, index, count, index, count, - mBidiFlags, widths, 0); + nGetTextAdvances(mNativePaint, text, index, count, index, count, mBidiFlags, widths, 0); setTextSize(oldSize); for (int i = 0; i < count; i++) { widths[i] *= mInvCompatScaling; @@ -2229,15 +2228,13 @@ public class Paint { return 0; } if (!mHasCompatScaling) { - nGetTextAdvances(mNativePaint, mNativeTypeface, text, start, end, start, end, - mBidiFlags, widths, 0); + nGetTextAdvances(mNativePaint, text, start, end, start, end, mBidiFlags, widths, 0); return end - start; } final float oldSize = getTextSize(); setTextSize(oldSize * mCompatScaling); - nGetTextAdvances(mNativePaint, mNativeTypeface, text, start, end, start, end, - mBidiFlags, widths, 0); + nGetTextAdvances(mNativePaint, text, start, end, start, end, mBidiFlags, widths, 0); setTextSize(oldSize); for (int i = 0; i < end - start; i++) { widths[i] *= mInvCompatScaling; @@ -2284,16 +2281,15 @@ public class Paint { return 0f; } if (!mHasCompatScaling) { - return nGetTextAdvances(mNativePaint, mNativeTypeface, chars, index, count, - contextIndex, contextCount, isRtl ? BIDI_FORCE_RTL : BIDI_FORCE_LTR, advances, + return nGetTextAdvances(mNativePaint, chars, index, count, contextIndex, contextCount, + isRtl ? BIDI_FORCE_RTL : BIDI_FORCE_LTR, advances, advancesIndex); } final float oldSize = getTextSize(); setTextSize(oldSize * mCompatScaling); - float res = nGetTextAdvances(mNativePaint, mNativeTypeface, chars, index, count, - contextIndex, contextCount, isRtl ? BIDI_FORCE_RTL : BIDI_FORCE_LTR, advances, - advancesIndex); + final float res = nGetTextAdvances(mNativePaint, chars, index, count, contextIndex, + contextCount, isRtl ? BIDI_FORCE_RTL : BIDI_FORCE_LTR, advances, advancesIndex); setTextSize(oldSize); if (advances != null) { @@ -2411,16 +2407,14 @@ public class Paint { } if (!mHasCompatScaling) { - return nGetTextAdvances(mNativePaint, mNativeTypeface, text, start, end, - contextStart, contextEnd, isRtl ? BIDI_FORCE_RTL : BIDI_FORCE_LTR, advances, - advancesIndex); + return nGetTextAdvances(mNativePaint, text, start, end, contextStart, contextEnd, + isRtl ? BIDI_FORCE_RTL : BIDI_FORCE_LTR, advances, advancesIndex); } final float oldSize = getTextSize(); setTextSize(oldSize * mCompatScaling); - float totalAdvance = nGetTextAdvances(mNativePaint, mNativeTypeface, text, start, - end, contextStart, contextEnd, isRtl ? BIDI_FORCE_RTL : BIDI_FORCE_LTR, advances, - advancesIndex); + final float totalAdvance = nGetTextAdvances(mNativePaint, text, start, end, contextStart, + contextEnd, isRtl ? BIDI_FORCE_RTL : BIDI_FORCE_LTR, advances, advancesIndex); setTextSize(oldSize); if (advances != null) { @@ -2467,8 +2461,8 @@ public class Paint { throw new IndexOutOfBoundsException(); } - return nGetTextRunCursor(mNativePaint, mNativeTypeface, text, - contextStart, contextLength, dir, offset, cursorOpt); + return nGetTextRunCursor(mNativePaint, text, contextStart, contextLength, dir, offset, + cursorOpt); } /** @@ -2553,8 +2547,8 @@ public class Paint { throw new IndexOutOfBoundsException(); } - return nGetTextRunCursor(mNativePaint, mNativeTypeface, text, - contextStart, contextEnd, dir, offset, cursorOpt); + return nGetTextRunCursor(mNativePaint, text, contextStart, contextEnd, dir, offset, + cursorOpt); } /** @@ -2574,8 +2568,7 @@ public class Paint { if ((index | count) < 0 || index + count > text.length) { throw new ArrayIndexOutOfBoundsException(); } - nGetTextPath(mNativePaint, mNativeTypeface, mBidiFlags, text, index, count, x, y, - path.mutateNI()); + nGetTextPath(mNativePaint, mBidiFlags, text, index, count, x, y, path.mutateNI()); } /** @@ -2595,8 +2588,7 @@ public class Paint { if ((start | end | (end - start) | (text.length() - end)) < 0) { throw new IndexOutOfBoundsException(); } - nGetTextPath(mNativePaint, mNativeTypeface, mBidiFlags, text, start, end, x, y, - path.mutateNI()); + nGetTextPath(mNativePaint, mBidiFlags, text, start, end, x, y, path.mutateNI()); } /** @@ -2615,7 +2607,7 @@ public class Paint { if (bounds == null) { throw new NullPointerException("need bounds Rect"); } - nGetStringBounds(mNativePaint, mNativeTypeface, text, start, end, mBidiFlags, bounds); + nGetStringBounds(mNativePaint, text, start, end, mBidiFlags, bounds); } /** @@ -2657,7 +2649,7 @@ public class Paint { if (bounds == null) { throw new NullPointerException("need bounds Rect"); } - nGetCharArrayBounds(mNativePaint, mNativeTypeface, text, index, count, mBidiFlags, + nGetCharArrayBounds(mNativePaint, text, index, count, mBidiFlags, bounds); } @@ -2678,7 +2670,7 @@ public class Paint { * @return true if the typeface has a glyph for the string */ public boolean hasGlyph(String string) { - return nHasGlyph(mNativePaint, mNativeTypeface, mBidiFlags, string); + return nHasGlyph(mNativePaint, mBidiFlags, string); } /** @@ -2731,8 +2723,8 @@ public class Paint { return 0.0f; } // TODO: take mCompatScaling into account (or eliminate compat scaling)? - return nGetRunAdvance(mNativePaint, mNativeTypeface, text, start, end, - contextStart, contextEnd, isRtl, offset); + return nGetRunAdvance(mNativePaint, text, start, end, contextStart, contextEnd, isRtl, + offset); } /** @@ -2808,8 +2800,8 @@ public class Paint { throw new IndexOutOfBoundsException(); } // TODO: take mCompatScaling into account (or eliminate compat scaling)? - return nGetOffsetForAdvance(mNativePaint, mNativeTypeface, text, start, end, - contextStart, contextEnd, isRtl, advance); + return nGetOffsetForAdvance(mNativePaint, text, start, end, contextStart, contextEnd, + isRtl, advance); } /** @@ -2843,42 +2835,45 @@ public class Paint { return result; } + /** + * Returns true of the passed {@link Paint} will have the same effect on text measurement + * + * @param other A {@link Paint} object. + * @return true if the other {@link Paint} has the same effect on text measurement. + */ + public boolean equalsForTextMeasurement(@NonNull Paint other) { + return nEqualsForTextMeasurement(mNativePaint, other.mNativePaint); + } + // regular JNI private static native long nGetNativeFinalizer(); private static native long nInit(); private static native long nInitWithPaint(long paint); - private static native int nBreakText(long nObject, long nTypeface, - char[] text, int index, int count, + private static native int nBreakText(long nObject, char[] text, int index, int count, float maxWidth, int bidiFlags, float[] measuredWidth); - private static native int nBreakText(long nObject, long nTypeface, - String text, boolean measureForwards, + private static native int nBreakText(long nObject, String text, boolean measureForwards, float maxWidth, int bidiFlags, float[] measuredWidth); - private static native float nGetTextAdvances(long paintPtr, long typefacePtr, - char[] text, int index, int count, int contextIndex, int contextCount, - int bidiFlags, float[] advances, int advancesIndex); - private static native float nGetTextAdvances(long paintPtr, long typefacePtr, - String text, int start, int end, int contextStart, int contextEnd, - int bidiFlags, float[] advances, int advancesIndex); - private native int nGetTextRunCursor(long paintPtr, long typefacePtr, char[] text, - int contextStart, int contextLength, int dir, int offset, int cursorOpt); - private native int nGetTextRunCursor(long paintPtr, long typefacePtr, String text, - int contextStart, int contextEnd, int dir, int offset, int cursorOpt); - private static native void nGetTextPath(long paintPtr, long typefacePtr, - int bidiFlags, char[] text, int index, int count, float x, float y, long path); - private static native void nGetTextPath(long paintPtr, long typefacePtr, - int bidiFlags, String text, int start, int end, float x, float y, long path); - private static native void nGetStringBounds(long nativePaint, long typefacePtr, - String text, int start, int end, int bidiFlags, Rect bounds); - private static native void nGetCharArrayBounds(long nativePaint, long typefacePtr, - char[] text, int index, int count, int bidiFlags, Rect bounds); - private static native boolean nHasGlyph(long paintPtr, long typefacePtr, - int bidiFlags, String string); - private static native float nGetRunAdvance(long paintPtr, long typefacePtr, - char[] text, int start, int end, int contextStart, int contextEnd, boolean isRtl, - int offset); - private static native int nGetOffsetForAdvance(long paintPtr, - long typefacePtr, char[] text, int start, int end, int contextStart, int contextEnd, - boolean isRtl, float advance); + private static native float nGetTextAdvances(long paintPtr, char[] text, int index, int count, + int contextIndex, int contextCount, int bidiFlags, float[] advances, int advancesIndex); + private static native float nGetTextAdvances(long paintPtr, String text, int start, int end, + int contextStart, int contextEnd, int bidiFlags, float[] advances, int advancesIndex); + private native int nGetTextRunCursor(long paintPtr, char[] text, int contextStart, + int contextLength, int dir, int offset, int cursorOpt); + private native int nGetTextRunCursor(long paintPtr, String text, int contextStart, + int contextEnd, int dir, int offset, int cursorOpt); + private static native void nGetTextPath(long paintPtr, int bidiFlags, char[] text, int index, + int count, float x, float y, long path); + private static native void nGetTextPath(long paintPtr, int bidiFlags, String text, int start, + int end, float x, float y, long path); + private static native void nGetStringBounds(long nativePaint, String text, int start, int end, + int bidiFlags, Rect bounds); + private static native void nGetCharArrayBounds(long nativePaint, char[] text, int index, + int count, int bidiFlags, Rect bounds); + private static native boolean nHasGlyph(long paintPtr, int bidiFlags, String string); + private static native float nGetRunAdvance(long paintPtr, char[] text, int start, int end, + int contextStart, int contextEnd, boolean isRtl, int offset); + private static native int nGetOffsetForAdvance(long paintPtr, char[] text, int start, int end, + int contextStart, int contextEnd, boolean isRtl, float advance); // ---------------- @FastNative ------------------------ @@ -2888,11 +2883,9 @@ public class Paint { @FastNative private static native void nSetFontFeatureSettings(long paintPtr, String settings); @FastNative - private static native float nGetFontMetrics(long paintPtr, - long typefacePtr, FontMetrics metrics); + private static native float nGetFontMetrics(long paintPtr, FontMetrics metrics); @FastNative - private static native int nGetFontMetricsInt(long paintPtr, - long typefacePtr, FontMetricsInt fmi); + private static native int nGetFontMetricsInt(long paintPtr, FontMetricsInt fmi); // ---------------- @CriticalNative ------------------------ @@ -2926,14 +2919,14 @@ public class Paint { @CriticalNative private static native long nSetMaskFilter(long paintPtr, long maskfilter); @CriticalNative - private static native long nSetTypeface(long paintPtr, long typeface); + private static native void nSetTypeface(long paintPtr, long typeface); @CriticalNative private static native int nGetTextAlign(long paintPtr); @CriticalNative private static native void nSetTextAlign(long paintPtr, int align); @CriticalNative - private static native void nSetTextLocalesByMinikinLangListId(long paintPtr, - int mMinikinLangListId); + private static native void nSetTextLocalesByMinikinLocaleListId(long paintPtr, + int mMinikinLocaleListId); @CriticalNative private static native void nSetShadowLayer(long paintPtr, float radius, float dx, float dy, int color); @@ -3006,17 +2999,19 @@ public class Paint { @CriticalNative private static native void nSetTextSkewX(long paintPtr, float skewX); @CriticalNative - private static native float nAscent(long paintPtr, long typefacePtr); + private static native float nAscent(long paintPtr); @CriticalNative - private static native float nDescent(long paintPtr, long typefacePtr); + private static native float nDescent(long paintPtr); @CriticalNative - private static native float nGetUnderlinePosition(long paintPtr, long typefacePtr); + private static native float nGetUnderlinePosition(long paintPtr); @CriticalNative - private static native float nGetUnderlineThickness(long paintPtr, long typefacePtr); + private static native float nGetUnderlineThickness(long paintPtr); @CriticalNative - private static native float nGetStrikeThruPosition(long paintPtr, long typefacePtr); + private static native float nGetStrikeThruPosition(long paintPtr); @CriticalNative - private static native float nGetStrikeThruThickness(long paintPtr, long typefacePtr); + private static native float nGetStrikeThruThickness(long paintPtr); @CriticalNative private static native void nSetTextSize(long paintPtr, float textSize); + @CriticalNative + private static native boolean nEqualsForTextMeasurement(long leftPaintPtr, long rightPaintPtr); } diff --git a/graphics/java/android/graphics/Picture.java b/graphics/java/android/graphics/Picture.java index 08eeaff69f9b..9ac94d895a37 100644 --- a/graphics/java/android/graphics/Picture.java +++ b/graphics/java/android/graphics/Picture.java @@ -31,8 +31,9 @@ import java.io.OutputStream; * be replayed on a hardware accelerated canvas.</p> */ public class Picture { - private Canvas mRecordingCanvas; + private PictureCanvas mRecordingCanvas; private long mNativePicture; + private boolean mRequiresHwAcceleration; private static final int WORKING_STREAM_STORAGE = 16 * 1024; @@ -78,8 +79,12 @@ public class Picture { * into it. */ public Canvas beginRecording(int width, int height) { + if (mRecordingCanvas != null) { + throw new IllegalStateException("Picture already recording, must call #endRecording()"); + } long ni = nativeBeginRecording(mNativePicture, width, height); - mRecordingCanvas = new RecordingCanvas(this, ni); + mRecordingCanvas = new PictureCanvas(this, ni); + mRequiresHwAcceleration = false; return mRecordingCanvas; } @@ -91,6 +96,7 @@ public class Picture { */ public void endRecording() { if (mRecordingCanvas != null) { + mRequiresHwAcceleration = mRecordingCanvas.mHoldsHwBitmap; mRecordingCanvas = null; nativeEndRecording(mNativePicture); } @@ -113,6 +119,18 @@ public class Picture { } /** + * Indicates whether or not this Picture contains recorded commands that only work when + * drawn to a hardware-accelerated canvas. If this returns true then this Picture can only + * be drawn to another Picture or to a Canvas where canvas.isHardwareAccelerated() is true. + * + * @return true if the Picture can only be drawn to a hardware-accelerated canvas, + * false otherwise. + */ + public boolean requiresHardwareAcceleration() { + return mRequiresHwAcceleration; + } + + /** * Draw this picture on the canvas. * <p> * Prior to {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this call could @@ -129,6 +147,9 @@ public class Picture { if (mRecordingCanvas != null) { endRecording(); } + if (mRequiresHwAcceleration && !canvas.isHardwareAccelerated()) { + canvas.onHwBitmapInSwMode(); + } nativeDraw(canvas.getNativeCanvasWrapper(), mNativePicture); } @@ -164,8 +185,7 @@ public class Picture { if (stream == null) { throw new NullPointerException(); } - if (!nativeWriteToStream(mNativePicture, stream, - new byte[WORKING_STREAM_STORAGE])) { + if (!nativeWriteToStream(mNativePicture, stream, new byte[WORKING_STREAM_STORAGE])) { throw new RuntimeException(); } } @@ -182,10 +202,11 @@ public class Picture { OutputStream stream, byte[] storage); private static native void nativeDestructor(long nativePicture); - private static class RecordingCanvas extends Canvas { + private static class PictureCanvas extends Canvas { private final Picture mPicture; + boolean mHoldsHwBitmap; - public RecordingCanvas(Picture pict, long nativeCanvas) { + public PictureCanvas(Picture pict, long nativeCanvas) { super(nativeCanvas); mPicture = pict; } @@ -202,5 +223,10 @@ public class Picture { } super.drawPicture(picture); } + + @Override + protected void onHwBitmapInSwMode() { + mHoldsHwBitmap = true; + } } } diff --git a/graphics/java/android/graphics/PixelFormat.java b/graphics/java/android/graphics/PixelFormat.java index f93886dcb06b..96d6eeece0a3 100644 --- a/graphics/java/android/graphics/PixelFormat.java +++ b/graphics/java/android/graphics/PixelFormat.java @@ -185,4 +185,52 @@ public class PixelFormat { return false; } + + /** + * @hide + */ + public static String formatToString(@Format int format) { + switch (format) { + case UNKNOWN: + return "UNKNOWN"; + case TRANSLUCENT: + return "TRANSLUCENT"; + case TRANSPARENT: + return "TRANSPARENT"; + case RGBA_8888: + return "RGBA_8888"; + case RGBX_8888: + return "RGBX_8888"; + case RGB_888: + return "RGB_888"; + case RGB_565: + return "RGB_565"; + case RGBA_5551: + return "RGBA_5551"; + case RGBA_4444: + return "RGBA_4444"; + case A_8: + return "A_8"; + case L_8: + return "L_8"; + case LA_88: + return "LA_88"; + case RGB_332: + return "RGB_332"; + case YCbCr_422_SP: + return "YCbCr_422_SP"; + case YCbCr_420_SP: + return "YCbCr_420_SP"; + case YCbCr_422_I: + return "YCbCr_422_I"; + case RGBA_F16: + return "RGBA_F16"; + case RGBA_1010102: + return "RGBA_1010102"; + case JPEG: + return "JPEG"; + default: + return Integer.toString(format); + } + } } diff --git a/graphics/java/android/graphics/Point.java b/graphics/java/android/graphics/Point.java index abcccbdbc9fb..c6b6c668f03a 100644 --- a/graphics/java/android/graphics/Point.java +++ b/graphics/java/android/graphics/Point.java @@ -18,6 +18,7 @@ package android.graphics; import android.os.Parcel; import android.os.Parcelable; +import android.util.proto.ProtoOutputStream; import java.io.PrintWriter; @@ -121,6 +122,21 @@ public class Point implements Parcelable { out.writeInt(y); } + /** + * Write to a protocol buffer output stream. + * Protocol buffer message definition at {@link android.graphics.PointProto} + * + * @param protoOutputStream Stream to write the Rect object to. + * @param fieldId Field Id of the Rect as defined in the parent message + * @hide + */ + public void writeToProto(ProtoOutputStream protoOutputStream, long fieldId) { + final long token = protoOutputStream.start(fieldId); + protoOutputStream.write(PointProto.X, x); + protoOutputStream.write(PointProto.Y, y); + protoOutputStream.end(token); + } + public static final Parcelable.Creator<Point> CREATOR = new Parcelable.Creator<Point>() { /** * Return a new point from the data in the specified parcel. diff --git a/graphics/java/android/graphics/PostProcessor.java b/graphics/java/android/graphics/PostProcessor.java new file mode 100644 index 000000000000..b1712e92e2fe --- /dev/null +++ b/graphics/java/android/graphics/PostProcessor.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2017 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.graphics; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.graphics.drawable.Drawable; + +/** + * Helper interface for adding custom processing to an image. + * + * <p>The image being processed may be a {@link Drawable}, {@link Bitmap} or frame + * of an animated image produced by {@link ImageDecoder}. This is called before + * the requested object is returned.</p> + * + * <p>This custom processing also applies to image types that are otherwise + * immutable, such as {@link Bitmap.Config#HARDWARE}.</p> + * + * <p>On an animated image, the callback will only be called once, but the drawing + * commands will be applied to each frame, as if the {@code Canvas} had been + * returned by {@link Picture#beginRecording}.<p> + * + * <p>Supplied to ImageDecoder via {@link ImageDecoder#setPostProcessor}.</p> + */ +public interface PostProcessor { + /** + * Do any processing after (for example) decoding. + * + * <p>Drawing to the {@link Canvas} will behave as if the initial processing + * (e.g. decoding) already exists in the Canvas. An implementation can draw + * effects on top of this, or it can even draw behind it using + * {@link PorterDuff.Mode#DST_OVER}. A common effect is to add transparency + * to the corners to achieve rounded corners. That can be done with the + * following code:</p> + * + * <code> + * Path path = new Path(); + * path.setFillType(Path.FillType.INVERSE_EVEN_ODD); + * int width = canvas.getWidth(); + * int height = canvas.getHeight(); + * path.addRoundRect(0, 0, width, height, 20, 20, Path.Direction.CW); + * Paint paint = new Paint(); + * paint.setAntiAlias(true); + * paint.setColor(Color.TRANSPARENT); + * paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); + * canvas.drawPath(path, paint); + * return PixelFormat.TRANSLUCENT; + * </code> + * + * + * @param canvas The {@link Canvas} to draw to. + * @return Opacity of the result after drawing. + * {@link PixelFormat#UNKNOWN} means that the implementation did not + * change whether the image has alpha. Return this unless you added + * transparency (e.g. with the code above, in which case you should + * return {@code PixelFormat.TRANSLUCENT}) or you forced the image to + * be opaque (e.g. by drawing everywhere with an opaque color and + * {@code PorterDuff.Mode.DST_OVER}, in which case you should return + * {@code PixelFormat.OPAQUE}). + * {@link PixelFormat#TRANSLUCENT} means that the implementation added + * transparency. This is safe to return even if the image already had + * transparency. This is also safe to return if the result is opaque, + * though it may draw more slowly. + * {@link PixelFormat#OPAQUE} means that the implementation forced the + * image to be opaque. This is safe to return even if the image was + * already opaque. + * {@link PixelFormat#TRANSPARENT} (or any other integer) is not + * allowed, and will result in throwing an + * {@link java.lang.IllegalArgumentException}. + */ + @PixelFormat.Opacity + public int onPostProcess(@NonNull Canvas canvas); +} diff --git a/graphics/java/android/graphics/Rect.java b/graphics/java/android/graphics/Rect.java index deafb6638ece..aff942da78d1 100644 --- a/graphics/java/android/graphics/Rect.java +++ b/graphics/java/android/graphics/Rect.java @@ -21,6 +21,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import android.util.proto.ProtoOutputStream; import java.io.PrintWriter; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -194,7 +195,24 @@ public final class Rect implements Parcelable { pw.print(top); pw.print("]["); pw.print(right); pw.print(','); pw.print(bottom); pw.print(']'); } - + + /** + * Write to a protocol buffer output stream. + * Protocol buffer message definition at {@link android.graphics.RectProto} + * + * @param protoOutputStream Stream to write the Rect object to. + * @param fieldId Field Id of the Rect as defined in the parent message + * @hide + */ + public void writeToProto(ProtoOutputStream protoOutputStream, long fieldId) { + final long token = protoOutputStream.start(fieldId); + protoOutputStream.write(RectProto.LEFT, left); + protoOutputStream.write(RectProto.TOP, top); + protoOutputStream.write(RectProto.RIGHT, right); + protoOutputStream.write(RectProto.BOTTOM, bottom); + protoOutputStream.end(token); + } + /** * Returns true if the rectangle is empty (left >= right or top >= bottom) */ @@ -457,6 +475,19 @@ public final class Rect implements Parcelable { } /** + * If the specified rectangle intersects this rectangle, set this rectangle to that + * intersection, otherwise set this rectangle to the empty rectangle. + * @see #inset(int, int, int, int) but without checking if the rects overlap. + * @hide + */ + public void intersectUnchecked(Rect other) { + left = Math.max(left, other.left); + top = Math.max(top, other.top); + right = Math.min(right, other.right); + bottom = Math.min(bottom, other.bottom); + } + + /** * If rectangles a and b intersect, return true and set this rectangle to * that intersection, otherwise return false and do not change this * rectangle. No check is performed to see if either rectangle is empty. diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java index c4b56c333c64..8595165aab27 100644 --- a/graphics/java/android/graphics/Typeface.java +++ b/graphics/java/android/graphics/Typeface.java @@ -16,28 +16,21 @@ package android.graphics; -import static android.content.res.FontResourcesParser.ProviderResourceEntry; -import static android.content.res.FontResourcesParser.FontFileResourceEntry; -import static android.content.res.FontResourcesParser.FontFamilyFilesResourceEntry; import static android.content.res.FontResourcesParser.FamilyResourceEntry; +import static android.content.res.FontResourcesParser.FontFamilyFilesResourceEntry; +import static android.content.res.FontResourcesParser.FontFileResourceEntry; +import static android.content.res.FontResourcesParser.ProviderResourceEntry; -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.res.AssetManager; -import android.graphics.FontListParser; import android.graphics.fonts.FontVariationAxis; import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.os.ParcelFileDescriptor; -import android.os.ResultReceiver; import android.provider.FontRequest; import android.provider.FontsContract; import android.text.FontConfig; +import android.util.ArrayMap; import android.util.Base64; import android.util.Log; import android.util.LongSparseArray; @@ -45,10 +38,9 @@ import android.util.LruCache; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; -import libcore.io.IoUtils; - import org.xmlpull.v1.XmlPullParserException; import java.io.File; @@ -56,18 +48,15 @@ import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; -import java.util.Arrays; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; /** * The Typeface class specifies the typeface and intrinsic style of a font. @@ -95,21 +84,33 @@ public class Typeface { public static final Typeface MONOSPACE; static Typeface[] sDefaults; - private static final LongSparseArray<SparseArray<Typeface>> sTypefaceCache = + + /** + * Cache for Typeface objects for style variant. Currently max size is 3. + */ + @GuardedBy("sStyledCacheLock") + private static final LongSparseArray<SparseArray<Typeface>> sStyledTypefaceCache = new LongSparseArray<>(3); + private static final Object sStyledCacheLock = new Object(); + + /** + * Cache for Typeface objects for weight variant. Currently max size is 3. + */ + @GuardedBy("sWeightCacheLock") + private static final LongSparseArray<SparseArray<Typeface>> sWeightTypefaceCache = + new LongSparseArray<>(3); + private static final Object sWeightCacheLock = new Object(); /** * Cache for Typeface objects dynamically loaded from assets. Currently max size is 16. */ - @GuardedBy("sLock") + @GuardedBy("sDynamicCacheLock") private static final LruCache<String, Typeface> sDynamicTypefaceCache = new LruCache<>(16); + private static final Object sDynamicCacheLock = new Object(); static Typeface sDefaultTypeface; - static Map<String, Typeface> sSystemFontMap; - static FontFamily[] sFallbackFonts; - private static final Object sLock = new Object(); - - static final String FONTS_CONFIG = "fonts.xml"; + static final Map<String, Typeface> sSystemFontMap; + static final Map<String, FontFamily[]> sSystemFallbackMap; /** * @hide @@ -121,6 +122,7 @@ public class Typeface { public static final int BOLD = 1; public static final int ITALIC = 2; public static final int BOLD_ITALIC = 3; + /** @hide */ public static final int STYLE_MASK = 0x03; private int mStyle = 0; private int mWeight = 0; @@ -129,6 +131,7 @@ public class Typeface { // Must be the same as the C++ constant in core/jni/android/graphics/FontFamily.cpp /** @hide */ public static final int RESOLVE_BY_FONT_TABLE = -1; + private static final String DEFAULT_FAMILY = "sans-serif"; // Style value for building typeface. private static final int STYLE_NORMAL = 0; @@ -142,6 +145,13 @@ public class Typeface { nativeSetDefault(t.native_instance); } + // TODO: Make this public API. (b/64852739) + /** @hide */ + @VisibleForTesting + public int getWeight() { + return mWeight; + } + /** Returns the typeface's intrinsic style attributes */ public int getStyle() { return mStyle; @@ -163,28 +173,27 @@ public class Typeface { */ @Nullable public static Typeface createFromResources(AssetManager mgr, String path, int cookie) { - if (sFallbackFonts != null) { - synchronized (sDynamicTypefaceCache) { - final String key = Builder.createAssetUid( - mgr, path, 0 /* ttcIndex */, null /* axes */, - RESOLVE_BY_FONT_TABLE /* weight */, RESOLVE_BY_FONT_TABLE /* italic */); - Typeface typeface = sDynamicTypefaceCache.get(key); - if (typeface != null) return typeface; - - FontFamily fontFamily = new FontFamily(); - // TODO: introduce ttc index and variation settings to resource type font. - if (fontFamily.addFontFromAssetManager(mgr, path, cookie, false /* isAsset */, - 0 /* ttcIndex */, RESOLVE_BY_FONT_TABLE /* weight */, - RESOLVE_BY_FONT_TABLE /* italic */, null /* axes */)) { - if (!fontFamily.freeze()) { - return null; - } - FontFamily[] families = {fontFamily}; - typeface = createFromFamiliesWithDefault(families, - RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE); - sDynamicTypefaceCache.put(key, typeface); - return typeface; + synchronized (sDynamicCacheLock) { + final String key = Builder.createAssetUid( + mgr, path, 0 /* ttcIndex */, null /* axes */, + RESOLVE_BY_FONT_TABLE /* weight */, RESOLVE_BY_FONT_TABLE /* italic */, + DEFAULT_FAMILY); + Typeface typeface = sDynamicTypefaceCache.get(key); + if (typeface != null) return typeface; + + FontFamily fontFamily = new FontFamily(); + // TODO: introduce ttc index and variation settings to resource type font. + if (fontFamily.addFontFromAssetManager(mgr, path, cookie, false /* isAsset */, + 0 /* ttcIndex */, RESOLVE_BY_FONT_TABLE /* weight */, + RESOLVE_BY_FONT_TABLE /* italic */, null /* axes */)) { + if (!fontFamily.freeze()) { + return null; } + FontFamily[] families = {fontFamily}; + typeface = createFromFamiliesWithDefault(families, DEFAULT_FAMILY, + RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE); + sDynamicTypefaceCache.put(key, typeface); + return typeface; } } return null; @@ -197,61 +206,57 @@ public class Typeface { @Nullable public static Typeface createFromResources( FamilyResourceEntry entry, AssetManager mgr, String path) { - if (sFallbackFonts != null) { - if (entry instanceof ProviderResourceEntry) { - final ProviderResourceEntry providerEntry = (ProviderResourceEntry) entry; - // Downloadable font - List<List<String>> givenCerts = providerEntry.getCerts(); - List<List<byte[]>> certs = new ArrayList<>(); - if (givenCerts != null) { - for (int i = 0; i < givenCerts.size(); i++) { - List<String> certSet = givenCerts.get(i); - List<byte[]> byteArraySet = new ArrayList<>(); - for (int j = 0; j < certSet.size(); j++) { - byteArraySet.add(Base64.decode(certSet.get(j), Base64.DEFAULT)); - } - certs.add(byteArraySet); + if (entry instanceof ProviderResourceEntry) { + final ProviderResourceEntry providerEntry = (ProviderResourceEntry) entry; + // Downloadable font + List<List<String>> givenCerts = providerEntry.getCerts(); + List<List<byte[]>> certs = new ArrayList<>(); + if (givenCerts != null) { + for (int i = 0; i < givenCerts.size(); i++) { + List<String> certSet = givenCerts.get(i); + List<byte[]> byteArraySet = new ArrayList<>(); + for (int j = 0; j < certSet.size(); j++) { + byteArraySet.add(Base64.decode(certSet.get(j), Base64.DEFAULT)); } + certs.add(byteArraySet); } - // Downloaded font and it wasn't cached, request it again and return a - // default font instead (nothing we can do now). - FontRequest request = new FontRequest(providerEntry.getAuthority(), - providerEntry.getPackage(), providerEntry.getQuery(), certs); - Typeface typeface = FontsContract.getFontSync(request); - return typeface == null ? DEFAULT : typeface; } + // Downloaded font and it wasn't cached, request it again and return a + // default font instead (nothing we can do now). + FontRequest request = new FontRequest(providerEntry.getAuthority(), + providerEntry.getPackage(), providerEntry.getQuery(), certs); + Typeface typeface = FontsContract.getFontSync(request); + return typeface == null ? DEFAULT : typeface; + } - Typeface typeface = findFromCache(mgr, path); - if (typeface != null) return typeface; + Typeface typeface = findFromCache(mgr, path); + if (typeface != null) return typeface; - // family is FontFamilyFilesResourceEntry - final FontFamilyFilesResourceEntry filesEntry = - (FontFamilyFilesResourceEntry) entry; + // family is FontFamilyFilesResourceEntry + final FontFamilyFilesResourceEntry filesEntry = (FontFamilyFilesResourceEntry) entry; - FontFamily fontFamily = new FontFamily(); - for (final FontFileResourceEntry fontFile : filesEntry.getEntries()) { - // TODO: Add ttc and variation font support. (b/37853920) - if (!fontFamily.addFontFromAssetManager(mgr, fontFile.getFileName(), - 0 /* resourceCookie */, false /* isAsset */, 0 /* ttcIndex */, - fontFile.getWeight(), fontFile.getItalic(), null /* axes */)) { - return null; - } - } - if (!fontFamily.freeze()) { + FontFamily fontFamily = new FontFamily(); + for (final FontFileResourceEntry fontFile : filesEntry.getEntries()) { + if (!fontFamily.addFontFromAssetManager(mgr, fontFile.getFileName(), + 0 /* resourceCookie */, false /* isAsset */, fontFile.getTtcIndex(), + fontFile.getWeight(), fontFile.getItalic(), + FontVariationAxis.fromFontVariationSettings(fontFile.getVariationSettings()))) { return null; } - FontFamily[] familyChain = { fontFamily }; - typeface = createFromFamiliesWithDefault(familyChain, - RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE); - synchronized (sDynamicTypefaceCache) { - final String key = Builder.createAssetUid(mgr, path, 0 /* ttcIndex */, - null /* axes */, RESOLVE_BY_FONT_TABLE /* weight */, - RESOLVE_BY_FONT_TABLE /* italic */); - sDynamicTypefaceCache.put(key, typeface); - } - return typeface; } - return null; + if (!fontFamily.freeze()) { + return null; + } + FontFamily[] familyChain = { fontFamily }; + typeface = createFromFamiliesWithDefault(familyChain, DEFAULT_FAMILY, + RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE); + synchronized (sDynamicCacheLock) { + final String key = Builder.createAssetUid(mgr, path, 0 /* ttcIndex */, + null /* axes */, RESOLVE_BY_FONT_TABLE /* weight */, + RESOLVE_BY_FONT_TABLE /* italic */, DEFAULT_FAMILY); + sDynamicTypefaceCache.put(key, typeface); + } + return typeface; } /** @@ -259,9 +264,10 @@ public class Typeface { * @hide */ public static Typeface findFromCache(AssetManager mgr, String path) { - synchronized (sDynamicTypefaceCache) { + synchronized (sDynamicCacheLock) { final String key = Builder.createAssetUid(mgr, path, 0 /* ttcIndex */, null /* axes */, - RESOLVE_BY_FONT_TABLE /* weight */, RESOLVE_BY_FONT_TABLE /* italic */); + RESOLVE_BY_FONT_TABLE /* weight */, RESOLVE_BY_FONT_TABLE /* italic */, + DEFAULT_FAMILY); Typeface typeface = sDynamicTypefaceCache.get(key); if (typeface != null) { return typeface; @@ -375,7 +381,7 @@ public class Typeface { * weight and italic information, so {@link #setWeight} and {@link #setItalic} are used * for style matching during font selection. * - * @param results The array of {@link FontsContract.FontInfo} + * @param fonts The array of {@link FontsContract.FontInfo} * @param buffers The mapping from URI to buffers to be used during building. * @hide */ @@ -410,7 +416,7 @@ public class Typeface { } /** - * Sets an index of the font collection. + * Sets an index of the font collection. See {@link android.R.attr#ttcIndex}. * * Can not be used for Typeface source. build() method will return null for invalid index. * @param ttcIndex An index of the font collection. If the font source is not font @@ -498,7 +504,7 @@ public class Typeface { * @return Unique id for a given AssetManager and asset path. */ private static String createAssetUid(final AssetManager mgr, String path, int ttcIndex, - @Nullable FontVariationAxis[] axes, int weight, int italic) { + @Nullable FontVariationAxis[] axes, int weight, int italic, String fallback) { final SparseArray<String> pkgs = mgr.getAssignedPackageIdentifiers(); final StringBuilder builder = new StringBuilder(); final int size = pkgs.size(); @@ -513,7 +519,11 @@ public class Typeface { builder.append(Integer.toString(weight)); builder.append("-"); builder.append(Integer.toString(italic)); - builder.append("-"); + // Family name may contain hyphen. Use double hyphen for avoiding key conflicts before + // and after appending falblack name. + builder.append("--"); + builder.append(fallback); + builder.append("--"); if (axes != null) { for (FontVariationAxis axis : axes) { builder.append(axis.getTag()); @@ -524,12 +534,6 @@ public class Typeface { return builder.toString(); } - private static final Object sLock = new Object(); - // TODO: Unify with Typeface.sTypefaceCache. - @GuardedBy("sLock") - private static final LongSparseArray<SparseArray<Typeface>> sTypefaceCache = - new LongSparseArray<>(3); - private Typeface resolveFallbackTypeface() { if (mFallbackFamilyName == null) { return null; @@ -547,29 +551,7 @@ public class Typeface { final int weight = (mWeight == RESOLVE_BY_FONT_TABLE) ? base.mWeight : mWeight; final boolean italic = (mItalic == RESOLVE_BY_FONT_TABLE) ? (base.mStyle & ITALIC) != 0 : mItalic == 1; - final int key = weight << 1 | (italic ? 1 : 0); - - Typeface typeface; - synchronized(sLock) { - SparseArray<Typeface> innerCache = sTypefaceCache.get(base.native_instance); - if (innerCache != null) { - typeface = innerCache.get(key); - if (typeface != null) { - return typeface; - } - } - - typeface = new Typeface( - nativeCreateFromTypefaceWithExactStyle( - base.native_instance, weight, italic)); - - if (innerCache == null) { - innerCache = new SparseArray<>(4); // [regular, bold] x [upright, italic] - sTypefaceCache.put(base.native_instance, innerCache); - } - innerCache.put(key, typeface); - } - return typeface; + return createWeightStyle(base, weight, italic); } /** @@ -593,14 +575,16 @@ public class Typeface { return resolveFallbackTypeface(); } FontFamily[] families = { fontFamily }; - return createFromFamiliesWithDefault(families, mWeight, mItalic); + return createFromFamiliesWithDefault(families, mFallbackFamilyName, mWeight, + mItalic); } catch (IOException e) { return resolveFallbackTypeface(); } } else if (mAssetManager != null) { // Builder is created with asset manager. final String key = createAssetUid( - mAssetManager, mPath, mTtcIndex, mAxes, mWeight, mItalic); - synchronized (sLock) { + mAssetManager, mPath, mTtcIndex, mAxes, mWeight, mItalic, + mFallbackFamilyName); + synchronized (sDynamicCacheLock) { Typeface typeface = sDynamicTypefaceCache.get(key); if (typeface != null) return typeface; final FontFamily fontFamily = new FontFamily(); @@ -613,7 +597,8 @@ public class Typeface { return resolveFallbackTypeface(); } FontFamily[] families = { fontFamily }; - typeface = createFromFamiliesWithDefault(families, mWeight, mItalic); + typeface = createFromFamiliesWithDefault(families, mFallbackFamilyName, + mWeight, mItalic); sDynamicTypefaceCache.put(key, typeface); return typeface; } @@ -627,7 +612,8 @@ public class Typeface { return resolveFallbackTypeface(); } FontFamily[] families = { fontFamily }; - return createFromFamiliesWithDefault(families, mWeight, mItalic); + return createFromFamiliesWithDefault(families, mFallbackFamilyName, mWeight, + mItalic); } else if (mFonts != null) { final FontFamily fontFamily = new FontFamily(); boolean atLeastOneFont = false; @@ -653,7 +639,8 @@ public class Typeface { } fontFamily.freeze(); FontFamily[] families = { fontFamily }; - return createFromFamiliesWithDefault(families, mWeight, mItalic); + return createFromFamiliesWithDefault(families, mFallbackFamilyName, mWeight, + mItalic); } // Must not reach here. @@ -673,10 +660,7 @@ public class Typeface { * @return The best matching typeface. */ public static Typeface create(String familyName, int style) { - if (sSystemFontMap != null) { - return create(sSystemFontMap.get(familyName), style); - } - return null; + return create(sSystemFontMap.get(familyName), style); } /** @@ -685,42 +669,98 @@ public class Typeface { * style from the same family of an existing typeface object. If family is * null, this selects from the default font's family. * - * @param family May be null. The name of the existing type face. + * <p> + * This method is not thread safe on API 27 or before. + * This method is thread safe on API 28 or after. + * </p> + * + * @param family An existing {@link Typeface} object. In case of {@code null}, the default + * typeface is used instead. * @param style The style (normal, bold, italic) of the typeface. * e.g. NORMAL, BOLD, ITALIC, BOLD_ITALIC * @return The best matching typeface. */ public static Typeface create(Typeface family, int style) { - if (style < 0 || style > 3) { - style = 0; + if ((style & ~STYLE_MASK) != 0) { + style = NORMAL; + } + if (family == null) { + family = sDefaultTypeface; } - long ni = 0; - if (family != null) { - // Return early if we're asked for the same face/style - if (family.mStyle == style) { - return family; - } - ni = family.native_instance; + // Return early if we're asked for the same face/style + if (family.mStyle == style) { + return family; } + final long ni = family.native_instance; + Typeface typeface; - SparseArray<Typeface> styles = sTypefaceCache.get(ni); + synchronized (sStyledCacheLock) { + SparseArray<Typeface> styles = sStyledTypefaceCache.get(ni); - if (styles != null) { - typeface = styles.get(style); - if (typeface != null) { - return typeface; + if (styles == null) { + styles = new SparseArray<Typeface>(4); + sStyledTypefaceCache.put(ni, styles); + } else { + typeface = styles.get(style); + if (typeface != null) { + return typeface; + } } + + typeface = new Typeface(nativeCreateFromTypeface(ni, style)); + styles.put(style, typeface); } + return typeface; + } - typeface = new Typeface(nativeCreateFromTypeface(ni, style)); - if (styles == null) { - styles = new SparseArray<Typeface>(4); - sTypefaceCache.put(ni, styles); + /** + * Creates a typeface object that best matches the specified existing typeface and the specified + * weight and italic style + * + * <p> + * This method is thread safe. + * </p> + * + * @param family An existing {@link Typeface} object. In case of {@code null}, the default + * typeface is used instead. + * @param weight The desired weight to be drawn. + * @param italic {@code true} if italic style is desired to be drawn. Otherwise, {@code false} + * @return A {@link Typeface} object for drawing specified weight and italic style. Never + * returns {@code null} + */ + public static @NonNull Typeface create(@Nullable Typeface family, + @IntRange(from = 1, to = 1000) int weight, boolean italic) { + Preconditions.checkArgumentInRange(weight, 0, 1000, "weight"); + if (family == null) { + family = sDefaultTypeface; } - styles.put(style, typeface); + return createWeightStyle(family, weight, italic); + } + + private static @NonNull Typeface createWeightStyle(@NonNull Typeface base, + @IntRange(from = 1, to = 1000) int weight, boolean italic) { + final int key = (weight << 1) | (italic ? 1 : 0); + + Typeface typeface; + synchronized(sWeightCacheLock) { + SparseArray<Typeface> innerCache = sWeightTypefaceCache.get(base.native_instance); + if (innerCache == null) { + innerCache = new SparseArray<>(4); + sWeightTypefaceCache.put(base.native_instance, innerCache); + } else { + typeface = innerCache.get(key); + if (typeface != null) { + return typeface; + } + } + typeface = new Typeface( + nativeCreateFromTypefaceWithExactStyle( + base.native_instance, weight, italic)); + innerCache.put(key, typeface); + } return typeface; } @@ -748,40 +788,18 @@ public class Typeface { * @return The new typeface. */ public static Typeface createFromAsset(AssetManager mgr, String path) { - if (path == null) { - throw new NullPointerException(); // for backward compatibility - } - if (sFallbackFonts != null) { - synchronized (sLock) { - Typeface typeface = new Builder(mgr, path).build(); - if (typeface != null) return typeface; - - final String key = Builder.createAssetUid(mgr, path, 0 /* ttcIndex */, - null /* axes */, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE); - typeface = sDynamicTypefaceCache.get(key); - if (typeface != null) return typeface; + Preconditions.checkNotNull(path); // for backward compatibility + Preconditions.checkNotNull(mgr); - final FontFamily fontFamily = new FontFamily(); - if (fontFamily.addFontFromAssetManager(mgr, path, 0, true /* isAsset */, - 0 /* ttc index */, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, - null /* axes */)) { - // Due to backward compatibility, even if the font is not supported by our font - // stack, we need to place the empty font at the first place. The typeface with - // empty font behaves different from default typeface especially in fallback - // font selection. - fontFamily.allowUnsupportedFont(); - fontFamily.freeze(); - final FontFamily[] families = { fontFamily }; - typeface = createFromFamiliesWithDefault(families, RESOLVE_BY_FONT_TABLE, - RESOLVE_BY_FONT_TABLE); - sDynamicTypefaceCache.put(key, typeface); - return typeface; - } else { - fontFamily.abortCreation(); - } - } + Typeface typeface = new Builder(mgr, path).build(); + if (typeface != null) return typeface; + // check if the file exists, and throw an exception for backward compatibility + try (InputStream inputStream = mgr.open(path)) { + } catch (IOException e) { + throw new RuntimeException("Font asset not found " + path); } - throw new RuntimeException("Font asset not found " + path); + + return Typeface.DEFAULT; } /** @@ -799,13 +817,22 @@ public class Typeface { /** * Create a new typeface from the specified font file. * - * @param path The path to the font data. + * @param file The path to the font data. * @return The new typeface. */ - public static Typeface createFromFile(@Nullable File path) { + public static Typeface createFromFile(@Nullable File file) { // For the compatibility reasons, leaving possible NPE here. // See android.graphics.cts.TypefaceTest#testCreateFromFileByFileReferenceNull - return createFromFile(path.getAbsolutePath()); + + Typeface typeface = new Builder(file).build(); + if (typeface != null) return typeface; + + // check if the file exists, and throw an exception for backward compatibility + if (!file.exists()) { + throw new RuntimeException("Font asset not found " + file.getAbsolutePath()); + } + + return Typeface.DEFAULT; } /** @@ -815,24 +842,8 @@ public class Typeface { * @return The new typeface. */ public static Typeface createFromFile(@Nullable String path) { - if (sFallbackFonts != null) { - final FontFamily fontFamily = new FontFamily(); - if (fontFamily.addFont(path, 0 /* ttcIndex */, null /* axes */, - RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)) { - // Due to backward compatibility, even if the font is not supported by our font - // stack, we need to place the empty font at the first place. The typeface with - // empty font behaves different from default typeface especially in fallback font - // selection. - fontFamily.allowUnsupportedFont(); - fontFamily.freeze(); - FontFamily[] families = { fontFamily }; - return createFromFamiliesWithDefault(families, RESOLVE_BY_FONT_TABLE, - RESOLVE_BY_FONT_TABLE); - } else { - fontFamily.abortCreation(); - } - } - throw new RuntimeException("Font not found " + path); + Preconditions.checkNotNull(path); // for backward compatibility + return createFromFile(new File(path)); } /** @@ -852,24 +863,28 @@ public class Typeface { /** * Create a new typeface from an array of font families, including * also the font families in the fallback list. - * @param weight the weight for this family. {@link RESOLVE_BY_FONT_TABLE} can be used. In that - * case, the table information in the first family's font is used. If the first - * family has multiple fonts, the closest to the regular weight and upright font - * is used. - * @param italic the italic information for this family. {@link RESOLVE_BY_FONT_TABLE} can be - * used. In that case, the table information in the first family's font is used. - * If the first family has multiple fonts, the closest to the regular weight and - * upright font is used. + * @param fallbackName the family name. If given families don't support characters, the + * characters will be rendered with this family. + * @param weight the weight for this family. In that case, the table information in the first + * family's font is used. If the first family has multiple fonts, the closest to + * the regular weight and upright font is used. + * @param italic the italic information for this family. In that case, the table information in + * the first family's font is used. If the first family has multiple fonts, the + * closest to the regular weight and upright font is used. * @param families array of font families */ private static Typeface createFromFamiliesWithDefault(FontFamily[] families, - int weight, int italic) { - long[] ptrArray = new long[families.length + sFallbackFonts.length]; + String fallbackName, int weight, int italic) { + FontFamily[] fallback = sSystemFallbackMap.get(fallbackName); + if (fallback == null) { + fallback = sSystemFallbackMap.get(DEFAULT_FAMILY); + } + long[] ptrArray = new long[families.length + fallback.length]; for (int i = 0; i < families.length; i++) { ptrArray[i] = families[i].mNativePtr; } - for (int i = 0; i < sFallbackFonts.length; i++) { - ptrArray[i + families.length] = sFallbackFonts[i].mNativePtr; + for (int i = 0; i < fallback.length; i++) { + ptrArray[i + families.length] = fallback[i].mNativePtr; } return new Typeface(nativeCreateFromArray(ptrArray, weight, italic)); } @@ -885,113 +900,194 @@ public class Typeface { mWeight = nativeGetWeight(ni); } - private static FontFamily makeFamilyFromParsed(FontConfig.Family family, - Map<String, ByteBuffer> bufferForPath) { - FontFamily fontFamily = new FontFamily(family.getLanguage(), family.getVariant()); - for (FontConfig.Font font : family.getFonts()) { - String fullPathName = "/system/fonts/" + font.getFontName(); - ByteBuffer fontBuffer = bufferForPath.get(fullPathName); - if (fontBuffer == null) { - try (FileInputStream file = new FileInputStream(fullPathName)) { - FileChannel fileChannel = file.getChannel(); - long fontSize = fileChannel.size(); - fontBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize); - bufferForPath.put(fullPathName, fontBuffer); - } catch (IOException e) { - Log.e(TAG, "Error mapping font file " + fullPathName); + private static @Nullable ByteBuffer mmap(String fullPath) { + try (FileInputStream file = new FileInputStream(fullPath)) { + final FileChannel fileChannel = file.getChannel(); + final long fontSize = fileChannel.size(); + return fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize); + } catch (IOException e) { + Log.e(TAG, "Error mapping font file " + fullPath); + return null; + } + } + + private static @Nullable FontFamily createFontFamily( + String familyName, List<FontConfig.Font> fonts, String[] languageTags, int variant, + Map<String, ByteBuffer> cache, String fontDir) { + final FontFamily family = new FontFamily(languageTags, variant); + for (int i = 0; i < fonts.size(); i++) { + final FontConfig.Font font = fonts.get(i); + final String fullPath = fontDir + font.getFontName(); + ByteBuffer buffer = cache.get(fullPath); + if (buffer == null) { + if (cache.containsKey(fullPath)) { + continue; // Already failed to mmap. Skip it. + } + buffer = mmap(fullPath); + cache.put(fullPath, buffer); + if (buffer == null) { continue; } } - if (!fontFamily.addFontFromBuffer(fontBuffer, font.getTtcIndex(), font.getAxes(), + if (!family.addFontFromBuffer(buffer, font.getTtcIndex(), font.getAxes(), font.getWeight(), font.isItalic() ? STYLE_ITALIC : STYLE_NORMAL)) { - Log.e(TAG, "Error creating font " + fullPathName + "#" + font.getTtcIndex()); + Log.e(TAG, "Error creating font " + fullPath + "#" + font.getTtcIndex()); } } - if (!fontFamily.freeze()) { - // Treat as system error since reaching here means that a system pre-installed font - // can't be used by our font stack. - Log.e(TAG, "Unable to load Family: " + family.getName() + ":" + family.getLanguage()); + if (!family.freeze()) { + Log.e(TAG, "Unable to load Family: " + familyName + " : " + + Arrays.toString(languageTags)); return null; } - return fontFamily; + return family; } - /* - * (non-Javadoc) + private static void pushFamilyToFallback(FontConfig.Family xmlFamily, + ArrayMap<String, ArrayList<FontFamily>> fallbackMap, + Map<String, ByteBuffer> cache, + String fontDir) { + + final String[] languageTags = xmlFamily.getLanguages(); + final int variant = xmlFamily.getVariant(); + + final ArrayList<FontConfig.Font> defaultFonts = new ArrayList<>(); + final ArrayMap<String, ArrayList<FontConfig.Font>> specificFallbackFonts = new ArrayMap<>(); + + // Collect default fallback and specific fallback fonts. + for (final FontConfig.Font font : xmlFamily.getFonts()) { + final String fallbackName = font.getFallbackFor(); + if (fallbackName == null) { + defaultFonts.add(font); + } else { + ArrayList<FontConfig.Font> fallback = specificFallbackFonts.get(fallbackName); + if (fallback == null) { + fallback = new ArrayList<>(); + specificFallbackFonts.put(fallbackName, fallback); + } + fallback.add(font); + } + } + + final FontFamily defaultFamily = defaultFonts.isEmpty() ? null : createFontFamily( + xmlFamily.getName(), defaultFonts, languageTags, variant, cache, fontDir); + + // Insert family into fallback map. + for (int i = 0; i < fallbackMap.size(); i++) { + final ArrayList<FontConfig.Font> fallback = + specificFallbackFonts.get(fallbackMap.keyAt(i)); + if (fallback == null) { + if (defaultFamily != null) { + fallbackMap.valueAt(i).add(defaultFamily); + } + } else { + final FontFamily family = createFontFamily( + xmlFamily.getName(), fallback, languageTags, variant, cache, fontDir); + if (family != null) { + fallbackMap.valueAt(i).add(family); + } else if (defaultFamily != null) { + fallbackMap.valueAt(i).add(defaultFamily); + } else { + // There is no valid for for default fallback. Ignore. + } + } + } + } + + /** + * Build the system fallback from xml file. * - * This should only be called once, from the static class initializer block. + * @param xmlPath A full path string to the fonts.xml file. + * @param fontDir A full path string to the system font directory. This must end with + * slash('/'). + * @param fontMap An output system font map. Caller must pass empty map. + * @param fallbackMap An output system fallback map. Caller must pass empty map. + * @hide */ - private static void init() { - // Load font config and initialize Minikin state - File systemFontConfigLocation = getSystemFontConfigLocation(); - File configFilename = new File(systemFontConfigLocation, FONTS_CONFIG); + @VisibleForTesting + public static void buildSystemFallback(String xmlPath, String fontDir, + ArrayMap<String, Typeface> fontMap, ArrayMap<String, FontFamily[]> fallbackMap) { try { - FileInputStream fontsIn = new FileInputStream(configFilename); - FontConfig fontConfig = FontListParser.parse(fontsIn); - - Map<String, ByteBuffer> bufferForPath = new HashMap<String, ByteBuffer>(); - - List<FontFamily> familyList = new ArrayList<FontFamily>(); - // Note that the default typeface is always present in the fallback list; - // this is an enhancement from pre-Minikin behavior. - for (int i = 0; i < fontConfig.getFamilies().length; i++) { - FontConfig.Family f = fontConfig.getFamilies()[i]; - if (i == 0 || f.getName() == null) { - FontFamily family = makeFamilyFromParsed(f, bufferForPath); - if (family != null) { - familyList.add(family); - } + final FileInputStream fontsIn = new FileInputStream(xmlPath); + final FontConfig fontConfig = FontListParser.parse(fontsIn); + + final HashMap<String, ByteBuffer> bufferCache = new HashMap<String, ByteBuffer>(); + final FontConfig.Family[] xmlFamilies = fontConfig.getFamilies(); + + final ArrayMap<String, ArrayList<FontFamily>> fallbackListMap = new ArrayMap<>(); + // First traverse families which have a 'name' attribute to create fallback map. + for (final FontConfig.Family xmlFamily : xmlFamilies) { + final String familyName = xmlFamily.getName(); + if (familyName == null) { + continue; } + final FontFamily family = createFontFamily( + xmlFamily.getName(), Arrays.asList(xmlFamily.getFonts()), + xmlFamily.getLanguages(), xmlFamily.getVariant(), bufferCache, fontDir); + if (family == null) { + continue; + } + final ArrayList<FontFamily> fallback = new ArrayList<>(); + fallback.add(family); + fallbackListMap.put(familyName, fallback); } - sFallbackFonts = familyList.toArray(new FontFamily[familyList.size()]); - setDefault(Typeface.createFromFamilies(sFallbackFonts)); - - Map<String, Typeface> systemFonts = new HashMap<String, Typeface>(); - for (int i = 0; i < fontConfig.getFamilies().length; i++) { - Typeface typeface; - FontConfig.Family f = fontConfig.getFamilies()[i]; - if (f.getName() != null) { - if (i == 0) { - // The first entry is the default typeface; no sense in - // duplicating the corresponding FontFamily. - typeface = sDefaultTypeface; - } else { - FontFamily fontFamily = makeFamilyFromParsed(f, bufferForPath); - if (fontFamily == null) { - continue; - } - FontFamily[] families = { fontFamily }; - typeface = Typeface.createFromFamiliesWithDefault(families, - RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE); - } - systemFonts.put(f.getName(), typeface); + + // Then, add fallback fonts to the each fallback map. + for (int i = 0; i < xmlFamilies.length; i++) { + final FontConfig.Family xmlFamily = xmlFamilies[i]; + // The first family (usually the sans-serif family) is always placed immediately + // after the primary family in the fallback. + if (i == 0 || xmlFamily.getName() == null) { + pushFamilyToFallback(xmlFamily, fallbackListMap, bufferCache, fontDir); } } - for (FontConfig.Alias alias : fontConfig.getAliases()) { - Typeface base = systemFonts.get(alias.getToName()); + + // Build the font map and fallback map. + for (int i = 0; i < fallbackListMap.size(); i++) { + final String fallbackName = fallbackListMap.keyAt(i); + final List<FontFamily> familyList = fallbackListMap.valueAt(i); + final FontFamily[] families = familyList.toArray(new FontFamily[familyList.size()]); + + fallbackMap.put(fallbackName, families); + final long[] ptrArray = new long[families.length]; + for (int j = 0; j < families.length; j++) { + ptrArray[j] = families[j].mNativePtr; + } + fontMap.put(fallbackName, new Typeface(nativeCreateFromArray( + ptrArray, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE))); + } + + // Insert alias to font maps. + for (final FontConfig.Alias alias : fontConfig.getAliases()) { + Typeface base = fontMap.get(alias.getToName()); Typeface newFace = base; int weight = alias.getWeight(); if (weight != 400) { newFace = new Typeface(nativeCreateWeightAlias(base.native_instance, weight)); } - systemFonts.put(alias.getName(), newFace); + fontMap.put(alias.getName(), newFace); } - sSystemFontMap = systemFonts; - } catch (RuntimeException e) { Log.w(TAG, "Didn't create default family (most likely, non-Minikin build)", e); // TODO: normal in non-Minikin case, remove or make error when Minikin-only } catch (FileNotFoundException e) { - Log.e(TAG, "Error opening " + configFilename, e); + Log.e(TAG, "Error opening " + xmlPath, e); } catch (IOException e) { - Log.e(TAG, "Error reading " + configFilename, e); + Log.e(TAG, "Error reading " + xmlPath, e); } catch (XmlPullParserException e) { - Log.e(TAG, "XML parse exception for " + configFilename, e); + Log.e(TAG, "XML parse exception for " + xmlPath, e); } } static { - init(); + final ArrayMap<String, Typeface> systemFontMap = new ArrayMap<>(); + final ArrayMap<String, FontFamily[]> systemFallbackMap = new ArrayMap<>(); + buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/", systemFontMap, + systemFallbackMap); + sSystemFontMap = Collections.unmodifiableMap(systemFontMap); + sSystemFallbackMap = Collections.unmodifiableMap(systemFallbackMap); + + setDefault(sSystemFontMap.get(DEFAULT_FAMILY)); + // Set up defaults and typefaces exposed in public API DEFAULT = create((String) null, 0); DEFAULT_BOLD = create((String) null, Typeface.BOLD); @@ -1008,10 +1104,6 @@ public class Typeface { } - private static File getSystemFontConfigLocation() { - return new File("/system/etc/"); - } - @Override protected void finalize() throws Throwable { try { diff --git a/graphics/java/android/graphics/drawable/AnimatedImageDrawable.java b/graphics/java/android/graphics/drawable/AnimatedImageDrawable.java new file mode 100644 index 000000000000..86e6fa8c2fc5 --- /dev/null +++ b/graphics/java/android/graphics/drawable/AnimatedImageDrawable.java @@ -0,0 +1,470 @@ +/* + * Copyright (C) 2018 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.graphics.drawable; + +import dalvik.annotation.optimization.FastNative; + +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.res.AssetFileDescriptor; +import android.content.res.Resources; +import android.content.res.Resources.Theme; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.InflateException; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.ImageDecoder; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Looper; +import android.util.DisplayMetrics; +import android.util.TypedValue; + +import com.android.internal.R; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import libcore.io.IoUtils; +import libcore.util.NativeAllocationRegistry; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.Runnable; +import java.util.ArrayList; + +/** + * {@link Drawable} for drawing animated images (like GIF). + * + * <p>Created by {@link ImageDecoder#decodeDrawable}. A user needs to call + * {@link #start} to start the animation.</p> + */ +public class AnimatedImageDrawable extends Drawable implements Animatable2 { + private int mIntrinsicWidth; + private int mIntrinsicHeight; + + private boolean mStarting; + + private Handler mHandler; + + private class State { + State(long nativePtr, InputStream is, AssetFileDescriptor afd) { + mNativePtr = nativePtr; + mInputStream = is; + mAssetFd = afd; + } + + public final long mNativePtr; + + // These just keep references so the native code can continue using them. + private final InputStream mInputStream; + private final AssetFileDescriptor mAssetFd; + } + + private State mState; + + private Runnable mRunnable; + + /** + * Pass this to {@link #setLoopCount} to loop infinitely. + * + * <p>{@link Animatable2.AnimationCallback#onAnimationEnd} will never be + * called unless there is an error.</p> + */ + public static final int LOOP_INFINITE = -1; + + /** + * Specify the number of times to loop the animation. + * + * <p>By default, the loop count in the encoded data is respected.</p> + */ + public void setLoopCount(int loopCount) { + if (mState == null) { + throw new IllegalStateException("called setLoopCount on empty AnimatedImageDrawable"); + } + nSetLoopCount(mState.mNativePtr, loopCount); + } + + /** + * Create an empty AnimatedImageDrawable. + */ + public AnimatedImageDrawable() { + mState = null; + } + + @Override + public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) + throws XmlPullParserException, IOException { + super.inflate(r, parser, attrs, theme); + + final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimatedImageDrawable); + updateStateFromTypedArray(a, mSrcDensityOverride); + } + + private void updateStateFromTypedArray(TypedArray a, int srcDensityOverride) + throws XmlPullParserException { + final Resources r = a.getResources(); + final int srcResId = a.getResourceId(R.styleable.AnimatedImageDrawable_src, 0); + if (srcResId != 0) { + // Follow the density handling in BitmapDrawable. + final TypedValue value = new TypedValue(); + r.getValueForDensity(srcResId, srcDensityOverride, value, true); + if (srcDensityOverride > 0 && value.density > 0 + && value.density != TypedValue.DENSITY_NONE) { + if (value.density == srcDensityOverride) { + value.density = r.getDisplayMetrics().densityDpi; + } else { + value.density = + (value.density * r.getDisplayMetrics().densityDpi) / srcDensityOverride; + } + } + + int density = Bitmap.DENSITY_NONE; + if (value.density == TypedValue.DENSITY_DEFAULT) { + density = DisplayMetrics.DENSITY_DEFAULT; + } else if (value.density != TypedValue.DENSITY_NONE) { + density = value.density; + } + + Drawable drawable = null; + try { + InputStream is = r.openRawResource(srcResId, value); + ImageDecoder.Source source = ImageDecoder.createSource(r, is, density); + drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -> { + if (!info.isAnimated()) { + throw new IllegalArgumentException("image is not animated"); + } + }); + } catch (IOException e) { + throw new XmlPullParserException(a.getPositionDescription() + + ": <animated-image> requires a valid 'src' attribute", null, e); + } + + if (!(drawable instanceof AnimatedImageDrawable)) { + throw new XmlPullParserException(a.getPositionDescription() + + ": <animated-image> did not decode animated"); + } + + // Transfer the state of other to this one. other will be discarded. + AnimatedImageDrawable other = (AnimatedImageDrawable) drawable; + mState = other.mState; + other.mState = null; + mIntrinsicWidth = other.mIntrinsicWidth; + mIntrinsicHeight = other.mIntrinsicHeight; + } + } + + /** + * @hide + * This should only be called by ImageDecoder. + * + * decoder is only non-null if it has a PostProcess + */ + public AnimatedImageDrawable(long nativeImageDecoder, + @Nullable ImageDecoder decoder, int width, int height, + int srcDensity, int dstDensity, Rect cropRect, + InputStream inputStream, AssetFileDescriptor afd) + throws IOException { + width = Bitmap.scaleFromDensity(width, srcDensity, dstDensity); + height = Bitmap.scaleFromDensity(height, srcDensity, dstDensity); + + if (cropRect == null) { + mIntrinsicWidth = width; + mIntrinsicHeight = height; + } else { + cropRect.set(Bitmap.scaleFromDensity(cropRect.left, srcDensity, dstDensity), + Bitmap.scaleFromDensity(cropRect.top, srcDensity, dstDensity), + Bitmap.scaleFromDensity(cropRect.right, srcDensity, dstDensity), + Bitmap.scaleFromDensity(cropRect.bottom, srcDensity, dstDensity)); + mIntrinsicWidth = cropRect.width(); + mIntrinsicHeight = cropRect.height(); + } + + mState = new State(nCreate(nativeImageDecoder, decoder, width, height, cropRect), + inputStream, afd); + + // FIXME: Use the right size for the native allocation. + long nativeSize = 200; + NativeAllocationRegistry registry = new NativeAllocationRegistry( + AnimatedImageDrawable.class.getClassLoader(), nGetNativeFinalizer(), nativeSize); + registry.registerNativeAllocation(mState, mState.mNativePtr); + } + + @Override + public int getIntrinsicWidth() { + return mIntrinsicWidth; + } + + @Override + public int getIntrinsicHeight() { + return mIntrinsicHeight; + } + + // nDraw returns -1 if the animation has finished. + private static final int FINISHED = -1; + + @Override + public void draw(@NonNull Canvas canvas) { + if (mState == null) { + throw new IllegalStateException("called draw on empty AnimatedImageDrawable"); + } + + if (mStarting) { + mStarting = false; + + postOnAnimationStart(); + } + + long nextUpdate = nDraw(mState.mNativePtr, canvas.getNativeCanvasWrapper()); + // a value <= 0 indicates that the drawable is stopped or that renderThread + // will manage the animation + if (nextUpdate > 0) { + if (mRunnable == null) { + mRunnable = this::invalidateSelf; + } + scheduleSelf(mRunnable, nextUpdate); + } else if (nextUpdate == FINISHED) { + // This means the animation was drawn in software mode and ended. + postOnAnimationEnd(); + } + } + + @Override + public void setAlpha(@IntRange(from=0,to=255) int alpha) { + if (alpha < 0 || alpha > 255) { + throw new IllegalArgumentException("Alpha must be between 0 and" + + " 255! provided " + alpha); + } + + if (mState == null) { + throw new IllegalStateException("called setAlpha on empty AnimatedImageDrawable"); + } + + nSetAlpha(mState.mNativePtr, alpha); + invalidateSelf(); + } + + @Override + public int getAlpha() { + if (mState == null) { + throw new IllegalStateException("called getAlpha on empty AnimatedImageDrawable"); + } + return nGetAlpha(mState.mNativePtr); + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + if (mState == null) { + throw new IllegalStateException("called setColorFilter on empty AnimatedImageDrawable"); + } + + long nativeFilter = colorFilter == null ? 0 : colorFilter.getNativeInstance(); + nSetColorFilter(mState.mNativePtr, nativeFilter); + invalidateSelf(); + } + + @Override + public @PixelFormat.Opacity int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + if (!super.setVisible(visible, restart)) { + return false; + } + + if (!visible) { + nMarkInvisible(mState.mNativePtr); + } + + return true; + } + + // Animatable overrides + /** + * Return whether the animation is currently running. + * + * <p>When this drawable is created, this will return {@code false}. A client + * needs to call {@link #start} to start the animation.</p> + */ + @Override + public boolean isRunning() { + if (mState == null) { + throw new IllegalStateException("called isRunning on empty AnimatedImageDrawable"); + } + return nIsRunning(mState.mNativePtr); + } + + /** + * Start the animation. + * + * <p>Does nothing if the animation is already running. If the animation is stopped, + * this will reset it.</p> + * + * <p>If the animation starts, this will call + * {@link Animatable2.AnimationCallback#onAnimationStart}.</p> + */ + @Override + public void start() { + if (mState == null) { + throw new IllegalStateException("called start on empty AnimatedImageDrawable"); + } + + if (nStart(mState.mNativePtr)) { + mStarting = true; + invalidateSelf(); + } + } + + /** + * Stop the animation. + * + * <p>If the animation is stopped, it will continue to display the frame + * it was displaying when stopped.</p> + */ + @Override + public void stop() { + if (mState == null) { + throw new IllegalStateException("called stop on empty AnimatedImageDrawable"); + } + if (nStop(mState.mNativePtr)) { + postOnAnimationEnd(); + } + } + + // Animatable2 overrides + private ArrayList<Animatable2.AnimationCallback> mAnimationCallbacks = null; + + @Override + public void registerAnimationCallback(@NonNull AnimationCallback callback) { + if (callback == null) { + return; + } + + if (mAnimationCallbacks == null) { + mAnimationCallbacks = new ArrayList<Animatable2.AnimationCallback>(); + nSetOnAnimationEndListener(mState.mNativePtr, this); + } + + if (!mAnimationCallbacks.contains(callback)) { + mAnimationCallbacks.add(callback); + } + } + + @Override + public boolean unregisterAnimationCallback(@NonNull AnimationCallback callback) { + if (callback == null || mAnimationCallbacks == null + || !mAnimationCallbacks.remove(callback)) { + return false; + } + + if (mAnimationCallbacks.isEmpty()) { + clearAnimationCallbacks(); + } + + return true; + } + + @Override + public void clearAnimationCallbacks() { + if (mAnimationCallbacks != null) { + mAnimationCallbacks = null; + nSetOnAnimationEndListener(mState.mNativePtr, null); + } + } + + private void postOnAnimationStart() { + if (mAnimationCallbacks == null) { + return; + } + + getHandler().post(() -> { + for (Animatable2.AnimationCallback callback : mAnimationCallbacks) { + callback.onAnimationStart(this); + } + }); + } + + private void postOnAnimationEnd() { + if (mAnimationCallbacks == null) { + return; + } + + getHandler().post(() -> { + for (Animatable2.AnimationCallback callback : mAnimationCallbacks) { + callback.onAnimationEnd(this); + } + }); + } + + private Handler getHandler() { + if (mHandler == null) { + mHandler = new Handler(Looper.getMainLooper()); + } + return mHandler; + } + + /** + * Called by JNI. + * + * The JNI code has already posted this to the thread that created the + * callback, so no need to post. + */ + @SuppressWarnings("unused") + private void onAnimationEnd() { + if (mAnimationCallbacks != null) { + for (Animatable2.AnimationCallback callback : mAnimationCallbacks) { + callback.onAnimationEnd(this); + } + } + } + + + private static native long nCreate(long nativeImageDecoder, + @Nullable ImageDecoder decoder, int width, int height, Rect cropRect) + throws IOException; + @FastNative + private static native long nGetNativeFinalizer(); + private static native long nDraw(long nativePtr, long canvasNativePtr); + @FastNative + private static native void nSetAlpha(long nativePtr, int alpha); + @FastNative + private static native int nGetAlpha(long nativePtr); + @FastNative + private static native void nSetColorFilter(long nativePtr, long nativeFilter); + @FastNative + private static native boolean nIsRunning(long nativePtr); + // Return whether the animation started. + @FastNative + private static native boolean nStart(long nativePtr); + @FastNative + private static native boolean nStop(long nativePtr); + @FastNative + private static native void nSetLoopCount(long nativePtr, int loopCount); + // Pass the drawable down to native so it can call onAnimationEnd. + private static native void nSetOnAnimationEndListener(long nativePtr, + @Nullable AnimatedImageDrawable drawable); + @FastNative + private static native long nNativeByteSize(long nativePtr); + @FastNative + private static native void nMarkInvisible(long nativePtr); +} diff --git a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java index 90d6ab867fe1..e74dc6dc9671 100644 --- a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java +++ b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java @@ -132,7 +132,7 @@ import dalvik.annotation.optimization.FastNative; * <td>translateY</td> * </tr> * <tr> - * <td rowspan="8"><path></td> + * <td rowspan="9"><path></td> * <td>pathData</td> * </tr> * <tr> @@ -154,6 +154,9 @@ import dalvik.annotation.optimization.FastNative; * <td>trimPathStart</td> * </tr> * <tr> + * <td>trimPathEnd</td> + * </tr> + * <tr> * <td>trimPathOffset</td> * </tr> * <tr> diff --git a/graphics/java/android/graphics/drawable/Drawable.java b/graphics/java/android/graphics/drawable/Drawable.java index f17cd768c386..05533d787aa1 100644 --- a/graphics/java/android/graphics/drawable/Drawable.java +++ b/graphics/java/android/graphics/drawable/Drawable.java @@ -1168,9 +1168,13 @@ public abstract class Drawable { /** * Create a drawable from an inputstream, using the given resources and * value to determine density information. + * + * @deprecated Prefer the version without an Options object. */ - public static Drawable createFromResourceStream(Resources res, TypedValue value, - InputStream is, String srcName, BitmapFactory.Options opts) { + @Nullable + public static Drawable createFromResourceStream(@Nullable Resources res, + @Nullable TypedValue value, @Nullable InputStream is, @Nullable String srcName, + @Nullable BitmapFactory.Options opts) { if (is == null) { return null; } diff --git a/graphics/java/android/graphics/drawable/DrawableInflater.java b/graphics/java/android/graphics/drawable/DrawableInflater.java index eea7048ca534..0ee9071f4d06 100644 --- a/graphics/java/android/graphics/drawable/DrawableInflater.java +++ b/graphics/java/android/graphics/drawable/DrawableInflater.java @@ -185,6 +185,8 @@ public final class DrawableInflater { return new BitmapDrawable(); case "nine-patch": return new NinePatchDrawable(); + case "animated-image": + return new AnimatedImageDrawable(); default: return null; } diff --git a/graphics/java/android/graphics/drawable/GradientDrawable.java b/graphics/java/android/graphics/drawable/GradientDrawable.java index 6c3aea2202a2..8b5114c50581 100644 --- a/graphics/java/android/graphics/drawable/GradientDrawable.java +++ b/graphics/java/android/graphics/drawable/GradientDrawable.java @@ -42,6 +42,7 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.SweepGradient; +import android.graphics.Xfermode; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; @@ -814,6 +815,24 @@ public class GradientDrawable extends Drawable { } } + /** + * @param mode to draw this drawable with + * @hide + */ + @Override + public void setXfermode(@Nullable Xfermode mode) { + super.setXfermode(mode); + mFillPaint.setXfermode(mode); + } + + /** + * @param aa to draw this drawable with + * @hide + */ + public void setAntiAlias(boolean aa) { + mFillPaint.setAntiAlias(aa); + } + private void buildPathIfDirty() { final GradientState st = mGradientState; if (mPathIsDirty) { diff --git a/graphics/java/android/graphics/drawable/Icon.java b/graphics/java/android/graphics/drawable/Icon.java index c329918afc27..749b75941ef9 100644 --- a/graphics/java/android/graphics/drawable/Icon.java +++ b/graphics/java/android/graphics/drawable/Icon.java @@ -819,8 +819,10 @@ public final class Icon implements Parcelable { if (bitmapWidth > maxWidth || bitmapHeight > maxHeight) { float scale = Math.min((float) maxWidth / bitmapWidth, (float) maxHeight / bitmapHeight); - bitmap = Bitmap.createScaledBitmap(bitmap, (int) (scale * bitmapWidth), - (int) (scale * bitmapHeight), true /* filter */); + bitmap = Bitmap.createScaledBitmap(bitmap, + Math.max(1, (int) (scale * bitmapWidth)), + Math.max(1, (int) (scale * bitmapHeight)), + true /* filter */); } return bitmap; } diff --git a/graphics/java/android/graphics/drawable/RippleBackground.java b/graphics/java/android/graphics/drawable/RippleBackground.java index 6bd2646f9299..41d36986dfe2 100644 --- a/graphics/java/android/graphics/drawable/RippleBackground.java +++ b/graphics/java/android/graphics/drawable/RippleBackground.java @@ -16,17 +16,12 @@ package android.graphics.drawable; -import android.animation.Animator; -import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.graphics.Canvas; -import android.graphics.CanvasProperty; import android.graphics.Paint; import android.graphics.Rect; import android.util.FloatProperty; -import android.view.DisplayListCanvas; -import android.view.RenderNodeAnimator; import android.view.animation.LinearInterpolator; /** @@ -36,138 +31,71 @@ class RippleBackground extends RippleComponent { private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); - private static final int OPACITY_ENTER_DURATION = 600; - private static final int OPACITY_ENTER_DURATION_FAST = 120; - private static final int OPACITY_EXIT_DURATION = 480; + private static final int OPACITY_DURATION = 80; - // Hardware rendering properties. - private CanvasProperty<Paint> mPropPaint; - private CanvasProperty<Float> mPropRadius; - private CanvasProperty<Float> mPropX; - private CanvasProperty<Float> mPropY; + private ObjectAnimator mAnimator; - // Software rendering properties. private float mOpacity = 0; /** Whether this ripple is bounded. */ private boolean mIsBounded; - public RippleBackground(RippleDrawable owner, Rect bounds, boolean isBounded, - boolean forceSoftware) { - super(owner, bounds, forceSoftware); + private boolean mFocused = false; + private boolean mHovered = false; + + public RippleBackground(RippleDrawable owner, Rect bounds, boolean isBounded) { + super(owner, bounds); mIsBounded = isBounded; } public boolean isVisible() { - return mOpacity > 0 || isHardwareAnimating(); + return mOpacity > 0; } - @Override - protected boolean drawSoftware(Canvas c, Paint p) { - boolean hasContent = false; - + public void draw(Canvas c, Paint p) { final int origAlpha = p.getAlpha(); - final int alpha = (int) (origAlpha * mOpacity + 0.5f); + final int alpha = Math.min((int) (origAlpha * mOpacity + 0.5f), 255); if (alpha > 0) { p.setAlpha(alpha); c.drawCircle(0, 0, mTargetRadius, p); p.setAlpha(origAlpha); - hasContent = true; } - - return hasContent; - } - - @Override - protected boolean drawHardware(DisplayListCanvas c) { - c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); - return true; } - @Override - protected Animator createSoftwareEnter(boolean fast) { - // Linear enter based on current opacity. - final int maxDuration = fast ? OPACITY_ENTER_DURATION_FAST : OPACITY_ENTER_DURATION; - final int duration = (int) ((1 - mOpacity) * maxDuration); - - final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1); - opacity.setAutoCancel(true); - opacity.setDuration(duration); - opacity.setInterpolator(LINEAR_INTERPOLATOR); - - return opacity; - } - - @Override - protected Animator createSoftwareExit() { - final AnimatorSet set = new AnimatorSet(); - - // Linear exit after enter is completed. - final ObjectAnimator exit = ObjectAnimator.ofFloat(this, RippleBackground.OPACITY, 0); - exit.setInterpolator(LINEAR_INTERPOLATOR); - exit.setDuration(OPACITY_EXIT_DURATION); - exit.setAutoCancel(true); - - final AnimatorSet.Builder builder = set.play(exit); - - // Linear "fast" enter based on current opacity. - final int fastEnterDuration = mIsBounded ? - (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST) : 0; - if (fastEnterDuration > 0) { - final ObjectAnimator enter = ObjectAnimator.ofFloat(this, RippleBackground.OPACITY, 1); - enter.setInterpolator(LINEAR_INTERPOLATOR); - enter.setDuration(fastEnterDuration); - enter.setAutoCancel(true); - - builder.after(enter); + public void setState(boolean focused, boolean hovered, boolean pressed) { + if (!mFocused) { + focused = focused && !pressed; } - - return set; - } - - @Override - protected RenderNodeAnimatorSet createHardwareExit(Paint p) { - final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet(); - - final int targetAlpha = p.getAlpha(); - final int currentAlpha = (int) (mOpacity * targetAlpha + 0.5f); - p.setAlpha(currentAlpha); - - mPropPaint = CanvasProperty.createPaint(p); - mPropRadius = CanvasProperty.createFloat(mTargetRadius); - mPropX = CanvasProperty.createFloat(0); - mPropY = CanvasProperty.createFloat(0); - - final int fastEnterDuration = mIsBounded ? - (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST) : 0; - - // Linear exit after enter is completed. - final RenderNodeAnimator exit = new RenderNodeAnimator( - mPropPaint, RenderNodeAnimator.PAINT_ALPHA, 0); - exit.setInterpolator(LINEAR_INTERPOLATOR); - exit.setDuration(OPACITY_EXIT_DURATION); - if (fastEnterDuration > 0) { - exit.setStartDelay(fastEnterDuration); - exit.setStartValue(targetAlpha); + if (!mHovered) { + hovered = hovered && !pressed; } - set.add(exit); - - // Linear "fast" enter based on current opacity. - if (fastEnterDuration > 0) { - final RenderNodeAnimator enter = new RenderNodeAnimator( - mPropPaint, RenderNodeAnimator.PAINT_ALPHA, targetAlpha); - enter.setInterpolator(LINEAR_INTERPOLATOR); - enter.setDuration(fastEnterDuration); - set.add(enter); + if (mHovered != hovered || mFocused != focused) { + mHovered = hovered; + mFocused = focused; + onStateChanged(); } + } - return set; + private void onStateChanged() { + float newOpacity = 0.0f; + if (mHovered) newOpacity += .25f; + if (mFocused) newOpacity += .75f; + if (mAnimator != null) { + mAnimator.cancel(); + mAnimator = null; + } + mAnimator = ObjectAnimator.ofFloat(this, OPACITY, newOpacity); + mAnimator.setDuration(OPACITY_DURATION); + mAnimator.setInterpolator(LINEAR_INTERPOLATOR); + mAnimator.start(); } - @Override - protected void jumpValuesToExit() { - mOpacity = 0; + public void jumpToFinal() { + if (mAnimator != null) { + mAnimator.end(); + mAnimator = null; + } } private static abstract class BackgroundProperty extends FloatProperty<RippleBackground> { diff --git a/graphics/java/android/graphics/drawable/RippleComponent.java b/graphics/java/android/graphics/drawable/RippleComponent.java index e83513c644db..626bcee9454b 100644 --- a/graphics/java/android/graphics/drawable/RippleComponent.java +++ b/graphics/java/android/graphics/drawable/RippleComponent.java @@ -27,23 +27,14 @@ import android.view.RenderNodeAnimator; import java.util.ArrayList; /** - * Abstract class that handles hardware/software hand-off and lifecycle for - * animated ripple foreground and background components. + * Abstract class that handles size & positioning common to the ripple & focus states. */ abstract class RippleComponent { - private final RippleDrawable mOwner; + protected final RippleDrawable mOwner; /** Bounds used for computing max radius. May be modified by the owner. */ protected final Rect mBounds; - /** Whether we can use hardware acceleration for the exit animation. */ - private boolean mHasDisplayListCanvas; - - private boolean mHasPendingHardwareAnimator; - private RenderNodeAnimatorSet mHardwareAnimator; - - private Animator mSoftwareAnimator; - /** Whether we have an explicit maximum radius. */ private boolean mHasMaxRadius; @@ -53,16 +44,9 @@ abstract class RippleComponent { /** Screen density used to adjust pixel-based constants. */ protected float mDensityScale; - /** - * If set, force all ripple animations to not run on RenderThread, even if it would be - * available. - */ - private final boolean mForceSoftware; - - public RippleComponent(RippleDrawable owner, Rect bounds, boolean forceSoftware) { + public RippleComponent(RippleDrawable owner, Rect bounds) { mOwner = owner; mBounds = bounds; - mForceSoftware = forceSoftware; } public void onBoundsChange() { @@ -92,89 +76,6 @@ abstract class RippleComponent { } /** - * Starts a ripple enter animation. - * - * @param fast whether the ripple should enter quickly - */ - public final void enter(boolean fast) { - cancel(); - - mSoftwareAnimator = createSoftwareEnter(fast); - - if (mSoftwareAnimator != null) { - mSoftwareAnimator.start(); - } - } - - /** - * Starts a ripple exit animation. - */ - public final void exit() { - cancel(); - - if (mHasDisplayListCanvas) { - // We don't have access to a canvas here, but we expect one on the - // next frame. We'll start the render thread animation then. - mHasPendingHardwareAnimator = true; - - // Request another frame. - invalidateSelf(); - } else { - mSoftwareAnimator = createSoftwareExit(); - mSoftwareAnimator.start(); - } - } - - /** - * Cancels all animations. Software animation values are left in the - * current state, while hardware animation values jump to the end state. - */ - public void cancel() { - cancelSoftwareAnimations(); - endHardwareAnimations(); - } - - /** - * Ends all animations, jumping values to the end state. - */ - public void end() { - endSoftwareAnimations(); - endHardwareAnimations(); - } - - /** - * Draws the ripple to the canvas, inheriting the paint's color and alpha - * properties. - * - * @param c the canvas to which the ripple should be drawn - * @param p the paint used to draw the ripple - * @return {@code true} if something was drawn, {@code false} otherwise - */ - public boolean draw(Canvas c, Paint p) { - final boolean hasDisplayListCanvas = !mForceSoftware && c.isHardwareAccelerated() - && c instanceof DisplayListCanvas; - if (mHasDisplayListCanvas != hasDisplayListCanvas) { - mHasDisplayListCanvas = hasDisplayListCanvas; - - if (!hasDisplayListCanvas) { - // We've switched from hardware to non-hardware mode. Panic. - endHardwareAnimations(); - } - } - - if (hasDisplayListCanvas) { - final DisplayListCanvas hw = (DisplayListCanvas) c; - startPendingAnimation(hw, p); - - if (mHardwareAnimator != null) { - return drawHardware(hw); - } - } - - return drawSoftware(c, p); - } - - /** * Populates {@code bounds} with the maximum drawing bounds of the ripple * relative to its center. The resulting bounds should be translated into * parent drawable coordinates before use. @@ -186,85 +87,14 @@ abstract class RippleComponent { bounds.set(-r, -r, r, r); } - /** - * Starts the pending hardware animation, if available. - * - * @param hw hardware canvas on which the animation should draw - * @param p paint whose properties the hardware canvas should use - */ - private void startPendingAnimation(DisplayListCanvas hw, Paint p) { - if (mHasPendingHardwareAnimator) { - mHasPendingHardwareAnimator = false; - - mHardwareAnimator = createHardwareExit(new Paint(p)); - mHardwareAnimator.start(hw); - - // Preemptively jump the software values to the end state now that - // the hardware exit has read whatever values it needs. - jumpValuesToExit(); - } - } - - /** - * Cancels any current software animations, leaving the values in their - * current state. - */ - private void cancelSoftwareAnimations() { - if (mSoftwareAnimator != null) { - mSoftwareAnimator.cancel(); - mSoftwareAnimator = null; - } - } - - /** - * Ends any current software animations, jumping the values to their end - * state. - */ - private void endSoftwareAnimations() { - if (mSoftwareAnimator != null) { - mSoftwareAnimator.end(); - mSoftwareAnimator = null; - } - } - - /** - * Ends any pending or current hardware animations. - * <p> - * Hardware animations can't synchronize values back to the software - * thread, so there is no "cancel" equivalent. - */ - private void endHardwareAnimations() { - if (mHardwareAnimator != null) { - mHardwareAnimator.end(); - mHardwareAnimator = null; - } - - if (mHasPendingHardwareAnimator) { - mHasPendingHardwareAnimator = false; - - // Manually jump values to their exited state. Normally we'd do that - // later when starting the hardware exit, but we're aborting early. - jumpValuesToExit(); - } - } - protected final void invalidateSelf() { mOwner.invalidateSelf(false); } - protected final boolean isHardwareAnimating() { - return mHardwareAnimator != null && mHardwareAnimator.isRunning() - || mHasPendingHardwareAnimator; - } - protected final void onHotspotBoundsChanged() { if (!mHasMaxRadius) { - final float halfWidth = mBounds.width() / 2.0f; - final float halfHeight = mBounds.height() / 2.0f; - final float targetRadius = (float) Math.sqrt(halfWidth * halfWidth - + halfHeight * halfHeight); - - onTargetRadiusChanged(targetRadius); + mTargetRadius = getTargetRadius(mBounds); + onTargetRadiusChanged(mTargetRadius); } } @@ -276,76 +106,4 @@ abstract class RippleComponent { protected void onTargetRadiusChanged(float targetRadius) { // Stub. } - - protected abstract Animator createSoftwareEnter(boolean fast); - - protected abstract Animator createSoftwareExit(); - - protected abstract RenderNodeAnimatorSet createHardwareExit(Paint p); - - protected abstract boolean drawHardware(DisplayListCanvas c); - - protected abstract boolean drawSoftware(Canvas c, Paint p); - - /** - * Called when the hardware exit is cancelled. Jumps software values to end - * state to ensure that software and hardware values are synchronized. - */ - protected abstract void jumpValuesToExit(); - - public static class RenderNodeAnimatorSet { - private final ArrayList<RenderNodeAnimator> mAnimators = new ArrayList<>(); - - public void add(RenderNodeAnimator anim) { - mAnimators.add(anim); - } - - public void clear() { - mAnimators.clear(); - } - - public void start(DisplayListCanvas target) { - if (target == null) { - throw new IllegalArgumentException("Hardware canvas must be non-null"); - } - - final ArrayList<RenderNodeAnimator> animators = mAnimators; - final int N = animators.size(); - for (int i = 0; i < N; i++) { - final RenderNodeAnimator anim = animators.get(i); - anim.setTarget(target); - anim.start(); - } - } - - public void cancel() { - final ArrayList<RenderNodeAnimator> animators = mAnimators; - final int N = animators.size(); - for (int i = 0; i < N; i++) { - final RenderNodeAnimator anim = animators.get(i); - anim.cancel(); - } - } - - public void end() { - final ArrayList<RenderNodeAnimator> animators = mAnimators; - final int N = animators.size(); - for (int i = 0; i < N; i++) { - final RenderNodeAnimator anim = animators.get(i); - anim.end(); - } - } - - public boolean isRunning() { - final ArrayList<RenderNodeAnimator> animators = mAnimators; - final int N = animators.size(); - for (int i = 0; i < N; i++) { - final RenderNodeAnimator anim = animators.get(i); - if (anim.isRunning()) { - return true; - } - } - return false; - } - } } diff --git a/graphics/java/android/graphics/drawable/RippleDrawable.java b/graphics/java/android/graphics/drawable/RippleDrawable.java index 8f314c9c36aa..0da61c29bd8d 100644 --- a/graphics/java/android/graphics/drawable/RippleDrawable.java +++ b/graphics/java/android/graphics/drawable/RippleDrawable.java @@ -16,11 +16,6 @@ package android.graphics.drawable; -import com.android.internal.R; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - import android.annotation.NonNull; import android.annotation.Nullable; import android.content.pm.ActivityInfo.Config; @@ -42,6 +37,11 @@ import android.graphics.Rect; import android.graphics.Shader; import android.util.AttributeSet; +import com.android.internal.R; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + import java.io.IOException; import java.util.Arrays; @@ -135,9 +135,6 @@ public class RippleDrawable extends LayerDrawable { private PorterDuffColorFilter mMaskColorFilter; private boolean mHasValidMask; - /** Whether we expect to draw a background when visible. */ - private boolean mBackgroundActive; - /** The current ripple. May be actively animating or pending entry. */ private RippleForeground mRipple; @@ -217,7 +214,7 @@ public class RippleDrawable extends LayerDrawable { } if (mBackground != null) { - mBackground.end(); + mBackground.jumpToFinal(); } cancelExitingRipples(); @@ -267,7 +264,7 @@ public class RippleDrawable extends LayerDrawable { } setRippleActive(enabled && pressed); - setBackgroundActive(hovered || focused || (enabled && pressed), focused || hovered); + setBackgroundActive(hovered, focused, pressed); return changed; } @@ -283,14 +280,13 @@ public class RippleDrawable extends LayerDrawable { } } - private void setBackgroundActive(boolean active, boolean focused) { - if (mBackgroundActive != active) { - mBackgroundActive = active; - if (active) { - tryBackgroundEnter(focused); - } else { - tryBackgroundExit(); - } + private void setBackgroundActive(boolean hovered, boolean focused, boolean pressed) { + if (mBackground == null && (hovered || focused)) { + mBackground = new RippleBackground(this, mHotspotBounds, isBounded()); + mBackground.setup(mState.mMaxRadius, mDensity); + } + if (mBackground != null) { + mBackground.setState(focused, hovered, pressed); } } @@ -303,6 +299,12 @@ public class RippleDrawable extends LayerDrawable { onHotspotBoundsChanged(); } + final int count = mExitingRipplesCount; + final RippleForeground[] ripples = mExitingRipples; + for (int i = 0; i < count; i++) { + ripples[i].onBoundsChange(); + } + if (mBackground != null) { mBackground.onBoundsChange(); } @@ -327,10 +329,6 @@ public class RippleDrawable extends LayerDrawable { tryRippleEnter(); } - if (mBackgroundActive) { - tryBackgroundEnter(false); - } - // Skip animations, just show the correct final states. jumpToCurrentState(); } @@ -546,26 +544,6 @@ public class RippleDrawable extends LayerDrawable { } /** - * Creates an active hotspot at the specified location. - */ - private void tryBackgroundEnter(boolean focused) { - if (mBackground == null) { - final boolean isBounded = isBounded(); - mBackground = new RippleBackground(this, mHotspotBounds, isBounded, mForceSoftware); - } - - mBackground.setup(mState.mMaxRadius, mDensity); - mBackground.enter(focused); - } - - private void tryBackgroundExit() { - if (mBackground != null) { - // Don't null out the background, we need it to draw! - mBackground.exit(); - } - } - - /** * Attempts to start an enter animation for the active hotspot. Fails if * there are too many animating ripples. */ @@ -588,12 +566,11 @@ public class RippleDrawable extends LayerDrawable { y = mHotspotBounds.exactCenterY(); } - final boolean isBounded = isBounded(); - mRipple = new RippleForeground(this, mHotspotBounds, x, y, isBounded, mForceSoftware); + mRipple = new RippleForeground(this, mHotspotBounds, x, y, mForceSoftware); } mRipple.setup(mState.mMaxRadius, mDensity); - mRipple.enter(false); + mRipple.enter(); } /** @@ -623,9 +600,7 @@ public class RippleDrawable extends LayerDrawable { } if (mBackground != null) { - mBackground.end(); - mBackground = null; - mBackgroundActive = false; + mBackground.setState(false, false, false); } cancelExitingRipples(); @@ -693,7 +668,9 @@ public class RippleDrawable extends LayerDrawable { // have a mask or content and the ripple bounds if we're projecting. final Rect bounds = getDirtyBounds(); final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); - canvas.clipRect(bounds); + if (isBounded()) { + canvas.clipRect(bounds); + } drawContent(canvas); drawBackgroundAndRipples(canvas); @@ -856,38 +833,8 @@ public class RippleDrawable extends LayerDrawable { final float y = mHotspotBounds.exactCenterY(); canvas.translate(x, y); - updateMaskShaderIfNeeded(); - - // Position the shader to account for canvas translation. - if (mMaskShader != null) { - final Rect bounds = getBounds(); - mMaskMatrix.setTranslate(bounds.left - x, bounds.top - y); - mMaskShader.setLocalMatrix(mMaskMatrix); - } - - // Grab the color for the current state and cut the alpha channel in - // half so that the ripple and background together yield full alpha. - final int color = mState.mColor.getColorForState(getState(), Color.BLACK); - final int halfAlpha = (Color.alpha(color) / 2) << 24; final Paint p = getRipplePaint(); - if (mMaskColorFilter != null) { - // The ripple timing depends on the paint's alpha value, so we need - // to push just the alpha channel into the paint and let the filter - // handle the full-alpha color. - final int fullAlphaColor = color | (0xFF << 24); - mMaskColorFilter.setColor(fullAlphaColor); - - p.setColor(halfAlpha); - p.setColorFilter(mMaskColorFilter); - p.setShader(mMaskShader); - } else { - final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha; - p.setColor(halfAlphaColor); - p.setColorFilter(null); - p.setShader(null); - } - if (background != null && background.isVisible()) { background.draw(canvas, p); } @@ -910,13 +857,48 @@ public class RippleDrawable extends LayerDrawable { mMask.draw(canvas); } - private Paint getRipplePaint() { + Paint getRipplePaint() { if (mRipplePaint == null) { mRipplePaint = new Paint(); mRipplePaint.setAntiAlias(true); mRipplePaint.setStyle(Paint.Style.FILL); } - return mRipplePaint; + + final float x = mHotspotBounds.exactCenterX(); + final float y = mHotspotBounds.exactCenterY(); + + updateMaskShaderIfNeeded(); + + // Position the shader to account for canvas translation. + if (mMaskShader != null) { + final Rect bounds = getBounds(); + mMaskMatrix.setTranslate(bounds.left - x, bounds.top - y); + mMaskShader.setLocalMatrix(mMaskMatrix); + } + + // Grab the color for the current state and cut the alpha channel in + // half so that the ripple and background together yield full alpha. + int color = mState.mColor.getColorForState(getState(), Color.BLACK); + if (Color.alpha(color) > 128) { + color = (color & 0x00FFFFFF) | 0x80000000; + } + final Paint p = mRipplePaint; + + if (mMaskColorFilter != null) { + // The ripple timing depends on the paint's alpha value, so we need + // to push just the alpha channel into the paint and let the filter + // handle the full-alpha color. + mMaskColorFilter.setColor(color | 0xFF000000); + p.setColor(color & 0xFF000000); + p.setColorFilter(mMaskColorFilter); + p.setShader(mMaskShader); + } else { + p.setColor(color); + p.setColorFilter(null); + p.setShader(null); + } + + return p; } @Override diff --git a/graphics/java/android/graphics/drawable/RippleForeground.java b/graphics/java/android/graphics/drawable/RippleForeground.java index 829733e9b097..a8dc34af292b 100644 --- a/graphics/java/android/graphics/drawable/RippleForeground.java +++ b/graphics/java/android/graphics/drawable/RippleForeground.java @@ -18,7 +18,6 @@ package android.graphics.drawable; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.graphics.Canvas; @@ -29,29 +28,29 @@ import android.util.FloatProperty; import android.util.MathUtils; import android.view.DisplayListCanvas; import android.view.RenderNodeAnimator; +import android.view.animation.AnimationUtils; import android.view.animation.LinearInterpolator; +import android.view.animation.PathInterpolator; + +import java.util.ArrayList; /** * Draws a ripple foreground. */ class RippleForeground extends RippleComponent { private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); - private static final TimeInterpolator DECELERATE_INTERPOLATOR = new LogDecelerateInterpolator( - 400f, 1.4f, 0); - - // Pixel-based accelerations and velocities. - private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024; - private static final float WAVE_TOUCH_UP_ACCELERATION = 3400; - private static final float WAVE_OPACITY_DECAY_VELOCITY = 3; + // Matches R.interpolator.fast_out_slow_in but as we have no context we can't just import that + private static final TimeInterpolator DECELERATE_INTERPOLATOR = + new PathInterpolator(0.4f, 0f, 0.2f, 1f); - // Bounded ripple animation properties. - private static final int BOUNDED_ORIGIN_EXIT_DURATION = 300; - private static final int BOUNDED_RADIUS_EXIT_DURATION = 800; - private static final int BOUNDED_OPACITY_EXIT_DURATION = 400; - private static final float MAX_BOUNDED_RADIUS = 350; + // Time it takes for the ripple to expand + private static final int RIPPLE_ENTER_DURATION = 225; + // Time it takes for the ripple to slide from the touch to the center point + private static final int RIPPLE_ORIGIN_DURATION = 225; - private static final int RIPPLE_ENTER_DELAY = 80; - private static final int OPACITY_ENTER_DURATION_FAST = 120; + private static final int OPACITY_ENTER_DURATION = 75; + private static final int OPACITY_EXIT_DURATION = 150; + private static final int OPACITY_HOLD_DURATION = OPACITY_ENTER_DURATION + 150; // Parent-relative values for starting position. private float mStartingX; @@ -69,48 +68,58 @@ class RippleForeground extends RippleComponent { private float mTargetX = 0; private float mTargetY = 0; - /** Ripple target radius used when bounded. Not used for clamping. */ - private float mBoundedRadius = 0; - // Software rendering properties. - private float mOpacity = 1; + private float mOpacity = 0; // Values used to tween between the start and end positions. private float mTweenRadius = 0; private float mTweenX = 0; private float mTweenY = 0; - /** Whether this ripple is bounded. */ - private boolean mIsBounded; - /** Whether this ripple has finished its exit animation. */ private boolean mHasFinishedExit; + /** Whether we can use hardware acceleration for the exit animation. */ + private boolean mUsingProperties; + + private long mEnterStartedAtMillis; + + private ArrayList<RenderNodeAnimator> mPendingHwAnimators = new ArrayList<>(); + private ArrayList<RenderNodeAnimator> mRunningHwAnimators = new ArrayList<>(); + + private ArrayList<Animator> mRunningSwAnimators = new ArrayList<>(); + + /** + * If set, force all ripple animations to not run on RenderThread, even if it would be + * available. + */ + private final boolean mForceSoftware; + + /** + * If we have a bound, don't start from 0. Start from 60% of the max out of width and height. + */ + private float mStartRadius = 0; + public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY, - boolean isBounded, boolean forceSoftware) { - super(owner, bounds, forceSoftware); + boolean forceSoftware) { + super(owner, bounds); - mIsBounded = isBounded; + mForceSoftware = forceSoftware; mStartingX = startingX; mStartingY = startingY; - if (isBounded) { - mBoundedRadius = MAX_BOUNDED_RADIUS * 0.9f - + (float) (MAX_BOUNDED_RADIUS * Math.random() * 0.1); - } else { - mBoundedRadius = 0; - } + // Take 60% of the maximum of the width and height, then divided half to get the radius. + mStartRadius = Math.max(bounds.width(), bounds.height()) * 0.3f; + clampStartingPosition(); } @Override protected void onTargetRadiusChanged(float targetRadius) { clampStartingPosition(); + switchToUiThreadAnimation(); } - @Override - protected boolean drawSoftware(Canvas c, Paint p) { - boolean hasContent = false; - + private void drawSoftware(Canvas c, Paint p) { final int origAlpha = p.getAlpha(); final int alpha = (int) (origAlpha * mOpacity + 0.5f); final float radius = getCurrentRadius(); @@ -120,16 +129,51 @@ class RippleForeground extends RippleComponent { p.setAlpha(alpha); c.drawCircle(x, y, radius, p); p.setAlpha(origAlpha); - hasContent = true; } + } - return hasContent; + private void startPending(DisplayListCanvas c) { + if (!mPendingHwAnimators.isEmpty()) { + for (int i = 0; i < mPendingHwAnimators.size(); i++) { + RenderNodeAnimator animator = mPendingHwAnimators.get(i); + animator.setTarget(c); + animator.start(); + mRunningHwAnimators.add(animator); + } + mPendingHwAnimators.clear(); + } } - @Override - protected boolean drawHardware(DisplayListCanvas c) { - c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); - return true; + private void pruneHwFinished() { + if (!mRunningHwAnimators.isEmpty()) { + for (int i = mRunningHwAnimators.size() - 1; i >= 0; i--) { + if (!mRunningHwAnimators.get(i).isRunning()) { + mRunningHwAnimators.remove(i); + } + } + } + } + + private void pruneSwFinished() { + if (!mRunningSwAnimators.isEmpty()) { + for (int i = mRunningSwAnimators.size() - 1; i >= 0; i--) { + if (!mRunningSwAnimators.get(i).isRunning()) { + mRunningSwAnimators.remove(i); + } + } + } + } + + private void drawHardware(DisplayListCanvas c, Paint p) { + startPending(c); + pruneHwFinished(); + if (mPropPaint != null) { + mUsingProperties = true; + c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); + } else { + mUsingProperties = false; + drawSoftware(c, p); + } } /** @@ -160,170 +204,143 @@ class RippleForeground extends RippleComponent { return mHasFinishedExit; } - @Override - protected Animator createSoftwareEnter(boolean fast) { - // Bounded ripples don't have enter animations. - if (mIsBounded) { - return null; + private long computeFadeOutDelay() { + long timeSinceEnter = AnimationUtils.currentAnimationTimeMillis() - mEnterStartedAtMillis; + if (timeSinceEnter > 0 && timeSinceEnter < OPACITY_HOLD_DURATION) { + return OPACITY_HOLD_DURATION - timeSinceEnter; } - - final int duration = (int) - (1000 * Math.sqrt(mTargetRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensityScale) + 0.5); - - final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1); - tweenRadius.setAutoCancel(true); - tweenRadius.setDuration(duration); - tweenRadius.setInterpolator(LINEAR_INTERPOLATOR); - tweenRadius.setStartDelay(RIPPLE_ENTER_DELAY); - - final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1); - tweenOrigin.setAutoCancel(true); - tweenOrigin.setDuration(duration); - tweenOrigin.setInterpolator(LINEAR_INTERPOLATOR); - tweenOrigin.setStartDelay(RIPPLE_ENTER_DELAY); - - final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1); - opacity.setAutoCancel(true); - opacity.setDuration(OPACITY_ENTER_DURATION_FAST); - opacity.setInterpolator(LINEAR_INTERPOLATOR); - - final AnimatorSet set = new AnimatorSet(); - set.play(tweenOrigin).with(tweenRadius).with(opacity); - - return set; - } - - private float getCurrentX() { - return MathUtils.lerp(mClampedStartingX - mBounds.exactCenterX(), mTargetX, mTweenX); - } - - private float getCurrentY() { - return MathUtils.lerp(mClampedStartingY - mBounds.exactCenterY(), mTargetY, mTweenY); + return 0; } - private int getRadiusExitDuration() { - final float remainingRadius = mTargetRadius - getCurrentRadius(); - return (int) (1000 * Math.sqrt(remainingRadius / (WAVE_TOUCH_UP_ACCELERATION - + WAVE_TOUCH_DOWN_ACCELERATION) * mDensityScale) + 0.5); - } - - private float getCurrentRadius() { - return MathUtils.lerp(0, mTargetRadius, mTweenRadius); - } - - private int getOpacityExitDuration() { - return (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); - } - - /** - * Compute target values that are dependent on bounding. - */ - private void computeBoundedTargetValues() { - mTargetX = (mClampedStartingX - mBounds.exactCenterX()) * .7f; - mTargetY = (mClampedStartingY - mBounds.exactCenterY()) * .7f; - mTargetRadius = mBoundedRadius; - } - - @Override - protected Animator createSoftwareExit() { - final int radiusDuration; - final int originDuration; - final int opacityDuration; - if (mIsBounded) { - computeBoundedTargetValues(); - - radiusDuration = BOUNDED_RADIUS_EXIT_DURATION; - originDuration = BOUNDED_ORIGIN_EXIT_DURATION; - opacityDuration = BOUNDED_OPACITY_EXIT_DURATION; - } else { - radiusDuration = getRadiusExitDuration(); - originDuration = radiusDuration; - opacityDuration = getOpacityExitDuration(); + private void startSoftwareEnter() { + for (int i = 0; i < mRunningSwAnimators.size(); i++) { + mRunningSwAnimators.get(i).cancel(); } + mRunningSwAnimators.clear(); final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1); - tweenRadius.setAutoCancel(true); - tweenRadius.setDuration(radiusDuration); + tweenRadius.setDuration(RIPPLE_ENTER_DURATION); tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR); + tweenRadius.start(); + mRunningSwAnimators.add(tweenRadius); final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1); - tweenOrigin.setAutoCancel(true); - tweenOrigin.setDuration(originDuration); + tweenOrigin.setDuration(RIPPLE_ORIGIN_DURATION); tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR); + tweenOrigin.start(); + mRunningSwAnimators.add(tweenOrigin); - final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0); - opacity.setAutoCancel(true); - opacity.setDuration(opacityDuration); + final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1); + opacity.setDuration(OPACITY_ENTER_DURATION); opacity.setInterpolator(LINEAR_INTERPOLATOR); - - final AnimatorSet set = new AnimatorSet(); - set.play(tweenOrigin).with(tweenRadius).with(opacity); - set.addListener(mAnimationListener); - - return set; + opacity.start(); + mRunningSwAnimators.add(opacity); } - @Override - protected RenderNodeAnimatorSet createHardwareExit(Paint p) { - final int radiusDuration; - final int originDuration; - final int opacityDuration; - if (mIsBounded) { - computeBoundedTargetValues(); - - radiusDuration = BOUNDED_RADIUS_EXIT_DURATION; - originDuration = BOUNDED_ORIGIN_EXIT_DURATION; - opacityDuration = BOUNDED_OPACITY_EXIT_DURATION; - } else { - radiusDuration = getRadiusExitDuration(); - originDuration = radiusDuration; - opacityDuration = getOpacityExitDuration(); - } - - final float startX = getCurrentX(); - final float startY = getCurrentY(); - final float startRadius = getCurrentRadius(); - - p.setAlpha((int) (p.getAlpha() * mOpacity + 0.5f)); + private void startSoftwareExit() { + final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0); + opacity.setDuration(OPACITY_EXIT_DURATION); + opacity.setInterpolator(LINEAR_INTERPOLATOR); + opacity.addListener(mAnimationListener); + opacity.setStartDelay(computeFadeOutDelay()); + opacity.start(); + mRunningSwAnimators.add(opacity); + } - mPropPaint = CanvasProperty.createPaint(p); - mPropRadius = CanvasProperty.createFloat(startRadius); - mPropX = CanvasProperty.createFloat(startX); - mPropY = CanvasProperty.createFloat(startY); + private void startHardwareEnter() { + if (mForceSoftware) { return; } + mPropX = CanvasProperty.createFloat(getCurrentX()); + mPropY = CanvasProperty.createFloat(getCurrentY()); + mPropRadius = CanvasProperty.createFloat(getCurrentRadius()); + final Paint paint = mOwner.getRipplePaint(); + mPropPaint = CanvasProperty.createPaint(paint); final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius); - radius.setDuration(radiusDuration); + radius.setDuration(RIPPLE_ORIGIN_DURATION); radius.setInterpolator(DECELERATE_INTERPOLATOR); + mPendingHwAnimators.add(radius); final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mTargetX); - x.setDuration(originDuration); + x.setDuration(RIPPLE_ORIGIN_DURATION); x.setInterpolator(DECELERATE_INTERPOLATOR); + mPendingHwAnimators.add(x); final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mTargetY); - y.setDuration(originDuration); + y.setDuration(RIPPLE_ORIGIN_DURATION); y.setInterpolator(DECELERATE_INTERPOLATOR); + mPendingHwAnimators.add(y); + + final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint, + RenderNodeAnimator.PAINT_ALPHA, paint.getAlpha()); + opacity.setDuration(OPACITY_ENTER_DURATION); + opacity.setInterpolator(LINEAR_INTERPOLATOR); + opacity.setStartValue(0); + mPendingHwAnimators.add(opacity); + + invalidateSelf(); + } + + private void startHardwareExit() { + // Only run a hardware exit if we had a hardware enter to continue from + if (mForceSoftware || mPropPaint == null) return; final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint, RenderNodeAnimator.PAINT_ALPHA, 0); - opacity.setDuration(opacityDuration); + opacity.setDuration(OPACITY_EXIT_DURATION); opacity.setInterpolator(LINEAR_INTERPOLATOR); opacity.addListener(mAnimationListener); + opacity.setStartDelay(computeFadeOutDelay()); + opacity.setStartValue(mOwner.getRipplePaint().getAlpha()); + mPendingHwAnimators.add(opacity); + invalidateSelf(); + } - final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet(); - set.add(radius); - set.add(opacity); - set.add(x); - set.add(y); + /** + * Starts a ripple enter animation. + */ + public final void enter() { + mEnterStartedAtMillis = AnimationUtils.currentAnimationTimeMillis(); + startSoftwareEnter(); + startHardwareEnter(); + } - return set; + /** + * Starts a ripple exit animation. + */ + public final void exit() { + startSoftwareExit(); + startHardwareExit(); } - @Override - protected void jumpValuesToExit() { - mOpacity = 0; - mTweenX = 1; - mTweenY = 1; - mTweenRadius = 1; + private float getCurrentX() { + return MathUtils.lerp(mClampedStartingX - mBounds.exactCenterX(), mTargetX, mTweenX); + } + + private float getCurrentY() { + return MathUtils.lerp(mClampedStartingY - mBounds.exactCenterY(), mTargetY, mTweenY); + } + + private float getCurrentRadius() { + return MathUtils.lerp(mStartRadius, mTargetRadius, mTweenRadius); + } + + /** + * Draws the ripple to the canvas, inheriting the paint's color and alpha + * properties. + * + * @param c the canvas to which the ripple should be drawn + * @param p the paint used to draw the ripple + */ + public void draw(Canvas c, Paint p) { + final boolean hasDisplayListCanvas = !mForceSoftware && c instanceof DisplayListCanvas; + + pruneSwFinished(); + if (hasDisplayListCanvas) { + final DisplayListCanvas hw = (DisplayListCanvas) c; + drawHardware(hw, p); + } else { + drawSoftware(c, p); + } } /** @@ -334,7 +351,7 @@ class RippleForeground extends RippleComponent { final float cY = mBounds.exactCenterY(); final float dX = mStartingX - cX; final float dY = mStartingY - cY; - final float r = mTargetRadius; + final float r = mTargetRadius - mStartRadius; if (dX * dX + dY * dY > r * r) { // Point is outside the circle, clamp to the perimeter. final double angle = Math.atan2(dY, dX); @@ -346,38 +363,56 @@ class RippleForeground extends RippleComponent { } } + /** + * Ends all animations, jumping values to the end state. + */ + public void end() { + for (int i = 0; i < mRunningSwAnimators.size(); i++) { + mRunningSwAnimators.get(i).end(); + } + mRunningSwAnimators.clear(); + for (int i = 0; i < mRunningHwAnimators.size(); i++) { + mRunningHwAnimators.get(i).end(); + } + mRunningHwAnimators.clear(); + } + + private void onAnimationPropertyChanged() { + if (!mUsingProperties) { + invalidateSelf(); + } + } + + private void clearHwProps() { + mPropPaint = null; + mPropRadius = null; + mPropX = null; + mPropY = null; + mUsingProperties = false; + } + private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animator) { mHasFinishedExit = true; - } - }; - - /** - * Interpolator with a smooth log deceleration. - */ - private static final class LogDecelerateInterpolator implements TimeInterpolator { - private final float mBase; - private final float mDrift; - private final float mTimeScale; - private final float mOutputScale; - - public LogDecelerateInterpolator(float base, float timeScale, float drift) { - mBase = base; - mDrift = drift; - mTimeScale = 1f / timeScale; - - mOutputScale = 1f / computeLog(1f); - } + pruneHwFinished(); + pruneSwFinished(); - private float computeLog(float t) { - return 1f - (float) Math.pow(mBase, -t * mTimeScale) + (mDrift * t); + if (mRunningHwAnimators.isEmpty()) { + clearHwProps(); + } } + }; - @Override - public float getInterpolation(float t) { - return computeLog(t) * mOutputScale; + private void switchToUiThreadAnimation() { + for (int i = 0; i < mRunningHwAnimators.size(); i++) { + Animator animator = mRunningHwAnimators.get(i); + animator.removeListener(mAnimationListener); + animator.end(); } + mRunningHwAnimators.clear(); + clearHwProps(); + invalidateSelf(); } /** @@ -388,7 +423,7 @@ class RippleForeground extends RippleComponent { @Override public void setValue(RippleForeground object, float value) { object.mTweenRadius = value; - object.invalidateSelf(); + object.onAnimationPropertyChanged(); } @Override @@ -402,18 +437,18 @@ class RippleForeground extends RippleComponent { */ private static final FloatProperty<RippleForeground> TWEEN_ORIGIN = new FloatProperty<RippleForeground>("tweenOrigin") { - @Override - public void setValue(RippleForeground object, float value) { - object.mTweenX = value; - object.mTweenY = value; - object.invalidateSelf(); - } + @Override + public void setValue(RippleForeground object, float value) { + object.mTweenX = value; + object.mTweenY = value; + object.onAnimationPropertyChanged(); + } - @Override - public Float get(RippleForeground object) { - return object.mTweenX; - } - }; + @Override + public Float get(RippleForeground object) { + return object.mTweenX; + } + }; /** * Property for animating opacity between 0 and its target value. @@ -423,7 +458,7 @@ class RippleForeground extends RippleComponent { @Override public void setValue(RippleForeground object, float value) { object.mOpacity = value; - object.invalidateSelf(); + object.onAnimationPropertyChanged(); } @Override diff --git a/graphics/java/android/graphics/drawable/VectorDrawable.java b/graphics/java/android/graphics/drawable/VectorDrawable.java index ceac3253e178..c71585f32155 100644 --- a/graphics/java/android/graphics/drawable/VectorDrawable.java +++ b/graphics/java/android/graphics/drawable/VectorDrawable.java @@ -213,12 +213,79 @@ import dalvik.system.VMRuntime; * </vector> * </pre> * </li> - * <li>And here is an example of linear gradient color, which is supported in SDK 24+. + * <h4>Gradient support</h4> + * We support 3 types of gradients: {@link android.graphics.LinearGradient}, + * {@link android.graphics.RadialGradient}, or {@link android.graphics.SweepGradient}. + * <p/> + * And we support all of 3 types of tile modes {@link android.graphics.Shader.TileMode}: + * CLAMP, REPEAT, MIRROR. + * <p/> + * All of the attributes are listed in {@link android.R.styleable#GradientColor}. + * Note that different attributes are relevant for different types of gradient. + * <table border="2" align="center" cellpadding="5"> + * <thead> + * <tr> + * <th>LinearGradient</th> + * <th>RadialGradient</th> + * <th>SweepGradient</th> + * </tr> + * </thead> + * <tr> + * <td>startColor </td> + * <td>startColor</td> + * <td>startColor</td> + * </tr> + * <tr> + * <td>centerColor</td> + * <td>centerColor</td> + * <td>centerColor</td> + * </tr> + * <tr> + * <td>endColor</td> + * <td>endColor</td> + * <td>endColor</td> + * </tr> + * <tr> + * <td>type</td> + * <td>type</td> + * <td>type</td> + * </tr> + * <tr> + * <td>tileMode</td> + * <td>tileMode</td> + * <td>tileMode</td> + * </tr> + * <tr> + * <td>startX</td> + * <td>centerX</td> + * <td>centerX</td> + * </tr> + * <tr> + * <td>startY</td> + * <td>centerY</td> + * <td>centerY</td> + * </tr> + * <tr> + * <td>endX</td> + * <td>gradientRadius</td> + * <td></td> + * </tr> + * <tr> + * <td>endY</td> + * <td></td> + * <td></td> + * </tr> + * </table> + * <p/> + * Also note that if any color item {@link android.R.styleable#GradientColorItem} is defined, then + * startColor, centerColor and endColor will be ignored. + * <p/> * See more details in {@link android.R.styleable#GradientColor} and * {@link android.R.styleable#GradientColorItem}. + * <p/> + * Here is a simple example that defines a linear gradient. * <pre> * <gradient xmlns:android="http://schemas.android.com/apk/res/android" - * android:angle="90" * android:startColor="?android:attr/colorPrimary" * android:endColor="?android:attr/colorControlActivated" * android:centerColor="#f00" @@ -229,7 +296,18 @@ import dalvik.system.VMRuntime; * android:type="linear"> * </gradient> * </pre> - * </li> + * And here is a simple example that defines a radial gradient using color items. + * <pre> + * <gradient xmlns:android="http://schemas.android.com/apk/res/android" + * android:centerX="300" + * android:centerY="300" + * android:gradientRadius="100" + * android:type="radial"> + * <item android:offset="0.1" android:color="#0ff"/> + * <item android:offset="0.4" android:color="#fff"/> + * <item android:offset="0.9" android:color="#ff0"/> + * </gradient> + * </pre> * */ @@ -818,6 +896,13 @@ public class VectorDrawable extends Drawable { return mVectorState.getNativeRenderer(); } + /** + * @hide + */ + public void setAntiAlias(boolean aa) { + nSetAntiAlias(mVectorState.mNativeTree.get(), aa); + } + static class VectorDrawableState extends ConstantState { // Variables below need to be copied (deep copy if applicable) for mutation. int[] mThemeAttrs; @@ -2191,6 +2276,8 @@ public class VectorDrawable extends Drawable { @FastNative private static native float nGetRootAlpha(long rendererPtr); @FastNative + private static native void nSetAntiAlias(long rendererPtr, boolean aa); + @FastNative private static native void nSetAllowCaching(long rendererPtr, boolean allowCaching); @FastNative diff --git a/graphics/java/android/graphics/fonts/FontVariationAxis.java b/graphics/java/android/graphics/fonts/FontVariationAxis.java index 99564fab94cd..1b7408a03294 100644 --- a/graphics/java/android/graphics/fonts/FontVariationAxis.java +++ b/graphics/java/android/graphics/fonts/FontVariationAxis.java @@ -178,7 +178,7 @@ public final class FontVariationAxis { * @return String a valid font variation settings string. */ public static @NonNull String toFontVariationSettings(@Nullable FontVariationAxis[] axes) { - if (axes == null || axes.length == 0) { + if (axes == null) { return ""; } return TextUtils.join(",", axes); diff --git a/graphics/java/android/graphics/pdf/PdfEditor.java b/graphics/java/android/graphics/pdf/PdfEditor.java index dc4b11791c1e..3821bc7ab063 100644 --- a/graphics/java/android/graphics/pdf/PdfEditor.java +++ b/graphics/java/android/graphics/pdf/PdfEditor.java @@ -40,7 +40,7 @@ public final class PdfEditor { private final CloseGuard mCloseGuard = CloseGuard.get(); - private final long mNativeDocument; + private long mNativeDocument; private int mPageCount; @@ -78,12 +78,17 @@ public final class PdfEditor { } catch (ErrnoException ee) { throw new IllegalArgumentException("file descriptor not seekable"); } - mInput = input; synchronized (PdfRenderer.sPdfiumLock) { mNativeDocument = nativeOpen(mInput.getFd(), size); - mPageCount = nativeGetPageCount(mNativeDocument); + try { + mPageCount = nativeGetPageCount(mNativeDocument); + } catch (Throwable t) { + nativeClose(mNativeDocument); + mNativeDocument = 0; + throw t; + } } mCloseGuard.open("close"); @@ -275,20 +280,24 @@ public final class PdfEditor { mCloseGuard.warnIfOpen(); } - if (mInput != null) { - doClose(); - } + doClose(); } finally { super.finalize(); } } private void doClose() { - synchronized (PdfRenderer.sPdfiumLock) { - nativeClose(mNativeDocument); + if (mNativeDocument != 0) { + synchronized (PdfRenderer.sPdfiumLock) { + nativeClose(mNativeDocument); + } + mNativeDocument = 0; + } + + if (mInput != null) { + IoUtils.closeQuietly(mInput); + mInput = null; } - IoUtils.closeQuietly(mInput); - mInput = null; mCloseGuard.close(); } diff --git a/graphics/java/android/graphics/pdf/PdfRenderer.java b/graphics/java/android/graphics/pdf/PdfRenderer.java index 29e1ea0fee5d..4a91705239c1 100644 --- a/graphics/java/android/graphics/pdf/PdfRenderer.java +++ b/graphics/java/android/graphics/pdf/PdfRenderer.java @@ -31,6 +31,8 @@ import android.system.OsConstants; import com.android.internal.util.Preconditions; import dalvik.system.CloseGuard; +import libcore.io.IoUtils; + import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -110,7 +112,7 @@ public final class PdfRenderer implements AutoCloseable { private final Point mTempPoint = new Point(); - private final long mNativeDocument; + private long mNativeDocument; private final int mPageCount; @@ -159,7 +161,6 @@ public final class PdfRenderer implements AutoCloseable { } catch (ErrnoException ee) { throw new IllegalArgumentException("file descriptor not seekable"); } - mInput = input; synchronized (sPdfiumLock) { @@ -168,6 +169,7 @@ public final class PdfRenderer implements AutoCloseable { mPageCount = nativeGetPageCount(mNativeDocument); } catch (Throwable t) { nativeClose(mNativeDocument); + mNativeDocument = 0; throw t; } } @@ -234,9 +236,7 @@ public final class PdfRenderer implements AutoCloseable { mCloseGuard.warnIfOpen(); } - if (mInput != null) { - doClose(); - } + doClose(); } finally { super.finalize(); } @@ -245,16 +245,20 @@ public final class PdfRenderer implements AutoCloseable { private void doClose() { if (mCurrentPage != null) { mCurrentPage.close(); + mCurrentPage = null; } - synchronized (sPdfiumLock) { - nativeClose(mNativeDocument); + + if (mNativeDocument != 0) { + synchronized (sPdfiumLock) { + nativeClose(mNativeDocument); + } + mNativeDocument = 0; } - try { - mInput.close(); - } catch (IOException ioe) { - /* ignore - best effort */ + + if (mInput != null) { + IoUtils.closeQuietly(mInput); + mInput = null; } - mInput = null; mCloseGuard.close(); } @@ -451,19 +455,20 @@ public final class PdfRenderer implements AutoCloseable { mCloseGuard.warnIfOpen(); } - if (mNativePage != 0) { - doClose(); - } + doClose(); } finally { super.finalize(); } } private void doClose() { - synchronized (sPdfiumLock) { - nativeClosePage(mNativePage); + if (mNativePage != 0) { + synchronized (sPdfiumLock) { + nativeClosePage(mNativePage); + } + mNativePage = 0; } - mNativePage = 0; + mCloseGuard.close(); mCurrentPage = null; } |