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() {