Merge "Revert "Made unfocusable views in RecyclerView visible when using key navigation"" into nyc-support-25.2-dev
diff --git a/api/current.txt b/api/current.txt
index 24fb7c4..a2b9eea 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -135,7 +135,9 @@
public class CustomTabsCallback {
ctor public CustomTabsCallback();
method public void extraCallback(java.lang.String, android.os.Bundle);
+ method public void onMessageChannelReady(android.os.Bundle);
method public void onNavigationEvent(int, android.os.Bundle);
+ method public void onPostMessage(java.lang.String, android.os.Bundle);
field public static final int NAVIGATION_ABORTED = 4; // 0x4
field public static final int NAVIGATION_FAILED = 3; // 0x3
field public static final int NAVIGATION_FINISHED = 2; // 0x2
@@ -215,10 +217,19 @@
method protected abstract boolean mayLaunchUrl(android.support.customtabs.CustomTabsSessionToken, android.net.Uri, android.os.Bundle, java.util.List<android.os.Bundle>);
method protected abstract boolean newSession(android.support.customtabs.CustomTabsSessionToken);
method public android.os.IBinder onBind(android.content.Intent);
+ method protected abstract int postMessage(android.support.customtabs.CustomTabsSessionToken, java.lang.String, android.os.Bundle);
+ method protected abstract boolean requestPostMessageChannel(android.support.customtabs.CustomTabsSessionToken, android.net.Uri);
method protected abstract boolean updateVisuals(android.support.customtabs.CustomTabsSessionToken, android.os.Bundle);
method protected abstract boolean warmup(long);
field public static final java.lang.String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService";
field public static final java.lang.String KEY_URL = "android.support.customtabs.otherurls.URL";
+ field public static final int RESULT_FAILURE_DISALLOWED = -1; // 0xffffffff
+ field public static final int RESULT_FAILURE_MESSAGING_ERROR = -3; // 0xfffffffd
+ field public static final int RESULT_FAILURE_REMOTE_ERROR = -2; // 0xfffffffe
+ field public static final int RESULT_SUCCESS = 0; // 0x0
+ }
+
+ public static abstract class CustomTabsService.Result implements java.lang.annotation.Annotation {
}
public abstract class CustomTabsServiceConnection implements android.content.ServiceConnection {
@@ -229,6 +240,8 @@
public final class CustomTabsSession {
method public boolean mayLaunchUrl(android.net.Uri, android.os.Bundle, java.util.List<android.os.Bundle>);
+ method public int postMessage(java.lang.String, android.os.Bundle);
+ method public boolean requestPostMessageChannel(android.net.Uri);
method public boolean setActionButton(android.graphics.Bitmap, java.lang.String);
method public boolean setSecondaryToolbarViews(android.widget.RemoteViews, int[], android.app.PendingIntent);
method public deprecated boolean setToolbarItem(int, android.graphics.Bitmap, java.lang.String);
@@ -237,6 +250,24 @@
public class CustomTabsSessionToken {
method public android.support.customtabs.CustomTabsCallback getCallback();
method public static android.support.customtabs.CustomTabsSessionToken getSessionTokenFromIntent(android.content.Intent);
+ method public boolean isAssociatedWith(android.support.customtabs.CustomTabsSession);
+ }
+
+ public class PostMessageService extends android.app.Service {
+ ctor public PostMessageService();
+ method public android.os.IBinder onBind(android.content.Intent);
+ }
+
+ public abstract class PostMessageServiceConnection implements android.content.ServiceConnection {
+ ctor public PostMessageServiceConnection(android.support.customtabs.CustomTabsSessionToken);
+ method public boolean bindSessionToPostMessageService(android.content.Context, java.lang.String);
+ method public final boolean notifyMessageChannelReady(android.os.Bundle);
+ method public void onPostMessageServiceConnected();
+ method public void onPostMessageServiceDisconnected();
+ method public final void onServiceConnected(android.content.ComponentName, android.os.IBinder);
+ method public final void onServiceDisconnected(android.content.ComponentName);
+ method public final boolean postMessage(java.lang.String, android.os.Bundle);
+ method public void unbindFromContext(android.content.Context);
}
}
@@ -874,7 +905,8 @@
method public java.lang.String getAttribute(java.lang.String);
method public double getAttributeDouble(java.lang.String, double);
method public int getAttributeInt(java.lang.String, int);
- method public boolean getLatLong(float[]);
+ method public deprecated boolean getLatLong(float[]);
+ method public double[] getLatLong();
method public byte[] getThumbnail();
method public android.graphics.Bitmap getThumbnailBitmap();
method public byte[] getThumbnailBytes();
@@ -883,6 +915,7 @@
method public boolean isThumbnailCompressed();
method public void saveAttributes() throws java.io.IOException;
method public void setAttribute(java.lang.String, java.lang.String);
+ 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
field public static final int ORIENTATION_NORMAL = 1; // 0x1
@@ -5971,9 +6004,11 @@
method public java.util.List<android.support.v4.media.session.MediaSessionCompat.QueueItem> getQueue();
method public java.lang.CharSequence getQueueTitle();
method public int getRatingType();
+ method public int getRepeatMode();
method public android.app.PendingIntent getSessionActivity();
method public android.support.v4.media.session.MediaSessionCompat.Token getSessionToken();
method public android.support.v4.media.session.MediaControllerCompat.TransportControls getTransportControls();
+ method public boolean isShuffleModeEnabled();
method public void registerCallback(android.support.v4.media.session.MediaControllerCompat.Callback);
method public void registerCallback(android.support.v4.media.session.MediaControllerCompat.Callback, android.os.Handler);
method public void sendCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
@@ -5991,8 +6026,10 @@
method public void onPlaybackStateChanged(android.support.v4.media.session.PlaybackStateCompat);
method public void onQueueChanged(java.util.List<android.support.v4.media.session.MediaSessionCompat.QueueItem>);
method public void onQueueTitleChanged(java.lang.CharSequence);
+ method public void onRepeatModeChanged(int);
method public void onSessionDestroyed();
method public void onSessionEvent(java.lang.String, android.os.Bundle);
+ method public void onShuffleModeChanged(boolean);
}
public static final class MediaControllerCompat.PlaybackInfo {
@@ -6021,6 +6058,8 @@
method public abstract void sendCustomAction(android.support.v4.media.session.PlaybackStateCompat.CustomAction, android.os.Bundle);
method public abstract void sendCustomAction(java.lang.String, android.os.Bundle);
method public abstract void setRating(android.support.v4.media.RatingCompat);
+ method public abstract void setRepeatMode(int);
+ method public abstract void setShuffleModeEnabled(boolean);
method public abstract void skipToNext();
method public abstract void skipToPrevious();
method public abstract void skipToQueueItem(long);
@@ -6054,7 +6093,9 @@
method public void setQueue(java.util.List<android.support.v4.media.session.MediaSessionCompat.QueueItem>);
method public void setQueueTitle(java.lang.CharSequence);
method public void setRatingType(int);
+ method public void setRepeatMode(int);
method public void setSessionActivity(android.app.PendingIntent);
+ method public void setShuffleModeEnabled(boolean);
field public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1; // 0x1
field public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 2; // 0x2
}
@@ -6077,6 +6118,8 @@
method public void onRewind();
method public void onSeekTo(long);
method public void onSetRating(android.support.v4.media.RatingCompat);
+ method public void onSetRepeatMode(int);
+ method public void onSetShuffleModeEnabled(boolean);
method public void onSkipToNext();
method public void onSkipToPrevious();
method public void onSkipToQueueItem(long);
@@ -6153,6 +6196,8 @@
field public static final long ACTION_REWIND = 8L; // 0x8L
field public static final long ACTION_SEEK_TO = 256L; // 0x100L
field public static final long ACTION_SET_RATING = 128L; // 0x80L
+ field public static final long ACTION_SET_REPEAT_MODE = 262144L; // 0x40000L
+ field public static final long ACTION_SET_SHUFFLE_MODE_ENABLED = 524288L; // 0x80000L
field public static final long ACTION_SKIP_TO_NEXT = 32L; // 0x20L
field public static final long ACTION_SKIP_TO_PREVIOUS = 16L; // 0x10L
field public static final long ACTION_SKIP_TO_QUEUE_ITEM = 4096L; // 0x1000L
@@ -6171,6 +6216,9 @@
field public static final int ERROR_CODE_SKIP_LIMIT_REACHED = 9; // 0x9
field public static final int ERROR_CODE_UNKNOWN_ERROR = 0; // 0x0
field public static final long PLAYBACK_POSITION_UNKNOWN = -1L; // 0xffffffffffffffffL
+ field public static final int REPEAT_MODE_ALL = 2; // 0x2
+ field public static final int REPEAT_MODE_NONE = 0; // 0x0
+ field public static final int REPEAT_MODE_ONE = 1; // 0x1
field public static final int STATE_BUFFERING = 6; // 0x6
field public static final int STATE_CONNECTING = 8; // 0x8
field public static final int STATE_ERROR = 7; // 0x7
diff --git a/compat/java/android/support/v4/app/NotificationCompatExtras.java b/compat/java/android/support/v4/app/NotificationCompatExtras.java
index 6a2ee93..bf9ba0f 100644
--- a/compat/java/android/support/v4/app/NotificationCompatExtras.java
+++ b/compat/java/android/support/v4/app/NotificationCompatExtras.java
@@ -61,7 +61,7 @@
* Extras key used internally by {@link NotificationCompat} to store the value of
* the {@link android.app.Notification.Action#getRemoteInputs} before the field
* was available.
- * If possible, use {@link NotificationCompat.Action#getRemoteInputs to access this field.
+ * If possible, use {@link NotificationCompat.Action#getRemoteInputs} to access this field.
*/
public static final String EXTRA_REMOTE_INPUTS =
NotificationCompatJellybean.EXTRA_REMOTE_INPUTS;
diff --git a/compat/java/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java b/compat/java/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java
index f01ac17..26045f7 100644
--- a/compat/java/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java
+++ b/compat/java/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java
@@ -79,7 +79,7 @@
* Request authentication of a crypto object. This call warms up the fingerprint hardware
* and starts scanning for a fingerprint. It terminates when
* {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
- * {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult) is called, at
+ * {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)} is called, at
* which point the object is no longer valid. The operation can be canceled by using the
* provided cancel object.
*
diff --git a/core-utils/java/android/support/v4/app/TaskStackBuilder.java b/core-utils/java/android/support/v4/app/TaskStackBuilder.java
index 51b5f1d..0358b46 100644
--- a/core-utils/java/android/support/v4/app/TaskStackBuilder.java
+++ b/core-utils/java/android/support/v4/app/TaskStackBuilder.java
@@ -313,7 +313,7 @@
* the new task stack will be created in its entirety.</p>
*
* @param options Additional options for how the Activity should be started.
- * See {@link android.content.Context#startActivity(Intent, Bundle)
+ * See {@link android.content.Context#startActivity(Intent, Bundle)}
*/
public void startActivities(Bundle options) {
if (mIntents.isEmpty()) {
@@ -357,7 +357,7 @@
* {@link Intent#fillIn(Intent, int)} to control which unspecified parts of the
* intent that can be supplied when the actual send happens.
* @param options Additional options for how the Activity should be started.
- * See {@link android.content.Context#startActivity(Intent, Bundle)
+ * See {@link android.content.Context#startActivity(Intent, Bundle)}
* @return The obtained PendingIntent
*/
public PendingIntent getPendingIntent(int requestCode, int flags, Bundle options) {
diff --git a/customtabs/build.gradle b/customtabs/build.gradle
index e427d1e..bfbeb1a 100644
--- a/customtabs/build.gradle
+++ b/customtabs/build.gradle
@@ -31,6 +31,7 @@
androidTest.setRoot('tests')
androidTest.java.srcDir('tests/src/')
+ androidTest.manifest.srcFile 'tests/AndroidManifest.xml'
}
compileOptions {
diff --git a/customtabs/src/android/support/customtabs/CustomTabsCallback.java b/customtabs/src/android/support/customtabs/CustomTabsCallback.java
index d7fdd39..818118a 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsCallback.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsCallback.java
@@ -19,7 +19,8 @@
import android.os.Bundle;
/**
- * A callback class for custom tabs client to get messages regarding events in their custom tabs.
+ * A callback class for custom tabs client to get messages regarding events in their custom tabs. In
+ * the implementation, all callbacks are sent to the UI thread for the client.
*/
public class CustomTabsCallback {
/**
@@ -76,4 +77,25 @@
* @param args Arguments for the calback
*/
public void extraCallback(String callbackName, Bundle args) {}
+
+ /**
+ * Called when {@link CustomTabsSession} has requested a postMessage channel through
+ * {@link CustomTabsService#requestPostMessageChannel(
+ * CustomTabsSessionToken, android.net.Uri)} and the channel
+ * is ready for sending and receiving messages on both ends.
+ *
+ * @param extras Reserved for future use.
+ */
+ public void onMessageChannelReady(Bundle extras) {}
+
+ /**
+ * Called when a tab controlled by this {@link CustomTabsSession} has sent a postMessage.
+ * If postMessage() is called from a single thread, then the messages will be posted in the
+ * same order. When received on the client side, it is the client's responsibility to preserve
+ * the ordering further.
+ *
+ * @param message The message sent.
+ * @param extras Reserved for future use.
+ */
+ public void onPostMessage(String message, Bundle extras) {}
}
diff --git a/customtabs/src/android/support/customtabs/CustomTabsClient.java b/customtabs/src/android/support/customtabs/CustomTabsClient.java
index 548c152..09f3110 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsClient.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsClient.java
@@ -26,6 +26,8 @@
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
@@ -72,7 +74,7 @@
/**
* Returns the preferred package to use for Custom Tabs, preferring the default VIEW handler.
*
- * @see {@link #getPackageName(Context, List<String>, boolean)}.
+ * @see #getPackageName(Context, List<String>, boolean)
*/
public static String getPackageName(Context context, @Nullable List<String> packages) {
return getPackageName(context, packages, false);
@@ -177,21 +179,60 @@
* then later with a Custom Tab. The client can then send later service calls or intents to
* through same session-intent-Custom Tab association.
* @param callback The callback through which the client will receive updates about the created
- * session. Can be null.
+ * session. Can be null. All the callbacks will be received on the UI thread.
* @return The session object that was created as a result of the transaction. The client can
- * use this to relay {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)} calls.
+ * use this to relay session specific calls.
* Null on error.
*/
public CustomTabsSession newSession(final CustomTabsCallback callback) {
ICustomTabsCallback.Stub wrapper = new ICustomTabsCallback.Stub() {
+ private Handler mHandler = new Handler(Looper.getMainLooper());
+
@Override
- public void onNavigationEvent(int navigationEvent, Bundle extras) {
- if (callback != null) callback.onNavigationEvent(navigationEvent, extras);
+ public void onNavigationEvent(final int navigationEvent, final Bundle extras) {
+ if (callback == null) return;
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onNavigationEvent(navigationEvent, extras);
+ }
+ });
}
@Override
- public void extraCallback(String callbackName, Bundle args) throws RemoteException {
- if (callback != null) callback.extraCallback(callbackName, args);
+ public void extraCallback(final String callbackName, final Bundle args)
+ throws RemoteException {
+ if (callback == null) return;
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.extraCallback(callbackName, args);
+ }
+ });
+ }
+
+ @Override
+ public void onMessageChannelReady(final Bundle extras)
+ throws RemoteException {
+ if (callback == null) return;
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onMessageChannelReady(extras);
+ }
+ });
+ }
+
+ @Override
+ public void onPostMessage(final String message, final Bundle extras)
+ throws RemoteException {
+ if (callback == null) return;
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onPostMessage(message, extras);
+ }
+ });
}
};
diff --git a/customtabs/src/android/support/customtabs/CustomTabsIntent.java b/customtabs/src/android/support/customtabs/CustomTabsIntent.java
index f2de314..009182a 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsIntent.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsIntent.java
@@ -393,8 +393,8 @@
/**
* Sets the action button that is displayed in the Toolbar with default tinting behavior.
*
- * @see {@link CustomTabsIntent.Builder#setActionButton(
- * Bitmap, String, PendingIntent, boolean)}
+ * @see CustomTabsIntent.Builder#setActionButton(
+ * Bitmap, String, PendingIntent, boolean)
*/
public Builder setActionButton(@NonNull Bitmap icon, @NonNull String description,
@NonNull PendingIntent pendingIntent) {
diff --git a/customtabs/src/android/support/customtabs/CustomTabsService.java b/customtabs/src/android/support/customtabs/CustomTabsService.java
index 25697c5..1ff1d7f 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsService.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsService.java
@@ -23,6 +23,7 @@
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.RemoteException;
+import android.support.annotation.IntDef;
import android.support.v4.util.ArrayMap;
import java.util.List;
@@ -35,162 +36,233 @@
* implementers that want to provide Custom Tabs functionality, not by clients that want to launch
* Custom Tabs.
*/
- public abstract class CustomTabsService extends Service {
- /**
- * The Intent action that a CustomTabsService must respond to.
- */
- public static final String ACTION_CUSTOM_TABS_CONNECTION =
- "android.support.customtabs.action.CustomTabsService";
+public abstract class CustomTabsService extends Service {
+ /**
+ * The Intent action that a CustomTabsService must respond to.
+ */
+ public static final String ACTION_CUSTOM_TABS_CONNECTION =
+ "android.support.customtabs.action.CustomTabsService";
- /**
- * For {@link CustomTabsService#mayLaunchUrl} calls that wants to specify more than one url,
- * this key can be used with {@link Bundle#putParcelable(String, android.os.Parcelable)}
- * to insert a new url to each bundle inside list of bundles.
- */
- public static final String KEY_URL =
- "android.support.customtabs.otherurls.URL";
+ /**
+ * For {@link CustomTabsService#mayLaunchUrl} calls that wants to specify more than one url,
+ * this key can be used with {@link Bundle#putParcelable(String, android.os.Parcelable)}
+ * to insert a new url to each bundle inside list of bundles.
+ */
+ public static final String KEY_URL =
+ "android.support.customtabs.otherurls.URL";
- private final Map<IBinder, DeathRecipient> mDeathRecipientMap = new ArrayMap<>();
+ @IntDef({RESULT_SUCCESS, RESULT_FAILURE_DISALLOWED,
+ RESULT_FAILURE_REMOTE_ERROR, RESULT_FAILURE_MESSAGING_ERROR})
+ public @interface Result {
+ }
- private ICustomTabsService.Stub mBinder = new ICustomTabsService.Stub() {
+ /**
+ * Indicates that the postMessage request was accepted.
+ */
+ public static final int RESULT_SUCCESS = 0;
+ /**
+ * Indicates that the postMessage request was not allowed due to a bad argument or requesting
+ * at a disallowed time like when in background.
+ */
+ public static final int RESULT_FAILURE_DISALLOWED = -1;
+ /**
+ * Indicates that the postMessage request has failed due to a {@link RemoteException} .
+ */
+ public static final int RESULT_FAILURE_REMOTE_ERROR = -2;
+ /**
+ * Indicates that the postMessage request has failed due to an internal error on the browser
+ * message channel.
+ */
+ public static final int RESULT_FAILURE_MESSAGING_ERROR = -3;
- @Override
- public boolean warmup(long flags) {
- return CustomTabsService.this.warmup(flags);
- }
+ private final Map<IBinder, DeathRecipient> mDeathRecipientMap = new ArrayMap<>();
- @Override
- public boolean newSession(ICustomTabsCallback callback) {
- final CustomTabsSessionToken sessionToken = new CustomTabsSessionToken(callback);
- try {
- DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
- @Override
- public void binderDied() {
- cleanUpSession(sessionToken);
- }
- };
- synchronized (mDeathRecipientMap) {
- callback.asBinder().linkToDeath(deathRecipient, 0);
- mDeathRecipientMap.put(callback.asBinder(), deathRecipient);
- }
- return CustomTabsService.this.newSession(sessionToken);
- } catch (RemoteException e) {
- return false;
- }
- }
+ private ICustomTabsService.Stub mBinder = new ICustomTabsService.Stub() {
- @Override
- public boolean mayLaunchUrl(ICustomTabsCallback callback, Uri url,
- Bundle extras, List<Bundle> otherLikelyBundles) {
- return CustomTabsService.this.mayLaunchUrl(
- new CustomTabsSessionToken(callback), url, extras, otherLikelyBundles);
- }
+ @Override
+ public boolean warmup(long flags) {
+ return CustomTabsService.this.warmup(flags);
+ }
- @Override
- public Bundle extraCommand(String commandName, Bundle args) {
- return CustomTabsService.this.extraCommand(commandName, args);
- }
+ @Override
+ public boolean newSession(ICustomTabsCallback callback) {
+ final CustomTabsSessionToken sessionToken = new CustomTabsSessionToken(callback);
+ try {
+ DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ cleanUpSession(sessionToken);
+ }
+ };
+ synchronized (mDeathRecipientMap) {
+ callback.asBinder().linkToDeath(deathRecipient, 0);
+ mDeathRecipientMap.put(callback.asBinder(), deathRecipient);
+ }
+ return CustomTabsService.this.newSession(sessionToken);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
- @Override
- public boolean updateVisuals(ICustomTabsCallback callback, Bundle bundle) {
- return CustomTabsService.this.updateVisuals(
- new CustomTabsSessionToken(callback), bundle);
- }
- };
+ @Override
+ public boolean mayLaunchUrl(ICustomTabsCallback callback, Uri url,
+ Bundle extras, List<Bundle> otherLikelyBundles) {
+ return CustomTabsService.this.mayLaunchUrl(
+ new CustomTabsSessionToken(callback), url, extras, otherLikelyBundles);
+ }
- @Override
- public IBinder onBind(Intent intent) {
- return mBinder;
- }
+ @Override
+ public Bundle extraCommand(String commandName, Bundle args) {
+ return CustomTabsService.this.extraCommand(commandName, args);
+ }
- /**
- * Called when the client side {@link IBinder} for this {@link CustomTabsSessionToken} is dead.
- * Can also be used to clean up {@link DeathRecipient} instances allocated for the given token.
- * @param sessionToken The session token for which the {@link DeathRecipient} call has been
- * received.
- * @return Whether the clean up was successful. Multiple calls with two tokens holdings the
- * same binder will return false.
- */
- protected boolean cleanUpSession(CustomTabsSessionToken sessionToken) {
- try {
- synchronized (mDeathRecipientMap) {
+ @Override
+ public boolean updateVisuals(ICustomTabsCallback callback, Bundle bundle) {
+ return CustomTabsService.this.updateVisuals(
+ new CustomTabsSessionToken(callback), bundle);
+ }
+
+ @Override
+ public boolean requestPostMessageChannel(ICustomTabsCallback callback,
+ Uri postMessageOrigin) {
+ return CustomTabsService.this.requestPostMessageChannel(
+ new CustomTabsSessionToken(callback), postMessageOrigin);
+ }
+
+ @Override
+ public int postMessage(ICustomTabsCallback callback, String message, Bundle extras) {
+ return CustomTabsService.this.postMessage(
+ new CustomTabsSessionToken(callback), message, extras);
+ }
+ };
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ /**
+ * Called when the client side {@link IBinder} for this {@link CustomTabsSessionToken} is dead.
+ * Can also be used to clean up {@link DeathRecipient} instances allocated for the given token.
+ *
+ * @param sessionToken The session token for which the {@link DeathRecipient} call has been
+ * received.
+ * @return Whether the clean up was successful. Multiple calls with two tokens holdings the
+ * same binder will return false.
+ */
+ protected boolean cleanUpSession(CustomTabsSessionToken sessionToken) {
+ try {
+ synchronized (mDeathRecipientMap) {
IBinder binder = sessionToken.getCallbackBinder();
DeathRecipient deathRecipient =
mDeathRecipientMap.get(binder);
binder.unlinkToDeath(deathRecipient, 0);
mDeathRecipientMap.remove(binder);
}
- } catch (NoSuchElementException e) {
- return false;
- }
- return true;
- }
+ } catch (NoSuchElementException e) {
+ return false;
+ }
+ return true;
+ }
- /**
- * Warms up the browser process asynchronously.
- *
- * @param flags Reserved for future use.
- * @return Whether warmup was/had been completed successfully. Multiple successful
- * calls will return true.
- */
- protected abstract boolean warmup(long flags);
+ /**
+ * Warms up the browser process asynchronously.
+ *
+ * @param flags Reserved for future use.
+ * @return Whether warmup was/had been completed successfully. Multiple successful
+ * calls will return true.
+ */
+ protected abstract boolean warmup(long flags);
- /**
- * Creates a new session through an ICustomTabsService with the optional callback. This session
- * can be used to associate any related communication through the service with an intent and
- * then later with a Custom Tab. The client can then send later service calls or intents to
- * through same session-intent-Custom Tab association.
- * @param sessionToken Session token to be used as a unique identifier. This also has access
- * to the {@link CustomTabsCallback} passed from the client side through
- * {@link CustomTabsSessionToken#getCallback()}.
- * @return Whether a new session was successfully created.
- */
- protected abstract boolean newSession(CustomTabsSessionToken sessionToken);
+ /**
+ * Creates a new session through an ICustomTabsService with the optional callback. This session
+ * can be used to associate any related communication through the service with an intent and
+ * then later with a Custom Tab. The client can then send later service calls or intents to
+ * through same session-intent-Custom Tab association.
+ *
+ * @param sessionToken Session token to be used as a unique identifier. This also has access
+ * to the {@link CustomTabsCallback} passed from the client side through
+ * {@link CustomTabsSessionToken#getCallback()}.
+ * @return Whether a new session was successfully created.
+ */
+ protected abstract boolean newSession(CustomTabsSessionToken sessionToken);
- /**
- * Tells the browser of a likely future navigation to a URL.
- *
- * The method {@link CustomTabsService#warmup(long)} has to be called beforehand.
- * The most likely URL has to be specified explicitly. Optionally, a list of
- * other likely URLs can be provided. They are treated as less likely than
- * the first one, and have to be sorted in decreasing priority order. These
- * additional URLs may be ignored.
- * All previous calls to this method will be deprioritized.
- *
- * @param sessionToken The unique identifier for the session. Can not be null.
- * @param url Most likely URL.
- * @param extras Reserved for future use.
- * @param otherLikelyBundles Other likely destinations, sorted in decreasing
- * likelihood order. Each Bundle has to provide a url.
- * @return Whether the call was successful.
- */
- protected abstract boolean mayLaunchUrl(CustomTabsSessionToken sessionToken, Uri url,
- Bundle extras, List<Bundle> otherLikelyBundles);
+ /**
+ * Tells the browser of a likely future navigation to a URL.
+ * <p>
+ * The method {@link CustomTabsService#warmup(long)} has to be called beforehand.
+ * The most likely URL has to be specified explicitly. Optionally, a list of
+ * other likely URLs can be provided. They are treated as less likely than
+ * the first one, and have to be sorted in decreasing priority order. These
+ * additional URLs may be ignored.
+ * All previous calls to this method will be deprioritized.
+ *
+ * @param sessionToken The unique identifier for the session. Can not be null.
+ * @param url Most likely URL.
+ * @param extras Reserved for future use.
+ * @param otherLikelyBundles Other likely destinations, sorted in decreasing
+ * likelihood order. Each Bundle has to provide a url.
+ * @return Whether the call was successful.
+ */
+ protected abstract boolean mayLaunchUrl(CustomTabsSessionToken sessionToken, Uri url,
+ Bundle extras, List<Bundle> otherLikelyBundles);
- /**
- * Unsupported commands that may be provided by the implementation.
- *
- * <p>
- * <strong>Note:</strong>Clients should <strong>never</strong> rely on this method to have a
- * defined behavior, as it is entirely implementation-defined and not supported.
- *
- * <p> This call can be used by implementations to add extra commands, for testing or
- * experimental purposes.
- *
- * @param commandName Name of the extra command to execute.
- * @param args Arguments for the command
- * @return The result {@link Bundle}, or null.
- */
- protected abstract Bundle extraCommand(String commandName, Bundle args);
+ /**
+ * Unsupported commands that may be provided by the implementation.
+ * <p>
+ * <p>
+ * <strong>Note:</strong>Clients should <strong>never</strong> rely on this method to have a
+ * defined behavior, as it is entirely implementation-defined and not supported.
+ * <p>
+ * <p> This call can be used by implementations to add extra commands, for testing or
+ * experimental purposes.
+ *
+ * @param commandName Name of the extra command to execute.
+ * @param args Arguments for the command
+ * @return The result {@link Bundle}, or null.
+ */
+ protected abstract Bundle extraCommand(String commandName, Bundle args);
/**
* Updates the visuals of custom tabs for the given session. Will only succeed if the given
* session matches the currently active one.
+ *
* @param sessionToken The currently active session that the custom tab belongs to.
* @param bundle The action button configuration bundle. This bundle should be constructed
* with the same structure in {@link CustomTabsIntent.Builder}.
* @return Whether the operation was successful.
*/
- protected abstract boolean updateVisuals(CustomTabsSessionToken sessionToken,
- Bundle bundle);
- }
+ protected abstract boolean updateVisuals(CustomTabsSessionToken sessionToken,
+ Bundle bundle);
+
+ /**
+ * Sends a request to create a two way postMessage channel between the client and the browser
+ * linked with the given {@link CustomTabsSession}.
+ *
+ * @param sessionToken The unique identifier for the session. Can not be null.
+ * @param postMessageOrigin A origin that the client is requesting to be identified as
+ * during the postMessage communication.
+ * @return Whether the implementation accepted the request. Note that returning true
+ * here doesn't mean an origin has already been assigned as the validation is
+ * asynchronous.
+ */
+ protected abstract boolean requestPostMessageChannel(CustomTabsSessionToken sessionToken,
+ Uri postMessageOrigin);
+
+ /**
+ * Sends a postMessage request using the origin communicated via
+ * {@link CustomTabsService#requestPostMessageChannel(
+ *CustomTabsSessionToken, Uri)}. Fails when called before
+ * {@link PostMessageServiceConnection#notifyMessageChannelReady(Bundle)} is received on the
+ * client side.
+ *
+ * @param sessionToken The unique identifier for the session. Can not be null.
+ * @param message The message that is being sent.
+ * @param extras Reserved for future use.
+ * @return An integer constant about the postMessage request result. Will return
+ * {@link CustomTabsService#RESULT_SUCCESS} if successful.
+ */
+ @Result
+ protected abstract int postMessage(
+ CustomTabsSessionToken sessionToken, String message, Bundle extras);
+}
diff --git a/customtabs/src/android/support/customtabs/CustomTabsSession.java b/customtabs/src/android/support/customtabs/CustomTabsSession.java
index bc05f8b..cad897c 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsSession.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsSession.java
@@ -25,6 +25,7 @@
import android.os.RemoteException;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.customtabs.CustomTabsService.Result;
import android.view.View;
import android.widget.RemoteViews;
@@ -36,6 +37,7 @@
*/
public final class CustomTabsSession {
private static final String TAG = "CustomTabsSession";
+ private final Object mLock = new Object();
private final ICustomTabsService mService;
private final ICustomTabsCallback mCallback;
private final ComponentName mComponentName;
@@ -78,7 +80,7 @@
* @param icon The new icon of the action button.
* @param description Content description of the action button.
*
- * @see {@link CustomTabsSession#setToolbarItem(int, Bitmap, String)}
+ * @see CustomTabsSession#setToolbarItem(int, Bitmap, String)
*/
public boolean setActionButton(@NonNull Bitmap icon, @NonNull String description) {
Bundle bundle = new Bundle();
@@ -142,6 +144,47 @@
}
}
+ /**
+ * Sends a request to create a two way postMessage channel between the client and the browser.
+ *
+ * @param postMessageOrigin A origin that the client is requesting to be identified as
+ * during the postMessage communication.
+ * @return Whether the implementation accepted the request. Note that returning true
+ * here doesn't mean an origin has already been assigned as the validation is
+ * asynchronous.
+ */
+ public boolean requestPostMessageChannel(Uri postMessageOrigin) {
+ try {
+ return mService.requestPostMessageChannel(
+ mCallback, postMessageOrigin);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Sends a postMessage request using the origin communicated via
+ * {@link CustomTabsService#requestPostMessageChannel(
+ * CustomTabsSessionToken, Uri)}. Fails when called before
+ * {@link PostMessageServiceConnection#notifyMessageChannelReady(Bundle)} is received on
+ * the client side.
+ *
+ * @param message The message that is being sent.
+ * @param extras Reserved for future use.
+ * @return An integer constant about the postMessage request result. Will return
+ * {@link CustomTabsService#RESULT_SUCCESS} if successful.
+ */
+ @Result
+ public int postMessage(String message, Bundle extras) {
+ synchronized (mLock) {
+ try {
+ return mService.postMessage(mCallback, message, extras);
+ } catch (RemoteException e) {
+ return CustomTabsService.RESULT_FAILURE_REMOTE_ERROR;
+ }
+ }
+ }
+
/* package */ IBinder getBinder() {
return mCallback.asBinder();
}
diff --git a/customtabs/src/android/support/customtabs/CustomTabsSessionToken.java b/customtabs/src/android/support/customtabs/CustomTabsSessionToken.java
index fdb5f91..adfadd9 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsSessionToken.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsSessionToken.java
@@ -58,6 +58,33 @@
Log.e(TAG, "RemoteException during ICustomTabsCallback transaction");
}
}
+
+ @Override
+ public void extraCallback(String callbackName, Bundle args) {
+ try {
+ mCallbackBinder.extraCallback(callbackName, args);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException during ICustomTabsCallback transaction");
+ }
+ }
+
+ @Override
+ public void onMessageChannelReady(Bundle extras) {
+ try {
+ mCallbackBinder.onMessageChannelReady(extras);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException during ICustomTabsCallback transaction");
+ }
+ }
+
+ @Override
+ public void onPostMessage(String message, Bundle extras) {
+ try {
+ mCallbackBinder.onPostMessage(message, extras);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException during ICustomTabsCallback transaction");
+ }
+ }
};
}
@@ -84,4 +111,11 @@
public CustomTabsCallback getCallback() {
return mCallback;
}
-}
\ No newline at end of file
+
+ /**
+ * @return Whether this token is associated with the given session.
+ */
+ public boolean isAssociatedWith(CustomTabsSession session) {
+ return session.getBinder().equals(mCallbackBinder);
+ }
+}
diff --git a/customtabs/src/android/support/customtabs/ICustomTabsCallback.aidl b/customtabs/src/android/support/customtabs/ICustomTabsCallback.aidl
index a467864..32b6e9b 100644
--- a/customtabs/src/android/support/customtabs/ICustomTabsCallback.aidl
+++ b/customtabs/src/android/support/customtabs/ICustomTabsCallback.aidl
@@ -22,7 +22,9 @@
* Interface to a CustomTabsCallback.
* @hide
*/
-oneway interface ICustomTabsCallback {
+interface ICustomTabsCallback {
void onNavigationEvent(int navigationEvent, in Bundle extras) = 1;
void extraCallback(String callbackName, in Bundle args) = 2;
+ void onMessageChannelReady(in Bundle extras) = 3;
+ void onPostMessage(String message, in Bundle extras) = 4;
}
diff --git a/customtabs/src/android/support/customtabs/ICustomTabsService.aidl b/customtabs/src/android/support/customtabs/ICustomTabsService.aidl
index e53ca84..b24b0dd 100644
--- a/customtabs/src/android/support/customtabs/ICustomTabsService.aidl
+++ b/customtabs/src/android/support/customtabs/ICustomTabsService.aidl
@@ -16,6 +16,7 @@
package android.support.customtabs;
+import android.content.ComponentName;
import android.net.Uri;
import android.os.Bundle;
import android.support.customtabs.ICustomTabsCallback;
@@ -33,4 +34,6 @@
in Bundle extras, in List<Bundle> otherLikelyBundles) = 3;
Bundle extraCommand(String commandName, in Bundle args) = 4;
boolean updateVisuals(in ICustomTabsCallback callback, in Bundle bundle) = 5;
+ boolean requestPostMessageChannel(in ICustomTabsCallback callback, in Uri postMessageOrigin) = 6;
+ int postMessage(in ICustomTabsCallback callback, String message, in Bundle extras) = 7;
}
diff --git a/customtabs/src/android/support/customtabs/IPostMessageService.aidl b/customtabs/src/android/support/customtabs/IPostMessageService.aidl
new file mode 100644
index 0000000..2c8a605
--- /dev/null
+++ b/customtabs/src/android/support/customtabs/IPostMessageService.aidl
@@ -0,0 +1,30 @@
+/*
+ * 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.customtabs;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.customtabs.ICustomTabsCallback;
+
+/**
+ * Interface to a PostMessageService.
+ * @hide
+ */
+interface IPostMessageService {
+ void onMessageChannelReady(in ICustomTabsCallback callback, in Bundle extras) = 1;
+ void onPostMessage(in ICustomTabsCallback callback, String message, in Bundle extras) = 2;
+}
diff --git a/customtabs/src/android/support/customtabs/PostMessageService.java b/customtabs/src/android/support/customtabs/PostMessageService.java
new file mode 100644
index 0000000..7355f4e
--- /dev/null
+++ b/customtabs/src/android/support/customtabs/PostMessageService.java
@@ -0,0 +1,48 @@
+/*
+ * 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.customtabs;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+/**
+ * A service to receive postMessage related communication from a Custom Tabs provider.
+ */
+public class PostMessageService extends Service {
+ private IPostMessageService.Stub mBinder = new IPostMessageService.Stub() {
+
+ @Override
+ public void onMessageChannelReady(
+ ICustomTabsCallback callback, Bundle extras) throws RemoteException {
+ callback.onMessageChannelReady(extras);
+ }
+
+ @Override
+ public void onPostMessage(ICustomTabsCallback callback,
+ String message, Bundle extras) throws RemoteException {
+ callback.onPostMessage(message, extras);
+ }
+ };
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+}
diff --git a/customtabs/src/android/support/customtabs/PostMessageServiceConnection.java b/customtabs/src/android/support/customtabs/PostMessageServiceConnection.java
new file mode 100644
index 0000000..4eef50c
--- /dev/null
+++ b/customtabs/src/android/support/customtabs/PostMessageServiceConnection.java
@@ -0,0 +1,126 @@
+/*
+ * 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.customtabs;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+/**
+ * A {@link ServiceConnection} for Custom Tabs providers to use while connecting to a
+ * {@link PostMessageService} on the client side.
+ */
+public abstract class PostMessageServiceConnection implements ServiceConnection {
+ private final Object mLock = new Object();
+ private final ICustomTabsCallback mSessionBinder;
+ private IPostMessageService mService;
+
+ public PostMessageServiceConnection(CustomTabsSessionToken session) {
+ mSessionBinder = ICustomTabsCallback.Stub.asInterface(session.getCallbackBinder());
+ }
+
+ /**
+ * Binds the browser side to the client app through the given {@link PostMessageService} name.
+ * After this, this {@link PostMessageServiceConnection} can be used for sending postMessage
+ * related communication back to the client.
+ * @param context A context to bind to the service.
+ * @param packageName The name of the package to be bound to.
+ * @return Whether the binding was successful.
+ */
+ public boolean bindSessionToPostMessageService(Context context, String packageName) {
+ Intent intent = new Intent();
+ intent.setClassName(packageName, PostMessageService.class.getName());
+ return context.bindService(intent, this, Context.BIND_AUTO_CREATE);
+ }
+
+ /**
+ * Unbinds this service connection from the given context.
+ * @param context The context to be unbound from.
+ */
+ public void unbindFromContext(Context context) {
+ context.unbindService(this);
+ }
+
+ @Override
+ public final void onServiceConnected(ComponentName name, IBinder service) {
+ mService = IPostMessageService.Stub.asInterface(service);
+ onPostMessageServiceConnected();
+ }
+
+ @Override
+ public final void onServiceDisconnected(ComponentName name) {
+ mService = null;
+ onPostMessageServiceDisconnected();
+ }
+
+ /**
+ * Notifies the client that the postMessage channel requested with
+ * {@link CustomTabsService#requestPostMessageChannel(
+ * CustomTabsSessionToken, android.net.Uri)} is ready. This method should be
+ * called when the browser binds to the client side {@link PostMessageService} and also readies
+ * a connection to the web frame.
+ *
+ * @param extras Reserved for future use.
+ * @return Whether the notification was sent to the remote successfully.
+ */
+ public final boolean notifyMessageChannelReady(Bundle extras) {
+ if (mService == null) return false;
+ synchronized (mLock) {
+ try {
+ mService.onMessageChannelReady(mSessionBinder, extras);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Posts a message to the client. This should be called when a tab controlled by related
+ * {@link CustomTabsSession} has sent a postMessage. If postMessage() is called from a single
+ * thread, then the messages will be posted in the same order.
+ *
+ * @param message The message sent.
+ * @param extras Reserved for future use.
+ * @return Whether the postMessage was sent to the remote successfully.
+ */
+ public final boolean postMessage(String message, Bundle extras) {
+ if (mService == null) return false;
+ synchronized (mLock) {
+ try {
+ mService.onPostMessage(mSessionBinder, message, extras);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Called when the {@link PostMessageService} connection is established.
+ */
+ public void onPostMessageServiceConnected() {}
+
+ /**
+ * Called when the connection is lost with the {@link PostMessageService}.
+ */
+ public void onPostMessageServiceDisconnected() {}
+}
diff --git a/customtabs/tests/src/AndroidManifest.xml b/customtabs/tests/AndroidManifest.xml
similarity index 76%
rename from customtabs/tests/src/AndroidManifest.xml
rename to customtabs/tests/AndroidManifest.xml
index d87a83f..6fe8ad9 100644
--- a/customtabs/tests/src/AndroidManifest.xml
+++ b/customtabs/tests/AndroidManifest.xml
@@ -23,9 +23,12 @@
tools:overrideLibrary="android.support.test, android.app, android.support.test.rule,
android.support.test.espresso, android.support.test.espresso.idling" />
- <application/>
+ <application>
+ <uses-library android:name="android.test.runner" />
+ <activity android:name="android.support.customtabs.TestActivity"/>
- <instrumentation
- android:name="android.test.InstrumentationTestRunner"
- android:targetPackage="android.support.customtabs.test"/>
+ <service android:name="android.support.customtabs.PostMessageService"/>
+
+ <service android:name="android.support.customtabs.TestCustomTabsService"/>
+ </application>
</manifest>
diff --git a/customtabs/tests/src/android/support/customtabs/CustomTabsIntentTest.java b/customtabs/tests/src/android/support/customtabs/CustomTabsIntentTest.java
index 0fc69f9..c965361 100644
--- a/customtabs/tests/src/android/support/customtabs/CustomTabsIntentTest.java
+++ b/customtabs/tests/src/android/support/customtabs/CustomTabsIntentTest.java
@@ -39,6 +39,7 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public class CustomTabsIntentTest {
+
@Test
public void testBareboneCustomTabIntent() {
CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder().build();
diff --git a/customtabs/tests/src/android/support/customtabs/PollingCheck.java b/customtabs/tests/src/android/support/customtabs/PollingCheck.java
new file mode 100644
index 0000000..0163e94
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/PollingCheck.java
@@ -0,0 +1,94 @@
+/*
+ * 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.customtabs;
+
+import junit.framework.Assert;
+
+import java.util.concurrent.Callable;
+
+public abstract class PollingCheck {
+ private static final long TIME_SLICE = 50;
+ private long mTimeout = 3000;
+
+ public interface PollingCheckCondition {
+ boolean canProceed();
+ }
+
+ public PollingCheck() {
+ }
+
+ public PollingCheck(long timeout) {
+ mTimeout = timeout;
+ }
+
+ protected abstract boolean check();
+
+ public void run() {
+ if (check()) {
+ return;
+ }
+
+ long timeout = mTimeout;
+ while (timeout > 0) {
+ try {
+ Thread.sleep(TIME_SLICE);
+ } catch (InterruptedException e) {
+ Assert.fail("unexpected InterruptedException");
+ }
+
+ if (check()) {
+ return;
+ }
+
+ timeout -= TIME_SLICE;
+ }
+
+ Assert.fail("unexpected timeout");
+ }
+
+ public static void check(CharSequence message, long timeout, Callable<Boolean> condition)
+ throws Exception {
+ while (timeout > 0) {
+ if (condition.call()) {
+ return;
+ }
+
+ Thread.sleep(TIME_SLICE);
+ timeout -= TIME_SLICE;
+ }
+
+ Assert.fail(message.toString());
+ }
+
+ public static void waitFor(final PollingCheckCondition condition) {
+ new PollingCheck() {
+ @Override
+ protected boolean check() {
+ return condition.canProceed();
+ }
+ }.run();
+ }
+
+ public static void waitFor(long timeout, final PollingCheckCondition condition) {
+ new PollingCheck(timeout) {
+ @Override
+ protected boolean check() {
+ return condition.canProceed();
+ }
+ }.run();
+ }
+}
diff --git a/customtabs/tests/src/android/support/customtabs/PostMessageServiceConnectionTest.java b/customtabs/tests/src/android/support/customtabs/PostMessageServiceConnectionTest.java
new file mode 100644
index 0000000..a2b1b31
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/PostMessageServiceConnectionTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.customtabs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.rule.ServiceTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Tests for {@link PostMessageServiceConnection} with no {@link CustomTabsService} component.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PostMessageServiceConnectionTest {
+ @Rule
+ public final ServiceTestRule mServiceRule;
+ @Rule
+ public final ActivityTestRule<TestActivity> mActivityTestRule;
+ private TestCustomTabsCallback mCallback;
+ private Context mContext;
+ private PostMessageServiceConnection mConnection;
+ private boolean mServiceConnected;
+
+
+ public PostMessageServiceConnectionTest() {
+ mActivityTestRule = new ActivityTestRule<TestActivity>(TestActivity.class);
+ mServiceRule = new ServiceTestRule();
+ }
+
+ @Before
+ public void setup() {
+ mCallback = new TestCustomTabsCallback();
+ mContext = mActivityTestRule.getActivity();
+ mConnection = new PostMessageServiceConnection(
+ new CustomTabsSessionToken(mCallback.getStub())) {
+ public void onPostMessageServiceConnected() {
+ mServiceConnected = true;
+ }
+
+ @Override
+ public void onPostMessageServiceDisconnected() {
+ mServiceConnected = false;
+ }
+ };
+ Intent intent = new Intent();
+ intent.setClassName(mContext.getPackageName(), PostMessageService.class.getName());
+ try {
+ mServiceRule.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+ } catch (TimeoutException e) {
+ fail();
+ }
+ }
+
+ @Test
+ public void testNotifyChannelCreationAndSendMessages() {
+ PollingCheck.waitFor(500, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mServiceConnected;
+ }
+ });
+ assertTrue(mServiceConnected);
+ mConnection.notifyMessageChannelReady(null);
+ assertTrue(mCallback.isMessageChannelReady());
+ mConnection.postMessage("message1", null);
+ assertEquals(mCallback.getMessages().size(), 1);
+ mConnection.postMessage("message2", null);
+ assertEquals(mCallback.getMessages().size(), 2);
+ }
+}
diff --git a/customtabs/tests/src/android/support/customtabs/PostMessageTest.java b/customtabs/tests/src/android/support/customtabs/PostMessageTest.java
new file mode 100644
index 0000000..b212693
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/PostMessageTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.customtabs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.rule.ServiceTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.TimeoutException;
+
+
+/**
+ * Tests for a complete loop between a browser side {@link CustomTabsService}
+ * and a client side {@link PostMessageService}. Both services are bound to through
+ * {@link ServiceTestRule}, but {@link CustomTabsCallback#extraCallback} is used to link browser
+ * side actions.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PostMessageTest {
+ @Rule
+ public final ServiceTestRule mServiceRule;
+ @Rule
+ public final ActivityTestRule<TestActivity> mActivityTestRule;
+ private TestCustomTabsCallback mCallback;
+ private Context mContext;
+ private CustomTabsServiceConnection mCustomTabsServiceConnection;
+ private PostMessageServiceConnection mPostMessageServiceConnection;
+ private boolean mCustomTabsServiceConnected;
+ private boolean mPostMessageServiceConnected;
+ private CustomTabsSession mSession;
+
+ public PostMessageTest() {
+ mActivityTestRule = new ActivityTestRule<TestActivity>(TestActivity.class);
+ mServiceRule = new ServiceTestRule();
+ }
+
+
+ @Before
+ public void setup() {
+ // Bind to PostMessageService only after CustomTabsService sends the callback to do so. This
+ // callback is sent after requestPostMessageChannel is called.
+ mCallback = new TestCustomTabsCallback() {
+ @Override
+ public void extraCallback(String callbackName, Bundle args) {
+ if (TestCustomTabsService.CALLBACK_BIND_TO_POST_MESSAGE.equals(callbackName)) {
+ Intent postMessageServiceIntent = new Intent();
+ postMessageServiceIntent.setClassName(
+ mContext.getPackageName(), PostMessageService.class.getName());
+ try {
+ mServiceRule.bindService(postMessageServiceIntent,
+ mPostMessageServiceConnection, Context.BIND_AUTO_CREATE);
+ } catch (TimeoutException e) {
+ fail();
+ }
+ }
+ }
+ };
+ mContext = mActivityTestRule.getActivity();
+ mCustomTabsServiceConnection = new CustomTabsServiceConnection() {
+ @Override
+ public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) {
+ mCustomTabsServiceConnected = true;
+ mSession = client.newSession(mCallback);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ mCustomTabsServiceConnected = false;
+ }
+ };
+ mPostMessageServiceConnection = new PostMessageServiceConnection(
+ new CustomTabsSessionToken(mCallback.getStub())) {
+ @Override
+ public void onPostMessageServiceConnected() {
+ mPostMessageServiceConnected = true;
+ }
+
+ @Override
+ public void onPostMessageServiceDisconnected() {
+ mPostMessageServiceConnected = false;
+ }
+ };
+ Intent customTabsServiceIntent = new Intent();
+ customTabsServiceIntent.setClassName(
+ mContext.getPackageName(), TestCustomTabsService.class.getName());
+ try {
+ mServiceRule.bindService(customTabsServiceIntent,
+ mCustomTabsServiceConnection, Context.BIND_AUTO_CREATE);
+ } catch (TimeoutException e) {
+ fail();
+ }
+ }
+
+ @Test
+ public void testCustomTabsConnection() {
+ PollingCheck.waitFor(500, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mCustomTabsServiceConnected;
+ }
+ });
+ assertTrue(mCustomTabsServiceConnected);
+ assertTrue(mSession.requestPostMessageChannel(Uri.EMPTY));
+ assertEquals(CustomTabsService.RESULT_SUCCESS, mSession.postMessage("", null));
+ PollingCheck.waitFor(500, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return mPostMessageServiceConnected;
+ }
+ });
+ assertTrue(mPostMessageServiceConnected);
+ }
+}
diff --git a/customtabs/tests/src/android/support/customtabs/TestActivity.java b/customtabs/tests/src/android/support/customtabs/TestActivity.java
new file mode 100644
index 0000000..1d7941a
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/TestActivity.java
@@ -0,0 +1,36 @@
+/*
+ * 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.customtabs;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+
+/**
+ * Simple test activity for custom tabs testing.
+ */
+
+public class TestActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(new FrameLayout(this));
+ }
+}
diff --git a/customtabs/tests/src/android/support/customtabs/TestCustomTabsCallback.java b/customtabs/tests/src/android/support/customtabs/TestCustomTabsCallback.java
new file mode 100644
index 0000000..56b1817
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/TestCustomTabsCallback.java
@@ -0,0 +1,82 @@
+/*
+ * 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.customtabs;
+
+import android.os.Bundle;
+import android.os.RemoteException;
+
+import java.util.ArrayList;
+
+/**
+ * A test class to check the incoming messages through {@link CustomTabsCallback}.
+ */
+public class TestCustomTabsCallback extends CustomTabsCallback {
+ private boolean mOnMessageChannelReady;
+ private ArrayList<String> mMessageList = new ArrayList<>();
+ private ICustomTabsCallback.Stub mWrapper = new ICustomTabsCallback.Stub() {
+ @Override
+ public void onNavigationEvent(final int navigationEvent, final Bundle extras) {
+ TestCustomTabsCallback.this.onNavigationEvent(navigationEvent, extras);
+ }
+
+ @Override
+ public void extraCallback(final String callbackName, final Bundle args)
+ throws RemoteException {
+ TestCustomTabsCallback.this.extraCallback(callbackName, args);
+ }
+
+ @Override
+ public void onMessageChannelReady(final Bundle extras)
+ throws RemoteException {
+ TestCustomTabsCallback.this.onMessageChannelReady(extras);
+ }
+
+ @Override
+ public void onPostMessage(final String message, final Bundle extras)
+ throws RemoteException {
+ TestCustomTabsCallback.this.onPostMessage(message, extras);
+ }
+ };
+
+ /* package */ ICustomTabsCallback getStub() {
+ return mWrapper;
+ }
+
+ @Override
+ public void onMessageChannelReady(Bundle extras) {
+ mOnMessageChannelReady = true;
+ }
+
+ /**
+ * @return Whether the message channel is ready.
+ */
+ public boolean isMessageChannelReady() {
+ return mOnMessageChannelReady;
+ }
+
+ @Override
+ public void onPostMessage(String message, Bundle extras) {
+ mMessageList.add(message);
+ }
+
+ /**
+ * @return A list of messages that have been sent so far.
+ */
+ public ArrayList<String> getMessages() {
+ return mMessageList;
+ }
+}
diff --git a/customtabs/tests/src/android/support/customtabs/TestCustomTabsService.java b/customtabs/tests/src/android/support/customtabs/TestCustomTabsService.java
new file mode 100644
index 0000000..b5c5e86
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/TestCustomTabsService.java
@@ -0,0 +1,74 @@
+/*
+ * 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.customtabs;
+
+import android.net.Uri;
+import android.os.Bundle;
+
+import java.util.List;
+
+/**
+ * A test class that simulates how a {@link CustomTabsService} would behave.
+ */
+
+public class TestCustomTabsService extends CustomTabsService {
+ public static final String CALLBACK_BIND_TO_POST_MESSAGE = "BindToPostMessageService";
+ private boolean mPostMessageRequested;
+ private CustomTabsSessionToken mSession;
+
+ @Override
+ protected boolean warmup(long flags) {
+ return false;
+ }
+
+ @Override
+ protected boolean newSession(CustomTabsSessionToken sessionToken) {
+ mSession = sessionToken;
+ return true;
+ }
+
+ @Override
+ protected boolean mayLaunchUrl(CustomTabsSessionToken sessionToken,
+ Uri url, Bundle extras, List<Bundle> otherLikelyBundles) {
+ return false;
+ }
+
+ @Override
+ protected Bundle extraCommand(String commandName, Bundle args) {
+ return null;
+ }
+
+ @Override
+ protected boolean updateVisuals(CustomTabsSessionToken sessionToken, Bundle bundle) {
+ return false;
+ }
+
+ @Override
+ protected boolean requestPostMessageChannel(
+ CustomTabsSessionToken sessionToken, Uri postMessageOrigin) {
+ if (mSession == null) return false;
+ mPostMessageRequested = true;
+ mSession.getCallback().extraCallback(CALLBACK_BIND_TO_POST_MESSAGE, null);
+ return true;
+ }
+
+ @Override
+ protected int postMessage(CustomTabsSessionToken sessionToken, String message, Bundle extras) {
+ if (!mPostMessageRequested) return CustomTabsService.RESULT_FAILURE_DISALLOWED;
+ return CustomTabsService.RESULT_SUCCESS;
+ }
+}
diff --git a/design/src/android/support/design/widget/TabLayout.java b/design/src/android/support/design/widget/TabLayout.java
index b6f94a4..d3a8dd1 100755
--- a/design/src/android/support/design/widget/TabLayout.java
+++ b/design/src/android/support/design/widget/TabLayout.java
@@ -1084,17 +1084,7 @@
final int targetScrollX = calculateScrollXForTab(newPosition, 0);
if (startScrollX != targetScrollX) {
- if (mScrollAnimator == null) {
- mScrollAnimator = ViewUtils.createAnimator();
- mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
- mScrollAnimator.setDuration(ANIMATION_DURATION);
- mScrollAnimator.addUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimatorCompat animator) {
- scrollTo(animator.getAnimatedIntValue(), 0);
- }
- });
- }
+ ensureScrollAnimator();
mScrollAnimator.setIntValues(startScrollX, targetScrollX);
mScrollAnimator.start();
@@ -1104,6 +1094,25 @@
mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION);
}
+ private void ensureScrollAnimator() {
+ if (mScrollAnimator == null) {
+ mScrollAnimator = ViewUtils.createAnimator();
+ mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
+ mScrollAnimator.setDuration(ANIMATION_DURATION);
+ mScrollAnimator.addUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimatorCompat animator) {
+ scrollTo(animator.getAnimatedIntValue(), 0);
+ }
+ });
+ }
+ }
+
+ void setScrollAnimatorListener(ValueAnimatorCompat.AnimatorListener listener) {
+ ensureScrollAnimator();
+ mScrollAnimator.addListener(listener);
+ }
+
private void setSelectedTabView(int position) {
final int tabCount = mTabStrip.getChildCount();
if (position < tabCount) {
@@ -1177,10 +1186,14 @@
final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0;
final int nextWidth = nextChild != null ? nextChild.getWidth() : 0;
- return selectedChild.getLeft()
- + ((int) ((selectedWidth + nextWidth) * positionOffset * 0.5f))
- + (selectedChild.getWidth() / 2)
- - (getWidth() / 2);
+ // base scroll amount: places center of tab in center of parent
+ int scrollBase = selectedChild.getLeft() + (selectedWidth / 2) - (getWidth() / 2);
+ // offset amount: fraction of the distance between centers of tabs
+ int scrollOffset = (int) ((selectedWidth + nextWidth) * 0.5f * positionOffset);
+
+ return (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR)
+ ? scrollBase + scrollOffset
+ : scrollBase - scrollOffset;
}
return 0;
}
diff --git a/design/tests/res/layout/design_tabs_fixed_width.xml b/design/tests/res/layout/design_tabs_fixed_width.xml
new file mode 100644
index 0000000..752034f
--- /dev/null
+++ b/design/tests/res/layout/design_tabs_fixed_width.xml
@@ -0,0 +1,51 @@
+<?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.
+-->
+
+<!-- Width is fixed to test scrolling -->
+<android.support.design.widget.TabLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/tabs"
+ android:layout_width="160dp"
+ android:layout_height="wrap_content"
+ app:tabMode="scrollable">
+
+ <android.support.design.widget.TabItem
+ android:text="Tab 0" />
+
+ <android.support.design.widget.TabItem
+ android:text="Tab 1" />
+
+ <android.support.design.widget.TabItem
+ android:text="Tab 2" />
+
+ <android.support.design.widget.TabItem
+ android:text="Tab 3" />
+
+ <android.support.design.widget.TabItem
+ android:text="Tab 4" />
+
+ <android.support.design.widget.TabItem
+ android:text="Tab 5" />
+
+ <android.support.design.widget.TabItem
+ android:text="Tab 6" />
+
+ <android.support.design.widget.TabItem
+ android:text="Tab 7" />
+
+</android.support.design.widget.TabLayout>
diff --git a/design/tests/src/android/support/design/testutils/TabLayoutActions.java b/design/tests/src/android/support/design/testutils/TabLayoutActions.java
index 149b14f..7c17850 100644
--- a/design/tests/src/android/support/design/testutils/TabLayoutActions.java
+++ b/design/tests/src/android/support/design/testutils/TabLayoutActions.java
@@ -16,23 +16,17 @@
package android.support.design.testutils;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
+
import android.support.annotation.Nullable;
import android.support.design.widget.TabLayout;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
-import android.support.test.espresso.action.CoordinatesProvider;
-import android.support.test.espresso.action.GeneralClickAction;
-import android.support.test.espresso.action.Press;
-import android.support.test.espresso.action.Tap;
-import android.support.v4.view.PagerAdapter;
-import android.support.v4.view.PagerTitleStrip;
+import android.support.test.espresso.matcher.ViewMatchers;
import android.support.v4.view.ViewPager;
import android.view.View;
-import android.widget.TextView;
-import org.hamcrest.Matcher;
-import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
-import static android.support.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
+import org.hamcrest.Matcher;
public class TabLayoutActions {
/**
@@ -143,4 +137,30 @@
}
};
}
+
+ /**
+ * Calls <code>setScrollPosition(position, positionOffset, true)</code> on the
+ * <code>TabLayout</code>
+ */
+ public static ViewAction setScrollPosition(final int position, final float positionOffset) {
+ return new ViewAction() {
+
+ @Override
+ public Matcher<View> getConstraints() {
+ return ViewMatchers.isAssignableFrom(TabLayout.class);
+ }
+
+ @Override
+ public String getDescription() {
+ return "setScrollPosition(" + position + ", " + positionOffset + ", true)";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ TabLayout tabs = (TabLayout) view;
+ tabs.setScrollPosition(position, positionOffset, true);
+ uiController.loopMainThreadUntilIdle();
+ }
+ };
+ }
}
diff --git a/design/tests/src/android/support/design/widget/TabLayoutTest.java b/design/tests/src/android/support/design/widget/TabLayoutTest.java
index 539ab86..e9255fa 100755
--- a/design/tests/src/android/support/design/widget/TabLayoutTest.java
+++ b/design/tests/src/android/support/design/widget/TabLayoutTest.java
@@ -16,6 +16,12 @@
package android.support.design.widget;
+import static android.support.design.testutils.TabLayoutActions.selectTab;
+import static android.support.design.testutils.TabLayoutActions.setScrollPosition;
+import static android.support.design.testutils.TestUtilsActions.setLayoutDirection;
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -29,7 +35,12 @@
import android.support.design.test.R;
import android.support.test.annotation.UiThreadTest;
+import android.support.test.espresso.Espresso;
+import android.support.test.espresso.IdlingResource;
+import android.support.test.espresso.NoMatchingViewException;
+import android.support.test.espresso.ViewAssertion;
import android.support.test.filters.SmallTest;
+import android.support.v4.view.ViewCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.InflateException;
import android.view.LayoutInflater;
@@ -37,6 +48,8 @@
import org.junit.Test;
+import java.util.concurrent.atomic.AtomicInteger;
+
@SmallTest
public class TabLayoutTest extends BaseInstrumentationTestCase<AppCompatActivity> {
public TabLayoutTest() {
@@ -188,4 +201,105 @@
}
}
}
+
+ @Test
+ public void setScrollPositionLtr() throws Throwable {
+ testSetScrollPosition(true);
+ }
+
+ @Test
+ public void setScrollPositionRtl() throws Throwable {
+ testSetScrollPosition(false);
+ }
+
+ private void testSetScrollPosition(final boolean isLtr) throws Throwable {
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivityTestRule.getActivity().setContentView(R.layout.design_tabs_fixed_width);
+ }
+ });
+ final TabLayout tabs = (TabLayout) mActivityTestRule.getActivity().findViewById(R.id.tabs);
+ assertEquals(TabLayout.MODE_SCROLLABLE, tabs.getTabMode());
+
+ final TabLayoutScrollIdlingResource idler = new TabLayoutScrollIdlingResource(tabs);
+ Espresso.registerIdlingResources(idler);
+
+ // We're going to call setScrollPosition() incrementally, as if scrolling between one tab
+ // and the next. Use the middle tab for best results. The positionOffsets should be in the
+ // range [0, 1), so the final call will wrap to 0 but use the next tab's position.
+ final int middleTab = tabs.getTabCount() / 2;
+ final int[] positions = {middleTab, middleTab, middleTab, middleTab, middleTab + 1};
+ final float[] positionOffsets = {0f, .25f, .5f, .75f, 0f};
+
+ // Set layout direction
+ onView(withId(R.id.tabs)).perform(setLayoutDirection(
+ isLtr ? ViewCompat.LAYOUT_DIRECTION_LTR : ViewCompat.LAYOUT_DIRECTION_RTL));
+ // Make sure it's scrolled all the way to the start
+ onView(withId(R.id.tabs)).perform(selectTab(0));
+
+ // Perform a series of setScrollPosition() calls
+ final AtomicInteger lastScrollX = new AtomicInteger(tabs.getScrollX());
+ for (int i = 0; i < positions.length; i++) {
+ onView(withId(R.id.tabs))
+ .perform(setScrollPosition(positions[i], positionOffsets[i]))
+ .check(new ViewAssertion() {
+ @Override
+ public void check(View view, NoMatchingViewException notFoundException) {
+ if (view == null) {
+ throw notFoundException;
+ }
+ // Verify increasing or decreasing scroll X values
+ int sx = view.getScrollX();
+ assertTrue(isLtr ? sx > lastScrollX.get() : sx < lastScrollX.get());
+ lastScrollX.set(sx);
+ }
+ });
+ }
+
+ Espresso.unregisterIdlingResources(idler);
+ }
+
+ static class TabLayoutScrollIdlingResource implements IdlingResource {
+
+ private boolean mIsIdle = true;
+ private ResourceCallback mCallback;
+
+ TabLayoutScrollIdlingResource(final TabLayout tabLayout) {
+ tabLayout.setScrollAnimatorListener(new ValueAnimatorCompat.AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(ValueAnimatorCompat animator) {
+ setIdle(false);
+ }
+
+ @Override
+ public void onAnimationEnd(ValueAnimatorCompat animator) {
+ setIdle(true);
+ }
+ });
+ }
+
+ @Override
+ public String getName() {
+ return "TabLayoutScrollIdlingResource";
+ }
+
+ @Override
+ public boolean isIdleNow() {
+ return mIsIdle;
+ }
+
+ @Override
+ public void registerIdleTransitionCallback(ResourceCallback callback) {
+ mCallback = callback;
+ }
+
+ private void setIdle(boolean idle) {
+ boolean wasIdle = mIsIdle;
+ mIsIdle = idle;
+ if (mIsIdle && !wasIdle && mCallback != null) {
+ mCallback.onTransitionToIdle();
+ }
+ }
+ }
}
diff --git a/exifinterface/build.gradle b/exifinterface/build.gradle
index c761832..c65cf71 100644
--- a/exifinterface/build.gradle
+++ b/exifinterface/build.gradle
@@ -19,7 +19,6 @@
sourceSets {
main.manifest.srcFile 'AndroidManifest.xml'
main.java.srcDirs = ['src']
- main.res.srcDirs = ['res']
androidTest.setRoot('tests')
androidTest.java.srcDir 'tests/src'
diff --git a/exifinterface/src/android/support/media/ExifInterface.java b/exifinterface/src/android/support/media/ExifInterface.java
index fccab43..4830ecc 100644
--- a/exifinterface/src/android/support/media/ExifInterface.java
+++ b/exifinterface/src/android/support/media/ExifInterface.java
@@ -1850,11 +1850,31 @@
}
/**
- * Stores the latitude and longitude value in a float array. The first element is
- * the latitude, and the second element is the longitude. Returns false if the
- * Exif tags are not available.
+ * Stores the latitude and longitude value in a float array. The first element is the latitude,
+ * and the second element is the longitude. Returns false if the Exif tags are not available.
+ *
+ * @deprecated Use {@link #getLatLong()} instead.
*/
+ @Deprecated
public boolean getLatLong(float output[]) {
+ double[] latLong = getLatLong();
+ if (latLong == null) {
+ return false;
+ }
+
+ output[0] = (float) latLong[0];
+ output[1] = (float) latLong[1];
+ return true;
+ }
+
+ /**
+ * Gets the latitude and longitude values.
+ * <p>
+ * If there are valid latitude and longitude values in the image, this method returns a double
+ * array where the first element is the latitude and the second element is the longitude.
+ * Otherwise, it returns null.
+ */
+ public double[] getLatLong() {
String latValue = getAttribute(TAG_GPS_LATITUDE);
String latRef = getAttribute(TAG_GPS_LATITUDE_REF);
String lngValue = getAttribute(TAG_GPS_LONGITUDE);
@@ -1862,16 +1882,39 @@
if (latValue != null && latRef != null && lngValue != null && lngRef != null) {
try {
- output[0] = convertRationalLatLonToFloat(latValue, latRef);
- output[1] = convertRationalLatLonToFloat(lngValue, lngRef);
- return true;
+ double latitude = convertRationalLatLonToDouble(latValue, latRef);
+ double longitude = convertRationalLatLonToDouble(lngValue, lngRef);
+ return new double[] {latitude, longitude};
} catch (IllegalArgumentException e) {
Log.w(TAG, "Latitude/longitude values are not parseable. " +
String.format("latValue=%s, latRef=%s, lngValue=%s, lngRef=%s",
latValue, latRef, lngValue, lngRef));
}
}
- return false;
+ return null;
+ }
+
+ /**
+ * Sets the latitude and longitude values.
+ *
+ * @param latitude the decimal value of latitude. Must be a valid double value between -90.0 and
+ * 90.0.
+ * @param longitude the decimal value of longitude. Must be a valid double value between -180.0
+ * and 180.0.
+ * @throws IllegalArgumentException If {@code latitude} or {@code longitude} is outside the
+ * specified range.
+ */
+ public void setLatLong(double latitude, double longitude) {
+ if (latitude < -90.0 || latitude > 90.0 || Double.isNaN(latitude)) {
+ throw new IllegalArgumentException("Latitude value " + latitude + " is not valid.");
+ }
+ if (longitude < -180.0 || longitude > 180.0 || Double.isNaN(longitude)) {
+ throw new IllegalArgumentException("Longitude value " + longitude + " is not valid.");
+ }
+ setAttribute(TAG_GPS_LATITUDE_REF, latitude >= 0 ? "N" : "S");
+ setAttribute(TAG_GPS_LATITUDE, convertDecimalDegree(Math.abs(latitude)));
+ setAttribute(TAG_GPS_LONGITUDE_REF, longitude >= 0 ? "E" : "W");
+ setAttribute(TAG_GPS_LONGITUDE, convertDecimalDegree(Math.abs(longitude)));
}
/**
@@ -1953,7 +1996,7 @@
}
}
- private static float convertRationalLatLonToFloat(String rationalString, String ref) {
+ private static double convertRationalLatLonToDouble(String rationalString, String ref) {
try {
String [] parts = rationalString.split(",");
@@ -1972,15 +2015,26 @@
double result = degrees + (minutes / 60.0) + (seconds / 3600.0);
if ((ref.equals("S") || ref.equals("W"))) {
- return (float) -result;
+ return -result;
+ } else if (ref.equals("N") || ref.equals("E")) {
+ return result;
+ } else {
+ // Not valid
+ throw new IllegalArgumentException();
}
- return (float) result;
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
// Not valid
throw new IllegalArgumentException();
}
}
+ private String convertDecimalDegree(double decimalDegree) {
+ long degrees = (long) decimalDegree;
+ long minutes = (long) ((decimalDegree - degrees) * 60.0);
+ long seconds = Math.round((decimalDegree - degrees - minutes / 60.0) * 3600.0 * 1e7);
+ return degrees + "/1," + minutes + "/1," + seconds + "/10000000";
+ }
+
// Checks the type of image file
private int getMimeType(BufferedInputStream in) throws IOException {
in.mark(SIGNATURE_CHECK_SIZE);
diff --git a/exifinterface/tests/src/android/support/media/ExifInterfaceTest.java b/exifinterface/tests/src/android/support/media/ExifInterfaceTest.java
index 87fb950..ee9e38e 100644
--- a/exifinterface/tests/src/android/support/media/ExifInterfaceTest.java
+++ b/exifinterface/tests/src/android/support/media/ExifInterfaceTest.java
@@ -16,6 +16,13 @@
package android.support.media;
+import static android.support.test.InstrumentationRegistry.getContext;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.fail;
+
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.os.Environment;
@@ -40,12 +47,6 @@
import java.io.InputStream;
import java.io.OutputStream;
-import static android.support.test.InstrumentationRegistry.getContext;
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.Assert.assertNull;
-import static junit.framework.Assert.fail;
-
/**
* Test {@link ExifInterface}.
*/
@@ -53,7 +54,6 @@
public class ExifInterfaceTest {
private static final String TAG = ExifInterface.class.getSimpleName();
private static final boolean VERBOSE = false; // lots of logging
-
private static final double DIFFERENCE_TOLERANCE = .001;
private static final String EXIF_BYTE_ORDER_II_JPEG = "image_exif_byte_order_ii.jpg";
@@ -64,6 +64,20 @@
private static final String[] IMAGE_FILENAMES = new String[] {
EXIF_BYTE_ORDER_II_JPEG, EXIF_BYTE_ORDER_MM_JPEG, LG_G4_ISO_800_DNG};
+ private static final String TEST_TEMP_FILE_NAME = "testImage";
+ private static final double DELTA = 1e-8;
+ 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};
+ private static final double[] TEST_LONGITUDE_VALID_VALUES = new double[]
+ {0, -45, 90, -120, 180, 0.00000001, -179.99999999999, -58.57834236352};
+ private static final double[] TEST_LATITUDE_INVALID_VALUES = new double[]
+ {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 90.0000000001,
+ 263.34763236326, -1e5, 347.32525, -176.346347754};
+ 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 String[] EXIF_TAGS = {
ExifInterface.TAG_MAKE,
ExifInterface.TAG_MODEL,
@@ -235,6 +249,52 @@
}
}
+ @Test
+ @SmallTest
+ public void testSetLatLong_withValidValues() throws IOException {
+ for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) {
+ ExifInterface exif = createTestExifInterface();
+ exif.setLatLong(TEST_LATITUDE_VALID_VALUES[i], TEST_LONGITUDE_VALID_VALUES[i]);
+
+ double[] latLong = exif.getLatLong();
+ assertNotNull(latLong);
+ assertEquals(TEST_LATITUDE_VALID_VALUES[i], latLong[0], DELTA);
+ assertEquals(TEST_LONGITUDE_VALID_VALUES[i], latLong[1], DELTA);
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testSetLatLong_withInvalidLatitude() throws IOException {
+ for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) {
+ ExifInterface exif = createTestExifInterface();
+ try {
+ exif.setLatLong(TEST_LATITUDE_INVALID_VALUES[i], TEST_LONGITUDE_VALID_VALUES[i]);
+ fail();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ assertNull(exif.getLatLong());
+ assertLatLongValuesAreNotSet(exif);
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testSetLatLong_withInvalidLongitude() throws IOException {
+ for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) {
+ ExifInterface exif = createTestExifInterface();
+ try {
+ exif.setLatLong(TEST_LATITUDE_VALID_VALUES[i], TEST_LONGITUDE_INVALID_VALUES[i]);
+ fail();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ assertNull(exif.getLatLong());
+ assertLatLongValuesAreNotSet(exif);
+ }
+ }
+
private void printExifTagsAndValues(String fileName, ExifInterface exifInterface) {
// Prints thumbnail information.
if (exifInterface.hasThumbnail()) {
@@ -264,8 +324,8 @@
// Prints GPS information.
Log.v(TAG, fileName + " Altitude = " + exifInterface.getAltitude(.0));
- float[] latLong = new float[2];
- if (exifInterface.getLatLong(latLong)) {
+ double[] latLong = exifInterface.getLatLong();
+ if (latLong != null) {
Log.v(TAG, fileName + " Latitude = " + latLong[0]);
Log.v(TAG, fileName + " Longitude = " + latLong[1]);
} else {
@@ -318,8 +378,8 @@
}
// Checks GPS information.
- float[] latLong = new float[2];
- assertEquals(expectedValue.hasLatLong, exifInterface.getLatLong(latLong));
+ double[] latLong = exifInterface.getLatLong();
+ assertEquals(expectedValue.hasLatLong, latLong != null);
if (expectedValue.hasLatLong) {
assertEquals(expectedValue.latitude, latLong[0], DIFFERENCE_TOLERANCE);
assertEquals(expectedValue.longitude, latLong[1], DIFFERENCE_TOLERANCE);
@@ -443,4 +503,17 @@
}
return total;
}
+
+ private void assertLatLongValuesAreNotSet(ExifInterface exif) {
+ assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE));
+ assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF));
+ assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE));
+ assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF));
+ }
+
+ private ExifInterface createTestExifInterface() throws IOException {
+ File image = File.createTempFile(TEST_TEMP_FILE_NAME, ".jpg");
+ image.deleteOnExit();
+ return new ExifInterface(image.getAbsolutePath());
+ }
}
diff --git a/fragment/java/android/support/v4/app/BackStackRecord.java b/fragment/java/android/support/v4/app/BackStackRecord.java
index 326521a..923c366 100644
--- a/fragment/java/android/support/v4/app/BackStackRecord.java
+++ b/fragment/java/android/support/v4/app/BackStackRecord.java
@@ -762,8 +762,11 @@
/**
* Reverses the execution of the operations within this transaction. The Fragment states will
* only be modified if optimizations are not allowed.
+ *
+ * @param moveToState {@code true} if added fragments should be moved to their final state
+ * in unoptimized transactions
*/
- void executePopOps() {
+ void executePopOps(boolean moveToState) {
for (int opNum = mOps.size() - 1; opNum >= 0; opNum--) {
final Op op = mOps.get(opNum);
Fragment f = op.fragment;
@@ -800,7 +803,7 @@
mManager.moveFragmentToExpectedState(f);
}
}
- if (!mAllowOptimization) {
+ if (!mAllowOptimization && moveToState) {
mManager.moveToState(mManager.mCurState, true);
}
}
diff --git a/fragment/java/android/support/v4/app/FragmentManager.java b/fragment/java/android/support/v4/app/FragmentManager.java
index 91d1d91..d0eef59 100644
--- a/fragment/java/android/support/v4/app/FragmentManager.java
+++ b/fragment/java/android/support/v4/app/FragmentManager.java
@@ -2230,7 +2230,7 @@
if (isPop) {
record.executeOps();
} else {
- record.executePopOps();
+ record.executePopOps(false);
}
// move to the end
@@ -2349,7 +2349,10 @@
final boolean isPop = isRecordPop.get(i);
if (isPop) {
record.bumpBackStackNesting(-1);
- record.executePopOps();
+ // Only execute the add operations at the end of
+ // all transactions.
+ boolean moveToState = i == (endIndex - 1);
+ record.executePopOps(moveToState);
} else {
record.bumpBackStackNesting(1);
record.executeOps();
diff --git a/fragment/tests/java/android/support/v4/app/FragmentViewTests.java b/fragment/tests/java/android/support/v4/app/FragmentViewTests.java
index 211ec35..ace24e9 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentViewTests.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentViewTests.java
@@ -1036,6 +1036,10 @@
FragmentTestUtil.executePendingTransactions(mActivityRule);
+ assertEquals(1, fragment1.onCreateViewCount);
+ assertEquals(1, fragment2.onCreateViewCount);
+ assertEquals(1, fragment3.onCreateViewCount);
+
FragmentTestUtil.popBackStackImmediate(mActivityRule, "two",
FragmentManager.POP_BACK_STACK_INCLUSIVE);
@@ -1043,6 +1047,10 @@
mActivityRule.getActivity().findViewById(R.id.fragmentContainer);
FragmentTestUtil.assertChildren(container, fragment1);
+
+ assertEquals(2, fragment1.onCreateViewCount);
+ assertEquals(1, fragment2.onCreateViewCount);
+ assertEquals(1, fragment3.onCreateViewCount);
}
private View findViewById(int viewId) {
@@ -1091,10 +1099,13 @@
}
public static class SimpleViewFragment extends Fragment {
+ public int onCreateViewCount;
+
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
+ onCreateViewCount++;
return inflater.inflate(R.layout.fragment_a, container, false);
}
}
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserCompat.java b/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
index c2fa6ab..243f016 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
@@ -121,7 +121,9 @@
*/
public MediaBrowserCompat(Context context, ComponentName serviceComponent,
ConnectionCallback callback, Bundle rootHints) {
- if (Build.VERSION.SDK_INT >= 24 || BuildCompat.isAtLeastN()) {
+ // To workaround an issue of {@link #unsubscribe(String, SubscriptionCallback)} on API 24
+ // and 25 devices, use the support library version of implementation on those devices.
+ if (Build.VERSION.SDK_INT >= 26 || BuildCompat.isAtLeastO()) {
mImpl = new MediaBrowserImplApi24(context, serviceComponent, callback, rootHints);
} else if (Build.VERSION.SDK_INT >= 23) {
mImpl = new MediaBrowserImplApi23(context, serviceComponent, callback, rootHints);
@@ -573,7 +575,7 @@
WeakReference<Subscription> mSubscriptionRef;
public SubscriptionCallback() {
- if (Build.VERSION.SDK_INT >= 24 || BuildCompat.isAtLeastN()) {
+ if (Build.VERSION.SDK_INT >= 26 || BuildCompat.isAtLeastO()) {
mSubscriptionCallbackObj =
MediaBrowserCompatApi24.createSubscriptionCallback(new StubApi24());
mToken = null;
@@ -1346,9 +1348,9 @@
public MediaBrowserImplApi21(Context context, ComponentName serviceComponent,
ConnectionCallback callback, Bundle rootHints) {
- // Do not send the client version for API 25 and higher, since we don't need to use
- // EXTRA_MESSENGER_BINDER for API 24 and higher.
- if (Build.VERSION.SDK_INT < 25) {
+ // Do not send the client version for API 26 and higher, since we don't need to use
+ // EXTRA_MESSENGER_BINDER for API 26 and higher.
+ if (Build.VERSION.SDK_INT <= 25) {
if (rootHints == null) {
rootHints = new Bundle();
}
@@ -1422,6 +1424,8 @@
sub.putCallback(copiedOptions, callback);
if (mServiceBinderWrapper == null) {
+ // TODO: When MediaBrowser is connected to framework's MediaBrowserService,
+ // subscribe with options won't work properly.
MediaBrowserCompatApi21.subscribe(
mBrowserObj, parentId, callback.mSubscriptionCallbackObj);
} else {
@@ -1625,6 +1629,7 @@
}
}
+ // TODO: Rename to MediaBrowserImplApi26 once O is released
static class MediaBrowserImplApi24 extends MediaBrowserImplApi23 {
public MediaBrowserImplApi24(Context context, ComponentName serviceComponent,
ConnectionCallback callback, Bundle rootHints) {
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java b/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
index 77a6e19..22c3d55 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
@@ -355,6 +355,7 @@
}
}
+ // TODO: Rename to MediaBrowserServiceImplApi26 once O is released
class MediaBrowserServiceImplApi24 extends MediaBrowserServiceImplApi23 implements
MediaBrowserServiceCompatApi24.ServiceCompatProxy {
@Override
@@ -786,7 +787,7 @@
@Override
public void onCreate() {
super.onCreate();
- if (Build.VERSION.SDK_INT >= 24 || BuildCompat.isAtLeastN()) {
+ if (Build.VERSION.SDK_INT >= 26 || BuildCompat.isAtLeastO()) {
mImpl = new MediaBrowserServiceImplApi24();
} else if (Build.VERSION.SDK_INT >= 23) {
mImpl = new MediaBrowserServiceImplApi23();
diff --git a/media-compat/java/android/support/v4/media/TransportMediator.java b/media-compat/java/android/support/v4/media/TransportMediator.java
index 177f6aa..ec3baec 100644
--- a/media-compat/java/android/support/v4/media/TransportMediator.java
+++ b/media-compat/java/android/support/v4/media/TransportMediator.java
@@ -83,28 +83,28 @@
public static final int KEYCODE_MEDIA_RECORD = 130;
/** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PREVIOUS
- * RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS */
+ * RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS} */
public final static int FLAG_KEY_MEDIA_PREVIOUS = 1 << 0;
/** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_REWIND
- * RemoteControlClient.FLAG_KEY_MEDIA_REWIND */
+ * RemoteControlClient.FLAG_KEY_MEDIA_REWIND} */
public final static int FLAG_KEY_MEDIA_REWIND = 1 << 1;
/** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY
- * RemoteControlClient.FLAG_KEY_MEDIA_PLAY */
+ * RemoteControlClient.FLAG_KEY_MEDIA_PLAY} */
public final static int FLAG_KEY_MEDIA_PLAY = 1 << 2;
/** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY_PAUSE
- * RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE */
+ * RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE} */
public final static int FLAG_KEY_MEDIA_PLAY_PAUSE = 1 << 3;
/** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PAUSE
- * RemoteControlClient.FLAG_KEY_MEDIA_PAUSE */
+ * RemoteControlClient.FLAG_KEY_MEDIA_PAUSE} */
public final static int FLAG_KEY_MEDIA_PAUSE = 1 << 4;
/** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_STOP
- * RemoteControlClient.FLAG_KEY_MEDIA_STOP */
+ * RemoteControlClient.FLAG_KEY_MEDIA_STOP} */
public final static int FLAG_KEY_MEDIA_STOP = 1 << 5;
/** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_FAST_FORWARD
- * RemoteControlClient.FLAG_KEY_MEDIA_FAST_FORWARD */
+ * RemoteControlClient.FLAG_KEY_MEDIA_FAST_FORWARD} */
public final static int FLAG_KEY_MEDIA_FAST_FORWARD = 1 << 6;
/** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_NEXT
- * RemoteControlClient.FLAG_KEY_MEDIA_NEXT */
+ * RemoteControlClient.FLAG_KEY_MEDIA_NEXT} */
public final static int FLAG_KEY_MEDIA_NEXT = 1 << 7;
static boolean isMediaKey(int keyCode) {
diff --git a/media-compat/java/android/support/v4/media/session/IMediaControllerCallback.aidl b/media-compat/java/android/support/v4/media/session/IMediaControllerCallback.aidl
index d905350..d1d143d 100644
--- a/media-compat/java/android/support/v4/media/session/IMediaControllerCallback.aidl
+++ b/media-compat/java/android/support/v4/media/session/IMediaControllerCallback.aidl
@@ -37,4 +37,6 @@
void onQueueTitleChanged(CharSequence title);
void onExtrasChanged(in Bundle extras);
void onVolumeInfoChanged(in ParcelableVolumeInfo info);
+ void onRepeatModeChanged(int repeatMode);
+ void onShuffleModeChanged(boolean enabled);
}
diff --git a/media-compat/java/android/support/v4/media/session/IMediaSession.aidl b/media-compat/java/android/support/v4/media/session/IMediaSession.aidl
index c7705e8..4f2e38a 100644
--- a/media-compat/java/android/support/v4/media/session/IMediaSession.aidl
+++ b/media-compat/java/android/support/v4/media/session/IMediaSession.aidl
@@ -52,6 +52,8 @@
CharSequence getQueueTitle() = 29;
Bundle getExtras() = 30;
int getRatingType() = 31;
+ int getRepeatMode() = 36;
+ boolean isShuffleModeEnabled() = 37;
// These commands are for the TransportControls
void prepare() = 32;
@@ -71,5 +73,7 @@
void rewind() = 22;
void seekTo(long pos) = 23;
void rate(in RatingCompat rating) = 24;
+ void setRepeatMode(int repeatMode) = 38;
+ void setShuffleModeEnabled(boolean shuffleMode) = 39;
void sendCustomAction(String action, in Bundle args) = 25;
}
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 74658a8..3e0bf4d 100644
--- a/media-compat/java/android/support/v4/media/session/MediaControllerCompat.java
+++ b/media-compat/java/android/support/v4/media/session/MediaControllerCompat.java
@@ -269,6 +269,25 @@
}
/**
+ * Get the repeat mode for this session.
+ *
+ * @return The latest repeat mode set to the session, or
+ * {@link PlaybackStateCompat#REPEAT_MODE_NONE} if not set.
+ */
+ public int getRepeatMode() {
+ return mImpl.getRepeatMode();
+ }
+
+ /**
+ * Return 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.
+ */
+ public boolean isShuffleModeEnabled() {
+ return mImpl.isShuffleModeEnabled();
+ }
+
+ /**
* Get the flags for this session. Flags are defined in
* {@link MediaSessionCompat}.
*
@@ -510,6 +529,25 @@
public void onAudioInfoChanged(PlaybackInfo info) {
}
+ /**
+ * Override to handle changes to the repeat mode.
+ *
+ * @param repeatMode The repeat mode. It should be one of followings:
+ * {@link PlaybackStateCompat#REPEAT_MODE_NONE},
+ * {@link PlaybackStateCompat#REPEAT_MODE_ONE},
+ * {@link PlaybackStateCompat#REPEAT_MODE_ALL}
+ */
+ public void onRepeatModeChanged(@PlaybackStateCompat.RepeatMode int repeatMode) {
+ }
+
+ /**
+ * Override to handle changes to the shuffle mode.
+ *
+ * @param enabled {@code true} if the shuffle mode is enabled, {@code false} otherwise.
+ */
+ public void onShuffleModeChanged(boolean enabled) {
+ }
+
@Override
public void binderDied() {
onSessionDestroyed();
@@ -614,6 +652,16 @@
}
@Override
+ public void onRepeatModeChanged(int repeatMode) throws RemoteException {
+ mHandler.post(MessageHandler.MSG_UPDATE_REPEAT_MODE, repeatMode, null);
+ }
+
+ @Override
+ public void onShuffleModeChanged(boolean enabled) throws RemoteException {
+ mHandler.post(MessageHandler.MSG_UPDATE_SHUFFLE_MODE, enabled, null);
+ }
+
+ @Override
public void onExtrasChanged(Bundle extras) throws RemoteException {
mHandler.post(MessageHandler.MSG_UPDATE_EXTRAS, extras, null);
}
@@ -638,6 +686,8 @@
private static final int MSG_UPDATE_QUEUE_TITLE = 6;
private static final int MSG_UPDATE_EXTRAS = 7;
private static final int MSG_DESTROYED = 8;
+ private static final int MSG_UPDATE_REPEAT_MODE = 9;
+ private static final int MSG_UPDATE_SHUFFLE_MODE = 10;
public MessageHandler(Looper looper) {
super(looper);
@@ -664,6 +714,12 @@
case MSG_UPDATE_QUEUE_TITLE:
onQueueTitleChanged((CharSequence) msg.obj);
break;
+ case MSG_UPDATE_REPEAT_MODE:
+ onRepeatModeChanged((int) msg.obj);
+ break;
+ case MSG_UPDATE_SHUFFLE_MODE:
+ onShuffleModeChanged((boolean) msg.obj);
+ break;
case MSG_UPDATE_EXTRAS:
onExtrasChanged((Bundle) msg.obj);
break;
@@ -839,6 +895,23 @@
public abstract void setRating(RatingCompat rating);
/**
+ * Set the repeat mode for this session.
+ *
+ * @param repeatMode The repeat mode. Must be one of the followings:
+ * {@link PlaybackStateCompat#REPEAT_MODE_NONE},
+ * {@link PlaybackStateCompat#REPEAT_MODE_ONE},
+ * {@link PlaybackStateCompat#REPEAT_MODE_ALL}
+ */
+ public abstract void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode);
+
+ /**
+ * Set the shuffle mode for this session.
+ *
+ * @param enabled {@code true} to enable the shuffle mode, {@code false} to disable.
+ */
+ public abstract void setShuffleModeEnabled(boolean enabled);
+
+ /**
* Send a custom action for the {@link MediaSessionCompat} to perform.
*
* @param customAction The action to perform.
@@ -963,6 +1036,8 @@
CharSequence getQueueTitle();
Bundle getExtras();
int getRatingType();
+ int getRepeatMode();
+ boolean isShuffleModeEnabled();
long getFlags();
PlaybackInfo getPlaybackInfo();
PendingIntent getSessionActivity();
@@ -1099,6 +1174,26 @@
}
@Override
+ public int getRepeatMode() {
+ try {
+ return mBinder.getRepeatMode();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getRepeatMode. " + e);
+ }
+ return 0;
+ }
+
+ @Override
+ public boolean isShuffleModeEnabled() {
+ try {
+ return mBinder.isShuffleModeEnabled();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in isShuffleModeEnabled. " + e);
+ }
+ return false;
+ }
+
+ @Override
public long getFlags() {
try {
return mBinder.getFlags();
@@ -1336,6 +1431,24 @@
}
@Override
+ public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+ try {
+ mBinder.setRepeatMode(repeatMode);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in setRepeatMode. " + e);
+ }
+ }
+
+ @Override
+ public void setShuffleModeEnabled(boolean enabled) {
+ try {
+ mBinder.setShuffleModeEnabled(enabled);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in setShuffleModeEnabled. " + e);
+ }
+ }
+
+ @Override
public void sendCustomAction(CustomAction customAction, Bundle args) {
sendCustomAction(customAction.getAction(), args);
}
@@ -1476,6 +1589,30 @@
}
@Override
+ public int getRepeatMode() {
+ if (mExtraBinder != null) {
+ try {
+ return mExtraBinder.getRepeatMode();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getRepeatMode. " + e);
+ }
+ }
+ return PlaybackStateCompat.REPEAT_MODE_NONE;
+ }
+
+ @Override
+ public boolean isShuffleModeEnabled() {
+ if (mExtraBinder != null) {
+ try {
+ return mExtraBinder.isShuffleModeEnabled();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in isShuffleModeEnabled. " + e);
+ }
+ }
+ return false;
+ }
+
+ @Override
public long getFlags() {
return MediaControllerCompatApi21.getFlags(mControllerObj);
}
@@ -1619,6 +1756,26 @@
}
@Override
+ public void onRepeatModeChanged(final int repeatMode) throws RemoteException {
+ mCallback.mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onRepeatModeChanged(repeatMode);
+ }
+ });
+ }
+
+ @Override
+ public void onShuffleModeChanged(final boolean enabled) throws RemoteException {
+ mCallback.mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onShuffleModeChanged(enabled);
+ }
+ });
+ }
+
+ @Override
public void onExtrasChanged(Bundle extras) throws RemoteException {
// Will not be called.
throw new AssertionError();
@@ -1715,6 +1872,20 @@
}
@Override
+ public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+ Bundle bundle = new Bundle();
+ bundle.putInt(MediaSessionCompat.ACTION_ARGUMENT_REPEAT_MODE, repeatMode);
+ sendCustomAction(MediaSessionCompat.ACTION_SET_REPEAT_MODE, bundle);
+ }
+
+ @Override
+ public void setShuffleModeEnabled(boolean enabled) {
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(MediaSessionCompat.ACTION_ARGUMENT_SHUFFLE_MODE_ENABLED, enabled);
+ sendCustomAction(MediaSessionCompat.ACTION_SET_SHUFFLE_MODE_ENABLED, bundle);
+ }
+
+ @Override
public void playFromMediaId(String mediaId, Bundle extras) {
MediaControllerCompatApi21.TransportControls.playFromMediaId(mControlsObj, mediaId,
extras);
@@ -1833,5 +2004,4 @@
MediaControllerCompatApi24.TransportControls.prepareFromUri(mControlsObj, uri, extras);
}
}
-
}
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 53e6c70..c560c1b 100644
--- a/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
+++ b/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
@@ -140,6 +140,18 @@
"android.support.v4.media.session.action.PREPARE_FROM_URI";
/**
+ * Custom action to invoke setRepeatMode() for the forward compatibility.
+ */
+ static final String ACTION_SET_REPEAT_MODE =
+ "android.support.v4.media.session.action.SET_REPEAT_MODE";
+
+ /**
+ * Custom action to invoke setShuffleModeEnabled() for the forward compatibility.
+ */
+ static final String ACTION_SET_SHUFFLE_MODE_ENABLED =
+ "android.support.v4.media.session.action.SET_SHUFFLE_MODE_ENABLED";
+
+ /**
* Argument for use with {@link #ACTION_PREPARE_FROM_MEDIA_ID} indicating media id to play.
*/
static final String ACTION_ARGUMENT_MEDIA_ID =
@@ -164,6 +176,19 @@
static final String ACTION_ARGUMENT_EXTRAS =
"android.support.v4.media.session.action.ARGUMENT_EXTRAS";
+ /**
+ * Argument for use with {@link #ACTION_SET_REPEAT_MODE} indicating repeat mode.
+ */
+ static final String ACTION_ARGUMENT_REPEAT_MODE =
+ "android.support.v4.media.session.action.ARGUMENT_REPEAT_MODE";
+
+ /**
+ * Argument for use with {@link #ACTION_SET_SHUFFLE_MODE_ENABLED} indicating that shuffle mode
+ * is enabled.
+ */
+ static final String ACTION_ARGUMENT_SHUFFLE_MODE_ENABLED =
+ "android.support.v4.media.session.action.ARGUMENT_SHUFFLE_MODE_ENABLED";
+
static final String EXTRA_BINDER = "android.support.v4.media.session.EXTRA_BINDER";
// Maximum size of the bitmap in dp.
@@ -238,6 +263,8 @@
if (android.os.Build.VERSION.SDK_INT >= 21) {
mImpl = new MediaSessionImplApi21(context, tag);
mImpl.setMediaButtonReceiver(mbrIntent);
+ // Set default callback to respond to controllers' extra binder requests.
+ setCallback(new Callback() {});
} else {
mImpl = new MediaSessionImplBase(context, tag, mbrComponent, mbrIntent);
}
@@ -501,6 +528,33 @@
}
/**
+ * Set 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}.
+ *
+ * @param repeatMode The repeat mode. Must be one of the followings:
+ * {@link PlaybackStateCompat#REPEAT_MODE_NONE},
+ * {@link PlaybackStateCompat#REPEAT_MODE_ONE},
+ * {@link PlaybackStateCompat#REPEAT_MODE_ALL}
+ */
+ public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+ mImpl.setRepeatMode(repeatMode);
+ }
+
+ /**
+ * Set the shuffle mode for this session.
+ * <p>
+ * Note that if this method is not called before,
+ * {@link MediaControllerCompat#isShuffleModeEnabled} will return {@code false}.
+ *
+ * @param enabled {@code true} to enable the shuffle mode, {@code false} to disable.
+ */
+ public void setShuffleModeEnabled(boolean enabled) {
+ mImpl.setShuffleModeEnabled(enabled);
+ }
+
+ /**
* Set 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
@@ -787,6 +841,33 @@
}
/**
+ * Override to handle the setting of the repeat mode.
+ * <p>
+ * You should call {@link #setRepeatMode} before end of this method in order to notify
+ * the change to the {@link MediaControllerCompat}, or
+ * {@link MediaControllerCompat#getRepeatMode} could return an invalid value.
+ *
+ * @param repeatMode The repeat mode which is one of followings:
+ * {@link PlaybackStateCompat#REPEAT_MODE_NONE},
+ * {@link PlaybackStateCompat#REPEAT_MODE_ONE},
+ * {@link PlaybackStateCompat#REPEAT_MODE_ALL}
+ */
+ public void onSetRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+ }
+
+ /**
+ * Override to handle the setting of the shuffle mode.
+ * <p>
+ * You should call {@link #setShuffleModeEnabled} before the end of this method in order to
+ * notify the change to the {@link MediaControllerCompat}, or
+ * {@link MediaControllerCompat#isShuffleModeEnabled} could return an invalid value.
+ *
+ * @param enabled true when the shuffle mode is enabled, false otherwise.
+ */
+ public void onSetShuffleModeEnabled(boolean enabled) {
+ }
+
+ /**
* Called when a {@link MediaControllerCompat} wants a
* {@link PlaybackStateCompat.CustomAction} to be performed.
*
@@ -902,6 +983,12 @@
Uri uri = extras.getParcelable(ACTION_ARGUMENT_URI);
Bundle bundle = extras.getBundle(ACTION_ARGUMENT_EXTRAS);
Callback.this.onPrepareFromUri(uri, bundle);
+ } else if (action.equals(ACTION_SET_REPEAT_MODE)) {
+ int repeatMode = extras.getInt(ACTION_ARGUMENT_REPEAT_MODE);
+ Callback.this.onSetRepeatMode(repeatMode);
+ } else if (action.equals(ACTION_SET_SHUFFLE_MODE_ENABLED)) {
+ boolean enabled = extras.getBoolean(ACTION_ARGUMENT_SHUFFLE_MODE_ENABLED);
+ Callback.this.onSetShuffleModeEnabled(enabled);
} else {
Callback.this.onCustomAction(action, extras);
}
@@ -1281,6 +1368,8 @@
void setQueueTitle(CharSequence title);
void setRatingType(@RatingCompat.Style int type);
+ void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode);
+ void setShuffleModeEnabled(boolean enabled);
void setExtras(Bundle extras);
Object getMediaSession();
@@ -1320,6 +1409,8 @@
List<QueueItem> mQueue;
CharSequence mQueueTitle;
@RatingCompat.Style int mRatingType;
+ @PlaybackStateCompat.RepeatMode int mRepeatMode;
+ boolean mShuffleModeEnabled;
Bundle mExtras;
int mVolumeType;
@@ -1607,6 +1698,22 @@
}
@Override
+ public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+ if (mRepeatMode != repeatMode) {
+ mRepeatMode = repeatMode;
+ sendRepeatMode(repeatMode);
+ }
+ }
+
+ @Override
+ public void setShuffleModeEnabled(boolean enabled) {
+ if (mShuffleModeEnabled != enabled) {
+ mShuffleModeEnabled = enabled;
+ sendShuffleModeEnabled(enabled);
+ }
+ }
+
+ @Override
public void setExtras(Bundle extras) {
mExtras = extras;
sendExtras(extras);
@@ -1825,6 +1932,30 @@
mControllerCallbacks.finishBroadcast();
}
+ private void sendRepeatMode(int repeatMode) {
+ int size = mControllerCallbacks.beginBroadcast();
+ for (int i = size - 1; i >= 0; i--) {
+ IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i);
+ try {
+ cb.onRepeatModeChanged(repeatMode);
+ } catch (RemoteException e) {
+ }
+ }
+ mControllerCallbacks.finishBroadcast();
+ }
+
+ private void sendShuffleModeEnabled(boolean enabled) {
+ int size = mControllerCallbacks.beginBroadcast();
+ for (int i = size - 1; i >= 0; i--) {
+ IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i);
+ try {
+ cb.onShuffleModeChanged(enabled);
+ } catch (RemoteException e) {
+ }
+ }
+ mControllerCallbacks.finishBroadcast();
+ }
+
private void sendExtras(Bundle extras) {
int size = mControllerCallbacks.beginBroadcast();
for (int i = size - 1; i >= 0; i--) {
@@ -2021,6 +2152,16 @@
}
@Override
+ public void setRepeatMode(int repeatMode) throws RemoteException {
+ postToHandler(MessageHandler.MSG_SET_REPEAT_MODE, repeatMode);
+ }
+
+ @Override
+ public void setShuffleModeEnabled(boolean enabled) throws RemoteException {
+ postToHandler(MessageHandler.MSG_SET_SHUFFLE_MODE_ENABLED, enabled);
+ }
+
+ @Override
public void sendCustomAction(String action, Bundle args)
throws RemoteException {
postToHandler(MessageHandler.MSG_CUSTOM_ACTION, action, args);
@@ -2062,6 +2203,17 @@
}
@Override
+ @PlaybackStateCompat.RepeatMode
+ public int getRepeatMode() {
+ return mRepeatMode;
+ }
+
+ @Override
+ public boolean isShuffleModeEnabled() {
+ return mShuffleModeEnabled;
+ }
+
+ @Override
public boolean isTransportControlEnabled() {
return (mFlags & FLAG_HANDLES_TRANSPORT_CONTROLS) != 0;
}
@@ -2103,6 +2255,8 @@
private static final int MSG_CUSTOM_ACTION = 20;
private static final int MSG_MEDIA_BUTTON = 21;
private static final int MSG_SET_VOLUME = 22;
+ private static final int MSG_SET_REPEAT_MODE = 23;
+ private static final int MSG_SET_SHUFFLE_MODE_ENABLED = 24;
// KeyEvent constants only available on API 11+
private static final int KEYCODE_MEDIA_PAUSE = 127;
@@ -2210,6 +2364,12 @@
case MSG_SET_VOLUME:
setVolumeTo((int) msg.obj, 0);
break;
+ case MSG_SET_REPEAT_MODE:
+ cb.onSetRepeatMode((int) msg.obj);
+ break;
+ case MSG_SET_SHUFFLE_MODE_ENABLED:
+ cb.onSetShuffleModeEnabled((boolean) msg.obj);
+ break;
}
}
@@ -2286,6 +2446,8 @@
private PlaybackStateCompat mPlaybackState;
@RatingCompat.Style int mRatingType;
+ @PlaybackStateCompat.RepeatMode int mRepeatMode;
+ boolean mShuffleModeEnabled;
public MediaSessionImplApi21(Context context, String tag) {
mSessionObj = MediaSessionCompatApi21.createSession(context, tag);
@@ -2420,6 +2582,38 @@
}
@Override
+ public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+ if (mRepeatMode != repeatMode) {
+ mRepeatMode = repeatMode;
+ int size = mExtraControllerCallbacks.beginBroadcast();
+ for (int i = size - 1; i >= 0; i--) {
+ IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i);
+ try {
+ cb.onRepeatModeChanged(repeatMode);
+ } catch (RemoteException e) {
+ }
+ }
+ mExtraControllerCallbacks.finishBroadcast();
+ }
+ }
+
+ @Override
+ public void setShuffleModeEnabled(boolean enabled) {
+ if (mShuffleModeEnabled != enabled) {
+ mShuffleModeEnabled = enabled;
+ int size = mExtraControllerCallbacks.beginBroadcast();
+ for (int i = size - 1; i >= 0; i--) {
+ IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i);
+ try {
+ cb.onShuffleModeChanged(enabled);
+ } catch (RemoteException e) {
+ }
+ }
+ mExtraControllerCallbacks.finishBroadcast();
+ }
+ }
+
+ @Override
public void setExtras(Bundle extras) {
MediaSessionCompatApi21.setExtras(mSessionObj, extras);
}
@@ -2621,6 +2815,18 @@
}
@Override
+ public void setRepeatMode(int repeatMode) throws RemoteException {
+ // Will not be called.
+ throw new AssertionError();
+ }
+
+ @Override
+ public void setShuffleModeEnabled(boolean enabled) throws RemoteException {
+ // Will not be called.
+ throw new AssertionError();
+ }
+
+ @Override
public void sendCustomAction(String action, Bundle args) throws RemoteException {
// Will not be called.
throw new AssertionError();
@@ -2662,6 +2868,17 @@
}
@Override
+ @PlaybackStateCompat.RepeatMode
+ public int getRepeatMode() {
+ return mRepeatMode;
+ }
+
+ @Override
+ public boolean isShuffleModeEnabled() {
+ return mShuffleModeEnabled;
+ }
+
+ @Override
public boolean isTransportControlEnabled() {
// Will not be called.
throw new AssertionError();
diff --git a/media-compat/java/android/support/v4/media/session/PlaybackStateCompat.java b/media-compat/java/android/support/v4/media/session/PlaybackStateCompat.java
index 0321fd7..7b1ad31 100644
--- a/media-compat/java/android/support/v4/media/session/PlaybackStateCompat.java
+++ b/media-compat/java/android/support/v4/media/session/PlaybackStateCompat.java
@@ -49,7 +49,8 @@
ACTION_SKIP_TO_PREVIOUS, ACTION_SKIP_TO_NEXT, ACTION_FAST_FORWARD, ACTION_SET_RATING,
ACTION_SEEK_TO, ACTION_PLAY_PAUSE, ACTION_PLAY_FROM_MEDIA_ID, ACTION_PLAY_FROM_SEARCH,
ACTION_SKIP_TO_QUEUE_ITEM, ACTION_PLAY_FROM_URI, ACTION_PREPARE,
- ACTION_PREPARE_FROM_MEDIA_ID, ACTION_PREPARE_FROM_SEARCH, ACTION_PREPARE_FROM_URI})
+ ACTION_PREPARE_FROM_MEDIA_ID, ACTION_PREPARE_FROM_SEARCH, ACTION_PREPARE_FROM_URI,
+ ACTION_SET_REPEAT_MODE, ACTION_SET_SHUFFLE_MODE_ENABLED})
@Retention(RetentionPolicy.SOURCE)
public @interface Actions {}
@@ -189,6 +190,20 @@
public static final long ACTION_PREPARE_FROM_URI = 1 << 17;
/**
+ * Indicates this session supports the set repeat mode command.
+ *
+ * @see Builder#setActions(long)
+ */
+ public static final long ACTION_SET_REPEAT_MODE = 1 << 18;
+
+ /**
+ * Indicates this session supports the set shuffle mode enabled command.
+ *
+ * @see Builder#setActions(long)
+ */
+ public static final long ACTION_SET_SHUFFLE_MODE_ENABLED = 1 << 19;
+
+ /**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@@ -386,6 +401,31 @@
*/
public static final int ERROR_CODE_END_OF_QUEUE = 11;
+ /**
+ * @hide
+ */
+ @IntDef({REPEAT_MODE_NONE, REPEAT_MODE_ONE, REPEAT_MODE_ALL})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RepeatMode {}
+
+ /**
+ * Use this value with {@link MediaControllerCompat.TransportControls#setRepeatMode}
+ * to indicate that the playback will be stopped at the end of the playing media list.
+ */
+ public static final int REPEAT_MODE_NONE = 0;
+
+ /**
+ * Use this value with {@link MediaControllerCompat.TransportControls#setRepeatMode}
+ * to indicate that the playback of the current playing media item will be repeated.
+ */
+ public static final int REPEAT_MODE_ONE = 1;
+
+ /**
+ * Use this value with {@link MediaControllerCompat.TransportControls#setRepeatMode}
+ * to indicate that the playback of the playing media list will be repeated.
+ */
+ public static final int REPEAT_MODE_ALL = 2;
+
// KeyEvent constants only available on API 11+
private static final int KEYCODE_MEDIA_PAUSE = 127;
private static final int KEYCODE_MEDIA_PLAY = 126;
@@ -583,6 +623,8 @@
* <li> {@link PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID}</li>
* <li> {@link PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH}</li>
* <li> {@link PlaybackStateCompat#ACTION_PREPARE_FROM_URI}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE_ENABLED}</li>
* </ul>
*/
@Actions
@@ -1121,6 +1163,8 @@
* <li> {@link PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID}</li>
* <li> {@link PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH}</li>
* <li> {@link PlaybackStateCompat#ACTION_PREPARE_FROM_URI}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE_ENABLED}</li>
* </ul>
*
* @return this
diff --git a/media-compat/tests/AndroidManifest.xml b/media-compat/tests/AndroidManifest.xml
index b118de4..93ead1e 100644
--- a/media-compat/tests/AndroidManifest.xml
+++ b/media-compat/tests/AndroidManifest.xml
@@ -26,7 +26,6 @@
<application android:supportsRtl="true">
<uses-library android:name="android.test.runner"/>
- <activity android:name="android.support.v4.media.session.TestActivity" />
<receiver android:name="android.support.v4.media.session.MediaButtonReceiver" >
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
diff --git a/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java b/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
index 99c3447..6a997bf 100644
--- a/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
+++ b/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
@@ -175,9 +175,8 @@
assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
}
- // TODO(hdmoon): Uncomment after fixing failing tests. (Fails on API 24, 25)
- // @Test
- // @SmallTest
+ @Test
+ @SmallTest
public void testSubscribeWithOptions() {
createMediaBrowser(TEST_BROWSER_SERVICE);
connectMediaBrowserService();
@@ -328,9 +327,8 @@
}
}
- // TODO(hdmoon): Uncomment after fixing failing tests. (Fails on API 24, 25)
- // @Test
- // @SmallTest
+ @Test
+ @SmallTest
public void testUnsubscribeWithSubscriptionCallbackForMultipleSubscriptions() {
createMediaBrowser(TEST_BROWSER_SERVICE);
connectMediaBrowserService();
diff --git a/media-compat/tests/src/android/support/v4/media/session/MediaControllerCompatTest.java b/media-compat/tests/src/android/support/v4/media/session/MediaControllerCompatTest.java
new file mode 100644
index 0000000..fa639a7
--- /dev/null
+++ b/media-compat/tests/src/android/support/v4/media/session/MediaControllerCompatTest.java
@@ -0,0 +1,596 @@
+/*
+ * 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.v4.media.session;
+
+import static android.support.test.InstrumentationRegistry.getContext;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.VolumeProviderCompat;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test {@link MediaControllerCompat}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaControllerCompatTest {
+ // The maximum time to wait for an operation.
+ private static final long TIME_OUT_MS = 3000L;
+ private static final String SESSION_TAG = "test-session";
+ private static final String EXTRAS_KEY = "test-key";
+ private static final String EXTRAS_VALUE = "test-val";
+ private static final float DELTA = 1e-4f;
+
+ private final Object mWaitLock = new Object();
+ private Handler mHandler = new Handler(Looper.getMainLooper());
+ private MediaSessionCompat mSession;
+ private MediaSessionCallback mCallback = new MediaSessionCallback();
+ private MediaControllerCompat mController;
+
+ @Before
+ public void setUp() throws Exception {
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mSession = new MediaSessionCompat(getContext(), SESSION_TAG);
+ mSession.setCallback(mCallback, mHandler);
+ mController = mSession.getController();
+ }
+ });
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mSession.release();
+ }
+
+ @Test
+ @SmallTest
+ public void testGetPackageName() {
+ assertEquals(getContext().getPackageName(), mController.getPackageName());
+ }
+
+ @Test
+ @SmallTest
+ public void testGetRatingType() {
+ assertEquals("Default rating type of a session must be RatingCompat.RATING_NONE",
+ RatingCompat.RATING_NONE, mController.getRatingType());
+
+ mSession.setRatingType(RatingCompat.RATING_5_STARS);
+ assertEquals(RatingCompat.RATING_5_STARS, mController.getRatingType());
+ }
+
+ @Test
+ @SmallTest
+ public void testGetSessionToken() throws Exception {
+ assertEquals(mSession.getSessionToken(), mController.getSessionToken());
+ }
+
+ @Test
+ @SmallTest
+ public void testSendCommand() throws Exception {
+ synchronized (mWaitLock) {
+ mCallback.reset();
+ final String command = "test-command";
+ final Bundle extras = new Bundle();
+ extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+ mController.sendCommand(command, extras, new ResultReceiver(null));
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnCommandCalled);
+ assertNotNull(mCallback.mCommandCallback);
+ assertEquals(command, mCallback.mCommand);
+ assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+ }
+ }
+
+ // TODO: Uncomment after fixing this test. This test causes an Exception on System UI.
+ // @Test
+ // @SmallTest
+ public void testVolumeControl() throws Exception {
+ VolumeProviderCompat vp =
+ new VolumeProviderCompat(VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, 11, 5) {
+ @Override
+ public void onSetVolumeTo(int volume) {
+ synchronized (mWaitLock) {
+ setCurrentVolume(volume);
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onAdjustVolume(int direction) {
+ synchronized (mWaitLock) {
+ switch (direction) {
+ case AudioManager.ADJUST_LOWER:
+ setCurrentVolume(getCurrentVolume() - 1);
+ break;
+ case AudioManager.ADJUST_RAISE:
+ setCurrentVolume(getCurrentVolume() + 1);
+ break;
+ }
+ mWaitLock.notify();
+ }
+ }
+ };
+ mSession.setPlaybackToRemote(vp);
+
+ synchronized (mWaitLock) {
+ // test setVolumeTo
+ mController.setVolumeTo(7, 0);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertEquals(7, vp.getCurrentVolume());
+
+ // test adjustVolume
+ mController.adjustVolume(AudioManager.ADJUST_LOWER, 0);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertEquals(6, vp.getCurrentVolume());
+
+ mController.adjustVolume(AudioManager.ADJUST_RAISE, 0);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertEquals(7, vp.getCurrentVolume());
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testTransportControlsAndMediaSessionCallback() throws Exception {
+ MediaControllerCompat.TransportControls controls = mController.getTransportControls();
+ synchronized (mWaitLock) {
+ mCallback.reset();
+ controls.play();
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPlayCalled);
+
+ mCallback.reset();
+ controls.pause();
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPauseCalled);
+
+ mCallback.reset();
+ controls.stop();
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnStopCalled);
+
+ mCallback.reset();
+ controls.fastForward();
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnFastForwardCalled);
+
+ mCallback.reset();
+ controls.rewind();
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnRewindCalled);
+
+ mCallback.reset();
+ controls.skipToPrevious();
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSkipToPreviousCalled);
+
+ mCallback.reset();
+ controls.skipToNext();
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSkipToNextCalled);
+
+ mCallback.reset();
+ final long seekPosition = 1000;
+ controls.seekTo(seekPosition);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSeekToCalled);
+ assertEquals(seekPosition, mCallback.mSeekPosition);
+
+ mCallback.reset();
+ final RatingCompat rating =
+ RatingCompat.newStarRating(RatingCompat.RATING_5_STARS, 3f);
+ controls.setRating(rating);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSetRatingCalled);
+ assertEquals(rating.getRatingStyle(), mCallback.mRating.getRatingStyle());
+ assertEquals(rating.getStarRating(), mCallback.mRating.getStarRating(), DELTA);
+
+ mCallback.reset();
+ final String mediaId = "test-media-id";
+ final Bundle extras = new Bundle();
+ extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+ controls.playFromMediaId(mediaId, extras);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPlayFromMediaIdCalled);
+ assertEquals(mediaId, mCallback.mMediaId);
+ assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+ mCallback.reset();
+ final String query = "test-query";
+ controls.playFromSearch(query, extras);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPlayFromSearchCalled);
+ assertEquals(query, mCallback.mQuery);
+ assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+ mCallback.reset();
+ final Uri uri = Uri.parse("content://test/popcorn.mod");
+ controls.playFromUri(uri, extras);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPlayFromUriCalled);
+ assertEquals(uri, mCallback.mUri);
+ assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+ mCallback.reset();
+ final String action = "test-action";
+ controls.sendCustomAction(action, extras);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnCustomActionCalled);
+ assertEquals(action, mCallback.mAction);
+ assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+ mCallback.reset();
+ mCallback.mOnCustomActionCalled = false;
+ final PlaybackStateCompat.CustomAction customAction =
+ new PlaybackStateCompat.CustomAction.Builder(action, action, -1)
+ .setExtras(extras)
+ .build();
+ controls.sendCustomAction(customAction, extras);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnCustomActionCalled);
+ assertEquals(action, mCallback.mAction);
+ assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+ mCallback.reset();
+ final long queueItemId = 1000;
+ controls.skipToQueueItem(queueItemId);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSkipToQueueItemCalled);
+ assertEquals(queueItemId, mCallback.mQueueItemId);
+
+ mCallback.reset();
+ controls.prepare();
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPrepareCalled);
+
+ mCallback.reset();
+ controls.prepareFromMediaId(mediaId, extras);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPrepareFromMediaIdCalled);
+ assertEquals(mediaId, mCallback.mMediaId);
+ assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+ mCallback.reset();
+ controls.prepareFromSearch(query, extras);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPrepareFromSearchCalled);
+ assertEquals(query, mCallback.mQuery);
+ assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+ mCallback.reset();
+ controls.prepareFromUri(uri, extras);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPrepareFromUriCalled);
+ assertEquals(uri, mCallback.mUri);
+ assertEquals(EXTRAS_VALUE, mCallback.mExtras.getString(EXTRAS_KEY));
+
+ mCallback.reset();
+ final int repeatMode = PlaybackStateCompat.REPEAT_MODE_ALL;
+ controls.setRepeatMode(repeatMode);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSetRepeatModeCalled);
+ assertEquals(repeatMode, mCallback.mRepeatMode);
+
+ mCallback.reset();
+ final boolean shuffleModeEnabled = true;
+ controls.setShuffleModeEnabled(shuffleModeEnabled);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSetShuffleModeEnabledCalled);
+ assertEquals(shuffleModeEnabled, mCallback.mShuffleModeEnabled);
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testPlaybackInfo() {
+ final int playbackType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL;
+ final int volumeControl = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+ final int maxVolume = 10;
+ final int currentVolume = 3;
+
+ int audioStream = 77;
+ MediaControllerCompat.PlaybackInfo info = new MediaControllerCompat.PlaybackInfo(
+ playbackType, audioStream, volumeControl, maxVolume, currentVolume);
+
+ assertEquals(playbackType, info.getPlaybackType());
+ assertEquals(audioStream, info.getAudioStream());
+ assertEquals(volumeControl, info.getVolumeControl());
+ assertEquals(maxVolume, info.getMaxVolume());
+ assertEquals(currentVolume, info.getCurrentVolume());
+ }
+
+ private class MediaSessionCallback extends MediaSessionCompat.Callback {
+ private long mSeekPosition;
+ private long mQueueItemId;
+ private RatingCompat mRating;
+ private String mMediaId;
+ private String mQuery;
+ private Uri mUri;
+ private String mAction;
+ private String mCommand;
+ private Bundle mExtras;
+ private ResultReceiver mCommandCallback;
+ private int mRepeatMode;
+ private boolean mShuffleModeEnabled;
+
+ private boolean mOnPlayCalled;
+ private boolean mOnPauseCalled;
+ private boolean mOnStopCalled;
+ private boolean mOnFastForwardCalled;
+ private boolean mOnRewindCalled;
+ private boolean mOnSkipToPreviousCalled;
+ private boolean mOnSkipToNextCalled;
+ private boolean mOnSeekToCalled;
+ private boolean mOnSkipToQueueItemCalled;
+ private boolean mOnSetRatingCalled;
+ private boolean mOnPlayFromMediaIdCalled;
+ private boolean mOnPlayFromSearchCalled;
+ private boolean mOnPlayFromUriCalled;
+ private boolean mOnCustomActionCalled;
+ private boolean mOnCommandCalled;
+ private boolean mOnPrepareCalled;
+ private boolean mOnPrepareFromMediaIdCalled;
+ private boolean mOnPrepareFromSearchCalled;
+ private boolean mOnPrepareFromUriCalled;
+ private boolean mOnSetRepeatModeCalled;
+ private boolean mOnSetShuffleModeEnabledCalled;
+
+ public void reset() {
+ mSeekPosition = -1;
+ mQueueItemId = -1;
+ mRating = null;
+ mMediaId = null;
+ mQuery = null;
+ mUri = null;
+ mAction = null;
+ mExtras = null;
+ mCommand = null;
+ mCommandCallback = null;
+ mShuffleModeEnabled = false;
+ mRepeatMode = PlaybackStateCompat.REPEAT_MODE_NONE;
+
+ mOnPlayCalled = false;
+ mOnPauseCalled = false;
+ mOnStopCalled = false;
+ mOnFastForwardCalled = false;
+ mOnRewindCalled = false;
+ mOnSkipToPreviousCalled = false;
+ mOnSkipToNextCalled = false;
+ mOnSkipToQueueItemCalled = false;
+ mOnSeekToCalled = false;
+ mOnSetRatingCalled = false;
+ mOnPlayFromMediaIdCalled = false;
+ mOnPlayFromSearchCalled = false;
+ mOnPlayFromUriCalled = false;
+ mOnCustomActionCalled = false;
+ mOnCommandCalled = false;
+ mOnPrepareCalled = false;
+ mOnPrepareFromMediaIdCalled = false;
+ mOnPrepareFromSearchCalled = false;
+ mOnPrepareFromUriCalled = false;
+ mOnSetRepeatModeCalled = false;
+ mOnSetShuffleModeEnabledCalled = false;
+ }
+
+ @Override
+ public void onPlay() {
+ synchronized (mWaitLock) {
+ mOnPlayCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ synchronized (mWaitLock) {
+ mOnPauseCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ synchronized (mWaitLock) {
+ mOnStopCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onFastForward() {
+ synchronized (mWaitLock) {
+ mOnFastForwardCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onRewind() {
+ synchronized (mWaitLock) {
+ mOnRewindCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSkipToPrevious() {
+ synchronized (mWaitLock) {
+ mOnSkipToPreviousCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSkipToNext() {
+ synchronized (mWaitLock) {
+ mOnSkipToNextCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ synchronized (mWaitLock) {
+ mOnSeekToCalled = true;
+ mSeekPosition = pos;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSetRating(RatingCompat rating) {
+ synchronized (mWaitLock) {
+ mOnSetRatingCalled = true;
+ mRating = rating;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPlayFromMediaId(String mediaId, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnPlayFromMediaIdCalled = true;
+ mMediaId = mediaId;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPlayFromSearch(String query, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnPlayFromSearchCalled = true;
+ mQuery = query;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPlayFromUri(Uri uri, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnPlayFromUriCalled = true;
+ mUri = uri;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onCustomAction(String action, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnCustomActionCalled = true;
+ mAction = action;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSkipToQueueItem(long id) {
+ synchronized (mWaitLock) {
+ mOnSkipToQueueItemCalled = true;
+ mQueueItemId = id;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onCommand(String command, Bundle extras, ResultReceiver cb) {
+ synchronized (mWaitLock) {
+ mOnCommandCalled = true;
+ mCommand = command;
+ mExtras = extras;
+ mCommandCallback = cb;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPrepare() {
+ synchronized (mWaitLock) {
+ mOnPrepareCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPrepareFromMediaId(String mediaId, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnPrepareFromMediaIdCalled = true;
+ mMediaId = mediaId;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPrepareFromSearch(String query, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnPrepareFromSearchCalled = true;
+ mQuery = query;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onPrepareFromUri(Uri uri, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnPrepareFromUriCalled = true;
+ mUri = uri;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSetRepeatMode(int repeatMode) {
+ synchronized (mWaitLock) {
+ mOnSetRepeatModeCalled = true;
+ mRepeatMode = repeatMode;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSetShuffleModeEnabled(boolean enabled) {
+ synchronized (mWaitLock) {
+ mOnSetShuffleModeEnabledCalled = true;
+ mShuffleModeEnabled = enabled;
+ mWaitLock.notify();
+ }
+ }
+ }
+}
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 72460f5..a5f11ed 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
@@ -16,85 +16,701 @@
package android.support.v4.media.session;
-import static junit.framework.Assert.fail;
+import static android.support.test.InstrumentationRegistry.getContext;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.rule.ActivityTestRule;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.VolumeProviderCompat;
+import android.view.KeyEvent;
+import org.junit.After;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
+import java.util.ArrayList;
+import java.util.List;
+/**
+ * Test {@link MediaSessionCompat}.
+ */
@RunWith(AndroidJUnit4.class)
public class MediaSessionCompatTest {
- @Rule
- public ActivityTestRule<TestActivity> mActivityRule =
- new ActivityTestRule<>(TestActivity.class);
- Context mContext;
- Map<String, LockedObject> results = new HashMap<>();
+ // The maximum time to wait for an operation.
+ private static final long TIME_OUT_MS = 3000L;
+ private static final int MAX_AUDIO_INFO_CHANGED_CALLBACK_COUNT = 10;
+ private static final String TEST_SESSION_TAG = "test-session-tag";
+ private static final String TEST_KEY = "test-key";
+ private static final String TEST_VALUE = "test-val";
+ private static final String TEST_SESSION_EVENT = "test-session-event";
+ private static final int TEST_CURRENT_VOLUME = 10;
+ private static final int TEST_MAX_VOLUME = 11;
+ private static final long TEST_QUEUE_ID = 12L;
+ private static final long TEST_ACTION = 55L;
+
+ private AudioManager mAudioManager;
+ private Handler mHandler = new Handler(Looper.getMainLooper());
+ private Object mWaitLock = new Object();
+ private MediaControllerCallback mCallback = new MediaControllerCallback();
+ private MediaSessionCompat mSession;
@Before
- public void setUp() {
- mContext = InstrumentationRegistry.getContext();
+ public void setUp() throws Exception {
+ getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
+ mSession = new MediaSessionCompat(getContext(), TEST_SESSION_TAG);
+ }
+ });
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ // It is OK to call release() twice.
+ mSession.release();
+ }
+
+ /**
+ * Tests that a session can be created and that all the fields are
+ * initialized correctly.
+ */
+ @Test
+ @SmallTest
+ public void testCreateSession() throws Exception {
+ assertNotNull(mSession.getSessionToken());
+ assertFalse("New session should not be active", mSession.isActive());
+
+ // Verify by getting the controller and checking all its fields
+ MediaControllerCompat controller = mSession.getController();
+ assertNotNull(controller);
+ verifyNewSession(controller, TEST_SESSION_TAG);
+ }
+
+ /**
+ * Tests MediaSessionCompat.Token created in the constructor of MediaSessionCompat.
+ */
+ @Test
+ @SmallTest
+ public void testSessionToken() throws Exception {
+ MediaSessionCompat.Token sessionToken = mSession.getSessionToken();
+
+ assertNotNull(sessionToken);
+ assertEquals(0, sessionToken.describeContents());
+
+ // Test writeToParcel
+ Parcel p = Parcel.obtain();
+ sessionToken.writeToParcel(p, 0);
+ p.setDataPosition(0);
+ MediaSessionCompat.Token token = MediaSessionCompat.Token.CREATOR.createFromParcel(p);
+ assertEquals(token, sessionToken);
+ p.recycle();
+ }
+
+ /**
+ * Tests that the various configuration bits on a session get passed to the
+ * controller.
+ */
+ @Test
+ @SmallTest
+ public void testConfigureSession() throws Exception {
+ MediaControllerCompat controller = mSession.getController();
+ controller.registerCallback(mCallback, mHandler);
+
+ synchronized (mWaitLock) {
+ // test setExtras
+ mCallback.resetLocked();
+ final Bundle extras = new Bundle();
+ extras.putString(TEST_KEY, TEST_VALUE);
+ mSession.setExtras(extras);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnExtraChangedCalled);
+
+ Bundle extrasOut = mCallback.mExtras;
+ assertNotNull(extrasOut);
+ assertEquals(TEST_VALUE, extrasOut.get(TEST_KEY));
+
+ extrasOut = controller.getExtras();
+ assertNotNull(extrasOut);
+ assertEquals(TEST_VALUE, extrasOut.get(TEST_KEY));
+
+ // test setFlags
+ mSession.setFlags(5);
+ assertEquals(5, controller.getFlags());
+
+ // test setMetadata
+ mCallback.resetLocked();
+ MediaMetadataCompat metadata =
+ new MediaMetadataCompat.Builder().putString(TEST_KEY, TEST_VALUE).build();
+ mSession.setMetadata(metadata);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnMetadataChangedCalled);
+
+ MediaMetadataCompat metadataOut = mCallback.mMediaMetadata;
+ assertNotNull(metadataOut);
+ assertEquals(TEST_VALUE, metadataOut.getString(TEST_KEY));
+
+ metadataOut = controller.getMetadata();
+ assertNotNull(metadataOut);
+ assertEquals(TEST_VALUE, metadataOut.getString(TEST_KEY));
+
+ // test setPlaybackState
+ mCallback.resetLocked();
+ PlaybackStateCompat state =
+ new PlaybackStateCompat.Builder().setActions(TEST_ACTION).build();
+ mSession.setPlaybackState(state);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnPlaybackStateChangedCalled);
+
+ PlaybackStateCompat stateOut = mCallback.mPlaybackState;
+ assertNotNull(stateOut);
+ assertEquals(TEST_ACTION, stateOut.getActions());
+
+ stateOut = controller.getPlaybackState();
+ assertNotNull(stateOut);
+ assertEquals(TEST_ACTION, stateOut.getActions());
+
+ // test setQueue and setQueueTitle
+ mCallback.resetLocked();
+ List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
+ MediaSessionCompat.QueueItem item = new MediaSessionCompat.QueueItem(
+ new MediaDescriptionCompat.Builder()
+ .setMediaId(TEST_VALUE)
+ .setTitle("title")
+ .build(),
+ TEST_QUEUE_ID);
+ queue.add(item);
+ mSession.setQueue(queue);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnQueueChangedCalled);
+
+ mSession.setQueueTitle(TEST_VALUE);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnQueueTitleChangedCalled);
+
+ assertEquals(TEST_VALUE, mCallback.mTitle);
+ assertEquals(queue.size(), mCallback.mQueue.size());
+ assertEquals(TEST_QUEUE_ID, mCallback.mQueue.get(0).getQueueId());
+ assertEquals(TEST_VALUE, mCallback.mQueue.get(0).getDescription().getMediaId());
+
+ assertEquals(TEST_VALUE, controller.getQueueTitle());
+ assertEquals(queue.size(), controller.getQueue().size());
+ assertEquals(TEST_QUEUE_ID, controller.getQueue().get(0).getQueueId());
+ assertEquals(TEST_VALUE, controller.getQueue().get(0).getDescription().getMediaId());
+
+ mCallback.resetLocked();
+ mSession.setQueue(null);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnQueueChangedCalled);
+
+ mSession.setQueueTitle(null);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnQueueTitleChangedCalled);
+
+ assertNull(mCallback.mTitle);
+ assertNull(mCallback.mQueue);
+ assertNull(controller.getQueueTitle());
+ assertNull(controller.getQueue());
+
+ // test setSessionActivity
+ Intent intent = new Intent("cts.MEDIA_SESSION_ACTION");
+ PendingIntent pi = PendingIntent.getActivity(getContext(), 555, intent, 0);
+ mSession.setSessionActivity(pi);
+ assertEquals(pi, controller.getSessionActivity());
+
+ // test setRepeatMode
+ mCallback.resetLocked();
+ final int repeatMode = PlaybackStateCompat.REPEAT_MODE_ALL;
+ mSession.setRepeatMode(repeatMode);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnRepeatModeChangedCalled);
+ assertEquals(repeatMode, mCallback.mRepeatMode);
+ assertEquals(repeatMode, controller.getRepeatMode());
+
+ // test setShuffleModeEnabled
+ mCallback.resetLocked();
+ final boolean shuffleModeEnabled = true;
+ mSession.setShuffleModeEnabled(shuffleModeEnabled);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnShuffleModeChangedCalled);
+ assertEquals(shuffleModeEnabled, mCallback.mShuffleModeEnabled);
+ assertEquals(shuffleModeEnabled, controller.isShuffleModeEnabled());
+
+ // test setActivity
+ mSession.setActive(true);
+ assertTrue(mSession.isActive());
+
+ // test sendSessionEvent
+ mCallback.resetLocked();
+ mSession.sendSessionEvent(TEST_SESSION_EVENT, extras);
+ mWaitLock.wait(TIME_OUT_MS);
+
+ assertTrue(mCallback.mOnSessionEventCalled);
+ assertEquals(TEST_SESSION_EVENT, mCallback.mEvent);
+ assertEquals(TEST_VALUE, mCallback.mExtras.getString(TEST_KEY));
+
+ // test release
+ mCallback.resetLocked();
+ mSession.release();
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnSessionDestroyedCalled);
+ }
+ }
+
+ /**
+ * Test {@link MediaSessionCompat#setPlaybackToLocal} and
+ * {@link MediaSessionCompat#setPlaybackToRemote}.
+ */
+ @Test
+ @SmallTest
+ public void testPlaybackToLocalAndRemote() throws Exception {
+ MediaControllerCompat controller = mSession.getController();
+ controller.registerCallback(mCallback, mHandler);
+
+ synchronized (mWaitLock) {
+ // test setPlaybackToRemote, do this before testing setPlaybackToLocal
+ // to ensure it switches correctly.
+ mCallback.resetLocked();
+ try {
+ mSession.setPlaybackToRemote(null);
+ fail("Expected IAE for setPlaybackToRemote(null)");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ VolumeProviderCompat vp = new VolumeProviderCompat(
+ VolumeProviderCompat.VOLUME_CONTROL_FIXED,
+ TEST_MAX_VOLUME,
+ TEST_CURRENT_VOLUME) {};
+ mSession.setPlaybackToRemote(vp);
+
+ MediaControllerCompat.PlaybackInfo info = null;
+ for (int i = 0; i < MAX_AUDIO_INFO_CHANGED_CALLBACK_COUNT; ++i) {
+ mCallback.mOnAudioInfoChangedCalled = false;
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(mCallback.mOnAudioInfoChangedCalled);
+ info = mCallback.mPlaybackInfo;
+ if (info != null && info.getCurrentVolume() == TEST_CURRENT_VOLUME
+ && info.getMaxVolume() == TEST_MAX_VOLUME
+ && info.getVolumeControl() == VolumeProviderCompat.VOLUME_CONTROL_FIXED
+ && info.getPlaybackType()
+ == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
+ break;
+ }
+ }
+ assertNotNull(info);
+ assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+ info.getPlaybackType());
+ assertEquals(TEST_MAX_VOLUME, info.getMaxVolume());
+ assertEquals(TEST_CURRENT_VOLUME, info.getCurrentVolume());
+ assertEquals(VolumeProviderCompat.VOLUME_CONTROL_FIXED,
+ info.getVolumeControl());
+
+ info = controller.getPlaybackInfo();
+ assertNotNull(info);
+ assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+ info.getPlaybackType());
+ assertEquals(TEST_MAX_VOLUME, info.getMaxVolume());
+ assertEquals(TEST_CURRENT_VOLUME, info.getCurrentVolume());
+ assertEquals(VolumeProviderCompat.VOLUME_CONTROL_FIXED, info.getVolumeControl());
+
+ // test setPlaybackToLocal
+ mSession.setPlaybackToLocal(AudioManager.STREAM_RING);
+ info = controller.getPlaybackInfo();
+ assertNotNull(info);
+ assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+ info.getPlaybackType());
+ }
+ }
+
+ /**
+ * Test {@link MediaSessionCompat.Callback#onMediaButtonEvent}.
+ */
+ @Test
+ @SmallTest
+ public void testCallbackOnMediaButtonEvent() throws Exception {
+ MediaSessionCallback sessionCallback = new MediaSessionCallback();
+ mSession.setCallback(sessionCallback, new Handler(Looper.getMainLooper()));
+ mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS);
+ mSession.setActive(true);
+
+ Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON).setComponent(
+ new ComponentName(getContext(), getContext().getClass()));
+ 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);
+
+ synchronized (mWaitLock) {
+ sessionCallback.reset();
+ sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(sessionCallback.mOnPlayCalled);
+
+ sessionCallback.reset();
+ sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PAUSE);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(sessionCallback.mOnPauseCalled);
+
+ sessionCallback.reset();
+ sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_NEXT);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(sessionCallback.mOnSkipToNextCalled);
+
+ sessionCallback.reset();
+ sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PREVIOUS);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(sessionCallback.mOnSkipToPreviousCalled);
+
+ sessionCallback.reset();
+ sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_STOP);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(sessionCallback.mOnStopCalled);
+
+ sessionCallback.reset();
+ sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(sessionCallback.mOnFastForwardCalled);
+
+ sessionCallback.reset();
+ sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_REWIND);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(sessionCallback.mOnRewindCalled);
+
+ // Test PLAY_PAUSE button twice.
+ // First, send PLAY_PAUSE button event while in STATE_PAUSED.
+ sessionCallback.reset();
+ mSession.setPlaybackState(new PlaybackStateCompat.Builder().setActions(supportedActions)
+ .setState(PlaybackStateCompat.STATE_PAUSED, 0L, 0.0f).build());
+ sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(sessionCallback.mOnPlayCalled);
+
+ // Next, send PLAY_PAUSE button event while in STATE_PLAYING.
+ sessionCallback.reset();
+ mSession.setPlaybackState(new PlaybackStateCompat.Builder().setActions(supportedActions)
+ .setState(PlaybackStateCompat.STATE_PLAYING, 0L, 0.0f).build());
+ sendMediaKeyInputToController(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
+ mWaitLock.wait(TIME_OUT_MS);
+ assertTrue(sessionCallback.mOnPauseCalled);
+ }
}
@Test
+ @SmallTest
public void testSetNullCallback() throws Throwable {
- initWait("testSetNullCallback");
- mActivityRule.runOnUiThread(new Runnable() {
+ getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
try {
- MediaSessionCompat session = new MediaSessionCompat(mContext, "TEST");
+ MediaSessionCompat session = new MediaSessionCompat(getContext(), "TEST");
session.setCallback(null);
} catch (Exception e) {
fail("Fail with an exception: " + e);
- } finally {
- setResultData("testSetNullCallback", true);
}
}
});
- waitFor("testSetNullCallback");
}
- private void initWait(String key) throws InterruptedException {
- results.put(key, new LockedObject());
+ /**
+ * Tests {@link MediaSessionCompat.QueueItem}.
+ */
+ @Test
+ @SmallTest
+ public void testQueueItem() {
+ MediaSessionCompat.QueueItem item = new MediaSessionCompat.QueueItem(
+ new MediaDescriptionCompat.Builder()
+ .setMediaId("media-id")
+ .setTitle("title")
+ .build(),
+ TEST_QUEUE_ID);
+ assertEquals(TEST_QUEUE_ID, item.getQueueId());
+ assertEquals("media-id", item.getDescription().getMediaId());
+ assertEquals("title", item.getDescription().getTitle());
+ assertEquals(0, item.describeContents());
+
+ Parcel p = Parcel.obtain();
+ item.writeToParcel(p, 0);
+ p.setDataPosition(0);
+ MediaSessionCompat.QueueItem other =
+ MediaSessionCompat.QueueItem.CREATOR.createFromParcel(p);
+ assertEquals(item.toString(), other.toString());
+ p.recycle();
}
- private Object[] waitFor(String key) throws InterruptedException {
- return results.get(key).waitFor();
+ /**
+ * Verifies that a new session hasn't had any configuration bits set yet.
+ *
+ * @param controller The controller for the session
+ */
+ private void verifyNewSession(MediaControllerCompat controller, String tag) {
+ assertEquals("New session has unexpected configuration", 0L, controller.getFlags());
+ assertNull("New session has unexpected configuration", controller.getExtras());
+ assertNull("New session has unexpected configuration", controller.getMetadata());
+ assertEquals("New session has unexpected configuration",
+ getContext().getPackageName(), controller.getPackageName());
+ assertNull("New session has unexpected configuration", controller.getPlaybackState());
+ assertNull("New session has unexpected configuration", controller.getQueue());
+ assertNull("New session has unexpected configuration", controller.getQueueTitle());
+ assertEquals("New session has unexpected configuration", RatingCompat.RATING_NONE,
+ controller.getRatingType());
+ assertNull("New session has unexpected configuration", controller.getSessionActivity());
+
+ assertNotNull(controller.getSessionToken());
+ assertNotNull(controller.getTransportControls());
+
+ MediaControllerCompat.PlaybackInfo info = controller.getPlaybackInfo();
+ assertNotNull(info);
+ assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+ info.getPlaybackType());
+ assertEquals(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC),
+ info.getCurrentVolume());
}
- private void setResultData(String key, Object... args) {
- if (results.containsKey(key)) {
- results.get(key).set(args);
+ private void sendMediaKeyInputToController(int keyCode) {
+ MediaControllerCompat controller = mSession.getController();
+ controller.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
+ controller.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
+ }
+
+ private class MediaControllerCallback extends MediaControllerCompat.Callback {
+ private volatile boolean mOnPlaybackStateChangedCalled;
+ private volatile boolean mOnMetadataChangedCalled;
+ private volatile boolean mOnQueueChangedCalled;
+ private volatile boolean mOnQueueTitleChangedCalled;
+ private volatile boolean mOnExtraChangedCalled;
+ private volatile boolean mOnAudioInfoChangedCalled;
+ private volatile boolean mOnSessionDestroyedCalled;
+ private volatile boolean mOnSessionEventCalled;
+ private volatile boolean mOnRepeatModeChangedCalled;
+ private volatile boolean mOnShuffleModeChangedCalled;
+
+ private volatile PlaybackStateCompat mPlaybackState;
+ private volatile MediaMetadataCompat mMediaMetadata;
+ private volatile List<MediaSessionCompat.QueueItem> mQueue;
+ private volatile CharSequence mTitle;
+ private volatile String mEvent;
+ private volatile Bundle mExtras;
+ private volatile MediaControllerCompat.PlaybackInfo mPlaybackInfo;
+ private volatile int mRepeatMode;
+ private volatile boolean mShuffleModeEnabled;
+
+ public void resetLocked() {
+ mOnPlaybackStateChangedCalled = false;
+ mOnMetadataChangedCalled = false;
+ mOnQueueChangedCalled = false;
+ mOnQueueTitleChangedCalled = false;
+ mOnExtraChangedCalled = false;
+ mOnAudioInfoChangedCalled = false;
+ mOnSessionDestroyedCalled = false;
+ mOnSessionEventCalled = false;
+ mOnRepeatModeChangedCalled = false;
+ mOnShuffleModeChangedCalled = false;
+
+ mPlaybackState = null;
+ mMediaMetadata = null;
+ mQueue = null;
+ mTitle = null;
+ mExtras = null;
+ mPlaybackInfo = null;
+ mRepeatMode = PlaybackStateCompat.REPEAT_MODE_NONE;
+ mShuffleModeEnabled = false;
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlaybackStateCompat state) {
+ synchronized (mWaitLock) {
+ mOnPlaybackStateChangedCalled = true;
+ mPlaybackState = state;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadataCompat metadata) {
+ synchronized (mWaitLock) {
+ mOnMetadataChangedCalled = true;
+ mMediaMetadata = metadata;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onQueueChanged(List<MediaSessionCompat.QueueItem> queue) {
+ synchronized (mWaitLock) {
+ mOnQueueChangedCalled = true;
+ mQueue = queue;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onQueueTitleChanged(CharSequence title) {
+ synchronized (mWaitLock) {
+ mOnQueueTitleChangedCalled = true;
+ mTitle = title;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onExtrasChanged(Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnExtraChangedCalled = true;
+ mExtras = extras;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onAudioInfoChanged(MediaControllerCompat.PlaybackInfo info) {
+ synchronized (mWaitLock) {
+ mOnAudioInfoChangedCalled = true;
+ mPlaybackInfo = info;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSessionDestroyed() {
+ synchronized (mWaitLock) {
+ mOnSessionDestroyedCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSessionEvent(String event, Bundle extras) {
+ synchronized (mWaitLock) {
+ mOnSessionEventCalled = true;
+ mEvent = event;
+ mExtras = (Bundle) extras.clone();
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onRepeatModeChanged(int repeatMode) {
+ synchronized (mWaitLock) {
+ mOnRepeatModeChangedCalled = true;
+ mRepeatMode = repeatMode;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onShuffleModeChanged(boolean enabled) {
+ synchronized (mWaitLock) {
+ mOnShuffleModeChangedCalled = true;
+ mShuffleModeEnabled = enabled;
+ mWaitLock.notify();
+ }
}
}
- private class LockedObject {
- private Semaphore mLock = new Semaphore(1);
- private volatile Object[] mArgs;
+ private class MediaSessionCallback extends MediaSessionCompat.Callback {
+ private boolean mOnPlayCalled;
+ private boolean mOnPauseCalled;
+ private boolean mOnStopCalled;
+ private boolean mOnFastForwardCalled;
+ private boolean mOnRewindCalled;
+ private boolean mOnSkipToPreviousCalled;
+ private boolean mOnSkipToNextCalled;
- public LockedObject() {
- mLock.drainPermits();
+ public void reset() {
+ mOnPlayCalled = false;
+ mOnPauseCalled = false;
+ mOnStopCalled = false;
+ mOnFastForwardCalled = false;
+ mOnRewindCalled = false;
+ mOnSkipToPreviousCalled = false;
+ mOnSkipToNextCalled = false;
}
- public void set(Object... args) {
- mArgs = args;
- mLock.release(1);
+ @Override
+ public void onPlay() {
+ synchronized (mWaitLock) {
+ mOnPlayCalled = true;
+ mWaitLock.notify();
+ }
}
- public Object[] waitFor() throws InterruptedException {
- mLock.tryAcquire(1, 2, TimeUnit.SECONDS);
- return mArgs;
+ @Override
+ public void onPause() {
+ synchronized (mWaitLock) {
+ mOnPauseCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ synchronized (mWaitLock) {
+ mOnStopCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onFastForward() {
+ synchronized (mWaitLock) {
+ mOnFastForwardCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onRewind() {
+ synchronized (mWaitLock) {
+ mOnRewindCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSkipToPrevious() {
+ synchronized (mWaitLock) {
+ mOnSkipToPreviousCalled = true;
+ mWaitLock.notify();
+ }
+ }
+
+ @Override
+ public void onSkipToNext() {
+ synchronized (mWaitLock) {
+ mOnSkipToNextCalled = true;
+ mWaitLock.notify();
+ }
}
}
}
diff --git a/media-compat/tests/src/android/support/v4/media/session/PlaybackStateCompatTest.java b/media-compat/tests/src/android/support/v4/media/session/PlaybackStateCompatTest.java
new file mode 100644
index 0000000..410ae01
--- /dev/null
+++ b/media-compat/tests/src/android/support/v4/media/session/PlaybackStateCompatTest.java
@@ -0,0 +1,291 @@
+/*
+ * 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.v4.media.session;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.os.Bundle;
+import android.os.Parcel;
+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;
+
+/**
+ * Test {@link PlaybackStateCompat}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class PlaybackStateCompatTest {
+
+ private static final long TEST_POSITION = 20000L;
+ private static final long TEST_BUFFERED_POSITION = 15000L;
+ private static final long TEST_UPDATE_TIME = 100000L;
+ private static final long TEST_ACTIONS = PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SEEK_TO;
+ private static final long TEST_QUEUE_ITEM_ID = 23L;
+ private static final float TEST_PLAYBACK_SPEED = 3.0f;
+ private static final float TEST_PLAYBACK_SPEED_ON_REWIND = -2.0f;
+ private static final float DELTA = 1e-7f;
+
+ private static final String TEST_ERROR_MSG = "test-error-msg";
+ private static final String TEST_CUSTOM_ACTION = "test-custom-action";
+ private static final String TEST_CUSTOM_ACTION_NAME = "test-custom-action-name";
+ private static final int TEST_ICON_RESOURCE_ID = android.R.drawable.ic_media_next;
+
+ private static final String EXTRAS_KEY = "test-key";
+ private static final String EXTRAS_VALUE = "test-value";
+
+ /**
+ * Test default values of {@link PlaybackStateCompat}.
+ */
+ @Test
+ @SmallTest
+ public void testBuilder() {
+ PlaybackStateCompat state = new PlaybackStateCompat.Builder().build();
+
+ assertEquals(new ArrayList<PlaybackStateCompat.CustomAction>(), state.getCustomActions());
+ assertEquals(0, state.getState());
+ assertEquals(0L, state.getPosition());
+ assertEquals(0L, state.getBufferedPosition());
+ assertEquals(0.0f, state.getPlaybackSpeed(), DELTA);
+ assertEquals(0L, state.getActions());
+ assertNull(state.getErrorMessage());
+ assertEquals(0L, state.getLastPositionUpdateTime());
+ assertEquals(MediaSessionCompat.QueueItem.UNKNOWN_ID, state.getActiveQueueItemId());
+ assertNull(state.getExtras());
+ }
+
+ /**
+ * Test following setter methods of {@link PlaybackStateCompat.Builder}:
+ * {@link PlaybackStateCompat.Builder#setState(int, long, float)}
+ * {@link PlaybackStateCompat.Builder#setActions(long)}
+ * {@link PlaybackStateCompat.Builder#setActiveQueueItemId(long)}
+ * {@link PlaybackStateCompat.Builder#setBufferedPosition(long)}
+ * {@link PlaybackStateCompat.Builder#setErrorMessage(CharSequence)}
+ * {@link PlaybackStateCompat.Builder#setExtras(Bundle)}
+ */
+ @Test
+ @SmallTest
+ public void testBuilder_setterMethods() {
+ Bundle extras = new Bundle();
+ extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+
+ PlaybackStateCompat state = new PlaybackStateCompat.Builder()
+ .setState(PlaybackStateCompat.STATE_PLAYING, TEST_POSITION, TEST_PLAYBACK_SPEED)
+ .setActions(TEST_ACTIONS)
+ .setActiveQueueItemId(TEST_QUEUE_ITEM_ID)
+ .setBufferedPosition(TEST_BUFFERED_POSITION)
+ .setErrorMessage(TEST_ERROR_MSG)
+ .setExtras(extras)
+ .build();
+ assertEquals(PlaybackStateCompat.STATE_PLAYING, state.getState());
+ assertEquals(TEST_POSITION, state.getPosition());
+ assertEquals(TEST_PLAYBACK_SPEED, state.getPlaybackSpeed(), DELTA);
+ assertEquals(TEST_ACTIONS, state.getActions());
+ assertEquals(TEST_QUEUE_ITEM_ID, state.getActiveQueueItemId());
+ assertEquals(TEST_BUFFERED_POSITION, state.getBufferedPosition());
+ assertEquals(TEST_ERROR_MSG, state.getErrorMessage().toString());
+ assertNotNull(state.getExtras());
+ assertEquals(EXTRAS_VALUE, state.getExtras().get(EXTRAS_KEY));
+ }
+
+ /**
+ * Test {@link PlaybackStateCompat.Builder#setState(int, long, float, long)}.
+ */
+ @Test
+ @SmallTest
+ public void testBuilder_setStateWithUpdateTime() {
+ PlaybackStateCompat state = new PlaybackStateCompat.Builder()
+ .setState(
+ PlaybackStateCompat.STATE_REWINDING,
+ TEST_POSITION,
+ TEST_PLAYBACK_SPEED_ON_REWIND,
+ TEST_UPDATE_TIME)
+ .build();
+ assertEquals(PlaybackStateCompat.STATE_REWINDING, state.getState());
+ assertEquals(TEST_POSITION, state.getPosition());
+ assertEquals(TEST_PLAYBACK_SPEED_ON_REWIND, state.getPlaybackSpeed(), DELTA);
+ assertEquals(TEST_UPDATE_TIME, state.getLastPositionUpdateTime());
+ }
+
+ /**
+ * Test {@link PlaybackStateCompat.Builder#addCustomAction(String, String, int)}.
+ */
+ @Test
+ @SmallTest
+ public void testBuilder_addCustomAction() {
+ ArrayList<PlaybackStateCompat.CustomAction> actions = new ArrayList<>();
+ PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
+
+ for (int i = 0; i < 5; i++) {
+ actions.add(new PlaybackStateCompat.CustomAction.Builder(
+ TEST_CUSTOM_ACTION + i, TEST_CUSTOM_ACTION_NAME + i, TEST_ICON_RESOURCE_ID + i)
+ .build());
+ builder.addCustomAction(
+ TEST_CUSTOM_ACTION + i, TEST_CUSTOM_ACTION_NAME + i, TEST_ICON_RESOURCE_ID + i);
+ }
+
+ PlaybackStateCompat state = builder.build();
+ assertEquals(actions.size(), state.getCustomActions().size());
+ for (int i = 0; i < actions.size(); i++) {
+ assertCustomActionEquals(actions.get(i), state.getCustomActions().get(i));
+ }
+ }
+
+ /**
+ * Test {@link PlaybackStateCompat.Builder#addCustomAction(PlaybackStateCompat.CustomAction)}.
+ */
+ @Test
+ @SmallTest
+ public void testBuilder_addCustomActionWithCustomActionObject() {
+ Bundle extras = new Bundle();
+ extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+
+ ArrayList<PlaybackStateCompat.CustomAction> actions = new ArrayList<>();
+ PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
+
+ for (int i = 0; i < 5; i++) {
+ actions.add(new PlaybackStateCompat.CustomAction.Builder(
+ TEST_CUSTOM_ACTION + i, TEST_CUSTOM_ACTION_NAME + i, TEST_ICON_RESOURCE_ID + i)
+ .setExtras(extras)
+ .build());
+ builder.addCustomAction(new PlaybackStateCompat.CustomAction.Builder(
+ TEST_CUSTOM_ACTION + i, TEST_CUSTOM_ACTION_NAME + i, TEST_ICON_RESOURCE_ID + i)
+ .setExtras(extras)
+ .build());
+ }
+
+ PlaybackStateCompat state = builder.build();
+ assertEquals(actions.size(), state.getCustomActions().size());
+ for (int i = 0; i < actions.size(); i++) {
+ assertCustomActionEquals(actions.get(i), state.getCustomActions().get(i));
+ }
+ }
+
+ /**
+ * Test {@link PlaybackStateCompat#writeToParcel(Parcel, int)}.
+ */
+ @Test
+ @SmallTest
+ public void testWriteToParcel() {
+ Bundle extras = new Bundle();
+ extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+
+ PlaybackStateCompat.Builder builder =
+ new PlaybackStateCompat.Builder()
+ .setState(PlaybackStateCompat.STATE_CONNECTING, TEST_POSITION,
+ TEST_PLAYBACK_SPEED, TEST_UPDATE_TIME)
+ .setActions(TEST_ACTIONS)
+ .setActiveQueueItemId(TEST_QUEUE_ITEM_ID)
+ .setBufferedPosition(TEST_BUFFERED_POSITION)
+ .setErrorMessage(TEST_ERROR_MSG)
+ .setExtras(extras);
+
+ for (int i = 0; i < 5; i++) {
+ builder.addCustomAction(
+ new PlaybackStateCompat.CustomAction.Builder(
+ TEST_CUSTOM_ACTION + i,
+ TEST_CUSTOM_ACTION_NAME + i,
+ TEST_ICON_RESOURCE_ID + i)
+ .setExtras(extras)
+ .build());
+ }
+ PlaybackStateCompat state = builder.build();
+
+ Parcel parcel = Parcel.obtain();
+ state.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+
+ PlaybackStateCompat stateOut = PlaybackStateCompat.CREATOR.createFromParcel(parcel);
+ assertEquals(PlaybackStateCompat.STATE_CONNECTING, stateOut.getState());
+ assertEquals(TEST_POSITION, stateOut.getPosition());
+ assertEquals(TEST_PLAYBACK_SPEED, stateOut.getPlaybackSpeed(), DELTA);
+ assertEquals(TEST_UPDATE_TIME, stateOut.getLastPositionUpdateTime());
+ assertEquals(TEST_BUFFERED_POSITION, stateOut.getBufferedPosition());
+ assertEquals(TEST_ACTIONS, stateOut.getActions());
+ assertEquals(TEST_QUEUE_ITEM_ID, stateOut.getActiveQueueItemId());
+ assertEquals(TEST_ERROR_MSG, stateOut.getErrorMessage());
+ assertNotNull(stateOut.getExtras());
+ assertEquals(EXTRAS_VALUE, stateOut.getExtras().get(EXTRAS_KEY));
+
+ assertEquals(state.getCustomActions().size(), stateOut.getCustomActions().size());
+ for (int i = 0; i < state.getCustomActions().size(); i++) {
+ assertCustomActionEquals(
+ state.getCustomActions().get(i), stateOut.getCustomActions().get(i));
+ }
+ parcel.recycle();
+ }
+
+ /**
+ * Test {@link PlaybackStateCompat#describeContents()}.
+ */
+ @Test
+ @SmallTest
+ public void testDescribeContents() {
+ assertEquals(0, new PlaybackStateCompat.Builder().build().describeContents());
+ }
+
+ /**
+ * Test {@link PlaybackStateCompat.CustomAction}.
+ */
+ @Test
+ @SmallTest
+ public void testCustomAction() {
+ Bundle extras = new Bundle();
+ extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+
+ // Test Builder/Getters
+ PlaybackStateCompat.CustomAction customAction = new PlaybackStateCompat.CustomAction
+ .Builder(TEST_CUSTOM_ACTION, TEST_CUSTOM_ACTION_NAME, TEST_ICON_RESOURCE_ID)
+ .setExtras(extras)
+ .build();
+ assertEquals(TEST_CUSTOM_ACTION, customAction.getAction());
+ assertEquals(TEST_CUSTOM_ACTION_NAME, customAction.getName().toString());
+ assertEquals(TEST_ICON_RESOURCE_ID, customAction.getIcon());
+ assertEquals(EXTRAS_VALUE, customAction.getExtras().get(EXTRAS_KEY));
+
+ // Test describeContents
+ assertEquals(0, customAction.describeContents());
+
+ // Test writeToParcel
+ Parcel parcel = Parcel.obtain();
+ customAction.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+
+ assertCustomActionEquals(
+ customAction, PlaybackStateCompat.CustomAction.CREATOR.createFromParcel(parcel));
+ parcel.recycle();
+ }
+
+ private void assertCustomActionEquals(PlaybackStateCompat.CustomAction action1,
+ PlaybackStateCompat.CustomAction action2) {
+ assertEquals(action1.getAction(), action2.getAction());
+ assertEquals(action1.getName(), action2.getName());
+ assertEquals(action1.getIcon(), action2.getIcon());
+
+ // To be the same, two extras should be both null or both not null.
+ assertEquals(action1.getExtras() != null, action2.getExtras() != null);
+ if (action1.getExtras() != null) {
+ assertEquals(action1.getExtras().get(EXTRAS_KEY), action2.getExtras().get(EXTRAS_KEY));
+ }
+ }
+}
diff --git a/media-compat/tests/src/android/support/v4/media/session/TestActivity.java b/media-compat/tests/src/android/support/v4/media/session/TestActivity.java
deleted file mode 100644
index dd56467..0000000
--- a/media-compat/tests/src/android/support/v4/media/session/TestActivity.java
+++ /dev/null
@@ -1,22 +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.v4.media.session;
-
-import android.app.Activity;
-
-public class TestActivity extends Activity {
-}
diff --git a/v13/java/android/support/v13/view/inputmethod/EditorInfoCompat.java b/v13/java/android/support/v13/view/inputmethod/EditorInfoCompat.java
index 17976a8..b1358d0 100644
--- a/v13/java/android/support/v13/view/inputmethod/EditorInfoCompat.java
+++ b/v13/java/android/support/v13/view/inputmethod/EditorInfoCompat.java
@@ -98,7 +98,7 @@
* @param editorInfo the editor with which we associate supported MIME types
* @param contentMimeTypes an array of MIME types. {@code null} and an empty array means that
* {@link InputConnectionCompat#commitContent(
- * InputConnection, EditorInfo, InputContentInfoCompat, int, Bundle)
+ * InputConnection, EditorInfo, InputContentInfoCompat, int, Bundle)}
* is not supported on this Editor
*/
public static void setContentMimeTypes(@NonNull EditorInfo editorInfo,
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
index 2b70139..d14dc74 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
@@ -47,8 +47,10 @@
@Override
public void onChildViewHolderSelected(RecyclerView parent,
RecyclerView.ViewHolder view, int position, int subposition) {
- mSelectedPosition = position;
- onRowSelected(parent, view, position, subposition);
+ if (!mLateSelectionObserver.mIsLateSelection) {
+ mSelectedPosition = position;
+ onRowSelected(parent, view, position, subposition);
+ }
}
};
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
index 0554105..d36f68f 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
@@ -50,8 +50,10 @@
@Override
public void onChildViewHolderSelected(RecyclerView parent,
RecyclerView.ViewHolder view, int position, int subposition) {
- mSelectedPosition = position;
- onRowSelected(parent, view, position, subposition);
+ if (!mLateSelectionObserver.mIsLateSelection) {
+ mSelectedPosition = position;
+ onRowSelected(parent, view, position, subposition);
+ }
}
};
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java b/v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java
index 483f3fb..c07f3b0 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java
@@ -170,7 +170,7 @@
/**
* Removes an item from the cache. This will force the item to be re-read
- * from the data source the next time (@link #get(int)} is called.
+ * from the data source the next time {@link #get(int)} is called.
*/
protected final void invalidateCache(int index) {
mItemCache.remove(index);
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java b/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java
index d2ee2f3..b307df8 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java
@@ -69,8 +69,8 @@
}
/**
- * Get a label for value. The label can be static ({@link #setStaticLabels(CharSequence[])}
- * or dynamically generated (@link {@link #setLabelFormat(String)} when static labels is null.
+ * Get a label for value. The label can be static {@link #setStaticLabels(CharSequence[])}
+ * or dynamically generated {@link #setLabelFormat(String)} when static labels is null.
*
* @param value Value between minValue and maxValue.
* @return Label for the value.
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
index 476023c..4148ff5 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
@@ -17,7 +17,6 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.graphics.Rect;
@@ -107,6 +106,12 @@
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
+ getVerticalGridView().requestLayout();
+ }
+ }, 100);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
ListRowPresenter lrp = new ListRowPresenter();
ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
setAdapter(adapter);
@@ -122,7 +127,6 @@
final VerticalGridView gridView = ((RowsFragment) mActivity.getTestFragment())
.getVerticalGridView();
- assertNull(gridView.findViewHolderForAdapterPosition(0));
assertEquals(7, gridView.getSelectedPosition());
assertNotNull(gridView.findViewHolderForAdapterPosition(7));
}
@@ -138,6 +142,12 @@
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
+ getVerticalGridView().requestLayout();
+ }
+ }, 100);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
loadData(adapter, 10, 1);
}
}, 1000);
@@ -150,7 +160,6 @@
final VerticalGridView gridView = ((RowsFragment) mActivity.getTestFragment())
.getVerticalGridView();
- assertNull(gridView.findViewHolderForAdapterPosition(0));
assertEquals(7, gridView.getSelectedPosition());
assertNotNull(gridView.findViewHolderForAdapterPosition(7));
}
@@ -178,7 +187,6 @@
final VerticalGridView gridView = ((RowsFragment) mActivity.getTestFragment())
.getVerticalGridView();
- assertNull(gridView.findViewHolderForAdapterPosition(0));
assertEquals(7, gridView.getSelectedPosition());
assertNotNull(gridView.findViewHolderForAdapterPosition(7));
}
@@ -220,7 +228,6 @@
// but we could get Fragment from static variable.
RowsFragment fragment = sLastF_restoreSelection.get();
final VerticalGridView gridView = fragment.getVerticalGridView();
- assertNull(gridView.findViewHolderForAdapterPosition(0));
assertEquals(7, gridView.getSelectedPosition());
assertNotNull(gridView.findViewHolderForAdapterPosition(7));
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
index f22b961..65ca014 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
@@ -20,7 +20,6 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.graphics.Rect;
@@ -110,6 +109,12 @@
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
+ getVerticalGridView().requestLayout();
+ }
+ }, 100);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
ListRowPresenter lrp = new ListRowPresenter();
ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
setAdapter(adapter);
@@ -125,7 +130,6 @@
final VerticalGridView gridView = ((RowsSupportFragment) mActivity.getTestFragment())
.getVerticalGridView();
- assertNull(gridView.findViewHolderForAdapterPosition(0));
assertEquals(7, gridView.getSelectedPosition());
assertNotNull(gridView.findViewHolderForAdapterPosition(7));
}
@@ -141,6 +145,12 @@
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
+ getVerticalGridView().requestLayout();
+ }
+ }, 100);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
loadData(adapter, 10, 1);
}
}, 1000);
@@ -153,7 +163,6 @@
final VerticalGridView gridView = ((RowsSupportFragment) mActivity.getTestFragment())
.getVerticalGridView();
- assertNull(gridView.findViewHolderForAdapterPosition(0));
assertEquals(7, gridView.getSelectedPosition());
assertNotNull(gridView.findViewHolderForAdapterPosition(7));
}
@@ -181,7 +190,6 @@
final VerticalGridView gridView = ((RowsSupportFragment) mActivity.getTestFragment())
.getVerticalGridView();
- assertNull(gridView.findViewHolderForAdapterPosition(0));
assertEquals(7, gridView.getSelectedPosition());
assertNotNull(gridView.findViewHolderForAdapterPosition(7));
}
@@ -223,7 +231,6 @@
// but we could get Fragment from static variable.
RowsSupportFragment fragment = sLastF_restoreSelection.get();
final VerticalGridView gridView = fragment.getVerticalGridView();
- assertNull(gridView.findViewHolderForAdapterPosition(0));
assertEquals(7, gridView.getSelectedPosition());
assertNotNull(gridView.findViewHolderForAdapterPosition(7));
diff --git a/v7/appcompat/src/android/support/v7/app/AlertDialog.java b/v7/appcompat/src/android/support/v7/app/AlertDialog.java
index 529a10c..051f5cc 100644
--- a/v7/appcompat/src/android/support/v7/app/AlertDialog.java
+++ b/v7/appcompat/src/android/support/v7/app/AlertDialog.java
@@ -145,9 +145,9 @@
}
/**
- * @see Builder#setCustomTitle(View)
- *
* This method has no effect if called after {@link #show()}.
+ *
+ * @see Builder#setCustomTitle(View)
*/
public void setCustomTitle(View customTitleView) {
mAlert.setCustomTitle(customTitleView);
diff --git a/v7/appcompat/src/android/support/v7/app/NotificationCompat.java b/v7/appcompat/src/android/support/v7/app/NotificationCompat.java
index bd72879..a744c5f 100644
--- a/v7/appcompat/src/android/support/v7/app/NotificationCompat.java
+++ b/v7/appcompat/src/android/support/v7/app/NotificationCompat.java
@@ -656,7 +656,7 @@
* Notification noti = new NotificationCompat.Builder()
* .setSmallIcon(R.drawable.ic_stat_player)
* .setLargeIcon(albumArtBitmap))
- * .setCustomContentView(contentView);
+ * .setCustomContentView(contentView)
* .setStyle(<b>new NotificationCompat.DecoratedCustomViewStyle()</b>)
* .build();
* </pre>
@@ -690,7 +690,7 @@
* Notification noti = new Notification.Builder()
* .setSmallIcon(R.drawable.ic_stat_player)
* .setLargeIcon(albumArtBitmap))
- * .setCustomContentView(contentView);
+ * .setCustomContentView(contentView)
* .setStyle(<b>new NotificationCompat.DecoratedMediaCustomViewStyle()</b>
* .setMediaSession(mySession))
* .build();
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java b/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java
index 44c05ae..34c07a3 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java
@@ -43,6 +43,8 @@
import android.graphics.Rect;
import android.os.SystemClock;
import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.LargeTest;
import android.support.test.filters.MediumTest;
import android.support.test.filters.SmallTest;
import android.support.v7.app.BaseInstrumentationTestCase;
@@ -125,8 +127,9 @@
.check(matches(isDisplayed()));
}
+ @FlakyTest(bugId = 33669575)
@Test
- @SmallTest
+ @LargeTest
public void testAnchoring() {
Builder popupBuilder = new Builder();
popupBuilder.wireToActionButton();
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/PopupMenuTest.java b/v7/appcompat/tests/src/android/support/v7/widget/PopupMenuTest.java
index 3cf511f..7d6e39f 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/PopupMenuTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/PopupMenuTest.java
@@ -48,6 +48,8 @@
import android.support.test.espresso.Root;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.LargeTest;
import android.support.test.filters.MediumTest;
import android.support.v7.app.BaseInstrumentationTestCase;
import android.support.v7.appcompat.test.R;
@@ -260,8 +262,9 @@
};
}
+ @FlakyTest(bugId = 33669575)
@Test
- @MediumTest
+ @LargeTest
public void testAnchoring() {
Builder menuBuilder = new Builder();
menuBuilder.wireToActionButton();
diff --git a/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java b/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java
index 7ade21d..2613f90 100644
--- a/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java
+++ b/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java
@@ -19,6 +19,7 @@
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.Log;
@@ -34,6 +35,11 @@
final class MediaRouterJellybean {
private static final String TAG = "MediaRouterJellybean";
+ // android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP = 0x80;
+ // android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES = 0x100;
+ // android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER = 0x200;
+ public static final int DEVICE_OUT_BLUETOOTH = 0x80 | 0x100 | 0x200;
+
public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1;
public static final int ROUTE_TYPE_LIVE_VIDEO = 0x2;
public static final int ROUTE_TYPE_USER = 0x00800000;
@@ -116,17 +122,15 @@
return new VolumeCallbackProxy<VolumeCallback>(callback);
}
- static boolean isBluetoothA2dpOn(Object routerObj) {
+ static boolean checkRoutedToBluetooth(Context context) {
try {
- Field globalRouterField = routerObj.getClass().getDeclaredField("sStatic");
- globalRouterField.setAccessible(true);
- Object globalRouterObj = globalRouterField.get(null);
- Method method = globalRouterObj.getClass().getDeclaredMethod("isBluetoothA2dpOn");
- method.setAccessible(true);
- Object result = method.invoke(globalRouterObj);
- return (Boolean) result;
- } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException
- | NoSuchMethodException | InvocationTargetException e) {
+ AudioManager audioManager = (AudioManager) context.getSystemService(
+ Context.AUDIO_SERVICE);
+ Method method = audioManager.getClass().getDeclaredMethod(
+ "getDevicesForStream", int.class);
+ int device = (Integer) method.invoke(audioManager, AudioManager.STREAM_MUSIC);
+ return (device & DEVICE_OUT_BLUETOOTH) != 0;
+ } catch (Exception e) {
return false;
}
}
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
index d398e15..3c8e64d 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
@@ -18,6 +18,7 @@
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.ContentResolver;
@@ -94,7 +95,7 @@
* the disconnect button to disconnect and keep playing.
* <p>
*
- * @see {@link MediaRouteDescriptor#canDisconnectAndKeepPlaying()}.
+ * @see MediaRouteDescriptor#canDisconnectAndKeepPlaying()
*/
public static final int UNSELECT_REASON_DISCONNECTED = 1;
/**
@@ -2249,30 +2250,20 @@
}
}
+ @TargetApi(16)
void syncSystemRoutes() {
Object routerObj = MediaRouterJellybean.getMediaRouter(mApplicationContext);
- // If a2dp is enabled, this means a BT route is the selected route, otherwise
- // the default route is the selected one.
- boolean a2dpEnabled = MediaRouterJellybean.isBluetoothA2dpOn(routerObj);
+ boolean routedToBluetooth = MediaRouterJellybean.checkRoutedToBluetooth(
+ mApplicationContext);
Object selectedRouteObj = MediaRouterJellybean.getSelectedRoute(
routerObj, MediaRouterJellybean.ALL_ROUTE_TYPES);
Object defaultRouteObj = mSystemProvider.getDefaultRoute();
+ Object bluetoothRouteObj = mSystemProvider.getSystemRoute(mBluetoothRoute);
- if (a2dpEnabled && selectedRouteObj == defaultRouteObj) {
- // A BT route is the currently selected route, but MediaRouter think the default
- // route is the selected one. By selecting the BT route via framework MediaRouter,
- // MediaRouter could correct its selected route information.
- for (Object routeObj : MediaRouterJellybean.getRoutes(routerObj)) {
- if (routeObj != defaultRouteObj) {
- MediaRouterJellybean.selectRoute(routerObj,
- MediaRouterJellybean.ALL_ROUTE_TYPES, routeObj);
- break;
- }
- }
- } else if (!a2dpEnabled && selectedRouteObj != defaultRouteObj) {
- // The default route is the currently selected route, but MediaRouter think a BT
- // route is the selected one. By selecting the default route via framework
- // MediaRouter, MediaRouter could correct its selected route information.
+ if (routedToBluetooth && selectedRouteObj == defaultRouteObj) {
+ MediaRouterJellybean.selectRoute(routerObj,
+ MediaRouterJellybean.ALL_ROUTE_TYPES, bluetoothRouteObj);
+ } else if (!routedToBluetooth && selectedRouteObj == bluetoothRouteObj) {
MediaRouterJellybean.selectRoute(routerObj,
MediaRouterJellybean.ALL_ROUTE_TYPES, defaultRouteObj);
}
@@ -2507,7 +2498,7 @@
}
if (mBluetoothRoute == null && !mRoutes.isEmpty()) {
for (RouteInfo route : mRoutes) {
- if (isSystemBluetoothRoute(route) && isRouteSelectable(route)) {
+ if (isSystemLiveAudioOnlyRoute(route) && isRouteSelectable(route)) {
mBluetoothRoute = route;
Log.i(TAG, "Found bluetooth route: " + mBluetoothRoute);
break;
@@ -2599,12 +2590,6 @@
SystemMediaRouteProvider.DEFAULT_ROUTE_ID);
}
- private boolean isSystemBluetoothRoute(RouteInfo route) {
- return route.getProviderInstance() == mSystemProvider
- && !route.mDescriptorId.equals(
- SystemMediaRouteProvider.DEFAULT_ROUTE_ID);
- }
-
private void setSelectedRouteInternal(RouteInfo route, int unselectReason) {
if (mSelectedRoute != route) {
if (mSelectedRoute != null) {
diff --git a/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java b/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java
index 5fcafa7..0833be3 100644
--- a/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java
+++ b/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java
@@ -103,6 +103,10 @@
return null;
}
+ protected Object getSystemRoute(MediaRouter.RouteInfo route) {
+ return null;
+ }
+
/**
* Legacy implementation for platform versions prior to Jellybean.
*/
@@ -664,6 +668,18 @@
return mGetDefaultRouteWorkaround.getDefaultRoute(mRouterObj);
}
+ @Override
+ protected Object getSystemRoute(MediaRouter.RouteInfo route) {
+ if (route == null) {
+ return null;
+ }
+ int index = findSystemRouteRecordByDescriptorId(route.getDescriptorId());
+ if (index >= 0) {
+ return mSystemRouteRecords.get(index).mRouteObj;
+ }
+ return null;
+ }
+
/**
* Represents a route that is provided by the framework media router
* and published by this route provider to the support library media router.
diff --git a/v7/recyclerview/src/android/support/v7/util/SortedList.java b/v7/recyclerview/src/android/support/v7/util/SortedList.java
index 74cf1fe..f96433f 100644
--- a/v7/recyclerview/src/android/support/v7/util/SortedList.java
+++ b/v7/recyclerview/src/android/support/v7/util/SortedList.java
@@ -126,9 +126,9 @@
* @param item The item to be added into the list.
*
* @return The index of the newly added item.
- * @see {@link Callback#compare(Object, Object)}
- * @see {@link Callback#areItemsTheSame(Object, Object)}
- * @see {@link Callback#areContentsTheSame(Object, Object)}}
+ * @see Callback#compare(Object, Object)
+ * @see Callback#areItemsTheSame(Object, Object)
+ * @see Callback#areContentsTheSame(Object, Object)}
*/
public int add(T item) {
throwIfMerging();
@@ -145,7 +145,7 @@
* </p>
* @param items Array of items to be added into the list.
* @param mayModifyInput If true, SortedList is allowed to modify the input.
- * @see {@link SortedList#addAll(Object[] items)}.
+ * @see SortedList#addAll(Object[] items)
*/
public void addAll(T[] items, boolean mayModifyInput) {
throwIfMerging();
@@ -165,7 +165,7 @@
/**
* Adds the given items to the list. Does not modify the input.
*
- * @see {@link SortedList#addAll(T[] items, boolean mayModifyInput)}
+ * @see SortedList#addAll(T[] items, boolean mayModifyInput)
*
* @param items Array of items to be added into the list.
*/
@@ -176,7 +176,7 @@
/**
* Adds the given items to the list. Does not modify the input.
*
- * @see {@link SortedList#addAll(T[] items, boolean mayModifyInput)}
+ * @see SortedList#addAll(T[] items, boolean mayModifyInput)
*
* @param items Collection of items to be added into the list.
*/
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index 24936e0..479d684 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -1339,7 +1339,7 @@
*
* @param extension ViewCacheExtension to be used or null if you want to clear the existing one.
*
- * @see {@link ViewCacheExtension#getViewForPositionAndType(Recycler, int, int)}
+ * @see ViewCacheExtension#getViewForPositionAndType(Recycler, int, int)
*/
public void setViewCacheExtension(ViewCacheExtension extension) {
mRecycler.setViewCacheExtension(extension);