/* * 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.os.Build; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.WorkerThread; import androidx.core.os.TraceCompat; import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ProcessLifecycleInitializer; import androidx.startup.AppInitializer; import androidx.startup.Initializer; import java.util.Collections; import java.util.List; import java.util.concurrent.ThreadPoolExecutor; /** * Initializer for configuring EmojiCompat with the system installed downloadable font provider. * *

This initializer will initialize EmojiCompat immediately then defer loading the font for a * short delay to avoid delaying application startup. Typically, the font will be loaded shortly * after the first screen of your application loads, which means users may see system emoji * briefly prior to the compat font loading.

* *

This is the recommended configuration for all apps that don't need specialized configuration, * and don't need to control the background thread that initialization runs on. For more information * see {@link androidx.emoji2.text.DefaultEmojiCompatConfig}.

* *

In addition to the reasons listed in {@code DefaultEmojiCompatConfig} you may wish to disable * this automatic configuration if you intend to call initialization from an existing background * thread pool in your application.

* *

This is enabled by default by including the {@code :emoji2:emoji2} gradle artifact. To * disable the default configuration (and allow manual configuration) add this to your manifest:

* *
 *     <provider
 *         android:name="androidx.startup.InitializationProvider"
 *         android:authorities="${applicationId}.androidx-startup"
 *         android:exported="false"
 *         tools:node="merge">
 *         <meta-data android:name="androidx.emoji2.text.EmojiCompatInitializer"
 *                   tools:node="remove" />
 *     </provider>
 * 
* * This initializer depends on {@link ProcessLifecycleInitializer}. * * @see androidx.emoji2.text.DefaultEmojiCompatConfig */ public class EmojiCompatInitializer implements Initializer { private static final long STARTUP_THREAD_CREATION_DELAY_MS = 500L; private static final String S_INITIALIZER_THREAD_NAME = "EmojiCompatInitializer"; /** * Initialize EmojiCompat with the app's context. * * @param context application context * @return result of default init */ @SuppressWarnings("AutoBoxing") @NonNull @Override public Boolean create(@NonNull Context context) { if (Build.VERSION.SDK_INT >= 19) { EmojiCompat.init(new BackgroundDefaultConfig(context)); delayUntilFirstResume(context); return true; } return false; } /** * Wait until the first frame of the application to do anything. * * This allows startup code to run before the delay is scheduled. */ @RequiresApi(19) void delayUntilFirstResume(@NonNull Context context) { // schedule delay after first Activity resumes AppInitializer appInitializer = AppInitializer.getInstance(context); LifecycleOwner lifecycleOwner = appInitializer .initializeComponent(ProcessLifecycleInitializer.class); Lifecycle lifecycle = lifecycleOwner.getLifecycle(); lifecycle.addObserver(new DefaultLifecycleObserver() { @Override public void onResume(@NonNull LifecycleOwner owner) { loadEmojiCompatAfterDelay(); lifecycle.removeObserver(this); } }); } @RequiresApi(19) void loadEmojiCompatAfterDelay() { final Handler mainHandler = ConcurrencyHelpers.mainHandlerAsync(); mainHandler.postDelayed(new LoadEmojiCompatRunnable(), STARTUP_THREAD_CREATION_DELAY_MS); } /** * Dependes on ProcessLifecycleInitializer */ @NonNull @Override public List>> dependencies() { return Collections.singletonList(ProcessLifecycleInitializer.class); } static class LoadEmojiCompatRunnable implements Runnable { @Override public void run() { try { // this is main thread, so mark what we're doing (this trace includes thread // start time in BackgroundLoadingLoader.load TraceCompat.beginSection("EmojiCompat.EmojiCompatInitializer.run"); if (EmojiCompat.isConfigured()) { EmojiCompat.get().load(); } } finally { TraceCompat.endSection(); } } } @RequiresApi(19) static class BackgroundDefaultConfig extends EmojiCompat.Config { protected BackgroundDefaultConfig(Context context) { super(new BackgroundDefaultLoader(context)); setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL); } } @RequiresApi(19) static class BackgroundDefaultLoader implements EmojiCompat.MetadataRepoLoader { private final Context mContext; BackgroundDefaultLoader(Context context) { mContext = context.getApplicationContext(); } @Override public void load(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback) { ThreadPoolExecutor executor = ConcurrencyHelpers.createBackgroundPriorityExecutor( S_INITIALIZER_THREAD_NAME); executor.execute(() -> doLoad(loaderCallback, executor)); } @WorkerThread void doLoad(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback, @NonNull ThreadPoolExecutor executor) { try { FontRequestEmojiCompatConfig config = DefaultEmojiCompatConfig.create(mContext); if (config == null) { throw new RuntimeException("EmojiCompat font provider not available on this " + "device."); } config.setLoadingExecutor(executor); config.getMetadataRepoLoader().load(new EmojiCompat.MetadataRepoLoaderCallback() { @Override public void onLoaded(@NonNull MetadataRepo metadataRepo) { try { // main thread is notified before returning, so we can quit now loaderCallback.onLoaded(metadataRepo); } finally { executor.shutdown(); } } @Override public void onFailed(@Nullable Throwable throwable) { try { // main thread is notified before returning, so we can quit now loaderCallback.onFailed(throwable); } finally { executor.shutdown(); } } }); } catch (Throwable t) { loaderCallback.onFailed(t); executor.shutdown(); } } } }