public class

FontRequestEmojiCompatConfig

extends EmojiCompat.Config

 java.lang.Object

androidx.emoji2.text.EmojiCompat.Config

↳androidx.emoji2.text.FontRequestEmojiCompatConfig

Gradle dependencies

compile group: 'androidx.emoji2', name: 'emoji2', version: '1.2.0-alpha04'

  • groupId: androidx.emoji2
  • artifactId: emoji2
  • version: 1.2.0-alpha04

Artifact androidx.emoji2:emoji2:1.2.0-alpha04 it located at Google repository (https://maven.google.com/)

Overview

EmojiCompat.Config implementation that asynchronously fetches the required font and the metadata using a FontRequest. FontRequest should be constructed to fetch an EmojiCompat compatible emoji font.

Summary

Constructors
publicFontRequestEmojiCompatConfig(Context context, FontRequest request)

publicFontRequestEmojiCompatConfig(Context context, FontRequest request, FontRequestEmojiCompatConfig.FontProviderHelper fontProviderHelper)

Methods
public FontRequestEmojiCompatConfigsetHandler(Handler handler)

Please us FontRequestEmojiCompatConfig.setLoadingExecutor(Executor) instead to set background loading thread.

public FontRequestEmojiCompatConfigsetLoadingExecutor(java.util.concurrent.Executor executor)

Sets the custom executor to be used for initialization.

public FontRequestEmojiCompatConfigsetRetryPolicy(FontRequestEmojiCompatConfig.RetryPolicy policy)

Sets the retry policy.

from EmojiCompat.ConfiggetMetadataRepoLoader, registerInitCallback, setEmojiSpanIndicatorColor, setEmojiSpanIndicatorEnabled, setGlyphChecker, setMetadataLoadStrategy, setReplaceAll, setUseEmojiAsDefaultStyle, setUseEmojiAsDefaultStyle, unregisterInitCallback
from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public FontRequestEmojiCompatConfig(Context context, FontRequest request)

Parameters:

context: Context instance, cannot be null
request: FontRequest to fetch the font asynchronously, cannot be null

public FontRequestEmojiCompatConfig(Context context, FontRequest request, FontRequestEmojiCompatConfig.FontProviderHelper fontProviderHelper)

Methods

public FontRequestEmojiCompatConfig setLoadingExecutor(java.util.concurrent.Executor executor)

Sets the custom executor to be used for initialization. Since font loading is too slow for the main thread, the metadata loader will fetch the fonts on a background thread. By default, FontRequestEmojiCompatConfig will create its own single threaded Executor, which causes a thread to be created. You can pass your own executor to control which thread the font is loaded on, and avoid an extra thread creation.

Parameters:

executor: background executor for performing font load

public FontRequestEmojiCompatConfig setHandler(Handler handler)

Deprecated: please call setLoadingExecutor instead

Please us FontRequestEmojiCompatConfig.setLoadingExecutor(Executor) instead to set background loading thread. This was deprecated in emoji2 1.0.0-alpha04. If migrating from androidx.emoji please prefer to use an existing background executor for setLoadingExecutor. Note: This method will no longer have any effect if passed null, which is a breaking change from androidx.emoji.

Parameters:

handler: background thread handler to wrap in an Executor, if null this method will do nothing

Sets the retry policy. FontRequestEmojiCompatConfig.RetryPolicy

Parameters:

policy: The policy to be used when the font provider is not ready to give the font file. Can be null. In case of null, the metadata loader never retries.

Source

/*
 * Copyright 2021 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 androidx.emoji2.text;

import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.ContentObserver;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Handler;
import android.os.SystemClock;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.WorkerThread;
import androidx.core.graphics.TypefaceCompatUtil;
import androidx.core.os.TraceCompat;
import androidx.core.provider.FontRequest;
import androidx.core.provider.FontsContractCompat;
import androidx.core.provider.FontsContractCompat.FontFamilyResult;
import androidx.core.util.Preconditions;

import java.nio.ByteBuffer;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * {@link EmojiCompat.Config} implementation that asynchronously fetches the required font and the
 * metadata using a {@link FontRequest}. FontRequest should be constructed to fetch an EmojiCompat
 * compatible emoji font.
 * <p/>
 */
public class FontRequestEmojiCompatConfig extends EmojiCompat.Config {

    /**
     * Retry policy used when the font provider is not ready to give the font file.
     *
     * To control the thread the retries are handled on, see
     * {@link FontRequestEmojiCompatConfig#setLoadingExecutor}.
     */
    public abstract static class RetryPolicy {
        /**
         * Called each time the metadata loading fails.
         *
         * This is primarily due to a pending download of the font.
         * If a value larger than zero is returned, metadata loader will retry after the given
         * milliseconds.
         * <br />
         * If {@code zero} is returned, metadata loader will retry immediately.
         * <br/>
         * If a value less than 0 is returned, the metadata loader will stop retrying and
         * EmojiCompat will get into {@link EmojiCompat#LOAD_STATE_FAILED} state.
         * <p/>
         * Note that the retry may happen earlier than you specified if the font provider notifies
         * that the download is completed.
         *
         * @return long milliseconds to wait until next retry
         */
        public abstract long getRetryDelay();
    }

    /**
     * A retry policy implementation that doubles the amount of time in between retries.
     *
     * If downloading hasn't finish within given amount of time, this policy give up and the
     * EmojiCompat will get into {@link EmojiCompat#LOAD_STATE_FAILED} state.
     */
    public static class ExponentialBackoffRetryPolicy extends RetryPolicy {
        private final long mTotalMs;
        private long mRetryOrigin;

        /**
         * @param totalMs A total amount of time to wait in milliseconds.
         */
        public ExponentialBackoffRetryPolicy(long totalMs) {
            mTotalMs = totalMs;
        }

        @Override
        public long getRetryDelay() {
            if (mRetryOrigin == 0) {
                mRetryOrigin = SystemClock.uptimeMillis();
                // Since download may be completed after getting query result and before registering
                // observer, requesting later at the same time.
                return 0;
            } else {
                // Retry periodically since we can't trust notify change event. Some font provider
                // may not notify us.
                final long elapsedMillis = SystemClock.uptimeMillis() - mRetryOrigin;
                if (elapsedMillis > mTotalMs) {
                    return -1;  // Give up since download hasn't finished in 10 min.
                }
                // Wait until the same amount of the time from the first scheduled time, but adjust
                // the minimum request interval is 1 sec and never exceeds 10 min in total.
                return Math.min(Math.max(elapsedMillis, 1000), mTotalMs - elapsedMillis);
            }
        }
    }

    /**
     * @param context Context instance, cannot be {@code null}
     * @param request {@link FontRequest} to fetch the font asynchronously, cannot be {@code null}
     */
    public FontRequestEmojiCompatConfig(@NonNull Context context, @NonNull FontRequest request) {
        super(new FontRequestMetadataLoader(context, request, DEFAULT_FONTS_CONTRACT));
    }

    /**
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public FontRequestEmojiCompatConfig(@NonNull Context context, @NonNull FontRequest request,
            @NonNull FontProviderHelper fontProviderHelper) {
        super(new FontRequestMetadataLoader(context, request, fontProviderHelper));
    }

    /**
     * Sets the custom executor to be used for initialization.
     *
     * Since font loading is too slow for the main thread, the metadata loader will fetch the fonts
     * on a background thread. By default, FontRequestEmojiCompatConfig will create its own
     * single threaded Executor, which causes a thread to be created.
     *
     * You can pass your own executor to control which thread the font is loaded on, and avoid an
     * extra thread creation.
     *
     * @param executor background executor for performing font load
     */
    @NonNull
    public FontRequestEmojiCompatConfig setLoadingExecutor(@NonNull Executor executor) {
        ((FontRequestMetadataLoader) getMetadataRepoLoader()).setExecutor(executor);
        return this;
    }

    /**
     * Please us {@link #setLoadingExecutor(Executor)} instead to set background loading thread.
     *
     * This was deprecated in emoji2 1.0.0-alpha04.
     *
     * If migrating from androidx.emoji please prefer to use an existing background executor for
     * setLoadingExecutor.
     *
     * Note: This method will no longer have any effect if passed null, which is a breaking
     * change from androidx.emoji.
     *
     * @deprecated please call setLoadingExecutor instead
     *
     * @param handler background thread handler to wrap in an Executor, if null this method will
     *                do nothing
     */
    @Deprecated
    @NonNull
    @SuppressWarnings("deprecation")
    public FontRequestEmojiCompatConfig setHandler(@Nullable Handler handler) {
        if (handler == null) {
            // this is a breaking behavior change from androidx.emoji, we no longer support
            // clearing executors
            return this;
        }
        setLoadingExecutor(ConcurrencyHelpers.convertHandlerToExecutor(handler));
        return this;
    }

    /**
     * Sets the retry policy.
     *
     * {@see RetryPolicy}
     * @param policy The policy to be used when the font provider is not ready to give the font
     *              file. Can be {@code null}. In case of {@code null}, the metadata loader never
     *              retries.
     */
    @NonNull
    public FontRequestEmojiCompatConfig setRetryPolicy(@Nullable RetryPolicy policy) {
        ((FontRequestMetadataLoader) getMetadataRepoLoader()).setRetryPolicy(policy);
        return this;
    }

    /**
     * MetadataRepoLoader implementation that uses FontsContractCompat and TypefaceCompat to load a
     * given FontRequest.
     */
    private static class FontRequestMetadataLoader implements EmojiCompat.MetadataRepoLoader {
        private static final String S_TRACE_BUILD_TYPEFACE =
                "EmojiCompat.FontRequestEmojiCompatConfig.buildTypeface";
        @NonNull
        private final Context mContext;
        @NonNull
        private final FontRequest mRequest;
        @NonNull
        private final FontProviderHelper mFontProviderHelper;
        @NonNull
        private final Object mLock = new Object();

        @GuardedBy("mLock")
        @Nullable
        private Handler mMainHandler;
        @GuardedBy("mLock")
        @Nullable
        private Executor mExecutor;
        @GuardedBy("mLock")
        @Nullable
        private ThreadPoolExecutor mMyThreadPoolExecutor;
        @GuardedBy("mLock")
        @Nullable
        private RetryPolicy mRetryPolicy;

        @GuardedBy("mLock")
        @Nullable
        EmojiCompat.MetadataRepoLoaderCallback mCallback;
        @GuardedBy("mLock")
        @Nullable
        private ContentObserver mObserver;
        @GuardedBy("mLock")
        @Nullable
        private Runnable mMainHandlerLoadCallback;

        FontRequestMetadataLoader(@NonNull Context context, @NonNull FontRequest request,
                @NonNull FontProviderHelper fontProviderHelper) {
            Preconditions.checkNotNull(context, "Context cannot be null");
            Preconditions.checkNotNull(request, "FontRequest cannot be null");
            mContext = context.getApplicationContext();
            mRequest = request;
            mFontProviderHelper = fontProviderHelper;
        }

        public void setExecutor(@NonNull Executor executor) {
            synchronized (mLock) {
                mExecutor = executor;
            }
        }

        public void setRetryPolicy(@Nullable RetryPolicy policy) {
            synchronized (mLock) {
                mRetryPolicy = policy;
            }
        }

        @Override
        @RequiresApi(19)
        public void load(@NonNull final EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
            Preconditions.checkNotNull(loaderCallback, "LoaderCallback cannot be null");
            synchronized (mLock) {
                mCallback = loaderCallback;
            }
            loadInternal();
        }

        @RequiresApi(19)
        void loadInternal() {
            synchronized (mLock) {
                if (mCallback == null) {
                    // do nothing; loading is already complete
                    return;
                }
                if (mExecutor == null) {
                    mMyThreadPoolExecutor = ConcurrencyHelpers.createBackgroundPriorityExecutor(
                            "emojiCompat");
                    mExecutor = mMyThreadPoolExecutor;
                }
                mExecutor.execute(this::createMetadata);
            }
        }

        @WorkerThread
        private FontsContractCompat.FontInfo retrieveFontInfo() {
            final FontsContractCompat.FontFamilyResult result;
            try {
                result = mFontProviderHelper.fetchFonts(mContext, mRequest);
            } catch (NameNotFoundException e) {
                throw new RuntimeException("provider not found", e);
            }
            if (result.getStatusCode() != FontsContractCompat.FontFamilyResult.STATUS_OK) {
                throw new RuntimeException("fetchFonts failed (" + result.getStatusCode() + ")");
            }
            final FontsContractCompat.FontInfo[] fonts = result.getFonts();
            if (fonts == null || fonts.length == 0) {
                throw new RuntimeException("fetchFonts failed (empty result)");
            }
            return fonts[0];  // Assuming the GMS Core provides only one font file.
        }

        @RequiresApi(19)
        @WorkerThread
        private void scheduleRetry(Uri uri, long waitMs) {
            synchronized (mLock) {
                Handler handler = mMainHandler;
                if (handler == null) {
                    handler = ConcurrencyHelpers.mainHandlerAsync();
                    mMainHandler = handler;
                }
                if (mObserver == null) {
                    mObserver = new ContentObserver(handler) {
                        @Override
                        public void onChange(boolean selfChange, Uri uri) {
                            loadInternal();
                        }
                    };
                    mFontProviderHelper.registerObserver(mContext, uri, mObserver);
                }
                if (mMainHandlerLoadCallback == null) {
                    mMainHandlerLoadCallback = this::loadInternal;
                }
                handler.postDelayed(mMainHandlerLoadCallback, waitMs);
            }
        }

        // Must be called on the mHandler.
        private void cleanUp() {
            synchronized (mLock) {
                mCallback = null;
                if (mObserver != null) {
                    mFontProviderHelper.unregisterObserver(mContext, mObserver);
                    mObserver = null;
                }
                if (mMainHandler != null) {
                    mMainHandler.removeCallbacks(mMainHandlerLoadCallback);
                }
                mMainHandler = null;
                if (mMyThreadPoolExecutor != null) {
                    // if we made the executor, shut it down
                    mMyThreadPoolExecutor.shutdown();
                }
                mExecutor = null;
                mMyThreadPoolExecutor = null;
            }
        }

        // Must be called on the mHandler.
        @RequiresApi(19)
        @SuppressWarnings("WeakerAccess") /* synthetic access */
        @WorkerThread
        void createMetadata() {
            synchronized (mLock) {
                if (mCallback == null) {
                    return;  // Already handled or cancelled. Do nothing.
                }
            }
            try {
                final FontsContractCompat.FontInfo font = retrieveFontInfo();

                final int resultCode = font.getResultCode();
                if (resultCode == FontsContractCompat.Columns.RESULT_CODE_FONT_UNAVAILABLE) {
                    // The font provider is now downloading. Ask RetryPolicy for when to retry next.
                    synchronized (mLock) {
                        if (mRetryPolicy != null) {
                            final long delayMs = mRetryPolicy.getRetryDelay();
                            if (delayMs >= 0) {
                                scheduleRetry(font.getUri(), delayMs);
                                return;
                            }
                        }
                    }
                }

                if (resultCode != FontsContractCompat.Columns.RESULT_CODE_OK) {
                    throw new RuntimeException("fetchFonts result is not OK. (" + resultCode + ")");
                }

                final MetadataRepo metadataRepo;
                try {
                    TraceCompat.beginSection(S_TRACE_BUILD_TYPEFACE);
                    // TODO: Good to add new API to create Typeface from FD not to open FD twice.
                    final Typeface typeface = mFontProviderHelper.buildTypeface(mContext, font);
                    final ByteBuffer buffer = TypefaceCompatUtil.mmap(mContext, null,
                            font.getUri());
                    if (buffer == null || typeface == null) {
                        throw new RuntimeException("Unable to open file.");
                    }
                    metadataRepo = MetadataRepo.create(typeface, buffer);
                } finally {
                    TraceCompat.endSection();
                }
                synchronized (mLock) {
                    if (mCallback != null) {
                        mCallback.onLoaded(metadataRepo);
                    }
                }
                cleanUp();
            } catch (Throwable t) {
                synchronized (mLock) {
                    if (mCallback != null) {
                        mCallback.onFailed(t);
                    }
                }
                cleanUp();
            }
        }
    }

    /**
     * Delegate class for mocking FontsContractCompat.fetchFonts.
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static class FontProviderHelper {
        /** Calls FontsContractCompat.fetchFonts. */
        @NonNull
        public FontFamilyResult fetchFonts(@NonNull Context context,
                @NonNull FontRequest request) throws NameNotFoundException {
            return FontsContractCompat.fetchFonts(context, null /* cancellation signal */, request);
        }

        /** Calls FontsContractCompat.buildTypeface. */
        @Nullable
        public Typeface buildTypeface(@NonNull Context context,
                @NonNull FontsContractCompat.FontInfo font) throws NameNotFoundException {
            return FontsContractCompat.buildTypeface(context, null /* cancellation signal */,
                new FontsContractCompat.FontInfo[] { font });
        }

        /** Calls Context.getContentObserver().registerObserver */
        public void registerObserver(@NonNull Context context, @NonNull Uri uri,
                @NonNull ContentObserver observer) {
            context.getContentResolver().registerContentObserver(
                    uri, false /* notifyForDescendants */, observer);

        }
        /** Calls Context.getContentObserver().unregisterObserver */
        public void unregisterObserver(@NonNull Context context,
                @NonNull ContentObserver observer) {
            context.getContentResolver().unregisterContentObserver(observer);
        }
    }

    private static final FontProviderHelper DEFAULT_FONTS_CONTRACT = new FontProviderHelper();

}