Refactor EmojiCompat.init(Context)

Added benchmark to run through the entire code path in
EmojiCompat.init(Context).

Change:
benchmark:         1,032 ns           9 allocs    CachedEmojiCompatInitBenchmark.actualEmojiCompatContextInit_fromQuickFactory
benchmark:         1,121 ns           9 allocs    CachedEmojiCompatInitBenchmark.actualEmojiCompatContextInit_fromQuickFactory_newVersion

Benchmark has high variance and it is unclear if this caused a change.
Even if it did bump, 70ns is acceptable.

No change to early-exit benchmarks.

Test: New benchmark
Bug: b/178035684
Change-Id: I1b43acf2eac5632375f534fa68b2496302d581fd
diff --git a/emoji2/emoji2-benchmark/build.gradle b/emoji2/emoji2-benchmark/build.gradle
index b632e87..c3dd0be 100644
--- a/emoji2/emoji2-benchmark/build.gradle
+++ b/emoji2/emoji2-benchmark/build.gradle
@@ -34,6 +34,9 @@
     androidTestImplementation(ANDROIDX_TEST_CORE)
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
     androidTestImplementation(ANDROIDX_TEST_RULES)
+    androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation project(':internal-testutils-runtime')
     androidTestImplementation(KOTLIN_STDLIB)
 }
 
diff --git a/emoji2/emoji2-benchmark/src/androidTest/java/androidx/emoji2/benchmark/text/CachedEmojiCompatInitBenchmark.kt b/emoji2/emoji2-benchmark/src/androidTest/java/androidx/emoji2/benchmark/text/CachedEmojiCompatInitBenchmark.kt
index bfe89c1..ea969a4 100644
--- a/emoji2/emoji2-benchmark/src/androidTest/java/androidx/emoji2/benchmark/text/CachedEmojiCompatInitBenchmark.kt
+++ b/emoji2/emoji2-benchmark/src/androidTest/java/androidx/emoji2/benchmark/text/CachedEmojiCompatInitBenchmark.kt
@@ -19,10 +19,12 @@
 import android.content.Context
 import androidx.benchmark.junit4.BenchmarkRule
 import androidx.benchmark.junit4.measureRepeated
+import androidx.emoji2.text.DefaultEmojiCompatConfig
 import androidx.emoji2.text.EmojiCompat
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
+import org.junit.Assert.assertNotNull
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -47,4 +49,42 @@
             EmojiCompat.init(context)
         }
     }
+
+    @Test
+    fun cachedEmojiCompatInit_returningNonNull() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        val config = NoFontTestEmojiConfig.emptyConfig()
+            .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL)
+        EmojiCompat.reset(config)
+        EmojiCompat.skipDefaultConfigurationLookup(true)
+        benchmarkRule.measureRepeated {
+            EmojiCompat.init(context)
+        }
+    }
+
+    @Test
+    fun actualEmojiCompatContextInit_fromQuickFactory() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        val config = NoFontTestEmojiConfig.emptyConfig()
+            .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL)
+        val factory = TestEmojiCompatConfigFactory(config)
+
+        benchmarkRule.measureRepeated {
+            runWithTimingDisabled {
+                EmojiCompat.reset(null as EmojiCompat?)
+                EmojiCompat.skipDefaultConfigurationLookup(false)
+            }
+            val result = EmojiCompat.init(context, factory)
+            runWithTimingDisabled {
+                assertNotNull(result)
+            }
+        }
+    }
+
+    class TestEmojiCompatConfigFactory(private val config: EmojiCompat.Config) :
+        DefaultEmojiCompatConfig.DefaultEmojiCompatConfigFactory(null) {
+        override fun create(context: Context): EmojiCompat.Config {
+            return config
+        }
+    }
 }
diff --git a/emoji2/emoji2-benchmark/src/androidTest/java/androidx/emoji2/benchmark/text/NoFontTestEmojiConfig.java b/emoji2/emoji2-benchmark/src/androidTest/java/androidx/emoji2/benchmark/text/NoFontTestEmojiConfig.java
new file mode 100644
index 0000000..f706f84
--- /dev/null
+++ b/emoji2/emoji2-benchmark/src/androidTest/java/androidx/emoji2/benchmark/text/NoFontTestEmojiConfig.java
@@ -0,0 +1,59 @@
+/*
+ * 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.benchmark.text;
+
+import static org.mockito.Mockito.mock;
+
+import android.graphics.Typeface;
+
+import androidx.annotation.NonNull;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.MetadataRepo;
+
+public class NoFontTestEmojiConfig extends EmojiCompat.Config {
+
+    static EmojiCompat.Config emptyConfig() {
+        return new NoFontTestEmojiConfig(new EmptyEmojiDataLoader());
+    }
+
+    static EmojiCompat.Config neverLoadsConfig() {
+        return new NoFontTestEmojiConfig(new NeverCompletesMetadataRepoLoader());
+    }
+
+    static EmojiCompat.Config fromLoader(EmojiCompat.MetadataRepoLoader loader) {
+        return new NoFontTestEmojiConfig(loader);
+    }
+
+    private NoFontTestEmojiConfig(EmojiCompat.MetadataRepoLoader loader) {
+        super(loader);
+    }
+
+    private static class EmptyEmojiDataLoader implements EmojiCompat.MetadataRepoLoader {
+        @Override
+        public void load(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+            loaderCallback.onLoaded(MetadataRepo.create(mock(Typeface.class)));
+        }
+    }
+
+    private static class NeverCompletesMetadataRepoLoader
+            implements EmojiCompat.MetadataRepoLoader {
+        @Override
+        public void load(@NonNull final EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+            // do nothing, this will be called on the test thread and is a no-op
+        }
+    }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
index 4ab73f9..94ecef2 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
@@ -199,10 +199,11 @@
     static final int EMOJI_COUNT_UNLIMITED = Integer.MAX_VALUE;
 
     private static final Object INSTANCE_LOCK = new Object();
+    private static final Object CONFIG_LOCK = new Object();
 
     @GuardedBy("INSTANCE_LOCK")
     private static volatile @Nullable EmojiCompat sInstance;
-    @GuardedBy("INSTANCE_LOCK")
+    @GuardedBy("CONFIG_LOCK")
     private static volatile boolean sHasDoneDefaultConfigLookup;
 
     private final @NonNull ReadWriteLock mInitLock;
@@ -323,25 +324,27 @@
     @SuppressWarnings("GuardedBy") /* double-check lock; volatile; threadsafe obj */
     public static EmojiCompat init(@NonNull Context context,
             @Nullable DefaultEmojiCompatConfig.DefaultEmojiCompatConfigFactory defaultFactory) {
-        if (sInstance != null || sHasDoneDefaultConfigLookup) {
+        EmojiCompat.Config config;
+        if (sHasDoneDefaultConfigLookup) {
             // sInstance is safe to return outside the lock because
             // 1) static fields are volatile
             // 2) all fields on EmojiCompat are final, or guarded by a lock
+            // 3) we only write this after sInstance is settled by the call to `init`
             return sInstance;
+        } else {
+            DefaultEmojiCompatConfig.DefaultEmojiCompatConfigFactory factory =
+                    defaultFactory != null ? defaultFactory :
+                            new DefaultEmojiCompatConfig.DefaultEmojiCompatConfigFactory(null);
+            config = factory.create(context);
         }
-        synchronized (INSTANCE_LOCK) {
-            if (sInstance == null && !sHasDoneDefaultConfigLookup) {
+        synchronized (CONFIG_LOCK) {
+            if (!sHasDoneDefaultConfigLookup) {
                 // sDefaultConfigLookup allows us to early-exit above, as well as avoid repeated
                 // calls to create in the case where the font provider is not found
-
-                DefaultEmojiCompatConfig.DefaultEmojiCompatConfigFactory factory =
-                        defaultFactory != null ? defaultFactory :
-                                new DefaultEmojiCompatConfig.DefaultEmojiCompatConfigFactory(null);
-                EmojiCompat.Config config = factory.create(context);
                 if (config != null) {
-                    sInstance = new EmojiCompat(config);
+                    init(config);
                 }
-                // write this after sInstance to allow early-exit
+                // write this after init to allow safe early-exit
                 sHasDoneDefaultConfigLookup = true;
 
             }
@@ -412,7 +415,7 @@
      */
     @RestrictTo(TESTS)
     public static void skipDefaultConfigurationLookup(boolean shouldSkip) {
-        synchronized (INSTANCE_LOCK) {
+        synchronized (CONFIG_LOCK) {
             sHasDoneDefaultConfigLookup = shouldSkip;
         }
     }