Merge "Fix public attribute name of fetch strategy." 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/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/tests/src/android/support/v4/media/MediaBrowserCompatTest.java b/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
index 7d0aa5d..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;
@@ -158,15 +159,24 @@
     }
 
     @Test
-    @LargeTest
+    @SmallTest
     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.
@@ -206,12 +216,19 @@
     }
 
     @Test
-    @LargeTest
+    @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.