Merge "Revert "Fixed keyboard autoclosing and wrong position for EditTexts" ag/2385121" into oc-support-26.0-dev
diff --git a/annotations/src/android/support/annotation/MainThread.java b/annotations/src/android/support/annotation/MainThread.java
index 5478751..2f50306 100644
--- a/annotations/src/android/support/annotation/MainThread.java
+++ b/annotations/src/android/support/annotation/MainThread.java
@@ -34,9 +34,17 @@
* @MainThread
* public void deliverResult(D data) { ... }
* </code></pre>
+ *
+ * <p class="note"><b>Note:</b> Ordinarily, an app's main thread is also the UI
+ * thread. However, However, under special circumstances, an app's main thread
+ * might not be its UI thread; for more information, see
+ * <a href="/studio/write/annotations.html#thread-annotations">Thread
+ * annotations</a>.
+ *
+ * @see android.support.annotation.UiThread
*/
@Documented
@Retention(CLASS)
@Target({METHOD,CONSTRUCTOR,TYPE})
public @interface MainThread {
-}
\ No newline at end of file
+}
diff --git a/annotations/src/android/support/annotation/UiThread.java b/annotations/src/android/support/annotation/UiThread.java
index ef81986..0a9a0c1 100644
--- a/annotations/src/android/support/annotation/UiThread.java
+++ b/annotations/src/android/support/annotation/UiThread.java
@@ -35,9 +35,17 @@
*
* public abstract void setText(@NonNull String text) { ... }
* </code></pre>
+ *
+ * <p class="note"><b>Note:</b> Ordinarily, an app's UI thread is also the main
+ * thread. However, However, under special circumstances, an app's UI thread
+ * might not be its main thread; for more information, see
+ * <a href="/studio/write/annotations.html#thread-annotations">Thread
+ * annotations</a>.
+ *
+ * @see android.support.annotation.MainThread
*/
@Documented
@Retention(CLASS)
@Target({METHOD,CONSTRUCTOR,TYPE})
public @interface UiThread {
-}
\ No newline at end of file
+}
diff --git a/api/26.0.0-SNAPSHOT.txt b/api/26.0.0-SNAPSHOT.txt
index fd83e30..aafd5f9 100644
--- a/api/26.0.0-SNAPSHOT.txt
+++ b/api/26.0.0-SNAPSHOT.txt
@@ -1040,7 +1040,9 @@
method public boolean hasThumbnail();
method public boolean isThumbnailCompressed();
method public void saveAttributes() throws java.io.IOException;
+ method public void setAltitude(double);
method public void setAttribute(java.lang.String, java.lang.String);
+ method public void setGpsInfo(android.location.Location);
method public void setLatLong(double, double);
field public static final int ORIENTATION_FLIP_HORIZONTAL = 2; // 0x2
field public static final int ORIENTATION_FLIP_VERTICAL = 4; // 0x4
@@ -1796,10 +1798,8 @@
field public static final java.lang.String EDITOR_INFO_METAVERSION_KEY = "android.support.text.emoji.emojiCompat_metadataVersion";
field public static final java.lang.String EDITOR_INFO_REPLACE_ALL_KEY = "android.support.text.emoji.emojiCompat_replaceAll";
field public static final int LOAD_STATE_FAILED = 2; // 0x2
- field public static final deprecated int LOAD_STATE_FAILURE = 2; // 0x2
field public static final int LOAD_STATE_LOADING = 0; // 0x0
field public static final int LOAD_STATE_SUCCEEDED = 1; // 0x1
- field public static final deprecated 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
@@ -1807,7 +1807,7 @@
public static abstract class EmojiCompat.Config {
ctor protected EmojiCompat.Config(android.support.text.emoji.EmojiCompat.MetadataRepoLoader);
- ctor protected deprecated EmojiCompat.Config(android.support.text.emoji.EmojiCompat.MetadataLoader);
+ method protected final android.support.text.emoji.EmojiCompat.MetadataRepoLoader getMetadataRepoLoader();
method public android.support.text.emoji.EmojiCompat.Config registerInitCallback(android.support.text.emoji.EmojiCompat.InitCallback);
method public android.support.text.emoji.EmojiCompat.Config setEmojiSpanIndicatorColor(int);
method public android.support.text.emoji.EmojiCompat.Config setEmojiSpanIndicatorEnabled(boolean);
@@ -1821,16 +1821,6 @@
method public void onInitialized();
}
- public static abstract deprecated class EmojiCompat.LoaderCallback {
- ctor public EmojiCompat.LoaderCallback();
- method public abstract void onFailed(java.lang.Throwable);
- method public abstract void onLoaded(android.support.text.emoji.MetadataRepo);
- }
-
- public static abstract deprecated interface EmojiCompat.MetadataLoader {
- method public abstract void load(android.support.text.emoji.EmojiCompat.LoaderCallback);
- }
-
public static abstract interface EmojiCompat.MetadataRepoLoader {
method public abstract void load(android.support.text.emoji.EmojiCompat.MetadataRepoLoaderCallback);
}
@@ -1847,6 +1837,18 @@
public class FontRequestEmojiCompatConfig extends android.support.text.emoji.EmojiCompat.Config {
ctor public FontRequestEmojiCompatConfig(android.content.Context, android.support.v4.provider.FontRequest);
+ method public android.support.text.emoji.FontRequestEmojiCompatConfig setHandler(android.os.Handler);
+ method public android.support.text.emoji.FontRequestEmojiCompatConfig setRetryPolicy(android.support.text.emoji.FontRequestEmojiCompatConfig.RetryPolicy);
+ }
+
+ public static class FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy extends android.support.text.emoji.FontRequestEmojiCompatConfig.RetryPolicy {
+ ctor public FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy(long);
+ method public long getRetryDelay();
+ }
+
+ public static abstract class FontRequestEmojiCompatConfig.RetryPolicy {
+ ctor public FontRequestEmojiCompatConfig.RetryPolicy();
+ method public abstract long getRetryDelay();
}
public final class MetadataRepo {
@@ -6233,7 +6235,6 @@
method public static int getActionCount(android.app.Notification);
method public static int getBadgeIconType(android.app.Notification);
method public static java.lang.String getCategory(android.app.Notification);
- method public static deprecated java.lang.String getChannel(android.app.Notification);
method public static java.lang.String getChannelId(android.app.Notification);
method public static android.os.Bundle getExtras(android.app.Notification);
method public static java.lang.String getGroup(android.app.Notification);
@@ -6241,7 +6242,6 @@
method public static boolean getLocalOnly(android.app.Notification);
method public static java.lang.String getShortcutId(android.app.Notification);
method public static java.lang.String getSortKey(android.app.Notification);
- method public static deprecated long getTimeout(android.app.Notification);
method public static long getTimeoutAfter(android.app.Notification);
method public static boolean isGroupSummary(android.app.Notification);
field public static final int BADGE_ICON_LARGE = 2; // 0x2
@@ -6398,7 +6398,6 @@
method public android.support.v4.app.NotificationCompat.Builder setAutoCancel(boolean);
method public android.support.v4.app.NotificationCompat.Builder setBadgeIconType(int);
method public android.support.v4.app.NotificationCompat.Builder setCategory(java.lang.String);
- method public deprecated android.support.v4.app.NotificationCompat.Builder setChannel(java.lang.String);
method public android.support.v4.app.NotificationCompat.Builder setChannelId(java.lang.String);
method public android.support.v4.app.NotificationCompat.Builder setColor(int);
method public android.support.v4.app.NotificationCompat.Builder setColorized(boolean);
@@ -6438,7 +6437,6 @@
method public android.support.v4.app.NotificationCompat.Builder setSubText(java.lang.CharSequence);
method public android.support.v4.app.NotificationCompat.Builder setTicker(java.lang.CharSequence);
method public android.support.v4.app.NotificationCompat.Builder setTicker(java.lang.CharSequence, android.widget.RemoteViews);
- method public deprecated android.support.v4.app.NotificationCompat.Builder setTimeout(long);
method public android.support.v4.app.NotificationCompat.Builder setTimeoutAfter(long);
method public android.support.v4.app.NotificationCompat.Builder setUsesChronometer(boolean);
method public android.support.v4.app.NotificationCompat.Builder setVibrate(long[]);
@@ -12656,7 +12654,8 @@
method public void attachToRecyclerView(android.support.v7.widget.RecyclerView) throws java.lang.IllegalStateException;
method public abstract int[] calculateDistanceToFinalSnap(android.support.v7.widget.RecyclerView.LayoutManager, android.view.View);
method public int[] calculateScrollDistance(int, int);
- method protected android.support.v7.widget.LinearSmoothScroller createSnapScroller(android.support.v7.widget.RecyclerView.LayoutManager);
+ method protected android.support.v7.widget.RecyclerView.SmoothScroller createScroller(android.support.v7.widget.RecyclerView.LayoutManager);
+ method protected deprecated android.support.v7.widget.LinearSmoothScroller createSnapScroller(android.support.v7.widget.RecyclerView.LayoutManager);
method public abstract android.view.View findSnapView(android.support.v7.widget.RecyclerView.LayoutManager);
method public abstract int findTargetSnapPosition(android.support.v7.widget.RecyclerView.LayoutManager, int, int);
method public boolean onFling(int, int);
@@ -12958,6 +12957,34 @@
field public int boxedEdges;
}
+ public class CircularProgressLayout extends android.widget.FrameLayout {
+ ctor public CircularProgressLayout(android.content.Context);
+ ctor public CircularProgressLayout(android.content.Context, android.util.AttributeSet);
+ ctor public CircularProgressLayout(android.content.Context, android.util.AttributeSet, int);
+ ctor public CircularProgressLayout(android.content.Context, android.util.AttributeSet, int, int);
+ method public int getBackgroundColor();
+ method public int[] getColorSchemeColors();
+ method public android.support.wear.widget.CircularProgressLayout.OnTimerFinishedListener getOnTimerFinishedListener();
+ method public android.support.v4.widget.CircularProgressDrawable getProgressDrawable();
+ method public float getStartingRotation();
+ method public float getStrokeWidth();
+ method public long getTotalTime();
+ method public boolean isIndeterminate();
+ method public boolean isTimerRunning();
+ method public void setColorSchemeColors(int...);
+ method public void setIndeterminate(boolean);
+ method public void setOnTimerFinishedListener(android.support.wear.widget.CircularProgressLayout.OnTimerFinishedListener);
+ method public void setStartingRotation(float);
+ method public void setStrokeWidth(float);
+ method public void setTotalTime(long);
+ method public void startTimer();
+ method public void stopTimer();
+ }
+
+ public static abstract interface CircularProgressLayout.OnTimerFinishedListener {
+ method public abstract void onTimerFinished(android.support.wear.widget.CircularProgressLayout);
+ }
+
public class CurvingLayoutCallback extends android.support.wear.widget.WearableLinearLayoutManager.LayoutCallback {
ctor public CurvingLayoutCallback(android.content.Context);
method public void adjustAnchorOffsetXY(android.view.View, float[]);
diff --git a/build.gradle b/build.gradle
index 5bd5d13..360ec02 100644
--- a/build.gradle
+++ b/build.gradle
@@ -35,6 +35,8 @@
init.configureSubProjects()
+init.configureBuildOnServer()
+
init.setupRelease()
init.enableDoclavaAndJDiff(this)
diff --git a/buildSrc/init.gradle b/buildSrc/init.gradle
index a3ce8ba..4e66dc8 100644
--- a/buildSrc/init.gradle
+++ b/buildSrc/init.gradle
@@ -17,9 +17,6 @@
import com.android.build.gradle.internal.coverage.JacocoPlugin
import com.android.build.gradle.internal.coverage.JacocoReportTask
import com.android.build.gradle.internal.tasks.DeviceProviderInstrumentTestTask
-import com.google.common.base.Charsets
-import com.google.common.io.Files
-
import org.gradle.api.logging.configuration.ShowStacktrace
def supportRoot = ext.supportRootFolder
@@ -60,14 +57,17 @@
apply from: "${ext.supportRootFolder}/buildSrc/diff_and_docs.gradle"
}
-def setSdkInLocalPropertiesFile() {
+def getFullSdkPath() {
final String osName = System.getProperty("os.name").toLowerCase();
final boolean isMacOsX =
osName.contains("mac os x") || osName.contains("darwin") || osName.contains("osx");
final String platform = isMacOsX ? 'darwin' : 'linux'
- System.setProperty('android.dir', "${supportRootFolder}/../../")
+ return "${repos.prebuiltsRoot}/fullsdk-${platform}"
+}
+
+def setSdkInLocalPropertiesFile() {
ext.buildToolsVersion = '26.0.0'
- final String fullSdkPath = "${repos.prebuiltsRoot}/fullsdk-${platform}"
+ final String fullSdkPath = getFullSdkPath();
if (file(fullSdkPath).exists()) {
gradle.ext.currentSdk = 26
project.ext.androidJar =
@@ -75,7 +75,6 @@
project.ext.androidSrcJar =
file("${fullSdkPath}/platforms/android-${gradle.currentSdk}/android-stubs-src.jar")
project.ext.androidApiTxt = null
- System.setProperty('android.home', "${repos.prebuiltsRoot}/fullsdk-${platform}")
File props = file("local.properties")
props.write "sdk.dir=${fullSdkPath}"
ext.usingFullSdk = true
@@ -84,6 +83,7 @@
project.ext.androidJar = files("${repos.prebuiltsRoot}/sdk/current/android.jar")
project.ext.androidSrcJar = null
project.ext.androidApiTxt = file("${repos.prebuiltsRoot}/sdk/api/26.txt")
+ System.setProperty('android.dir', "${supportRootFolder}/../../")
File props = file("local.properties")
props.write "android.dir=../../"
ext.usingFullSdk = false
@@ -126,6 +126,23 @@
ext.docsDir = new File(buildDir, 'javadoc')
}
+def configureBuildOnServer() {
+ def buildOnServerTask = rootProject.tasks.create("buildOnServer")
+ rootProject.tasks.whenTaskAdded { task ->
+ if ("createArchive".equals(task.name)) {
+ buildOnServerTask.dependsOn task
+ }
+ }
+
+ subprojects {
+ project.tasks.whenTaskAdded { task ->
+ if ("assembleErrorProne".equals(task.name) || "assembleAndroidTest".equals(task.name)) {
+ buildOnServerTask.dependsOn task
+ }
+ }
+ }
+}
+
def configureSubProjects() {
// lint every library
def lintTask = project.tasks.create("lint")
@@ -184,26 +201,26 @@
destinationDir file(project.distDir)
archiveName "${project.archivesBaseName}-${v.baseName}-allclasses.jar"
}
- def jacocoAntConfig =
- project.configurations[JacocoPlugin.ANT_CONFIGURATION_NAME]
- def jacocoAntArtifacts = jacocoAntConfig.resolvedConfiguration
- .resolvedArtifacts
- def version = jacocoAntArtifacts.find { "org.jacoco.ant".equals(it.name) }
- .moduleVersion.id.version
+
def collectJacocoAntPackages = project.tasks.create(
name: "collectJacocoAntPackages",
type: Jar) {
- from(jacocoAntArtifacts.collect { zipTree(it.getFile()) }) {
- // exclude all the signatures the jar might have
- exclude "META-INF/*.SF"
- exclude "META-INF/*.DSA"
- exclude "META-INF/*.RSA"
- }
+ inputs.files project.configurations[JacocoPlugin.ANT_CONFIGURATION_NAME]
+ from {
+ project.configurations[JacocoPlugin.ANT_CONFIGURATION_NAME]
+ .resolvedConfiguration
+ .resolvedArtifacts.collect{ zipTree(it.getFile()) }} {
+ // exclude all the signatures the jar might have
+ exclude "META-INF/*.SF"
+ exclude "META-INF/*.DSA"
+ exclude "META-INF/*.RSA"
+ }
destinationDir file(project.distDir)
- archiveName "jacocoant-" + version + ".jar"
+ archiveName "jacocoant.jar"
}
+
jarifyTask.dependsOn v.getJavaCompiler()
- v.assemble.dependsOn jarifyTask, collectJacocoAntPackages
+ v.assemble.dependsOn jarifyTask , collectJacocoAntPackages
}
}
@@ -342,4 +359,5 @@
ext.init.setupRepoOutAndBuildNumber = this.&setupRepoOutAndBuildNumber
ext.init.setupRelease = this.&setupRelease
ext.init.loadDefaultVersions = this.&loadDefaultVersions
-ext.init.configureSubProjects = this.&configureSubProjects
\ No newline at end of file
+ext.init.configureSubProjects = this.&configureSubProjects
+ext.init.configureBuildOnServer = this.&configureBuildOnServer
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/android/support/checkapi/ApiXmlConversionTask.groovy b/buildSrc/src/main/groovy/android/support/checkapi/ApiXmlConversionTask.groovy
index 61e84d4..bd4bda7 100644
--- a/buildSrc/src/main/groovy/android/support/checkapi/ApiXmlConversionTask.groovy
+++ b/buildSrc/src/main/groovy/android/support/checkapi/ApiXmlConversionTask.groovy
@@ -16,12 +16,10 @@
package android.support.checkapi
-import org.gradle.api.tasks.JavaExec
import org.gradle.api.tasks.InputFile
-import org.gradle.api.tasks.ParallelizableTask
+import org.gradle.api.tasks.JavaExec
import org.gradle.api.tasks.OutputFile
-@ParallelizableTask
public class ApiXmlConversionTask extends JavaExec {
@InputFile
diff --git a/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy b/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy
index 15b4594..2aa9d21 100644
--- a/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy
+++ b/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy
@@ -14,20 +14,18 @@
* limitations under the License.
*/
-package android.support.checkapi;
+package android.support.checkapi
import org.gradle.api.DefaultTask
-import org.gradle.api.Nullable
import org.gradle.api.GradleException
import org.gradle.api.InvalidUserDataException
+import org.gradle.api.Nullable
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
-import org.gradle.api.tasks.ParallelizableTask
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFile
-import org.gradle.process.ExecResult
import java.security.MessageDigest
@@ -41,7 +39,6 @@
* Specific failures may be ignored by specifying a list of SHAs in {@link #whitelistErrors}. Each
* SHA is unique to a specific API change and is logged to the error output on failure.
*/
-@ParallelizableTask
public class CheckApiTask extends DefaultTask {
/** Character that resets console output color. */
private static final String ANSI_RESET = "\u001B[0m";
diff --git a/buildSrc/src/main/groovy/android/support/doclava/DoclavaTask.groovy b/buildSrc/src/main/groovy/android/support/doclava/DoclavaTask.groovy
index ff5c968..b82ccc8 100644
--- a/buildSrc/src/main/groovy/android/support/doclava/DoclavaTask.groovy
+++ b/buildSrc/src/main/groovy/android/support/doclava/DoclavaTask.groovy
@@ -24,9 +24,7 @@
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.OutputFile
-import org.gradle.api.tasks.ParallelizableTask
-@ParallelizableTask
public class DoclavaTask extends Javadoc {
// external/doclava/src/com/google/doclava/Errors.java
diff --git a/buildSrc/src/main/groovy/android/support/jdiff/JDiffTask.groovy b/buildSrc/src/main/groovy/android/support/jdiff/JDiffTask.groovy
index 7bb9435..6e7402c 100644
--- a/buildSrc/src/main/groovy/android/support/jdiff/JDiffTask.groovy
+++ b/buildSrc/src/main/groovy/android/support/jdiff/JDiffTask.groovy
@@ -21,9 +21,7 @@
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.javadoc.Javadoc
import org.gradle.api.tasks.Optional
-import org.gradle.api.tasks.ParallelizableTask
-@ParallelizableTask
public class JDiffTask extends Javadoc {
@InputFiles
diff --git a/compat/api24/android/support/v4/app/NotificationCompatApi24.java b/compat/api24/android/support/v4/app/NotificationCompatApi24.java
index 17e5cad..67ea5dc 100644
--- a/compat/api24/android/support/v4/app/NotificationCompatApi24.java
+++ b/compat/api24/android/support/v4/app/NotificationCompatApi24.java
@@ -30,6 +30,7 @@
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Parcelable;
import android.support.annotation.RequiresApi;
import android.widget.RemoteViews;
@@ -181,8 +182,89 @@
}
actionExtras.putBoolean(NotificationCompatJellybean.EXTRA_ALLOW_GENERATED_REPLIES,
action.getAllowGeneratedReplies());
- actionBuilder.addExtras(actionExtras);
actionBuilder.setAllowGeneratedReplies(action.getAllowGeneratedReplies());
+ actionBuilder.addExtras(actionExtras);
b.addAction(actionBuilder.build());
}
+
+ public static NotificationCompatBase.Action getAction(Notification notif,
+ int actionIndex, NotificationCompatBase.Action.Factory actionFactory,
+ RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) {
+ return getActionCompatFromAction(notif.actions[actionIndex], actionFactory,
+ remoteInputFactory);
+ }
+
+ private static NotificationCompatBase.Action getActionCompatFromAction(
+ Notification.Action action, NotificationCompatBase.Action.Factory actionFactory,
+ RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) {
+ RemoteInputCompatBase.RemoteInput[] remoteInputs = RemoteInputCompatApi20.toCompat(
+ action.getRemoteInputs(), remoteInputFactory);
+ boolean allowGeneratedReplies = action.getExtras().getBoolean(
+ NotificationCompatJellybean.EXTRA_ALLOW_GENERATED_REPLIES)
+ || action.getAllowGeneratedReplies();
+ return actionFactory.build(action.icon, action.title, action.actionIntent,
+ action.getExtras(), remoteInputs, null, allowGeneratedReplies);
+ }
+
+ private static Notification.Action getActionFromActionCompat(
+ NotificationCompatBase.Action actionCompat) {
+ Notification.Action.Builder actionBuilder = new Notification.Action.Builder(
+ actionCompat.getIcon(), actionCompat.getTitle(), actionCompat.getActionIntent());
+ Bundle actionExtras;
+ if (actionCompat.getExtras() != null) {
+ actionExtras = new Bundle(actionCompat.getExtras());
+ } else {
+ actionExtras = new Bundle();
+ }
+ actionExtras.putBoolean(NotificationCompatJellybean.EXTRA_ALLOW_GENERATED_REPLIES,
+ actionCompat.getAllowGeneratedReplies());
+ actionBuilder.setAllowGeneratedReplies(actionCompat.getAllowGeneratedReplies());
+ actionBuilder.addExtras(actionExtras);
+ RemoteInputCompatBase.RemoteInput[] remoteInputCompats = actionCompat.getRemoteInputs();
+ if (remoteInputCompats != null) {
+ RemoteInput[] remoteInputs = RemoteInputCompatApi20.fromCompat(remoteInputCompats);
+ for (RemoteInput remoteInput : remoteInputs) {
+ actionBuilder.addRemoteInput(remoteInput);
+ }
+ }
+ return actionBuilder.build();
+ }
+
+ /**
+ * Get a list of notification compat actions by parsing actions stored within a list of
+ * parcelables using the {@link Bundle#getParcelableArrayList} function in the same
+ * manner that framework code would do so. In API20, Using Action parcelable directly
+ * is correct.
+ */
+ public static NotificationCompatBase.Action[] getActionsFromParcelableArrayList(
+ ArrayList<Parcelable> parcelables,
+ NotificationCompatBase.Action.Factory actionFactory,
+ RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) {
+ if (parcelables == null) {
+ return null;
+ }
+ NotificationCompatBase.Action[] actions = actionFactory.newArray(parcelables.size());
+ for (int i = 0; i < actions.length; i++) {
+ Notification.Action action = (Notification.Action) parcelables.get(i);
+ actions[i] = getActionCompatFromAction(action, actionFactory, remoteInputFactory);
+ }
+ return actions;
+ }
+
+ /**
+ * Get an array list of parcelables, suitable for {@link Bundle#putParcelableArrayList},
+ * that matches what framework code would do to store an actions list in this way. In API20,
+ * action parcelables were directly placed as entries in the array list.
+ */
+ public static ArrayList<Parcelable> getParcelableArrayListForActions(
+ NotificationCompatBase.Action[] actions) {
+ if (actions == null) {
+ return null;
+ }
+ ArrayList<Parcelable> parcelables = new ArrayList<Parcelable>(actions.length);
+ for (NotificationCompatBase.Action action : actions) {
+ parcelables.add(getActionFromActionCompat(action));
+ }
+ return parcelables;
+ }
}
diff --git a/compat/ics/android/support/v4/graphics/PaintCompatApi14.java b/compat/ics/android/support/v4/graphics/PaintCompatApi14.java
index 86e87d8..7a7de7c 100644
--- a/compat/ics/android/support/v4/graphics/PaintCompatApi14.java
+++ b/compat/ics/android/support/v4/graphics/PaintCompatApi14.java
@@ -24,6 +24,7 @@
class PaintCompatApi14 {
// U+DFFFD which is very end of unassigned plane.
private static final String TOFU_STRING = "\uDB3F\uDFFD";
+ private static final String EM_STRING = "m";
private static final ThreadLocal<Pair<Rect, Rect>> sRectThreadLocal = new ThreadLocal<>();
@@ -36,6 +37,8 @@
}
final float missingGlyphWidth = paint.measureText(TOFU_STRING);
+ final float emGlyphWidth = paint.measureText(EM_STRING);
+
final float width = paint.measureText(string);
if (width == 0f) {
@@ -46,7 +49,7 @@
if (string.codePointCount(0, string.length()) > 1) {
// Heuristic to detect fallback glyphs for ligatures like flags and ZWJ sequences
// Return false if string is rendered too widely
- if (width > 2 * missingGlyphWidth) {
+ if (width > 2 * emGlyphWidth) {
return false;
}
diff --git a/compat/java/android/support/v4/app/NotificationCompat.java b/compat/java/android/support/v4/app/NotificationCompat.java
index 9129cc7..10da04a 100644
--- a/compat/java/android/support/v4/app/NotificationCompat.java
+++ b/compat/java/android/support/v4/app/NotificationCompat.java
@@ -24,6 +24,7 @@
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
+import android.content.res.ColorStateList;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.media.AudioManager;
@@ -34,9 +35,15 @@
import android.support.annotation.ColorInt;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.annotation.RestrictTo;
+import android.support.v4.text.BidiFormatter;
import android.support.v4.view.GravityCompat;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.TextAppearanceSpan;
import android.view.Gravity;
import android.widget.RemoteViews;
@@ -597,10 +604,27 @@
@RestrictTo(LIBRARY_GROUP)
protected static class BuilderExtender {
public Notification build(Builder b, NotificationBuilderWithBuilderAccessor builder) {
+ RemoteViews styleContentView = b.mStyle != null
+ ? b.mStyle.makeContentView(builder)
+ : null;
Notification n = builder.build();
- if (b.mContentView != null) {
+ if (styleContentView != null) {
+ n.contentView = styleContentView;
+ } else if (b.mContentView != null) {
n.contentView = b.mContentView;
}
+ if (Build.VERSION.SDK_INT >= 16 && b.mStyle != null) {
+ RemoteViews styleBigContentView = b.mStyle.makeBigContentView(builder);
+ if (styleBigContentView != null) {
+ n.bigContentView = styleBigContentView;
+ }
+ }
+ if (Build.VERSION.SDK_INT >= 21 && b.mStyle != null) {
+ RemoteViews styleHeadsUpContentView = b.mStyle.makeHeadsUpContentView(builder);
+ if (styleHeadsUpContentView != null) {
+ n.headsUpContentView = styleHeadsUpContentView;
+ }
+ }
return n;
}
}
@@ -655,7 +679,7 @@
public Notification build(Builder b, BuilderExtender extender) {
BuilderBase builder =
new BuilderBase(b.mContext, b.mNotification,
- b.resolveTitle(), b.resolveText(), b.mContentInfo, b.mTickerView,
+ b.mContentTitle, b.mContentText, b.mContentInfo, b.mTickerView,
b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon,
b.mProgressMax, b.mProgress, b.mProgressIndeterminate);
return extender.build(b, builder);
@@ -694,13 +718,15 @@
@Override
public Notification build(Builder b, BuilderExtender extender) {
NotificationCompatJellybean.Builder builder = new NotificationCompatJellybean.Builder(
- b.mContext, b.mNotification, b.resolveTitle(), b.resolveText(), b.mContentInfo,
+ b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo,
b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon,
b.mProgressMax, b.mProgress, b.mProgressIndeterminate,
b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mExtras,
b.mGroupKey, b.mGroupSummary, b.mSortKey, b.mContentView, b.mBigContentView);
addActionsToBuilder(builder, b.mActions);
- addStyleToBuilderJellybean(builder, b.mStyle);
+ if (b.mStyle != null) {
+ b.mStyle.apply(builder);
+ }
Notification notification = extender.build(b, builder);
if (b.mStyle != null) {
Bundle extras = getExtras(notification);
@@ -736,14 +762,16 @@
@Override
public Notification build(Builder b, BuilderExtender extender) {
NotificationCompatKitKat.Builder builder = new NotificationCompatKitKat.Builder(
- b.mContext, b.mNotification, b.resolveTitle(), b.resolveText(), b.mContentInfo,
+ b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo,
b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon,
b.mProgressMax, b.mProgress, b.mProgressIndeterminate, b.mShowWhen,
b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly,
b.mPeople, b.mExtras, b.mGroupKey, b.mGroupSummary, b.mSortKey,
b.mContentView, b.mBigContentView);
addActionsToBuilder(builder, b.mActions);
- addStyleToBuilderJellybean(builder, b.mStyle);
+ if (b.mStyle != null) {
+ b.mStyle.apply(builder);
+ }
return extender.build(b, builder);
}
@@ -759,14 +787,16 @@
@Override
public Notification build(Builder b, BuilderExtender extender) {
NotificationCompatApi20.Builder builder = new NotificationCompatApi20.Builder(
- b.mContext, b.mNotification, b.resolveTitle(), b.resolveText(), b.mContentInfo,
+ b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo,
b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon,
b.mProgressMax, b.mProgress, b.mProgressIndeterminate, b.mShowWhen,
b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mPeople, b.mExtras,
b.mGroupKey, b.mGroupSummary, b.mSortKey, b.mContentView, b.mBigContentView,
b.mGroupAlertBehavior);
addActionsToBuilder(builder, b.mActions);
- addStyleToBuilderJellybean(builder, b.mStyle);
+ if (b.mStyle != null) {
+ b.mStyle.apply(builder);
+ }
Notification notification = extender.build(b, builder);
if (b.mStyle != null) {
b.mStyle.addCompatExtras(getExtras(notification));
@@ -799,7 +829,7 @@
@Override
public Notification build(Builder b, BuilderExtender extender) {
NotificationCompatApi21.Builder builder = new NotificationCompatApi21.Builder(
- b.mContext, b.mNotification, b.resolveTitle(), b.resolveText(), b.mContentInfo,
+ b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo,
b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon,
b.mProgressMax, b.mProgress, b.mProgressIndeterminate, b.mShowWhen,
b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mCategory,
@@ -807,7 +837,9 @@
b.mGroupKey, b.mGroupSummary, b.mSortKey, b.mContentView, b.mBigContentView,
b.mHeadsUpContentView, b.mGroupAlertBehavior);
addActionsToBuilder(builder, b.mActions);
- addStyleToBuilderJellybean(builder, b.mStyle);
+ if (b.mStyle != null) {
+ b.mStyle.apply(builder);
+ }
Notification notification = extender.build(b, builder);
if (b.mStyle != null) {
b.mStyle.addCompatExtras(getExtras(notification));
@@ -843,13 +875,34 @@
b.mGroupKey, b.mGroupSummary, b.mSortKey, b.mRemoteInputHistory, b.mContentView,
b.mBigContentView, b.mHeadsUpContentView, b.mGroupAlertBehavior);
addActionsToBuilder(builder, b.mActions);
- addStyleToBuilderApi24(builder, b.mStyle);
+ if (b.mStyle != null) {
+ b.mStyle.apply(builder);
+ }
Notification notification = extender.build(b, builder);
if (b.mStyle != null) {
b.mStyle.addCompatExtras(getExtras(notification));
}
return notification;
}
+
+ @Override
+ public Action getAction(Notification n, int actionIndex) {
+ return (Action) NotificationCompatApi24.getAction(n, actionIndex, Action.FACTORY,
+ RemoteInput.FACTORY);
+ }
+
+ @Override
+ public Action[] getActionsFromParcelableArrayList(
+ ArrayList<Parcelable> parcelables) {
+ return (Action[]) NotificationCompatApi24.getActionsFromParcelableArrayList(
+ parcelables, Action.FACTORY, RemoteInput.FACTORY);
+ }
+
+ @Override
+ public ArrayList<Parcelable> getParcelableArrayListForActions(
+ Action[] actions) {
+ return NotificationCompatApi24.getParcelableArrayListForActions(actions);
+ }
}
@RequiresApi(26)
@@ -868,7 +921,9 @@
b.mShortcutId, b.mTimeout, b.mColorized, b.mColorizedSet,
b.mGroupAlertBehavior);
addActionsToBuilder(builder, b.mActions);
- addStyleToBuilderApi24(builder, b.mStyle);
+ if (b.mStyle != null) {
+ b.mStyle.apply(builder);
+ }
Notification notification = extender.build(b, builder);
if (b.mStyle != null) {
b.mStyle.addCompatExtras(getExtras(notification));
@@ -884,65 +939,6 @@
}
}
- @RequiresApi(16)
- static void addStyleToBuilderJellybean(NotificationBuilderWithBuilderAccessor builder,
- Style style) {
- if (style != null) {
- if (style instanceof BigTextStyle) {
- BigTextStyle bigTextStyle = (BigTextStyle) style;
- NotificationCompatJellybean.addBigTextStyle(builder,
- bigTextStyle.mBigContentTitle,
- bigTextStyle.mSummaryTextSet,
- bigTextStyle.mSummaryText,
- bigTextStyle.mBigText);
- } else if (style instanceof InboxStyle) {
- InboxStyle inboxStyle = (InboxStyle) style;
- NotificationCompatJellybean.addInboxStyle(builder,
- inboxStyle.mBigContentTitle,
- inboxStyle.mSummaryTextSet,
- inboxStyle.mSummaryText,
- inboxStyle.mTexts);
- } else if (style instanceof BigPictureStyle) {
- BigPictureStyle bigPictureStyle = (BigPictureStyle) style;
- NotificationCompatJellybean.addBigPictureStyle(builder,
- bigPictureStyle.mBigContentTitle,
- bigPictureStyle.mSummaryTextSet,
- bigPictureStyle.mSummaryText,
- bigPictureStyle.mPicture,
- bigPictureStyle.mBigLargeIcon,
- bigPictureStyle.mBigLargeIconSet);
- }
- }
- }
-
- @RequiresApi(24)
- static void addStyleToBuilderApi24(NotificationBuilderWithBuilderAccessor builder,
- Style style) {
- if (style != null) {
- if (style instanceof MessagingStyle) {
- MessagingStyle messagingStyle = (MessagingStyle) style;
- List<CharSequence> texts = new ArrayList<>();
- List<Long> timestamps = new ArrayList<>();
- List<CharSequence> senders = new ArrayList<>();
- List<String> dataMimeTypes = new ArrayList<>();
- List<Uri> dataUris = new ArrayList<>();
-
- for (MessagingStyle.Message message : messagingStyle.mMessages) {
- texts.add(message.getText());
- timestamps.add(message.getTimestamp());
- senders.add(message.getSender());
- dataMimeTypes.add(message.getDataMimeType());
- dataUris.add(message.getDataUri());
- }
- NotificationCompatApi24.addMessagingStyle(builder, messagingStyle.mUserDisplayName,
- messagingStyle.mConversationTitle, texts, timestamps, senders,
- dataMimeTypes, dataUris);
- } else {
- addStyleToBuilderJellybean(builder, style);
- }
- }
- }
-
static {
if (Build.VERSION.SDK_INT >= 26) {
IMPL = new NotificationCompatApi26Impl();
@@ -1778,12 +1774,6 @@
return this;
}
- /** @deprecated removed from API 26 */
- @Deprecated
- public Builder setChannel(@NonNull String channelId) {
- return setChannelId(channelId);
- }
-
/**
* Specifies the time at which this notification should be canceled, if it is not already
* canceled.
@@ -1793,12 +1783,6 @@
return this;
}
- /** @deprecated removed from API 26 */
- @Deprecated
- public Builder setTimeout(long durationMs) {
- return setTimeoutAfter(durationMs);
- }
-
/**
* If this notification is duplicative of a Launcher shortcut, sets the
* {@link android.support.v4.content.pm.ShortcutInfoCompat#getId() id} of the shortcut, in
@@ -1936,27 +1920,6 @@
public int getColor() {
return mColor;
}
-
-
- /**
- * @return the text of the notification
- *
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- protected CharSequence resolveText() {
- return mContentText;
- }
-
- /**
- * @return the title of the notification
- *
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- protected CharSequence resolveTitle() {
- return mContentTitle;
- }
}
/**
@@ -1967,7 +1930,11 @@
* effect.
*/
public static abstract class Style {
- Builder mBuilder;
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ protected Builder mBuilder;
CharSequence mBigContentTitle;
CharSequence mSummaryText;
boolean mSummaryTextSet = false;
@@ -1994,6 +1961,38 @@
*/
@RestrictTo(LIBRARY_GROUP)
// TODO: implement for all styles
+ public void apply(NotificationBuilderWithBuilderAccessor builder) {
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public RemoteViews makeContentView(NotificationBuilderWithBuilderAccessor builder) {
+ return null;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public RemoteViews makeBigContentView(NotificationBuilderWithBuilderAccessor builder) {
+ return null;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public RemoteViews makeHeadsUpContentView(NotificationBuilderWithBuilderAccessor builder) {
+ return null;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ // TODO: implement for all styles
public void addCompatExtras(Bundle extras) {
}
@@ -2027,9 +2026,9 @@
* @see Notification#bigContentView
*/
public static class BigPictureStyle extends Style {
- Bitmap mPicture;
- Bitmap mBigLargeIcon;
- boolean mBigLargeIconSet;
+ private Bitmap mPicture;
+ private Bitmap mBigLargeIcon;
+ private boolean mBigLargeIconSet;
public BigPictureStyle() {
}
@@ -2072,6 +2071,23 @@
mBigLargeIconSet = true;
return this;
}
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void apply(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 16) {
+ NotificationCompatJellybean.addBigPictureStyle(builder,
+ mBigContentTitle,
+ mSummaryTextSet,
+ mSummaryText,
+ mPicture,
+ mBigLargeIcon,
+ mBigLargeIconSet);
+ }
+ }
}
/**
@@ -2096,7 +2112,7 @@
* @see Notification#bigContentView
*/
public static class BigTextStyle extends Style {
- CharSequence mBigText;
+ private CharSequence mBigText;
public BigTextStyle() {
}
@@ -2131,6 +2147,21 @@
mBigText = Builder.limitCharSequenceLength(cs);
return this;
}
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void apply(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 16) {
+ NotificationCompatJellybean.addBigTextStyle(builder,
+ mBigContentTitle,
+ mSummaryTextSet,
+ mSummaryText,
+ mBigText);
+ }
+ }
}
/**
@@ -2283,6 +2314,121 @@
return style;
}
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void apply(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ List<CharSequence> texts = new ArrayList<>();
+ List<Long> timestamps = new ArrayList<>();
+ List<CharSequence> senders = new ArrayList<>();
+ List<String> dataMimeTypes = new ArrayList<>();
+ List<Uri> dataUris = new ArrayList<>();
+
+ for (MessagingStyle.Message message : mMessages) {
+ texts.add(message.getText());
+ timestamps.add(message.getTimestamp());
+ senders.add(message.getSender());
+ dataMimeTypes.add(message.getDataMimeType());
+ dataUris.add(message.getDataUri());
+ }
+ NotificationCompatApi24.addMessagingStyle(builder, mUserDisplayName,
+ mConversationTitle, texts, timestamps, senders,
+ dataMimeTypes, dataUris);
+ } else {
+ MessagingStyle.Message latestIncomingMessage = findLatestIncomingMessage();
+ // Set the title
+ if (mConversationTitle != null) {
+ builder.getBuilder().setContentTitle(mConversationTitle);
+ } else if (latestIncomingMessage != null) {
+ builder.getBuilder().setContentTitle(latestIncomingMessage.getSender());
+ }
+ // Set the text
+ if (latestIncomingMessage != null) {
+ builder.getBuilder().setContentText(mConversationTitle != null
+ ? makeMessageLine(latestIncomingMessage)
+ : latestIncomingMessage.getText());
+ }
+ // Build a fallback BigTextStyle for API 16-23 devices
+ if (Build.VERSION.SDK_INT >= 16) {
+ SpannableStringBuilder completeMessage = new SpannableStringBuilder();
+ boolean showNames = mConversationTitle != null
+ || hasMessagesWithoutSender();
+ for (int i = mMessages.size() - 1; i >= 0; i--) {
+ MessagingStyle.Message message = mMessages.get(i);
+ CharSequence line;
+ line = showNames ? makeMessageLine(message) : message.getText();
+ if (i != mMessages.size() - 1) {
+ completeMessage.insert(0, "\n");
+ }
+ completeMessage.insert(0, line);
+ }
+ NotificationCompatJellybean.addBigTextStyle(builder,
+ null,
+ false,
+ null,
+ completeMessage);
+ }
+ }
+ }
+
+ @Nullable
+ private MessagingStyle.Message findLatestIncomingMessage() {
+ for (int i = mMessages.size() - 1; i >= 0; i--) {
+ MessagingStyle.Message message = mMessages.get(i);
+ // Incoming messages have a non-empty sender.
+ if (!TextUtils.isEmpty(message.getSender())) {
+ return message;
+ }
+ }
+ if (!mMessages.isEmpty()) {
+ // No incoming messages, fall back to outgoing message
+ return mMessages.get(mMessages.size() - 1);
+ }
+ return null;
+ }
+
+ private boolean hasMessagesWithoutSender() {
+ for (int i = mMessages.size() - 1; i >= 0; i--) {
+ MessagingStyle.Message message = mMessages.get(i);
+ if (message.getSender() == null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private CharSequence makeMessageLine(MessagingStyle.Message message) {
+ BidiFormatter bidi = BidiFormatter.getInstance();
+ SpannableStringBuilder sb = new SpannableStringBuilder();
+ final boolean afterLollipop = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
+ int color = afterLollipop ? Color.BLACK : Color.WHITE;
+ CharSequence replyName = message.getSender();
+ if (TextUtils.isEmpty(message.getSender())) {
+ replyName = mUserDisplayName == null
+ ? "" : mUserDisplayName;
+ color = afterLollipop && mBuilder.getColor() != NotificationCompat.COLOR_DEFAULT
+ ? mBuilder.getColor()
+ : color;
+ }
+ CharSequence senderText = bidi.unicodeWrap(replyName);
+ sb.append(senderText);
+ sb.setSpan(makeFontColorSpan(color),
+ sb.length() - senderText.length(),
+ sb.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE /* flags */);
+ CharSequence text = message.getText() == null ? "" : message.getText();
+ sb.append(" ").append(bidi.unicodeWrap(text));
+ return sb;
+ }
+
+ @NonNull
+ private TextAppearanceSpan makeFontColorSpan(int color) {
+ return new TextAppearanceSpan(null, 0, 0, ColorStateList.valueOf(color), null);
+ }
+
@Override
public void addCompatExtras(Bundle extras) {
super.addCompatExtras(extras);
@@ -2514,7 +2660,7 @@
* @see Notification#bigContentView
*/
public static class InboxStyle extends Style {
- ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>();
+ private ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>();
public InboxStyle() {
}
@@ -2548,6 +2694,21 @@
mTexts.add(Builder.limitCharSequenceLength(cs));
return this;
}
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void apply(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 16) {
+ NotificationCompatJellybean.addInboxStyle(builder,
+ mBigContentTitle,
+ mSummaryTextSet,
+ mSummaryText,
+ mTexts);
+ }
+ }
}
/**
@@ -4415,12 +4576,6 @@
}
}
- /** @deprecated removed from API 26 */
- @Deprecated
- public static String getChannel(Notification notification) {
- return getChannelId(notification);
- }
-
/**
* Returns the time at which this notification should be canceled by the system, if it's not
* canceled already.
@@ -4433,12 +4588,6 @@
}
}
- /** @deprecated removed from API 26 */
- @Deprecated
- public static long getTimeout(Notification notification) {
- return getTimeoutAfter(notification);
- }
-
/**
* Returns what icon should be shown for this notification if it is being displayed in a
* Launcher that supports badging. Will be one of {@link #BADGE_ICON_NONE},
diff --git a/compat/java/android/support/v4/content/res/FontResourcesParserCompat.java b/compat/java/android/support/v4/content/res/FontResourcesParserCompat.java
index d08d598..7fe86ad 100644
--- a/compat/java/android/support/v4/content/res/FontResourcesParserCompat.java
+++ b/compat/java/android/support/v4/content/res/FontResourcesParserCompat.java
@@ -99,16 +99,23 @@
* A class that represents a font element in an xml file which points to a file in resources.
*/
public static final class FontFileResourceEntry {
+ private final @NonNull String mFileName;
private int mWeight;
private boolean mItalic;
private int mResourceId;
- public FontFileResourceEntry(int weight, boolean italic, int resourceId) {
+ public FontFileResourceEntry(@NonNull String fileName, int weight, boolean italic,
+ int resourceId) {
+ mFileName = fileName;
mWeight = weight;
mItalic = italic;
mResourceId = resourceId;
}
+ public @NonNull String getFileName() {
+ return mFileName;
+ }
+
public int getWeight() {
return mWeight;
}
@@ -248,11 +255,12 @@
int weight = array.getInt(R.styleable.FontFamilyFont_fontWeight, NORMAL_WEIGHT);
boolean isItalic = ITALIC == array.getInt(R.styleable.FontFamilyFont_fontStyle, 0);
int resourceId = array.getResourceId(R.styleable.FontFamilyFont_font, 0);
+ String filename = array.getString(R.styleable.FontFamilyFont_font);
array.recycle();
while (parser.next() != XmlPullParser.END_TAG) {
skip(parser);
}
- return new FontFileResourceEntry(weight, isItalic, resourceId);
+ return new FontFileResourceEntry(filename, weight, isItalic, resourceId);
}
private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
diff --git a/compat/java/android/support/v4/content/res/ResourcesCompat.java b/compat/java/android/support/v4/content/res/ResourcesCompat.java
index f20fb35..43d78d0 100644
--- a/compat/java/android/support/v4/content/res/ResourcesCompat.java
+++ b/compat/java/android/support/v4/content/res/ResourcesCompat.java
@@ -27,7 +27,6 @@
import android.content.res.XmlResourceParser;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
-import android.os.Build;
import android.support.annotation.ColorInt;
import android.support.annotation.ColorRes;
import android.support.annotation.DrawableRes;
@@ -194,10 +193,6 @@
if (context.isRestricted()) {
return null;
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- // Use framework support.
- return context.getResources().getFont(id);
- }
return loadFont(context, id, new TypedValue(), Typeface.NORMAL, null);
}
@@ -254,7 +249,7 @@
return TypefaceCompat.createFromResourcesFamilyXml(
context, familyEntry, wrapper, id, style, targetView);
}
- return TypefaceCompat.createFromResourcesFontFile(context, wrapper, id, style);
+ return TypefaceCompat.createFromResourcesFontFile(context, wrapper, id, file, style);
} catch (XmlPullParserException e) {
Log.e(TAG, "Failed to parse xml resource " + file, e);
} catch (IOException e) {
diff --git a/compat/java/android/support/v4/graphics/TypefaceCompat.java b/compat/java/android/support/v4/graphics/TypefaceCompat.java
index 01b9016..6d114b6 100644
--- a/compat/java/android/support/v4/graphics/TypefaceCompat.java
+++ b/compat/java/android/support/v4/graphics/TypefaceCompat.java
@@ -34,8 +34,6 @@
import android.support.v4.util.LruCache;
import android.widget.TextView;
-import java.io.File;
-
/**
* Helper for accessing features in {@link Typeface}.
* @hide
@@ -46,7 +44,10 @@
private static final TypefaceCompatImpl sTypefaceCompatImpl;
static {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && TypefaceCompatApi24Impl.isUsable()) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ sTypefaceCompatImpl = new TypefaceCompatApi26Impl();
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
+ && TypefaceCompatApi24Impl.isUsable()) {
sTypefaceCompatImpl = new TypefaceCompatApi24Impl();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
sTypefaceCompatImpl = new TypefaceCompatApi21Impl();
@@ -69,6 +70,9 @@
Typeface createFromFontInfo(Context context,
@Nullable CancellationSignal cancellationSignal, @NonNull FontInfo[] fonts,
int style);
+
+ Typeface createFromResourcesFontFile(
+ Context context, Resources resources, int id, String path, int style);
}
private TypefaceCompat() {}
@@ -87,7 +91,7 @@
*
* @param resources Resources instance
* @param id a resource id
- * @param a style to be used for this resource, -1 if not availbale.
+ * @param style style to be used for this resource, -1 if not available.
* @return Unique id for a given resource and id.
*/
private static String createResourceUid(final Resources resources, int id, int style) {
@@ -123,28 +127,13 @@
*/
@Nullable
public static Typeface createFromResourcesFontFile(
- Context context, Resources resources, int id, int style) {
- final File tmpFile = TypefaceCompatUtil.getTempFile(context);
- if (tmpFile == null) {
- return null;
+ Context context, Resources resources, int id, String path, int style) {
+ Typeface typeface = sTypefaceCompatImpl.createFromResourcesFontFile(
+ context, resources, id, path, style);
+ if (typeface != null) {
+ sTypefaceCache.put(createResourceUid(resources, id, style), typeface);
}
- try {
- if (!TypefaceCompatUtil.copyToFile(tmpFile, resources, id)) {
- return null;
- }
- Typeface typeface = Typeface.createFromFile(tmpFile.getPath());
- if (typeface != null) {
- sTypefaceCache.put(createResourceUid(resources, id, style), typeface);
- }
- return typeface;
- } catch (RuntimeException e) {
- // This was thrown from Typeface.createFromFile when a Typeface could not be loaded.
- // such as due to an invalid ttf or unreadable file. We don't want to throw that
- // exception anymore.
- return null;
- } finally {
- tmpFile.delete();
- }
+ return typeface;
}
/**
diff --git a/compat/java/android/support/v4/graphics/TypefaceCompatApi24Impl.java b/compat/java/android/support/v4/graphics/TypefaceCompatApi24Impl.java
index 22d5f9d..c64e0c3 100644
--- a/compat/java/android/support/v4/graphics/TypefaceCompatApi24Impl.java
+++ b/compat/java/android/support/v4/graphics/TypefaceCompatApi24Impl.java
@@ -47,7 +47,7 @@
*/
@RestrictTo(LIBRARY_GROUP)
@RequiresApi(24)
-class TypefaceCompatApi24Impl implements TypefaceCompat.TypefaceCompatImpl {
+class TypefaceCompatApi24Impl extends TypefaceCompatBaseImpl {
private static final String TAG = "TypefaceCompatApi24Impl";
private static final String FONT_FAMILY_CLASS = "android.graphics.FontFamily";
@@ -90,6 +90,10 @@
* Returns true if API24 implementation is usable.
*/
public static boolean isUsable() {
+ if (sAddFontWeightStyle == null) {
+ Log.w(TAG, "Unable to collect necessary private methods."
+ + "Fallback to legacy implementation.");
+ }
return sAddFontWeightStyle != null;
}
diff --git a/compat/java/android/support/v4/graphics/TypefaceCompatApi26Impl.java b/compat/java/android/support/v4/graphics/TypefaceCompatApi26Impl.java
new file mode 100644
index 0000000..b217c00
--- /dev/null
+++ b/compat/java/android/support/v4/graphics/TypefaceCompatApi26Impl.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.graphics;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.graphics.fonts.FontVariationAxis;
+import android.net.Uri;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.support.annotation.RestrictTo;
+import android.support.v4.content.res.FontResourcesParserCompat;
+import android.support.v4.content.res.FontResourcesParserCompat.FontFileResourceEntry;
+import android.support.v4.provider.FontsContractCompat;
+import android.util.Log;
+
+import java.io.IOException;
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+import java.util.Map;
+
+/**
+ * Implementation of the Typeface compat methods for API 26 and above.
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+@RequiresApi(26)
+public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl {
+ private static final String TAG = "TypefaceCompatApi26Impl";
+
+ private static final String FONT_FAMILY_CLASS = "android.graphics.FontFamily";
+ private static final String ADD_FONT_FROM_ASSET_MANAGER_METHOD = "addFontFromAssetManager";
+ private static final String ADD_FONT_FROM_BUFFER_METHOD = "addFontFromBuffer";
+ private static final String CREATE_FROM_FAMILIES_WITH_DEFAULT_METHOD =
+ "createFromFamiliesWithDefault";
+ private static final String FREEZE_METHOD = "freeze";
+ private static final String ABORT_CREATION_METHOD = "abortCreation";
+ private static final Class sFontFamily;
+ private static final Constructor sFontFamilyCtor;
+ private static final Method sAddFontFromAssetManager;
+ private static final Method sAddFontFromBuffer;
+ private static final Method sFreeze;
+ private static final Method sAbortCreation;
+ private static final Method sCreateFromFamiliesWithDefault;
+ private static final int RESOLVE_BY_FONT_TABLE = -1;
+
+ static {
+ Class fontFamilyClass;
+ Constructor fontFamilyCtor;
+ Method addFontMethod;
+ Method addFromBufferMethod;
+ Method freezeMethod;
+ Method abortCreationMethod;
+ Method createFromFamiliesWithDefaultMethod;
+ try {
+ fontFamilyClass = Class.forName(FONT_FAMILY_CLASS);
+ fontFamilyCtor = fontFamilyClass.getConstructor();
+ addFontMethod = fontFamilyClass.getMethod(ADD_FONT_FROM_ASSET_MANAGER_METHOD,
+ AssetManager.class, String.class, Integer.TYPE, Boolean.TYPE, Integer.TYPE,
+ Integer.TYPE, Integer.TYPE, FontVariationAxis[].class);
+ addFromBufferMethod = fontFamilyClass.getMethod(ADD_FONT_FROM_BUFFER_METHOD,
+ ByteBuffer.class, Integer.TYPE, FontVariationAxis[].class, Integer.TYPE,
+ Integer.TYPE);
+ freezeMethod = fontFamilyClass.getMethod(FREEZE_METHOD);
+ abortCreationMethod = fontFamilyClass.getMethod(ABORT_CREATION_METHOD);
+ Object familyArray = Array.newInstance(fontFamilyClass, 1);
+ createFromFamiliesWithDefaultMethod =
+ Typeface.class.getDeclaredMethod(CREATE_FROM_FAMILIES_WITH_DEFAULT_METHOD,
+ familyArray.getClass(), Integer.TYPE, Integer.TYPE);
+ createFromFamiliesWithDefaultMethod.setAccessible(true);
+ } catch (ClassNotFoundException | NoSuchMethodException e) {
+ Log.e(TAG, "Unable to collect necessary methods for class " + e.getClass().getName(),
+ e);
+ fontFamilyClass = null;
+ fontFamilyCtor = null;
+ addFontMethod = null;
+ addFromBufferMethod = null;
+ freezeMethod = null;
+ abortCreationMethod = null;
+ createFromFamiliesWithDefaultMethod = null;
+ }
+ sFontFamilyCtor = fontFamilyCtor;
+ sFontFamily = fontFamilyClass;
+ sAddFontFromAssetManager = addFontMethod;
+ sAddFontFromBuffer = addFromBufferMethod;
+ sFreeze = freezeMethod;
+ sAbortCreation = abortCreationMethod;
+ sCreateFromFamiliesWithDefault = createFromFamiliesWithDefaultMethod;
+ }
+
+ /**
+ * Returns true if API26 implementation is usable.
+ */
+ private static boolean isFontFamilyPrivateAPIAvailable() {
+ if (sAddFontFromAssetManager == null) {
+ Log.w(TAG, "Unable to collect necessary private methods."
+ + "Fallback to legacy implementation.");
+ }
+ return sAddFontFromAssetManager != null;
+ }
+
+ /**
+ * Create a new FontFamily instance
+ */
+ private static Object newFamily() {
+ try {
+ return sFontFamilyCtor.newInstance();
+ } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Call FontFamily#addFontFromAssetManager(AssetManager mgr, String path, int cookie,
+ * boolean isAsset, int ttcIndex, int weight, int isItalic, FontVariationAxis[] axes)
+ */
+ private static boolean addFontFromAssetManager(Context context, Object family, String fileName,
+ int ttcIndex, int weight, int style) {
+ try {
+ final Boolean result = (Boolean) sAddFontFromAssetManager.invoke(family,
+ context.getAssets(), fileName, 0 /* cookie */, false /* isAsset */, ttcIndex,
+ weight, style, null /* axes */);
+ return result.booleanValue();
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Call FontFamily#addFontFromBuffer(ByteBuffer font, int ttcIndex, FontVariationAxis[] axes,
+ * int weight, int italic)
+ */
+ private static boolean addFontFromBuffer(Object family, ByteBuffer buffer,
+ int ttcIndex, int weight, int style) {
+ try {
+ final Boolean result = (Boolean) sAddFontFromBuffer.invoke(family,
+ buffer, ttcIndex, null /* axes */, weight, style);
+ return result.booleanValue();
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Call static method Typeface#createFromFamiliesWithDefault(
+ * FontFamily[] families, int weight, int italic)
+ */
+ private static Typeface createFromFamiliesWithDefault(Object family) {
+ try {
+ Object familyArray = Array.newInstance(sFontFamily, 1);
+ Array.set(familyArray, 0, family);
+ return (Typeface) sCreateFromFamiliesWithDefault.invoke(null /* static method */,
+ familyArray, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Call FontFamily#freeze()
+ */
+ private static boolean freeze(Object family) {
+ try {
+ Boolean result = (Boolean) sFreeze.invoke(family);
+ return result.booleanValue();
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Call FontFamily#abortCreation()
+ */
+ private static boolean abortCreation(Object family) {
+ try {
+ Boolean result = (Boolean) sAbortCreation.invoke(family);
+ return result.booleanValue();
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public Typeface createFromFontFamilyFilesResourceEntry(Context context,
+ FontResourcesParserCompat.FontFamilyFilesResourceEntry entry, Resources resources,
+ int style) {
+ if (!isFontFamilyPrivateAPIAvailable()) {
+ return super.createFromFontFamilyFilesResourceEntry(context, entry, resources, style);
+ }
+ Object fontFamily = newFamily();
+ for (final FontFileResourceEntry fontFile : entry.getEntries()) {
+ // TODO: Add ttc and variation font support. (b/37853920)
+ if (!addFontFromAssetManager(context, fontFamily, fontFile.getFileName(),
+ 0 /* ttcIndex */, fontFile.getWeight(), fontFile.isItalic() ? 1 : 0)) {
+ abortCreation(fontFamily);
+ return null;
+ }
+ }
+ if (!freeze(fontFamily)) {
+ return null;
+ }
+ return createFromFamiliesWithDefault(fontFamily);
+ }
+
+ @Override
+ public Typeface createFromFontInfo(Context context,
+ @Nullable CancellationSignal cancellationSignal,
+ @NonNull FontsContractCompat.FontInfo[] fonts, int style) {
+ if (fonts.length < 1) {
+ return null;
+ }
+ if (!isFontFamilyPrivateAPIAvailable()) {
+ // Even if the private API is not avaiable, don't use API 21 implemenation and use
+ // public API to create Typeface from file descriptor.
+ final FontsContractCompat.FontInfo bestFont = findBestInfo(fonts, style);
+ final ContentResolver resolver = context.getContentResolver();
+ try (ParcelFileDescriptor pfd =
+ resolver.openFileDescriptor(bestFont.getUri(), "r", cancellationSignal)) {
+ return new Typeface.Builder(pfd.getFileDescriptor())
+ .setWeight(bestFont.getWeight())
+ .setItalic(bestFont.isItalic())
+ .build();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+ Map<Uri, ByteBuffer> uriBuffer = FontsContractCompat.prepareFontData(
+ context, fonts, cancellationSignal);
+ final Object fontFamily = newFamily();
+ boolean atLeastOneFont = false;
+ for (FontsContractCompat.FontInfo font : fonts) {
+ final ByteBuffer fontBuffer = uriBuffer.get(font.getUri());
+ if (fontBuffer == null) {
+ continue; // skip
+ }
+ final boolean success = addFontFromBuffer(fontFamily, fontBuffer,
+ font.getTtcIndex(), font.getWeight(), font.isItalic() ? 1 : 0);
+ if (!success) {
+ abortCreation(fontFamily);
+ return null;
+ }
+ atLeastOneFont = true;
+ }
+ if (!atLeastOneFont) {
+ abortCreation(fontFamily);
+ return null;
+ }
+ if (!freeze(fontFamily)) {
+ return null;
+ }
+ return createFromFamiliesWithDefault(fontFamily);
+ }
+
+ /**
+ * Used by Resources to load a font resource of type font file.
+ */
+ @Nullable
+ @Override
+ public Typeface createFromResourcesFontFile(
+ Context context, Resources resources, int id, String path, int style) {
+ if (!isFontFamilyPrivateAPIAvailable()) {
+ super.createFromResourcesFontFile(context, resources, id, path, style);
+ }
+ Object fontFamily = newFamily();
+ if (!addFontFromAssetManager(context, fontFamily, path,
+ 0 /* ttcIndex */, RESOLVE_BY_FONT_TABLE /* weight */,
+ RESOLVE_BY_FONT_TABLE /* italic */)) {
+ abortCreation(fontFamily);
+ return null;
+ }
+ if (!freeze(fontFamily)) {
+ return null;
+ }
+ return createFromFamiliesWithDefault(fontFamily);
+ }
+}
diff --git a/compat/java/android/support/v4/graphics/TypefaceCompatBaseImpl.java b/compat/java/android/support/v4/graphics/TypefaceCompatBaseImpl.java
index 55289eb..8cfbd5d 100644
--- a/compat/java/android/support/v4/graphics/TypefaceCompatBaseImpl.java
+++ b/compat/java/android/support/v4/graphics/TypefaceCompatBaseImpl.java
@@ -145,6 +145,32 @@
return null;
}
return TypefaceCompat.createFromResourcesFontFile(
- context, resources, best.getResourceId(), style);
+ context, resources, best.getResourceId(), best.getFileName(), style);
+ }
+
+ /**
+ * Used by Resources to load a font resource of type font file.
+ */
+ @Nullable
+ @Override
+ public Typeface createFromResourcesFontFile(
+ Context context, Resources resources, int id, String path, int style) {
+ final File tmpFile = TypefaceCompatUtil.getTempFile(context);
+ if (tmpFile == null) {
+ return null;
+ }
+ try {
+ if (!TypefaceCompatUtil.copyToFile(tmpFile, resources, id)) {
+ return null;
+ }
+ return Typeface.createFromFile(tmpFile.getPath());
+ } catch (RuntimeException e) {
+ // This was thrown from Typeface.createFromFile when a Typeface could not be loaded.
+ // such as due to an invalid ttf or unreadable file. We don't want to throw that
+ // exception anymore.
+ return null;
+ } finally {
+ tmpFile.delete();
+ }
}
}
diff --git a/compat/java/android/support/v4/graphics/TypefaceCompatUtil.java b/compat/java/android/support/v4/graphics/TypefaceCompatUtil.java
index 8d8beb2..b7d4707 100644
--- a/compat/java/android/support/v4/graphics/TypefaceCompatUtil.java
+++ b/compat/java/android/support/v4/graphics/TypefaceCompatUtil.java
@@ -43,7 +43,7 @@
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
-class TypefaceCompatUtil {
+public class TypefaceCompatUtil {
private static final String TAG = "TypefaceCompatUtil";
private TypefaceCompatUtil() {} // Do not instantiate.
diff --git a/compat/java/android/support/v4/provider/FontsContractCompat.java b/compat/java/android/support/v4/provider/FontsContractCompat.java
index 8f7719a..6ad46a1 100644
--- a/compat/java/android/support/v4/provider/FontsContractCompat.java
+++ b/compat/java/android/support/v4/provider/FontsContractCompat.java
@@ -42,10 +42,12 @@
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
import android.support.annotation.RestrictTo;
import android.support.annotation.VisibleForTesting;
import android.support.v4.content.res.FontResourcesParserCompat;
import android.support.v4.graphics.TypefaceCompat;
+import android.support.v4.graphics.TypefaceCompatUtil;
import android.support.v4.provider.SelfDestructiveThread.ReplyCallback;
import android.support.v4.util.LruCache;
import android.support.v4.util.Preconditions;
@@ -55,11 +57,14 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
+import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.Callable;
/**
@@ -602,6 +607,39 @@
}
/**
+ * A helper function to create a mapping from {@link Uri} to {@link ByteBuffer}.
+ *
+ * Skip if the file contents is not ready to be read.
+ *
+ * @param context A {@link Context} to be used for resolving content URI in
+ * {@link FontInfo}.
+ * @param fonts An array of {@link FontInfo}.
+ * @return A map from {@link Uri} to {@link ByteBuffer}.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @RequiresApi(19)
+ public static Map<Uri, ByteBuffer> prepareFontData(Context context, FontInfo[] fonts,
+ CancellationSignal cancellationSignal) {
+ final HashMap<Uri, ByteBuffer> out = new HashMap<>();
+
+ for (FontInfo font : fonts) {
+ if (font.getResultCode() != Columns.RESULT_CODE_OK) {
+ continue;
+ }
+
+ final Uri uri = font.getUri();
+ if (out.containsKey(uri)) {
+ continue;
+ }
+
+ ByteBuffer buffer = TypefaceCompatUtil.mmap(context, cancellationSignal, uri);
+ out.put(uri, buffer);
+ }
+ return Collections.unmodifiableMap(out);
+ }
+
+ /**
* Fetch fonts given a font request.
*
* @param context A {@link Context} to be used for fetching fonts.
diff --git a/core-utils/java/android/support/v4/text/BidiFormatter.java b/compat/java/android/support/v4/text/BidiFormatter.java
similarity index 100%
rename from core-utils/java/android/support/v4/text/BidiFormatter.java
rename to compat/java/android/support/v4/text/BidiFormatter.java
diff --git a/compat/tests/java/android/support/v4/app/NotificationCompatTest.java b/compat/tests/java/android/support/v4/app/NotificationCompatTest.java
index 7e1ea30..dd870dd 100644
--- a/compat/tests/java/android/support/v4/app/NotificationCompatTest.java
+++ b/compat/tests/java/android/support/v4/app/NotificationCompatTest.java
@@ -41,7 +41,6 @@
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.support.v4.BaseInstrumentationTestCase;
-import android.support.v4.os.BuildCompat;
import org.junit.Before;
import org.junit.Test;
@@ -74,7 +73,7 @@
Notification n = new NotificationCompat.Builder(mActivityTestRule.getActivity())
.setBadgeIconType(badgeIcon)
.build();
- if (BuildCompat.isAtLeastO()) {
+ if (Build.VERSION.SDK_INT >= 26) {
assertEquals(badgeIcon, NotificationCompat.getBadgeIconType(n));
} else {
assertEquals(NotificationCompat.BADGE_ICON_NONE,
@@ -86,12 +85,12 @@
public void testTimeout() throws Throwable {
long timeout = 23552;
Notification n = new NotificationCompat.Builder(mActivityTestRule.getActivity())
- .setTimeout(timeout)
+ .setTimeoutAfter(timeout)
.build();
- if (BuildCompat.isAtLeastO()) {
- assertEquals(timeout, NotificationCompat.getTimeout(n));
+ if (Build.VERSION.SDK_INT >= 26) {
+ assertEquals(timeout, NotificationCompat.getTimeoutAfter(n));
} else {
- assertEquals(0, NotificationCompat.getTimeout(n));
+ assertEquals(0, NotificationCompat.getTimeoutAfter(n));
}
}
@@ -101,7 +100,7 @@
Notification n = new NotificationCompat.Builder(mActivityTestRule.getActivity())
.setShortcutId(shortcutId)
.build();
- if (BuildCompat.isAtLeastO()) {
+ if (Build.VERSION.SDK_INT >= 26) {
assertEquals(shortcutId, NotificationCompat.getShortcutId(n));
} else {
assertEquals(null, NotificationCompat.getShortcutId(n));
@@ -112,12 +111,12 @@
public void testNotificationChannel() throws Throwable {
String channelId = "new ID";
Notification n = new NotificationCompat.Builder(mActivityTestRule.getActivity())
- .setChannel(channelId)
+ .setChannelId(channelId)
.build();
- if (BuildCompat.isAtLeastO()) {
- assertEquals(channelId, NotificationCompat.getChannel(n));
+ if (Build.VERSION.SDK_INT >= 26) {
+ assertEquals(channelId, NotificationCompat.getChannelId(n));
} else {
- assertNull(NotificationCompat.getChannel(n));
+ assertNull(NotificationCompat.getChannelId(n));
}
}
@@ -126,17 +125,17 @@
String channelId = "new ID";
Notification n = new NotificationCompat.Builder(mActivityTestRule.getActivity(), channelId)
.build();
- if (BuildCompat.isAtLeastO()) {
- assertEquals(channelId, NotificationCompat.getChannel(n));
+ if (Build.VERSION.SDK_INT >= 26) {
+ assertEquals(channelId, NotificationCompat.getChannelId(n));
} else {
- assertNull(NotificationCompat.getChannel(n));
+ assertNull(NotificationCompat.getChannelId(n));
}
}
@Test
public void testNotificationActionBuilder_assignsColorized() throws Throwable {
Notification n = newNotificationBuilder().setColorized(true).build();
- if (BuildCompat.isAtLeastO()) {
+ if (Build.VERSION.SDK_INT >= 26) {
Bundle extras = NotificationCompat.getExtras(n);
assertTrue(Boolean.TRUE.equals(extras.get(EXTRA_COLORIZED)));
}
@@ -145,7 +144,7 @@
@Test
public void testNotificationActionBuilder_unassignesColorized() throws Throwable {
Notification n = newNotificationBuilder().setColorized(false).build();
- if (BuildCompat.isAtLeastO()) {
+ if (Build.VERSION.SDK_INT >= 26) {
Bundle extras = NotificationCompat.getExtras(n);
assertTrue(Boolean.FALSE.equals(extras.get(EXTRA_COLORIZED)));
}
@@ -154,7 +153,7 @@
@Test
public void testNotificationActionBuilder_doesntAssignColorized() throws Throwable {
Notification n = newNotificationBuilder().build();
- if (BuildCompat.isAtLeastO()) {
+ if (Build.VERSION.SDK_INT >= 26) {
Bundle extras = NotificationCompat.getExtras(n);
assertFalse(extras.containsKey(EXTRA_COLORIZED));
}
@@ -180,6 +179,18 @@
assertEquals(a.getAllowGeneratedReplies(), aCopy.getAllowGeneratedReplies());
}
+ @SdkSuppress(minSdkVersion = 24)
+ @TargetApi(24)
+ @Test
+ public void testFrameworkNotificationActionBuilder_setAllowGeneratedRepliesTrue()
+ throws Throwable {
+ Notification notif = new Notification.Builder(mContext)
+ .addAction(new Notification.Action.Builder(0, "title", null)
+ .setAllowGeneratedReplies(true).build()).build();
+ NotificationCompat.Action action = NotificationCompat.getAction(notif, 0);
+ assertTrue(action.getAllowGeneratedReplies());
+ }
+
@Test
public void testNotificationActionBuilder_defaultAllowGeneratedRepliesTrue() throws Throwable {
NotificationCompat.Action a = newActionBuilder().build();
@@ -288,7 +299,7 @@
Notification n = new NotificationCompat.Builder(mActivityTestRule.getActivity())
.setGroupAlertBehavior(GROUP_ALERT_CHILDREN)
.build();
- if (BuildCompat.isAtLeastO()) {
+ if (Build.VERSION.SDK_INT >= 26) {
assertEquals(GROUP_ALERT_CHILDREN, NotificationCompat.getGroupAlertBehavior(n));
} else {
assertEquals(GROUP_ALERT_ALL, NotificationCompat.getGroupAlertBehavior(n));
@@ -318,7 +329,7 @@
.setGroupSummary(false)
.build();
- if (Build.VERSION.SDK_INT >= 20 && !BuildCompat.isAtLeastO()) {
+ if (Build.VERSION.SDK_INT >= 20 && !(Build.VERSION.SDK_INT >= 26)) {
assertNull(n.sound);
assertNull(n.vibrate);
assertTrue((n.defaults & DEFAULT_LIGHTS) != 0);
@@ -373,7 +384,7 @@
.setGroupSummary(false)
.build();
- if (Build.VERSION.SDK_INT >= 20 && !BuildCompat.isAtLeastO()) {
+ if (Build.VERSION.SDK_INT >= 20 && !(Build.VERSION.SDK_INT >= 26)) {
assertNotNull(n.sound);
assertNotNull(n.vibrate);
assertTrue((n.defaults & DEFAULT_LIGHTS) != 0);
@@ -404,7 +415,7 @@
.setGroup(null)
.setGroupSummary(false)
.build();
- if (!BuildCompat.isAtLeastO()) {
+ if (!(Build.VERSION.SDK_INT >= 26)) {
assertNotNull(n.sound);
assertNotNull(n.vibrate);
assertTrue((n.defaults & DEFAULT_LIGHTS) != 0);
diff --git a/compat/tests/java/android/support/v4/content/pm/ShortcutManagerCompatTest.java b/compat/tests/java/android/support/v4/content/pm/ShortcutManagerCompatTest.java
index 54a17c6..77e7ae8 100644
--- a/compat/tests/java/android/support/v4/content/pm/ShortcutManagerCompatTest.java
+++ b/compat/tests/java/android/support/v4/content/pm/ShortcutManagerCompatTest.java
@@ -45,13 +45,13 @@
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.Bitmap;
+import android.os.Build;
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.v4.BaseInstrumentationTestCase;
import android.support.v4.app.TestSupportActivity;
-import android.support.v4.os.BuildCompat;
import org.junit.Before;
import org.junit.Test;
@@ -86,7 +86,7 @@
@SmallTest
@TargetApi(26)
public void testIsRequestPinShortcutSupported_v26() throws Throwable {
- if (!BuildCompat.isAtLeastO()) {
+ if (!(Build.VERSION.SDK_INT >= 26)) {
return;
}
@@ -104,7 +104,7 @@
@SmallTest
@TargetApi(26)
public void testRequestPinShortcut_v26() throws Throwable {
- if (!BuildCompat.isAtLeastO()) {
+ if (!(Build.VERSION.SDK_INT >= 26)) {
return;
}
@@ -124,7 +124,7 @@
@SmallTest
@TargetApi(26)
public void testCreateShortcutResultIntent_v26() throws Throwable {
- if (!BuildCompat.isAtLeastO()) {
+ if (!(Build.VERSION.SDK_INT >= 26)) {
return;
}
@@ -146,7 +146,7 @@
@SmallTest
@Test
public void testIsRequestPinShortcutSupported_v4() throws Throwable {
- if (BuildCompat.isAtLeastO()) {
+ if (Build.VERSION.SDK_INT >= 26) {
return;
}
setMockPm(mockResolveInfo(null));
@@ -173,7 +173,7 @@
@LargeTest
@Test
public void testRequestPinShortcut_v4_noCallback() throws Throwable {
- if (BuildCompat.isAtLeastO()) {
+ if (Build.VERSION.SDK_INT >= 26) {
return;
}
@@ -188,7 +188,7 @@
@MediumTest
@Test
public void testRequestPinShortcut_v4_withCallback() throws Throwable {
- if (BuildCompat.isAtLeastO()) {
+ if (Build.VERSION.SDK_INT >= 26) {
return;
}
@@ -209,7 +209,7 @@
@SmallTest
@Test
public void testCreateShortcutResultIntent_v4() throws Throwable {
- if (BuildCompat.isAtLeastO()) {
+ if (Build.VERSION.SDK_INT >= 26) {
return;
}
diff --git a/compat/tests/java/android/support/v4/graphics/PaintCompatHasGlyphTest.java b/compat/tests/java/android/support/v4/graphics/PaintCompatHasGlyphTest.java
index 26f0691..f17c881 100644
--- a/compat/tests/java/android/support/v4/graphics/PaintCompatHasGlyphTest.java
+++ b/compat/tests/java/android/support/v4/graphics/PaintCompatHasGlyphTest.java
@@ -47,6 +47,7 @@
{"\t\t\t", false}, // more white space
{"☺", SDK_INT >= 16}, // glyph added in API 16
{"\uD83D\uDC66\uD83C\uDFFF", SDK_INT >= 24}, // glyph added in API 24
+ {"\uD83C\uDDEF\uD83C\uDDF5", SDK_INT >= 20}, // Japan flag emoji, added in API 20
});
}
diff --git a/compat/tests/java/android/support/v4/graphics/TypefaceCompatTest.java b/compat/tests/java/android/support/v4/graphics/TypefaceCompatTest.java
index a2f2c7c..dab7f0f 100644
--- a/compat/tests/java/android/support/v4/graphics/TypefaceCompatTest.java
+++ b/compat/tests/java/android/support/v4/graphics/TypefaceCompatTest.java
@@ -17,7 +17,9 @@
package android.support.v4.graphics;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import android.annotation.SuppressLint;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.PackageInfo;
@@ -28,7 +30,6 @@
import android.graphics.Typeface;
import android.support.compat.test.R;
import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SdkSuppress;
import android.support.test.filters.SmallTest;
import android.support.testutils.PollingCheck;
import android.support.v4.content.res.FontResourcesParserCompat;
@@ -47,11 +48,8 @@
import java.util.ArrayList;
import java.util.List;
-@SdkSuppress(maxSdkVersion = 25) // on API 26, use platform implementation.
@SmallTest
public class TypefaceCompatTest {
- private static final String AUTHORITY = "android.provider.fonts.font";
- private static final String PACKAGE = "android.support.compat.test";
public Context mContext;
public Resources mResources;
@@ -225,6 +223,8 @@
@Test
public void testCreateFromResourcesFamilyXml_resourceFont() throws Exception {
+ @SuppressLint("ResourceType")
+ // We are retrieving the XML font as an XML resource for testing purposes.
final FamilyResourceEntry entry = FontResourcesParserCompat.parse(
mResources.getXml(R.font.styletestfont), mResources);
Typeface typeface = TypefaceCompat.createFromResourcesFamilyXml(mContext, entry, mResources,
@@ -262,14 +262,16 @@
@Test
public void testCreateFromResourcesFontFile() {
- Typeface typeface = TypefaceCompat.createFromResourcesFontFile(
- mContext, mResources, R.font.large_a, Typeface.NORMAL);
+ Typeface typeface = TypefaceCompat.createFromResourcesFontFile(mContext, mResources,
+ R.font.large_a, "res/font/large_a.ttf", Typeface.NORMAL);
+ assertNotNull(typeface);
assertEquals(typeface, TypefaceCompat.findFromCache(
mResources, R.font.large_a, Typeface.NORMAL));
assertEquals(R.font.large_a, getSelectedFontResourceId(typeface));
- typeface = TypefaceCompat.createFromResourcesFontFile(
- mContext, mResources, R.font.large_b, Typeface.NORMAL);
+ typeface = TypefaceCompat.createFromResourcesFontFile(mContext, mResources, R.font.large_b,
+ "res/font/large_b.ttf", Typeface.NORMAL);
+ assertNotNull(typeface);
assertEquals(typeface, TypefaceCompat.findFromCache(
mResources, R.font.large_b, Typeface.NORMAL));
assertEquals(R.font.large_b, getSelectedFontResourceId(typeface));
diff --git a/compat/tests/java/android/support/v4/graphics/drawable/IconCompatTest.java b/compat/tests/java/android/support/v4/graphics/drawable/IconCompatTest.java
index ddf56ed..d87ddac 100644
--- a/compat/tests/java/android/support/v4/graphics/drawable/IconCompatTest.java
+++ b/compat/tests/java/android/support/v4/graphics/drawable/IconCompatTest.java
@@ -34,7 +34,6 @@
import android.support.test.filters.SdkSuppress;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
-import android.support.v4.os.BuildCompat;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -105,7 +104,7 @@
bitmap.eraseColor(Color.GREEN);
IconCompat compat = IconCompat.createWithAdaptiveBitmap(bitmap);
Drawable d = compat.toIcon().loadDrawable(InstrumentationRegistry.getContext());
- if (BuildCompat.isAtLeastO()) {
+ if (Build.VERSION.SDK_INT >= 26) {
assertTrue(d instanceof AdaptiveIconDrawable);
} else {
assertTrue(d instanceof BitmapDrawable);
diff --git a/core-utils/tests/java/android/support/v4/text/BidiFormatterTest.java b/compat/tests/java/android/support/v4/text/BidiFormatterTest.java
similarity index 100%
rename from core-utils/tests/java/android/support/v4/text/BidiFormatterTest.java
rename to compat/tests/java/android/support/v4/text/BidiFormatterTest.java
diff --git a/compat/tests/res/font/dummyproviderfont.xml b/compat/tests/res/font/dummyproviderfont.xml
index b0b41c3..6295c91 100644
--- a/compat/tests/res/font/dummyproviderfont.xml
+++ b/compat/tests/res/font/dummyproviderfont.xml
@@ -1,10 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<font-family xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:fontProviderAuthority="android.provider.fonts.font"
- android:fontProviderPackage="android.support.compat.test"
- android:fontProviderQuery="styleTest"
- app:fontProviderAuthority="android.provider.fonts.font"
- app:fontProviderPackage="android.support.compat.test"
- app:fontProviderQuery="styleTest" >
+<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
+ app:fontProviderAuthority="android.provider.fonts.font"
+ app:fontProviderPackage="android.support.compat.test"
+ app:fontProviderQuery="styleTest" >
</font-family>
diff --git a/compat/tests/res/font/invalid_xmlfamily.xml b/compat/tests/res/font/invalid_xmlfamily.xml
index 02ae6bb..ea9ec97 100644
--- a/compat/tests/res/font/invalid_xmlfamily.xml
+++ b/compat/tests/res/font/invalid_xmlfamily.xml
@@ -1,5 +1,3 @@
-<invalid-tag xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
- <font android:fontStyle="normal" android:fontWeight="400" android:font="@font/samplefont"
- app:fontStyle="normal" app:fontWeight="400" app:font="@font/samplefont" />
+<invalid-tag xmlns:app="http://schemas.android.com/apk/res-auto">
+ <font app:fontStyle="normal" app:fontWeight="400" app:font="@font/samplefont" />
</invalid-tag>
diff --git a/compat/tests/res/font/invalid_xmlfont.xml b/compat/tests/res/font/invalid_xmlfont.xml
index d4bffb8..4d3afc2 100644
--- a/compat/tests/res/font/invalid_xmlfont.xml
+++ b/compat/tests/res/font/invalid_xmlfont.xml
@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<font-family xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
+<font-family xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- the tag inside font-family must be 'font' -->
- <ttf android:fontStyle="normal" android:fontWeight="400" android:font="@font/samplefont"
- app:fontStyle="normal" app:fontWeight="400" app:font="@font/samplefont" />
+ <ttf app:fontStyle="normal" app:fontWeight="400" app:font="@font/samplefont" />
</font-family>
diff --git a/compat/tests/res/font/invalid_xmlfont_contains_invalid_font_file.xml b/compat/tests/res/font/invalid_xmlfont_contains_invalid_font_file.xml
index b87db2b..f876e15 100644
--- a/compat/tests/res/font/invalid_xmlfont_contains_invalid_font_file.xml
+++ b/compat/tests/res/font/invalid_xmlfont_contains_invalid_font_file.xml
@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
-<font-family xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
- <font android:fontStyle="normal" android:fontWeight="400" android:font="@font/invalid_font"
- app:fontStyle="normal" app:fontWeight="400" app:font="@font/invalid_font" />
+<font-family xmlns:app="http://schemas.android.com/apk/res-auto">
+ <font app:fontStyle="normal" app:fontWeight="400" app:font="@font/invalid_font" />
</font-family>
diff --git a/compat/tests/res/font/samplexmldownloadedfont.xml b/compat/tests/res/font/samplexmldownloadedfont.xml
index 59a0a05..659d196 100644
--- a/compat/tests/res/font/samplexmldownloadedfont.xml
+++ b/compat/tests/res/font/samplexmldownloadedfont.xml
@@ -1,10 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<font-family xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:fontProviderAuthority="com.example.test.fontprovider.authority"
- android:fontProviderPackage="com.example.test.fontprovider.package"
- android:fontProviderQuery="MyRequestedFont"
- app:fontProviderAuthority="com.example.test.fontprovider.authority"
- app:fontProviderPackage="com.example.test.fontprovider.package"
- app:fontProviderQuery="MyRequestedFont">
-</font-family>
\ No newline at end of file
+<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
+ app:fontProviderAuthority="com.example.test.fontprovider.authority"
+ app:fontProviderPackage="com.example.test.fontprovider.package"
+ app:fontProviderQuery="MyRequestedFont">
+</font-family>
diff --git a/compat/tests/res/font/samplexmlfont.xml b/compat/tests/res/font/samplexmlfont.xml
index dc7fb4f..8a6876d 100644
--- a/compat/tests/res/font/samplexmlfont.xml
+++ b/compat/tests/res/font/samplexmlfont.xml
@@ -1,12 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
- <font app:fontStyle="normal" app:fontWeight="400" app:font="@font/samplefont"
- android:fontStyle="normal" android:fontWeight="400" android:font="@font/samplefont" />
- <font app:fontStyle="italic" app:fontWeight="400" app:font="@font/samplefont2"
- android:fontStyle="italic" android:fontWeight="400" android:font="@font/samplefont2" />
- <font app:fontStyle="normal" app:fontWeight="700" app:font="@font/samplefont3"
- android:fontStyle="normal" android:fontWeight="700" android:font="@font/samplefont3" />
- <font app:fontStyle="italic" app:fontWeight="700" app:font="@font/samplefont4"
- android:fontStyle="italic" android:fontWeight="700" android:font="@font/samplefont4" />
+ <font app:fontStyle="normal" app:fontWeight="400" app:font="@font/samplefont" />
+ <font app:fontStyle="italic" app:fontWeight="400" app:font="@font/samplefont2" />
+ <font app:fontStyle="normal" app:fontWeight="700" app:font="@font/samplefont3" />
+ <font app:fontStyle="italic" app:fontWeight="700" app:font="@font/samplefont4" />
</font-family>
diff --git a/compat/tests/res/font/styletest_async_providerfont.xml b/compat/tests/res/font/styletest_async_providerfont.xml
index 8d934cb..aa28179 100644
--- a/compat/tests/res/font/styletest_async_providerfont.xml
+++ b/compat/tests/res/font/styletest_async_providerfont.xml
@@ -1,11 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
-<font-family xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:fontProviderAuthority="android.support.provider.fonts.font"
- android:fontProviderPackage="android.support.compat.test"
- android:fontProviderQuery="styleTest"
- app:fontProviderAuthority="android.support.provider.fonts.font"
- app:fontProviderPackage="android.support.compat.test"
- app:fontProviderQuery="styleTest"
- app:fontProviderFetchStrategy="async">
+<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
+ app:fontProviderAuthority="android.support.provider.fonts.font"
+ app:fontProviderPackage="android.support.compat.test"
+ app:fontProviderQuery="styleTest"
+ app:fontProviderFetchStrategy="async">
</font-family>
diff --git a/compat/tests/res/font/styletest_sync_providerfont.xml b/compat/tests/res/font/styletest_sync_providerfont.xml
index 4e9c7d6..f89301b 100644
--- a/compat/tests/res/font/styletest_sync_providerfont.xml
+++ b/compat/tests/res/font/styletest_sync_providerfont.xml
@@ -1,12 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
-<font-family xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:fontProviderAuthority="android.support.provider.fonts.font"
- android:fontProviderPackage="android.support.compat.test"
- android:fontProviderQuery="styleTest"
- app:fontProviderAuthority="android.support.provider.fonts.font"
- app:fontProviderPackage="android.support.compat.test"
- app:fontProviderQuery="styleTest"
- app:fontProviderFetchStrategy="blocking"
- app:fontProviderFetchTimeout="forever">
+<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
+ app:fontProviderAuthority="android.support.provider.fonts.font"
+ app:fontProviderPackage="android.support.compat.test"
+ app:fontProviderQuery="styleTest"
+ app:fontProviderFetchStrategy="blocking"
+ app:fontProviderFetchTimeout="forever">
</font-family>
diff --git a/compat/tests/res/font/styletestfont.xml b/compat/tests/res/font/styletestfont.xml
index f1326d5..dca3fe2 100644
--- a/compat/tests/res/font/styletestfont.xml
+++ b/compat/tests/res/font/styletestfont.xml
@@ -1,12 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
-<font-family xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
- <font app:fontStyle="normal" app:fontWeight="400" app:font="@font/large_a"
- android:fontStyle="normal" android:fontWeight="400" android:font="@font/large_a" />
- <font app:fontStyle="italic" app:fontWeight="400" app:font="@font/large_b"
- android:fontStyle="italic" android:fontWeight="400" android:font="@font/large_b" />
- <font app:fontStyle="normal" app:fontWeight="700" app:font="@font/large_c"
- android:fontStyle="normal" android:fontWeight="700" android:font="@font/large_c" />
- <font app:fontStyle="italic" app:fontWeight="700" app:font="@font/large_d"
- android:fontStyle="italic" android:fontWeight="700" android:font="@font/large_d" />
+<font-family xmlns:app="http://schemas.android.com/apk/res-auto">
+ <font app:fontStyle="normal" app:fontWeight="400" app:font="@font/large_a" />
+ <font app:fontStyle="italic" app:fontWeight="400" app:font="@font/large_b" />
+ <font app:fontStyle="normal" app:fontWeight="700" app:font="@font/large_c" />
+ <font app:fontStyle="italic" app:fontWeight="700" app:font="@font/large_d" />
</font-family>
diff --git a/core-ui/Android.mk b/core-ui/Android.mk
index 9e4ad0e..bb7a4be 100644
--- a/core-ui/Android.mk
+++ b/core-ui/Android.mk
@@ -26,11 +26,7 @@
LOCAL_USE_AAPT2 := true
LOCAL_MODULE := android-support-core-ui
LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
-LOCAL_SRC_FILES := \
- $(call all-java-files-under,ics) \
- $(call all-java-files-under,jellybean-mr2) \
- $(call all-java-files-under,api21) \
- $(call all-java-files-under,java)
+LOCAL_SRC_FILES := $(call all-java-files-under,java)
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
LOCAL_SHARED_ANDROID_LIBRARIES := \
android-support-compat \
diff --git a/core-ui/api21/android/support/v4/widget/DrawerLayoutCompatApi21.java b/core-ui/api21/android/support/v4/widget/DrawerLayoutCompatApi21.java
deleted file mode 100644
index f0612d1..0000000
--- a/core-ui/api21/android/support/v4/widget/DrawerLayoutCompatApi21.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package android.support.v4.widget;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.RequiresApi;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowInsets;
-
-/**
- * Provides functionality for DrawerLayout unique to API 21
- */
-@RequiresApi(21)
-class DrawerLayoutCompatApi21 {
-
- private static final int[] THEME_ATTRS = {
- android.R.attr.colorPrimaryDark
- };
-
- public static void configureApplyInsets(View drawerLayout) {
- if (drawerLayout instanceof DrawerLayoutImpl) {
- drawerLayout.setOnApplyWindowInsetsListener(new InsetsListener());
- drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
- | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
- }
- }
-
- public static void dispatchChildInsets(View child, Object insets, int gravity) {
- WindowInsets wi = (WindowInsets) insets;
- if (gravity == Gravity.LEFT) {
- wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(),
- wi.getSystemWindowInsetTop(), 0, wi.getSystemWindowInsetBottom());
- } else if (gravity == Gravity.RIGHT) {
- wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(),
- wi.getSystemWindowInsetRight(), wi.getSystemWindowInsetBottom());
- }
- child.dispatchApplyWindowInsets(wi);
- }
-
- public static void applyMarginInsets(ViewGroup.MarginLayoutParams lp, Object insets,
- int gravity) {
- WindowInsets wi = (WindowInsets) insets;
- if (gravity == Gravity.LEFT) {
- wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(),
- wi.getSystemWindowInsetTop(), 0, wi.getSystemWindowInsetBottom());
- } else if (gravity == Gravity.RIGHT) {
- wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(),
- wi.getSystemWindowInsetRight(), wi.getSystemWindowInsetBottom());
- }
- lp.leftMargin = wi.getSystemWindowInsetLeft();
- lp.topMargin = wi.getSystemWindowInsetTop();
- lp.rightMargin = wi.getSystemWindowInsetRight();
- lp.bottomMargin = wi.getSystemWindowInsetBottom();
- }
-
- public static int getTopInset(Object insets) {
- return insets != null ? ((WindowInsets) insets).getSystemWindowInsetTop() : 0;
- }
-
- public static Drawable getDefaultStatusBarBackground(Context context) {
- final TypedArray a = context.obtainStyledAttributes(THEME_ATTRS);
- try {
- return a.getDrawable(0);
- } finally {
- a.recycle();
- }
- }
-
- static class InsetsListener implements View.OnApplyWindowInsetsListener {
- @Override
- public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
- final DrawerLayoutImpl drawerLayout = (DrawerLayoutImpl) v;
- drawerLayout.setChildInsets(insets, insets.getSystemWindowInsetTop() > 0);
- return insets.consumeSystemWindowInsets();
- }
- }
-}
diff --git a/core-ui/api21/android/support/v4/widget/DrawerLayoutImpl.java b/core-ui/api21/android/support/v4/widget/DrawerLayoutImpl.java
deleted file mode 100644
index 02545c1..0000000
--- a/core-ui/api21/android/support/v4/widget/DrawerLayoutImpl.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package android.support.v4.widget;
-
-/**
- * Interface used to communicate from the v21-specific code for configuring a DrawerLayout
- * to the DrawerLayout itself.
- */
-interface DrawerLayoutImpl {
- void setChildInsets(Object insets, boolean drawStatusBar);
-}
diff --git a/core-ui/build.gradle b/core-ui/build.gradle
index 2126921..2cd1806 100644
--- a/core-ui/build.gradle
+++ b/core-ui/build.gradle
@@ -23,9 +23,6 @@
sourceSets {
main.java.srcDirs = [
- 'ics',
- 'jellybean-mr2',
- 'api21',
'java'
]
}
diff --git a/core-ui/ics/android/support/v4/app/ActionBarDrawerToggleIcs.java b/core-ui/ics/android/support/v4/app/ActionBarDrawerToggleIcs.java
deleted file mode 100644
index a332b8a..0000000
--- a/core-ui/ics/android/support/v4/app/ActionBarDrawerToggleIcs.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.v4.app;
-
-import android.R;
-import android.app.ActionBar;
-import android.app.Activity;
-import android.content.res.TypedArray;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.support.annotation.RequiresApi;
-import android.util.Log;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-
-import java.lang.reflect.Method;
-
-/**
- * This class encapsulates some awful hacks.
- *
- * Before JB-MR2 (API 18) it was not possible to change the home-as-up indicator glyph
- * in an action bar without some really gross hacks. Since the MR2 SDK is not published as of
- * this writing, the new API is accessed via reflection here if available.
- */
-@RequiresApi(14)
-class ActionBarDrawerToggleIcs {
- private static final String TAG = "ActionBarDrawerToggle";
-
- private static final int[] THEME_ATTRS = new int[] {
- R.attr.homeAsUpIndicator
- };
-
- public static Object setActionBarUpIndicator(Object info, Activity activity,
- Drawable drawable, int contentDescRes) {
- if (info == null) {
- info = new SetIndicatorInfo(activity);
- }
- final SetIndicatorInfo sii = (SetIndicatorInfo) info;
- if (sii.setHomeAsUpIndicator != null) {
- try {
- final ActionBar actionBar = activity.getActionBar();
- sii.setHomeAsUpIndicator.invoke(actionBar, drawable);
- sii.setHomeActionContentDescription.invoke(actionBar, contentDescRes);
- } catch (Exception e) {
- Log.w(TAG, "Couldn't set home-as-up indicator via JB-MR2 API", e);
- }
- } else if (sii.upIndicatorView != null) {
- sii.upIndicatorView.setImageDrawable(drawable);
- } else {
- Log.w(TAG, "Couldn't set home-as-up indicator");
- }
- return info;
- }
-
- public static Object setActionBarDescription(Object info, Activity activity,
- int contentDescRes) {
- if (info == null) {
- info = new SetIndicatorInfo(activity);
- }
- final SetIndicatorInfo sii = (SetIndicatorInfo) info;
- if (sii.setHomeAsUpIndicator != null) {
- try {
- final ActionBar actionBar = activity.getActionBar();
- sii.setHomeActionContentDescription.invoke(actionBar, contentDescRes);
- if (Build.VERSION.SDK_INT <= 19) {
- // For API 19 and earlier, we need to manually force the
- // action bar to generate a new content description.
- actionBar.setSubtitle(actionBar.getSubtitle());
- }
- } catch (Exception e) {
- Log.w(TAG, "Couldn't set content description via JB-MR2 API", e);
- }
- }
- return info;
- }
-
- public static Drawable getThemeUpIndicator(Activity activity) {
- final TypedArray a = activity.obtainStyledAttributes(THEME_ATTRS);
- final Drawable result = a.getDrawable(0);
- a.recycle();
- return result;
- }
-
- private static class SetIndicatorInfo {
- public Method setHomeAsUpIndicator;
- public Method setHomeActionContentDescription;
- public ImageView upIndicatorView;
-
- SetIndicatorInfo(Activity activity) {
- try {
- setHomeAsUpIndicator = ActionBar.class.getDeclaredMethod("setHomeAsUpIndicator",
- Drawable.class);
- setHomeActionContentDescription = ActionBar.class.getDeclaredMethod(
- "setHomeActionContentDescription", Integer.TYPE);
-
- // If we got the method we won't need the stuff below.
- return;
- } catch (NoSuchMethodException e) {
- // Oh well. We'll use the other mechanism below instead.
- }
-
- final View home = activity.findViewById(android.R.id.home);
- if (home == null) {
- // Action bar doesn't have a known configuration, an OEM messed with things.
- return;
- }
-
- final ViewGroup parent = (ViewGroup) home.getParent();
- final int childCount = parent.getChildCount();
- if (childCount != 2) {
- // No idea which one will be the right one, an OEM messed with things.
- return;
- }
-
- final View first = parent.getChildAt(0);
- final View second = parent.getChildAt(1);
- final View up = first.getId() == android.R.id.home ? second : first;
-
- if (up instanceof ImageView) {
- // Jackpot! (Probably...)
- upIndicatorView = (ImageView) up;
- }
- }
- }
-}
diff --git a/core-ui/java/android/support/v4/app/ActionBarDrawerToggle.java b/core-ui/java/android/support/v4/app/ActionBarDrawerToggle.java
index 226154f..d95e42c 100644
--- a/core-ui/java/android/support/v4/app/ActionBarDrawerToggle.java
+++ b/core-ui/java/android/support/v4/app/ActionBarDrawerToggle.java
@@ -17,24 +17,31 @@
package android.support.v4.app;
+import android.app.ActionBar;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
+import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.os.Build;
import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.support.annotation.RequiresApi;
import android.support.annotation.StringRes;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.DrawerLayout;
+import android.util.Log;
import android.view.MenuItem;
import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import java.lang.reflect.Method;
/**
* This class provides a handy way to tie together the functionality of
@@ -107,71 +114,11 @@
void setActionBarDescription(@StringRes int contentDescRes);
}
- private interface ActionBarDrawerToggleImpl {
- Drawable getThemeUpIndicator(Activity activity);
- Object setActionBarUpIndicator(Object info, Activity activity,
- Drawable themeImage, int contentDescRes);
- Object setActionBarDescription(Object info, Activity activity, int contentDescRes);
- }
+ private static final String TAG = "ActionBarDrawerToggle";
- @RequiresApi(11)
- private static class ActionBarDrawerToggleImplIcs implements ActionBarDrawerToggleImpl {
- ActionBarDrawerToggleImplIcs() {
- }
-
- @Override
- public Drawable getThemeUpIndicator(Activity activity) {
- return ActionBarDrawerToggleIcs.getThemeUpIndicator(activity);
- }
-
- @Override
- public Object setActionBarUpIndicator(Object info, Activity activity,
- Drawable themeImage, int contentDescRes) {
- return ActionBarDrawerToggleIcs.setActionBarUpIndicator(info, activity,
- themeImage, contentDescRes);
- }
-
- @Override
- public Object setActionBarDescription(Object info, Activity activity, int contentDescRes) {
- return ActionBarDrawerToggleIcs.setActionBarDescription(info, activity,
- contentDescRes);
- }
- }
-
- @RequiresApi(18)
- private static class ActionBarDrawerToggleImplJellybeanMR2
- implements ActionBarDrawerToggleImpl {
- ActionBarDrawerToggleImplJellybeanMR2() {
- }
-
- @Override
- public Drawable getThemeUpIndicator(Activity activity) {
- return ActionBarDrawerToggleJellybeanMR2.getThemeUpIndicator(activity);
- }
-
- @Override
- public Object setActionBarUpIndicator(Object info, Activity activity,
- Drawable themeImage, int contentDescRes) {
- return ActionBarDrawerToggleJellybeanMR2.setActionBarUpIndicator(info, activity,
- themeImage, contentDescRes);
- }
-
- @Override
- public Object setActionBarDescription(Object info, Activity activity, int contentDescRes) {
- return ActionBarDrawerToggleJellybeanMR2.setActionBarDescription(info, activity,
- contentDescRes);
- }
- }
-
- private static final ActionBarDrawerToggleImpl IMPL;
-
- static {
- if (Build.VERSION.SDK_INT >= 18) {
- IMPL = new ActionBarDrawerToggleImplJellybeanMR2();
- } else {
- IMPL = new ActionBarDrawerToggleImplIcs();
- }
- }
+ private static final int[] THEME_ATTRS = new int[] {
+ android.R.attr.homeAsUpIndicator
+ };
/** Fraction of its total width by which to offset the toggle drawable. */
private static final float TOGGLE_DRAWABLE_OFFSET = 1 / 3f;
@@ -192,7 +139,7 @@
private final int mOpenDrawerContentDescRes;
private final int mCloseDrawerContentDescRes;
- private Object mSetIndicatorInfo;
+ private SetIndicatorInfo mSetIndicatorInfo;
/**
* Construct a new ActionBarDrawerToggle.
@@ -463,29 +410,133 @@
public void onDrawerStateChanged(int newState) {
}
- Drawable getThemeUpIndicator() {
+ private Drawable getThemeUpIndicator() {
if (mActivityImpl != null) {
return mActivityImpl.getThemeUpIndicator();
}
- return IMPL.getThemeUpIndicator(mActivity);
+ if (Build.VERSION.SDK_INT >= 18) {
+ final ActionBar actionBar = mActivity.getActionBar();
+ final Context context;
+ if (actionBar != null) {
+ context = actionBar.getThemedContext();
+ } else {
+ context = mActivity;
+ }
+
+ final TypedArray a = context.obtainStyledAttributes(null, THEME_ATTRS,
+ android.R.attr.actionBarStyle, 0);
+ final Drawable result = a.getDrawable(0);
+ a.recycle();
+ return result;
+ } else {
+ final TypedArray a = mActivity.obtainStyledAttributes(THEME_ATTRS);
+ final Drawable result = a.getDrawable(0);
+ a.recycle();
+ return result;
+ }
}
- void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) {
+ private void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) {
if (mActivityImpl != null) {
mActivityImpl.setActionBarUpIndicator(upDrawable, contentDescRes);
return;
}
- mSetIndicatorInfo = IMPL
- .setActionBarUpIndicator(mSetIndicatorInfo, mActivity, upDrawable, contentDescRes);
+ if (Build.VERSION.SDK_INT >= 18) {
+ final ActionBar actionBar = mActivity.getActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeAsUpIndicator(upDrawable);
+ actionBar.setHomeActionContentDescription(contentDescRes);
+ }
+ } else {
+ if (mSetIndicatorInfo == null) {
+ mSetIndicatorInfo = new SetIndicatorInfo(mActivity);
+ }
+ if (mSetIndicatorInfo.mSetHomeAsUpIndicator != null) {
+ try {
+ final ActionBar actionBar = mActivity.getActionBar();
+ mSetIndicatorInfo.mSetHomeAsUpIndicator.invoke(actionBar, upDrawable);
+ mSetIndicatorInfo.mSetHomeActionContentDescription.invoke(
+ actionBar, contentDescRes);
+ } catch (Exception e) {
+ Log.w(TAG, "Couldn't set home-as-up indicator via JB-MR2 API", e);
+ }
+ } else if (mSetIndicatorInfo.mUpIndicatorView != null) {
+ mSetIndicatorInfo.mUpIndicatorView.setImageDrawable(upDrawable);
+ } else {
+ Log.w(TAG, "Couldn't set home-as-up indicator");
+ }
+ }
}
- void setActionBarDescription(int contentDescRes) {
+ private void setActionBarDescription(int contentDescRes) {
if (mActivityImpl != null) {
mActivityImpl.setActionBarDescription(contentDescRes);
return;
}
- mSetIndicatorInfo = IMPL
- .setActionBarDescription(mSetIndicatorInfo, mActivity, contentDescRes);
+ if (Build.VERSION.SDK_INT >= 18) {
+ final ActionBar actionBar = mActivity.getActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeActionContentDescription(contentDescRes);
+ }
+ } else {
+ if (mSetIndicatorInfo == null) {
+ mSetIndicatorInfo = new SetIndicatorInfo(mActivity);
+ }
+ if (mSetIndicatorInfo.mSetHomeAsUpIndicator != null) {
+ try {
+ final ActionBar actionBar = mActivity.getActionBar();
+ mSetIndicatorInfo.mSetHomeActionContentDescription.invoke(
+ actionBar, contentDescRes);
+ // For API 19 and earlier, we need to manually force the
+ // action bar to generate a new content description.
+ actionBar.setSubtitle(actionBar.getSubtitle());
+ } catch (Exception e) {
+ Log.w(TAG, "Couldn't set content description via JB-MR2 API", e);
+ }
+ }
+ }
+ }
+
+ private static class SetIndicatorInfo {
+ Method mSetHomeAsUpIndicator;
+ Method mSetHomeActionContentDescription;
+ ImageView mUpIndicatorView;
+
+ SetIndicatorInfo(Activity activity) {
+ try {
+ mSetHomeAsUpIndicator = ActionBar.class.getDeclaredMethod("setHomeAsUpIndicator",
+ Drawable.class);
+ mSetHomeActionContentDescription = ActionBar.class.getDeclaredMethod(
+ "setHomeActionContentDescription", Integer.TYPE);
+
+ // If we got the method we won't need the stuff below.
+ return;
+ } catch (NoSuchMethodException e) {
+ // Oh well. We'll use the other mechanism below instead.
+ }
+
+ final View home = activity.findViewById(android.R.id.home);
+ if (home == null) {
+ // Action bar doesn't have a known configuration, an OEM messed with things.
+ return;
+ }
+
+ final ViewGroup parent = (ViewGroup) home.getParent();
+ final int childCount = parent.getChildCount();
+ if (childCount != 2) {
+ // No idea which one will be the right one, an OEM messed with things.
+ return;
+ }
+
+ final View first = parent.getChildAt(0);
+ final View second = parent.getChildAt(1);
+ final View up = first.getId() == android.R.id.home ? second : first;
+
+ if (up instanceof ImageView) {
+ // Jackpot! (Probably...)
+ mUpIndicatorView = (ImageView) up;
+ }
+ }
}
private class SlideDrawable extends InsetDrawable implements Drawable.Callback {
@@ -526,7 +577,7 @@
}
@Override
- public void draw(Canvas canvas) {
+ public void draw(@NonNull Canvas canvas) {
copyBounds(mTmpRect);
canvas.save();
diff --git a/core-ui/java/android/support/v4/widget/DrawerLayout.java b/core-ui/java/android/support/v4/widget/DrawerLayout.java
index fe35212..3b3a781 100644
--- a/core-ui/java/android/support/v4/widget/DrawerLayout.java
+++ b/core-ui/java/android/support/v4/widget/DrawerLayout.java
@@ -17,6 +17,9 @@
package android.support.v4.widget;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
@@ -34,7 +37,7 @@
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.support.annotation.RequiresApi;
+import android.support.annotation.RestrictTo;
import android.support.v4.content.ContextCompat;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v4.view.AbsSavedState;
@@ -51,6 +54,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
+import android.view.WindowInsets;
import android.view.accessibility.AccessibilityEvent;
import java.lang.annotation.Retention;
@@ -91,9 +95,13 @@
* href="{@docRoot}training/implementing-navigation/nav-drawer.html">Creating a Navigation
* Drawer</a>.</p>
*/
-public class DrawerLayout extends ViewGroup implements DrawerLayoutImpl {
+public class DrawerLayout extends ViewGroup {
private static final String TAG = "DrawerLayout";
+ private static final int[] THEME_ATTRS = {
+ android.R.attr.colorPrimaryDark
+ };
+
@IntDef({STATE_IDLE, STATE_DRAGGING, STATE_SETTLING})
@Retention(RetentionPolicy.SOURCE)
private @interface State {}
@@ -287,79 +295,6 @@
}
}
- interface DrawerLayoutCompatImpl {
- void configureApplyInsets(View drawerLayout);
- void dispatchChildInsets(View child, Object insets, int drawerGravity);
- void applyMarginInsets(MarginLayoutParams lp, Object insets, int drawerGravity);
- int getTopInset(Object lastInsets);
- Drawable getDefaultStatusBarBackground(Context context);
- }
-
- static class DrawerLayoutCompatImplBase implements DrawerLayoutCompatImpl {
- @Override
- public void configureApplyInsets(View drawerLayout) {
- // This space for rent
- }
-
- @Override
- public void dispatchChildInsets(View child, Object insets, int drawerGravity) {
- // This space for rent
- }
-
- @Override
- public void applyMarginInsets(MarginLayoutParams lp, Object insets, int drawerGravity) {
- // This space for rent
- }
-
- @Override
- public int getTopInset(Object insets) {
- return 0;
- }
-
- @Override
- public Drawable getDefaultStatusBarBackground(Context context) {
- return null;
- }
- }
-
- @RequiresApi(21)
- static class DrawerLayoutCompatImplApi21 implements DrawerLayoutCompatImpl {
- @Override
- public void configureApplyInsets(View drawerLayout) {
- DrawerLayoutCompatApi21.configureApplyInsets(drawerLayout);
- }
-
- @Override
- public void dispatchChildInsets(View child, Object insets, int drawerGravity) {
- DrawerLayoutCompatApi21.dispatchChildInsets(child, insets, drawerGravity);
- }
-
- @Override
- public void applyMarginInsets(MarginLayoutParams lp, Object insets, int drawerGravity) {
- DrawerLayoutCompatApi21.applyMarginInsets(lp, insets, drawerGravity);
- }
-
- @Override
- public int getTopInset(Object insets) {
- return DrawerLayoutCompatApi21.getTopInset(insets);
- }
-
- @Override
- public Drawable getDefaultStatusBarBackground(Context context) {
- return DrawerLayoutCompatApi21.getDefaultStatusBarBackground(context);
- }
- }
-
- static {
- if (Build.VERSION.SDK_INT >= 21) {
- IMPL = new DrawerLayoutCompatImplApi21();
- } else {
- IMPL = new DrawerLayoutCompatImplBase();
- }
- }
-
- static final DrawerLayoutCompatImpl IMPL;
-
public DrawerLayout(Context context) {
this(context, null);
}
@@ -397,8 +332,27 @@
ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate());
ViewGroupCompat.setMotionEventSplittingEnabled(this, false);
if (ViewCompat.getFitsSystemWindows(this)) {
- IMPL.configureApplyInsets(this);
- mStatusBarBackground = IMPL.getDefaultStatusBarBackground(context);
+ if (Build.VERSION.SDK_INT >= 21) {
+ setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
+ @TargetApi(21)
+ @Override
+ public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) {
+ final DrawerLayout drawerLayout = (DrawerLayout) view;
+ drawerLayout.setChildInsets(insets, insets.getSystemWindowInsetTop() > 0);
+ return insets.consumeSystemWindowInsets();
+ }
+ });
+ setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+ final TypedArray a = context.obtainStyledAttributes(THEME_ATTRS);
+ try {
+ mStatusBarBackground = a.getDrawable(0);
+ } finally {
+ a.recycle();
+ }
+ } else {
+ mStatusBarBackground = null;
+ }
}
mDrawerElevation = DRAWER_ELEVATION * density;
@@ -440,7 +394,7 @@
* @hide Internal use only; called to apply window insets when configured
* with fitsSystemWindows="true"
*/
- @Override
+ @RestrictTo(LIBRARY_GROUP)
public void setChildInsets(Object insets, boolean draw) {
mLastInsets = insets;
mDrawStatusBarBackground = draw;
@@ -1063,9 +1017,36 @@
if (applyInsets) {
final int cgrav = GravityCompat.getAbsoluteGravity(lp.gravity, layoutDirection);
if (ViewCompat.getFitsSystemWindows(child)) {
- IMPL.dispatchChildInsets(child, mLastInsets, cgrav);
+ if (Build.VERSION.SDK_INT >= 21) {
+ WindowInsets wi = (WindowInsets) mLastInsets;
+ if (cgrav == Gravity.LEFT) {
+ wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(),
+ wi.getSystemWindowInsetTop(), 0,
+ wi.getSystemWindowInsetBottom());
+ } else if (cgrav == Gravity.RIGHT) {
+ wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(),
+ wi.getSystemWindowInsetRight(),
+ wi.getSystemWindowInsetBottom());
+ }
+ child.dispatchApplyWindowInsets(wi);
+ }
} else {
- IMPL.applyMarginInsets(lp, mLastInsets, cgrav);
+ if (Build.VERSION.SDK_INT >= 21) {
+ WindowInsets wi = (WindowInsets) mLastInsets;
+ if (cgrav == Gravity.LEFT) {
+ wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(),
+ wi.getSystemWindowInsetTop(), 0,
+ wi.getSystemWindowInsetBottom());
+ } else if (cgrav == Gravity.RIGHT) {
+ wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(),
+ wi.getSystemWindowInsetRight(),
+ wi.getSystemWindowInsetBottom());
+ }
+ lp.leftMargin = wi.getSystemWindowInsetLeft();
+ lp.topMargin = wi.getSystemWindowInsetTop();
+ lp.rightMargin = wi.getSystemWindowInsetRight();
+ lp.bottomMargin = wi.getSystemWindowInsetBottom();
+ }
}
}
@@ -1340,7 +1321,13 @@
public void onDraw(Canvas c) {
super.onDraw(c);
if (mDrawStatusBarBackground && mStatusBarBackground != null) {
- final int inset = IMPL.getTopInset(mLastInsets);
+ final int inset;
+ if (Build.VERSION.SDK_INT >= 21) {
+ inset = mLastInsets != null
+ ? ((WindowInsets) mLastInsets).getSystemWindowInsetTop() : 0;
+ } else {
+ inset = 0;
+ }
if (inset > 0) {
mStatusBarBackground.setBounds(0, 0, getWidth(), inset);
mStatusBarBackground.draw(c);
diff --git a/core-ui/jellybean-mr2/android/support/v4/app/ActionBarDrawerToggleJellybeanMR2.java b/core-ui/jellybean-mr2/android/support/v4/app/ActionBarDrawerToggleJellybeanMR2.java
deleted file mode 100644
index 40d180c..0000000
--- a/core-ui/jellybean-mr2/android/support/v4/app/ActionBarDrawerToggleJellybeanMR2.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package android.support.v4.app;
-
-import android.R;
-import android.app.ActionBar;
-import android.app.Activity;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.RequiresApi;
-
-@RequiresApi(18)
-class ActionBarDrawerToggleJellybeanMR2 {
- private static final String TAG = "ActionBarDrawerToggleImplJellybeanMR2";
-
- private static final int[] THEME_ATTRS = new int[] {
- R.attr.homeAsUpIndicator
- };
-
- public static Object setActionBarUpIndicator(Object info, Activity activity,
- Drawable drawable, int contentDescRes) {
- final ActionBar actionBar = activity.getActionBar();
- if (actionBar != null) {
- actionBar.setHomeAsUpIndicator(drawable);
- actionBar.setHomeActionContentDescription(contentDescRes);
- }
- return info;
- }
-
- public static Object setActionBarDescription(Object info, Activity activity,
- int contentDescRes) {
- final ActionBar actionBar = activity.getActionBar();
- if (actionBar != null) {
- actionBar.setHomeActionContentDescription(contentDescRes);
- }
- return info;
- }
-
- public static Drawable getThemeUpIndicator(Activity activity) {
- final ActionBar actionBar = activity.getActionBar();
- final Context context;
- if (actionBar != null) {
- context = actionBar.getThemedContext();
- } else {
- context = activity;
- }
-
- final TypedArray a = context.obtainStyledAttributes(null, THEME_ATTRS,
- R.attr.actionBarStyle, 0);
- final Drawable result = a.getDrawable(0);
- a.recycle();
- return result;
- }
-}
diff --git a/design/src/android/support/design/internal/BottomNavigationItemView.java b/design/src/android/support/design/internal/BottomNavigationItemView.java
index 885ea02..fe5e636 100644
--- a/design/src/android/support/design/internal/BottomNavigationItemView.java
+++ b/design/src/android/support/design/internal/BottomNavigationItemView.java
@@ -125,7 +125,6 @@
public void setTitle(CharSequence title) {
mSmallLabel.setText(title);
mLargeLabel.setText(title);
- setContentDescription(title);
}
@Override
diff --git a/design/src/android/support/design/widget/TextInputLayout.java b/design/src/android/support/design/widget/TextInputLayout.java
index 52efdde..c9e8010 100644
--- a/design/src/android/support/design/widget/TextInputLayout.java
+++ b/design/src/android/support/design/widget/TextInputLayout.java
@@ -961,6 +961,7 @@
static class SavedState extends AbsSavedState {
CharSequence error;
+ boolean isPasswordToggledVisible;
SavedState(Parcelable superState) {
super(superState);
@@ -969,6 +970,7 @@
SavedState(Parcel source, ClassLoader loader) {
super(source, loader);
error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ isPasswordToggledVisible = (source.readInt() == 1);
}
@@ -976,6 +978,7 @@
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
TextUtils.writeToParcel(error, dest, flags);
+ dest.writeInt(isPasswordToggledVisible ? 1 : 0);
}
@Override
@@ -1010,6 +1013,7 @@
if (mErrorShown) {
ss.error = getError();
}
+ ss.isPasswordToggledVisible = mPasswordToggledVisible;
return ss;
}
@@ -1022,6 +1026,9 @@
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
setError(ss.error);
+ if (ss.isPasswordToggledVisible) {
+ passwordVisibilityToggleRequested(true);
+ }
requestLayout();
}
@@ -1100,7 +1107,7 @@
mPasswordToggleView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- passwordVisibilityToggleRequested();
+ passwordVisibilityToggleRequested(false);
}
});
}
@@ -1313,7 +1320,7 @@
applyPasswordToggleTint();
}
- void passwordVisibilityToggleRequested() {
+ private void passwordVisibilityToggleRequested(boolean shouldSkipAnimations) {
if (mPasswordToggleEnabled) {
// Store the current cursor position
final int selection = mEditText.getSelectionEnd();
@@ -1327,6 +1334,9 @@
}
mPasswordToggleView.setChecked(mPasswordToggledVisible);
+ if (shouldSkipAnimations) {
+ mPasswordToggleView.jumpDrawablesToCurrentState();
+ }
// And restore the cursor position
mEditText.setSelection(selection);
diff --git a/design/tests/res/menu/bottom_navigation_view_content.xml b/design/tests/res/menu/bottom_navigation_view_content.xml
index 6d19060..d61d6d8 100644
--- a/design/tests/res/menu/bottom_navigation_view_content.xml
+++ b/design/tests/res/menu/bottom_navigation_view_content.xml
@@ -13,14 +13,21 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/destination_home"
+ android:icon="@drawable/test_drawable_red"
android:title="@string/navigate_home"
- android:icon="@drawable/test_drawable_red" />
+ app:contentDescription="@string/navigate_home"
+ app:tooltipText="@string/navigate_home"/>
<item android:id="@+id/destination_profile"
+ android:icon="@drawable/test_drawable_green"
android:title="@string/navigate_profile"
- android:icon="@drawable/test_drawable_green" />
+ app:contentDescription="@string/navigate_profile"
+ app:tooltipText="@string/navigate_profile"/>
<item android:id="@+id/destination_people"
+ android:icon="@drawable/test_drawable_blue"
android:title="@string/navigate_people"
- android:icon="@drawable/test_drawable_blue" />
+ app:contentDescription="@string/navigate_people"
+ app:tooltipText="@string/navigate_people"/>
</menu>
diff --git a/design/tests/src/android/support/design/testutils/ActivityUtils.java b/design/tests/src/android/support/design/testutils/ActivityUtils.java
new file mode 100644
index 0000000..1ed6a3f
--- /dev/null
+++ b/design/tests/src/android/support/design/testutils/ActivityUtils.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.design.testutils;
+
+import static org.junit.Assert.assertTrue;
+
+import android.os.Looper;
+import android.support.test.rule.ActivityTestRule;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Utility methods for testing activities.
+ */
+public class ActivityUtils {
+ private static final Runnable DO_NOTHING = new Runnable() {
+ @Override
+ public void run() {
+ }
+ };
+
+ public static void waitForExecution(
+ final ActivityTestRule<? extends RecreatedAppCompatActivity> rule) {
+ // Wait for two cycles. When starting a postponed transition, it will post to
+ // the UI thread and then the execution will be added onto the queue after that.
+ // The two-cycle wait makes sure fragments have the opportunity to complete both
+ // before returning.
+ try {
+ rule.runOnUiThread(DO_NOTHING);
+ rule.runOnUiThread(DO_NOTHING);
+ } catch (Throwable throwable) {
+ throw new RuntimeException(throwable);
+ }
+ }
+
+ private static void runOnUiThreadRethrow(
+ ActivityTestRule<? extends RecreatedAppCompatActivity> rule, Runnable r) {
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ r.run();
+ } else {
+ try {
+ rule.runOnUiThread(r);
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ }
+ }
+
+ /**
+ * Restarts the RecreatedAppCompatActivity and waits for the new activity to be resumed.
+ *
+ * @return The newly-restarted RecreatedAppCompatActivity
+ */
+ public static <T extends RecreatedAppCompatActivity> T recreateActivity(
+ ActivityTestRule<? extends RecreatedAppCompatActivity> rule, final T activity)
+ throws InterruptedException {
+ // Now switch the orientation
+ RecreatedAppCompatActivity.sResumed = new CountDownLatch(1);
+ RecreatedAppCompatActivity.sDestroyed = new CountDownLatch(1);
+
+ runOnUiThreadRethrow(rule, new Runnable() {
+ @Override
+ public void run() {
+ activity.recreate();
+ }
+ });
+ assertTrue(RecreatedAppCompatActivity.sResumed.await(1, TimeUnit.SECONDS));
+ assertTrue(RecreatedAppCompatActivity.sDestroyed.await(1, TimeUnit.SECONDS));
+ T newActivity = (T) RecreatedAppCompatActivity.sActivity;
+
+ waitForExecution(rule);
+
+ RecreatedAppCompatActivity.clearState();
+ return newActivity;
+ }
+}
diff --git a/design/tests/src/android/support/design/testutils/RecreatedAppCompatActivity.java b/design/tests/src/android/support/design/testutils/RecreatedAppCompatActivity.java
new file mode 100644
index 0000000..52ba059
--- /dev/null
+++ b/design/tests/src/android/support/design/testutils/RecreatedAppCompatActivity.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.design.testutils;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * Activity that keeps track of resume / destroy lifecycle events, as well as of the last
+ * instance of itself.
+ */
+public class RecreatedAppCompatActivity extends AppCompatActivity {
+ // These must be cleared after each test using clearState()
+ public static RecreatedAppCompatActivity sActivity;
+ public static CountDownLatch sResumed;
+ public static CountDownLatch sDestroyed;
+
+ public static void clearState() {
+ sActivity = null;
+ sResumed = null;
+ sDestroyed = null;
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ sActivity = this;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (sResumed != null) {
+ sResumed.countDown();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (sDestroyed != null) {
+ sDestroyed.countDown();
+ }
+ }
+}
diff --git a/design/tests/src/android/support/design/testutils/TextInputLayoutActions.java b/design/tests/src/android/support/design/testutils/TextInputLayoutActions.java
index f49644f..c4d4520 100755
--- a/design/tests/src/android/support/design/testutils/TextInputLayoutActions.java
+++ b/design/tests/src/android/support/design/testutils/TextInputLayoutActions.java
@@ -19,9 +19,11 @@
import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import android.graphics.Typeface;
+import android.support.design.R;
import android.support.design.widget.TextInputLayout;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
+import android.support.test.espresso.matcher.ViewMatchers;
import android.view.View;
import org.hamcrest.Matcher;
@@ -196,4 +198,32 @@
};
}
+ /**
+ * Toggles password.
+ */
+ public static ViewAction clickPasswordToggle() {
+ return new ViewAction() {
+
+ @Override
+ public Matcher<View> getConstraints() {
+ return ViewMatchers.isAssignableFrom(TextInputLayout.class);
+ }
+
+ @Override
+ public String getDescription() {
+ return "Clicks the password toggle";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ TextInputLayout textInputLayout = (TextInputLayout) view;
+ // Reach in and find the password toggle since we don't have a public API
+ // to get a reference to it
+ View passwordToggle = textInputLayout.findViewById(R.id.text_input_password_toggle);
+ passwordToggle.performClick();
+ uiController.loopMainThreadUntilIdle();
+ }
+ };
+ }
+
}
diff --git a/design/tests/src/android/support/design/testutils/TextInputLayoutMatchers.java b/design/tests/src/android/support/design/testutils/TextInputLayoutMatchers.java
index d13a39d..d67e555 100755
--- a/design/tests/src/android/support/design/testutils/TextInputLayoutMatchers.java
+++ b/design/tests/src/android/support/design/testutils/TextInputLayoutMatchers.java
@@ -16,8 +16,11 @@
package android.support.design.testutils;
+import android.support.design.R;
+import android.support.design.widget.CheckableImageButton;
import android.support.design.widget.TextInputLayout;
import android.text.TextUtils;
+import android.view.View;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
@@ -29,7 +32,7 @@
* Returns a matcher that matches TextInputLayouts with non-empty content descriptions for
* the password toggle.
*/
- public static Matcher hasPasswordToggleContentDescription() {
+ public static Matcher passwordToggleHasContentDescription() {
return new TypeSafeMatcher<TextInputLayout>() {
@Override
public void describeTo(Description description) {
@@ -39,9 +42,53 @@
@Override
protected boolean matchesSafely(TextInputLayout item) {
- return !TextUtils.isEmpty(item.getPasswordVisibilityToggleContentDescription());
+ // Reach in and find the password toggle since we don't have a public API
+ // to get a reference to it
+ View passwordToggle = item.findViewById(R.id.text_input_password_toggle);
+ return !TextUtils.isEmpty(item.getPasswordVisibilityToggleContentDescription())
+ && !TextUtils.isEmpty(passwordToggle.getContentDescription());
}
};
}
+ /**
+ * Returns a matcher that matches TextInputLayouts with non-displayed password toggles
+ */
+ public static Matcher doesNotShowPasswordToggle() {
+ return new TypeSafeMatcher<TextInputLayout>() {
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("TextInputLayout shows password toggle.");
+ }
+
+ @Override
+ protected boolean matchesSafely(TextInputLayout item) {
+ // Reach in and find the password toggle since we don't have a public API
+ // to get a reference to it
+ View passwordToggle = item.findViewById(R.id.text_input_password_toggle);
+ return passwordToggle.getVisibility() != View.VISIBLE;
+ }
+ };
+ }
+
+ /**
+ * Returns a matcher that matches TextInputLayouts with non-displayed password toggles
+ */
+ public static Matcher passwordToggleIsNotChecked() {
+ return new TypeSafeMatcher<TextInputLayout>() {
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("TextInputLayout has checked password toggle.");
+ }
+
+ @Override
+ protected boolean matchesSafely(TextInputLayout item) {
+ // Reach in and find the password toggle since we don't have a public API
+ // to get a reference to it
+ CheckableImageButton passwordToggle = (CheckableImageButton) item.findViewById(
+ R.id.text_input_password_toggle);
+ return !passwordToggle.isChecked();
+ }
+ };
+ }
}
diff --git a/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarStateRestoreTest.java b/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarStateRestoreTest.java
index 82c9582..b9a6518 100644
--- a/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarStateRestoreTest.java
+++ b/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarStateRestoreTest.java
@@ -23,14 +23,10 @@
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
-import android.app.Activity;
-import android.content.pm.ActivityInfo;
-import android.content.res.Configuration;
import android.support.design.test.R;
+import android.support.design.testutils.ActivityUtils;
import android.support.test.filters.LargeTest;
-import android.support.testutils.PollingCheck;
-import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -38,8 +34,7 @@
public class AppBarWithCollapsingToolbarStateRestoreTest
extends BaseInstrumentationTestCase<AppBarLayoutCollapsePinTestActivity> {
- private Activity mActivity;
- private int mOldOrientation;
+ private AppBarLayoutCollapsePinTestActivity mActivity;
public AppBarWithCollapsingToolbarStateRestoreTest() {
super(AppBarLayoutCollapsePinTestActivity.class);
@@ -48,22 +43,10 @@
@Before
public void setup() {
mActivity = mActivityTestRule.getActivity();
- mOldOrientation = mActivity.getResources().getConfiguration().orientation;
- }
-
- @After
- public void tearDown() {
- mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return mActivity.getResources().getConfiguration().orientation == mOldOrientation;
- }
- });
}
@Test
- public void testRotateAndRestore() {
+ public void testRecreateAndRestore() throws Throwable {
final AppBarLayout appBar = (AppBarLayout) mActivity.findViewById(R.id.app_bar);
// Swipe up and collapse the AppBarLayout
@@ -76,27 +59,12 @@
.check(matches(hasZ()))
.check(matches(isCollapsed()));
- // Now rotate the Activity
- final int newOrientation = mOldOrientation == Configuration.ORIENTATION_PORTRAIT
- ? Configuration.ORIENTATION_LANDSCAPE
- : Configuration.ORIENTATION_PORTRAIT;
- mActivity.setRequestedOrientation(toActivityInfoConfiguration(newOrientation));
- PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
- @Override
- public boolean canProceed() {
- return mActivity.getResources().getConfiguration().orientation == newOrientation;
- }
- });
+ mActivity = ActivityUtils.recreateActivity(mActivityTestRule, mActivity);
+ ActivityUtils.waitForExecution(mActivityTestRule);
// And check that the app bar still is restored correctly
onView(withId(R.id.app_bar))
.check(matches(hasZ()))
.check(matches(isCollapsed()));
}
-
- private static int toActivityInfoConfiguration(int configuration) {
- return configuration == Configuration.ORIENTATION_PORTRAIT
- ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
- : ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
- }
}
diff --git a/design/tests/src/android/support/design/widget/BaseTestActivity.java b/design/tests/src/android/support/design/widget/BaseTestActivity.java
index 0c801cb..bdeb231 100755
--- a/design/tests/src/android/support/design/widget/BaseTestActivity.java
+++ b/design/tests/src/android/support/design/widget/BaseTestActivity.java
@@ -17,10 +17,10 @@
package android.support.design.widget;
import android.os.Bundle;
-import android.support.v7.app.AppCompatActivity;
+import android.support.design.testutils.RecreatedAppCompatActivity;
import android.view.WindowManager;
-abstract class BaseTestActivity extends AppCompatActivity {
+abstract class BaseTestActivity extends RecreatedAppCompatActivity {
private boolean mDestroyed;
diff --git a/design/tests/src/android/support/design/widget/BottomNavigationViewTest.java b/design/tests/src/android/support/design/widget/BottomNavigationViewTest.java
index cbf2fc9..3365fac 100644
--- a/design/tests/src/android/support/design/widget/BottomNavigationViewTest.java
+++ b/design/tests/src/android/support/design/widget/BottomNavigationViewTest.java
@@ -56,6 +56,7 @@
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.View;
+import android.view.ViewGroup;
import org.junit.Before;
import org.junit.Test;
@@ -434,6 +435,22 @@
});
}
+ @UiThreadTest
+ @Test
+ @SmallTest
+ public void testContentDescription() {
+ ViewGroup menuView = (ViewGroup) mBottomNavigation.getChildAt(0);
+ final int count = menuView.getChildCount();
+ for (int i = 0; i < count; i++) {
+ View child = menuView.getChildAt(i);
+ // We're using the same strings for content description
+ assertEquals(mMenuStringContent.get(child.getId()),
+ child.getContentDescription().toString());
+ }
+
+ menuView.getChildAt(0).getContentDescription();
+ }
+
private void checkAndVerifyExclusiveItem(final Menu menu, final int id) throws Throwable {
menu.findItem(id).setChecked(true);
for (int i = 0; i < menu.size(); i++) {
diff --git a/design/tests/src/android/support/design/widget/TextInputLayoutTest.java b/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
index 226463c..52471a9 100755
--- a/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
+++ b/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
@@ -21,6 +21,7 @@
import static android.support.design.testutils.TestUtilsMatchers.withCompoundDrawable;
import static android.support.design.testutils.TestUtilsMatchers.withTextColor;
import static android.support.design.testutils.TestUtilsMatchers.withTypeface;
+import static android.support.design.testutils.TextInputLayoutActions.clickPasswordToggle;
import static android.support.design.testutils.TextInputLayoutActions.setCounterEnabled;
import static android.support.design.testutils.TextInputLayoutActions.setCounterMaxLength;
import static android.support.design.testutils.TextInputLayoutActions.setError;
@@ -29,8 +30,10 @@
import static android.support.design.testutils.TextInputLayoutActions
.setPasswordVisibilityToggleEnabled;
import static android.support.design.testutils.TextInputLayoutActions.setTypeface;
+import static android.support.design.testutils.TextInputLayoutMatchers.doesNotShowPasswordToggle;
import static android.support.design.testutils.TextInputLayoutMatchers
- .hasPasswordToggleContentDescription;
+ .passwordToggleHasContentDescription;
+import static android.support.design.testutils.TextInputLayoutMatchers.passwordToggleIsNotChecked;
import static android.support.test.InstrumentationRegistry.getInstrumentation;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
@@ -38,15 +41,15 @@
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.contrib.AccessibilityChecks.accessibilityAssertion;
-import static android.support.test.espresso.matcher.ViewMatchers.hasContentDescription;
import static android.support.test.espresso.matcher.ViewMatchers.hasFocus;
-import static android.support.test.espresso.matcher.ViewMatchers.isChecked;
+import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isEnabled;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.not;
+import static org.hamcrest.core.AllOf.allOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
@@ -62,14 +65,19 @@
import android.os.Build;
import android.os.Parcelable;
import android.support.design.test.R;
+import android.support.design.testutils.ActivityUtils;
+import android.support.design.testutils.RecreatedAppCompatActivity;
import android.support.design.testutils.TestUtils;
import android.support.design.testutils.ViewStructureImpl;
import android.support.test.annotation.UiThreadTest;
import android.support.test.espresso.NoMatchingViewException;
import android.support.test.espresso.ViewAssertion;
+import android.support.test.filters.LargeTest;
import android.support.test.filters.MediumTest;
import android.support.test.filters.SdkSuppress;
import android.support.v4.widget.TextViewCompat;
+import android.text.method.PasswordTransformationMethod;
+import android.text.method.TransformationMethod;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.KeyEvent;
@@ -167,7 +175,7 @@
assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
// Now click the toggle button
- onView(withId(R.id.text_input_password_toggle)).perform(click());
+ onView(withId(R.id.textinput_password)).perform(clickPasswordToggle());
// And assert that the password is not disguised
assertEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
@@ -189,7 +197,7 @@
.perform(setPasswordVisibilityToggleEnabled(false));
// Check that the password toggle view is not visible
- onView(withId(R.id.text_input_password_toggle)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.textinput_password)).check(matches(doesNotShowPasswordToggle()));
// ...and that the password is disguised still
assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
}
@@ -205,7 +213,7 @@
assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
// Now click the toggle button
- onView(withId(R.id.text_input_password_toggle)).perform(click());
+ onView(withId(R.id.textinput_password)).perform(clickPasswordToggle());
// Disable the password toggle
onView(withId(R.id.textinput_password))
.perform(setPasswordVisibilityToggleEnabled(false));
@@ -252,7 +260,7 @@
// Type some text on the EditText and then click the toggle button
onView(withId(R.id.textinput_edittext_pwd)).perform(typeText(INPUT_TEXT));
- onView(withId(R.id.text_input_password_toggle)).perform(click());
+ onView(withId(R.id.textinput_password)).perform(clickPasswordToggle());
// Disable the password toggle, and then re-enable it
onView(withId(R.id.textinput_password))
@@ -261,7 +269,7 @@
// Check that the password is disguised and the toggle button reflects the same state
assertNotEquals(INPUT_TEXT, textInput.getLayout().getText().toString());
- onView(withId(R.id.text_input_password_toggle)).check(matches(not(isChecked())));
+ onView(withId(R.id.textinput_password)).check(matches(passwordToggleIsNotChecked()));
}
@Test
@@ -430,13 +438,10 @@
@Test
public void testPasswordToggleHasDefaultContentDescription() {
- // Check that the TextInputLayout says that it has a content description
+ // Check that the TextInputLayout says that it has a content description and that the
+ // underlying toggle has content description as well
onView(withId(R.id.textinput_password))
- .check(matches(hasPasswordToggleContentDescription()));
-
- // Check that the underlying toggle view says that it also has a content description
- onView(withId(R.id.text_input_password_toggle))
- .check(matches(hasContentDescription()));
+ .check(matches(passwordToggleHasContentDescription()));
}
/**
@@ -445,8 +450,8 @@
*/
@Test
public void testPasswordToggleIsAccessible() {
- onView(withId(R.id.text_input_password_toggle))
- .check(accessibilityAssertion());
+ onView(allOf(withId(R.id.text_input_password_toggle),
+ isDescendantOfA(withId(R.id.textinput_password)))).check(accessibilityAssertion());
}
@Test
@@ -489,8 +494,7 @@
@Test
public void testTextSetViaAttributeCollapsedHint() {
- onView(withId(R.id.textinput_with_text))
- .check(isHintExpanded(false));
+ onView(withId(R.id.textinput_with_text)).check(isHintExpanded(false));
}
@Test
@@ -508,6 +512,25 @@
.check(matches(hasFocus()));
}
+ @Test
+ @LargeTest
+ public void testSaveAndRestorePasswordVisibility() throws Throwable {
+ // Type some text on the EditText
+ onView(withId(R.id.textinput_edittext_pwd)).perform(typeText(INPUT_TEXT));
+ onView(withId(R.id.textinput_password)).check(isPasswordToggledVisible(false));
+
+ // Toggle password to be shown as plain text
+ onView(withId(R.id.textinput_password)).perform(clickPasswordToggle());
+ onView(withId(R.id.textinput_password)).check(isPasswordToggledVisible(true));
+
+ RecreatedAppCompatActivity activity = mActivityTestRule.getActivity();
+ activity = ActivityUtils.recreateActivity(mActivityTestRule, activity);
+ ActivityUtils.waitForExecution(mActivityTestRule);
+
+ // Check that the password is still toggled to be shown as plain text
+ onView(withId(R.id.textinput_password)).check(isPasswordToggledVisible(true));
+ }
+
static ViewAssertion isHintExpanded(final boolean expanded) {
return new ViewAssertion() {
@Override
@@ -517,4 +540,20 @@
}
};
}
+
+ static ViewAssertion isPasswordToggledVisible(final boolean isToggledVisible) {
+ return new ViewAssertion() {
+ @Override
+ public void check(View view, NoMatchingViewException noViewFoundException) {
+ assertTrue(view instanceof TextInputLayout);
+ EditText editText = ((TextInputLayout) view).getEditText();
+ TransformationMethod transformationMethod = editText.getTransformationMethod();
+ if (isToggledVisible) {
+ assertNull(transformationMethod);
+ } else {
+ assertEquals(PasswordTransformationMethod.getInstance(), transformationMethod);
+ }
+ }
+ };
+ }
}
diff --git a/dynamic-animation/src/android/support/animation/DynamicAnimation.java b/dynamic-animation/src/android/support/animation/DynamicAnimation.java
index 18b7d60..8ea48b9 100644
--- a/dynamic-animation/src/android/support/animation/DynamicAnimation.java
+++ b/dynamic-animation/src/android/support/animation/DynamicAnimation.java
@@ -380,7 +380,8 @@
}
/**
- * Start velocity of the animation. Default velocity is 0. Unit: pixel/second
+ * Start velocity of the animation. Default velocity is 0. Unit: change in property per
+ * second (e.g. pixels per second, scale/alpha value change per second).
*
* <p>Note when using a fixed value as the start velocity (as opposed to getting the velocity
* through touch events), it is recommended to define such a value in dp/second and convert it
@@ -393,7 +394,7 @@
* getResources().getDisplayMetrics());
* </pre>
*
- * @param startVelocity start velocity of the animation in pixel/second
+ * @param startVelocity start velocity of the animation
* @return the Animation whose start velocity is being set
*/
public T setStartVelocity(float startVelocity) {
diff --git a/emoji/core/src/android/support/text/emoji/EmojiCompat.java b/emoji/core/src/android/support/text/emoji/EmojiCompat.java
index 0629884..eb1c565 100644
--- a/emoji/core/src/android/support/text/emoji/EmojiCompat.java
+++ b/emoji/core/src/android/support/text/emoji/EmojiCompat.java
@@ -105,24 +105,12 @@
public static final int LOAD_STATE_SUCCEEDED = 1;
/**
- * @deprecated Use {@link #LOAD_STATE_SUCCEEDED} instead.
- */
- @Deprecated
- public static final int LOAD_STATE_SUCCESS = 1;
-
- /**
* An unrecoverable error occurred during initialization of EmojiCompat. Calls to functions
* such as {@link #process(CharSequence)} will fail.
*/
public static final int LOAD_STATE_FAILED = 2;
/**
- * @deprecated Use {@link #LOAD_STATE_FAILED} instead.
- */
- @Deprecated
- public static final int LOAD_STATE_FAILURE = 2;
-
- /**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@@ -191,14 +179,6 @@
private final MetadataRepoLoader mMetadataLoader;
/**
- * Old metadata loader instance given in the Config instance.
- * @deprecated Will be removed soon.
- */
- @SuppressWarnings("DeprecatedIsStillUsed")
- @Deprecated
- private final MetadataLoader mLegacyMetadataLoader;
-
- /**
* @see Config#setReplaceAll(boolean)
*/
private final boolean mReplaceAll;
@@ -224,8 +204,6 @@
mEmojiSpanIndicatorEnabled = config.mEmojiSpanIndicatorEnabled;
mEmojiSpanIndicatorColor = config.mEmojiSpanIndicatorColor;
mMetadataLoader = config.mMetadataLoader;
- //noinspection deprecation
- mLegacyMetadataLoader = config.mLegacyMetadataLoader;
mMainHandler = new Handler(Looper.getMainLooper());
mInitCallbacks = new ArraySet<>();
if (config.mInitCallbacks != null && !config.mInitCallbacks.isEmpty()) {
@@ -785,49 +763,6 @@
}
/**
- * Interface to load emoji metadata.
- *
- * @deprecated Use {@link MetadataRepoLoader} instead.
- */
- @SuppressWarnings({"DeprecatedIsStillUsed", "deprecation"})
- @Deprecated
- public interface MetadataLoader {
- /**
- * Start loading the metadata. When the loading operation is finished {@link
- * LoaderCallback#onLoaded(MetadataRepo)} or {@link LoaderCallback#onFailed(Throwable)}
- * should be called. When used on devices running API 18 or below, this function is never
- * called.
- *
- * @param loaderCallback callback to signal the loading state
- */
- void load(@NonNull LoaderCallback loaderCallback);
- }
-
- /**
- * Callback to inform EmojiCompat about the state of the metadata load. Passed to MetadataLoader
- * during {@link MetadataLoader#load(LoaderCallback)} call.
- *
- * @deprecated Use {@link MetadataRepoLoaderCallback} instead.
- */
- @SuppressWarnings({"DeprecatedIsStillUsed", "deprecation"})
- @Deprecated
- public abstract static class LoaderCallback {
- /**
- * Called by {@link MetadataLoader} when metadata is loaded successfully.
- *
- * @param metadataRepo MetadataRepo instance, cannot be {@code null}
- */
- public abstract void onLoaded(@NonNull MetadataRepo metadataRepo);
-
- /**
- * Called by {@link MetadataLoader} if an error occurs while loading the metadata.
- *
- * @param throwable the exception that caused the failure, {@code nullable}
- */
- public abstract void onFailed(@Nullable Throwable throwable);
- }
-
- /**
* Configuration class for EmojiCompat. Changes to the values will be ignored after
* {@link #init(Config)} is called.
*
@@ -835,8 +770,6 @@
*/
public abstract static class Config {
private final MetadataRepoLoader mMetadataLoader;
- @SuppressWarnings("deprecation")
- private final MetadataLoader mLegacyMetadataLoader;
private boolean mReplaceAll;
private Set<InitCallback> mInitCallbacks;
private boolean mEmojiSpanIndicatorEnabled;
@@ -850,22 +783,6 @@
protected Config(@NonNull final MetadataRepoLoader metadataLoader) {
Preconditions.checkNotNull(metadataLoader, "metadataLoader cannot be null.");
mMetadataLoader = metadataLoader;
- mLegacyMetadataLoader = null;
- }
-
- /**
- * Default constructor.
- *
- * @param metadataLoader MetadataLoader instance, cannot be {@code null}
- *
- * @deprecated Use constructor with MetadataRepoLoader instead.
- */
- @SuppressWarnings("deprecation")
- @Deprecated
- protected Config(@NonNull final MetadataLoader metadataLoader) {
- Preconditions.checkNotNull(metadataLoader, "metadataLoader cannot be null.");
- mLegacyMetadataLoader = metadataLoader;
- mMetadataLoader = null;
}
/**
@@ -941,10 +858,8 @@
/**
* Returns the {@link MetadataRepoLoader}.
- * @hide
*/
- @RestrictTo(LIBRARY_GROUP)
- public final MetadataRepoLoader getMetadataLoader() {
+ protected final MetadataRepoLoader getMetadataRepoLoader() {
return mMetadataLoader;
}
}
@@ -1059,35 +974,18 @@
@Override
void loadMetadata() {
try {
- if (mEmojiCompat.mMetadataLoader != null) {
- final MetadataRepoLoaderCallback callback = new MetadataRepoLoaderCallback() {
- @Override
- public void onLoaded(@NonNull MetadataRepo metadataRepo) {
- onMetadataLoadSuccess(metadataRepo);
- }
+ final MetadataRepoLoaderCallback callback = new MetadataRepoLoaderCallback() {
+ @Override
+ public void onLoaded(@NonNull MetadataRepo metadataRepo) {
+ onMetadataLoadSuccess(metadataRepo);
+ }
- @Override
- public void onFailed(@Nullable Throwable throwable) {
- mEmojiCompat.onMetadataLoadFailed(throwable);
- }
- };
- mEmojiCompat.mMetadataLoader.load(callback);
- } else {
- //noinspection deprecation
- final LoaderCallback callback = new LoaderCallback() {
- @Override
- public void onLoaded(@NonNull MetadataRepo metadataRepo) {
- onMetadataLoadSuccess(metadataRepo);
- }
-
- @Override
- public void onFailed(@Nullable Throwable throwable) {
- mEmojiCompat.onMetadataLoadFailed(throwable);
- }
- };
- mEmojiCompat.mLegacyMetadataLoader.load(callback);
- }
-
+ @Override
+ public void onFailed(@Nullable Throwable throwable) {
+ mEmojiCompat.onMetadataLoadFailed(throwable);
+ }
+ };
+ mEmojiCompat.mMetadataLoader.load(callback);
} catch (Throwable t) {
mEmojiCompat.onMetadataLoadFailed(t);
}
diff --git a/emoji/core/src/android/support/text/emoji/FontRequestEmojiCompatConfig.java b/emoji/core/src/android/support/text/emoji/FontRequestEmojiCompatConfig.java
index 8d4d68d..05f367c 100644
--- a/emoji/core/src/android/support/text/emoji/FontRequestEmojiCompatConfig.java
+++ b/emoji/core/src/android/support/text/emoji/FontRequestEmojiCompatConfig.java
@@ -16,24 +16,27 @@
package android.support.text.emoji;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.ContentObserver;
import android.graphics.Typeface;
-import android.os.ParcelFileDescriptor;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.os.SystemClock;
+import android.support.annotation.GuardedBy;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.annotation.RestrictTo;
+import android.support.v4.graphics.TypefaceCompatUtil;
import android.support.v4.provider.FontRequest;
import android.support.v4.provider.FontsContractCompat;
import android.support.v4.provider.FontsContractCompat.FontFamilyResult;
-import android.support.v4.provider.FontsContractCompat.FontInfo;
import android.support.v4.util.Preconditions;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
/**
* {@link EmojiCompat.Config} implementation that asynchronously fetches the required font and the
@@ -42,6 +45,72 @@
* <p/>
*/
public class FontRequestEmojiCompatConfig extends EmojiCompat.Config {
+
+ /**
+ * Retry policy used when the font provider is not ready to give the font file.
+ *
+ * To control the thread the retries are handled on, see
+ * {@link FontRequestEmojiCompatConfig#setHandler}.
+ */
+ public abstract static class RetryPolicy {
+ /**
+ * Called each time the metadata loading fails.
+ *
+ * This is primarily due to a pending download of the font.
+ * If a value larger than zero is returned, metadata loader will retry after the given
+ * milliseconds.
+ * <br />
+ * If {@code zero} is returned, metadata loader will retry immediately.
+ * <br/>
+ * If a value less than 0 is returned, the metadata loader will stop retrying and
+ * EmojiCompat will get into {@link EmojiCompat#LOAD_STATE_FAILED} state.
+ * <p/>
+ * Note that the retry may happen earlier than you specified if the font provider notifies
+ * that the download is completed.
+ *
+ * @return long milliseconds to wait until next retry
+ */
+ public abstract long getRetryDelay();
+ }
+
+ /**
+ * A retry policy implementation that doubles the amount of time in between retries.
+ *
+ * If downloading hasn't finish within given amount of time, this policy give up and the
+ * EmojiCompat will get into {@link EmojiCompat#LOAD_STATE_FAILED} state.
+ */
+ public static class ExponentialBackoffRetryPolicy extends RetryPolicy {
+ private final long mTotalMs;
+ private long mRetryOrigin;
+
+ /**
+ * @param totalMs A total amount of time to wait in milliseconds.
+ */
+ public ExponentialBackoffRetryPolicy(long totalMs) {
+ mTotalMs = totalMs;
+ }
+
+ @Override
+ public long getRetryDelay() {
+ if (mRetryOrigin == 0) {
+ mRetryOrigin = SystemClock.uptimeMillis();
+ // Since download may be completed after getting query result and before registering
+ // observer, requesting later at the same time.
+ return 0;
+ } else {
+ // Retry periodically since we can't trust notify change event. Some font provider
+ // may not notify us.
+ final long elapsedMillis = SystemClock.uptimeMillis() - mRetryOrigin;
+ if (elapsedMillis > mTotalMs) {
+ return -1; // Give up since download hasn't finished in 10 min.
+ }
+ // Wait until the same amount of the time from the first scheduled time, but adjust
+ // the minimum request interval is 1 sec and never exceeds 10 min in total.
+ return Math.min(Math.max(elapsedMillis, 1000), mTotalMs - elapsedMillis);
+ }
+ }
+ };
+
/**
* @param context Context instance, cannot be {@code null}
* @param request {@link FontRequest} to fetch the font asynchronously, cannot be {@code null}
@@ -55,10 +124,38 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public FontRequestEmojiCompatConfig(@NonNull Context context, @NonNull FontRequest request,
- @NonNull FontsContractDelegate fontsContract) {
- super(new FontRequestMetadataLoader(context, request, fontsContract));
+ @NonNull FontProviderHelper fontProviderHelper) {
+ super(new FontRequestMetadataLoader(context, request, fontProviderHelper));
}
+ /**
+ * Sets the custom handler to be used for initialization.
+ *
+ * Since font fetch take longer time, the metadata loader will fetch the fonts on the background
+ * thread. You can pass your own handler for this background fetching. This handler is also used
+ * for retrying.
+ *
+ * @param handler A {@link Handler} to be used for initialization. Can be {@code null}. In case
+ * of {@code null}, the metadata loader creates own {@link HandlerThread} for
+ * initialization.
+ */
+ public FontRequestEmojiCompatConfig setHandler(Handler handler) {
+ ((FontRequestMetadataLoader) getMetadataRepoLoader()).setHandler(handler);
+ return this;
+ }
+
+ /**
+ * Sets the retry policy.
+ *
+ * {@see RetryPolicy}
+ * @param policy The policy to be used when the font provider is not ready to give the font
+ * file. Can be {@code null}. In case of {@code null}, the metadata loader never
+ * retries.
+ */
+ public FontRequestEmojiCompatConfig setRetryPolicy(RetryPolicy policy) {
+ ((FontRequestMetadataLoader) getMetadataRepoLoader()).setRetryPolicy(policy);
+ return this;
+ }
/**
* MetadataRepoLoader implementation that uses FontsContractCompat and TypefaceCompat to load a
@@ -67,98 +164,162 @@
private static class FontRequestMetadataLoader implements EmojiCompat.MetadataRepoLoader {
private final Context mContext;
private final FontRequest mRequest;
- private final FontsContractDelegate mFontsContract;
+ private final FontProviderHelper mFontProviderHelper;
+
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private Handler mHandler;
+ @GuardedBy("mLock")
+ private HandlerThread mThread;
+ @GuardedBy("mLock")
+ private @Nullable RetryPolicy mRetryPolicy;
+
+ // Following three variables must be touched only on the thread associated with mHandler.
+ private EmojiCompat.MetadataRepoLoaderCallback mCallback;
+ private ContentObserver mObserver;
+ private Runnable mHandleMetadataCreationRunner;
FontRequestMetadataLoader(@NonNull Context context, @NonNull FontRequest request,
- @NonNull FontsContractDelegate fontsContract) {
+ @NonNull FontProviderHelper fontProviderHelper) {
Preconditions.checkNotNull(context, "Context cannot be null");
Preconditions.checkNotNull(request, "FontRequest cannot be null");
mContext = context.getApplicationContext();
mRequest = request;
- mFontsContract = fontsContract;
+ mFontProviderHelper = fontProviderHelper;
+ }
+
+ public void setHandler(Handler handler) {
+ synchronized (mLock) {
+ mHandler = handler;
+ }
+ }
+
+ public void setRetryPolicy(RetryPolicy policy) {
+ synchronized (mLock) {
+ mRetryPolicy = policy;
+ }
}
@Override
@RequiresApi(19)
public void load(@NonNull final EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
Preconditions.checkNotNull(loaderCallback, "LoaderCallback cannot be null");
- final InitRunnable runnable =
- new InitRunnable(mContext, mRequest, mFontsContract, loaderCallback);
- final Thread thread = new Thread(runnable);
- thread.setDaemon(false);
- thread.start();
- }
- }
-
- /**
- * Runnable used to create the Typeface and MetadataRepo from a given FontResult.
- */
- @RequiresApi(19)
- private static class InitRunnable implements Runnable {
- private final EmojiCompat.MetadataRepoLoaderCallback mLoaderCallback;
- private final Context mContext;
- private final FontsContractDelegate mFontsContract;
- private final FontRequest mFontRequest;
-
- private InitRunnable(final Context context,
- final FontRequest fontRequest,
- final FontsContractDelegate fontsContract,
- final EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
- mContext = context;
- mFontRequest = fontRequest;
- mFontsContract = fontsContract;
- mLoaderCallback = loaderCallback;
- }
-
- @Override
- public void run() {
- try {
- FontFamilyResult result = null;
- try {
- result = mFontsContract.fetchFonts(mContext, mFontRequest);
- } catch (NameNotFoundException e) {
- throwException("provider not found");
+ synchronized (mLock) {
+ if (mHandler == null) {
+ // Developer didn't give a thread for fetching. Create our own one.
+ mThread = new HandlerThread("emojiCompat", Process.THREAD_PRIORITY_BACKGROUND);
+ mThread.start();
+ mHandler = new Handler(mThread.getLooper());
}
- if (result.getStatusCode() != FontFamilyResult.STATUS_OK) {
- throwException("fetchFonts failed (" + result.getStatusCode() + ")");
- }
- final FontInfo[] fonts = result.getFonts();
- if (fonts == null || fonts.length == 0) {
- throwException("fetchFonts failed (empty result)");
- }
- // Assuming the GMS Core provides only one font file.
- final FontInfo font = fonts[0];
- if (font.getResultCode() != FontsContractCompat.Columns.RESULT_CODE_OK) {
- throwException("fetchFonts result is not OK. (" + font.getResultCode() + ")");
- }
-
- final ContentResolver resolver = mContext.getContentResolver();
- ByteBuffer buffer = null;
- try (ParcelFileDescriptor fd = resolver.openFileDescriptor(font.getUri(), "r");
- FileInputStream inputStream = new FileInputStream(fd.getFileDescriptor())) {
- final FileChannel fileChannel = inputStream.getChannel();
- buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
- } catch (FileNotFoundException e) {
- throwException("Unable to open file.");
- }
-
- // TODO(nona): Introduce public API to make Typeface from filedescriptor so that we
- // can stop opening file descriptor twice.
- final Typeface typeface = FontsContractCompat.buildTypeface(mContext,
- null /* cancellation signal */, fonts);
- if (typeface == null) {
- throwException("Failed to create Typeface.");
- }
-
- mLoaderCallback.onLoaded(MetadataRepo.create(typeface, buffer));
- } catch (Throwable t) {
- mLoaderCallback.onFailed(t);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mCallback = loaderCallback;
+ createMetadata();
+ }
+ });
}
}
- }
- private static void throwException(String msg) {
- throw new RuntimeException("Cannot load metadata: " + msg);
+ private FontsContractCompat.FontInfo retrieveFontInfo() {
+ final FontsContractCompat.FontFamilyResult result;
+ try {
+ result = mFontProviderHelper.fetchFonts(mContext, mRequest);
+ } catch (NameNotFoundException e) {
+ throw new RuntimeException("provider not found", e);
+ }
+ if (result.getStatusCode() != FontsContractCompat.FontFamilyResult.STATUS_OK) {
+ throw new RuntimeException("fetchFonts failed (" + result.getStatusCode() + ")");
+ }
+ final FontsContractCompat.FontInfo[] fonts = result.getFonts();
+ if (fonts == null || fonts.length == 0) {
+ throw new RuntimeException("fetchFonts failed (empty result)");
+ }
+ return fonts[0]; // Assuming the GMS Core provides only one font file.
+ }
+
+ // Must be called on the mHandler.
+ @RequiresApi(19)
+ private void scheduleRetry(Uri uri, long waitMs) {
+ synchronized (mLock) {
+ if (mObserver == null) {
+ mObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ createMetadata();
+ }
+ };
+ mFontProviderHelper.registerObserver(mContext, uri, mObserver);
+ }
+ if (mHandleMetadataCreationRunner == null) {
+ mHandleMetadataCreationRunner = new Runnable() {
+ @Override
+ public void run() {
+ createMetadata();
+ }
+ };
+ }
+ mHandler.postDelayed(mHandleMetadataCreationRunner, waitMs);
+ }
+ }
+
+ // Must be called on the mHandler.
+ private void cleanUp() {
+ mCallback = null;
+ if (mObserver != null) {
+ mFontProviderHelper.unregisterObserver(mContext, mObserver);
+ mObserver = null;
+ }
+ synchronized (mLock) {
+ mHandler.removeCallbacks(mHandleMetadataCreationRunner);
+ if (mThread != null) {
+ mThread.quit();
+ }
+ mHandler = null;
+ mThread = null;
+ }
+ }
+
+ // Must be called on the mHandler.
+ @RequiresApi(19)
+ private void createMetadata() {
+ if (mCallback == null) {
+ return; // Already handled or cancelled. Do nothing.
+ }
+ try {
+ final FontsContractCompat.FontInfo font = retrieveFontInfo();
+
+ final int resultCode = font.getResultCode();
+ if (resultCode == FontsContractCompat.Columns.RESULT_CODE_FONT_UNAVAILABLE) {
+ // The font provider is now downloading. Ask RetryPolicy for when to retry next.
+ synchronized (mLock) {
+ if (mRetryPolicy != null) {
+ final long delayMs = mRetryPolicy.getRetryDelay();
+ if (delayMs >= 0) {
+ scheduleRetry(font.getUri(), delayMs);
+ return;
+ }
+ }
+ }
+ }
+
+ if (resultCode != FontsContractCompat.Columns.RESULT_CODE_OK) {
+ throw new RuntimeException("fetchFonts result is not OK. (" + resultCode + ")");
+ }
+
+ // TODO: Good to add new API to create Typeface from FD not to open FD twice.
+ final Typeface typeface = mFontProviderHelper.buildTypeface(mContext, font);
+ final ByteBuffer buffer = TypefaceCompatUtil.mmap(mContext, null, font.getUri());
+ if (buffer == null) {
+ throw new RuntimeException("Unable to open file.");
+ }
+ mCallback.onLoaded(MetadataRepo.create(typeface, buffer));
+ cleanUp();
+ } catch (Throwable t) {
+ mCallback.onFailed(t);
+ cleanUp();
+ }
+ }
}
/**
@@ -166,14 +327,34 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static class FontsContractDelegate {
+ public static class FontProviderHelper {
/** Calls FontsContractCompat.fetchFonts. */
public FontFamilyResult fetchFonts(@NonNull Context context,
@NonNull FontRequest request) throws NameNotFoundException {
return FontsContractCompat.fetchFonts(context, null /* cancellation signal */, request);
}
- }
- private static final FontsContractDelegate DEFAULT_FONTS_CONTRACT = new FontsContractDelegate();
+ /** Calls FontsContractCompat.buildTypeface. */
+ public Typeface buildTypeface(@NonNull Context context,
+ @NonNull FontsContractCompat.FontInfo font) throws NameNotFoundException {
+ return FontsContractCompat.buildTypeface(context, null /* cancellation signal */,
+ new FontsContractCompat.FontInfo[] { font });
+ }
+
+ /** Calls Context.getContentObserver().registerObserver */
+ public void registerObserver(@NonNull Context context, @NonNull Uri uri,
+ @NonNull ContentObserver observer) {
+ context.getContentResolver().registerContentObserver(
+ uri, false /* notifyForDescendants */, observer);
+
+ }
+ /** Calls Context.getContentObserver().unregisterObserver */
+ public void unregisterObserver(@NonNull Context context,
+ @NonNull ContentObserver observer) {
+ context.getContentResolver().unregisterContentObserver(observer);
+ }
+ };
+
+ private static final FontProviderHelper DEFAULT_FONTS_CONTRACT = new FontProviderHelper();
}
diff --git a/emoji/core/tests/java/android/support/text/emoji/FontRequestEmojiCompatConfigTest.java b/emoji/core/tests/java/android/support/text/emoji/FontRequestEmojiCompatConfigTest.java
index 8201e96..ce2b098 100644
--- a/emoji/core/tests/java/android/support/text/emoji/FontRequestEmojiCompatConfigTest.java
+++ b/emoji/core/tests/java/android/support/text/emoji/FontRequestEmojiCompatConfigTest.java
@@ -28,10 +28,13 @@
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -39,7 +42,11 @@
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.ContentObserver;
import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.support.annotation.GuardedBy;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.test.InstrumentationRegistry;
@@ -70,14 +77,14 @@
private static final int DEFAULT_TIMEOUT_MILLIS = 3000;
private Context mContext;
private FontRequest mFontRequest;
- private FontRequestEmojiCompatConfig.FontsContractDelegate mFontsContract;
+ private FontRequestEmojiCompatConfig.FontProviderHelper mFontProviderHelper;
@Before
public void setup() {
mContext = InstrumentationRegistry.getContext();
mFontRequest = new FontRequest("authority", "package", "query",
new ArrayList<List<byte[]>>());
- mFontsContract = mock(FontRequestEmojiCompatConfig.FontsContractDelegate.class);
+ mFontProviderHelper = mock(FontRequestEmojiCompatConfig.FontProviderHelper.class);
}
@Test(expected = NullPointerException.class)
@@ -91,28 +98,32 @@
}
@Test
+ @SdkSuppress(minSdkVersion = 19)
+ @TargetApi(19)
public void testLoad_whenGetFontThrowsException() throws NameNotFoundException {
final Exception exception = new RuntimeException();
- doThrow(exception).when(mFontsContract).fetchFonts(
+ doThrow(exception).when(mFontProviderHelper).fetchFonts(
any(Context.class), any(FontRequest.class));
final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext, mFontRequest,
- mFontsContract);
+ mFontProviderHelper);
- config.getMetadataLoader().load(callback);
+ config.getMetadataRepoLoader().load(callback);
callback.await(DEFAULT_TIMEOUT_MILLIS);
verify(callback, times(1)).onFailed(same(exception));
}
@Test
+ @SdkSuppress(minSdkVersion = 19)
+ @TargetApi(19)
public void testLoad_providerNotFound() throws NameNotFoundException {
- doThrow(new NameNotFoundException()).when(mFontsContract).fetchFonts(
+ doThrow(new NameNotFoundException()).when(mFontProviderHelper).fetchFonts(
any(Context.class), any(FontRequest.class));
final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
- mFontRequest, mFontsContract);
+ mFontRequest, mFontProviderHelper);
- config.getMetadataLoader().load(callback);
+ config.getMetadataRepoLoader().load(callback);
callback.await(DEFAULT_TIMEOUT_MILLIS);
final ArgumentCaptor<Throwable> argumentCaptor = ArgumentCaptor.forClass(Throwable.class);
@@ -121,26 +132,32 @@
}
@Test
+ @SdkSuppress(minSdkVersion = 19)
+ @TargetApi(19)
public void testLoad_wrongCertificate() throws NameNotFoundException {
verifyLoaderOnFailedCalled(STATUS_WRONG_CERTIFICATES, null /* fonts */,
"fetchFonts failed (" + STATUS_WRONG_CERTIFICATES + ")");
}
@Test
+ @SdkSuppress(minSdkVersion = 19)
+ @TargetApi(19)
public void testLoad_fontNotFound() throws NameNotFoundException {
verifyLoaderOnFailedCalled(STATUS_OK,
getTestFontInfoWithInvalidPath(RESULT_CODE_FONT_NOT_FOUND),
"fetchFonts result is not OK. (" + RESULT_CODE_FONT_NOT_FOUND + ")");
}
- @Test
+ @Test@SdkSuppress(minSdkVersion = 19)
+ @TargetApi(19)
public void testLoad_fontUnavailable() throws NameNotFoundException {
verifyLoaderOnFailedCalled(STATUS_OK,
getTestFontInfoWithInvalidPath(RESULT_CODE_FONT_UNAVAILABLE),
"fetchFonts result is not OK. (" + RESULT_CODE_FONT_UNAVAILABLE + ")");
}
- @Test
+ @Test@SdkSuppress(minSdkVersion = 19)
+ @TargetApi(19)
public void testLoad_malformedQuery() throws NameNotFoundException {
verifyLoaderOnFailedCalled(STATUS_OK,
getTestFontInfoWithInvalidPath(RESULT_CODE_MALFORMED_QUERY),
@@ -148,18 +165,24 @@
}
@Test
+ @SdkSuppress(minSdkVersion = 19)
+ @TargetApi(19)
public void testLoad_resultNotFound() throws NameNotFoundException {
verifyLoaderOnFailedCalled(STATUS_OK, new FontInfo[] {},
"fetchFonts failed (empty result)");
}
@Test
+ @SdkSuppress(minSdkVersion = 19)
+ @TargetApi(19)
public void testLoad_nullFontInfo() throws NameNotFoundException {
verifyLoaderOnFailedCalled(STATUS_OK, null /* fonts */,
"fetchFonts failed (empty result)");
}
@Test
+ @SdkSuppress(minSdkVersion = 19)
+ @TargetApi(19)
public void testLoad_cannotLoadTypeface() throws NameNotFoundException {
// getTestFontInfoWithInvalidPath returns FontInfo with invalid path to file.
verifyLoaderOnFailedCalled(STATUS_OK,
@@ -176,26 +199,306 @@
new FontInfo(Uri.fromFile(file), 0 /* ttc index */, 400 /* weight */,
false /* italic */, RESULT_CODE_OK)
};
- doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontsContract).fetchFonts(
+ doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
any(Context.class), any(FontRequest.class));
final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
- mFontRequest, mFontsContract);
+ mFontRequest, mFontProviderHelper);
- config.getMetadataLoader().load(callback);
+ config.getMetadataRepoLoader().load(callback);
callback.await(DEFAULT_TIMEOUT_MILLIS);
verify(callback, times(1)).onLoaded(any(MetadataRepo.class));
}
+ @Test
+ @SdkSuppress(minSdkVersion = 19)
+ @TargetApi(19)
+ public void testLoad_retryPolicy() throws IOException, NameNotFoundException {
+ final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
+ final FontInfo[] fonts = new FontInfo[] {
+ new FontInfo(Uri.fromFile(file), 0 /* ttc index */, 400 /* weight */,
+ false /* italic */, RESULT_CODE_FONT_UNAVAILABLE)
+ };
+ doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
+ any(Context.class), any(FontRequest.class));
+ final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
+ final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(-1, 1));
+ final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
+ mFontRequest, mFontProviderHelper).setRetryPolicy(retryPolicy);
+
+ config.getMetadataRepoLoader().load(callback);
+ callback.await(DEFAULT_TIMEOUT_MILLIS);
+ verify(callback, never()).onLoaded(any(MetadataRepo.class));
+ verify(callback, times(1)).onFailed(any(Throwable.class));
+ verify(retryPolicy, times(1)).getRetryDelay();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 19)
+ @TargetApi(19)
+ public void testLoad_keepRetryingAndGiveUp() throws IOException, NameNotFoundException {
+ final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
+ final FontInfo[] fonts = new FontInfo[] {
+ new FontInfo(Uri.fromFile(file), 0 /* ttc index */, 400 /* weight */,
+ false /* italic */, RESULT_CODE_FONT_UNAVAILABLE)
+ };
+ doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
+ any(Context.class), any(FontRequest.class));
+ final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
+ final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(500, 1));
+ final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
+ mFontRequest, mFontProviderHelper).setRetryPolicy(retryPolicy);
+
+ config.getMetadataRepoLoader().load(callback);
+ retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
+ verify(callback, never()).onLoaded(any(MetadataRepo.class));
+ verify(callback, never()).onFailed(any(Throwable.class));
+ verify(retryPolicy, atLeastOnce()).getRetryDelay();
+ retryPolicy.changeReturnValue(-1);
+ callback.await(DEFAULT_TIMEOUT_MILLIS);
+ verify(callback, never()).onLoaded(any(MetadataRepo.class));
+ verify(callback, times(1)).onFailed(any(Throwable.class));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 19)
+ @TargetApi(19)
+ public void testLoad_keepRetryingAndFail() throws IOException, NameNotFoundException {
+ final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
+ final Uri uri = Uri.fromFile(file);
+
+ final FontInfo[] fonts = new FontInfo[] {
+ new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
+ false /* italic */, RESULT_CODE_FONT_UNAVAILABLE)
+ };
+ doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
+ any(Context.class), any(FontRequest.class));
+ final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
+ final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(500, 1));
+
+ HandlerThread thread = new HandlerThread("testThread");
+ thread.start();
+ try {
+ Handler handler = new Handler(thread.getLooper());
+
+ final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
+ mFontRequest, mFontProviderHelper).setHandler(handler)
+ .setRetryPolicy(retryPolicy);
+
+ config.getMetadataRepoLoader().load(callback);
+ retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
+ verify(callback, never()).onLoaded(any(MetadataRepo.class));
+ verify(callback, never()).onFailed(any(Throwable.class));
+ verify(retryPolicy, atLeastOnce()).getRetryDelay();
+
+ // To avoid race condition, change the fetchFonts result on the handler thread.
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ final FontInfo[] fontsSuccess = new FontInfo[] {
+ new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
+ false /* italic */, RESULT_CODE_FONT_NOT_FOUND)
+ };
+
+ doReturn(new FontFamilyResult(STATUS_OK, fontsSuccess)).when(
+ mFontProviderHelper).fetchFonts(any(Context.class),
+ any(FontRequest.class));
+ } catch (NameNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ });
+
+ callback.await(DEFAULT_TIMEOUT_MILLIS);
+ verify(callback, never()).onLoaded(any(MetadataRepo.class));
+ verify(callback, times(1)).onFailed(any(Throwable.class));
+ } finally {
+ thread.quit();
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 19)
+ @TargetApi(19)
+ public void testLoad_keepRetryingAndSuccess() throws IOException, NameNotFoundException {
+ final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
+ final Uri uri = Uri.fromFile(file);
+
+ final FontInfo[] fonts = new FontInfo[]{
+ new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
+ false /* italic */, RESULT_CODE_FONT_UNAVAILABLE)
+ };
+ doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
+ any(Context.class), any(FontRequest.class));
+ final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
+ final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(500, 1));
+
+ HandlerThread thread = new HandlerThread("testThread");
+ thread.start();
+ try {
+ Handler handler = new Handler(thread.getLooper());
+
+ final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
+ mFontRequest, mFontProviderHelper).setHandler(handler)
+ .setRetryPolicy(retryPolicy);
+
+ config.getMetadataRepoLoader().load(callback);
+ retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
+ verify(callback, never()).onLoaded(any(MetadataRepo.class));
+ verify(callback, never()).onFailed(any(Throwable.class));
+ verify(retryPolicy, atLeastOnce()).getRetryDelay();
+
+ final FontInfo[] fontsSuccess = new FontInfo[]{
+ new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
+ false /* italic */, RESULT_CODE_OK)
+ };
+
+ // To avoid race condition, change the fetchFonts result on the handler thread.
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ doReturn(new FontFamilyResult(STATUS_OK, fontsSuccess)).when(
+ mFontProviderHelper).fetchFonts(any(Context.class),
+ any(FontRequest.class));
+ } catch (NameNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ });
+
+ callback.await(DEFAULT_TIMEOUT_MILLIS);
+ verify(callback, times(1)).onLoaded(any(MetadataRepo.class));
+ verify(callback, never()).onFailed(any(Throwable.class));
+ } finally {
+ thread.quit();
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 19)
+ @TargetApi(19)
+ public void testLoad_ObserverNotifyAndSuccess() throws IOException, NameNotFoundException {
+ final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
+ final Uri uri = Uri.fromFile(file);
+ final FontInfo[] fonts = new FontInfo[]{
+ new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
+ false /* italic */, RESULT_CODE_FONT_UNAVAILABLE)
+ };
+ doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
+ any(Context.class), any(FontRequest.class));
+ final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
+ final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(500, 2));
+
+ HandlerThread thread = new HandlerThread("testThread");
+ thread.start();
+ try {
+ Handler handler = new Handler(thread.getLooper());
+ final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
+ mFontRequest, mFontProviderHelper).setHandler(handler)
+ .setRetryPolicy(retryPolicy);
+
+ ArgumentCaptor<ContentObserver> observerCaptor =
+ ArgumentCaptor.forClass(ContentObserver.class);
+
+ config.getMetadataRepoLoader().load(callback);
+ retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
+ verify(callback, never()).onLoaded(any(MetadataRepo.class));
+ verify(callback, never()).onFailed(any(Throwable.class));
+ verify(retryPolicy, atLeastOnce()).getRetryDelay();
+ verify(mFontProviderHelper, times(1)).registerObserver(
+ any(Context.class), eq(uri), observerCaptor.capture());
+
+ final FontInfo[] fontsSuccess = new FontInfo[]{
+ new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
+ false /* italic */, RESULT_CODE_OK)
+ };
+ doReturn(new FontFamilyResult(STATUS_OK, fontsSuccess)).when(
+ mFontProviderHelper).fetchFonts(any(Context.class), any(FontRequest.class));
+
+ final ContentObserver observer = observerCaptor.getValue();
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ observer.onChange(false /* self change */, uri);
+ }
+ });
+
+ callback.await(DEFAULT_TIMEOUT_MILLIS);
+ verify(callback, times(1)).onLoaded(any(MetadataRepo.class));
+ verify(callback, never()).onFailed(any(Throwable.class));
+ } finally {
+ thread.quit();
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 19)
+ @TargetApi(19)
+ public void testLoad_ObserverNotifyAndFail() throws IOException, NameNotFoundException {
+ final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
+ final Uri uri = Uri.fromFile(file);
+ final FontInfo[] fonts = new FontInfo[]{
+ new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
+ false /* italic */, RESULT_CODE_FONT_UNAVAILABLE)
+ };
+ doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
+ any(Context.class), any(FontRequest.class));
+ final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
+ final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(500, 2));
+
+ HandlerThread thread = new HandlerThread("testThread");
+ thread.start();
+ try {
+ Handler handler = new Handler(thread.getLooper());
+ final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
+ mFontRequest, mFontProviderHelper).setHandler(handler)
+ .setRetryPolicy(retryPolicy);
+
+ ArgumentCaptor<ContentObserver> observerCaptor =
+ ArgumentCaptor.forClass(ContentObserver.class);
+
+ config.getMetadataRepoLoader().load(callback);
+ retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
+ verify(callback, never()).onLoaded(any(MetadataRepo.class));
+ verify(callback, never()).onFailed(any(Throwable.class));
+ verify(retryPolicy, atLeastOnce()).getRetryDelay();
+ verify(mFontProviderHelper, times(1)).registerObserver(
+ any(Context.class), eq(uri), observerCaptor.capture());
+
+ final FontInfo[] fontsSuccess = new FontInfo[]{
+ new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
+ false /* italic */, RESULT_CODE_FONT_NOT_FOUND)
+ };
+ doReturn(new FontFamilyResult(STATUS_OK, fontsSuccess)).when(
+ mFontProviderHelper).fetchFonts(any(Context.class), any(FontRequest.class));
+
+ final ContentObserver observer = observerCaptor.getValue();
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ observer.onChange(false /* self change */, uri);
+ }
+ });
+
+ callback.await(DEFAULT_TIMEOUT_MILLIS);
+ verify(callback, never()).onLoaded(any(MetadataRepo.class));
+ verify(callback, times(1)).onFailed(any(Throwable.class));
+ } finally {
+ thread.quit();
+ }
+ }
+
private void verifyLoaderOnFailedCalled(final int statusCode,
final FontInfo[] fonts, String exceptionMessage) throws NameNotFoundException {
- doReturn(new FontFamilyResult(statusCode, fonts)).when(mFontsContract).fetchFonts(
+ doReturn(new FontFamilyResult(statusCode, fonts)).when(mFontProviderHelper).fetchFonts(
any(Context.class), any(FontRequest.class));
final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext, mFontRequest,
- mFontsContract);
+ mFontProviderHelper);
- config.getMetadataLoader().load(callback);
+ config.getMetadataRepoLoader().load(callback);
callback.await(DEFAULT_TIMEOUT_MILLIS);
final ArgumentCaptor<Throwable> argumentCaptor = ArgumentCaptor.forClass(Throwable.class);
@@ -203,6 +506,42 @@
assertThat(argumentCaptor.getValue().getMessage(), containsString(exceptionMessage));
}
+ public static class WaitingRetryPolicy extends FontRequestEmojiCompatConfig.RetryPolicy {
+ private final CountDownLatch mLatch;
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private long mReturnValue;
+
+ public WaitingRetryPolicy(long returnValue, int callCount) {
+ mLatch = new CountDownLatch(callCount);
+ synchronized (mLock) {
+ mReturnValue = returnValue;
+ }
+ }
+
+ @Override
+ public long getRetryDelay() {
+ mLatch.countDown();
+ synchronized (mLock) {
+ return mReturnValue;
+ }
+ }
+
+ public void changeReturnValue(long value) {
+ synchronized (mLock) {
+ mReturnValue = value;
+ }
+ }
+
+ public void await(long timeoutMillis) {
+ try {
+ mLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
public static class WaitingLoaderCallback extends EmojiCompat.MetadataRepoLoaderCallback {
final CountDownLatch mLatch;
diff --git a/exifinterface/src/android/support/media/ExifInterface.java b/exifinterface/src/android/support/media/ExifInterface.java
index dac55bd..4a68dd7 100644
--- a/exifinterface/src/android/support/media/ExifInterface.java
+++ b/exifinterface/src/android/support/media/ExifInterface.java
@@ -19,6 +19,7 @@
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
+import android.location.Location;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.util.Log;
@@ -51,6 +52,7 @@
import java.util.HashSet;
import java.util.Map;
import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -517,6 +519,10 @@
public final long numerator;
public final long denominator;
+ private Rational(double value) {
+ this((long) (value * 10000), 10000);
+ }
+
private Rational(long numerator, long denominator) {
// Handle erroneous case
if (denominator == 0) {
@@ -1446,7 +1452,7 @@
} else {
try {
double doubleValue = Double.parseDouble(value);
- value = (long) (doubleValue * 10000L) + "/10000";
+ value = new Rational(doubleValue).toString();
} catch (NumberFormatException e) {
Log.w(TAG, "Invalid value for " + tag + " : " + value);
return;
@@ -1899,6 +1905,28 @@
}
/**
+ * Sets the GPS-related information. It will set GPS processing method, latitude and longitude
+ * values, GPS timestamp, and speed information at the same time.
+ *
+ * @param location the {@link Location} object returned by GPS service.
+ */
+ public void setGpsInfo(Location location) {
+ if (location == null) {
+ return;
+ }
+ setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider());
+ setLatLong(location.getLatitude(), location.getLongitude());
+ setAltitude(location.getAltitude());
+ // Location objects store speeds in m/sec. Translates it to km/hr here.
+ setAttribute(TAG_GPS_SPEED_REF, "K");
+ setAttribute(TAG_GPS_SPEED, new Rational(location.getSpeed()
+ * TimeUnit.HOURS.toSeconds(1) / 1000).toString());
+ String[] dateTime = sFormatter.format(new Date(location.getTime())).split("\\s+");
+ setAttribute(ExifInterface.TAG_GPS_DATESTAMP, dateTime[0]);
+ setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, dateTime[1]);
+ }
+
+ /**
* Sets the latitude and longitude values.
*
* @param latitude the decimal value of latitude. Must be a valid double value between -90.0 and
@@ -1939,6 +1967,27 @@
}
/**
+ * Sets the altitude in meters.
+ */
+ public void setAltitude(double altitude) {
+ String ref = altitude >= 0 ? "0" : "1";
+ setAttribute(TAG_GPS_ALTITUDE, new Rational(Math.abs(altitude)).toString());
+ setAttribute(TAG_GPS_ALTITUDE_REF, ref);
+ }
+
+ /**
+ * Set the date time value.
+ *
+ * @param timeStamp number of milliseconds since Jan. 1, 1970, midnight local time.
+ * @hide
+ */
+ public void setDateTime(long timeStamp) {
+ long sub = timeStamp % 1000;
+ setAttribute(TAG_DATETIME, sFormatter.format(new Date(timeStamp)));
+ setAttribute(TAG_SUBSEC_TIME, Long.toString(sub));
+ }
+
+ /**
* Returns number of milliseconds since Jan. 1, 1970, midnight local time.
* Returns -1 if the date time information if not available.
* @hide
diff --git a/exifinterface/tests/src/android/support/media/ExifInterfaceTest.java b/exifinterface/tests/src/android/support/media/ExifInterfaceTest.java
index 4af8a4a..2223029 100644
--- a/exifinterface/tests/src/android/support/media/ExifInterfaceTest.java
+++ b/exifinterface/tests/src/android/support/media/ExifInterfaceTest.java
@@ -25,6 +25,7 @@
import android.content.res.TypedArray;
import android.graphics.Bitmap;
+import android.location.Location;
import android.os.Environment;
import android.support.exifinterface.test.R;
import android.support.test.filters.LargeTest;
@@ -46,6 +47,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.concurrent.TimeUnit;
/**
* Test {@link ExifInterface}.
@@ -66,6 +68,8 @@
private static final String TEST_TEMP_FILE_NAME = "testImage";
private static final double DELTA = 1e-8;
+ // We translate double to rational in a 1/10000 precision.
+ private static final double RATIONAL_DELTA = 0.0001;
private static final int TEST_LAT_LONG_VALUES_ARRAY_LENGTH = 8;
private static final double[] TEST_LATITUDE_VALID_VALUES = new double[]
{0, 45, 90, -60, 0.00000001, -89.999999999, 14.2465923626, -68.3434534737};
@@ -77,6 +81,8 @@
private static final double[] TEST_LONGITUDE_INVALID_VALUES = new double[]
{Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 180.0000000001,
263.34763236326, -1e10, 347.325252623, -4000.346323236};
+ private static final double[] TEST_ALTITUDE_VALUES = new double[]
+ {0, -2000, 10000, -355.99999999999, 18.02038};
private static final String[] EXIF_TAGS = {
ExifInterface.TAG_MAKE,
@@ -251,6 +257,38 @@
@Test
@SmallTest
+ public void testSetGpsInfo() throws IOException {
+ final String provider = "ExifInterfaceTest";
+ final long timestamp = System.currentTimeMillis();
+ final float speedInMeterPerSec = 36.627533f;
+ Location location = new Location(provider);
+ location.setLatitude(TEST_LATITUDE_VALID_VALUES[TEST_LATITUDE_VALID_VALUES.length - 1]);
+ location.setLongitude(TEST_LONGITUDE_VALID_VALUES[TEST_LONGITUDE_VALID_VALUES.length - 1]);
+ location.setAltitude(TEST_ALTITUDE_VALUES[TEST_ALTITUDE_VALUES.length - 1]);
+ location.setSpeed(speedInMeterPerSec);
+ location.setTime(timestamp);
+ ExifInterface exif = createTestExifInterface();
+ exif.setGpsInfo(location);
+
+ double[] latLong = exif.getLatLong();
+ assertNotNull(latLong);
+ assertEquals(TEST_LATITUDE_VALID_VALUES[TEST_LATITUDE_VALID_VALUES.length - 1],
+ latLong[0], DELTA);
+ assertEquals(TEST_LONGITUDE_VALID_VALUES[TEST_LONGITUDE_VALID_VALUES.length - 1],
+ latLong[1], DELTA);
+ assertEquals(TEST_ALTITUDE_VALUES[TEST_ALTITUDE_VALUES.length - 1], exif.getAltitude(0),
+ RATIONAL_DELTA);
+ assertEquals("K", exif.getAttribute(ExifInterface.TAG_GPS_SPEED_REF));
+ assertEquals(speedInMeterPerSec, exif.getAttributeDouble(ExifInterface.TAG_GPS_SPEED, 0.0)
+ * 1000 / TimeUnit.HOURS.toSeconds(1), RATIONAL_DELTA);
+ assertEquals(provider, exif.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD));
+ // GPS time's precision is secs.
+ assertEquals(TimeUnit.MILLISECONDS.toSeconds(timestamp),
+ TimeUnit.MILLISECONDS.toSeconds(exif.getGpsDateTime()));
+ }
+
+ @Test
+ @SmallTest
public void testSetLatLong_withValidValues() throws IOException {
for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) {
ExifInterface exif = createTestExifInterface();
@@ -297,6 +335,16 @@
@Test
@SmallTest
+ public void testSetAltitude() throws IOException {
+ for (int i = 0; i < TEST_ALTITUDE_VALUES.length; i++) {
+ ExifInterface exif = createTestExifInterface();
+ exif.setAltitude(TEST_ALTITUDE_VALUES[i]);
+ assertEquals(TEST_ALTITUDE_VALUES[i], exif.getAltitude(Double.NaN), RATIONAL_DELTA);
+ }
+ }
+
+ @Test
+ @SmallTest
public void testSetDateTime() throws IOException {
final String dateTimeValue = "2017:02:02 22:22:22";
final String dateTimeOriginalValue = "2017:01:01 11:11:11";
@@ -320,6 +368,12 @@
// When the DATETIME has no value, then it should be set to DATETIME_ORIGINAL's value.
exif = new ExifInterface(imageFile.getAbsolutePath());
assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
+
+ long currentTimeStamp = System.currentTimeMillis();
+ exif.setDateTime(currentTimeStamp);
+ exif.saveAttributes();
+ exif = new ExifInterface(imageFile.getAbsolutePath());
+ assertEquals(currentTimeStamp, exif.getDateTime());
}
private void printExifTagsAndValues(String fileName, ExifInterface exifInterface) {
diff --git a/gradle.properties b/gradle.properties
index d1c2c14..eea983a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,5 @@
org.gradle.jvmargs=-Xmx4g
org.gradle.daemon=true
org.gradle.configureondemand=true
-org.gradle.parallel=true
\ No newline at end of file
+org.gradle.parallel=true
+org.gradle.caching=true
diff --git a/media-compat/java/android/support/v4/media/session/MediaControllerCompat.java b/media-compat/java/android/support/v4/media/session/MediaControllerCompat.java
index 47bef3b..cfe13b3 100644
--- a/media-compat/java/android/support/v4/media/session/MediaControllerCompat.java
+++ b/media-compat/java/android/support/v4/media/session/MediaControllerCompat.java
@@ -241,7 +241,7 @@
}
/**
- * Get a {@link TransportControls} instance for this session.
+ * Gets a {@link TransportControls} instance for this session.
*
* @return A controls instance
*/
@@ -250,7 +250,7 @@
}
/**
- * Send the specified media button event to the session. Only media keys can
+ * Sends the specified media button event to the session. Only media keys can
* be sent by this method, other keys will be ignored.
*
* @param keyEvent The media button event to dispatch.
@@ -264,7 +264,7 @@
}
/**
- * Get the current playback state for this session.
+ * Gets the current playback state for this session.
*
* @return The current PlaybackState or null
*/
@@ -273,7 +273,7 @@
}
/**
- * Get the current metadata for this session.
+ * Gets the current metadata for this session.
*
* @return The current MediaMetadata or null.
*/
@@ -282,7 +282,7 @@
}
/**
- * Get the current play queue for this session if one is set. If you only
+ * Gets the current play queue for this session if one is set. If you only
* care about the current item {@link #getMetadata()} should be used.
*
* @return The current play queue or null.
@@ -292,7 +292,7 @@
}
/**
- * Add a queue item from the given {@code description} at the end of the play queue
+ * Adds a queue item from the given {@code description} at the end of the play queue
* of this session. Not all sessions may support this. To know whether the session supports
* this, get the session's flags with {@link #getFlags()} and check that the flag
* {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
@@ -308,7 +308,7 @@
}
/**
- * Add a queue item from the given {@code description} at the specified position
+ * Adds a queue item from the given {@code description} at the specified position
* in the play queue of this session. Shifts the queue item currently at that position
* (if any) and any subsequent queue items to the right (adds one to their indices).
* Not all sessions may support this. To know whether the session supports this,
@@ -328,7 +328,7 @@
}
/**
- * Remove the first occurrence of the specified {@link MediaSessionCompat.QueueItem}
+ * Removes the first occurrence of the specified {@link MediaSessionCompat.QueueItem}
* with the given {@link MediaDescriptionCompat description} in the play queue of the
* associated session. Not all sessions may support this. To know whether the session supports
* this, get the session's flags with {@link #getFlags()} and check that the flag
@@ -345,7 +345,7 @@
}
/**
- * Remove an queue item at the specified position in the play queue
+ * Removes an queue item at the specified position in the play queue
* of this session. Not all sessions may support this. To know whether the session supports
* this, get the session's flags with {@link #getFlags()} and check that the flag
* {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
@@ -368,21 +368,21 @@
}
/**
- * Get the queue title for this session.
+ * Gets the queue title for this session.
*/
public CharSequence getQueueTitle() {
return mImpl.getQueueTitle();
}
/**
- * Get the extras for this session.
+ * Gets the extras for this session.
*/
public Bundle getExtras() {
return mImpl.getExtras();
}
/**
- * Get the rating type supported by the session. One of:
+ * Gets the rating type supported by the session. One of:
* <ul>
* <li>{@link RatingCompat#RATING_NONE}</li>
* <li>{@link RatingCompat#RATING_HEART}</li>
@@ -400,7 +400,7 @@
}
/**
- * Return whether captioning is enabled for this session.
+ * Returns whether captioning is enabled for this session.
*
* @return {@code true} if captioning is enabled, {@code false} if disabled or not set.
*/
@@ -409,7 +409,7 @@
}
/**
- * Get the repeat mode for this session.
+ * Gets the repeat mode for this session.
*
* @return The latest repeat mode set to the session, or
* {@link PlaybackStateCompat#REPEAT_MODE_NONE} if not set.
@@ -419,7 +419,7 @@
}
/**
- * Return whether the shuffle mode is enabled for this session.
+ * Returns whether the shuffle mode is enabled for this session.
*
* @return {@code true} if the shuffle mode is enabled, {@code false} if disabled or not set.
* @deprecated Use {@link #getShuffleMode} instead.
@@ -430,7 +430,7 @@
}
/**
- * Get the shuffle mode for this session.
+ * Gets the shuffle mode for this session.
*
* @return The latest shuffle mode set to the session, or
* {@link PlaybackStateCompat#SHUFFLE_MODE_NONE} if not set.
@@ -440,7 +440,7 @@
}
/**
- * Get the flags for this session. Flags are defined in
+ * Gets the flags for this session. Flags are defined in
* {@link MediaSessionCompat}.
*
* @return The current set of flags for the session.
@@ -450,7 +450,7 @@
}
/**
- * Get the current playback info for this session.
+ * Gets the current playback info for this session.
*
* @return The current playback info or null.
*/
@@ -459,7 +459,7 @@
}
/**
- * Get an intent for launching UI associated with this session if one
+ * Gets an intent for launching UI associated with this session if one
* exists.
*
* @return A {@link PendingIntent} to launch UI or null.
@@ -469,7 +469,7 @@
}
/**
- * Get the token for the session this controller is connected to.
+ * Gets the token for the session this controller is connected to.
*
* @return The session's token.
*/
@@ -478,7 +478,7 @@
}
/**
- * Set the volume of the output this session is playing on. The command will
+ * Sets the volume of the output this session is playing on. The command will
* be ignored if it does not support
* {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. The flags in
* {@link AudioManager} may be used to affect the handling.
@@ -493,7 +493,7 @@
}
/**
- * Adjust the volume of the output this session is playing on. The direction
+ * Adjusts the volume of the output this session is playing on. The direction
* must be one of {@link AudioManager#ADJUST_LOWER},
* {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}.
* The command will be ignored if the session does not support
@@ -538,7 +538,7 @@
}
/**
- * Stop receiving updates on the specified callback. If an update has
+ * Stops receiving updates on the specified callback. If an update has
* already been posted you may still receive it after calling this method.
*
* @param callback The callback to remove
@@ -567,7 +567,7 @@
}
/**
- * Get the session owner's package name.
+ * Gets the session owner's package name.
*
* @return The package name of of the session owner.
*/
@@ -665,7 +665,7 @@
}
/**
- * Override to handle chagnes to the {@link MediaSessionCompat} extras.
+ * Override to handle changes to the {@link MediaSessionCompat} extras.
*
* @param extras The extras that can include other information
* associated with the {@link MediaSessionCompat}.
@@ -728,7 +728,7 @@
}
/**
- * Set the handler to use for pre 21 callbacks.
+ * Sets the handler to use for pre 21 callbacks.
*/
private void setHandler(Handler handler) {
mHandler = new MessageHandler(handler.getLooper());
@@ -1040,7 +1040,7 @@
public abstract void playFromUri(Uri uri, Bundle extras);
/**
- * Play an item with a specific id in the play queue. If you specify an
+ * Plays an item with a specific id in the play queue. If you specify an
* id that is not in the play queue, the behavior is undefined.
*/
public abstract void skipToQueueItem(long id);
@@ -1058,36 +1058,36 @@
public abstract void stop();
/**
- * Move to a new location in the media stream.
+ * Moves to a new location in the media stream.
*
* @param pos Position to move to, in milliseconds.
*/
public abstract void seekTo(long pos);
/**
- * Start fast forwarding. If playback is already fast forwarding this
+ * Starts fast forwarding. If playback is already fast forwarding this
* may increase the rate.
*/
public abstract void fastForward();
/**
- * Skip to the next item.
+ * Skips to the next item.
*/
public abstract void skipToNext();
/**
- * Start rewinding. If playback is already rewinding this may increase
+ * Starts rewinding. If playback is already rewinding this may increase
* the rate.
*/
public abstract void rewind();
/**
- * Skip to the previous item.
+ * Skips to the previous item.
*/
public abstract void skipToPrevious();
/**
- * Rate the current content. This will cause the rating to be set for
+ * Rates the current content. This will cause the rating to be set for
* the current user. The Rating type must match the type returned by
* {@link #getRatingType()}.
*
@@ -1096,14 +1096,14 @@
public abstract void setRating(RatingCompat rating);
/**
- * Enable/disable captioning for this session.
+ * Enables/disables captioning for this session.
*
* @param enabled {@code true} to enable captioning, {@code false} to disable.
*/
public abstract void setCaptioningEnabled(boolean enabled);
/**
- * Set the repeat mode for this session.
+ * Sets the repeat mode for this session.
*
* @param repeatMode The repeat mode. Must be one of the followings:
* {@link PlaybackStateCompat#REPEAT_MODE_NONE},
@@ -1114,7 +1114,7 @@
public abstract void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode);
/**
- * Set the shuffle mode for this session.
+ * Sets the shuffle mode for this session.
*
* @param enabled {@code true} to enable the shuffle mode, {@code false} to disable.
* @deprecated Use {@link #setShuffleMode} instead.
@@ -1123,7 +1123,7 @@
public abstract void setShuffleModeEnabled(boolean enabled);
/**
- * Set the shuffle mode for this session.
+ * Sets the shuffle mode for this session.
*
* @param shuffleMode The shuffle mode. Must be one of the followings:
* {@link PlaybackStateCompat#SHUFFLE_MODE_NONE},
@@ -1133,7 +1133,7 @@
public abstract void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode);
/**
- * Send a custom action for the {@link MediaSessionCompat} to perform.
+ * Sends a custom action for the {@link MediaSessionCompat} to perform.
*
* @param customAction The action to perform.
* @param args Optional arguments to supply to the
@@ -1143,7 +1143,7 @@
Bundle args);
/**
- * Send the id and args from a custom action for the
+ * Sends the id and args from a custom action for the
* {@link MediaSessionCompat} to perform.
*
* @see #sendCustomAction(PlaybackStateCompat.CustomAction action,
@@ -1190,7 +1190,7 @@
}
/**
- * Get the type of volume handling, either local or remote. One of:
+ * Gets the type of volume handling, either local or remote. One of:
* <ul>
* <li>{@link PlaybackInfo#PLAYBACK_TYPE_LOCAL}</li>
* <li>{@link PlaybackInfo#PLAYBACK_TYPE_REMOTE}</li>
@@ -1203,7 +1203,7 @@
}
/**
- * Get the stream this is currently controlling volume on. When the volume
+ * Gets the stream this is currently controlling volume on. When the volume
* type is {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} this value does not
* have meaning and should be ignored.
*
@@ -1215,7 +1215,7 @@
}
/**
- * Get the type of volume control that can be used. One of:
+ * Gets the type of volume control that can be used. One of:
* <ul>
* <li>{@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}</li>
* <li>{@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE}</li>
@@ -1230,7 +1230,7 @@
}
/**
- * Get the maximum volume that may be set for this session.
+ * Gets the maximum volume that may be set for this session.
*
* @return The maximum allowed volume where this session is playing.
*/
@@ -1239,7 +1239,7 @@
}
/**
- * Get the current volume for this session.
+ * Gets the current volume for this session.
*
* @return The current volume where this session is playing.
*/
diff --git a/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java b/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
index ecc2dbc..5df4b72 100644
--- a/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
+++ b/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
@@ -115,19 +115,19 @@
public @interface SessionFlags {}
/**
- * Set this flag on the session to indicate that it can handle media button
+ * Sets this flag on the session to indicate that it can handle media button
* events.
*/
public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1 << 0;
/**
- * Set this flag on the session to indicate that it handles transport
+ * Sets this flag on the session to indicate that it handles transport
* control commands through its {@link Callback}.
*/
public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1;
/**
- * Set this flag on the session to indicate that it handles queue
+ * Sets this flag on the session to indicate that it handles queue
* management commands through its {@link Callback}.
*/
public static final int FLAG_HANDLES_QUEUE_COMMANDS = 1 << 2;
@@ -435,7 +435,7 @@
}
/**
- * Add a callback to receive updates on for the MediaSession. This includes
+ * Adds a callback to receive updates on for the MediaSession. This includes
* media button and volume events. The caller's thread will be used to post
* events.
*
@@ -446,7 +446,7 @@
}
/**
- * Set the callback to receive updates for the MediaSession. This includes
+ * Sets the callback to receive updates for the MediaSession. This includes
* media button and volume events. Set the callback to null to stop
* receiving events.
*
@@ -458,7 +458,7 @@
}
/**
- * Set an intent for launching UI for this Session. This can be used as a
+ * Sets an intent for launching UI for this Session. This can be used as a
* quick link to an ongoing media screen. The intent should be for an
* activity that may be started using
* {@link Activity#startActivity(Intent)}.
@@ -470,7 +470,7 @@
}
/**
- * Set a pending intent for your media button receiver to allow restarting
+ * Sets a pending intent for your media button receiver to allow restarting
* playback after the session has been stopped. If your app is started in
* this way an {@link Intent#ACTION_MEDIA_BUTTON} intent will be sent via
* the pending intent.
@@ -487,7 +487,7 @@
}
/**
- * Set any flags for the session.
+ * Sets any flags for the session.
*
* @param flags The flags to set for this session.
*/
@@ -496,7 +496,7 @@
}
/**
- * Set the stream this session is playing on. This will affect the system's
+ * Sets the stream this session is playing on. This will affect the system's
* volume handling for this session. If {@link #setPlaybackToRemote} was
* previously called it will stop receiving volume commands and the system
* will begin sending volume changes to the appropriate stream.
@@ -510,7 +510,7 @@
}
/**
- * Configure this session to use remote volume handling. This must be called
+ * Configures this session to use remote volume handling. This must be called
* to receive volume button events, otherwise the system will adjust the
* current stream volume for this session. If {@link #setPlaybackToLocal}
* was previously called that stream will stop receiving volume changes for
@@ -532,7 +532,7 @@
}
/**
- * Set if this session is currently active and ready to receive commands. If
+ * Sets if this session is currently active and ready to receive commands. If
* set to false your session's controller may not be discoverable. You must
* set the session to active before it can start receiving media button
* events or transport commands.
@@ -552,7 +552,7 @@
}
/**
- * Get the current active state of this session.
+ * Gets the current active state of this session.
*
* @return True if the session is active, false otherwise.
*/
@@ -561,7 +561,7 @@
}
/**
- * Send a proprietary event to all MediaControllers listening to this
+ * Sends a proprietary event to all MediaControllers listening to this
* Session. It's up to the Controller/Session owner to determine the meaning
* of any events.
*
@@ -585,7 +585,7 @@
}
/**
- * Retrieve a token object that can be used by apps to create a
+ * Retrieves a token object that can be used by apps to create a
* {@link MediaControllerCompat} for interacting with this session. The
* owner of the session is responsible for deciding how to distribute these
* tokens.
@@ -603,7 +603,7 @@
}
/**
- * Get a controller for this session. This is a convenience method to avoid
+ * Gets a controller for this session. This is a convenience method to avoid
* having to cache your own controller in process.
*
* @return A controller for this session.
@@ -613,7 +613,7 @@
}
/**
- * Update the current playback state.
+ * Updates the current playback state.
*
* @param state The current state of playback
*/
@@ -622,7 +622,7 @@
}
/**
- * Update the current metadata. New metadata can be created using
+ * Updates the current metadata. New metadata can be created using
* {@link android.support.v4.media.MediaMetadataCompat.Builder}. This operation may take time
* proportional to the size of the bitmap to replace large bitmaps with a scaled down copy.
*
@@ -634,7 +634,7 @@
}
/**
- * Update the list of items in the play queue. It is an ordered list and
+ * Updates the list of items in the play queue. It is an ordered list and
* should contain the current item, and previous or upcoming items if they
* exist. Specify null if there is no current play queue.
* <p>
@@ -649,7 +649,7 @@
}
/**
- * Set the title of the play queue. The UI should display this title along
+ * Sets the title of the play queue. The UI should display this title along
* with the play queue itself. e.g. "Play Queue", "Now Playing", or an album
* name.
*
@@ -660,7 +660,7 @@
}
/**
- * Set the style of rating used by this session. Apps trying to set the
+ * Sets the style of rating used by this session. Apps trying to set the
* rating should use this style. Must be one of the following:
* <ul>
* <li>{@link RatingCompat#RATING_NONE}</li>
@@ -677,7 +677,7 @@
}
/**
- * Enable/disable captioning for this session.
+ * Enables/disables captioning for this session.
*
* @param enabled {@code true} to enable captioning, {@code false} to disable.
*/
@@ -686,7 +686,7 @@
}
/**
- * Set the repeat mode for this session.
+ * Sets the repeat mode for this session.
* <p>
* Note that if this method is not called before, {@link MediaControllerCompat#getRepeatMode}
* will return {@link PlaybackStateCompat#REPEAT_MODE_NONE}.
@@ -702,7 +702,7 @@
}
/**
- * Set the shuffle mode for this session.
+ * Sets the shuffle mode for this session.
* <p>
* Note that if this method is not called before,
* {@link MediaControllerCompat#isShuffleModeEnabled} will return {@code false}.
@@ -716,7 +716,7 @@
}
/**
- * Set the shuffle mode for this session.
+ * Sets the shuffle mode for this session.
* <p>
* Note that if this method is not called before, {@link MediaControllerCompat#getShuffleMode}
* will return {@link PlaybackStateCompat#SHUFFLE_MODE_NONE}.
@@ -731,7 +731,7 @@
}
/**
- * Set some extras that can be associated with the
+ * Sets some extras that can be associated with the
* {@link MediaSessionCompat}. No assumptions should be made as to how a
* {@link MediaControllerCompat} will handle these extras. Keys should be
* fully qualified (e.g. com.example.MY_EXTRA) to avoid conflicts.
@@ -867,7 +867,7 @@
final Object mCallbackObj;
private WeakReference<MediaSessionImpl> mSessionImpl;
private CallbackHandler mCallbackHandler = null;
- private boolean mMediaPlayPauseKeyHandled;
+ private boolean mMediaPlayPauseKeyPending;
public Callback() {
if (android.os.Build.VERSION.SDK_INT >= 24) {
@@ -924,42 +924,45 @@
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
case KeyEvent.KEYCODE_HEADSETHOOK:
if (keyEvent.getRepeatCount() > 0) {
+ // Consider long-press as a single tap.
+ handleMediaPlayPauseKeySingleTapIfPending();
+ } else if (mMediaPlayPauseKeyPending) {
mCallbackHandler.removeMessages(
CallbackHandler.MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT);
- if (keyEvent.getRepeatCount() == 1) {
- handleMediaPlayPauseKeySingleTapIfUnhandled();
- }
- } else if (mCallbackHandler.hasMessages(
- CallbackHandler.MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT)) {
- mCallbackHandler.removeMessages(
- CallbackHandler.MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT);
+ mMediaPlayPauseKeyPending = false;
PlaybackStateCompat state = impl.getPlaybackState();
long validActions = state == null ? 0 : state.getActions();
// Consider double tap as the next.
if ((validActions & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0) {
onSkipToNext();
}
- mMediaPlayPauseKeyHandled = true;
} else {
- mMediaPlayPauseKeyHandled = false;
+ mMediaPlayPauseKeyPending = true;
mCallbackHandler.sendEmptyMessageDelayed(
CallbackHandler.MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT,
ViewConfiguration.getDoubleTapTimeout());
}
return true;
+ default:
+ // If another key is pressed within double tap timeout, consider the pending
+ // pending play/pause as a single tap to handle media keys in order.
+ handleMediaPlayPauseKeySingleTapIfPending();
+ break;
}
return false;
}
- private void handleMediaPlayPauseKeySingleTapIfUnhandled() {
- if (mMediaPlayPauseKeyHandled) {
+ private void handleMediaPlayPauseKeySingleTapIfPending() {
+ if (!mMediaPlayPauseKeyPending) {
return;
}
+ mMediaPlayPauseKeyPending = false;
+ mCallbackHandler.removeMessages(
+ CallbackHandler.MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT);
MediaSessionImpl impl = mSessionImpl.get();
if (impl == null) {
return;
}
- mMediaPlayPauseKeyHandled = true;
PlaybackStateCompat state = impl.getPlaybackState();
long validActions = state == null ? 0 : state.getActions();
boolean isPlaying = state != null
@@ -1230,7 +1233,7 @@
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT) {
- handleMediaPlayPauseKeySingleTapIfUnhandled();
+ handleMediaPlayPauseKeySingleTapIfPending();
}
}
}
@@ -1585,7 +1588,7 @@
private Object mItem;
/**
- * Create a new {@link MediaSessionCompat.QueueItem}.
+ * Creates a new {@link MediaSessionCompat.QueueItem}.
*
* @param description The {@link MediaDescriptionCompat} for this item.
* @param id An identifier for this item. It must be unique within the
@@ -1613,14 +1616,14 @@
}
/**
- * Get the description for this item.
+ * Gets the description for this item.
*/
public MediaDescriptionCompat getDescription() {
return mDescription;
}
/**
- * Get the queue id for this item.
+ * Gets the queue id for this item.
*/
public long getQueueId() {
return mId;
@@ -1638,7 +1641,7 @@
}
/**
- * Get the underlying
+ * Gets the underlying
* {@link android.media.session.MediaSession.QueueItem}.
* <p>
* On builds before {@link android.os.Build.VERSION_CODES#LOLLIPOP} null
diff --git a/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java b/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java
index a3744cc..b44a085 100644
--- a/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java
+++ b/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java
@@ -600,18 +600,8 @@
PendingIntent pi = PendingIntent.getBroadcast(getContext(), 0, mediaButtonIntent, 0);
mSession.setMediaButtonReceiver(pi);
- long supportedActions = PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE
- | PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_STOP
- | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
- | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
- | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND;
-
// Set state to STATE_PLAYING to get higher priority.
- PlaybackStateCompat defaultState = new PlaybackStateCompat.Builder()
- .setActions(supportedActions)
- .setState(PlaybackStateCompat.STATE_PLAYING, 0L, 0.0f)
- .build();
- mSession.setPlaybackState(defaultState);
+ setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
sessionCallback.reset(1);
sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY);
@@ -651,24 +641,21 @@
// Test PLAY_PAUSE button twice.
// First, send PLAY_PAUSE button event while in STATE_PAUSED.
sessionCallback.reset(1);
- mSession.setPlaybackState(new PlaybackStateCompat.Builder().setActions(supportedActions)
- .setState(PlaybackStateCompat.STATE_PAUSED, 0L, 0.0f).build());
+ setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
assertTrue(sessionCallback.await(TIME_OUT_MS));
assertEquals(1, sessionCallback.mOnPlayCalledCount);
// Next, send PLAY_PAUSE button event while in STATE_PLAYING.
sessionCallback.reset(1);
- mSession.setPlaybackState(new PlaybackStateCompat.Builder().setActions(supportedActions)
- .setState(PlaybackStateCompat.STATE_PLAYING, 0L, 0.0f).build());
+ setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
assertTrue(sessionCallback.await(TIME_OUT_MS));
assertTrue(sessionCallback.mOnPauseCalled);
// Double tap of PLAY_PAUSE is the next track.
sessionCallback.reset(2);
- mSession.setPlaybackState(new PlaybackStateCompat.Builder().setActions(supportedActions)
- .setState(PlaybackStateCompat.STATE_PAUSED, 0L, 0.0f).build());
+ setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
assertFalse(sessionCallback.await(WAIT_TIME_MS));
@@ -679,8 +666,7 @@
// Test PLAY_PAUSE button long-press.
// It should be the same as the single short-press.
sessionCallback.reset(1);
- mSession.setPlaybackState(new PlaybackStateCompat.Builder().setActions(supportedActions)
- .setState(PlaybackStateCompat.STATE_PAUSED, 0L, 0.0f).build());
+ setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, true);
assertTrue(sessionCallback.await(TIME_OUT_MS));
assertEquals(1, sessionCallback.mOnPlayCalledCount);
@@ -700,16 +686,53 @@
// Initial long-press of the PLAY_PAUSE is considered as the single short-press already,
// so it shouldn't be used as the first tap of the double tap.
sessionCallback.reset(2);
- mSession.setPlaybackState(new PlaybackStateCompat.Builder().setActions(supportedActions)
- .setState(PlaybackStateCompat.STATE_PAUSED, 0L, 0.0f).build());
+ setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, true);
sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
assertTrue(sessionCallback.await(TIME_OUT_MS));
// onMediaButtonEvent() calls either onPlay() or onPause() depending on the playback state,
// so onPlay() should be called twice while onPause() isn't called.
- assertEquals(2, sessionCallback.mOnPlayCalledCount);
- assertFalse(sessionCallback.mOnPauseCalled);
+ assertEquals(1, sessionCallback.mOnPlayCalledCount);
+ assertTrue(sessionCallback.mOnPauseCalled);
assertFalse(sessionCallback.mOnSkipToNextCalled);
+
+ // If another media key is pressed while the double tap of PLAY_PAUSE,
+ // PLAY_PAUSE should be handles as normal.
+ sessionCallback.reset(3);
+ setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
+ sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
+ sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_STOP);
+ sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
+ assertTrue(sessionCallback.await(TIME_OUT_MS));
+ assertFalse(sessionCallback.mOnSkipToNextCalled);
+ assertTrue(sessionCallback.mOnStopCalled);
+ assertEquals(2, sessionCallback.mOnPlayCalledCount);
+
+ // Test if media keys are handled in order.
+ sessionCallback.reset(2);
+ setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
+ sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
+ sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_STOP);
+ assertTrue(sessionCallback.await(TIME_OUT_MS));
+ assertEquals(1, sessionCallback.mOnPlayCalledCount);
+ assertTrue(sessionCallback.mOnStopCalled);
+ synchronized (mWaitLock) {
+ assertEquals(PlaybackStateCompat.STATE_STOPPED,
+ mSession.getController().getPlaybackState().getState());
+ }
+ }
+
+ private void setPlaybackState(int state) {
+ final long allActions = PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE
+ | PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_STOP
+ | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+ | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
+ | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND;
+ PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder().setActions(allActions)
+ .setState(state, 0L, 0.0f).build();
+ synchronized (mWaitLock) {
+ mSession.setPlaybackState(playbackState);
+ }
}
@Test
@@ -995,18 +1018,21 @@
@Override
public void onPlay() {
mOnPlayCalledCount++;
+ setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
mLatch.countDown();
}
@Override
public void onPause() {
mOnPauseCalled = true;
+ setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
mLatch.countDown();
}
@Override
public void onStop() {
mOnStopCalled = true;
+ setPlaybackState(PlaybackStateCompat.STATE_STOPPED);
mLatch.countDown();
}
diff --git a/samples/Support13Demos/AndroidManifest.xml b/samples/Support13Demos/AndroidManifest.xml
index cdc246f..af7fad2 100644
--- a/samples/Support13Demos/AndroidManifest.xml
+++ b/samples/Support13Demos/AndroidManifest.xml
@@ -24,8 +24,6 @@
<uses-permission android:name="android.permission.READ_CONTACTS" />
- <uses-sdk android:minSdkVersion="14" />
-
<!-- The smallest screen this app works on is a phone. The app will
scale its UI to larger screens but doesn't make good use of them
so allow the compatibility mode button to be shown (mostly because
diff --git a/samples/Support13Demos/build.gradle b/samples/Support13Demos/build.gradle
index d1d4471..c63312f 100644
--- a/samples/Support13Demos/build.gradle
+++ b/samples/Support13Demos/build.gradle
@@ -9,6 +9,7 @@
defaultConfig {
minSdkVersion 14
+ targetSdkVersion project.ext.currentSdk
}
sourceSets {
diff --git a/samples/Support4Demos/AndroidManifest.xml b/samples/Support4Demos/AndroidManifest.xml
index 893259c..812d4e8 100644
--- a/samples/Support4Demos/AndroidManifest.xml
+++ b/samples/Support4Demos/AndroidManifest.xml
@@ -26,8 +26,6 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
- <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="22" />
-
<!-- The smallest screen this app works on is a phone. The app will
scale its UI to larger screens but doesn't make good use of them
so allow the compatibility mode button to be shown (mostly because
@@ -51,7 +49,7 @@
<activity android:name=".app.SendResult"
android:theme="@style/ThemeDialogWhenLarge">
</activity>
-
+
<!-- Fragment Support Samples -->
<activity android:name=".app.FragmentAlertDialogSupport"
@@ -144,7 +142,7 @@
<category android:name="com.example.android.supportv4.SUPPORT4_SAMPLE_CODE" />
</intent-filter>
</activity>
-
+
<activity android:name=".app.FragmentRetainInstanceSupport"
android:label="@string/fragment_retain_instance_support">
<intent-filter>
@@ -216,7 +214,7 @@
<category android:name="com.example.android.supportv4.SUPPORT4_SAMPLE_CODE" />
</intent-filter>
</activity>
-
+
<activity android:name=".app.LoaderCustomSupport"
android:label="@string/loader_custom_support">
<intent-filter>
diff --git a/samples/Support4Demos/build.gradle b/samples/Support4Demos/build.gradle
index 353b17f..836b212 100644
--- a/samples/Support4Demos/build.gradle
+++ b/samples/Support4Demos/build.gradle
@@ -9,6 +9,7 @@
defaultConfig {
minSdkVersion 14
+ targetSdkVersion project.ext.currentSdk
}
sourceSets {
diff --git a/samples/Support7Demos/AndroidManifest.xml b/samples/Support7Demos/AndroidManifest.xml
index 25b2427..1604267 100644
--- a/samples/Support7Demos/AndroidManifest.xml
+++ b/samples/Support7Demos/AndroidManifest.xml
@@ -34,8 +34,6 @@
<!-- Permission for ACCESS_COARSE_LOCATION is required for DayNight themes. -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
- <uses-sdk android:minSdkVersion="7" android:targetSdkVersion="21" />
-
<!-- The smallest screen this app works on is a phone. The app will
scale its UI to larger screens but doesn't make good use of them
so allow the compatibility mode button to be shown (mostly because
diff --git a/samples/Support7Demos/build.gradle b/samples/Support7Demos/build.gradle
index 0da74be..618cfab 100644
--- a/samples/Support7Demos/build.gradle
+++ b/samples/Support7Demos/build.gradle
@@ -14,6 +14,7 @@
defaultConfig {
minSdkVersion 14
+ targetSdkVersion project.ext.currentSdk
vectorDrawables.useSupportLibrary = true
}
diff --git a/samples/SupportAnimationDemos/AndroidManifest.xml b/samples/SupportAnimationDemos/AndroidManifest.xml
index 25e5ec8..243e6e4 100644
--- a/samples/SupportAnimationDemos/AndroidManifest.xml
+++ b/samples/SupportAnimationDemos/AndroidManifest.xml
@@ -16,9 +16,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.support.animation">
-
- <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="25" />
-
<application android:label="@string/activity_sample_code"
android:supportsRtl="true"
android:icon="@drawable/app_sample_code"
diff --git a/samples/SupportAnimationDemos/build.gradle b/samples/SupportAnimationDemos/build.gradle
index 6f3daf9..c619f6b 100644
--- a/samples/SupportAnimationDemos/build.gradle
+++ b/samples/SupportAnimationDemos/build.gradle
@@ -9,6 +9,7 @@
defaultConfig {
minSdkVersion 16
+ targetSdkVersion project.ext.currentSdk
}
sourceSets {
diff --git a/samples/SupportAppNavigation/AndroidManifest.xml b/samples/SupportAppNavigation/AndroidManifest.xml
index c8e4fa8..4681b33 100644
--- a/samples/SupportAppNavigation/AndroidManifest.xml
+++ b/samples/SupportAppNavigation/AndroidManifest.xml
@@ -18,9 +18,6 @@
android:versionName="1"
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.support.appnavigation">
-
- <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="15" />
-
<application android:label="@string/app_name">
<activity android:name=".app.AppNavHomeActivity"
android:label="@string/app_nav_home_label">
diff --git a/samples/SupportAppNavigation/build.gradle b/samples/SupportAppNavigation/build.gradle
index 456d677..bb48f2f 100644
--- a/samples/SupportAppNavigation/build.gradle
+++ b/samples/SupportAppNavigation/build.gradle
@@ -25,6 +25,7 @@
defaultConfig {
minSdkVersion 14
+ targetSdkVersion project.ext.currentSdk
}
sourceSets {
diff --git a/samples/SupportDesignDemos/AndroidManifest.xml b/samples/SupportDesignDemos/AndroidManifest.xml
index 88c423a..bedbea9 100644
--- a/samples/SupportDesignDemos/AndroidManifest.xml
+++ b/samples/SupportDesignDemos/AndroidManifest.xml
@@ -22,8 +22,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.support.design">
- <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="21" />
-
<application android:label="@string/activity_sample_code"
android:supportsRtl="true"
android:icon="@drawable/app_sample_code"
diff --git a/samples/SupportDesignDemos/build.gradle b/samples/SupportDesignDemos/build.gradle
index ac0b714..7987f52 100644
--- a/samples/SupportDesignDemos/build.gradle
+++ b/samples/SupportDesignDemos/build.gradle
@@ -9,6 +9,7 @@
defaultConfig {
minSdkVersion 14
+ targetSdkVersion project.ext.currentSdk
vectorDrawables.useSupportLibrary = true
}
diff --git a/samples/SupportDesignDemos/res/menu/sample_bottom_menu.xml b/samples/SupportDesignDemos/res/menu/sample_bottom_menu.xml
index d6d4761..f485789 100644
--- a/samples/SupportDesignDemos/res/menu/sample_bottom_menu.xml
+++ b/samples/SupportDesignDemos/res/menu/sample_bottom_menu.xml
@@ -13,14 +13,21 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_search"
android:title="@string/menu_search"
+ app:contentDescription="@string/menu_search"
+ app:tooltipText="@string/menu_search"
android:icon="@drawable/ic_search"/>
<item android:id="@+id/action_settings"
android:title="@string/menu_settings"
+ app:contentDescription="@string/menu_settings"
+ app:tooltipText="@string/menu_settings"
android:icon="@drawable/ic_add"/>
<item android:id="@+id/action_music"
android:title="@string/tab_text"
+ app:contentDescription="@string/tab_text"
+ app:tooltipText="@string/tab_text"
android:icon="@drawable/ic_action_navigation_menu"/>
-</menu>
\ No newline at end of file
+</menu>
diff --git a/samples/SupportEmojiDemos/build.gradle b/samples/SupportEmojiDemos/build.gradle
index 3c32d48..230e478 100644
--- a/samples/SupportEmojiDemos/build.gradle
+++ b/samples/SupportEmojiDemos/build.gradle
@@ -31,6 +31,7 @@
defaultConfig {
minSdkVersion 14
+ targetSdkVersion project.ext.currentSdk
}
sourceSets {
diff --git a/samples/SupportEmojiDemos/src/com/example/android/support/text/emoji/Config.java b/samples/SupportEmojiDemos/src/com/example/android/support/text/emoji/Config.java
index d70c43f..ea58121 100644
--- a/samples/SupportEmojiDemos/src/com/example/android/support/text/emoji/Config.java
+++ b/samples/SupportEmojiDemos/src/com/example/android/support/text/emoji/Config.java
@@ -107,9 +107,9 @@
config = new BundledEmojiCompatConfig(mContext);
}
} else {
- config = new EmojiCompat.Config(new EmojiCompat.MetadataLoader() {
+ config = new EmojiCompat.Config(new EmojiCompat.MetadataRepoLoader() {
@Override
- public void load(@NonNull EmojiCompat.LoaderCallback loaderCallback) {
+ public void load(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
loaderCallback.onFailed(new RuntimeException("Disable"));
}
}) {
diff --git a/samples/SupportLeanbackDemos/AndroidManifest.xml b/samples/SupportLeanbackDemos/AndroidManifest.xml
index 067bb36..93bddce 100644
--- a/samples/SupportLeanbackDemos/AndroidManifest.xml
+++ b/samples/SupportLeanbackDemos/AndroidManifest.xml
@@ -4,7 +4,6 @@
android:versionCode="1"
android:versionName="1.0">
- <uses-sdk android:minSdkVersion="17" android:targetSdkVersion="23" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
diff --git a/samples/SupportLeanbackDemos/build.gradle b/samples/SupportLeanbackDemos/build.gradle
index 122d718..ea6ed9e 100644
--- a/samples/SupportLeanbackDemos/build.gradle
+++ b/samples/SupportLeanbackDemos/build.gradle
@@ -10,6 +10,7 @@
defaultConfig {
minSdkVersion 17
+ targetSdkVersion project.ext.currentSdk
}
sourceSets {
diff --git a/samples/SupportLeanbackJank/AndroidManifest.xml b/samples/SupportLeanbackJank/AndroidManifest.xml
index 87be772..348f2d6 100644
--- a/samples/SupportLeanbackJank/AndroidManifest.xml
+++ b/samples/SupportLeanbackJank/AndroidManifest.xml
@@ -17,11 +17,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.leanbackjank">
-
- <uses-sdk
- android:minSdkVersion="21"
- android:targetSdkVersion="24"/>
-
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false"/>
diff --git a/samples/SupportLeanbackJank/build.gradle b/samples/SupportLeanbackJank/build.gradle
index 0b22bed..44346f7 100644
--- a/samples/SupportLeanbackJank/build.gradle
+++ b/samples/SupportLeanbackJank/build.gradle
@@ -11,6 +11,7 @@
defaultConfig {
minSdkVersion 17
+ targetSdkVersion project.ext.currentSdk
}
sourceSets {
diff --git a/samples/SupportPercentDemos/AndroidManifest.xml b/samples/SupportPercentDemos/AndroidManifest.xml
index 5c22277..c720dbd 100644
--- a/samples/SupportPercentDemos/AndroidManifest.xml
+++ b/samples/SupportPercentDemos/AndroidManifest.xml
@@ -21,9 +21,6 @@
to come from a domain that you own or have control over. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.support.percent">
-
- <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="21" />
-
<application android:label="@string/activity_sample_code"
android:supportsRtl="true"
android:icon="@drawable/app_sample_code">
diff --git a/samples/SupportPercentDemos/build.gradle b/samples/SupportPercentDemos/build.gradle
index 78eba62..7990a21 100644
--- a/samples/SupportPercentDemos/build.gradle
+++ b/samples/SupportPercentDemos/build.gradle
@@ -9,6 +9,7 @@
defaultConfig {
minSdkVersion 14
+ targetSdkVersion project.ext.currentSdk
}
sourceSets {
diff --git a/samples/SupportPreferenceDemos/AndroidManifest.xml b/samples/SupportPreferenceDemos/AndroidManifest.xml
index f6554b3..4fb2492 100644
--- a/samples/SupportPreferenceDemos/AndroidManifest.xml
+++ b/samples/SupportPreferenceDemos/AndroidManifest.xml
@@ -19,7 +19,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.example.android.supportpreference">
- <uses-sdk android:targetSdkVersion="24"
+ <uses-sdk
tools:overrideLibrary="android.support.v17.preference, android.support.v17.leanback" />
<uses-feature android:name="android.software.Leanback" android:required="false" />
diff --git a/samples/SupportPreferenceDemos/build.gradle b/samples/SupportPreferenceDemos/build.gradle
index 572ae73..5bee5e9 100644
--- a/samples/SupportPreferenceDemos/build.gradle
+++ b/samples/SupportPreferenceDemos/build.gradle
@@ -14,6 +14,7 @@
defaultConfig {
minSdkVersion 14
+ targetSdkVersion project.ext.currentSdk
}
sourceSets {
diff --git a/samples/SupportTransitionDemos/AndroidManifest.xml b/samples/SupportTransitionDemos/AndroidManifest.xml
index a6f52db..a58c189 100644
--- a/samples/SupportTransitionDemos/AndroidManifest.xml
+++ b/samples/SupportTransitionDemos/AndroidManifest.xml
@@ -21,9 +21,6 @@
to come from a domain that you own or have control over. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.support.transition">
-
- <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="23" />
-
<application android:label="@string/activity_sample_code"
android:supportsRtl="true"
android:icon="@drawable/app_sample_code"
diff --git a/samples/SupportTransitionDemos/build.gradle b/samples/SupportTransitionDemos/build.gradle
index c5033f7..7eda8b3 100644
--- a/samples/SupportTransitionDemos/build.gradle
+++ b/samples/SupportTransitionDemos/build.gradle
@@ -10,6 +10,7 @@
defaultConfig {
minSdkVersion 14
+ targetSdkVersion project.ext.currentSdk
}
sourceSets {
diff --git a/samples/SupportVectorDrawableDemos/AndroidManifest.xml b/samples/SupportVectorDrawableDemos/AndroidManifest.xml
index 1de3a5f..b9ccf77 100644
--- a/samples/SupportVectorDrawableDemos/AndroidManifest.xml
+++ b/samples/SupportVectorDrawableDemos/AndroidManifest.xml
@@ -16,9 +16,6 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.support.vectordrawable" >
-
- <uses-sdk android:minSdkVersion="11" android:targetSdkVersion="23"/>
-
<application android:icon="@drawable/app_sample_code" android:label="SupportVectorDrawableDemos" >
<activity android:name="com.example.android.support.vectordrawable.app.SupportVectorDrawableDemos">
<intent-filter>
diff --git a/samples/SupportVectorDrawableDemos/build.gradle b/samples/SupportVectorDrawableDemos/build.gradle
index b7c2167..8520eb1 100644
--- a/samples/SupportVectorDrawableDemos/build.gradle
+++ b/samples/SupportVectorDrawableDemos/build.gradle
@@ -27,6 +27,7 @@
defaultConfig {
minSdkVersion 14
+ targetSdkVersion project.ext.currentSdk
vectorDrawables.useSupportLibrary = true
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java b/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
index 3eea164..33ec805 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
@@ -35,6 +35,7 @@
import android.support.v17.leanback.widget.GuidedActionAdapter;
import android.support.v17.leanback.widget.GuidedActionAdapterGroup;
import android.support.v17.leanback.widget.GuidedActionsStylist;
+import android.support.v17.leanback.widget.NonOverlappingLinearLayout;
import android.support.v4.app.ActivityCompat;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
@@ -1045,6 +1046,7 @@
ViewGroup guidanceContainer = (ViewGroup) root.findViewById(R.id.content_fragment);
ViewGroup actionContainer = (ViewGroup) root.findViewById(R.id.action_fragment);
+ ((NonOverlappingLinearLayout) actionContainer).setFocusableViewAvailableFixEnabled(true);
Guidance guidance = onCreateGuidance(savedInstanceState);
View guidanceView = mGuidanceStylist.onCreateView(inflater, guidanceContainer, guidance);
diff --git a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java
index a1fd860..9bd9b08 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java
@@ -38,6 +38,7 @@
import android.support.v17.leanback.widget.GuidedActionAdapter;
import android.support.v17.leanback.widget.GuidedActionAdapterGroup;
import android.support.v17.leanback.widget.GuidedActionsStylist;
+import android.support.v17.leanback.widget.NonOverlappingLinearLayout;
import android.support.v4.app.ActivityCompat;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
@@ -1048,6 +1049,7 @@
ViewGroup guidanceContainer = (ViewGroup) root.findViewById(R.id.content_fragment);
ViewGroup actionContainer = (ViewGroup) root.findViewById(R.id.action_fragment);
+ ((NonOverlappingLinearLayout) actionContainer).setFocusableViewAvailableFixEnabled(true);
Guidance guidance = onCreateGuidance(savedInstanceState);
View guidanceView = mGuidanceStylist.onCreateView(inflater, guidanceContainer, guidance);
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
index d4e694a..748c993 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -3315,6 +3315,15 @@
return count == 0 || mBaseGridView.findViewHolderForAdapterPosition(0) != null;
}
+ boolean isItemFullyVisible(int pos) {
+ RecyclerView.ViewHolder vh = mBaseGridView.findViewHolderForAdapterPosition(pos);
+ if (vh == null) {
+ return false;
+ }
+ return vh.itemView.getLeft() >= 0 && vh.itemView.getRight() < mBaseGridView.getWidth()
+ && vh.itemView.getTop() >= 0 && vh.itemView.getBottom() < mBaseGridView.getHeight();
+ }
+
boolean canScrollTo(View view) {
return view.getVisibility() == View.VISIBLE && (!hasFocus() || view.hasFocusable());
}
@@ -3609,11 +3618,10 @@
saveContext(recycler, state);
switch (action) {
case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD:
- // try to focus all the way to the last visible item on the same row.
- processSelectionMoves(false, -mState.getItemCount());
+ processSelectionMoves(false, -1);
break;
case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD:
- processSelectionMoves(false, mState.getItemCount());
+ processSelectionMoves(false, 1);
break;
}
leaveContext();
@@ -3678,11 +3686,12 @@
public void onInitializeAccessibilityNodeInfo(Recycler recycler, State state,
AccessibilityNodeInfoCompat info) {
saveContext(recycler, state);
- if (mScrollEnabled && !hasCreatedFirstItem()) {
+ int count = state.getItemCount();
+ if (mScrollEnabled && count > 1 && !isItemFullyVisible(0)) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
info.setScrollable(true);
}
- if (mScrollEnabled && !hasCreatedLastItem()) {
+ if (mScrollEnabled && count > 1 && !isItemFullyVisible(count - 1)) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
info.setScrollable(true);
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayout.java b/v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayout.java
index 28a5c0c..8985f82 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayout.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayout.java
@@ -13,11 +13,26 @@
*/
package android.support.v17.leanback.widget;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
import android.content.Context;
+import android.support.annotation.RestrictTo;
import android.util.AttributeSet;
+import android.view.View;
import android.widget.LinearLayout;
-class NonOverlappingLinearLayout extends LinearLayout {
+import java.util.ArrayList;
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public class NonOverlappingLinearLayout extends LinearLayout {
+
+ boolean mFocusableViewAvailableFixEnabled = false;
+ boolean mDeferFocusableViewAvailableInLayout;
+ final ArrayList<ArrayList<View>> mSortedAvailableViews = new ArrayList();
+
public NonOverlappingLinearLayout(Context context) {
this(context, null);
@@ -38,4 +53,60 @@
public boolean hasOverlappingRendering() {
return false;
}
+
+ public void setFocusableViewAvailableFixEnabled(boolean enabled) {
+ mFocusableViewAvailableFixEnabled = enabled;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ try {
+ mDeferFocusableViewAvailableInLayout = mFocusableViewAvailableFixEnabled
+ && getOrientation() == HORIZONTAL
+ && getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+ if (mDeferFocusableViewAvailableInLayout) {
+ while (mSortedAvailableViews.size() > getChildCount()) {
+ mSortedAvailableViews.remove(mSortedAvailableViews.size() - 1);
+ }
+ while (mSortedAvailableViews.size() < getChildCount()) {
+ mSortedAvailableViews.add(new ArrayList());
+ }
+ }
+ super.onLayout(changed, l, t, r, b);
+ if (mDeferFocusableViewAvailableInLayout) {
+ for (int i = 0; i < mSortedAvailableViews.size(); i++) {
+ for (int j = 0; j < mSortedAvailableViews.get(i).size(); j++) {
+ super.focusableViewAvailable(mSortedAvailableViews.get(i).get(j));
+ }
+ }
+ }
+ } finally {
+ if (mDeferFocusableViewAvailableInLayout) {
+ mDeferFocusableViewAvailableInLayout = false;
+ for (int i = 0; i < mSortedAvailableViews.size(); i++) {
+ mSortedAvailableViews.get(i).clear();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void focusableViewAvailable(View v) {
+ if (mDeferFocusableViewAvailableInLayout) {
+ View i = v;
+ int index = -1;
+ while (i != this && i != null) {
+ if (i.getParent() == this) {
+ index = indexOfChild(i);
+ break;
+ }
+ i = (View) i.getParent();
+ }
+ if (index != -1) {
+ mSortedAvailableViews.get(index).add(v);
+ }
+ } else {
+ super.focusableViewAvailable(v);
+ }
+ }
}
\ No newline at end of file
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/WindowAlignment.java b/v17/leanback/src/android/support/v17/leanback/widget/WindowAlignment.java
index a651b2e..3ddb6f0 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/WindowAlignment.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/WindowAlignment.java
@@ -254,27 +254,49 @@
}
}
if (!isMaxUnknown && !isMinUnknown) {
- if (!mReversedFlow ? (mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0
- : (mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0) {
- if (!mReversedFlow ? isPreferKeylineOverLowEdge()
- : isPreferKeylineOverHighEdge()) {
- // if we prefer key line, might align max child to key line for minScroll
- mMinScroll = Math.min(mMinScroll,
- calculateScrollToKeyLine(maxChildViewCenter, keyLine));
- } else {
- // don't over scroll max
- mMaxScroll = Math.max(mMinScroll, mMaxScroll);
+ if (!mReversedFlow) {
+ if ((mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0) {
+ if (isPreferKeylineOverLowEdge()) {
+ // if we prefer key line, might align max child to key line for
+ // minScroll
+ mMinScroll = Math.min(mMinScroll,
+ calculateScrollToKeyLine(maxChildViewCenter, keyLine));
+ } else {
+ // don't over scroll max
+ mMaxScroll = Math.max(mMinScroll, mMaxScroll);
+ }
+ } else if ((mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0) {
+ if (isPreferKeylineOverHighEdge()) {
+ // if we prefer key line, might align min child to key line for
+ // maxScroll
+ mMaxScroll = Math.max(mMaxScroll,
+ calculateScrollToKeyLine(minChildViewCenter, keyLine));
+ } else {
+ // don't over scroll min
+ mMinScroll = Math.min(mMinScroll, mMaxScroll);
+ }
}
- } else if (!mReversedFlow ? (mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0
- : (mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0) {
- if (!mReversedFlow ? isPreferKeylineOverHighEdge()
- : isPreferKeylineOverLowEdge()) {
- // if we prefer key line, might align min child to key line for maxScroll
- mMaxScroll = Math.max(mMaxScroll,
- calculateScrollToKeyLine(minChildViewCenter, keyLine));
- } else {
- // don't over scroll min
- mMinScroll = Math.min(mMinScroll, mMaxScroll);
+ } else {
+ if ((mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0) {
+ if (isPreferKeylineOverLowEdge()) {
+ // if we prefer key line, might align min child to key line for
+ // maxScroll
+ mMaxScroll = Math.max(mMaxScroll,
+ calculateScrollToKeyLine(minChildViewCenter, keyLine));
+ } else {
+ // don't over scroll min
+ mMinScroll = Math.min(mMinScroll, mMaxScroll);
+ }
+ } else if ((mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0) {
+ if (isPreferKeylineOverHighEdge()) {
+ // if we prefer key line, might align max child to key line for
+ // minScroll
+ mMinScroll = Math.min(mMinScroll,
+ calculateScrollToKeyLine(maxChildViewCenter, keyLine));
+ } else {
+ // don't over scroll max
+ mMaxScroll = Math.max(mMinScroll, mMaxScroll);
+ }
}
}
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java
index bd245aa..fa324bf 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java
@@ -420,4 +420,32 @@
}
+ @Test
+ public void buttonActionsRtl() throws Throwable {
+ final String firstFragmentName = generateMethodTestName("first");
+ GuidedStepTestFragment.Provider first = mockProvider(firstFragmentName);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ actions.add(new GuidedAction.Builder().id(1000).title("action").build());
+ return null;
+ }
+ }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ actions.add(new GuidedAction.Builder().id(1001).title("button action").build());
+ return null;
+ }
+ }).when(first).onCreateButtonActions(any(List.class), nullable(Bundle.class));
+
+ final GuidedStepFragmentTestActivity activity = launchTestActivity(firstFragmentName,
+ true, View.LAYOUT_DIRECTION_RTL);
+
+ assertEquals(View.LAYOUT_DIRECTION_RTL, first.getFragment().getView().getLayoutDirection());
+ View firstView = first.getFragment().getActionItemView(0);
+ assertTrue(firstView.hasFocus());
+ }
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java
index 06beab6..4dcf188 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java
@@ -32,12 +32,21 @@
*/
public static final String EXTRA_ADD_AS_ROOT = "addAsRoot";
+ /**
+ * Layout direction
+ */
+ public static final String EXTRA_LAYOUT_DIRECTION = "layoutDir";
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
+ int layoutDirection = intent.getIntExtra(EXTRA_LAYOUT_DIRECTION, -1);
+ if (layoutDirection != -1) {
+ findViewById(android.R.id.content).setLayoutDirection(layoutDirection);
+ }
if (savedInstanceState == null) {
String firstTestName = intent.getStringExtra(EXTRA_TEST_NAME);
if (firstTestName != null) {
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java
index 6ed254a..7059c9a 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java
@@ -125,6 +125,15 @@
return activityTestRule.launchActivity(intent);
}
+ public GuidedStepFragmentTestActivity launchTestActivity(String firstTestName,
+ boolean addAsRoot, int layoutDirection) {
+ Intent intent = new Intent();
+ intent.putExtra(GuidedStepFragmentTestActivity.EXTRA_TEST_NAME, firstTestName);
+ intent.putExtra(GuidedStepFragmentTestActivity.EXTRA_ADD_AS_ROOT, addAsRoot);
+ intent.putExtra(GuidedStepFragmentTestActivity.EXTRA_LAYOUT_DIRECTION, layoutDirection);
+ return activityTestRule.launchActivity(intent);
+ }
+
public GuidedStepTestFragment.Provider mockProvider(String testName) {
GuidedStepTestFragment.Provider test = mock(GuidedStepTestFragment.Provider.class);
when(test.getActivity()).thenCallRealMethod();
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java
index 5741181..b4d9b59 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java
@@ -105,7 +105,7 @@
verify(second, times(1)).onCreateGuidance(nullable(Bundle.class));
verify(second, times(1)).onCreateActions(any(List.class), nullable(Bundle.class));
verify(second, times(1)).onCreateButtonActions(any(List.class), nullable(Bundle.class));
- verify(second, times(1)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class),
+ verify(second, times(1)).onCreateView(any(LayoutInflater.class), nullable(ViewGroup.class),
nullable(Bundle.class), any(View.class));
verify(second, times(1)).onViewStateRestored(nullable(Bundle.class));
verify(second, times(1)).onStart();
@@ -423,4 +423,32 @@
}
+ @Test
+ public void buttonActionsRtl() throws Throwable {
+ final String firstFragmentName = generateMethodTestName("first");
+ GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ actions.add(new GuidedAction.Builder().id(1000).title("action").build());
+ return null;
+ }
+ }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ List actions = (List) invocation.getArguments()[0];
+ actions.add(new GuidedAction.Builder().id(1001).title("button action").build());
+ return null;
+ }
+ }).when(first).onCreateButtonActions(any(List.class), nullable(Bundle.class));
+
+ final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName,
+ true, View.LAYOUT_DIRECTION_RTL);
+
+ assertEquals(View.LAYOUT_DIRECTION_RTL, first.getFragment().getView().getLayoutDirection());
+ View firstView = first.getFragment().getActionItemView(0);
+ assertTrue(firstView.hasFocus());
+ }
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java
index 2fc8d1e..fb877ed 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java
@@ -35,12 +35,21 @@
*/
public static final String EXTRA_ADD_AS_ROOT = "addAsRoot";
+ /**
+ * Layout direction
+ */
+ public static final String EXTRA_LAYOUT_DIRECTION = "layoutDir";
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
+ int layoutDirection = intent.getIntExtra(EXTRA_LAYOUT_DIRECTION, -1);
+ if (layoutDirection != -1) {
+ findViewById(android.R.id.content).setLayoutDirection(layoutDirection);
+ }
if (savedInstanceState == null) {
String firstTestName = intent.getStringExtra(EXTRA_TEST_NAME);
if (firstTestName != null) {
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java
index 4fe4a24..17533fa 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java
@@ -128,6 +128,15 @@
return activityTestRule.launchActivity(intent);
}
+ public GuidedStepSupportFragmentTestActivity launchTestActivity(String firstTestName,
+ boolean addAsRoot, int layoutDirection) {
+ Intent intent = new Intent();
+ intent.putExtra(GuidedStepSupportFragmentTestActivity.EXTRA_TEST_NAME, firstTestName);
+ intent.putExtra(GuidedStepSupportFragmentTestActivity.EXTRA_ADD_AS_ROOT, addAsRoot);
+ intent.putExtra(GuidedStepSupportFragmentTestActivity.EXTRA_LAYOUT_DIRECTION, layoutDirection);
+ return activityTestRule.launchActivity(intent);
+ }
+
public GuidedStepTestSupportFragment.Provider mockProvider(String testName) {
GuidedStepTestSupportFragment.Provider test = mock(GuidedStepTestSupportFragment.Provider.class);
when(test.getActivity()).thenCallRealMethod();
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
index 1a173b4..86fb4eb 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
@@ -1419,6 +1419,27 @@
}
@Test
+ public void testItemMovedHorizontalRtl() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear_rtl);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ intent.putExtra(GridActivity.EXTRA_ITEMS, new int[] {40, 40, 40});
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.moveItem(0, 1, true);
+ }
+ });
+ assertEquals(mGridView.getWidth() - mGridView.getPaddingRight(),
+ mGridView.findViewHolderForAdapterPosition(0).itemView.getRight());
+ }
+
+ @Test
public void testScrollSecondaryCannotScroll() throws Throwable {
Intent intent = new Intent();
intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
@@ -3888,6 +3909,108 @@
assertTrue(selectedPosition2 < selectedPosition1);
}
+ @Test
+ public void testAccessibilityScrollForwardHalfVisible() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.item_button_at_bottom);
+ intent.putExtra(GridActivity.EXTRA_ITEMS, new int[]{});
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ int height = mGridView.getHeight() - mGridView.getPaddingTop()
+ - mGridView.getPaddingBottom();
+ final int childHeight = height - mGridView.getVerticalSpacing() - 100;
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setWindowAlignment(BaseGridView.WINDOW_ALIGN_NO_EDGE);
+ mGridView.setWindowAlignmentOffset(100);
+ mGridView.setWindowAlignmentOffsetPercent(BaseGridView
+ .WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
+ mGridView.setItemAlignmentOffset(0);
+ mGridView.setItemAlignmentOffsetPercent(BaseGridView
+ .ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
+ }
+ });
+ mActivity.addItems(0, new int[]{childHeight, childHeight});
+ waitForItemAnimation();
+ setSelectedPosition(0);
+
+ final RecyclerViewAccessibilityDelegate delegateCompat = mGridView
+ .getCompatAccessibilityDelegate();
+ final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ delegateCompat.onInitializeAccessibilityNodeInfo(mGridView, info);
+ }
+ });
+ assertTrue("test sanity", info.isScrollable());
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ delegateCompat.performAccessibilityAction(mGridView,
+ AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD, null);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ assertEquals(1, mGridView.getSelectedPosition());
+ }
+
+ @Test
+ public void testAccessibilityScrollBackwardHalfVisible() throws Throwable {
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.item_button_at_top);
+ intent.putExtra(GridActivity.EXTRA_ITEMS, new int[]{});
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ initActivity(intent);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 1;
+
+ int height = mGridView.getHeight() - mGridView.getPaddingTop()
+ - mGridView.getPaddingBottom();
+ final int childHeight = height - mGridView.getVerticalSpacing() - 100;
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setWindowAlignment(BaseGridView.WINDOW_ALIGN_NO_EDGE);
+ mGridView.setWindowAlignmentOffset(100);
+ mGridView.setWindowAlignmentOffsetPercent(BaseGridView
+ .WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
+ mGridView.setItemAlignmentOffset(0);
+ mGridView.setItemAlignmentOffsetPercent(BaseGridView
+ .ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
+ }
+ });
+ mActivity.addItems(0, new int[]{childHeight, childHeight});
+ waitForItemAnimation();
+ setSelectedPosition(1);
+
+ final RecyclerViewAccessibilityDelegate delegateCompat = mGridView
+ .getCompatAccessibilityDelegate();
+ final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ delegateCompat.onInitializeAccessibilityNodeInfo(mGridView, info);
+ }
+ });
+ assertTrue("test sanity", info.isScrollable());
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ delegateCompat.performAccessibilityAction(mGridView,
+ AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD, null);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ assertEquals(0, mGridView.getSelectedPosition());
+ }
+
void slideInAndWaitIdle() throws Throwable {
slideInAndWaitIdle(5000);
}
@@ -4884,14 +5007,14 @@
void prepareKeyLineTest(int numItems) throws Throwable {
Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_linear);
int[] items = new int[numItems];
for (int i = 0; i < items.length; i++) {
items[i] = 32;
}
intent.putExtra(GridActivity.EXTRA_ITEMS, items);
intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mOrientation = BaseGridView.VERTICAL;
+ mOrientation = BaseGridView.HORIZONTAL;
mNumRows = 1;
initActivity(intent);
@@ -4932,51 +5055,76 @@
final boolean preferKeyLineOverHigh,
ItemAt assertFirstItemLocation,
ItemAt assertLastItemLocation) throws Throwable {
+ TestPreferKeyLineOptions options = new TestPreferKeyLineOptions();
+ options.mAssertItemLocations = new ItemAt[] {assertFirstItemLocation,
+ assertLastItemLocation};
+ options.mPreferKeyLineOverLow = preferKeyLineOverLow;
+ options.mPreferKeyLineOverHigh = preferKeyLineOverHigh;
+ options.mWindowAlignment = windowAlignment;
+
+ options.mRtl = false;
+ testPreferKeyLine(options);
+
+ options.mRtl = true;
+ testPreferKeyLine(options);
+ }
+
+ static class TestPreferKeyLineOptions {
+ int mWindowAlignment;
+ boolean mPreferKeyLineOverLow;
+ boolean mPreferKeyLineOverHigh;
+ ItemAt[] mAssertItemLocations;
+ boolean mRtl;
+ }
+
+ public void testPreferKeyLine(final TestPreferKeyLineOptions options) throws Throwable {
startWaitLayout();
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
- mGridView.setWindowAlignment(windowAlignment);
+ if (options.mRtl) {
+ mGridView.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
+ } else {
+ mGridView.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
+ }
+ mGridView.setWindowAlignment(options.mWindowAlignment);
mGridView.setWindowAlignmentOffsetPercent(50);
mGridView.setWindowAlignmentOffset(0);
- mGridView.setWindowAlignmentPreferKeyLineOverLowEdge(preferKeyLineOverLow);
- mGridView.setWindowAlignmentPreferKeyLineOverHighEdge(preferKeyLineOverHigh);
+ mGridView.setWindowAlignmentPreferKeyLineOverLowEdge(options.mPreferKeyLineOverLow);
+ mGridView.setWindowAlignmentPreferKeyLineOverHighEdge(
+ options.mPreferKeyLineOverHigh);
}
});
waitForLayout();
- final int lowPadding = mGridView.getPaddingTop();
- final int highPadding = mGridView.getHeight() - mGridView.getPaddingBottom();
- final int windowAlignCenter = mGridView.getHeight() / 2;
+ final int paddingStart = mGridView.getPaddingStart();
+ final int paddingEnd = mGridView.getPaddingEnd();
+ final int windowAlignCenter = mGridView.getWidth() / 2;
- setSelectedPosition(assertFirstItemLocation.mScrollPosition);
- View view = mGridView.findViewHolderForAdapterPosition(assertFirstItemLocation.mPosition)
- .itemView;
- switch (assertFirstItemLocation.mLocation) {
- case ITEM_AT_LOW:
- assertEquals(lowPadding, view.getTop());
- break;
- case ITEM_AT_HIGH:
- assertEquals(highPadding, view.getBottom());
- break;
- case ITEM_AT_KEY_LINE:
- assertEquals(windowAlignCenter, view.getTop() + view.getHeight() / 2, DELTA);
- break;
- }
-
- setSelectedPosition(assertLastItemLocation.mScrollPosition);
- view = mGridView.findViewHolderForAdapterPosition(assertLastItemLocation.mPosition)
- .itemView;
- switch (assertLastItemLocation.mLocation) {
- case ITEM_AT_LOW:
- assertEquals(lowPadding, view.getTop());
- break;
- case ITEM_AT_HIGH:
- assertEquals(highPadding, view.getBottom());
- break;
- case ITEM_AT_KEY_LINE:
- assertEquals(windowAlignCenter, view.getTop() + view.getHeight() / 2, DELTA);
- break;
+ for (int i = 0; i < options.mAssertItemLocations.length; i++) {
+ ItemAt assertItemLocation = options.mAssertItemLocations[i];
+ setSelectedPosition(assertItemLocation.mScrollPosition);
+ View view = mGridView.findViewHolderForAdapterPosition(assertItemLocation.mPosition)
+ .itemView;
+ switch (assertItemLocation.mLocation) {
+ case ITEM_AT_LOW:
+ if (options.mRtl) {
+ assertEquals(mGridView.getWidth() - paddingStart, view.getRight());
+ } else {
+ assertEquals(paddingStart, view.getLeft());
+ }
+ break;
+ case ITEM_AT_HIGH:
+ if (options.mRtl) {
+ assertEquals(paddingEnd, view.getLeft());
+ } else {
+ assertEquals(mGridView.getWidth() - paddingEnd, view.getRight());
+ }
+ break;
+ case ITEM_AT_KEY_LINE:
+ assertEquals(windowAlignCenter, (view.getLeft() + view.getRight()) / 2, DELTA);
+ break;
+ }
}
}
diff --git a/v17/leanback/tests/res/layout/item_button_at_bottom.xml b/v17/leanback/tests/res/layout/item_button_at_bottom.xml
new file mode 100644
index 0000000..8afc622
--- /dev/null
+++ b/v17/leanback/tests/res/layout/item_button_at_bottom.xml
@@ -0,0 +1,32 @@
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="400dp"
+ android:layout_height="400dp"
+ >
+ <TextView
+ android:layout_alignParentTop="true"
+ android:text="unfocusable text"
+ android:layout_width="match_parent"
+ android:layout_height="50dp"/>
+ <Button
+ android:layout_alignParentBottom="true"
+ android:text="button"
+ android:focusable="true"
+ android:layout_width="match_parent"
+ android:layout_height="50dp"/>
+</RelativeLayout>
diff --git a/v17/leanback/tests/res/layout/item_button_at_top.xml b/v17/leanback/tests/res/layout/item_button_at_top.xml
new file mode 100644
index 0000000..5199193
--- /dev/null
+++ b/v17/leanback/tests/res/layout/item_button_at_top.xml
@@ -0,0 +1,33 @@
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="400dp"
+ android:layout_height="400dp"
+ >
+ <TextView
+ android:layout_alignParentBottom="true"
+ android:text="unfocusable text"
+ android:layout_width="match_parent"
+ android:layout_height="50dp"/>
+ <Button
+ android:layout_alignParentTop="true"
+ android:text="button"
+ android:focusable="true"
+ android:layout_width="match_parent"
+ android:layout_height="50dp"/>
+</RelativeLayout>
diff --git a/v7/appcompat/src/android/support/v7/app/NotificationCompat.java b/v7/appcompat/src/android/support/v7/app/NotificationCompat.java
index 16840d2..450248c 100644
--- a/v7/appcompat/src/android/support/v7/app/NotificationCompat.java
+++ b/v7/appcompat/src/android/support/v7/app/NotificationCompat.java
@@ -21,8 +21,7 @@
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.Color;
+import android.media.session.MediaSession;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
@@ -32,22 +31,12 @@
import android.support.v4.app.BundleCompat;
import android.support.v4.app.NotificationBuilderWithBuilderAccessor;
import android.support.v4.media.session.MediaSessionCompat;
-import android.support.v4.text.BidiFormatter;
import android.support.v7.appcompat.R;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-import android.text.TextUtils;
-import android.text.style.TextAppearanceSpan;
+import android.view.View;
import android.widget.RemoteViews;
-import java.util.List;
-
/**
- * An extension of {@link android.support.v4.app.NotificationCompat} which supports
- * {@link android.support.v7.app.NotificationCompat.MediaStyle},
- * {@link android.support.v7.app.NotificationCompat.DecoratedCustomViewStyle},
- * and {@link android.support.v7.app.NotificationCompat.DecoratedMediaCustomViewStyle}.
- * You should start using this variant if you need support any of these styles.
+ * An extension of {@link android.support.v4.app.NotificationCompat} which adds additional styles.
*/
public class NotificationCompat extends android.support.v4.app.NotificationCompat {
@@ -83,299 +72,8 @@
return null;
}
- @RequiresApi(24)
- private static void addStyleToBuilderApi24(NotificationBuilderWithBuilderAccessor builder,
- android.support.v4.app.NotificationCompat.Builder b) {
- if (b.mStyle instanceof DecoratedCustomViewStyle) {
- NotificationCompatImpl24.addDecoratedCustomViewStyle(builder);
- } else if (b.mStyle instanceof DecoratedMediaCustomViewStyle) {
- DecoratedMediaCustomViewStyle mediaStyle = (DecoratedMediaCustomViewStyle) b.mStyle;
- NotificationCompatImpl24.addDecoratedMediaCustomViewStyle(builder,
- mediaStyle.mActionsToShowInCompact,
- mediaStyle.mToken != null ? mediaStyle.mToken.getToken() : null);
- } else if (!(b.mStyle instanceof MessagingStyle)) {
- addStyleGetContentViewLollipop(builder, b);
- }
- }
-
- @RequiresApi(21)
- private static RemoteViews addStyleGetContentViewLollipop(
- NotificationBuilderWithBuilderAccessor builder,
- android.support.v4.app.NotificationCompat.Builder b) {
- if (b.mStyle instanceof MediaStyle) {
- MediaStyle mediaStyle = (MediaStyle) b.mStyle;
- NotificationCompatImpl21.addMediaStyle(builder,
- mediaStyle.mActionsToShowInCompact,
- mediaStyle.mToken != null ? mediaStyle.mToken.getToken() : null);
-
- boolean hasContentView = b.getContentView() != null;
- // If we are on L/M the media notification will only be colored if the expanded version
- // is of media style, so we have to create a custom view for the collapsed version as
- // well in that case.
- boolean isMorL = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
- && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M;
- boolean createCustomContent = hasContentView
- || (isMorL && b.getBigContentView() != null);
- if (b.mStyle instanceof DecoratedMediaCustomViewStyle && createCustomContent) {
- RemoteViews contentViewMedia = NotificationCompatImplBase.overrideContentViewMedia(
- builder, b.mContext, b.mContentTitle, b.mContentText, b.mContentInfo,
- b.mNumber, b.mLargeIcon, b.mSubText, b.mUseChronometer,
- b.getWhenIfShowing(), b.getPriority(), b.mActions,
- mediaStyle.mActionsToShowInCompact, false /* no cancel button on L */,
- null /* cancelButtonIntent */, hasContentView /* isDecoratedCustomView */);
- if (hasContentView) {
- NotificationCompatImplBase.buildIntoRemoteViews(b.mContext, contentViewMedia,
- b.getContentView());
- }
- setBackgroundColor(b.mContext, contentViewMedia, b.getColor());
- return contentViewMedia;
- }
- return null;
- } else if (b.mStyle instanceof DecoratedCustomViewStyle) {
- return getDecoratedContentView(b);
- }
- return addStyleGetContentViewJellybean(builder, b);
- }
-
- @RequiresApi(16)
- private static RemoteViews addStyleGetContentViewJellybean(
- NotificationBuilderWithBuilderAccessor builder,
- android.support.v4.app.NotificationCompat.Builder b) {
- if (b.mStyle instanceof MessagingStyle) {
- addMessagingFallBackStyle((MessagingStyle) b.mStyle, builder, b);
- }
- return addStyleGetContentViewIcs(builder, b);
- }
-
- private static MessagingStyle.Message findLatestIncomingMessage(MessagingStyle style) {
- List<MessagingStyle.Message> messages = style.getMessages();
- for (int i = messages.size() - 1; i >= 0; i--) {
- MessagingStyle.Message m = messages.get(i);
- // Incoming messages have a non-empty sender.
- if (!TextUtils.isEmpty(m.getSender())) {
- return m;
- }
- }
- if (!messages.isEmpty()) {
- // No incoming messages, fall back to outgoing message
- return messages.get(messages.size() - 1);
- }
- return null;
- }
-
- private static CharSequence makeMessageLine(android.support.v4.app.NotificationCompat.Builder b,
- MessagingStyle style,
- MessagingStyle.Message m) {
- BidiFormatter bidi = BidiFormatter.getInstance();
- SpannableStringBuilder sb = new SpannableStringBuilder();
- boolean afterLollipop = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
- int color = afterLollipop || Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1
- ? Color.BLACK : Color.WHITE;
- CharSequence replyName = m.getSender();
- if (TextUtils.isEmpty(m.getSender())) {
- replyName = style.getUserDisplayName() == null
- ? "" : style.getUserDisplayName();
- color = afterLollipop && b.getColor() != NotificationCompat.COLOR_DEFAULT
- ? b.getColor()
- : color;
- }
- CharSequence senderText = bidi.unicodeWrap(replyName);
- sb.append(senderText);
- sb.setSpan(makeFontColorSpan(color),
- sb.length() - senderText.length(),
- sb.length(),
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE /* flags */);
- CharSequence text = m.getText() == null ? "" : m.getText();
- sb.append(" ").append(bidi.unicodeWrap(text));
- return sb;
- }
-
- private static TextAppearanceSpan makeFontColorSpan(int color) {
- return new TextAppearanceSpan(null, 0, 0, ColorStateList.valueOf(color), null);
- }
-
- @RequiresApi(16)
- private static void addMessagingFallBackStyle(MessagingStyle style,
- NotificationBuilderWithBuilderAccessor builder,
- android.support.v4.app.NotificationCompat.Builder b) {
- SpannableStringBuilder completeMessage = new SpannableStringBuilder();
- List<MessagingStyle.Message> messages = style.getMessages();
- boolean showNames = style.getConversationTitle() != null
- || hasMessagesWithoutSender(style.getMessages());
- for (int i = messages.size() - 1; i >= 0; i--) {
- MessagingStyle.Message m = messages.get(i);
- CharSequence line;
- line = showNames ? makeMessageLine(b, style, m) : m.getText();
- if (i != messages.size() - 1) {
- completeMessage.insert(0, "\n");
- }
- completeMessage.insert(0, line);
- }
- NotificationCompatImplJellybean.addBigTextStyle(builder, completeMessage);
- }
-
- private static boolean hasMessagesWithoutSender(
- List<MessagingStyle.Message> messages) {
- for (int i = messages.size() - 1; i >= 0; i--) {
- MessagingStyle.Message m = messages.get(i);
- if (m.getSender() == null) {
- return true;
- }
- }
- return false;
- }
-
- @RequiresApi(14)
- private static RemoteViews addStyleGetContentViewIcs(
- NotificationBuilderWithBuilderAccessor builder,
- android.support.v4.app.NotificationCompat.Builder b) {
- if (b.mStyle instanceof MediaStyle) {
- MediaStyle mediaStyle = (MediaStyle) b.mStyle;
- boolean isDecorated = b.mStyle instanceof DecoratedMediaCustomViewStyle
- && b.getContentView() != null;
- RemoteViews contentViewMedia = NotificationCompatImplBase.overrideContentViewMedia(
- builder, b.mContext, b.mContentTitle, b.mContentText, b.mContentInfo, b.mNumber,
- b.mLargeIcon, b.mSubText, b.mUseChronometer, b.getWhenIfShowing(),
- b.getPriority(), b.mActions, mediaStyle.mActionsToShowInCompact,
- mediaStyle.mShowCancelButton, mediaStyle.mCancelButtonIntent, isDecorated);
- if (isDecorated) {
- NotificationCompatImplBase.buildIntoRemoteViews(b.mContext, contentViewMedia,
- b.getContentView());
- return contentViewMedia;
- }
- } else if (b.mStyle instanceof DecoratedCustomViewStyle) {
- return getDecoratedContentView(b);
- }
- return null;
- }
-
- @RequiresApi(16)
- private static void addBigStyleToBuilderJellybean(Notification n,
- android.support.v4.app.NotificationCompat.Builder b) {
- if (b.mStyle instanceof MediaStyle) {
- MediaStyle mediaStyle = (MediaStyle) b.mStyle;
- RemoteViews innerView = b.getBigContentView() != null
- ? b.getBigContentView()
- : b.getContentView();
- boolean isDecorated = b.mStyle instanceof DecoratedMediaCustomViewStyle
- && innerView != null;
- NotificationCompatImplBase.overrideMediaBigContentView(n, b.mContext,
- b.mContentTitle, b.mContentText, b.mContentInfo, b.mNumber, b.mLargeIcon,
- b.mSubText, b.mUseChronometer, b.getWhenIfShowing(), b.getPriority(), 0,
- b.mActions, mediaStyle.mShowCancelButton, mediaStyle.mCancelButtonIntent,
- isDecorated);
- if (isDecorated) {
- NotificationCompatImplBase.buildIntoRemoteViews(b.mContext, n.bigContentView,
- innerView);
- }
- } else if (b.mStyle instanceof DecoratedCustomViewStyle) {
- addDecoratedBigStyleToBuilderJellybean(n, b);
- }
- }
-
- private static RemoteViews getDecoratedContentView(
- android.support.v4.app.NotificationCompat.Builder b) {
- if (b.getContentView() == null) {
- // No special content view
- return null;
- }
- RemoteViews remoteViews = NotificationCompatImplBase.applyStandardTemplateWithActions(
- b.mContext, b.mContentTitle, b.mContentText, b.mContentInfo, b.mNumber,
- b.mNotification.icon, b.mLargeIcon, b.mSubText, b.mUseChronometer,
- b.getWhenIfShowing(), b.getPriority(), b.getColor(),
- R.layout.notification_template_custom_big, false /* fitIn1U */, null /* actions */);
- NotificationCompatImplBase.buildIntoRemoteViews(b.mContext, remoteViews,
- b.getContentView());
- return remoteViews;
- }
-
- @RequiresApi(16)
- private static void addDecoratedBigStyleToBuilderJellybean(Notification n,
- android.support.v4.app.NotificationCompat.Builder b) {
- RemoteViews bigContentView = b.getBigContentView();
- RemoteViews innerView = bigContentView != null ? bigContentView : b.getContentView();
- if (innerView == null) {
- // No expandable notification
- return;
- }
- RemoteViews remoteViews = NotificationCompatImplBase.applyStandardTemplateWithActions(
- b.mContext, b.mContentTitle, b.mContentText, b.mContentInfo, b.mNumber,
- n.icon ,b.mLargeIcon, b.mSubText, b.mUseChronometer, b.getWhenIfShowing(),
- b.getPriority(), b.getColor(), R.layout.notification_template_custom_big,
- false /* fitIn1U */, b.mActions);
- NotificationCompatImplBase.buildIntoRemoteViews(b.mContext, remoteViews, innerView);
- n.bigContentView = remoteViews;
- }
-
- @RequiresApi(21)
- private static void addDecoratedHeadsUpToBuilderLollipop(Notification n,
- android.support.v4.app.NotificationCompat.Builder b) {
- RemoteViews headsUp = b.getHeadsUpContentView();
- RemoteViews innerView = headsUp != null ? headsUp : b.getContentView();
- if (headsUp == null) {
- // No expandable notification
- return;
- }
- RemoteViews remoteViews = NotificationCompatImplBase.applyStandardTemplateWithActions(
- b.mContext, b.mContentTitle, b.mContentText, b.mContentInfo, b.mNumber, n.icon,
- b.mLargeIcon, b.mSubText, b.mUseChronometer, b.getWhenIfShowing(), b.getPriority(),
- b.getColor(), R.layout.notification_template_custom_big, false /* fitIn1U */,
- b.mActions);
- NotificationCompatImplBase.buildIntoRemoteViews(b.mContext, remoteViews, innerView);
- n.headsUpContentView = remoteViews;
- }
-
- @RequiresApi(21)
- private static void addBigStyleToBuilderLollipop(Notification n,
- android.support.v4.app.NotificationCompat.Builder b) {
- RemoteViews innerView = b.getBigContentView() != null
- ? b.getBigContentView()
- : b.getContentView();
- if (b.mStyle instanceof DecoratedMediaCustomViewStyle && innerView != null) {
- NotificationCompatImplBase.overrideMediaBigContentView(n, b.mContext,
- b.mContentTitle, b.mContentText, b.mContentInfo, b.mNumber, b.mLargeIcon,
- b.mSubText, b.mUseChronometer, b.getWhenIfShowing(), b.getPriority(), 0,
- b.mActions, false /* showCancelButton */, null /* cancelButtonIntent */,
- true /* decoratedCustomView */);
- NotificationCompatImplBase.buildIntoRemoteViews(b.mContext, n.bigContentView,
- innerView);
- setBackgroundColor(b.mContext, n.bigContentView, b.getColor());
- } else if (b.mStyle instanceof DecoratedCustomViewStyle) {
- addDecoratedBigStyleToBuilderJellybean(n, b);
- }
- }
-
- private static void setBackgroundColor(Context context, RemoteViews views, int color) {
- if (color == COLOR_DEFAULT) {
- color = context.getResources().getColor(
- R.color.notification_material_background_media_default_color);
- }
- views.setInt(R.id.status_bar_latest_event_content, "setBackgroundColor", color);
- }
-
- @RequiresApi(21)
- private static void addHeadsUpToBuilderLollipop(Notification n,
- android.support.v4.app.NotificationCompat.Builder b) {
- RemoteViews innerView = b.getHeadsUpContentView() != null
- ? b.getHeadsUpContentView()
- : b.getContentView();
- if (b.mStyle instanceof DecoratedMediaCustomViewStyle && innerView != null) {
- n.headsUpContentView = NotificationCompatImplBase.generateMediaBigView(b.mContext,
- b.mContentTitle, b.mContentText, b.mContentInfo, b.mNumber,
- b.mLargeIcon, b.mSubText, b.mUseChronometer, b.getWhenIfShowing(),
- b.getPriority(), 0, b.mActions, false /* showCancelButton */,
- null /* cancelButtonIntent */, true /* decoratedCustomView */);
- NotificationCompatImplBase.buildIntoRemoteViews(b.mContext, n.headsUpContentView,
- innerView);
- setBackgroundColor(b.mContext, n.headsUpContentView, b.getColor());
- } else if (b.mStyle instanceof DecoratedCustomViewStyle) {
- addDecoratedHeadsUpToBuilderLollipop(n, b);
- }
- }
-
/**
- * See {@link android.support.v4.app.NotificationCompat}. In addition to the builder in v4, this
- * builder also supports {@link MediaStyle}.
+ * See {@link android.support.v4.app.NotificationCompat}.
*/
public static class Builder extends android.support.v4.app.NotificationCompat.Builder {
@@ -385,140 +83,6 @@
public Builder(Context context) {
super(context);
}
-
- /**
- * @return the text of the notification
- *
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- @Override
- protected CharSequence resolveText() {
- if (mStyle instanceof MessagingStyle) {
- MessagingStyle style = (MessagingStyle) mStyle;
- MessagingStyle.Message m = findLatestIncomingMessage(style);
- CharSequence conversationTitle = style.getConversationTitle();
- if (m != null) {
- return conversationTitle != null ? makeMessageLine(this, style, m)
- : m.getText();
- }
- }
- return super.resolveText();
- }
-
- /**
- * @return the title of the notification
- *
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- @Override
- protected CharSequence resolveTitle() {
- if (mStyle instanceof MessagingStyle) {
- MessagingStyle style = (MessagingStyle) mStyle;
- MessagingStyle.Message m = findLatestIncomingMessage(style);
- CharSequence conversationTitle = style.getConversationTitle();
- if (conversationTitle != null || m != null) {
- return conversationTitle != null ? conversationTitle : m.getSender();
- }
- }
- return super.resolveTitle();
- }
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- @Override
- protected BuilderExtender getExtender() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- return new Api24Extender();
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- return new LollipopExtender();
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
- return new JellybeanExtender();
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
- return new IceCreamSandwichExtender();
- } else {
- return super.getExtender();
- }
- }
- }
-
- @RequiresApi(14)
- private static class IceCreamSandwichExtender extends BuilderExtender {
-
- IceCreamSandwichExtender() {
- }
-
- @Override
- public Notification build(android.support.v4.app.NotificationCompat.Builder b,
- NotificationBuilderWithBuilderAccessor builder) {
- RemoteViews contentView = addStyleGetContentViewIcs(builder, b);
- Notification n = builder.build();
- // The above call might override decorated content views again, let's make sure it
- // sticks.
- if (contentView != null) {
- n.contentView = contentView;
- } else if (b.getContentView() != null) {
- n.contentView = b.getContentView();
- }
- return n;
- }
- }
-
- @RequiresApi(16)
- private static class JellybeanExtender extends BuilderExtender {
-
- JellybeanExtender() {
- }
-
- @Override
- public Notification build(android.support.v4.app.NotificationCompat.Builder b,
- NotificationBuilderWithBuilderAccessor builder) {
- RemoteViews contentView = addStyleGetContentViewJellybean(builder, b);
- Notification n = builder.build();
- // The above call might override decorated content views again, let's make sure it
- // sticks.
- if (contentView != null) {
- n.contentView = contentView;
- }
- addBigStyleToBuilderJellybean(n, b);
- return n;
- }
- }
-
- @RequiresApi(21)
- private static class LollipopExtender extends BuilderExtender {
-
- LollipopExtender() {
- }
-
- @Override
- public Notification build(android.support.v4.app.NotificationCompat.Builder b,
- NotificationBuilderWithBuilderAccessor builder) {
- RemoteViews contentView = addStyleGetContentViewLollipop(builder, b);
- Notification n = builder.build();
- // The above call might override decorated content views again, let's make sure it
- // sticks.
- if (contentView != null) {
- n.contentView = contentView;
- }
- addBigStyleToBuilderLollipop(n, b);
- addHeadsUpToBuilderLollipop(n, b);
- return n;
- }
- }
-
- @RequiresApi(24)
- private static class Api24Extender extends BuilderExtender {
-
- @Override
- public Notification build(android.support.v4.app.NotificationCompat.Builder b,
- NotificationBuilderWithBuilderAccessor builder) {
- addStyleToBuilderApi24(builder, b);
- return builder.build();
- }
}
/**
@@ -562,6 +126,9 @@
*/
public static class MediaStyle extends android.support.v4.app.NotificationCompat.Style {
+ private static final int MAX_MEDIA_BUTTONS_IN_COMPACT = 3;
+ private static final int MAX_MEDIA_BUTTONS = 5;
+
int[] mActionsToShowInCompact = null;
MediaSessionCompat.Token mToken;
boolean mShowCancelButton;
@@ -619,7 +186,9 @@
* @param show whether to show a cancel button
*/
public MediaStyle setShowCancelButton(boolean show) {
- mShowCancelButton = show;
+ if (Build.VERSION.SDK_INT < 21) {
+ mShowCancelButton = show;
+ }
return this;
}
@@ -633,6 +202,150 @@
mCancelButtonIntent = pendingIntent;
return this;
}
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void apply(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 21) {
+ builder.getBuilder().setStyle(
+ fillInMediaStyle(new Notification.MediaStyle()));
+ } else if (mShowCancelButton) {
+ builder.getBuilder().setOngoing(true);
+ }
+ }
+
+ @RequiresApi(21)
+ Notification.MediaStyle fillInMediaStyle(Notification.MediaStyle style) {
+ if (mActionsToShowInCompact != null) {
+ style.setShowActionsInCompactView(mActionsToShowInCompact);
+ }
+ if (mToken != null) {
+ style.setMediaSession((MediaSession.Token) mToken.getToken());
+ }
+ return style;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public RemoteViews makeContentView(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 21) {
+ // No custom content view required
+ return null;
+ }
+ return generateContentView();
+ }
+
+ RemoteViews generateContentView() {
+ RemoteViews view = NotificationCompatImplBase.applyStandardTemplate(
+ mBuilder.mContext, mBuilder.mContentTitle, mBuilder.mContentText,
+ mBuilder.mContentInfo, mBuilder.mNumber, 0 /* smallIcon */,
+ mBuilder.mLargeIcon, mBuilder.mSubText, mBuilder.mUseChronometer,
+ mBuilder.getWhenIfShowing(), mBuilder.getPriority(),
+ 0 /* color is unused on media */,
+ getContentViewLayoutResource(), true /* fitIn1U */);
+
+ final int numActions = mBuilder.mActions.size();
+ final int numActionsInCompact = mActionsToShowInCompact == null
+ ? 0
+ : Math.min(mActionsToShowInCompact.length, MAX_MEDIA_BUTTONS_IN_COMPACT);
+ view.removeAllViews(R.id.media_actions);
+ if (numActionsInCompact > 0) {
+ for (int i = 0; i < numActionsInCompact; i++) {
+ if (i >= numActions) {
+ throw new IllegalArgumentException(String.format(
+ "setShowActionsInCompactView: action %d out of bounds (max %d)",
+ i, numActions - 1));
+ }
+
+ final NotificationCompat.Action action =
+ mBuilder.mActions.get(mActionsToShowInCompact[i]);
+ final RemoteViews button = generateMediaActionButton(action);
+ view.addView(R.id.media_actions, button);
+ }
+ }
+ if (mShowCancelButton) {
+ view.setViewVisibility(R.id.end_padder, View.GONE);
+ view.setViewVisibility(R.id.cancel_action, View.VISIBLE);
+ view.setOnClickPendingIntent(R.id.cancel_action, mCancelButtonIntent);
+ view.setInt(R.id.cancel_action, "setAlpha", mBuilder.mContext
+ .getResources().getInteger(R.integer.cancel_button_image_alpha));
+ } else {
+ view.setViewVisibility(R.id.end_padder, View.VISIBLE);
+ view.setViewVisibility(R.id.cancel_action, View.GONE);
+ }
+ return view;
+ }
+
+ private RemoteViews generateMediaActionButton(NotificationCompat.Action action) {
+ final boolean tombstone = (action.getActionIntent() == null);
+ RemoteViews button = new RemoteViews(mBuilder.mContext.getPackageName(),
+ R.layout.notification_media_action);
+ button.setImageViewResource(R.id.action0, action.getIcon());
+ if (!tombstone) {
+ button.setOnClickPendingIntent(R.id.action0, action.getActionIntent());
+ }
+ if (Build.VERSION.SDK_INT >= 15) {
+ button.setContentDescription(R.id.action0, action.getTitle());
+ }
+ return button;
+ }
+
+ int getContentViewLayoutResource() {
+ return R.layout.notification_template_media;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public RemoteViews makeBigContentView(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 21) {
+ // No custom content view required
+ return null;
+ }
+ return generateBigContentView();
+ }
+
+ RemoteViews generateBigContentView() {
+ final int actionCount = Math.min(mBuilder.mActions.size(), MAX_MEDIA_BUTTONS);
+ RemoteViews big = NotificationCompatImplBase.applyStandardTemplate(
+ mBuilder.mContext, mBuilder.mContentTitle, mBuilder.mContentText,
+ mBuilder.mContentInfo, mBuilder.mNumber, 0 /* smallIcon */,
+ mBuilder.mLargeIcon, mBuilder.mSubText, mBuilder.mUseChronometer,
+ mBuilder.getWhenIfShowing(), mBuilder.getPriority(),
+ 0 /* color is unused on media */,
+ getBigContentViewLayoutResource(actionCount), false /* fitIn1U */);
+
+ big.removeAllViews(R.id.media_actions);
+ if (actionCount > 0) {
+ for (int i = 0; i < actionCount; i++) {
+ final RemoteViews button = generateMediaActionButton(mBuilder.mActions.get(i));
+ big.addView(R.id.media_actions, button);
+ }
+ }
+ if (mShowCancelButton) {
+ big.setViewVisibility(R.id.cancel_action, View.VISIBLE);
+ big.setInt(R.id.cancel_action, "setAlpha", mBuilder.mContext
+ .getResources().getInteger(R.integer.cancel_button_image_alpha));
+ big.setOnClickPendingIntent(R.id.cancel_action, mCancelButtonIntent);
+ } else {
+ big.setViewVisibility(R.id.cancel_action, View.GONE);
+ }
+ return big;
+ }
+
+ int getBigContentViewLayoutResource(int actionCount) {
+ return actionCount <= 3
+ ? R.layout.notification_template_big_media_narrow
+ : R.layout.notification_template_big_media;
+ }
}
@@ -666,8 +379,124 @@
*/
public static class DecoratedCustomViewStyle extends Style {
+ private static final int MAX_ACTION_BUTTONS = 3;
+
public DecoratedCustomViewStyle() {
}
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void apply(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ builder.getBuilder().setStyle(new Notification.DecoratedCustomViewStyle());
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public RemoteViews makeContentView(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ // No custom content view required
+ return null;
+ }
+ if (mBuilder.getContentView() == null) {
+ // No special content view
+ return null;
+ }
+ return createRemoteViews(mBuilder.getContentView(), false);
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public RemoteViews makeBigContentView(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ // No custom big content view required
+ return null;
+ }
+ RemoteViews bigContentView = mBuilder.getBigContentView();
+ RemoteViews innerView = bigContentView != null
+ ? bigContentView
+ : mBuilder.getContentView();
+ if (innerView == null) {
+ // No expandable notification
+ return null;
+ }
+ return createRemoteViews(innerView, true);
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public RemoteViews makeHeadsUpContentView(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ // No custom heads up content view required
+ return null;
+ }
+ RemoteViews headsUp = mBuilder.getHeadsUpContentView();
+ RemoteViews innerView = headsUp != null ? headsUp : mBuilder.getContentView();
+ if (headsUp == null) {
+ // No expandable notification
+ return null;
+ }
+ return createRemoteViews(innerView, true);
+ }
+
+ private RemoteViews createRemoteViews(RemoteViews innerView, boolean showActions) {
+ RemoteViews remoteViews = NotificationCompatImplBase.applyStandardTemplate(
+ mBuilder.mContext, mBuilder.mContentTitle, mBuilder.mContentText,
+ mBuilder.mContentInfo, mBuilder.mNumber, mBuilder.mNotification.icon,
+ mBuilder.mLargeIcon, mBuilder.mSubText, mBuilder.mUseChronometer,
+ mBuilder.getWhenIfShowing(), mBuilder.getPriority(), mBuilder.getColor(),
+ R.layout.notification_template_custom_big, false /* fitIn1U */);
+ remoteViews.removeAllViews(R.id.actions);
+ boolean actionsVisible = false;
+ if (showActions && mBuilder.mActions != null) {
+ int numActions = Math.max(mBuilder.mActions.size(), MAX_ACTION_BUTTONS);
+ if (numActions > 0) {
+ actionsVisible = true;
+ for (int i = 0; i < numActions; i++) {
+ final RemoteViews button = generateActionButton(mBuilder.mActions.get(i));
+ remoteViews.addView(R.id.actions, button);
+ }
+ }
+ }
+ int actionVisibility = actionsVisible ? View.VISIBLE : View.GONE;
+ remoteViews.setViewVisibility(R.id.actions, actionVisibility);
+ remoteViews.setViewVisibility(R.id.action_divider, actionVisibility);
+ NotificationCompatImplBase.buildIntoRemoteViews(mBuilder.mContext,
+ remoteViews, innerView);
+ return remoteViews;
+ }
+
+ private RemoteViews generateActionButton(NotificationCompat.Action action) {
+ final boolean tombstone = (action.actionIntent == null);
+ RemoteViews button = new RemoteViews(mBuilder.mContext.getPackageName(),
+ tombstone ? R.layout.notification_action_tombstone
+ : R.layout.notification_action);
+ button.setImageViewBitmap(R.id.action_image,
+ NotificationCompatImplBase.createColoredBitmap(mBuilder.mContext,
+ action.getIcon(), mBuilder.mContext.getResources()
+ .getColor(R.color.notification_action_color_filter)));
+ button.setTextViewText(R.id.action_text, action.title);
+ if (!tombstone) {
+ button.setOnClickPendingIntent(R.id.action_container, action.actionIntent);
+ }
+ if (Build.VERSION.SDK_INT >= 15) {
+ button.setContentDescription(R.id.action_container, action.title);
+ }
+ return button;
+ }
}
/**
@@ -707,5 +536,134 @@
public DecoratedMediaCustomViewStyle() {
}
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void apply(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ builder.getBuilder().setStyle(
+ fillInMediaStyle(new Notification.DecoratedMediaCustomViewStyle()));
+ } else {
+ super.apply(builder);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public RemoteViews makeContentView(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ // No custom content view required
+ return null;
+ }
+ boolean hasContentView = mBuilder.getContentView() != null;
+ if (Build.VERSION.SDK_INT >= 21) {
+ // If we are on L/M the media notification will only be colored if the expanded
+ // version is of media style, so we have to create a custom view for the collapsed
+ // version as well in that case.
+ boolean createCustomContent = hasContentView
+ || mBuilder.getBigContentView() != null;
+ if (createCustomContent) {
+ RemoteViews contentView = generateContentView();
+ if (hasContentView) {
+ NotificationCompatImplBase.buildIntoRemoteViews(mBuilder.mContext,
+ contentView,
+ mBuilder.getContentView());
+ }
+ setBackgroundColor(contentView);
+ return contentView;
+ }
+ } else {
+ RemoteViews contentView = generateContentView();
+ if (hasContentView) {
+ NotificationCompatImplBase.buildIntoRemoteViews(mBuilder.mContext,
+ contentView,
+ mBuilder.getContentView());
+ return contentView;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ int getContentViewLayoutResource() {
+ return mBuilder.getContentView() != null
+ ? R.layout.notification_template_media_custom
+ : super.getContentViewLayoutResource();
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public RemoteViews makeBigContentView(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ // No custom big content view required
+ return null;
+ }
+ RemoteViews innerView = mBuilder.getBigContentView() != null
+ ? mBuilder.getBigContentView()
+ : mBuilder.getContentView();
+ if (innerView == null) {
+ // No expandable notification
+ return null;
+ }
+ RemoteViews bigContentView = generateBigContentView();
+ NotificationCompatImplBase.buildIntoRemoteViews(mBuilder.mContext,
+ bigContentView,
+ innerView);
+ if (Build.VERSION.SDK_INT >= 21) {
+ setBackgroundColor(bigContentView);
+ }
+ return bigContentView;
+ }
+
+ @Override
+ int getBigContentViewLayoutResource(int actionCount) {
+ return actionCount <= 3
+ ? R.layout.notification_template_big_media_narrow_custom
+ : R.layout.notification_template_big_media_custom;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public RemoteViews makeHeadsUpContentView(NotificationBuilderWithBuilderAccessor builder) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ // No custom heads up content view required
+ return null;
+ }
+ RemoteViews innerView = mBuilder.getHeadsUpContentView() != null
+ ? mBuilder.getHeadsUpContentView()
+ : mBuilder.getContentView();
+ if (innerView == null) {
+ // No expandable notification
+ return null;
+ }
+ RemoteViews headsUpContentView = generateBigContentView();
+ NotificationCompatImplBase.buildIntoRemoteViews(mBuilder.mContext,
+ headsUpContentView,
+ innerView);
+ if (Build.VERSION.SDK_INT >= 21) {
+ setBackgroundColor(headsUpContentView);
+ }
+ return headsUpContentView;
+ }
+
+ private void setBackgroundColor(RemoteViews views) {
+ int color = mBuilder.getColor() != COLOR_DEFAULT
+ ? mBuilder.getColor()
+ : mBuilder.mContext.getResources().getColor(
+ R.color.notification_material_background_media_default_color);
+ views.setInt(R.id.status_bar_latest_event_content, "setBackgroundColor", color);
+ }
}
}
diff --git a/v7/appcompat/src/android/support/v7/app/NotificationCompatImpl21.java b/v7/appcompat/src/android/support/v7/app/NotificationCompatImpl21.java
deleted file mode 100644
index 2a4bf7b..0000000
--- a/v7/appcompat/src/android/support/v7/app/NotificationCompatImpl21.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.v7.app;
-
-import android.app.Notification;
-import android.media.session.MediaSession;
-import android.support.annotation.RequiresApi;
-import android.support.v4.app.NotificationBuilderWithBuilderAccessor;
-
-@RequiresApi(21)
-class NotificationCompatImpl21 {
-
- public static void addMediaStyle(NotificationBuilderWithBuilderAccessor b,
- int[] actionsToShowInCompact,
- Object token) {
- Notification.MediaStyle style = new Notification.MediaStyle(b.getBuilder());
- if (actionsToShowInCompact != null) {
- style.setShowActionsInCompactView(actionsToShowInCompact);
- }
- if (token != null) {
- style.setMediaSession((MediaSession.Token) token);
- }
- }
-}
diff --git a/v7/appcompat/src/android/support/v7/app/NotificationCompatImpl24.java b/v7/appcompat/src/android/support/v7/app/NotificationCompatImpl24.java
deleted file mode 100644
index cc09bb5..0000000
--- a/v7/appcompat/src/android/support/v7/app/NotificationCompatImpl24.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package android.support.v7.app;
-
-import android.app.Notification;
-import android.media.session.MediaSession;
-import android.support.annotation.RequiresApi;
-import android.support.v4.app.NotificationBuilderWithBuilderAccessor;
-
-@RequiresApi(24)
-class NotificationCompatImpl24 {
-
- public static void addDecoratedCustomViewStyle(NotificationBuilderWithBuilderAccessor b) {
- Notification.Builder builder = b.getBuilder();
- builder.setStyle(new Notification.DecoratedCustomViewStyle());
- }
-
- public static void addDecoratedMediaCustomViewStyle(NotificationBuilderWithBuilderAccessor b,
- int[] actionsToShowInCompact,
- Object token) {
- Notification.Builder builder = b.getBuilder();
- Notification.DecoratedMediaCustomViewStyle style =
- new Notification.DecoratedMediaCustomViewStyle();
- if (actionsToShowInCompact != null) {
- style.setShowActionsInCompactView(actionsToShowInCompact);
- }
- if (token != null) {
- style.setMediaSession((MediaSession.Token) token);
- }
- builder.setStyle(style);
- }
-}
diff --git a/v7/appcompat/src/android/support/v7/app/NotificationCompatImplBase.java b/v7/appcompat/src/android/support/v7/app/NotificationCompatImplBase.java
index c1432c9..8df0315 100644
--- a/v7/appcompat/src/android/support/v7/app/NotificationCompatImplBase.java
+++ b/v7/appcompat/src/android/support/v7/app/NotificationCompatImplBase.java
@@ -16,8 +16,6 @@
package android.support.v7.app;
-import android.app.Notification;
-import android.app.PendingIntent;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
@@ -29,17 +27,13 @@
import android.os.Build;
import android.os.SystemClock;
import android.support.annotation.RequiresApi;
-import android.support.v4.app.NotificationBuilderWithBuilderAccessor;
import android.support.v4.app.NotificationCompat;
-import android.support.v4.app.NotificationCompatBase;
import android.support.v7.appcompat.R;
import android.util.TypedValue;
import android.view.View;
import android.widget.RemoteViews;
import java.text.NumberFormat;
-import java.util.ArrayList;
-import java.util.List;
/**
* Helper class to generate MediaStyle notifications for pre-Lollipop platforms. Overrides
@@ -48,196 +42,7 @@
@RequiresApi(9)
class NotificationCompatImplBase {
- static final int MAX_MEDIA_BUTTONS_IN_COMPACT = 3;
- static final int MAX_MEDIA_BUTTONS = 5;
- private static final int MAX_ACTION_BUTTONS = 3;
-
- @RequiresApi(11)
- public static <T extends NotificationCompatBase.Action> RemoteViews overrideContentViewMedia(
- NotificationBuilderWithBuilderAccessor builder,
- Context context, CharSequence contentTitle, CharSequence contentText,
- CharSequence contentInfo, int number, Bitmap largeIcon, CharSequence subText,
- boolean useChronometer, long when, int priority, List<T> actions,
- int[] actionsToShowInCompact, boolean showCancelButton,
- PendingIntent cancelButtonIntent, boolean isDecoratedCustomView) {
- RemoteViews views = generateContentViewMedia(context, contentTitle, contentText, contentInfo,
- number, largeIcon, subText, useChronometer, when, priority, actions,
- actionsToShowInCompact, showCancelButton, cancelButtonIntent,
- isDecoratedCustomView);
- builder.getBuilder().setContent(views);
- if (showCancelButton) {
- builder.getBuilder().setOngoing(true);
- }
- return views;
- }
-
- @RequiresApi(11)
- private static <T extends NotificationCompatBase.Action> RemoteViews generateContentViewMedia(
- Context context, CharSequence contentTitle, CharSequence contentText,
- CharSequence contentInfo, int number, Bitmap largeIcon, CharSequence subText,
- boolean useChronometer, long when, int priority, List<T> actions,
- int[] actionsToShowInCompact, boolean showCancelButton,
- PendingIntent cancelButtonIntent, boolean isDecoratedCustomView) {
- RemoteViews view = applyStandardTemplate(context, contentTitle, contentText, contentInfo,
- number, 0 /* smallIcon */, largeIcon, subText, useChronometer, when, priority,
- 0 /* color is unused on media */,
- isDecoratedCustomView ? R.layout.notification_template_media_custom
- : R.layout.notification_template_media,
- true /* fitIn1U */);
-
- final int numActions = actions.size();
- final int N = actionsToShowInCompact == null
- ? 0
- : Math.min(actionsToShowInCompact.length, MAX_MEDIA_BUTTONS_IN_COMPACT);
- view.removeAllViews(R.id.media_actions);
- if (N > 0) {
- for (int i = 0; i < N; i++) {
- if (i >= numActions) {
- throw new IllegalArgumentException(String.format(
- "setShowActionsInCompactView: action %d out of bounds (max %d)",
- i, numActions - 1));
- }
-
- final NotificationCompatBase.Action action = actions.get(actionsToShowInCompact[i]);
- final RemoteViews button = generateMediaActionButton(context, action);
- view.addView(R.id.media_actions, button);
- }
- }
- if (showCancelButton) {
- view.setViewVisibility(R.id.end_padder, View.GONE);
- view.setViewVisibility(R.id.cancel_action, View.VISIBLE);
- view.setOnClickPendingIntent(R.id.cancel_action, cancelButtonIntent);
- view.setInt(R.id.cancel_action, "setAlpha",
- context.getResources().getInteger(R.integer.cancel_button_image_alpha));
- } else {
- view.setViewVisibility(R.id.end_padder, View.VISIBLE);
- view.setViewVisibility(R.id.cancel_action, View.GONE);
- }
- return view;
- }
-
- @RequiresApi(16)
- public static <T extends NotificationCompatBase.Action> void overrideMediaBigContentView(
- Notification n, Context context, CharSequence contentTitle, CharSequence contentText,
- CharSequence contentInfo, int number, Bitmap largeIcon, CharSequence subText,
- boolean useChronometer, long when, int priority, int color, List<T> actions,
- boolean showCancelButton, PendingIntent cancelButtonIntent,
- boolean decoratedCustomView) {
- n.bigContentView = generateMediaBigView(context, contentTitle, contentText, contentInfo,
- number, largeIcon, subText, useChronometer, when, priority, color, actions,
- showCancelButton, cancelButtonIntent, decoratedCustomView);
- if (showCancelButton) {
- n.flags |= Notification.FLAG_ONGOING_EVENT;
- }
- }
-
- @RequiresApi(11)
- public static <T extends NotificationCompatBase.Action> RemoteViews generateMediaBigView(
- Context context, CharSequence contentTitle, CharSequence contentText,
- CharSequence contentInfo, int number, Bitmap largeIcon, CharSequence subText,
- boolean useChronometer, long when, int priority, int color, List<T> actions,
- boolean showCancelButton, PendingIntent cancelButtonIntent,
- boolean decoratedCustomView) {
- final int actionCount = Math.min(actions.size(), MAX_MEDIA_BUTTONS);
- RemoteViews big = applyStandardTemplate(context, contentTitle, contentText, contentInfo,
- number, 0 /* smallIcon */, largeIcon, subText, useChronometer, when, priority,
- color, /* fitIn1U */getBigMediaLayoutResource(decoratedCustomView, actionCount),
- false);
-
- big.removeAllViews(R.id.media_actions);
- if (actionCount > 0) {
- for (int i = 0; i < actionCount; i++) {
- final RemoteViews button = generateMediaActionButton(context, actions.get(i));
- big.addView(R.id.media_actions, button);
- }
- }
- if (showCancelButton) {
- big.setViewVisibility(R.id.cancel_action, View.VISIBLE);
- big.setInt(R.id.cancel_action, "setAlpha",
- context.getResources().getInteger(R.integer.cancel_button_image_alpha));
- big.setOnClickPendingIntent(R.id.cancel_action, cancelButtonIntent);
- } else {
- big.setViewVisibility(R.id.cancel_action, View.GONE);
- }
- return big;
- }
-
- @RequiresApi(11)
- private static RemoteViews generateMediaActionButton(Context context,
- NotificationCompatBase.Action action) {
- final boolean tombstone = (action.getActionIntent() == null);
- RemoteViews button = new RemoteViews(context.getPackageName(),
- R.layout.notification_media_action);
- button.setImageViewResource(R.id.action0, action.getIcon());
- if (!tombstone) {
- button.setOnClickPendingIntent(R.id.action0, action.getActionIntent());
- }
- if (Build.VERSION.SDK_INT >= 15) {
- button.setContentDescription(R.id.action0, action.getTitle());
- }
- return button;
- }
-
- @RequiresApi(11)
- private static int getBigMediaLayoutResource(boolean decoratedCustomView, int actionCount) {
- if (actionCount <= 3) {
- return decoratedCustomView
- ? R.layout.notification_template_big_media_narrow_custom
- : R.layout.notification_template_big_media_narrow;
- } else {
- return decoratedCustomView
- ? R.layout.notification_template_big_media_custom
- : R.layout.notification_template_big_media;
- }
- }
-
- public static RemoteViews applyStandardTemplateWithActions(Context context,
- CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo,
- int number, int smallIcon, Bitmap largeIcon, CharSequence subText,
- boolean useChronometer, long when, int priority, int color, int resId, boolean fitIn1U,
- ArrayList<NotificationCompat.Action> actions) {
- RemoteViews remoteViews = applyStandardTemplate(context, contentTitle, contentText,
- contentInfo, number, smallIcon, largeIcon, subText, useChronometer, when, priority,
- color, resId, fitIn1U);
- remoteViews.removeAllViews(R.id.actions);
- boolean actionsVisible = false;
- if (actions != null) {
- int N = actions.size();
- if (N > 0) {
- actionsVisible = true;
- if (N > MAX_ACTION_BUTTONS) N = MAX_ACTION_BUTTONS;
- for (int i = 0; i < N; i++) {
- final RemoteViews button = generateActionButton(context, actions.get(i));
- remoteViews.addView(R.id.actions, button);
- }
- }
- }
- int actionVisibility = actionsVisible ? View.VISIBLE : View.GONE;
- remoteViews.setViewVisibility(R.id.actions, actionVisibility);
- remoteViews.setViewVisibility(R.id.action_divider, actionVisibility);
- return remoteViews;
- }
-
- private static RemoteViews generateActionButton(Context context,
- NotificationCompat.Action action) {
- final boolean tombstone = (action.actionIntent == null);
- RemoteViews button = new RemoteViews(context.getPackageName(),
- tombstone ? getActionTombstoneLayoutResource()
- : getActionLayoutResource());
- button.setImageViewBitmap(R.id.action_image,
- createColoredBitmap(context, action.getIcon(),
- context.getResources().getColor(R.color.notification_action_color_filter)));
- button.setTextViewText(R.id.action_text, action.title);
- if (!tombstone) {
- button.setOnClickPendingIntent(R.id.action_container, action.actionIntent);
- }
- if (Build.VERSION.SDK_INT >= 15) {
- button.setContentDescription(R.id.action_container, action.title);
- }
- return button;
- }
-
- private static Bitmap createColoredBitmap(Context context, int iconId, int color) {
+ static Bitmap createColoredBitmap(Context context, int iconId, int color) {
return createColoredBitmap(context, iconId, color, 0);
}
@@ -256,14 +61,6 @@
return resultBitmap;
}
- private static int getActionLayoutResource() {
- return R.layout.notification_action;
- }
-
- private static int getActionTombstoneLayoutResource() {
- return R.layout.notification_action_tombstone;
- }
-
public static RemoteViews applyStandardTemplate(Context context,
CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo,
int number, int smallIcon, Bitmap largeIcon, CharSequence subText,
diff --git a/v7/appcompat/src/android/support/v7/app/NotificationCompatImplJellybean.java b/v7/appcompat/src/android/support/v7/app/NotificationCompatImplJellybean.java
deleted file mode 100644
index 2fca0f0..0000000
--- a/v7/appcompat/src/android/support/v7/app/NotificationCompatImplJellybean.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package android.support.v7.app;
-
-import android.app.Notification;
-import android.support.annotation.RequiresApi;
-import android.support.v4.app.NotificationBuilderWithBuilderAccessor;
-
-@RequiresApi(16)
-class NotificationCompatImplJellybean {
-
- public static void addBigTextStyle(NotificationBuilderWithBuilderAccessor b,
- CharSequence bigText) {
- Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle(b.getBuilder());
- bigTextStyle.bigText(bigText);
- }
-}
diff --git a/v7/appcompat/src/android/support/v7/widget/TintTypedArray.java b/v7/appcompat/src/android/support/v7/widget/TintTypedArray.java
index 0a86600..2213dd3 100644
--- a/v7/appcompat/src/android/support/v7/widget/TintTypedArray.java
+++ b/v7/appcompat/src/android/support/v7/widget/TintTypedArray.java
@@ -109,9 +109,6 @@
*/
@Nullable
public Typeface getFont(@StyleableRes int index, int style, @NonNull TextView targetView) {
- if (Build.VERSION.SDK_INT >= 26) {
- return mWrapped.getFont(index);
- }
final int resourceId = mWrapped.getResourceId(index, 0);
if (resourceId == 0) {
return null;
diff --git a/v7/appcompat/src/android/support/v7/widget/TooltipCompatHandler.java b/v7/appcompat/src/android/support/v7/widget/TooltipCompatHandler.java
index b7f0c2c..5ce1f8b 100644
--- a/v7/appcompat/src/android/support/v7/widget/TooltipCompatHandler.java
+++ b/v7/appcompat/src/android/support/v7/widget/TooltipCompatHandler.java
@@ -84,11 +84,7 @@
view.setLongClickable(false);
view.setOnHoverListener(null);
} else {
- if (sActiveHandler != null && sActiveHandler.mAnchor == view) {
- sActiveHandler.update(tooltipText);
- } else {
- new TooltipCompatHandler(view, tooltipText);
- }
+ new TooltipCompatHandler(view, tooltipText);
}
}
@@ -187,8 +183,4 @@
mAnchor.removeCallbacks(mShowRunnable);
mAnchor.removeCallbacks(mHideRunnable);
}
-
- private void update(CharSequence tooltipText) {
- mPopup.updateContent(tooltipText);
- }
}
diff --git a/v7/appcompat/src/android/support/v7/widget/TooltipPopup.java b/v7/appcompat/src/android/support/v7/widget/TooltipPopup.java
index 6f78a79..f707c8f 100644
--- a/v7/appcompat/src/android/support/v7/widget/TooltipPopup.java
+++ b/v7/appcompat/src/android/support/v7/widget/TooltipPopup.java
@@ -97,10 +97,6 @@
return mContentView.getParent() != null;
}
- void updateContent(CharSequence tooltipText) {
- mMessageView.setText(tooltipText);
- }
-
private void computePosition(View anchorView, int anchorX, int anchorY, boolean fromTouch,
WindowManager.LayoutParams outParams) {
final int tooltipPreciseAnchorThreshold = mContext.getResources().getDimensionPixelOffset(
diff --git a/v7/appcompat/tests/res/font/samplexmldownloadedfont.xml b/v7/appcompat/tests/res/font/samplexmldownloadedfont.xml
index 59a0a05..659d196 100644
--- a/v7/appcompat/tests/res/font/samplexmldownloadedfont.xml
+++ b/v7/appcompat/tests/res/font/samplexmldownloadedfont.xml
@@ -1,10 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<font-family xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:fontProviderAuthority="com.example.test.fontprovider.authority"
- android:fontProviderPackage="com.example.test.fontprovider.package"
- android:fontProviderQuery="MyRequestedFont"
- app:fontProviderAuthority="com.example.test.fontprovider.authority"
- app:fontProviderPackage="com.example.test.fontprovider.package"
- app:fontProviderQuery="MyRequestedFont">
-</font-family>
\ No newline at end of file
+<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
+ app:fontProviderAuthority="com.example.test.fontprovider.authority"
+ app:fontProviderPackage="com.example.test.fontprovider.package"
+ app:fontProviderQuery="MyRequestedFont">
+</font-family>
diff --git a/v7/appcompat/tests/res/font/samplexmlfont.xml b/v7/appcompat/tests/res/font/samplexmlfont.xml
index 3dba563..e225025 100644
--- a/v7/appcompat/tests/res/font/samplexmlfont.xml
+++ b/v7/appcompat/tests/res/font/samplexmlfont.xml
@@ -1,12 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
-<font-family xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
- <font android:fontStyle="normal" android:fontWeight="400" android:font="@font/samplefont"
- app:fontStyle="normal" app:fontWeight="400" app:font="@font/samplefont" />
- <font android:fontStyle="italic" android:fontWeight="400" android:font="@font/samplefont2"
- app:fontStyle="italic" app:fontWeight="400" app:font="@font/samplefont2" />
- <font android:fontStyle="normal" android:fontWeight="800" android:font="@font/samplefont3"
- app:fontStyle="normal" app:fontWeight="800" app:font="@font/samplefont3"/>
- <font android:fontStyle="italic" android:fontWeight="800" android:font="@font/samplefont4"
- app:fontStyle="italic" app:fontWeight="800" app:font="@font/samplefont4"/>
-</font-family>
\ No newline at end of file
+<font-family xmlns:app="http://schemas.android.com/apk/res-auto">
+ <font app:fontStyle="normal" app:fontWeight="400" app:font="@font/samplefont" />
+ <font app:fontStyle="italic" app:fontWeight="400" app:font="@font/samplefont2" />
+ <font app:fontStyle="normal" app:fontWeight="800" app:font="@font/samplefont3"/>
+ <font app:fontStyle="italic" app:fontWeight="800" app:font="@font/samplefont4"/>
+</font-family>
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AppCompatMenuItemShortcutsTest.java b/v7/appcompat/tests/src/android/support/v7/app/AppCompatMenuItemShortcutsTest.java
index bf55d69..b0a7fb7 100644
--- a/v7/appcompat/tests/src/android/support/v7/app/AppCompatMenuItemShortcutsTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatMenuItemShortcutsTest.java
@@ -19,15 +19,13 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import android.os.Build;
import android.os.SystemClock;
import android.support.test.filters.SmallTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
-import android.support.v4.os.BuildCompat;
import android.support.v7.appcompat.test.R;
import android.view.KeyEvent;
-import android.view.Menu;
-import android.view.MenuInflater;
import org.junit.Before;
import org.junit.Rule;
@@ -42,8 +40,6 @@
public class AppCompatMenuItemShortcutsTest {
private AppCompatMenuItemShortcutsTestActivity mActivity;
- private MenuInflater mMenuInflater;
- private Menu mMenu;
@Rule
public ActivityTestRule<AppCompatMenuItemShortcutsTestActivity> mActivityTestRule =
@@ -57,7 +53,7 @@
@Test
public void testPerformShortcut() {
// The support library is only needed for API 25 or lower.
- if (!BuildCompat.isAtLeastO()) {
+ if (Build.VERSION.SDK_INT < 26) {
final long downTime = SystemClock.uptimeMillis();
int keyCodeToSend, metaState;
KeyEvent keyEventToSend;
diff --git a/v7/mediarouter/build.gradle b/v7/mediarouter/build.gradle
index d0448dd..093c6c5 100644
--- a/v7/mediarouter/build.gradle
+++ b/v7/mediarouter/build.gradle
@@ -5,7 +5,13 @@
compile project(":support-appcompat-v7")
compile project(":support-palette-v7")
- androidTestCompile libs.test_runner
+ androidTestCompile (libs.test_runner) {
+ exclude module: 'support-annotations'
+ }
+ androidTestCompile (libs.espresso_core) {
+ exclude module: 'support-annotations'
+ }
+ androidTestCompile libs.mockito_core
}
android {
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java
index 9f2d396..0ab2eb1 100644
--- a/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java
@@ -92,7 +92,10 @@
}
public MediaRouteChooserDialog(Context context, int theme) {
- super(MediaRouterThemeHelper.createThemedContext(context, theme), theme);
+ // If we pass theme ID of 0 to AppCompatDialog, it will apply dialogTheme on the context,
+ // which may override our style settings. Passes our uppermost theme ID to prevent this.
+ super(MediaRouterThemeHelper.createThemedContext(context, theme),
+ theme == 0 ? MediaRouterThemeHelper.createThemeForDialog(context, theme) : theme);
context = getContext();
mRouter = MediaRouter.getInstance(context);
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
index a7bceec..86f4753 100644
--- a/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
@@ -201,8 +201,12 @@
}
public MediaRouteControllerDialog(Context context, int theme) {
+ // If we pass theme ID of 0 to AppCompatDialog, it will apply dialogTheme on the context,
+ // which may override our style settings. Passes our uppermost theme ID to prevent this.
super(MediaRouterThemeHelper.createThemedContext(context,
- MediaRouterThemeHelper.getAlertDialogResolvedTheme(context, theme)), theme);
+ MediaRouterThemeHelper.getAlertDialogResolvedTheme(context, theme)), theme == 0
+ ? MediaRouterThemeHelper.createThemeForDialog(context, MediaRouterThemeHelper
+ .getAlertDialogResolvedTheme(context, theme)) : theme);
mContext = getContext();
mControllerCallback = new MediaControllerCallback();
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouterThemeHelper.java b/v7/mediarouter/src/android/support/v7/app/MediaRouterThemeHelper.java
index 23f3bad..9ef218e 100644
--- a/v7/mediarouter/src/android/support/v7/app/MediaRouterThemeHelper.java
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouterThemeHelper.java
@@ -54,29 +54,21 @@
* {@code 0} to use the parent {@code context}'s default theme.
* @return The themed context.
*/
- public static Context createThemedContext(Context context, int style) {
+ static Context createThemedContext(Context context, int style) {
// First, apply dialog property overlay.
+ Context themedContext =
+ new ContextThemeWrapper(context, getStyledRouterThemeId(context, style));
+ int customizedThemeId = getThemeResource(context, R.attr.mediaRouteTheme);
+ return customizedThemeId == 0 ? themedContext
+ : new ContextThemeWrapper(themedContext, customizedThemeId);
+ }
- int theme;
- if (isLightTheme(context)) {
- if (getControllerColor(context, style) == COLOR_DARK_ON_LIGHT_BACKGROUND) {
- theme = R.style.Theme_MediaRouter_Light;
- } else {
- theme = R.style.Theme_MediaRouter_Light_DarkControlPanel;
- }
- } else {
- if (getControllerColor(context, style) == COLOR_DARK_ON_LIGHT_BACKGROUND) {
- theme = R.style.Theme_MediaRouter_LightControlPanel;
- } else {
- theme = R.style.Theme_MediaRouter;
- }
- }
- int mediaRouteThemeResId = getThemeResource(context, R.attr.mediaRouteTheme);
- Context themedContext = new ContextThemeWrapper(context, theme);
- if (mediaRouteThemeResId != 0) {
- themedContext = new ContextThemeWrapper(themedContext, mediaRouteThemeResId);
- }
- return themedContext;
+ /**
+ * Creates the theme resource ID intended to be used by dialogs.
+ */
+ static int createThemeForDialog(Context context, int style) {
+ int customizedThemeId = getThemeResource(context, R.attr.mediaRouteTheme);
+ return customizedThemeId != 0 ? customizedThemeId : getStyledRouterThemeId(context, style);
}
public static int getThemeResource(Context context, int attr) {
@@ -180,4 +172,22 @@
}
return value.data;
}
+
+ private static int getStyledRouterThemeId(Context context, int style) {
+ int themeId;
+ if (isLightTheme(context)) {
+ if (getControllerColor(context, style) == COLOR_DARK_ON_LIGHT_BACKGROUND) {
+ themeId = R.style.Theme_MediaRouter_Light;
+ } else {
+ themeId = R.style.Theme_MediaRouter_Light_DarkControlPanel;
+ }
+ } else {
+ if (getControllerColor(context, style) == COLOR_DARK_ON_LIGHT_BACKGROUND) {
+ themeId = R.style.Theme_MediaRouter_LightControlPanel;
+ } else {
+ themeId = R.style.Theme_MediaRouter;
+ }
+ }
+ return themeId;
+ }
}
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderDescriptor.java b/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderDescriptor.java
index f6daeff..ac3ae5a 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderDescriptor.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderDescriptor.java
@@ -30,13 +30,12 @@
* </p>
*/
public final class MediaRouteProviderDescriptor {
- static final String KEY_ROUTES = "routes";
+ private static final String KEY_ROUTES = "routes";
- final Bundle mBundle;
- List<MediaRouteDescriptor> mRoutes;
+ private final Bundle mBundle;
+ private List<MediaRouteDescriptor> mRoutes;
- MediaRouteProviderDescriptor(Bundle bundle,
- List<MediaRouteDescriptor> routes) {
+ private MediaRouteProviderDescriptor(Bundle bundle, List<MediaRouteDescriptor> routes) {
mBundle = bundle;
mRoutes = routes;
}
@@ -49,7 +48,7 @@
return mRoutes;
}
- void ensureRoutes() {
+ private void ensureRoutes() {
if (mRoutes == null) {
ArrayList<Bundle> routeBundles = mBundle.<Bundle>getParcelableArrayList(KEY_ROUTES);
if (routeBundles == null || routeBundles.isEmpty()) {
@@ -179,6 +178,19 @@
}
/**
+ * Sets the list of routes.
+ */
+ Builder setRoutes(Collection<MediaRouteDescriptor> routes) {
+ if (routes == null || routes.isEmpty()) {
+ mRoutes = null;
+ mBundle.remove(KEY_ROUTES);
+ } else {
+ mRoutes = new ArrayList<>(routes);
+ }
+ return this;
+ }
+
+ /**
* Builds the {@link MediaRouteProviderDescriptor media route provider descriptor}.
*/
public MediaRouteProviderDescriptor build() {
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderService.java b/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderService.java
index 0d9c396..d788a3c 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderService.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderService.java
@@ -56,12 +56,12 @@
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
+import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.util.SparseArray;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
-import java.util.List;
/**
* Base class for media route provider services.
@@ -192,7 +192,8 @@
MediaRouteProviderDescriptor descriptor = mProvider.getDescriptor();
sendReply(messenger, SERVICE_MSG_REGISTERED,
requestId, SERVICE_VERSION_CURRENT,
- createDescriptorBundleForClient(descriptor, client), null);
+ createDescriptorBundleForClientVersion(descriptor,
+ client.mVersion), null);
}
return true;
}
@@ -413,32 +414,29 @@
for (int i = 0; i < count; i++) {
ClientRecord client = mClients.get(i);
sendReply(client.mMessenger, SERVICE_MSG_DESCRIPTOR_CHANGED, 0, 0,
- createDescriptorBundleForClient(descriptor, client), null);
+ createDescriptorBundleForClientVersion(descriptor, client.mVersion), null);
if (DEBUG) {
Log.d(TAG, client + ": Sent descriptor change event, descriptor=" + descriptor);
}
}
}
- private Bundle createDescriptorBundleForClient(MediaRouteProviderDescriptor descriptor,
- ClientRecord client) {
+ @VisibleForTesting
+ static Bundle createDescriptorBundleForClientVersion(MediaRouteProviderDescriptor descriptor,
+ int clientVersion) {
if (descriptor == null) {
return null;
}
- List<MediaRouteDescriptor> routes = descriptor.getRoutes();
- for (int i = routes.size() - 1; i >= 0; i--) {
- if (client.mVersion < routes.get(i).getMinClientVersion()
- || client.mVersion > routes.get(i).getMaxClientVersion()) {
- routes.remove(i);
+ MediaRouteProviderDescriptor.Builder builder =
+ new MediaRouteProviderDescriptor.Builder(descriptor);
+ builder.setRoutes(null);
+ for (MediaRouteDescriptor route : descriptor.getRoutes()) {
+ if (clientVersion >= route.getMinClientVersion()
+ && clientVersion <= route.getMaxClientVersion()) {
+ builder.addRoute(route);
}
}
-
- // Keep the values of the bundle from descriptor excepts routes values.
- Bundle bundle = descriptor.asBundle();
- bundle.remove(MediaRouteProviderDescriptor.KEY_ROUTES);
- return new MediaRouteProviderDescriptor.Builder(
- MediaRouteProviderDescriptor.fromBundle(bundle))
- .addRoutes(routes).build().asBundle();
+ return builder.build().asBundle();
}
boolean updateCompositeDiscoveryRequest() {
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
index e052035..8b64688 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
@@ -229,7 +229,7 @@
*/
public static final int AVAILABILITY_FLAG_REQUIRE_MATCH = 1 << 1;
- MediaRouter(Context context) {
+ private MediaRouter(Context context) {
mContext = context;
}
@@ -1973,10 +1973,11 @@
// the framework media router. This one is special and receives
// synchronization messages from the media router.
mSystemProvider = SystemMediaRouteProvider.obtain(applicationContext, this);
- addProvider(mSystemProvider);
}
public void start() {
+ addProvider(mSystemProvider);
+
// Start watching for routes published by registered media route
// provider services.
mRegisteredProviderWatcher = new RegisteredMediaRouteProviderWatcher(
@@ -2561,7 +2562,7 @@
private void setSelectedRouteInternal(RouteInfo route, int unselectReason) {
// TODO: Remove the following logging when no longer needed.
- if (mBluetoothRoute != null && route.isDefault()) {
+ if (mBluetoothRoute != null && route != null && route.isDefault()) {
final StackTraceElement[] callStack = Thread.currentThread().getStackTrace();
StringBuffer sb = new StringBuffer();
// callStack[3] is the caller of this method.
diff --git a/v7/mediarouter/tests/AndroidManifest.xml b/v7/mediarouter/tests/AndroidManifest.xml
index 5187e65..e1c6443 100644
--- a/v7/mediarouter/tests/AndroidManifest.xml
+++ b/v7/mediarouter/tests/AndroidManifest.xml
@@ -19,6 +19,11 @@
<uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
<application android:supportsRtl="true">
+ <activity
+ android:name="android.support.v7.app.MediaRouteChooserDialogTestActivity"
+ android:label="MediaRouteChooserDialogTestActivity"
+ android:theme="@style/Theme.AppCompat" />
+
<receiver android:name="android.support.v4.media.session.MediaButtonReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON"/>
diff --git a/v7/mediarouter/tests/res/layout/mr_chooser_dialog_activity.xml b/v7/mediarouter/tests/res/layout/mr_chooser_dialog_activity.xml
new file mode 100644
index 0000000..484bd23
--- /dev/null
+++ b/v7/mediarouter/tests/res/layout/mr_chooser_dialog_activity.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+</FrameLayout>
+
diff --git a/v7/mediarouter/tests/res/values/themes.xml b/v7/mediarouter/tests/res/values/themes.xml
new file mode 100644
index 0000000..31ae4ce
--- /dev/null
+++ b/v7/mediarouter/tests/res/values/themes.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+ <style name="HasWindowTitle">
+ <item name="windowNoTitle">false</item>
+ </style>
+
+</resources>
\ No newline at end of file
diff --git a/v7/mediarouter/tests/src/android/support/v7/app/MediaRouteChooserDialogTest.java b/v7/mediarouter/tests/src/android/support/v7/app/MediaRouteChooserDialogTest.java
index 7a21cdb..415ec1f 100644
--- a/v7/mediarouter/tests/src/android/support/v7/app/MediaRouteChooserDialogTest.java
+++ b/v7/mediarouter/tests/src/android/support/v7/app/MediaRouteChooserDialogTest.java
@@ -16,28 +16,76 @@
package android.support.v7.app;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.v7.media.MediaRouter.RouteInfo;
import android.support.v7.media.TestUtils;
+import android.support.v7.mediarouter.test.R;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
-@SmallTest
public class MediaRouteChooserDialogTest {
+
+ @Rule
+ public final ActivityTestRule<MediaRouteChooserDialogTestActivity> mActivityTestRule;
private MediaRouteChooserDialog.RouteComparator mComparator;
+ public MediaRouteChooserDialogTest() {
+ mActivityTestRule = new ActivityTestRule<>(MediaRouteChooserDialogTestActivity.class);
+ }
+
@Before
public void setup() {
mComparator = new MediaRouteChooserDialog.RouteComparator();
}
@Test
+ @SmallTest
+ @UiThreadTest
+ public void testWindowNoTitle() {
+ final Context context = mActivityTestRule.getActivity();
+ TypedArray typedArray;
+
+ // Without any base theme or customized theme
+ MediaRouteChooserDialog dialog = new MediaRouteChooserDialog(context);
+ typedArray = dialog.getContext().obtainStyledAttributes(R.styleable.AppCompatTheme);
+ assertTrue(typedArray.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false));
+ typedArray.recycle();
+
+ // No base theme, with a customized theme (has window title)
+ dialog = new MediaRouteChooserDialog(context, R.style.HasWindowTitle);
+ typedArray = dialog.getContext().obtainStyledAttributes(R.styleable.AppCompatTheme);
+ assertFalse(typedArray.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false));
+ typedArray.recycle();
+
+ // With base theme (has window title), no customized theme
+ context.setTheme(R.style.HasWindowTitle);
+ dialog = new MediaRouteChooserDialog(context);
+ typedArray = dialog.getContext().obtainStyledAttributes(R.styleable.AppCompatTheme);
+ assertTrue(typedArray.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false));
+ typedArray.recycle();
+
+ // With base theme and a customized theme (both has window title)
+ dialog = new MediaRouteChooserDialog(context, R.style.HasWindowTitle);
+ typedArray = dialog.getContext().obtainStyledAttributes(R.styleable.AppCompatTheme);
+ assertFalse(typedArray.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false));
+ typedArray.recycle();
+
+ context.setTheme(0);
+ }
+
+ @Test
public void testRouteComparatorWithSameRouteName() {
RouteInfo routeInfo1 = TestUtils.createRouteInfo("ROUTE_ID_1", "ROUTE_NAME_1");
RouteInfo routeInfo2 = TestUtils.createRouteInfo("ROUTE_ID_2", "ROUTE_NAME_1");
diff --git a/v7/mediarouter/tests/src/android/support/v7/app/MediaRouteChooserDialogTestActivity.java b/v7/mediarouter/tests/src/android/support/v7/app/MediaRouteChooserDialogTestActivity.java
new file mode 100644
index 0000000..39e4d7c
--- /dev/null
+++ b/v7/mediarouter/tests/src/android/support/v7/app/MediaRouteChooserDialogTestActivity.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.app;
+
+import android.os.Bundle;
+import android.support.v7.mediarouter.test.R;
+
+public class MediaRouteChooserDialogTestActivity extends AppCompatActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.mr_chooser_dialog_activity);
+ }
+}
diff --git a/v7/mediarouter/tests/src/android/support/v7/media/MediaRouteProviderTest.java b/v7/mediarouter/tests/src/android/support/v7/media/MediaRouteProviderTest.java
new file mode 100644
index 0000000..ff4409a
--- /dev/null
+++ b/v7/mediarouter/tests/src/android/support/v7/media/MediaRouteProviderTest.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.media;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Bundle;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test {@link MediaRouteProvider} and its related classes.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaRouteProviderTest {
+ private static final String FAKE_MEDIA_ROUTE_ID_1 = "fakeMediaRouteId1";
+ private static final String FAKE_MEDIA_ROUTE_ID_2 = "fakeMediaRouteId2";
+ private static final String FAKE_MEDIA_ROUTE_ID_3 = "fakeMediaRouteId3";
+ private static final String FAKE_MEDIA_ROUTE_ID_4 = "fakeMediaRouteId4";
+ private static final String FAKE_MEDIA_ROUTE_NAME_1 = "fakeMediaRouteName1";
+ private static final String FAKE_MEDIA_ROUTE_NAME_2 = "fakeMediaRouteName2";
+ private static final String FAKE_MEDIA_ROUTE_NAME_3 = "fakeMediaRouteName3";
+ private static final String FAKE_MEDIA_ROUTE_NAME_4 = "fakeMediaRouteName4";
+
+ @Test
+ @SmallTest
+ public void testDescriptorBuilder() {
+ // Tests for empty descriptor
+ MediaRouteProviderDescriptor.Builder builder = new MediaRouteProviderDescriptor.Builder();
+ MediaRouteProviderDescriptor descriptor = builder.build();
+ assertTrue(descriptor.getRoutes().isEmpty());
+
+ // Tests for addRoute()
+ builder.addRoute(new MediaRouteDescriptor.Builder(FAKE_MEDIA_ROUTE_ID_1,
+ FAKE_MEDIA_ROUTE_NAME_1).build());
+ builder.addRoute(new MediaRouteDescriptor.Builder(FAKE_MEDIA_ROUTE_ID_2,
+ FAKE_MEDIA_ROUTE_NAME_2).build());
+ descriptor = builder.build();
+ List<MediaRouteDescriptor> routes = descriptor.getRoutes();
+ assertEquals(2, routes.size());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_1, routes.get(0).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_1, routes.get(0).getName());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_2, routes.get(1).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_2, routes.get(1).getName());
+
+ // Tests for addRoutes()
+ List<MediaRouteDescriptor> otherRoutes = new ArrayList<>();
+ otherRoutes.add(new MediaRouteDescriptor.Builder(FAKE_MEDIA_ROUTE_ID_3,
+ FAKE_MEDIA_ROUTE_NAME_3).build());
+ otherRoutes.add(new MediaRouteDescriptor.Builder(FAKE_MEDIA_ROUTE_ID_4,
+ FAKE_MEDIA_ROUTE_NAME_4).build());
+ builder.addRoutes(otherRoutes);
+ descriptor = builder.build();
+ routes = descriptor.getRoutes();
+ assertEquals(4, routes.size());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_1, routes.get(0).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_1, routes.get(0).getName());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_2, routes.get(1).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_2, routes.get(1).getName());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_3, routes.get(2).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_3, routes.get(2).getName());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_4, routes.get(3).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_4, routes.get(3).getName());
+
+ // Tests for setRoutes()
+ builder.setRoutes(otherRoutes);
+ descriptor = builder.build();
+ routes = descriptor.getRoutes();
+ assertEquals(2, routes.size());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_3, routes.get(0).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_3, routes.get(0).getName());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_4, routes.get(1).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_4, routes.get(1).getName());
+
+ // Tests setRoutes() for side effects
+ otherRoutes.add(new MediaRouteDescriptor.Builder(FAKE_MEDIA_ROUTE_ID_1,
+ FAKE_MEDIA_ROUTE_NAME_1).build());
+ descriptor = builder.build();
+ routes = descriptor.getRoutes();
+ assertEquals(2, routes.size());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_3, routes.get(0).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_3, routes.get(0).getName());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_4, routes.get(1).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_4, routes.get(1).getName());
+
+ // Tests setRoutes against null
+ builder.setRoutes(null);
+ descriptor = builder.build();
+ assertTrue(descriptor.getRoutes().isEmpty());
+ }
+
+ @Test
+ @SmallTest
+ public void testCreateDescriptorBundleForClient() {
+ MediaRouteProviderDescriptor.Builder builder = new MediaRouteProviderDescriptor.Builder();
+ builder.addRoute(new MediaRouteDescriptor.Builder(FAKE_MEDIA_ROUTE_ID_1,
+ FAKE_MEDIA_ROUTE_NAME_1).setMaxClientVersion(15).setMinClientVersion(10).build());
+ builder.addRoute(new MediaRouteDescriptor.Builder(FAKE_MEDIA_ROUTE_ID_2,
+ FAKE_MEDIA_ROUTE_NAME_2).setMaxClientVersion(18).setMinClientVersion(11).build());
+ builder.addRoute(new MediaRouteDescriptor.Builder(FAKE_MEDIA_ROUTE_ID_3,
+ FAKE_MEDIA_ROUTE_NAME_3).setMaxClientVersion(25).setMinClientVersion(16).build());
+ builder.addRoute(new MediaRouteDescriptor.Builder(FAKE_MEDIA_ROUTE_ID_4,
+ FAKE_MEDIA_ROUTE_NAME_4).setMaxClientVersion(12).setMinClientVersion(4).build());
+ MediaRouteProviderDescriptor descriptor = builder.build();
+
+ Bundle bundle = MediaRouteProviderService
+ .createDescriptorBundleForClientVersion(descriptor, 3);
+ MediaRouteProviderDescriptor resultDescriptor =
+ MediaRouteProviderDescriptor.fromBundle(bundle);
+ assertTrue(resultDescriptor.getRoutes().isEmpty());
+
+ bundle = MediaRouteProviderService.createDescriptorBundleForClientVersion(descriptor, 4);
+ resultDescriptor = MediaRouteProviderDescriptor.fromBundle(bundle);
+ List<MediaRouteDescriptor> routes = resultDescriptor.getRoutes();
+ assertEquals(1, routes.size());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_4, routes.get(0).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_4, routes.get(0).getName());
+
+ bundle = MediaRouteProviderService.createDescriptorBundleForClientVersion(descriptor, 10);
+ resultDescriptor = MediaRouteProviderDescriptor.fromBundle(bundle);
+ routes = resultDescriptor.getRoutes();
+ assertEquals(2, routes.size());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_1, routes.get(0).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_1, routes.get(0).getName());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_4, routes.get(1).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_4, routes.get(1).getName());
+
+ bundle = MediaRouteProviderService.createDescriptorBundleForClientVersion(descriptor, 12);
+ resultDescriptor = MediaRouteProviderDescriptor.fromBundle(bundle);
+ routes = resultDescriptor.getRoutes();
+ assertEquals(3, routes.size());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_1, routes.get(0).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_1, routes.get(0).getName());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_2, routes.get(1).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_2, routes.get(1).getName());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_4, routes.get(2).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_4, routes.get(2).getName());
+
+ bundle = MediaRouteProviderService.createDescriptorBundleForClientVersion(descriptor, 15);
+ resultDescriptor = MediaRouteProviderDescriptor.fromBundle(bundle);
+ routes = resultDescriptor.getRoutes();
+ assertEquals(2, routes.size());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_1, routes.get(0).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_1, routes.get(0).getName());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_2, routes.get(1).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_2, routes.get(1).getName());
+
+ bundle = MediaRouteProviderService.createDescriptorBundleForClientVersion(descriptor, 16);
+ resultDescriptor = MediaRouteProviderDescriptor.fromBundle(bundle);
+ routes = resultDescriptor.getRoutes();
+ assertEquals(2, routes.size());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_2, routes.get(0).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_2, routes.get(0).getName());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_3, routes.get(1).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_3, routes.get(1).getName());
+
+ bundle = MediaRouteProviderService.createDescriptorBundleForClientVersion(descriptor, 19);
+ resultDescriptor = MediaRouteProviderDescriptor.fromBundle(bundle);
+ routes = resultDescriptor.getRoutes();
+ assertEquals(1, routes.size());
+ assertEquals(FAKE_MEDIA_ROUTE_ID_3, routes.get(0).getId());
+ assertEquals(FAKE_MEDIA_ROUTE_NAME_3, routes.get(0).getName());
+
+ bundle = MediaRouteProviderService.createDescriptorBundleForClientVersion(descriptor, 26);
+ resultDescriptor = MediaRouteProviderDescriptor.fromBundle(bundle);
+ assertTrue(resultDescriptor.getRoutes().isEmpty());
+ }
+}
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index e452009..8033400 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -636,20 +636,15 @@
setNestedScrollingEnabled(nestedScrollingEnabled);
}
- @Override
- public String toString() {
- return super.toString()
- + ", adapter:" + mAdapter
- + ", layout:" + mLayout
- + ", context:" + getContext();
- }
-
/**
* Label appended to all public exception strings, used to help find which RV in an app is
* hitting an exception.
*/
String exceptionLabel() {
- return " " + this;
+ return " " + super.toString()
+ + ", adapter:" + mAdapter
+ + ", layout:" + mLayout
+ + ", context:" + getContext();
}
/**
diff --git a/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java b/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java
index d63045b..d85a27a 100644
--- a/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java
+++ b/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java
@@ -19,6 +19,7 @@
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView.LayoutManager;
+import android.support.v7.widget.RecyclerView.SmoothScroller;
import android.support.v7.widget.RecyclerView.SmoothScroller.ScrollVectorProvider;
import android.util.DisplayMetrics;
import android.view.View;
@@ -159,7 +160,7 @@
return false;
}
- RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
+ SmoothScroller smoothScroller = createScroller(layoutManager);
if (smoothScroller == null) {
return false;
}
@@ -203,9 +204,24 @@
* @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}.
*
- * @return a {@link LinearSmoothScroller} which will handle the scrolling.
+ * @return a {@link SmoothScroller} which will handle the scrolling.
*/
@Nullable
+ protected SmoothScroller createScroller(LayoutManager layoutManager) {
+ return createSnapScroller(layoutManager);
+ }
+
+ /**
+ * Creates a scroller to be used in the snapping implementation.
+ *
+ * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
+ * {@link RecyclerView}.
+ *
+ * @return a {@link LinearSmoothScroller} which will handle the scrolling.
+ * @deprecated use {@link #createScroller(LayoutManager)} instead.
+ */
+ @Nullable
+ @Deprecated
protected LinearSmoothScroller createSnapScroller(LayoutManager layoutManager) {
if (!(layoutManager instanceof ScrollVectorProvider)) {
return null;
@@ -281,4 +297,4 @@
*/
public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX,
int velocityY);
-}
\ No newline at end of file
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
index a7530d1..5a13f52 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
@@ -460,32 +460,23 @@
}
@Test
- public void toStringContainsClasses() {
- RecyclerView recyclerView = new RecyclerView(getContext());
- recyclerView.setAdapter(new MockAdapter(10));
- recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
-
- String string = recyclerView.toString();
- assertTrue("must contain RV class", string.contains(RecyclerView.class.getName()));
- assertTrue("must contain Adapter class", string.contains(MockAdapter.class.getName()));
- assertTrue("must contain LM class", string.contains(LinearLayoutManager.class.getName()));
- assertTrue("must contain ctx class", string.contains(getContext().getClass().getName()));
- }
-
- @Test
public void exceptionContainsClasses() {
- RecyclerView recyclerView = new RecyclerView(getContext());
- recyclerView.setAdapter(new MockAdapter(10));
+ RecyclerView first = new RecyclerView(getContext());
+ first.setLayoutManager(new LinearLayoutManager(getContext()));
+ first.setAdapter(new MockAdapter(10));
+ RecyclerView second = new RecyclerView(getContext());
try {
- recyclerView.generateDefaultLayoutParams();
+ second.setLayoutManager(first.getLayoutManager());
fail("exception expected");
- } catch (IllegalStateException e) {
- String message = e.getMessage();
- assertTrue("must contain RV class",
- message.contains(RecyclerView.class.getName()));
- assertTrue("must contain Adapter class",
- message.contains(MockAdapter.class.getName()));
+ } catch (IllegalArgumentException e) {
+ // Note: exception contains first RV
+ String m = e.getMessage();
+ assertTrue("must contain RV class", m.contains(RecyclerView.class.getName()));
+ assertTrue("must contain Adapter class", m.contains(MockAdapter.class.getName()));
+ assertTrue("must contain LM class", m.contains(LinearLayoutManager.class.getName()));
+ assertTrue("must contain ctx class", m.contains(getContext().getClass().getName()));
+
}
}
diff --git a/wear/build.gradle b/wear/build.gradle
index 6ceff5b..d81f84e 100644
--- a/wear/build.gradle
+++ b/wear/build.gradle
@@ -18,7 +18,7 @@
android {
defaultConfig {
- minSdkVersion 24
+ minSdkVersion 23
}
sourceSets {
diff --git a/wear/res-public/values/public_styles.xml b/wear/res-public/values-v24/public_styles.xml
similarity index 100%
rename from wear/res-public/values/public_styles.xml
rename to wear/res-public/values-v24/public_styles.xml
diff --git a/wear/res-public/values/public_attrs.xml b/wear/res-public/values/public_attrs.xml
index 285c3dc..b4b35db 100644
--- a/wear/res-public/values/public_attrs.xml
+++ b/wear/res-public/values/public_attrs.xml
@@ -36,4 +36,10 @@
<!-- WearableNavigationDrawerView -->
<public type="attr" name="navigationStyle" />
+
+ <!-- CircularProgressLayout -->
+ <public type="attr" name="backgroundColor" />
+ <public type="attr" name="colorSchemeColors" />
+ <public type="attr" name="strokeWidth" />
+ <public type="attr" name="indeterminate" />
</resources>
diff --git a/wear/res/values-v24/styles.xml b/wear/res/values-v24/styles.xml
new file mode 100644
index 0000000..a7f50fa
--- /dev/null
+++ b/wear/res/values-v24/styles.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <style name="Widget.Wear.RoundSwitch"
+ parent="@android:style/Widget.Material.CompoundButton.Switch">
+ <item name="android:layout_width">@dimen/ws_switch_size</item>
+ <item name="android:layout_height">@dimen/ws_switch_size</item>
+ <item name="android:switchMinWidth">@dimen/ws_switch_size</item>
+ <item name="android:layout_gravity">center</item>
+ <item name="android:thumb">@drawable/ws_switch_thumb_material_anim</item>
+ <item name="android:thumbTint">@color/ws_switch_thumb_color_material</item>
+ <item name="android:thumbTintMode">multiply</item>
+ <item name="android:track">@drawable/ws_switch_track_mtrl</item>
+ <item name="android:trackTint">@color/ws_switch_track_color_material</item>
+ <item name="android:background">@empty</item>
+ <item name="android:showText">false</item>
+ </style>
+</resources>
diff --git a/wear/res/values/arrays.xml b/wear/res/values/arrays.xml
new file mode 100644
index 0000000..3d7740d
--- /dev/null
+++ b/wear/res/values/arrays.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <!-- Default color scheme colors for CircularProgressLayout -->
+ <array name="circular_progress_layout_color_scheme_colors">
+ <item>@color/circular_progress_layout_red</item>
+ <item>@color/circular_progress_layout_yellow</item>
+ <item>@color/circular_progress_layout_green</item>
+ <item>@color/circular_progress_layout_blue</item>
+ </array>
+</resources>
\ No newline at end of file
diff --git a/wear/res/values/attrs.xml b/wear/res/values/attrs.xml
index d98e1b6..c8b47c7 100644
--- a/wear/res/values/attrs.xml
+++ b/wear/res/values/attrs.xml
@@ -171,4 +171,16 @@
</attr>
</declare-styleable>
+ <declare-styleable name="CircularProgressLayout">
+ <!-- Sets the color of the background circle. -->
+ <attr name="backgroundColor" format="color" />
+ <!-- Sets the stroke width of the progress indicator. -->
+ <attr name="strokeWidth" format="dimension" />
+ <!-- Sets the color scheme used by the progress indicator. This may be an array of colors or
+ a single color. If an array of colors is used, first color will be used for determinate
+ progress indicator, while the rest will be shown in order during indeterminate spinner. -->
+ <attr name="colorSchemeColors" format="reference|color" />
+ <!-- Sets if the progress should be shown as an indeterminate spinner. -->
+ <attr name="indeterminate" format="boolean" />
+ </declare-styleable>
</resources>
diff --git a/wear/res/values/colors.xml b/wear/res/values/colors.xml
new file mode 100644
index 0000000..a44c7a0
--- /dev/null
+++ b/wear/res/values/colors.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <!-- From material color palette -->
+ <color name="circular_progress_layout_red">#FFF44336</color>
+ <color name="circular_progress_layout_yellow">#FFFFEB3B</color>
+ <color name="circular_progress_layout_green">#FF4CAF50</color>
+ <color name="circular_progress_layout_blue">#FF2196F3</color>
+
+ <!-- Default background color for CircularProgressLayout -->
+ <color name="circular_progress_layout_background_color">#00000000</color>
+</resources>
\ No newline at end of file
diff --git a/wear/res/values/dimens.xml b/wear/res/values/dimens.xml
index 4e5496d..26d6c0a 100644
--- a/wear/res/values/dimens.xml
+++ b/wear/res/values/dimens.xml
@@ -63,4 +63,7 @@
<dimen name="ws_drawer_view_edge_size">38dp</dimen>
<!-- Dimensions for the wearable switch. -->
<dimen name="ws_switch_size">40dp</dimen>
+
+ <!-- Default stroke width for CircularProgressLayout -->
+ <dimen name="circular_progress_layout_stroke_width">4dp</dimen>
</resources>
diff --git a/wear/res/values/styles.xml b/wear/res/values/styles.xml
index c0036f1..44ab0b0 100644
--- a/wear/res/values/styles.xml
+++ b/wear/res/values/styles.xml
@@ -37,18 +37,4 @@
<item name="android:elevation">@dimen/ws_wearable_drawer_view_elevation</item>
<item name="android:background">?android:attr/colorBackgroundFloating</item>
</style>
- <style name="Widget.Wear.RoundSwitch"
- parent="@android:style/Widget.Material.CompoundButton.Switch">
- <item name="android:layout_width">@dimen/ws_switch_size</item>
- <item name="android:layout_height">@dimen/ws_switch_size</item>
- <item name="android:switchMinWidth">@dimen/ws_switch_size</item>
- <item name="android:layout_gravity">center</item>
- <item name="android:thumb">@drawable/ws_switch_thumb_material_anim</item>
- <item name="android:thumbTint">@color/ws_switch_thumb_color_material</item>
- <item name="android:thumbTintMode">multiply</item>
- <item name="android:track">@drawable/ws_switch_track_mtrl</item>
- <item name="android:trackTint">@color/ws_switch_track_color_material</item>
- <item name="android:background">@empty</item>
- <item name="android:showText">false</item>
- </style>
</resources>
diff --git a/wear/src/android/support/wear/widget/CircularProgressLayout.java b/wear/src/android/support/wear/widget/CircularProgressLayout.java
new file mode 100644
index 0000000..1bfcc39
--- /dev/null
+++ b/wear/src/android/support/wear/widget/CircularProgressLayout.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.widget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.widget.CircularProgressDrawable;
+import android.support.wear.R;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.FrameLayout;
+
+/**
+ * {@link CircularProgressLayout} adds a circular countdown timer behind the view it contains,
+ * typically used to automatically confirm an operation after a short delay has elapsed.
+ *
+ * <p>The developer can specify a countdown interval via {@link #setTotalTime(long)} and a listener
+ * via {@link #setOnTimerFinishedListener(OnTimerFinishedListener)} to be called when the time has
+ * elapsed after {@link #startTimer()} has been called. Tap action can be received via {@link
+ * #setOnClickListener(OnClickListener)} and can be used to cancel the timer via {@link
+ * #stopTimer()} method.
+ *
+ * <p>Alternatively, this layout can be used to show indeterminate progress by calling {@link
+ * #setIndeterminate(boolean)} method.
+ */
+public class CircularProgressLayout extends FrameLayout {
+
+ /**
+ * Update interval for 30 fps.
+ */
+ private static final long DEFAULT_UPDATE_INTERVAL = 1000 / 30;
+
+ /**
+ * Starting rotation for the progress indicator. Geometric clockwise [0..360] degree range
+ * correspond to [0..1] range. 0.75 corresponds to 12 o'clock direction on a watch.
+ */
+ private static final float DEFAULT_ROTATION = 0.75f;
+
+ /**
+ * Used as background of this layout.
+ */
+ private CircularProgressDrawable mProgressDrawable;
+
+ /**
+ * Used to control this layout.
+ */
+ private CircularProgressLayoutController mController;
+
+ /**
+ * Angle for the progress to start from.
+ */
+ private float mStartingRotation = DEFAULT_ROTATION;
+
+ /**
+ * Duration of the timer in milliseconds.
+ */
+ private long mTotalTime;
+
+
+ /**
+ * Interface to implement for listening to {@link
+ * OnTimerFinishedListener#onTimerFinished(CircularProgressLayout)} event.
+ */
+ public interface OnTimerFinishedListener {
+
+ /**
+ * Called when the timer started by {@link #startTimer()} method finishes.
+ *
+ * @param layout {@link CircularProgressLayout} that calls this method.
+ */
+ void onTimerFinished(CircularProgressLayout layout);
+ }
+
+ public CircularProgressLayout(Context context) {
+ this(context, null);
+ }
+
+ public CircularProgressLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CircularProgressLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public CircularProgressLayout(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ mProgressDrawable = new CircularProgressDrawable(context);
+ mProgressDrawable.setProgressRotation(DEFAULT_ROTATION);
+ setBackground(mProgressDrawable);
+
+ // If a child view is added, make it center aligned so it fits in the progress drawable.
+ setOnHierarchyChangeListener(new OnHierarchyChangeListener() {
+ @Override
+ public void onChildViewAdded(View parent, View child) {
+ // Ensure that child view is aligned in center
+ LayoutParams params = (LayoutParams) child.getLayoutParams();
+ params.gravity = Gravity.CENTER;
+ child.setLayoutParams(params);
+ }
+
+ @Override
+ public void onChildViewRemoved(View parent, View child) {
+
+ }
+ });
+
+ mController = new CircularProgressLayoutController(this);
+
+ Resources r = context.getResources();
+ TypedArray a = r.obtainAttributes(attrs, R.styleable.CircularProgressLayout);
+
+ if (a.getType(R.styleable.CircularProgressLayout_colorSchemeColors) == TypedValue
+ .TYPE_REFERENCE || !a.hasValue(
+ R.styleable.CircularProgressLayout_colorSchemeColors)) {
+ int arrayResId = a.getResourceId(R.styleable.CircularProgressLayout_colorSchemeColors,
+ R.array.circular_progress_layout_color_scheme_colors);
+ setColorSchemeColors(getColorListFromResources(r, arrayResId));
+ } else {
+ setColorSchemeColors(a.getColor(R.styleable.CircularProgressLayout_colorSchemeColors,
+ Color.BLACK));
+ }
+
+ setStrokeWidth(a.getDimensionPixelSize(R.styleable.CircularProgressLayout_strokeWidth,
+ r.getDimensionPixelSize(
+ R.dimen.circular_progress_layout_stroke_width)));
+
+ setBackgroundColor(a.getColor(R.styleable.CircularProgressLayout_backgroundColor,
+ r.getColor(R.color.circular_progress_layout_background_color, null)));
+
+ setIndeterminate(a.getBoolean(R.styleable.CircularProgressLayout_indeterminate, false));
+
+ a.recycle();
+ }
+
+ private int[] getColorListFromResources(Resources resources, int arrayResId) {
+ TypedArray colorArray = resources.obtainTypedArray(arrayResId);
+ int[] colors = new int[colorArray.length()];
+ for (int i = 0; i < colorArray.length(); i++) {
+ colors[i] = colorArray.getColor(i, 0);
+ }
+ colorArray.recycle();
+ return colors;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (getChildCount() != 0) {
+ View childView = getChildAt(0);
+ // Wrap the drawable around the child view
+ mProgressDrawable.setCenterRadius(
+ Math.min(childView.getWidth(), childView.getHeight()) / 2f);
+ } else {
+ // Fill the bounds if no child view is present
+ mProgressDrawable.setCenterRadius(0f);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mController.reset();
+ }
+
+ /**
+ * Sets the background color of the {@link CircularProgressDrawable}, which is drawn as a circle
+ * inside the progress drawable. Colors are in ARGB format defined in {@link Color}.
+ *
+ * @param color an ARGB color
+ */
+ @Override
+ public void setBackgroundColor(@ColorInt int color) {
+ mProgressDrawable.setBackgroundColor(color);
+ }
+
+ /**
+ * Returns the background color of the {@link CircularProgressDrawable}.
+ *
+ * @return an ARGB color
+ */
+ @ColorInt
+ public int getBackgroundColor() {
+ return mProgressDrawable.getBackgroundColor();
+ }
+
+ /**
+ * Returns the {@link CircularProgressDrawable} used as background of this layout.
+ *
+ * @return {@link CircularProgressDrawable}
+ */
+ @NonNull
+ public CircularProgressDrawable getProgressDrawable() {
+ return mProgressDrawable;
+ }
+
+ /**
+ * Sets if progress should be shown as an indeterminate spinner.
+ *
+ * @param indeterminate {@code true} if indeterminate spinner should be shown, {@code false}
+ * otherwise.
+ */
+ public void setIndeterminate(boolean indeterminate) {
+ mController.setIndeterminate(indeterminate);
+ }
+
+ /**
+ * Returns if progress is showing as an indeterminate spinner.
+ *
+ * @return {@code true} if indeterminate spinner is shown, {@code false} otherwise.
+ */
+ public boolean isIndeterminate() {
+ return mController.isIndeterminate();
+ }
+
+ /**
+ * Sets the total time in milliseconds for the timer to countdown to. Calling this method while
+ * the timer is already running will not change the duration of the current timer.
+ *
+ * @param totalTime total time in milliseconds
+ */
+ public void setTotalTime(long totalTime) {
+ if (totalTime <= 0) {
+ throw new IllegalArgumentException("Total time should be greater than zero.");
+ }
+ mTotalTime = totalTime;
+ }
+
+ /**
+ * Returns the total time in milliseconds for the timer to countdown to.
+ *
+ * @return total time in milliseconds
+ */
+ public long getTotalTime() {
+ return mTotalTime;
+ }
+
+ /**
+ * Starts the timer countdown. Once the countdown is finished, if there is an {@link
+ * OnTimerFinishedListener} registered by {@link
+ * #setOnTimerFinishedListener(OnTimerFinishedListener)} method, its
+ * {@link OnTimerFinishedListener#onTimerFinished(CircularProgressLayout)} method is called. If
+ * this method is called while there is already a running timer, it will restart the timer.
+ */
+ public void startTimer() {
+ mController.startTimer(mTotalTime, DEFAULT_UPDATE_INTERVAL);
+ mProgressDrawable.setProgressRotation(mStartingRotation);
+ }
+
+ /**
+ * Stops the timer countdown. If there is no timer running, calling this method will not do
+ * anything.
+ */
+ public void stopTimer() {
+ mController.stopTimer();
+ }
+
+ /**
+ * Returns if the timer is running.
+ *
+ * @return {@code true} if the timer is running, {@code false} otherwise
+ */
+ public boolean isTimerRunning() {
+ return mController.isTimerRunning();
+ }
+
+ /**
+ * Sets the starting rotation for the progress drawable to start from. Default starting rotation
+ * is {@code 0.75} and it corresponds clockwise geometric 270 degrees (12 o'clock on a watch)
+ *
+ * @param rotation starting rotation from [0..1]
+ */
+ public void setStartingRotation(float rotation) {
+ mStartingRotation = rotation;
+ }
+
+ /**
+ * Returns the starting rotation of the progress drawable.
+ *
+ * @return starting rotation from [0..1]
+ */
+ public float getStartingRotation() {
+ return mStartingRotation;
+ }
+
+ /**
+ * Sets the stroke width of the progress drawable in pixels.
+ *
+ * @param strokeWidth stroke width in pixels
+ */
+ public void setStrokeWidth(float strokeWidth) {
+ mProgressDrawable.setStrokeWidth(strokeWidth);
+ }
+
+ /**
+ * Returns the stroke width of the progress drawable in pixels.
+ *
+ * @return stroke width in pixels
+ */
+ public float getStrokeWidth() {
+ return mProgressDrawable.getStrokeWidth();
+ }
+
+ /**
+ * Sets the color scheme colors of the progress drawable, which is equivalent to calling {@link
+ * CircularProgressDrawable#setColorSchemeColors(int...)} method on background drawable of this
+ * layout.
+ *
+ * @param colors list of ARGB colors
+ */
+ public void setColorSchemeColors(int... colors) {
+ mProgressDrawable.setColorSchemeColors(colors);
+ }
+
+ /**
+ * Returns the color scheme colors of the progress drawable
+ *
+ * @return list of ARGB colors
+ */
+ public int[] getColorSchemeColors() {
+ return mProgressDrawable.getColorSchemeColors();
+ }
+
+ /**
+ * Returns the {@link OnTimerFinishedListener} that is registered to this layout.
+ *
+ * @return registered {@link OnTimerFinishedListener}
+ */
+ @Nullable
+ public OnTimerFinishedListener getOnTimerFinishedListener() {
+ return mController.getOnTimerFinishedListener();
+ }
+
+ /**
+ * Sets the {@link OnTimerFinishedListener} to be notified when timer countdown is finished.
+ *
+ * @param listener {@link OnTimerFinishedListener} to be notified, or {@code null} to clear
+ */
+ public void setOnTimerFinishedListener(@Nullable OnTimerFinishedListener listener) {
+ mController.setOnTimerFinishedListener(listener);
+ }
+}
diff --git a/wear/src/android/support/wear/widget/CircularProgressLayoutController.java b/wear/src/android/support/wear/widget/CircularProgressLayoutController.java
new file mode 100644
index 0000000..4fd7156
--- /dev/null
+++ b/wear/src/android/support/wear/widget/CircularProgressLayoutController.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.widget;
+
+import android.os.CountDownTimer;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+
+/**
+ * Controller for {@link CircularProgressLayout}.
+ */
+class CircularProgressLayoutController {
+
+ private final CircularProgressLayout mLayout;
+ @VisibleForTesting CountDownTimer mTimer;
+ private boolean mIsIndeterminate;
+ private boolean mIsTimerRunning;
+
+ /**
+ * Called when the timer is finished.
+ */
+ @Nullable
+ private CircularProgressLayout.OnTimerFinishedListener mOnTimerFinishedListener;
+
+ CircularProgressLayoutController(CircularProgressLayout layout) {
+ mLayout = layout;
+ }
+
+ /**
+ * Returns the registered {@link CircularProgressLayout.OnTimerFinishedListener}.
+ */
+ @Nullable
+ public CircularProgressLayout.OnTimerFinishedListener getOnTimerFinishedListener() {
+ return mOnTimerFinishedListener;
+ }
+
+ /**
+ * Sets the {@link CircularProgressLayout.OnTimerFinishedListener} to be notified when timer
+ * countdown is finished.
+ */
+ public void setOnTimerFinishedListener(
+ @Nullable CircularProgressLayout.OnTimerFinishedListener listener) {
+ mOnTimerFinishedListener = listener;
+ }
+
+ /** Returns true if the progress is shown as an indeterminate spinner. */
+ boolean isIndeterminate() {
+ return mIsIndeterminate;
+ }
+
+ /** Returns true if timer is running. */
+ boolean isTimerRunning() {
+ return mIsTimerRunning;
+ }
+
+ /** Sets if the progress should be shown as an indeterminate spinner. */
+ void setIndeterminate(boolean indeterminate) {
+ if (mIsIndeterminate == indeterminate) {
+ return;
+ }
+ mIsIndeterminate = indeterminate;
+ if (mIsIndeterminate) {
+ if (mIsTimerRunning) {
+ stopTimer();
+ }
+ mLayout.getProgressDrawable().start();
+ } else {
+ mLayout.getProgressDrawable().stop();
+ }
+ }
+
+ void startTimer(long totalTime, long updateInterval) {
+ reset();
+ mIsTimerRunning = true;
+ mTimer = new CircularProgressTimer(totalTime, updateInterval);
+ mTimer.start();
+ }
+
+ void stopTimer() {
+ if (mIsTimerRunning) {
+ mTimer.cancel();
+ mIsTimerRunning = false;
+ mLayout.getProgressDrawable().setStartEndTrim(0f, 0f); // Reset the progress
+ }
+ }
+
+ /**
+ * Resets everything.
+ */
+ void reset() {
+ setIndeterminate(false); // If showing indeterminate progress, stop it
+ stopTimer(); // Stop the previous timer if there is one
+ mLayout.getProgressDrawable().setStartEndTrim(0f, 0f); // Reset the progress
+ }
+
+ /**
+ * Class to handle timing for {@link CircularProgressLayout}.
+ */
+ private class CircularProgressTimer extends CountDownTimer {
+
+ private final long mTotalTime;
+
+ CircularProgressTimer(long totalTime, long updateInterval) {
+ super(totalTime, updateInterval);
+ mTotalTime = totalTime;
+ }
+
+ @Override
+ public void onTick(long millisUntilFinished) {
+ mLayout.getProgressDrawable()
+ .setStartEndTrim(0f, 1f - (float) millisUntilFinished / (float) mTotalTime);
+ mLayout.invalidate();
+ }
+
+ @Override
+ public void onFinish() {
+ mLayout.getProgressDrawable().setStartEndTrim(0f, 1f);
+ if (mOnTimerFinishedListener != null) {
+ mOnTimerFinishedListener.onTimerFinished(mLayout);
+ }
+ mIsTimerRunning = false;
+ }
+ }
+}
diff --git a/wear/tests/res/layout/circular_progress_layout.xml b/wear/tests/res/layout/circular_progress_layout.xml
new file mode 100644
index 0000000..cce69e6
--- /dev/null
+++ b/wear/tests/res/layout/circular_progress_layout.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:orientation="vertical">
+ <android.support.wear.widget.CircularProgressLayout
+ android:id="@+id/circular_progress_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:padding="10dp"
+ app:strokeWidth="10dp">
+ <View
+ android:id="@+id/child_view"
+ android:layout_width="40dp"
+ android:layout_height="40dp"/>
+ </android.support.wear.widget.CircularProgressLayout>
+</FrameLayout >
\ No newline at end of file
diff --git a/wear/tests/src/android/support/wear/widget/CircularProgressLayoutControllerTest.java b/wear/tests/src/android/support/wear/widget/CircularProgressLayoutControllerTest.java
new file mode 100644
index 0000000..8d5406d
--- /dev/null
+++ b/wear/tests/src/android/support/wear/widget/CircularProgressLayoutControllerTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.widget.CircularProgressDrawable;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.concurrent.TimeUnit;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class CircularProgressLayoutControllerTest {
+
+ private static final long TOTAL_TIME = TimeUnit.SECONDS.toMillis(1);
+ private static final long UPDATE_INTERVAL = TimeUnit.MILLISECONDS.toMillis(30);
+
+ private CircularProgressLayoutController mControllerUnderTest;
+
+ @Mock
+ CircularProgressDrawable mMockDrawable;
+ @Mock
+ CircularProgressLayout mMockLayout;
+ @Mock
+ CircularProgressLayout.OnTimerFinishedListener mMockListener;
+
+ @Before
+ public void setUp() {
+ mMockDrawable = mock(CircularProgressDrawable.class);
+ mMockLayout = mock(CircularProgressLayout.class);
+ mMockListener = mock(CircularProgressLayout.OnTimerFinishedListener.class);
+ when(mMockLayout.getProgressDrawable()).thenReturn(mMockDrawable);
+ when(mMockLayout.getOnTimerFinishedListener()).thenReturn(mMockListener);
+ mControllerUnderTest = new CircularProgressLayoutController(mMockLayout);
+ }
+
+ @Test
+ public void testSetIndeterminate() {
+ mControllerUnderTest.setIndeterminate(true);
+
+ assertEquals(true, mControllerUnderTest.isIndeterminate());
+ verify(mMockDrawable).start();
+ }
+
+ @Test
+ public void testIsIndeterminateAfterSetToFalse() {
+ mControllerUnderTest.setIndeterminate(true);
+ mControllerUnderTest.setIndeterminate(false);
+
+ assertEquals(false, mControllerUnderTest.isIndeterminate());
+ verify(mMockDrawable).stop();
+ }
+
+ @Test
+ @UiThreadTest
+ public void testIsTimerRunningAfterStart() {
+ mControllerUnderTest.startTimer(TOTAL_TIME, UPDATE_INTERVAL);
+
+ assertEquals(true, mControllerUnderTest.isTimerRunning());
+ }
+
+ @Test
+ @UiThreadTest
+ public void testIsTimerRunningAfterStop() {
+ mControllerUnderTest.startTimer(TOTAL_TIME, UPDATE_INTERVAL);
+ mControllerUnderTest.stopTimer();
+
+ assertEquals(false, mControllerUnderTest.isTimerRunning());
+ }
+
+ @Test
+ @UiThreadTest
+ public void testSwitchFromIndeterminateToDeterminate() {
+ mControllerUnderTest.setIndeterminate(true);
+ mControllerUnderTest.startTimer(TOTAL_TIME, UPDATE_INTERVAL);
+
+ assertEquals(false, mControllerUnderTest.isIndeterminate());
+ assertEquals(true, mControllerUnderTest.isTimerRunning());
+ verify(mMockDrawable).stop();
+ }
+
+ @Test
+ @UiThreadTest
+ public void testSwitchFromDeterminateToIndeterminate() {
+ mControllerUnderTest.startTimer(TOTAL_TIME, UPDATE_INTERVAL);
+ mControllerUnderTest.setIndeterminate(true);
+
+ assertEquals(true, mControllerUnderTest.isIndeterminate());
+ assertEquals(false, mControllerUnderTest.isTimerRunning());
+ verify(mMockDrawable).start();
+ }
+}
diff --git a/wear/tests/src/android/support/wear/widget/CircularProgressLayoutTest.java b/wear/tests/src/android/support/wear/widget/CircularProgressLayoutTest.java
new file mode 100644
index 0000000..ff98c30
--- /dev/null
+++ b/wear/tests/src/android/support/wear/widget/CircularProgressLayoutTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.widget;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Intent;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.wear.test.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.TimeUnit;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class CircularProgressLayoutTest {
+
+ private static final long TOTAL_TIME = TimeUnit.SECONDS.toMillis(1);
+
+ @Rule
+ public final ActivityTestRule<LayoutTestActivity> mActivityRule = new ActivityTestRule<>(
+ LayoutTestActivity.class, true, false);
+ private CircularProgressLayout mLayoutUnderTest;
+
+ @Before
+ public void setUp() {
+ mActivityRule.launchActivity(new Intent().putExtra(LayoutTestActivity
+ .EXTRA_LAYOUT_RESOURCE_ID, R.layout.circular_progress_layout));
+ mLayoutUnderTest = mActivityRule.getActivity().findViewById(R.id.circular_progress_layout);
+ mLayoutUnderTest.setOnTimerFinishedListener(new FakeListener());
+ }
+
+ @Test
+ public void testListenerIsNotified() {
+ mLayoutUnderTest.setTotalTime(TOTAL_TIME);
+ startTimerOnUiThread();
+ waitForTimer(TOTAL_TIME + 100);
+ assertNotNull(mLayoutUnderTest.getOnTimerFinishedListener());
+ assertTrue(((FakeListener) mLayoutUnderTest.getOnTimerFinishedListener()).mFinished);
+ }
+
+ @Test
+ public void testListenerIsNotNotifiedWhenStopped() {
+ mLayoutUnderTest.setTotalTime(TOTAL_TIME);
+ startTimerOnUiThread();
+ stopTimerOnUiThread();
+ waitForTimer(TOTAL_TIME + 100);
+ assertNotNull(mLayoutUnderTest.getOnTimerFinishedListener());
+ assertFalse(((FakeListener) mLayoutUnderTest.getOnTimerFinishedListener()).mFinished);
+ }
+
+ private class FakeListener implements CircularProgressLayout.OnTimerFinishedListener {
+
+ boolean mFinished;
+
+ @Override
+ public void onTimerFinished(CircularProgressLayout layout) {
+ mFinished = true;
+ }
+ }
+
+ private void startTimerOnUiThread() {
+ mActivityRule.getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mLayoutUnderTest.startTimer();
+ }
+ });
+ }
+
+ private void stopTimerOnUiThread() {
+ mActivityRule.getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mLayoutUnderTest.stopTimer();
+ }
+ });
+ }
+
+ private void waitForTimer(long time) {
+ try {
+ Thread.sleep(time);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+}