Merge "Load animation drawable in worker threads" into oc-support-26.0-dev
diff --git a/api/26.0.0-SNAPSHOT.txt b/api/26.0.0-SNAPSHOT.txt
index 036ee52..0311835 100644
--- a/api/26.0.0-SNAPSHOT.txt
+++ b/api/26.0.0-SNAPSHOT.txt
@@ -1895,6 +1895,7 @@
     method public java.lang.CharSequence process(java.lang.CharSequence);
     method public java.lang.CharSequence process(java.lang.CharSequence, int, int);
     method public java.lang.CharSequence process(java.lang.CharSequence, int, int, int);
+    method public java.lang.CharSequence process(java.lang.CharSequence, int, int, int, int);
     method public void registerInitCallback(android.support.text.emoji.EmojiCompat.InitCallback);
     method public void unregisterInitCallback(android.support.text.emoji.EmojiCompat.InitCallback);
     field public static final java.lang.String EDITOR_INFO_METAVERSION_KEY = "android.support.text.emoji.emojiCompat_metadataVersion";
@@ -1902,6 +1903,9 @@
     field public static final int LOAD_STATE_FAILURE = 2; // 0x2
     field public static final int LOAD_STATE_LOADING = 0; // 0x0
     field public static final int LOAD_STATE_SUCCESS = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_ALL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int REPLACE_STRATEGY_NON_EXISTENT = 2; // 0x2
   }
 
   public static abstract class EmojiCompat.Config {
diff --git a/compat/java/android/support/v4/content/res/FontResourcesParserCompat.java b/compat/java/android/support/v4/content/res/FontResourcesParserCompat.java
index a7bb56f..8f12146 100644
--- a/compat/java/android/support/v4/content/res/FontResourcesParserCompat.java
+++ b/compat/java/android/support/v4/content/res/FontResourcesParserCompat.java
@@ -21,6 +21,7 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.support.annotation.ArrayRes;
+import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.RestrictTo;
@@ -34,6 +35,8 @@
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -47,6 +50,18 @@
     private static final int NORMAL_WEIGHT = 400;
     private static final int ITALIC = 1;
 
+    @IntDef({FETCH_STRATEGY_BLOCKING, FETCH_STRATEGY_ASYNC})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface FetchStrategy {}
+
+    public static final int FETCH_STRATEGY_BLOCKING = 0;
+    public static final int FETCH_STRATEGY_ASYNC = 1;
+
+    // A special timeout value for infinite blocking.
+    public static final int INFINITE_TIMEOUT_VALUE = -1;
+
+    private static final int DEFAULT_TIMEOUT_MILLIS = 500;
+
     /**
      * A class that represents a single entry of font-family in an xml file.
      */
@@ -57,14 +72,27 @@
      */
     public static final class ProviderResourceEntry implements FamilyResourceEntry {
         private final @NonNull FontRequest mRequest;
+        private final int mTimeoutMs;
+        private final @FetchStrategy int mStrategy;
 
-        public ProviderResourceEntry(@NonNull FontRequest request) {
+        public ProviderResourceEntry(@NonNull FontRequest request, @FetchStrategy int strategy,
+                int timeoutMs) {
             mRequest = request;
+            mStrategy = strategy;
+            mTimeoutMs = timeoutMs;
         }
 
         public @NonNull FontRequest getRequest() {
             return mRequest;
         }
+
+        public @FetchStrategy int getFetchStrategy() {
+            return mStrategy;
+        }
+
+        public int getTimeout() {
+            return mTimeoutMs;
+        }
     }
 
     /**
@@ -146,6 +174,10 @@
         String providerPackage = array.getString(R.styleable.FontFamily_fontProviderPackage);
         String query = array.getString(R.styleable.FontFamily_fontProviderQuery);
         int certsId = array.getResourceId(R.styleable.FontFamily_fontProviderCerts, 0);
+        int strategy = array.getInteger(R.styleable.FontFamily_fontProviderFetchStrategy,
+                FETCH_STRATEGY_ASYNC);
+        int timeoutMs = array.getInteger(R.styleable.FontFamily_fontProviderFetchTimeout,
+                DEFAULT_TIMEOUT_MILLIS);
         array.recycle();
         if (authority != null && providerPackage != null && query != null) {
             while (parser.next() != XmlPullParser.END_TAG) {
@@ -153,7 +185,7 @@
             }
             List<List<byte[]>> certs = readCerts(resources, certsId);
             return new ProviderResourceEntry(
-                    new FontRequest(authority, providerPackage, query, certs));
+                    new FontRequest(authority, providerPackage, query, certs), strategy, timeoutMs);
         }
         List<FontFileResourceEntry> fonts = new ArrayList<>();
         while (parser.next() != XmlPullParser.END_TAG) {
diff --git a/compat/java/android/support/v4/content/res/ResourcesCompat.java b/compat/java/android/support/v4/content/res/ResourcesCompat.java
index 9a4b258..526b79b 100644
--- a/compat/java/android/support/v4/content/res/ResourcesCompat.java
+++ b/compat/java/android/support/v4/content/res/ResourcesCompat.java
@@ -39,6 +39,7 @@
 import android.support.v4.graphics.TypefaceCompat;
 import android.util.Log;
 import android.util.TypedValue;
+import android.widget.TextView;
 
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -198,24 +199,24 @@
             // Use framework support.
             return context.getResources().getFont(id);
         }
-        return loadFont(context, id, new TypedValue(), Typeface.NORMAL);
+        return loadFont(context, id, new TypedValue(), Typeface.NORMAL, null);
     }
 
     /** @hide */
     @RestrictTo(LIBRARY_GROUP)
     public static Typeface getFont(@NonNull Context context, @FontRes int id, TypedValue value,
-            int style) throws NotFoundException {
+            int style, @Nullable TextView targetView) throws NotFoundException {
         if (context.isRestricted()) {
             return null;
         }
-        return loadFont(context, id, value, style);
+        return loadFont(context, id, value, style, targetView);
     }
 
     private static Typeface loadFont(@NonNull Context context, int id, TypedValue value,
-            int style) {
+            int style, @Nullable TextView targetView) {
         final Resources resources = context.getResources();
         resources.getValue(id, value, true);
-        Typeface typeface = loadFont(context, resources, value, id, style);
+        Typeface typeface = loadFont(context, resources, value, id, style, targetView);
         if (typeface != null) {
             return typeface;
         }
@@ -224,7 +225,8 @@
     }
 
     private static Typeface loadFont(
-            @NonNull Context context, Resources wrapper, TypedValue value, int id, int style) {
+            @NonNull Context context, Resources wrapper, TypedValue value, int id, int style,
+            @Nullable TextView targetView) {
         if (value.string == null) {
             throw new NotFoundException("Resource \"" + wrapper.getResourceName(id) + "\" ("
                     + Integer.toHexString(id) + ") is not a Font: " + value);
@@ -251,7 +253,7 @@
                     return null;
                 }
                 return TypefaceCompat.createFromResourcesFamilyXml(
-                        context, familyEntry, wrapper, id, style);
+                        context, familyEntry, wrapper, id, style, targetView);
             }
             return TypefaceCompat.createFromResourcesFontFile(context, wrapper, id, style);
         } catch (XmlPullParserException e) {
diff --git a/compat/java/android/support/v4/graphics/TypefaceCompat.java b/compat/java/android/support/v4/graphics/TypefaceCompat.java
index 444616a..e9b2edd 100644
--- a/compat/java/android/support/v4/graphics/TypefaceCompat.java
+++ b/compat/java/android/support/v4/graphics/TypefaceCompat.java
@@ -31,6 +31,7 @@
 import android.support.v4.provider.FontsContractCompat;
 import android.support.v4.provider.FontsContractCompat.FontInfo;
 import android.support.v4.util.LruCache;
+import android.widget.TextView;
 
 import java.nio.ByteBuffer;
 import java.util.Map;
@@ -92,11 +93,14 @@
      * @return null if failed to create.
      */
     public static Typeface createFromResourcesFamilyXml(
-            Context context, FamilyResourceEntry entry, Resources resources, int id, int style) {
+            Context context, FamilyResourceEntry entry, Resources resources, int id, int style,
+            @Nullable TextView targetView) {
         Typeface typeface;
         if (entry instanceof ProviderResourceEntry) {
+            ProviderResourceEntry providerEntry = (ProviderResourceEntry) entry;
             typeface = FontsContractCompat.getFontSync(context,
-                    ((ProviderResourceEntry) entry).getRequest());
+                    providerEntry.getRequest(), targetView, providerEntry.getFetchStrategy(),
+                    providerEntry.getTimeout(), style);
         } else {
             typeface = sTypefaceCompatImpl.createFromFontFamilyFilesResourceEntry(
                     context, (FontFamilyFilesResourceEntry) entry, resources, id, style);
diff --git a/compat/java/android/support/v4/provider/FontsContractCompat.java b/compat/java/android/support/v4/provider/FontsContractCompat.java
index c465a4c..ba44019 100644
--- a/compat/java/android/support/v4/provider/FontsContractCompat.java
+++ b/compat/java/android/support/v4/provider/FontsContractCompat.java
@@ -17,6 +17,7 @@
 package android.support.v4.provider;
 
 import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static android.support.v4.content.res.FontResourcesParserCompat.FetchStrategy;
 
 import android.content.ContentResolver;
 import android.content.ContentUris;
@@ -35,6 +36,7 @@
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
 import android.provider.BaseColumns;
+import android.support.annotation.GuardedBy;
 import android.support.annotation.IntDef;
 import android.support.annotation.IntRange;
 import android.support.annotation.NonNull;
@@ -43,8 +45,11 @@
 import android.support.annotation.VisibleForTesting;
 import android.support.v4.content.res.FontResourcesParserCompat;
 import android.support.v4.graphics.TypefaceCompat;
+import android.support.v4.provider.SelfDestructiveThread.ReplyCallback;
 import android.support.v4.util.LruCache;
 import android.support.v4.util.Preconditions;
+import android.support.v4.util.SimpleArrayMap;
+import android.widget.TextView;
 
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -173,31 +178,91 @@
             new SelfDestructiveThread("fonts", Process.THREAD_PRIORITY_BACKGROUND,
                     BACKGROUND_THREAD_KEEP_ALIVE_DURATION_MS);
 
+    private static Typeface getFontInternal(final Context context, final FontRequest request) {
+        FontFamilyResult result;
+        try {
+            result = fetchFonts(context, null /* CancellationSignal */, request);
+        } catch (PackageManager.NameNotFoundException e) {
+            return null;
+        }
+        if (result.getStatusCode() == FontFamilyResult.STATUS_OK) {
+            return buildTypeface(context, null /* CancellationSignal */, result.getFonts());
+        }
+        return null;
+    }
+
+    private static final Object sLock = new Object();
+    @GuardedBy("sLock")
+    private static final SimpleArrayMap<String, ArrayList<ReplyCallback<Typeface>>>
+            sPendingReplies = new SimpleArrayMap<>();
+
     /** @hide */
     @RestrictTo(LIBRARY_GROUP)
-    public static Typeface getFontSync(final Context context, final FontRequest request) {
+    public static Typeface getFontSync(final Context context, final FontRequest request,
+            final TextView targetView, @FetchStrategy int strategy, int timeout, final int style) {
         final String id = request.getIdentifier();
         Typeface cached = sTypefaceCache.get(id);
         if (cached != null) {
             return cached;
         }
 
-        try {
-            return sBackgroundThread.postAndWait(new Callable<Typeface>() {
+        final boolean isBlockingFetch =
+                strategy == FontResourcesParserCompat.FETCH_STRATEGY_BLOCKING;
+
+        if (isBlockingFetch && timeout == FontResourcesParserCompat.INFINITE_TIMEOUT_VALUE) {
+            // Wait forever. No need to post to the thread.
+            return getFontInternal(context, request);
+        }
+
+        final Callable<Typeface> fetcher = new Callable<Typeface>() {
+            @Override
+            public Typeface call() throws Exception {
+                Typeface typeface = getFontInternal(context, request);
+                if (typeface != null) {
+                    sTypefaceCache.put(id, typeface);
+                }
+                return typeface;
+            }
+        };
+
+        if (isBlockingFetch) {
+            try {
+                return sBackgroundThread.postAndWait(fetcher, timeout);
+            } catch (InterruptedException e) {
+                return null;
+            }
+        } else {
+            final ReplyCallback<Typeface> reply = new ReplyCallback<Typeface>() {
                 @Override
-                public Typeface call() throws Exception {
-                    FontFamilyResult result = fetchFonts(context, null, request);
-                    if (result.getStatusCode() == FontFamilyResult.STATUS_OK) {
-                        Typeface typeface = buildTypeface(context, null, result.getFonts());
-                        if (typeface != null) {
-                            sTypefaceCache.put(id, typeface);
-                        }
-                        return typeface;
-                    }
+                public void onReply(final Typeface typeface) {
+                    targetView.setTypeface(typeface, style);
+                }
+            };
+
+            synchronized (sLock) {
+                if (sPendingReplies.containsKey(id)) {
+                    // Already requested. Do not request the same provider again and insert the
+                    // reply to the queue instead.
+                    sPendingReplies.get(id).add(reply);
                     return null;
                 }
-            }, 500);
-        } catch (InterruptedException e) {
+                ArrayList<ReplyCallback<Typeface>> pendingReplies = new ArrayList<>();
+                pendingReplies.add(reply);
+                sPendingReplies.put(id, pendingReplies);
+            }
+            sBackgroundThread.postAndReply(fetcher, new ReplyCallback<Typeface>() {
+                @Override
+                public void onReply(final Typeface typeface) {
+                    final ArrayList<ReplyCallback<Typeface>> replies;
+                    synchronized (sLock) {
+                        replies = sPendingReplies.get(id);
+                        sPendingReplies.remove(id);
+                    }
+                    for (int i = 0; i < replies.size(); ++i) {
+                        replies.get(i).onReply(typeface);
+                    }
+                };
+            });
             return null;
         }
     }
diff --git a/compat/java/android/support/v4/provider/SelfDestructiveThread.java b/compat/java/android/support/v4/provider/SelfDestructiveThread.java
index 258f522..885799b 100644
--- a/compat/java/android/support/v4/provider/SelfDestructiveThread.java
+++ b/compat/java/android/support/v4/provider/SelfDestructiveThread.java
@@ -116,6 +116,44 @@
     }
 
     /**
+     * Reply callback for postAndReply
+     *
+     * @param <T> A type which will be received as the argument.
+     */
+    public interface ReplyCallback<T> {
+        /**
+         * Called when the task was finished.
+         */
+        void onReply(T value);
+    }
+
+    /**
+     * Execute the specific callable object on this thread and call the reply callback on the
+     * calling thread once it finishs.
+     */
+    public <T> void postAndReply(final Callable<T> callable, final ReplyCallback<T> reply) {
+        final Handler callingHandler = new Handler();
+        post(new Runnable() {
+            @Override
+            public void run() {
+                T t;
+                try {
+                    t = callable.call();
+                } catch (Exception e) {
+                    t = null;
+                }
+                final T result = t;
+                callingHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        reply.onReply(result);
+                    }
+                });
+            }
+        });
+    }
+
+    /**
      * Execute the specified callable object on this thread and returns the returned value to the
      * caller.
      *
diff --git a/compat/res-public/values/public_attrs.xml b/compat/res-public/values/public_attrs.xml
index 30e1bd2..e45f8c2 100644
--- a/compat/res-public/values/public_attrs.xml
+++ b/compat/res-public/values/public_attrs.xml
@@ -21,6 +21,8 @@
      <public type="attr" name="fontProviderPackage"/>
      <public type="attr" name="fontProviderQuery"/>
      <public type="attr" name="fontProviderCerts"/>
+     <public type="attr" name="fontProviderFetchStrategy"/>
+     <public type="attr" name="fontProviderFetchTimeout"/>
      <public type="attr" name="fontStyle"/>
      <public type="attr" name="font"/>
      <public type="attr" name="fontWeight"/>
diff --git a/compat/res/values/attrs.xml b/compat/res/values/attrs.xml
index 4dd6d3f..1833794 100644
--- a/compat/res/values/attrs.xml
+++ b/compat/res/values/attrs.xml
@@ -31,6 +31,28 @@
         individual list represents one collection of signature hashes. Refer to your font provider's
         documentation for these values. -->
         <attr name="fontProviderCerts" format="reference" />
+        <!-- The strategy to be used when fetching font data from a font provider in XML layouts.
+          -->
+        <attr name="fontProviderFetchStrategy">
+            <!-- The blocking font fetch works as follows.
+              First, check the local cache, then if the requested font is not cached, request the
+              font from the provider and wait until it is finished.  You can change the length of
+              the timeout by modifying fontProviderFetchTimeout.  If the timeout happens, the
+              default typeface will be used instead. -->
+            <enum name="blocking" value="0" />
+            <!-- The async font fetch works as follows.
+              First, check the local cache, then if the requeted font is not cached, trigger a
+              request the font and continue with layout inflation. Once the font fetch succeeds, the
+              target text view will be refreshed with the downloaded font data. The
+              fontProviderFetchTimeout will be ignored if async loading is specified. -->
+            <enum name="async" value="1" />
+        </attr>
+        <!-- The length of the timeout during fetching. -->
+        <attr name="fontProviderFetchTimeout" format="integer">
+          <!-- A special value for the timeout. In this case, the blocking font fetching will not
+            timeout and wait until a reply is received from the font provider. -->
+            <enum name="forever" value="-1" />
+        </attr>
     </declare-styleable>
 
     <!-- Attributes that are read when parsing a <font> tag, which is a child of
@@ -53,4 +75,4 @@
          in the font's header tables will be used. -->
         <attr name="fontWeight" format="integer" />
     </declare-styleable>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/compat/tests/java/android/support/v4/provider/SelfDestructiveThreadTest.java b/compat/tests/java/android/support/v4/provider/SelfDestructiveThreadTest.java
index 09b2412..84fd77f 100644
--- a/compat/tests/java/android/support/v4/provider/SelfDestructiveThreadTest.java
+++ b/compat/tests/java/android/support/v4/provider/SelfDestructiveThreadTest.java
@@ -16,6 +16,8 @@
 
 package android.support.v4.provider;
 
+import static android.support.v4.provider.SelfDestructiveThread.ReplyCallback;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
@@ -23,6 +25,8 @@
 import static org.junit.Assert.fail;
 
 import android.os.Process;
+import android.support.annotation.GuardedBy;
+import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.MediumTest;
 import android.support.test.runner.AndroidJUnit4;
 
@@ -31,6 +35,8 @@
 
 import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * Tests for {@link SelfDestructiveThread}
@@ -184,4 +190,85 @@
              // pass
         }
     }
+
+    private class WaitableReplyCallback implements ReplyCallback<Integer> {
+        private final ReentrantLock mLock = new ReentrantLock();
+        private final Condition mCond = mLock.newCondition();
+
+        @GuardedBy("mLock")
+        private Integer mValue;
+
+        private static final int NOT_STARTED = 0;
+        private static final int WAITING = 1;
+        private static final int FINISHED = 2;
+        private static final int TIMEOUT = 3;
+        @GuardedBy("mLock")
+        int mState = NOT_STARTED;
+
+        @Override
+        public void onReply(Integer value) {
+            mLock.lock();
+            try {
+                if (mState != TIMEOUT) {
+                    mValue = value;
+                    mState = FINISHED;
+                }
+                mCond.signalAll();
+            } finally {
+                mLock.unlock();
+            }
+        }
+
+        public Integer waitUntil(long timeoutMillis) {
+            mLock.lock();
+            try {
+                if (mState == FINISHED) {
+                    return mValue;
+                }
+                mState = WAITING;
+                long remaining = TimeUnit.MILLISECONDS.toNanos(timeoutMillis);
+                while (mState == WAITING) {
+                    try {
+                        remaining = mCond.awaitNanos(remaining);
+                    } catch (InterruptedException e) {
+                        // Ignore.
+                    }
+                    if (mState == FINISHED) {
+                        return mValue;
+                    }
+                    if (remaining <= 0) {
+                        mState = TIMEOUT;
+                        fail("Timeout");
+                    }
+                }
+                throw new IllegalStateException("mState becomes unexpected state");
+            } finally {
+                mLock.unlock();
+            }
+        }
+    }
+
+    @Test
+    public void testPostAndReply() {
+        final int destructAfterLastActivityInMs = 300;
+        final Integer expectedResult = 123;
+
+        final Callable<Integer> callable = new Callable<Integer>() {
+            @Override
+            public Integer call() throws Exception {
+                return expectedResult;
+            }
+        };
+        final WaitableReplyCallback reply = new WaitableReplyCallback();
+        final SelfDestructiveThread thread = new SelfDestructiveThread(
+                "test", Process.THREAD_PRIORITY_BACKGROUND, destructAfterLastActivityInMs);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                thread.postAndReply(callable, reply);
+            }
+        });
+
+        assertEquals(expectedResult, reply.waitUntil(DEFAULT_TIMEOUT));
+    }
 }
diff --git a/emoji/core/src/android/support/text/emoji/EmojiCompat.java b/emoji/core/src/android/support/text/emoji/EmojiCompat.java
index 671ea78..b5db6f6 100644
--- a/emoji/core/src/android/support/text/emoji/EmojiCompat.java
+++ b/emoji/core/src/android/support/text/emoji/EmojiCompat.java
@@ -118,6 +118,30 @@
     public @interface LoadState {
     }
 
+    /**
+     * Replace strategy that uses the value given in {@link EmojiCompat.Config}.
+     */
+    public static final int REPLACE_STRATEGY_DEFAULT = 0;
+
+    /**
+     * Replace strategy to add {@link EmojiSpan}s for all emoji that were found.
+     */
+    public static final int REPLACE_STRATEGY_ALL = 1;
+
+    /**
+     * Replace strategy to add {@link EmojiSpan}s only for emoji that do not exist in the system.
+     */
+    public static final int REPLACE_STRATEGY_NON_EXISTENT = 2;
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @IntDef({REPLACE_STRATEGY_DEFAULT, REPLACE_STRATEGY_NON_EXISTENT, REPLACE_STRATEGY_ALL})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ReplaceStrategy {
+    }
+
     private static final Object sInstanceLock = new Object();
 
     @GuardedBy("sInstanceLock")
@@ -231,6 +255,17 @@
     }
 
     /**
+     * Used by the tests to set GlyphChecker for EmojiProcessor.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @VisibleForTesting
+    void setGlyphChecker(@NonNull final EmojiProcessor.GlyphChecker glyphChecker) {
+        mHelper.setGlyphChecker(glyphChecker);
+    }
+
+    /**
      * Return singleton EmojiCompat instance. Should be called after
      * {@link #init(EmojiCompat.Config)} is called to initialize the singleton instance.
      *
@@ -543,6 +578,45 @@
     public CharSequence process(@NonNull final CharSequence charSequence,
             @IntRange(from = 0) final int start, @IntRange(from = 0) final int end,
             @IntRange(from = 0) final int maxEmojiCount) {
+        return process(charSequence, start, end, maxEmojiCount, REPLACE_STRATEGY_DEFAULT);
+    }
+
+    /**
+     * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found.
+     * <p>
+     * <ul>
+     * <li>If no emojis are found, {@code charSequence} given as the input is returned without
+     * any changes. i.e. charSequence is a String, and no emojis are found, the same String is
+     * returned.</li>
+     * <li>If the given input is not a Spannable (such as String), and at least one emoji is found
+     * a new {@link android.text.Spannable} instance is returned. </li>
+     * <li>If the given input is a Spannable, the same instance is returned. </li>
+     * </ul>
+     * When used on devices running API 18 or below, returns the given {@code charSequence} without
+     * processing it.
+     *
+     * @param charSequence CharSequence to add the EmojiSpans, cannot be {@code null}
+     * @param start start index in the charSequence to look for emojis, should be greater than or
+     *              equal to {@code 0}, also less than {@code charSequence.length()}
+     * @param end end index in the charSequence to look for emojis, should be greater than or
+     *            equal to {@code start} parameter, also less than {@code charSequence.length()}
+     * @param maxEmojiCount maximum number of emojis in the {@code charSequence}, should be greater
+     *                      than or equal to {@code 0}
+     * @param replaceStrategy whether to replace all emoji with {@link EmojiSpan}s, should be one of
+     *                        {@link #REPLACE_STRATEGY_DEFAULT},
+     *                        {@link #REPLACE_STRATEGY_NON_EXISTENT},
+     *                        {@link #REPLACE_STRATEGY_ALL}
+     *
+     * @throws IllegalStateException if not initialized yet
+     * @throws IllegalArgumentException in the following cases:
+     *                                  {@code start < 0}, {@code end < 0}, {@code end < start},
+     *                                  {@code start > charSequence.length()},
+     *                                  {@code end > charSequence.length()}
+     *                                  {@code maxEmojiCount < 0}
+     */
+    public CharSequence process(@NonNull final CharSequence charSequence,
+            @IntRange(from = 0) final int start, @IntRange(from = 0) final int end,
+            @IntRange(from = 0) final int maxEmojiCount, @ReplaceStrategy int replaceStrategy) {
         Preconditions.checkState(isInitialized(), "Not initialized yet");
         Preconditions.checkArgumentNonnegative(start, "start cannot be negative");
         Preconditions.checkArgumentNonnegative(end, "end cannot be negative");
@@ -564,7 +638,21 @@
             return charSequence;
         }
 
-        return mHelper.process(charSequence, start, end, maxEmojiCount);
+        final boolean replaceAll;
+        switch (replaceStrategy) {
+            case REPLACE_STRATEGY_ALL:
+                replaceAll = true;
+                break;
+            case REPLACE_STRATEGY_NON_EXISTENT:
+                replaceAll = false;
+                break;
+            case REPLACE_STRATEGY_DEFAULT:
+            default:
+                replaceAll = mReplaceAll;
+                break;
+        }
+
+        return mHelper.process(charSequence, start, end, maxEmojiCount, replaceAll);
     }
 
     /**
@@ -838,7 +926,7 @@
 
         CharSequence process(@NonNull final CharSequence charSequence,
                 @IntRange(from = 0) final int start, @IntRange(from = 0) final int end,
-                @IntRange(from = 0) final int maxEmojiCount) {
+                @IntRange(from = 0) final int maxEmojiCount, boolean replaceAll) {
             // Returns the given charSequence as it is.
             return charSequence;
         }
@@ -846,6 +934,10 @@
         void updateEditorInfoAttrs(@NonNull final EditorInfo outAttrs) {
             // Does not add any EditorInfo attributes.
         }
+
+        void setGlyphChecker(@NonNull EmojiProcessor.GlyphChecker glyphChecker) {
+            // intentionally empty
+        }
     }
 
     @RequiresApi(19)
@@ -893,8 +985,7 @@
             }
 
             mMetadataRepo = metadataRepo;
-            mProcessor = new EmojiProcessor(mMetadataRepo, new SpanFactory(),
-                    mEmojiCompat.mReplaceAll);
+            mProcessor = new EmojiProcessor(mMetadataRepo, new SpanFactory());
 
             mEmojiCompat.onMetadataLoadSuccess();
         }
@@ -912,8 +1003,8 @@
 
         @Override
         CharSequence process(@NonNull CharSequence charSequence, int start, int end,
-                int maxEmojiCount) {
-            return mProcessor.process(charSequence, start, end, maxEmojiCount);
+                int maxEmojiCount, boolean replaceAll) {
+            return mProcessor.process(charSequence, start, end, maxEmojiCount, replaceAll);
         }
 
         @Override
@@ -921,5 +1012,10 @@
             outAttrs.extras.putInt(EDITOR_INFO_METAVERSION_KEY, mMetadataRepo.getMetadataVersion());
             outAttrs.extras.putBoolean(EDITOR_INFO_REPLACE_ALL_KEY, mEmojiCompat.mReplaceAll);
         }
+
+        @Override
+        void setGlyphChecker(@NonNull EmojiProcessor.GlyphChecker glyphChecker) {
+            mProcessor.setGlyphChecker(glyphChecker);
+        }
     }
 }
diff --git a/emoji/core/src/android/support/text/emoji/EmojiProcessor.java b/emoji/core/src/android/support/text/emoji/EmojiProcessor.java
index b51a698..f168e59 100644
--- a/emoji/core/src/android/support/text/emoji/EmojiProcessor.java
+++ b/emoji/core/src/android/support/text/emoji/EmojiProcessor.java
@@ -26,6 +26,7 @@
 import android.support.annotation.RestrictTo;
 import android.support.text.emoji.widget.SpannableBuilder;
 import android.support.v4.graphics.PaintCompat;
+import android.support.v4.util.Preconditions;
 import android.text.Editable;
 import android.text.Selection;
 import android.text.Spannable;
@@ -75,17 +76,6 @@
     private static final int ACTION_FLUSH = 3;
 
     /**
-     * Default text size for {@link #mTextPaint}.
-     */
-    private static final int PAINT_TEXT_SIZE = 10;
-
-    /**
-     * Used to create strings required by
-     * {@link PaintCompat#hasGlyph(android.graphics.Paint, String)}.
-     */
-    private static final ThreadLocal<StringBuilder> sStringBuilder = new ThreadLocal<>();
-
-    /**
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
@@ -97,28 +87,19 @@
     private final EmojiCompat.SpanFactory mSpanFactory;
 
     /**
-     * @see EmojiCompat.Config#setReplaceAll(boolean)
-     */
-    private final boolean mReplaceAll;
-
-    /**
      * Emoji metadata repository.
      */
     private final MetadataRepo mMetadataRepo;
 
     /**
-     * TextPaint used during {@link PaintCompat#hasGlyph(android.graphics.Paint, String)} check.
+     * Utility class that checks if the system can render a given glyph.
      */
-    private final TextPaint mTextPaint;
+    private GlyphChecker mGlyphChecker = new GlyphChecker();
 
     EmojiProcessor(@NonNull final MetadataRepo metadataRepo,
-            @NonNull final EmojiCompat.SpanFactory spanFactory,
-            final boolean replaceAll) {
+            @NonNull final EmojiCompat.SpanFactory spanFactory) {
         mSpanFactory = spanFactory;
         mMetadataRepo = metadataRepo;
-        mReplaceAll = replaceAll;
-        mTextPaint = new TextPaint();
-        mTextPaint.setTextSize(PAINT_TEXT_SIZE);
     }
 
     EmojiMetadata getEmojiMetadata(@NonNull final CharSequence charSequence) {
@@ -161,9 +142,11 @@
      *            equal to {@code start} parameter, also less than {@code charSequence.length()}
      * @param maxEmojiCount maximum number of emojis in the {@code charSequence}, should be greater
      *                      than or equal to {@code 0}
+     * @param replaceAll whether to replace all emoji with {@link EmojiSpan}s
      */
     CharSequence process(@NonNull final CharSequence charSequence, @IntRange(from = 0) int start,
-            @IntRange(from = 0) int end, @IntRange(from = 0) int maxEmojiCount) {
+            @IntRange(from = 0) int end, @IntRange(from = 0) int maxEmojiCount,
+            final boolean replaceAll) {
         final boolean isSpannableBuilder = charSequence instanceof SpannableBuilder;
         if (isSpannableBuilder) {
             ((SpannableBuilder) charSequence).beginBatchEdit();
@@ -235,7 +218,7 @@
                         }
                         break;
                     case ACTION_FLUSH:
-                        if (mReplaceAll || !hasGlyph(charSequence, start, currentOffset,
+                        if (replaceAll || !hasGlyph(charSequence, start, currentOffset,
                                 sm.getFlushMetadata())) {
                             if (spannable == null) {
                                 spannable = new SpannableString(charSequence);
@@ -253,7 +236,7 @@
             // state machine is waiting to see if there is an emoji sequence (i.e. ZWJ).
             // Need to check if it is in such a state.
             if (sm.isInFlushableState() && addedCount < maxEmojiCount) {
-                if (mReplaceAll || !hasGlyph(charSequence, start, currentOffset,
+                if (replaceAll || !hasGlyph(charSequence, start, currentOffset,
                         sm.getCurrentMetadata())) {
                     if (spannable == null) {
                         spannable = new SpannableString(charSequence);
@@ -454,26 +437,19 @@
 
         // if the existence is not calculated yet
         if (metadata.getHasGlyph() == EmojiMetadata.HAS_GLYPH_UNKNOWN) {
-            final StringBuilder builder = getStringBuilder();
-            builder.setLength(0);
-
-            while (start < end) {
-                builder.append(charSequence.charAt(start));
-                start++;
-            }
-
-            final boolean hasGlyph = PaintCompat.hasGlyph(mTextPaint, builder.toString());
+            final boolean hasGlyph = mGlyphChecker.hasGlyph(charSequence, start, end);
             metadata.setHasGlyph(hasGlyph);
         }
 
         return metadata.getHasGlyph() == EmojiMetadata.HAS_GLYPH_EXISTS;
     }
 
-    private static StringBuilder getStringBuilder() {
-        if (sStringBuilder.get() == null) {
-            sStringBuilder.set(new StringBuilder());
-        }
-        return sStringBuilder.get();
+    /**
+     * Set the GlyphChecker instance used by EmojiProcessor. Used for testing.
+     */
+    void setGlyphChecker(@NonNull final GlyphChecker glyphChecker) {
+        Preconditions.checkNotNull(glyphChecker);
+        mGlyphChecker = glyphChecker;
     }
 
     /**
@@ -743,4 +719,63 @@
             }
         }
     }
+
+    /**
+     * Utility class that checks if the system can render a given glyph.
+     *
+     * @hide
+     */
+    @AnyThread
+    @RestrictTo(LIBRARY_GROUP)
+    public static class GlyphChecker {
+        /**
+         * Default text size for {@link #mTextPaint}.
+         */
+        private static final int PAINT_TEXT_SIZE = 10;
+
+        /**
+         * Used to create strings required by
+         * {@link PaintCompat#hasGlyph(android.graphics.Paint, String)}.
+         */
+        private static final ThreadLocal<StringBuilder> sStringBuilder = new ThreadLocal<>();
+
+        /**
+         * TextPaint used during {@link PaintCompat#hasGlyph(android.graphics.Paint, String)} check.
+         */
+        private final TextPaint mTextPaint;
+
+        GlyphChecker() {
+            mTextPaint = new TextPaint();
+            mTextPaint.setTextSize(PAINT_TEXT_SIZE);
+        }
+
+        /**
+         * Returns whether the system can render an emoji.
+         *
+         * @param charSequence the CharSequence that the emoji is in
+         * @param start start index of the emoji in the CharSequence
+         * @param end end index of the emoji in the CharSequence
+         *
+         * @return {@code true} if the OS can render emoji, {@code false} otherwise
+         */
+        public boolean hasGlyph(final CharSequence charSequence, int start, final int end) {
+            final StringBuilder builder = getStringBuilder();
+            builder.setLength(0);
+
+            while (start < end) {
+                builder.append(charSequence.charAt(start));
+                start++;
+            }
+
+            return PaintCompat.hasGlyph(mTextPaint, builder.toString());
+        }
+
+        private static StringBuilder getStringBuilder() {
+            if (sStringBuilder.get() == null) {
+                sStringBuilder.set(new StringBuilder());
+            }
+            return sStringBuilder.get();
+        }
+
+    }
 }
diff --git a/emoji/core/tests/java/android/support/text/emoji/EmojiCompatTest.java b/emoji/core/tests/java/android/support/text/emoji/EmojiCompatTest.java
index 00df303..29964ce 100644
--- a/emoji/core/tests/java/android/support/text/emoji/EmojiCompatTest.java
+++ b/emoji/core/tests/java/android/support/text/emoji/EmojiCompatTest.java
@@ -54,10 +54,15 @@
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
 
 import android.annotation.SuppressLint;
 import android.os.Bundle;
@@ -479,6 +484,50 @@
                         original.length()));
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_withReplaceNonExistent_callsGlyphChecker() {
+        final Config config = TestConfigBuilder.config().setReplaceAll(true);
+        EmojiCompat.reset(config);
+
+        final EmojiProcessor.GlyphChecker glyphChecker = mock(EmojiProcessor.GlyphChecker.class);
+        when(glyphChecker.hasGlyph(any(CharSequence.class), anyInt(), anyInt())).thenReturn(true);
+        EmojiCompat.get().setGlyphChecker(glyphChecker);
+
+        final String original = new TestString(EMOJI_SINGLE_CODEPOINT).toString();
+
+        CharSequence processed = EmojiCompat.get().process(original, 0, original.length(),
+                Integer.MAX_VALUE /*maxEmojiCount*/, EmojiCompat.REPLACE_STRATEGY_NON_EXISTENT);
+
+        // when function overrides config level replaceAll, a call to GlyphChecker is expected.
+        verify(glyphChecker, times(1)).hasGlyph(any(CharSequence.class), anyInt(), anyInt());
+
+        // since replaceAll is false, there should be no EmojiSpans
+        assertThat(processed, not(hasEmoji()));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_withReplaceDefault_doesNotCallGlyphChecker() {
+        final Config config = TestConfigBuilder.config().setReplaceAll(true);
+        EmojiCompat.reset(config);
+
+        final EmojiProcessor.GlyphChecker glyphChecker = mock(EmojiProcessor.GlyphChecker.class);
+        when(glyphChecker.hasGlyph(any(CharSequence.class), anyInt(), anyInt())).thenReturn(true);
+        EmojiCompat.get().setGlyphChecker(glyphChecker);
+
+        final String original = new TestString(EMOJI_SINGLE_CODEPOINT).toString();
+        // call without replaceAll, config value (true) should be used
+        final CharSequence processed = EmojiCompat.get().process(original, 0, original.length(),
+                Integer.MAX_VALUE /*maxEmojiCount*/, EmojiCompat.REPLACE_STRATEGY_DEFAULT);
+
+        // replaceAll=true should not call hasGlyph
+        verify(glyphChecker, times(0)).hasGlyph(any(CharSequence.class), anyInt(), anyInt());
+
+        assertThat(processed, hasEmojiCount(1));
+        assertThat(processed, hasEmoji(EMOJI_SINGLE_CODEPOINT));
+    }
+
     @Test(expected = NullPointerException.class)
     public void testHasEmojiGlyph_withNullCharSequence() {
         EmojiCompat.get().hasEmojiGlyph(null);
@@ -682,6 +731,4 @@
         charSequence = EmojiCompat.get().process(string.toString());
         assertThat(charSequence, not(hasEmoji()));
     }
-
-    //FAILS: CHAR_DIGIT, CHAR_VS_EMOJI, CHAR_VS_TEXT
 }
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java b/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
index 542e045..12617b7 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
@@ -240,6 +240,7 @@
     @RequiresApi(21)
     class MediaBrowserServiceImplApi21 implements MediaBrowserServiceImpl,
             MediaBrowserServiceCompatApi21.ServiceCompatProxy {
+        final List<Bundle> mRootExtrasList = new ArrayList<>();
         Object mServiceObj;
         Messenger mMessenger;
 
@@ -256,8 +257,23 @@
         }
 
         @Override
-        public void setSessionToken(MediaSessionCompat.Token token) {
-            MediaBrowserServiceCompatApi21.setSessionToken(mServiceObj, token.getToken());
+        public void setSessionToken(final MediaSessionCompat.Token token) {
+            mHandler.postOrRun(new Runnable() {
+                @Override
+                public void run() {
+                    if (!mRootExtrasList.isEmpty()) {
+                        IMediaSession extraBinder = token.getExtraBinder();
+                        if (extraBinder != null) {
+                            for (Bundle rootExtras : mRootExtrasList) {
+                                BundleCompat.putBinder(rootExtras, EXTRA_SESSION_BINDER,
+                                        extraBinder.asBinder());
+                            }
+                        }
+                        mRootExtrasList.clear();
+                    }
+                    MediaBrowserServiceCompatApi21.setSessionToken(mServiceObj, token.getToken());
+                }
+            });
         }
 
         @Override
@@ -313,6 +329,8 @@
                     IMediaSession extraBinder = mSession.getExtraBinder();
                     BundleCompat.putBinder(rootExtras, EXTRA_SESSION_BINDER,
                             extraBinder == null ? null : extraBinder.asBinder());
+                } else {
+                    mRootExtrasList.add(rootExtras);
                 }
             }
             BrowserRoot root = MediaBrowserServiceCompat.this.onGetRoot(
diff --git a/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java b/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
index 20d6119..ab21eda 100644
--- a/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
+++ b/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
@@ -28,6 +28,7 @@
 import android.content.ComponentName;
 import android.os.Bundle;
 import android.support.test.filters.LargeTest;
+import android.support.test.filters.MediumTest;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.testutils.PollingCheck;
@@ -162,11 +163,20 @@
     public void testReconnection() throws Exception {
         createMediaBrowser(TEST_BROWSER_SERVICE);
 
-        // Reconnect before the first connection was established.
-        mMediaBrowser.connect();
-        mMediaBrowser.disconnect();
-        resetCallbacks();
-        connectMediaBrowserService();
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mMediaBrowser.connect();
+                // Reconnect before the first connection was established.
+                mMediaBrowser.disconnect();
+                mMediaBrowser.connect();
+            }
+        });
+
+        synchronized (mConnectionCallback.mWaitLock) {
+            mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(1, mConnectionCallback.mConnectedCount);
+        }
 
         synchronized (mSubscriptionCallback.mWaitLock) {
             // Test subscribe.
@@ -209,9 +219,16 @@
     @SmallTest
     public void testConnectionCallbackNotCalledAfterDisconnect() {
         createMediaBrowser(TEST_BROWSER_SERVICE);
-        mMediaBrowser.connect();
-        mMediaBrowser.disconnect();
-        resetCallbacks();
+
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mMediaBrowser.connect();
+                mMediaBrowser.disconnect();
+                resetCallbacks();
+            }
+        });
+
         try {
             Thread.sleep(SLEEP_MS);
         } catch (InterruptedException e) {
@@ -370,29 +387,28 @@
     }
 
     @Test
-    @LargeTest
+    @SmallTest
     public void testUnsubscribeForMultipleSubscriptions() throws Exception {
         createMediaBrowser(TEST_BROWSER_SERVICE);
         connectMediaBrowserService();
         final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
         final int pageSize = 1;
 
-        synchronized (mSubscriptionCallback.mWaitLock) {
-            // Subscribe four pages, one item per page.
-            for (int page = 0; page < 4; page++) {
-                final StubSubscriptionCallback callback = new StubSubscriptionCallback();
-                subscriptionCallbacks.add(callback);
+        // Subscribe four pages, one item per page.
+        for (int page = 0; page < 4; page++) {
+            final StubSubscriptionCallback callback = new StubSubscriptionCallback();
+            subscriptionCallbacks.add(callback);
 
-                Bundle options = new Bundle();
-                options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
-                options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
-                mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, options,
-                        callback);
-                mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
-
-                // Each onChildrenLoaded() must be called.
-                assertEquals(1, callback.mChildrenLoadedWithOptionCount);
+            Bundle options = new Bundle();
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+            mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, options,
+                    callback);
+            synchronized (callback.mWaitLock) {
+                callback.mWaitLock.wait(TIME_OUT_MS);
             }
+            // Each onChildrenLoaded() must be called.
+            assertEquals(1, callback.mChildrenLoadedWithOptionCount);
         }
 
         // Reset callbacks and unsubscribe.
@@ -418,29 +434,28 @@
     }
 
     @Test
-    @LargeTest
+    @MediumTest
     public void testUnsubscribeWithSubscriptionCallbackForMultipleSubscriptions() throws Exception {
         createMediaBrowser(TEST_BROWSER_SERVICE);
         connectMediaBrowserService();
         final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
         final int pageSize = 1;
 
-        synchronized (mSubscriptionCallback.mWaitLock) {
-            // Subscribe four pages, one item per page.
-            for (int page = 0; page < 4; page++) {
-                final StubSubscriptionCallback callback = new StubSubscriptionCallback();
-                subscriptionCallbacks.add(callback);
+        // Subscribe four pages, one item per page.
+        for (int page = 0; page < 4; page++) {
+            final StubSubscriptionCallback callback = new StubSubscriptionCallback();
+            subscriptionCallbacks.add(callback);
 
-                Bundle options = new Bundle();
-                options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
-                options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
-                mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, options,
-                        callback);
-                mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
-
-                // Each onChildrenLoaded() must be called.
-                assertEquals(1, callback.mChildrenLoadedWithOptionCount);
+            Bundle options = new Bundle();
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+            mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, options,
+                    callback);
+            synchronized (callback.mWaitLock) {
+                callback.mWaitLock.wait(TIME_OUT_MS);
             }
+            // Each onChildrenLoaded() must be called.
+            assertEquals(1, callback.mChildrenLoadedWithOptionCount);
         }
 
         // Unsubscribe existing subscriptions one-by-one.
@@ -494,7 +509,7 @@
     }
 
     @Test
-    @SmallTest
+    @LargeTest
     public void testGetItemWhenOnLoadItemIsNotImplemented() throws Exception {
         createMediaBrowser(TEST_BROWSER_SERVICE);
         connectMediaBrowserService();
diff --git a/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java b/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java
index 0640f4d..4856cfd 100644
--- a/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java
+++ b/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java
@@ -24,6 +24,7 @@
 import static junit.framework.Assert.assertTrue;
 
 import android.content.ComponentName;
+import android.os.Build;
 import android.os.Bundle;
 import android.support.test.filters.MediumTest;
 import android.support.test.filters.SmallTest;
@@ -381,6 +382,11 @@
     @Test
     @SmallTest
     public void testDelayedSetSessionToken() throws Exception {
+        // This test has no meaning in API 21. The framework MediaBrowserService just connects to
+        // the media browser without waiting setMediaSession() to be called.
+        if (Build.VERSION.SDK_INT == 21) {
+            return;
+        }
         final ConnectionCallbackForDelayedMediaSession callback =
                 new ConnectionCallbackForDelayedMediaSession();
 
@@ -401,6 +407,11 @@
             StubMediaBrowserServiceCompatWithDelayedMediaSession.sInstance.callSetSessionToken();
             mWaitLock.wait(TIME_OUT_MS);
             assertEquals(1, callback.mConnectedCount);
+
+            if (Build.VERSION.SDK_INT >= 21) {
+                assertNotNull(
+                        mMediaBrowserForDelayedMediaSession.getSessionToken().getExtraBinder());
+            }
         }
     }
 
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatTextHelper.java b/v7/appcompat/src/android/support/v7/widget/AppCompatTextHelper.java
index d8c4f16..c5f6e17 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatTextHelper.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatTextHelper.java
@@ -205,7 +205,7 @@
                     : R.styleable.TextAppearance_fontFamily;
             if (!context.isRestricted()) {
                 try {
-                    mFontTypeface = a.getFont(fontFamilyId, mStyle);
+                    mFontTypeface = a.getFont(fontFamilyId, mStyle, mView);
                 } catch (UnsupportedOperationException | Resources.NotFoundException e) {
                     // Expected if it is not a font resource.
                 }
diff --git a/v7/appcompat/src/android/support/v7/widget/TintTypedArray.java b/v7/appcompat/src/android/support/v7/widget/TintTypedArray.java
index b962013..b920505 100644
--- a/v7/appcompat/src/android/support/v7/widget/TintTypedArray.java
+++ b/v7/appcompat/src/android/support/v7/widget/TintTypedArray.java
@@ -25,6 +25,7 @@
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.RequiresApi;
 import android.support.annotation.RestrictTo;
@@ -34,6 +35,7 @@
 import android.support.v7.content.res.AppCompatResources;
 import android.util.AttributeSet;
 import android.util.TypedValue;
+import android.widget.TextView;
 
 /**
  * A class that wraps a {@link android.content.res.TypedArray} and provides the same public API
@@ -96,6 +98,10 @@
      * not a font.
      *
      * @param index Index of attribute to retrieve.
+     * @param style A style value used for selecting best match font from the list of family. Note
+     * that this value will be ignored if the platform supports font family(API 24 or later).
+     * @param targetView A text view to be applied this font. If async loading is specified in XML,
+     * this view will be refreshed with result typeface.
      *
      * @return Typeface for the attribute, or {@code null} if not defined.
      * @throws RuntimeException if the TypedArray has already been recycled.
@@ -103,7 +109,7 @@
      *         not a font resource.
      */
     @Nullable
-    public Typeface getFont(@StyleableRes int index, int style) {
+    public Typeface getFont(@StyleableRes int index, int style, @NonNull TextView targetView) {
         if (BuildCompat.isAtLeastO()) {
             return mWrapped.getFont(index);
         }
@@ -114,7 +120,7 @@
         if (mTypedValue == null) {
             mTypedValue = new TypedValue();
         }
-        return ResourcesCompat.getFont(mContext, resourceId, mTypedValue, style);
+        return ResourcesCompat.getFont(mContext, resourceId, mTypedValue, style, targetView);
     }
 
     public int length() {