Merge "Add PlaybackControlGlue for support v4 fragment" into mnc-ub-dev
diff --git a/design/build.gradle b/design/build.gradle
index e785f83..af46827 100644
--- a/design/build.gradle
+++ b/design/build.gradle
@@ -39,3 +39,70 @@
consumerProguardFiles 'proguard-rules.pro'
}
}
+
+android.libraryVariants.all { variant ->
+ def name = variant.buildType.name
+
+ if (name.equals(com.android.builder.core.BuilderConstants.DEBUG)) {
+ return; // Skip debug builds.
+ }
+ def suffix = name.capitalize()
+
+ def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
+ dependsOn variant.javaCompile
+ from variant.javaCompile.destinationDir
+ from 'LICENSE.txt'
+ }
+ def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
+ source android.sourceSets.main.java
+ classpath = files(variant.javaCompile.classpath.files) + files(
+ "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
+ }
+
+ def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
+ classifier = 'javadoc'
+ from 'build/docs/javadoc'
+ }
+
+ def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
+ classifier = 'sources'
+ from android.sourceSets.main.java.srcDirs
+ }
+
+ artifacts.add('archives', javadocJarTask);
+ artifacts.add('archives', sourcesJarTask);
+}
+
+uploadArchives {
+ repositories {
+ mavenDeployer {
+ repository(url: uri(rootProject.ext.supportRepoOut)) {
+ }
+
+ pom.project {
+ name 'Android Design Support Library'
+ description "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 4 or later."
+ url 'http://developer.android.com/tools/extras/support-library.html'
+ inceptionYear '2011'
+
+ licenses {
+ license {
+ name 'The Apache Software License, Version 2.0'
+ url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+ distribution 'repo'
+ }
+ }
+
+ scm {
+ url "http://source.android.com"
+ connection "scm:git:https://android.googlesource.com/platform/frameworks/support"
+ }
+ developers {
+ developer {
+ name 'The Android Open Source Project'
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/v17/preference-leanback/res/layout/leanback_preference.xml b/v17/preference-leanback/res/layout/leanback_preference.xml
index 498b1073..05ca21c 100644
--- a/v17/preference-leanback/res/layout/leanback_preference.xml
+++ b/v17/preference-leanback/res/layout/leanback_preference.xml
@@ -34,8 +34,8 @@
android:layout_gravity="center_vertical" >
<ImageView
android:id="@android:id/icon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
+ android:layout_width="@dimen/lb_preference_item_icon_size"
+ android:layout_height="@dimen/lb_preference_item_icon_size"
android:layout_marginEnd="@dimen/lb_preference_item_icon_margin_end"
/>
</FrameLayout>
diff --git a/v4/java/android/support/v4/media/MediaBrowserCompat.java b/v4/java/android/support/v4/media/MediaBrowserCompat.java
index b4559b0..6e2c9ad 100644
--- a/v4/java/android/support/v4/media/MediaBrowserCompat.java
+++ b/v4/java/android/support/v4/media/MediaBrowserCompat.java
@@ -52,29 +52,7 @@
* @hide
*/
public final class MediaBrowserCompat {
- private static final String TAG = "MediaBrowserCompat";
- private static final boolean DBG = false;
-
- private static final int CONNECT_STATE_DISCONNECTED = 0;
- private static final int CONNECT_STATE_CONNECTING = 1;
- private static final int CONNECT_STATE_CONNECTED = 2;
- private static final int CONNECT_STATE_SUSPENDED = 3;
-
- private final Context mContext;
- private final ComponentName mServiceComponent;
- private final ConnectionCallback mCallback;
- private final Bundle mRootHints;
- private final Handler mHandler = new Handler();
- private final ArrayMap<String,Subscription> mSubscriptions =
- new ArrayMap<String, MediaBrowserCompat.Subscription>();
-
- private int mState = CONNECT_STATE_DISCONNECTED;
- private MediaServiceConnection mServiceConnection;
- private IMediaBrowserServiceCompat mServiceBinder;
- private IMediaBrowserServiceCompatCallbacks mServiceCallbacks;
- private String mRootId;
- private MediaSessionCompat.Token mMediaSessionToken;
- private Bundle mExtras;
+ private final MediaBrowserImplBase mImpl;
/**
* Creates a media browser for the specified media browse service.
@@ -89,19 +67,7 @@
*/
public MediaBrowserCompat(Context context, ComponentName serviceComponent,
ConnectionCallback callback, Bundle rootHints) {
- if (context == null) {
- throw new IllegalArgumentException("context must not be null");
- }
- if (serviceComponent == null) {
- throw new IllegalArgumentException("service component must not be null");
- }
- if (callback == null) {
- throw new IllegalArgumentException("connection callback must not be null");
- }
- mContext = context;
- mServiceComponent = serviceComponent;
- mCallback = callback;
- mRootHints = rootHints;
+ mImpl = new MediaBrowserImplBase(context, serviceComponent, callback, rootHints);
}
/**
@@ -112,62 +78,7 @@
* </p>
*/
public void connect() {
- if (mState != CONNECT_STATE_DISCONNECTED) {
- throw new IllegalStateException("connect() called while not disconnected (state="
- + getStateLabel(mState) + ")");
- }
- // TODO: remove this extra check.
- if (DBG) {
- if (mServiceConnection != null) {
- throw new RuntimeException("mServiceConnection should be null. Instead it is "
- + mServiceConnection);
- }
- }
- if (mServiceBinder != null) {
- throw new RuntimeException("mServiceBinder should be null. Instead it is "
- + mServiceBinder);
- }
- if (mServiceCallbacks != null) {
- throw new RuntimeException("mServiceCallbacks should be null. Instead it is "
- + mServiceCallbacks);
- }
-
- mState = CONNECT_STATE_CONNECTING;
-
- final Intent intent = new Intent(MediaBrowserServiceCompat.SERVICE_INTERFACE);
- intent.setComponent(mServiceComponent);
-
- final ServiceConnection thisConnection = mServiceConnection = new MediaServiceConnection();
-
- boolean bound = false;
- try {
- bound = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
- } catch (Exception ex) {
- Log.e(TAG, "Failed binding to service " + mServiceComponent);
- }
-
- if (!bound) {
- // Tell them that it didn't work. We are already on the main thread,
- // but we don't want to do callbacks inside of connect(). So post it,
- // and then check that we are on the same ServiceConnection. We know
- // we won't also get an onServiceConnected or onServiceDisconnected,
- // so we won't be doing double callbacks.
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- // Ensure that nobody else came in or tried to connect again.
- if (thisConnection == mServiceConnection) {
- forceCloseConnection();
- mCallback.onConnectionFailed();
- }
- }
- });
- }
-
- if (DBG) {
- Log.d(TAG, "connect...");
- dump();
- }
+ mImpl.connect();
}
/**
@@ -175,53 +86,14 @@
* After this, no more callbacks will be received.
*/
public void disconnect() {
- // It's ok to call this any state, because allowing this lets apps not have
- // to check isConnected() unnecessarily. They won't appreciate the extra
- // assertions for this. We do everything we can here to go back to a sane state.
- if (mServiceCallbacks != null) {
- try {
- mServiceBinder.disconnect(mServiceCallbacks);
- } catch (RemoteException ex) {
- // We are disconnecting anyway. Log, just for posterity but it's not
- // a big problem.
- Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
- }
- }
- forceCloseConnection();
-
- if (DBG) {
- Log.d(TAG, "disconnect...");
- dump();
- }
- }
-
- /**
- * Null out the variables and unbind from the service. This doesn't include
- * calling disconnect on the service, because we only try to do that in the
- * clean shutdown cases.
- * <p>
- * Everywhere that calls this EXCEPT for disconnect() should follow it with
- * a call to mCallback.onConnectionFailed(). Disconnect doesn't do that callback
- * for a clean shutdown, but everywhere else is a dirty shutdown and should
- * notify the app.
- */
- private void forceCloseConnection() {
- if (mServiceConnection != null) {
- mContext.unbindService(mServiceConnection);
- }
- mState = CONNECT_STATE_DISCONNECTED;
- mServiceConnection = null;
- mServiceBinder = null;
- mServiceCallbacks = null;
- mRootId = null;
- mMediaSessionToken = null;
+ mImpl.disconnect();
}
/**
* Returns whether the browser is connected to the service.
*/
public boolean isConnected() {
- return mState == CONNECT_STATE_CONNECTED;
+ return mImpl.isConnected();
}
/**
@@ -229,11 +101,7 @@
*/
public @NonNull
ComponentName getServiceComponent() {
- if (!isConnected()) {
- throw new IllegalStateException("getServiceComponent() called while not connected" +
- " (state=" + mState + ")");
- }
- return mServiceComponent;
+ return mImpl.getServiceComponent();
}
/**
@@ -246,11 +114,7 @@
* @throws IllegalStateException if not connected.
*/
public @NonNull String getRoot() {
- if (!isConnected()) {
- throw new IllegalStateException("getSessionToken() called while not connected (state="
- + getStateLabel(mState) + ")");
- }
- return mRootId;
+ return mImpl.getRoot();
}
/**
@@ -260,11 +124,7 @@
*/
public @Nullable
Bundle getExtras() {
- if (!isConnected()) {
- throw new IllegalStateException("getExtras() called while not connected (state="
- + getStateLabel(mState) + ")");
- }
- return mExtras;
+ return mImpl.getExtras();
}
/**
@@ -279,11 +139,7 @@
* @throws IllegalStateException if not connected.
*/
public @NonNull MediaSessionCompat.Token getSessionToken() {
- if (!isConnected()) {
- throw new IllegalStateException("getSessionToken() called while not connected (state="
- + mState + ")");
- }
- return mMediaSessionToken;
+ return mImpl.getSessionToken();
}
/**
@@ -305,34 +161,7 @@
* @param callback The callback to receive the list of children.
*/
public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
- // Check arguments.
- if (parentId == null) {
- throw new IllegalArgumentException("parentId is null");
- }
- if (callback == null) {
- throw new IllegalArgumentException("callback is null");
- }
-
- // Update or create the subscription.
- Subscription sub = mSubscriptions.get(parentId);
- boolean newSubscription = sub == null;
- if (newSubscription) {
- sub = new Subscription(parentId);
- mSubscriptions.put(parentId, sub);
- }
- sub.callback = callback;
-
- // If we are connected, tell the service that we are watching. If we aren't
- // connected, the service will be told when we connect.
- if (mState == CONNECT_STATE_CONNECTED) {
- try {
- mServiceBinder.addSubscription(parentId, mServiceCallbacks);
- } catch (RemoteException ex) {
- // Process is crashing. We will disconnect, and upon reconnect we will
- // automatically reregister. So nothing to do here.
- Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId);
- }
- }
+ mImpl.subscribe(parentId, callback);
}
/**
@@ -346,24 +175,7 @@
* will be unsubscribed.
*/
public void unsubscribe(@NonNull String parentId) {
- // Check arguments.
- if (TextUtils.isEmpty(parentId)) {
- throw new IllegalArgumentException("parentId is empty.");
- }
-
- // Remove from our list.
- final Subscription sub = mSubscriptions.remove(parentId);
-
- // Tell the service if necessary.
- if (mState == CONNECT_STATE_CONNECTED && sub != null) {
- try {
- mServiceBinder.removeSubscription(parentId, mServiceCallbacks);
- } catch (RemoteException ex) {
- // Process is crashing. We will disconnect, and upon reconnect we will
- // automatically reregister. So nothing to do here.
- Log.d(TAG, "removeSubscription failed with RemoteException parentId=" + parentId);
- }
- }
+ mImpl.unsubscribe(parentId);
}
/**
@@ -375,207 +187,7 @@
* @param cb The callback to receive the result on.
*/
public void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb) {
- if (TextUtils.isEmpty(mediaId)) {
- throw new IllegalArgumentException("mediaId is empty.");
- }
- if (cb == null) {
- throw new IllegalArgumentException("cb is null.");
- }
- if (mState != CONNECT_STATE_CONNECTED) {
- Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- cb.onError(mediaId);
- }
- });
- return;
- }
- ResultReceiver receiver = new ResultReceiver(mHandler) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- if (resultCode != 0 || resultData == null
- || !resultData.containsKey(MediaBrowserServiceCompat.KEY_MEDIA_ITEM)) {
- cb.onError(mediaId);
- return;
- }
- Parcelable item =
- resultData.getParcelable(MediaBrowserServiceCompat.KEY_MEDIA_ITEM);
- if (!(item instanceof MediaItem)) {
- cb.onError(mediaId);
- return;
- }
- cb.onItemLoaded((MediaItem)item);
- }
- };
- try {
- mServiceBinder.getMediaItem(mediaId, receiver);
- } catch (RemoteException e) {
- Log.i(TAG, "Remote error getting media item.");
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- cb.onError(mediaId);
- }
- });
- }
- }
-
- /**
- * For debugging.
- */
- private static String getStateLabel(int state) {
- switch (state) {
- case CONNECT_STATE_DISCONNECTED:
- return "CONNECT_STATE_DISCONNECTED";
- case CONNECT_STATE_CONNECTING:
- return "CONNECT_STATE_CONNECTING";
- case CONNECT_STATE_CONNECTED:
- return "CONNECT_STATE_CONNECTED";
- case CONNECT_STATE_SUSPENDED:
- return "CONNECT_STATE_SUSPENDED";
- default:
- return "UNKNOWN/" + state;
- }
- }
-
- private final void onServiceConnected(final IMediaBrowserServiceCompatCallbacks callback,
- final String root, final MediaSessionCompat.Token session, final Bundle extra) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- // Check to make sure there hasn't been a disconnect or a different
- // ServiceConnection.
- if (!isCurrent(callback, "onConnect")) {
- return;
- }
- // Don't allow them to call us twice.
- if (mState != CONNECT_STATE_CONNECTING) {
- Log.w(TAG, "onConnect from service while mState="
- + getStateLabel(mState) + "... ignoring");
- return;
- }
- mRootId = root;
- mMediaSessionToken = session;
- mExtras = extra;
- mState = CONNECT_STATE_CONNECTED;
-
- if (DBG) {
- Log.d(TAG, "ServiceCallbacks.onConnect...");
- dump();
- }
- mCallback.onConnected();
-
- // we may receive some subscriptions before we are connected, so re-subscribe
- // everything now
- for (String id : mSubscriptions.keySet()) {
- try {
- mServiceBinder.addSubscription(id, mServiceCallbacks);
- } catch (RemoteException ex) {
- // Process is crashing. We will disconnect, and upon reconnect we will
- // automatically reregister. So nothing to do here.
- Log.d(TAG, "addSubscription failed with RemoteException parentId=" + id);
- }
- }
- }
- });
- }
-
- private final void onConnectionFailed(final IMediaBrowserServiceCompatCallbacks callback) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- Log.e(TAG, "onConnectFailed for " + mServiceComponent);
-
- // Check to make sure there hasn't been a disconnect or a different
- // ServiceConnection.
- if (!isCurrent(callback, "onConnectFailed")) {
- return;
- }
- // Don't allow them to call us twice.
- if (mState != CONNECT_STATE_CONNECTING) {
- Log.w(TAG, "onConnect from service while mState="
- + getStateLabel(mState) + "... ignoring");
- return;
- }
-
- // Clean up
- forceCloseConnection();
-
- // Tell the app.
- mCallback.onConnectionFailed();
- }
- });
- }
-
- private final void onLoadChildren(final IMediaBrowserServiceCompatCallbacks callback,
- final String parentId, final List list) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- // Check that there hasn't been a disconnect or a different
- // ServiceConnection.
- if (!isCurrent(callback, "onLoadChildren")) {
- return;
- }
-
- List<MediaItem> data = list;
- if (DBG) {
- Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId);
- }
- if (data == null) {
- data = Collections.emptyList();
- }
-
- // Check that the subscription is still subscribed.
- final Subscription subscription = mSubscriptions.get(parentId);
- if (subscription == null) {
- if (DBG) {
- Log.d(TAG, "onLoadChildren for id that isn't subscribed id="
- + parentId);
- }
- return;
- }
-
- // Tell the app.
- subscription.callback.onChildrenLoaded(parentId, data);
- }
- });
- }
-
- /**
- * Return true if {@code callback} is the current ServiceCallbacks. Also logs if it's not.
- */
- private boolean isCurrent(IMediaBrowserServiceCompatCallbacks callback, String funcName) {
- if (mServiceCallbacks != callback) {
- if (mState != CONNECT_STATE_DISCONNECTED) {
- Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
- + mServiceCallbacks + " this=" + this);
- }
- return false;
- }
- return true;
- }
-
- private ServiceCallbacks getNewServiceCallbacks() {
- return new ServiceCallbacks(this);
- }
-
- /**
- * Log internal state.
- * @hide
- */
- void dump() {
- Log.d(TAG, "MediaBrowserCompat...");
- Log.d(TAG, " mServiceComponent=" + mServiceComponent);
- Log.d(TAG, " mCallback=" + mCallback);
- Log.d(TAG, " mRootHints=" + mRootHints);
- Log.d(TAG, " mState=" + getStateLabel(mState));
- Log.d(TAG, " mServiceConnection=" + mServiceConnection);
- Log.d(TAG, " mServiceBinder=" + mServiceBinder);
- Log.d(TAG, " mServiceCallbacks=" + mServiceCallbacks);
- Log.d(TAG, " mRootId=" + mRootId);
- Log.d(TAG, " mMediaSessionToken=" + mMediaSessionToken);
+ mImpl.getItem(mediaId, cb);
}
public static class MediaItem implements Parcelable {
@@ -772,140 +384,582 @@
}
}
- /**
- * ServiceConnection to the other app.
- */
- private class MediaServiceConnection implements ServiceConnection {
- @Override
- public void onServiceConnected(ComponentName name, IBinder binder) {
+ static class MediaBrowserImplBase {
+ private static final String TAG = "MediaBrowserCompat";
+ private static final boolean DBG = false;
+
+ private static final int CONNECT_STATE_DISCONNECTED = 0;
+ private static final int CONNECT_STATE_CONNECTING = 1;
+ private static final int CONNECT_STATE_CONNECTED = 2;
+ private static final int CONNECT_STATE_SUSPENDED = 3;
+
+ private final Context mContext;
+ private final ComponentName mServiceComponent;
+ private final ConnectionCallback mCallback;
+ private final Bundle mRootHints;
+ private final Handler mHandler = new Handler();
+ private final ArrayMap<String,Subscription> mSubscriptions = new ArrayMap<>();
+
+ private int mState = CONNECT_STATE_DISCONNECTED;
+ private MediaServiceConnection mServiceConnection;
+ private IMediaBrowserServiceCompat mServiceBinder;
+ private IMediaBrowserServiceCompatCallbacks mServiceCallbacks;
+ private String mRootId;
+ private MediaSessionCompat.Token mMediaSessionToken;
+ private Bundle mExtras;
+
+ public MediaBrowserImplBase(Context context, ComponentName serviceComponent,
+ ConnectionCallback callback, Bundle rootHints) {
+ if (context == null) {
+ throw new IllegalArgumentException("context must not be null");
+ }
+ if (serviceComponent == null) {
+ throw new IllegalArgumentException("service component must not be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("connection callback must not be null");
+ }
+ mContext = context;
+ mServiceComponent = serviceComponent;
+ mCallback = callback;
+ mRootHints = rootHints;
+ }
+
+ public void connect() {
+ if (mState != CONNECT_STATE_DISCONNECTED) {
+ throw new IllegalStateException("connect() called while not disconnected (state="
+ + getStateLabel(mState) + ")");
+ }
+ // TODO: remove this extra check.
if (DBG) {
- Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name
- + " binder=" + binder);
- dump();
+ if (mServiceConnection != null) {
+ throw new RuntimeException("mServiceConnection should be null. Instead it is "
+ + mServiceConnection);
+ }
+ }
+ if (mServiceBinder != null) {
+ throw new RuntimeException("mServiceBinder should be null. Instead it is "
+ + mServiceBinder);
+ }
+ if (mServiceCallbacks != null) {
+ throw new RuntimeException("mServiceCallbacks should be null. Instead it is "
+ + mServiceCallbacks);
}
- // Make sure we are still the current connection, and that they haven't called
- // disconnect().
- if (!isCurrent("onServiceConnected")) {
- return;
- }
-
- // Save their binder
- mServiceBinder = IMediaBrowserServiceCompat.Stub.asInterface(binder);
-
- // We make a new mServiceCallbacks each time we connect so that we can drop
- // responses from previous connections.
- mServiceCallbacks = getNewServiceCallbacks();
mState = CONNECT_STATE_CONNECTING;
- // Call connect, which is async. When we get a response from that we will
- // say that we're connected.
+ final Intent intent = new Intent(MediaBrowserServiceCompat.SERVICE_INTERFACE);
+ intent.setComponent(mServiceComponent);
+
+ final ServiceConnection thisConnection = mServiceConnection =
+ new MediaServiceConnection();
+
+ boolean bound = false;
try {
- if (DBG) {
- Log.d(TAG, "ServiceCallbacks.onConnect...");
- dump();
- }
- mServiceBinder.connect(mContext.getPackageName(), mRootHints, mServiceCallbacks);
- } catch (RemoteException ex) {
- // Connect failed, which isn't good. But the auto-reconnect on the service
- // will take over and we will come back. We will also get the
- // onServiceDisconnected, which has all the cleanup code. So let that do it.
- Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
- if (DBG) {
- Log.d(TAG, "ServiceCallbacks.onConnect...");
- dump();
- }
+ bound = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+ } catch (Exception ex) {
+ Log.e(TAG, "Failed binding to service " + mServiceComponent);
+ }
+
+ if (!bound) {
+ // Tell them that it didn't work. We are already on the main thread,
+ // but we don't want to do callbacks inside of connect(). So post it,
+ // and then check that we are on the same ServiceConnection. We know
+ // we won't also get an onServiceConnected or onServiceDisconnected,
+ // so we won't be doing double callbacks.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // Ensure that nobody else came in or tried to connect again.
+ if (thisConnection == mServiceConnection) {
+ forceCloseConnection();
+ mCallback.onConnectionFailed();
+ }
+ }
+ });
+ }
+
+ if (DBG) {
+ Log.d(TAG, "connect...");
+ dump();
}
}
- @Override
- public void onServiceDisconnected(ComponentName name) {
+ public void disconnect() {
+ // It's ok to call this any state, because allowing this lets apps not have
+ // to check isConnected() unnecessarily. They won't appreciate the extra
+ // assertions for this. We do everything we can here to go back to a sane state.
+ if (mServiceCallbacks != null) {
+ try {
+ mServiceBinder.disconnect(mServiceCallbacks);
+ } catch (RemoteException ex) {
+ // We are disconnecting anyway. Log, just for posterity but it's not
+ // a big problem.
+ Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
+ }
+ }
+ forceCloseConnection();
+
if (DBG) {
- Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name
- + " this=" + this + " mServiceConnection=" + mServiceConnection);
+ Log.d(TAG, "disconnect...");
dump();
}
-
- // Make sure we are still the current connection, and that they haven't called
- // disconnect().
- if (!isCurrent("onServiceDisconnected")) {
- return;
- }
-
- // Clear out what we set in onServiceConnected
- mServiceBinder = null;
- mServiceCallbacks = null;
-
- // And tell the app that it's suspended.
- mState = CONNECT_STATE_SUSPENDED;
- mCallback.onConnectionSuspended();
}
/**
- * Return true if this is the current ServiceConnection. Also logs if it's not.
+ * Null out the variables and unbind from the service. This doesn't include
+ * calling disconnect on the service, because we only try to do that in the
+ * clean shutdown cases.
+ * <p>
+ * Everywhere that calls this EXCEPT for disconnect() should follow it with
+ * a call to mCallback.onConnectionFailed(). Disconnect doesn't do that callback
+ * for a clean shutdown, but everywhere else is a dirty shutdown and should
+ * notify the app.
*/
- private boolean isCurrent(String funcName) {
- if (mServiceConnection != this) {
+ private void forceCloseConnection() {
+ if (mServiceConnection != null) {
+ mContext.unbindService(mServiceConnection);
+ }
+ mState = CONNECT_STATE_DISCONNECTED;
+ mServiceConnection = null;
+ mServiceBinder = null;
+ mServiceCallbacks = null;
+ mRootId = null;
+ mMediaSessionToken = null;
+ }
+
+ public boolean isConnected() {
+ return mState == CONNECT_STATE_CONNECTED;
+ }
+
+ public @NonNull
+ ComponentName getServiceComponent() {
+ if (!isConnected()) {
+ throw new IllegalStateException("getServiceComponent() called while not connected" +
+ " (state=" + mState + ")");
+ }
+ return mServiceComponent;
+ }
+
+ public @NonNull String getRoot() {
+ if (!isConnected()) {
+ throw new IllegalStateException("getSessionToken() called while not connected"
+ + "(state=" + getStateLabel(mState) + ")");
+ }
+ return mRootId;
+ }
+
+ public @Nullable
+ Bundle getExtras() {
+ if (!isConnected()) {
+ throw new IllegalStateException("getExtras() called while not connected (state="
+ + getStateLabel(mState) + ")");
+ }
+ return mExtras;
+ }
+
+ public @NonNull MediaSessionCompat.Token getSessionToken() {
+ if (!isConnected()) {
+ throw new IllegalStateException("getSessionToken() called while not connected"
+ + "(state=" + mState + ")");
+ }
+ return mMediaSessionToken;
+ }
+
+ public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
+ // Check arguments.
+ if (parentId == null) {
+ throw new IllegalArgumentException("parentId is null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback is null");
+ }
+
+ // Update or create the subscription.
+ Subscription sub = mSubscriptions.get(parentId);
+ boolean newSubscription = sub == null;
+ if (newSubscription) {
+ sub = new Subscription(parentId);
+ mSubscriptions.put(parentId, sub);
+ }
+ sub.callback = callback;
+
+ // If we are connected, tell the service that we are watching. If we aren't
+ // connected, the service will be told when we connect.
+ if (mState == CONNECT_STATE_CONNECTED) {
+ try {
+ mServiceBinder.addSubscription(parentId, mServiceCallbacks);
+ } catch (RemoteException ex) {
+ // Process is crashing. We will disconnect, and upon reconnect we will
+ // automatically reregister. So nothing to do here.
+ Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId);
+ }
+ }
+ }
+
+ public void unsubscribe(@NonNull String parentId) {
+ // Check arguments.
+ if (TextUtils.isEmpty(parentId)) {
+ throw new IllegalArgumentException("parentId is empty.");
+ }
+
+ // Remove from our list.
+ final Subscription sub = mSubscriptions.remove(parentId);
+
+ // Tell the service if necessary.
+ if (mState == CONNECT_STATE_CONNECTED && sub != null) {
+ try {
+ mServiceBinder.removeSubscription(parentId, mServiceCallbacks);
+ } catch (RemoteException ex) {
+ // Process is crashing. We will disconnect, and upon reconnect we will
+ // automatically reregister. So nothing to do here.
+ Log.d(TAG, "removeSubscription failed with RemoteException parentId="
+ + parentId);
+ }
+ }
+ }
+
+ public void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb) {
+ if (TextUtils.isEmpty(mediaId)) {
+ throw new IllegalArgumentException("mediaId is empty.");
+ }
+ if (cb == null) {
+ throw new IllegalArgumentException("cb is null.");
+ }
+ if (mState != CONNECT_STATE_CONNECTED) {
+ Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ cb.onError(mediaId);
+ }
+ });
+ return;
+ }
+ ResultReceiver receiver = new ResultReceiver(mHandler) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultCode != 0 || resultData == null
+ || !resultData.containsKey(MediaBrowserServiceCompat.KEY_MEDIA_ITEM)) {
+ cb.onError(mediaId);
+ return;
+ }
+ Parcelable item =
+ resultData.getParcelable(MediaBrowserServiceCompat.KEY_MEDIA_ITEM);
+ if (!(item instanceof MediaItem)) {
+ cb.onError(mediaId);
+ return;
+ }
+ cb.onItemLoaded((MediaItem)item);
+ }
+ };
+ try {
+ mServiceBinder.getMediaItem(mediaId, receiver);
+ } catch (RemoteException e) {
+ Log.i(TAG, "Remote error getting media item.");
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ cb.onError(mediaId);
+ }
+ });
+ }
+ }
+
+ /**
+ * For debugging.
+ */
+ private static String getStateLabel(int state) {
+ switch (state) {
+ case CONNECT_STATE_DISCONNECTED:
+ return "CONNECT_STATE_DISCONNECTED";
+ case CONNECT_STATE_CONNECTING:
+ return "CONNECT_STATE_CONNECTING";
+ case CONNECT_STATE_CONNECTED:
+ return "CONNECT_STATE_CONNECTED";
+ case CONNECT_STATE_SUSPENDED:
+ return "CONNECT_STATE_SUSPENDED";
+ default:
+ return "UNKNOWN/" + state;
+ }
+ }
+
+ private final void onServiceConnected(final IMediaBrowserServiceCompatCallbacks callback,
+ final String root, final MediaSessionCompat.Token session, final Bundle extra) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // Check to make sure there hasn't been a disconnect or a different
+ // ServiceConnection.
+ if (!isCurrent(callback, "onConnect")) {
+ return;
+ }
+ // Don't allow them to call us twice.
+ if (mState != CONNECT_STATE_CONNECTING) {
+ Log.w(TAG, "onConnect from service while mState="
+ + getStateLabel(mState) + "... ignoring");
+ return;
+ }
+ mRootId = root;
+ mMediaSessionToken = session;
+ mExtras = extra;
+ mState = CONNECT_STATE_CONNECTED;
+
+ if (DBG) {
+ Log.d(TAG, "ServiceCallbacks.onConnect...");
+ dump();
+ }
+ mCallback.onConnected();
+
+ // we may receive some subscriptions before we are connected, so re-subscribe
+ // everything now
+ for (String id : mSubscriptions.keySet()) {
+ try {
+ mServiceBinder.addSubscription(id, mServiceCallbacks);
+ } catch (RemoteException ex) {
+ // Process is crashing. We will disconnect, and upon reconnect we will
+ // automatically reregister. So nothing to do here.
+ Log.d(TAG, "addSubscription failed with RemoteException parentId="
+ + id);
+ }
+ }
+ }
+ });
+ }
+
+ private final void onConnectionFailed(final IMediaBrowserServiceCompatCallbacks callback) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ Log.e(TAG, "onConnectFailed for " + mServiceComponent);
+
+ // Check to make sure there hasn't been a disconnect or a different
+ // ServiceConnection.
+ if (!isCurrent(callback, "onConnectFailed")) {
+ return;
+ }
+ // Don't allow them to call us twice.
+ if (mState != CONNECT_STATE_CONNECTING) {
+ Log.w(TAG, "onConnect from service while mState="
+ + getStateLabel(mState) + "... ignoring");
+ return;
+ }
+
+ // Clean up
+ forceCloseConnection();
+
+ // Tell the app.
+ mCallback.onConnectionFailed();
+ }
+ });
+ }
+
+ private final void onLoadChildren(final IMediaBrowserServiceCompatCallbacks callback,
+ final String parentId, final List list) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // Check that there hasn't been a disconnect or a different
+ // ServiceConnection.
+ if (!isCurrent(callback, "onLoadChildren")) {
+ return;
+ }
+
+ List<MediaItem> data = list;
+ if (DBG) {
+ Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId);
+ }
+ if (data == null) {
+ data = Collections.emptyList();
+ }
+
+ // Check that the subscription is still subscribed.
+ final Subscription subscription = mSubscriptions.get(parentId);
+ if (subscription == null) {
+ if (DBG) {
+ Log.d(TAG, "onLoadChildren for id that isn't subscribed id="
+ + parentId);
+ }
+ return;
+ }
+
+ // Tell the app.
+ subscription.callback.onChildrenLoaded(parentId, data);
+ }
+ });
+ }
+
+ /**
+ * Return true if {@code callback} is the current ServiceCallbacks. Also logs if it's not.
+ */
+ private boolean isCurrent(IMediaBrowserServiceCompatCallbacks callback, String funcName) {
+ if (mServiceCallbacks != callback) {
if (mState != CONNECT_STATE_DISCONNECTED) {
- // Check mState, because otherwise this log is noisy.
Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
- + mServiceConnection + " this=" + this);
+ + mServiceCallbacks + " this=" + this);
}
return false;
}
return true;
}
- }
- /**
- * Callbacks from the service.
- */
- private static class ServiceCallbacks extends IMediaBrowserServiceCompatCallbacks.Stub {
- private WeakReference<MediaBrowserCompat> mMediaBrowser;
-
- public ServiceCallbacks(MediaBrowserCompat mediaBrowser) {
- mMediaBrowser = new WeakReference<MediaBrowserCompat>(mediaBrowser);
+ private ServiceCallbacks getNewServiceCallbacks() {
+ return new ServiceCallbacks(this);
}
/**
- * The other side has acknowledged our connection. The parameters to this function
- * are the initial data as requested.
+ * Log internal state.
+ * @hide
*/
- @Override
- public void onConnect(final String root, final MediaSessionCompat.Token session,
- final Bundle extras) {
- MediaBrowserCompat mediaBrowser = mMediaBrowser.get();
- if (mediaBrowser != null) {
- mediaBrowser.onServiceConnected(this, root, session, extras);
+ void dump() {
+ Log.d(TAG, "MediaBrowserCompat...");
+ Log.d(TAG, " mServiceComponent=" + mServiceComponent);
+ Log.d(TAG, " mCallback=" + mCallback);
+ Log.d(TAG, " mRootHints=" + mRootHints);
+ Log.d(TAG, " mState=" + getStateLabel(mState));
+ Log.d(TAG, " mServiceConnection=" + mServiceConnection);
+ Log.d(TAG, " mServiceBinder=" + mServiceBinder);
+ Log.d(TAG, " mServiceCallbacks=" + mServiceCallbacks);
+ Log.d(TAG, " mRootId=" + mRootId);
+ Log.d(TAG, " mMediaSessionToken=" + mMediaSessionToken);
+ }
+
+ /**
+ * ServiceConnection to the other app.
+ */
+ private class MediaServiceConnection implements ServiceConnection {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder binder) {
+ if (DBG) {
+ Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name
+ + " binder=" + binder);
+ dump();
+ }
+
+ // Make sure we are still the current connection, and that they haven't called
+ // disconnect().
+ if (!isCurrent("onServiceConnected")) {
+ return;
+ }
+
+ // Save their binder
+ mServiceBinder = IMediaBrowserServiceCompat.Stub.asInterface(binder);
+
+ // We make a new mServiceCallbacks each time we connect so that we can drop
+ // responses from previous connections.
+ mServiceCallbacks = getNewServiceCallbacks();
+ mState = CONNECT_STATE_CONNECTING;
+
+ // Call connect, which is async. When we get a response from that we will
+ // say that we're connected.
+ try {
+ if (DBG) {
+ Log.d(TAG, "ServiceCallbacks.onConnect...");
+ dump();
+ }
+ mServiceBinder.connect(
+ mContext.getPackageName(), mRootHints, mServiceCallbacks);
+ } catch (RemoteException ex) {
+ // Connect failed, which isn't good. But the auto-reconnect on the service
+ // will take over and we will come back. We will also get the
+ // onServiceDisconnected, which has all the cleanup code. So let that do it.
+ Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
+ if (DBG) {
+ Log.d(TAG, "ServiceCallbacks.onConnect...");
+ dump();
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (DBG) {
+ Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name
+ + " this=" + this + " mServiceConnection=" + mServiceConnection);
+ dump();
+ }
+
+ // Make sure we are still the current connection, and that they haven't called
+ // disconnect().
+ if (!isCurrent("onServiceDisconnected")) {
+ return;
+ }
+
+ // Clear out what we set in onServiceConnected
+ mServiceBinder = null;
+ mServiceCallbacks = null;
+
+ // And tell the app that it's suspended.
+ mState = CONNECT_STATE_SUSPENDED;
+ mCallback.onConnectionSuspended();
+ }
+
+ /**
+ * Return true if this is the current ServiceConnection. Also logs if it's not.
+ */
+ private boolean isCurrent(String funcName) {
+ if (mServiceConnection != this) {
+ if (mState != CONNECT_STATE_DISCONNECTED) {
+ // Check mState, because otherwise this log is noisy.
+ Log.i(TAG, funcName + " for " + mServiceComponent +
+ " with mServiceConnection=" + mServiceConnection + " this=" + this);
+ }
+ return false;
+ }
+ return true;
}
}
/**
- * The other side does not like us. Tell the app via onConnectionFailed.
+ * Callbacks from the service.
*/
- @Override
- public void onConnectFailed() {
- MediaBrowserCompat mediaBrowser = mMediaBrowser.get();
- if (mediaBrowser != null) {
- mediaBrowser.onConnectionFailed(this);
+ private static class ServiceCallbacks extends IMediaBrowserServiceCompatCallbacks.Stub {
+ private WeakReference<MediaBrowserImplBase> mMediaBrowser;
+
+ public ServiceCallbacks(MediaBrowserImplBase mediaBrowser) {
+ mMediaBrowser = new WeakReference<>(mediaBrowser);
+ }
+
+ /**
+ * The other side has acknowledged our connection. The parameters to this function
+ * are the initial data as requested.
+ */
+ @Override
+ public void onConnect(final String root, final MediaSessionCompat.Token session,
+ final Bundle extras) {
+ MediaBrowserImplBase mediaBrowser = mMediaBrowser.get();
+ if (mediaBrowser != null) {
+ mediaBrowser.onServiceConnected(this, root, session, extras);
+ }
+ }
+
+ /**
+ * The other side does not like us. Tell the app via onConnectionFailed.
+ */
+ @Override
+ public void onConnectFailed() {
+ MediaBrowserImplBase mediaBrowser = mMediaBrowser.get();
+ if (mediaBrowser != null) {
+ mediaBrowser.onConnectionFailed(this);
+ }
+ }
+
+ @Override
+ public void onLoadChildren(final String parentId, final List list) {
+ MediaBrowserImplBase mediaBrowser = mMediaBrowser.get();
+ if (mediaBrowser != null) {
+ mediaBrowser.onLoadChildren(this, parentId, list);
+ }
}
}
- @Override
- public void onLoadChildren(final String parentId, final List list) {
- MediaBrowserCompat mediaBrowser = mMediaBrowser.get();
- if (mediaBrowser != null) {
- mediaBrowser.onLoadChildren(this, parentId, list);
+ private static class Subscription {
+ final String id;
+ SubscriptionCallback callback;
+
+ Subscription(String id) {
+ this.id = id;
}
}
}
-
- private static class Subscription {
- final String id;
- SubscriptionCallback callback;
-
- Subscription(String id) {
- this.id = id;
- }
- }
}
diff --git a/v7/mediarouter/build.gradle b/v7/mediarouter/build.gradle
index 0f680a5..48f4750 100644
--- a/v7/mediarouter/build.gradle
+++ b/v7/mediarouter/build.gradle
@@ -117,7 +117,7 @@
}
pom.project {
- name 'Android Support Library v4'
+ name 'Android MediaRouter Support Library'
description "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren't a part of the framework APIs. Compatible on devices running API 4 or later."
url 'http://developer.android.com/tools/extras/support-library.html'
inceptionYear '2011'
diff --git a/v7/mediarouter/res/interpolator/mr_fast_out_slow_in.xml b/v7/mediarouter/res/interpolator/mr_fast_out_slow_in.xml
new file mode 100644
index 0000000..a51bfce
--- /dev/null
+++ b/v7/mediarouter/res/interpolator/mr_fast_out_slow_in.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:controlX1="0.4"
+ android:controlY1="0"
+ android:controlX2="0.2"
+ android:controlY2="1"/>
diff --git a/v7/mediarouter/res/interpolator/mr_linear_out_slow_in.xml b/v7/mediarouter/res/interpolator/mr_linear_out_slow_in.xml
new file mode 100644
index 0000000..d500c8b
--- /dev/null
+++ b/v7/mediarouter/res/interpolator/mr_linear_out_slow_in.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:controlX1="0"
+ android:controlY1="0"
+ android:controlX2="0.2"
+ android:controlY2="1"/>
diff --git a/v7/mediarouter/res/layout/mr_controller_material_dialog_b.xml b/v7/mediarouter/res/layout/mr_controller_material_dialog_b.xml
index 8d17c25..b7cc517 100644
--- a/v7/mediarouter/res/layout/mr_controller_material_dialog_b.xml
+++ b/v7/mediarouter/res/layout/mr_controller_material_dialog_b.xml
@@ -14,41 +14,46 @@
limitations under the License.
-->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical">
- <LinearLayout android:id="@+id/mr_title_bar"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/mr_expandable_area"
+ android:background="@android:color/transparent"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+ <LinearLayout android:id="@+id/mr_dialog_area"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
- android:paddingLeft="24dp"
- android:paddingRight="12dp"
- android:orientation="horizontal" >
- <TextView android:id="@+id/mr_name"
- android:layout_width="0dp"
- android:layout_height="72dp"
- android:layout_weight="1"
- android:gravity="center_vertical"
- android:singleLine="true"
- android:ellipsize="end"
- android:textAppearance="?attr/mediaRouteControllerTitleTextStyle" />
- <ImageButton android:id="@+id/mr_close"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:layout_gravity="center_vertical"
- android:contentDescription="@string/mr_controller_close_description"
- android:src="?attr/mediaRouteCloseDrawable"
- android:background="?attr/selectableItemBackgroundBorderless" />
- </LinearLayout>
- <FrameLayout android:id="@+id/mr_custom_control"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:visibility="gone" />
- <FrameLayout android:id="@+id/mr_default_control"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content">
- <FrameLayout android:layout_width="fill_parent"
- android:layout_height="fill_parent">
+ android:background="?android:attr/windowBackground"
+ android:layout_gravity="center"
+ android:orientation="vertical">
+ <LinearLayout android:id="@+id/mr_title_bar"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="24dp"
+ android:paddingRight="12dp"
+ android:orientation="horizontal" >
+ <TextView android:id="@+id/mr_name"
+ android:layout_width="0dp"
+ android:layout_height="72dp"
+ android:layout_weight="1"
+ android:gravity="center_vertical"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textAppearance="?attr/mediaRouteControllerTitleTextStyle" />
+ <ImageButton android:id="@+id/mr_close"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_gravity="center_vertical"
+ android:contentDescription="@string/mr_controller_close_description"
+ android:src="?attr/mediaRouteCloseDrawable"
+ android:background="?attr/selectableItemBackgroundBorderless" />
+ </LinearLayout>
+ <FrameLayout android:id="@+id/mr_custom_control"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone" />
+ <FrameLayout android:id="@+id/mr_default_control"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
<ImageView android:id="@+id/mr_art"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
@@ -57,35 +62,39 @@
android:background="?attr/colorPrimary"
android:layout_gravity="top"
android:visibility="gone" />
- <ListView android:id="@+id/mr_volume_group_list"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:paddingTop="@dimen/mr_controller_volume_group_list_padding_top"
- android:scrollbarStyle="outsideInset"
- android:clipToPadding="false"
- android:background="?attr/colorPrimaryDark"
- android:layout_gravity="bottom"
- android:visibility="gone" />
+ <LinearLayout android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_gravity="bottom">
+ <LinearLayout android:id="@+id/mr_media_main_control"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp"
+ android:background="?attr/colorPrimary"
+ android:layout_gravity="bottom">
+ <include android:id="@+id/mr_playback_control"
+ layout="@layout/mr_playback_control" />
+ <View android:id="@+id/mr_control_divider"
+ android:layout_width="fill_parent"
+ android:layout_height="8dp"
+ android:background="?attr/colorPrimary"
+ android:visibility="gone" />
+ <include android:id="@+id/mr_volume_control"
+ layout="@layout/mr_volume_control" />
+ </LinearLayout>
+ <ListView android:id="@+id/mr_volume_group_list"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/mr_controller_volume_group_list_padding_top"
+ android:scrollbarStyle="outsideInset"
+ android:clipToPadding="false"
+ android:background="?attr/colorPrimaryDark"
+ android:transcriptMode="alwaysScroll"
+ android:visibility="gone" />
+ </LinearLayout>
</FrameLayout>
- <LinearLayout android:id="@+id/mr_media_main_control"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- android:paddingTop="16dp"
- android:paddingBottom="16dp"
- android:background="?attr/colorPrimary"
- android:layout_gravity="bottom">
- <include android:id="@+id/mr_playback_control"
- layout="@layout/mr_playback_control" />
- <View android:id="@+id/mr_control_divider"
- android:layout_width="fill_parent"
- android:layout_height="8dp"
- android:background="?attr/colorPrimary"
- android:visibility="gone" />
- <include android:id="@+id/mr_volume_control"
- layout="@layout/mr_volume_control" />
- </LinearLayout>
- </FrameLayout>
-
- <include layout="@layout/abc_alert_dialog_button_bar_material" />
-</LinearLayout>
+ <include layout="@layout/abc_alert_dialog_button_bar_material" />
+ </LinearLayout>
+</FrameLayout>
\ No newline at end of file
diff --git a/v7/mediarouter/res/layout/mr_controller_volume_item.xml b/v7/mediarouter/res/layout/mr_controller_volume_item.xml
index 6cd0fd6..3b6ce71 100644
--- a/v7/mediarouter/res/layout/mr_controller_volume_item.xml
+++ b/v7/mediarouter/res/layout/mr_controller_volume_item.xml
@@ -39,6 +39,8 @@
android:src="?attr/mediaRouteAudioTrackDrawable" />
<android.support.v7.app.MediaRouteVolumeSlider android:id="@+id/mr_volume_slider"
android:layout_width="fill_parent"
- android:layout_height="wrap_content" />
+ android:layout_height="wrap_content"
+ android:minHeight="40dp"
+ android:maxHeight="40dp" />
</LinearLayout>
</LinearLayout>
diff --git a/v7/mediarouter/res/layout/mr_volume_control.xml b/v7/mediarouter/res/layout/mr_volume_control.xml
index 18d7864..d231c7a 100644
--- a/v7/mediarouter/res/layout/mr_volume_control.xml
+++ b/v7/mediarouter/res/layout/mr_volume_control.xml
@@ -32,6 +32,8 @@
android:id="@+id/mr_volume_slider"
android:layout_width="0dp"
android:layout_height="wrap_content"
+ android:minHeight="48dp"
+ android:maxHeight="48dp"
android:layout_weight="1"/>
<android.support.v7.app.MediaRouteExpandCollapseButton
android:id="@+id/mr_group_expand_collapse"
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
index f4797cf..2b98398 100644
--- a/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
@@ -107,8 +107,10 @@
private ImageButton mCloseButton;
private MediaRouteExpandCollapseButton mGroupExpandCollapseButton;
- private FrameLayout mCustomControlLayout;
+ private FrameLayout mExpandableAreaLayout;
+ private LinearLayout mDialogAreaLayout;
private FrameLayout mDefaultControlLayout;
+ private FrameLayout mCustomControlLayout;
private ImageView mArtView;
private TextView mTitleView;
private TextView mSubtitleView;
@@ -268,6 +270,7 @@
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ getWindow().setBackgroundDrawableResource(android.R.color.transparent);
setContentView(R.layout.mr_controller_material_dialog_b);
// Remove the neutral button.
@@ -275,6 +278,20 @@
ClickListener listener = new ClickListener();
+ mExpandableAreaLayout = (FrameLayout) findViewById(R.id.mr_expandable_area);
+ mExpandableAreaLayout.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dismiss();
+ }
+ });
+ mDialogAreaLayout = (LinearLayout) findViewById(R.id.mr_dialog_area);
+ mDialogAreaLayout.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // Eat unhandled touch events.
+ }
+ });
int color = MediaRouterThemeHelper.getButtonTextColor(mContext);
mDisconnectButton = (Button) findViewById(BUTTON_DISCONNECT_RES_ID);
mDisconnectButton.setText(R.string.mr_controller_disconnect);
@@ -328,7 +345,7 @@
}
});
mGroupListAnimationDurationMs = mContext.getResources().getInteger(
- R.integer.mr_controller_volume_group_list_animation_duration_ms);
+ R.integer.mr_controller_volume_group_list_animation_duration_ms);
mCustomControlView = onCreateMediaControlView(savedInstanceState);
if (mCustomControlView != null) {
@@ -481,14 +498,14 @@
return;
}
// Measure the size of widgets and get the height of main components.
+ int oldHeight = getLayoutHeight(mMediaMainControlLayout);
+ setLayoutHeight(mMediaMainControlLayout, ViewGroup.LayoutParams.FILL_PARENT);
updateMediaControlVisibility(isPlaybackControlAvailable());
- int oldBottomMargin = getLayoutBottomMargin(mMediaMainControlLayout);
- setLayoutBottomMargin(mMediaMainControlLayout, 0);
View decorView = getWindow().getDecorView();
decorView.measure(
MeasureSpec.makeMeasureSpec(getWindow().getAttributes().width, MeasureSpec.EXACTLY),
MeasureSpec.UNSPECIFIED);
- setLayoutBottomMargin(mMediaMainControlLayout, oldBottomMargin);
+ setLayoutHeight(mMediaMainControlLayout, oldHeight);
int artViewHeight = 0;
if (mArtView.getDrawable() instanceof BitmapDrawable) {
Bitmap art = ((BitmapDrawable) mArtView.getDrawable()).getBitmap();
@@ -502,16 +519,8 @@
int volumeGroupListCount = mVolumeGroupList.getAdapter() != null
? mVolumeGroupList.getAdapter().getCount() : 0;
// Scale down volume group list items in landscape mode.
- for (int i = 0; i < volumeGroupListCount; i++) {
- View item = mVolumeGroupList.getChildAt(i);
- if (item != null) {
- setLayoutHeight(item, mVolumeGroupListItemHeight);
- View icon = item.findViewById(R.id.mr_volume_item_icon);
- ViewGroup.LayoutParams lp = icon.getLayoutParams();
- lp.width = mVolumeGroupListItemIconSize;
- lp.height = mVolumeGroupListItemIconSize;
- icon.setLayoutParams(lp);
- }
+ for (int i = 0; i < mVolumeGroupList.getChildCount(); i++) {
+ updateVolumeGroupItemHeight(mVolumeGroupList.getChildAt(i));
}
int expandedGroupListHeight = mVolumeGroupListItemHeight * volumeGroupListCount;
if (volumeGroupListCount > 0) {
@@ -527,7 +536,7 @@
// Height of non-control views in decor view.
// This includes title bar, button bar, and dialog's vertical padding which should be
// always shown.
- int nonControlViewHeight = decorView.getMeasuredHeight()
+ int nonControlViewHeight = mDialogAreaLayout.getMeasuredHeight()
- mDefaultControlLayout.getMeasuredHeight();
// Maximum allowed height for controls to fit screen.
int maximumControlViewHeight = visibleRect.height() - nonControlViewHeight;
@@ -537,10 +546,14 @@
mArtView.setVisibility(View.VISIBLE);
setLayoutHeight(mArtView, artViewHeight);
} else {
+ if (getLayoutHeight(mVolumeGroupList) + mMediaMainControlLayout.getMeasuredHeight()
+ >= mDefaultControlLayout.getMeasuredHeight()) {
+ mArtView.setVisibility(View.GONE);
+ }
artViewHeight = 0;
desiredControlLayoutHeight = visibleGroupListHeight + mainControllerHeight;
}
- // Show control if it fits the screen
+ // Show the playback control if it fits the screen.
if (isPlaybackControlAvailable()
&& desiredControlLayoutHeight <= maximumControlViewHeight) {
mPlaybackControl.setVisibility(View.VISIBLE);
@@ -558,82 +571,49 @@
visibleGroupListHeight -= (desiredControlLayoutHeight - maximumControlViewHeight);
desiredControlLayoutHeight = maximumControlViewHeight;
}
- setLayoutHeight(mDefaultControlLayout, desiredControlLayoutHeight);
-
- // Animate the main control position if needed.
- if (mVolumeGroupList.getVisibility() == View.VISIBLE
- && mArtView.getVisibility() == View.VISIBLE && mIsGroupListAnimationNeeded) {
- setLayoutHeight(mVolumeGroupList, mIsGroupExpanded ? expandedGroupListHeight
- : Math.min(mArtView.getHeight(), getLayoutHeight(mVolumeGroupList)));
- updateMainControlBottomMargin(visibleGroupListHeight, mainControllerHeight,
- true /* animation */);
+ // Update the layouts with the computed heights.
+ mMediaMainControlLayout.clearAnimation();
+ mVolumeGroupList.clearAnimation();
+ mDefaultControlLayout.clearAnimation();
+ if (mIsGroupListAnimationNeeded) {
+ animateLayoutHeight(mMediaMainControlLayout, mainControllerHeight);
+ animateLayoutHeight(mVolumeGroupList, visibleGroupListHeight);
+ animateLayoutHeight(mDefaultControlLayout, desiredControlLayoutHeight);
} else {
- // Rely on AlertDialog's animation if there is no art work.
- // TODO: Add group list animation even when there is no art work.
+ setLayoutHeight(mMediaMainControlLayout, mainControllerHeight);
setLayoutHeight(mVolumeGroupList, visibleGroupListHeight);
- updateMainControlBottomMargin(visibleGroupListHeight, mainControllerHeight,
- false /* animation */);
- if (artViewHeight == 0) {
- mArtView.setVisibility(View.GONE);
- }
- if (!mIsGroupExpanded) {
- mVolumeGroupList.setVisibility(View.GONE);
- }
+ setLayoutHeight(mDefaultControlLayout, desiredControlLayoutHeight);
}
mIsGroupListAnimationNeeded = false;
+ // Maximize the window size with a transparent layout in advance for smooth animation.
+ setLayoutHeight(mExpandableAreaLayout, visibleRect.height());
}
- private void updateMainControlBottomMargin(final int bottomMargin,
- final int mainControllerHeight, boolean animation) {
- final boolean isExpanding = bottomMargin != 0;
- if (!animation) {
- setLayoutBottomMargin(mMediaMainControlLayout, bottomMargin);
- View frontView = isExpanding ? mVolumeGroupList : mArtView;
- frontView.bringToFront();
- ((View) frontView.getParent()).invalidate();
- } else {
- Animation existingAnim = mMediaMainControlLayout.getAnimation();
- boolean animationInProgress = existingAnim != null && !existingAnim.hasEnded();
- if (animationInProgress) {
- mMediaMainControlLayout.clearAnimation();
- }
- final int volumeGroupListHeight = getLayoutHeight(mVolumeGroupList);
- int rightBelowArtWork = getLayoutHeight(mDefaultControlLayout)
- - mArtView.getHeight() - mainControllerHeight;
- final int startValue = animationInProgress
- ? getLayoutBottomMargin(mMediaMainControlLayout)
- : isExpanding ? rightBelowArtWork : volumeGroupListHeight;
- final int endValue = bottomMargin;
- Animation anim = new Animation() {
- private boolean mReordered;
+ private void updateVolumeGroupItemHeight(View item) {
+ setLayoutHeight(item, mVolumeGroupListItemHeight);
+ View icon = item.findViewById(R.id.mr_volume_item_icon);
+ ViewGroup.LayoutParams lp = icon.getLayoutParams();
+ lp.width = mVolumeGroupListItemIconSize;
+ lp.height = mVolumeGroupListItemIconSize;
+ icon.setLayoutParams(lp);
+ }
- @Override
- protected void applyTransformation(float interpolatedTime, Transformation t) {
- int margin = startValue - (int) ((startValue - endValue) * interpolatedTime);
- setLayoutBottomMargin(mMediaMainControlLayout, margin);
- // Since there could be an overlapping area of the artwork and volume group list
- // , z-order of the art work and volume group list should be exchanged when the
- // main control covers the overlapping area.
- if (!mReordered) {
- if (isExpanding) {
- if (margin + mainControllerHeight >= volumeGroupListHeight) {
- mVolumeGroupList.bringToFront();
- ((View) mVolumeGroupList.getParent()).invalidate();
- mReordered = true;
- }
- } else {
- if (volumeGroupListHeight >= margin + mainControllerHeight) {
- mArtView.bringToFront();
- ((View) mArtView.getParent()).invalidate();
- mReordered = true;
- }
- }
- }
- }
- };
- anim.setDuration(mGroupListAnimationDurationMs);
- mMediaMainControlLayout.startAnimation(anim);
+ private void animateLayoutHeight(final View view, int targetHeight) {
+ final int startValue = getLayoutHeight(view);
+ final int endValue = targetHeight;
+ Animation anim = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ int height = startValue - (int) ((startValue - endValue) * interpolatedTime);
+ setLayoutHeight(view, height);
+ }
+ };
+ anim.setDuration(mGroupListAnimationDurationMs);
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ anim.setInterpolator(mContext, mIsGroupExpanded ? R.interpolator.mr_linear_out_slow_in
+ : R.interpolator.mr_fast_out_slow_in);
}
+ view.startAnimation(anim);
}
private void updateVolumeControl() {
@@ -649,7 +629,7 @@
VolumeGroupAdapter adapter =
(VolumeGroupAdapter) mVolumeGroupList.getAdapter();
if (adapter != null) {
- adapter.notifyDataSetChanged();
+ adapter.notifyDataSetChanged();
}
}
} else {
@@ -902,6 +882,8 @@
if (v == null) {
v = LayoutInflater.from(mContext).inflate(
R.layout.mr_controller_volume_item, parent, false);
+ } else {
+ updateVolumeGroupItemHeight(v);
}
MediaRouter.RouteInfo route = getItem(position);
diff --git a/v7/recyclerview/src/android/support/v7/widget/ChildHelper.java b/v7/recyclerview/src/android/support/v7/widget/ChildHelper.java
index 66ebab5..fd1cee4 100644
--- a/v7/recyclerview/src/android/support/v7/widget/ChildHelper.java
+++ b/v7/recyclerview/src/android/support/v7/widget/ChildHelper.java
@@ -339,6 +339,25 @@
}
}
+ /**
+ * Moves a child view from hidden list to regular list.
+ * Calling this method should probably be followed by a detach, otherwise, it will suddenly
+ * show up in LayoutManager's children list.
+ *
+ * @param view The hidden View to unhide
+ */
+ void unhide(View view) {
+ final int offset = mCallback.indexOfChild(view);
+ if (offset < 0) {
+ throw new IllegalArgumentException("view is not a child, cannot hide " + view);
+ }
+ if (!mBucket.get(offset)) {
+ throw new RuntimeException("trying to unhide a view that was not hidden" + view);
+ }
+ mBucket.clear(offset);
+ unhideViewInternal(view);
+ }
+
@Override
public String toString() {
return mBucket.toString() + ", hidden list:" + mHiddenViews.size();
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index ee4278f..f0b0afe 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -72,6 +72,7 @@
import java.util.List;
import static android.support.v7.widget.AdapterHelper.Callback;
+import static android.support.v7.widget.AdapterHelper.POSITION_TYPE_INVISIBLE;
import static android.support.v7.widget.AdapterHelper.UpdateOp;
import android.support.v7.widget.RecyclerView.ItemAnimator.ItemHolderInfo;
@@ -2773,8 +2774,8 @@
onEnterLayoutOrScroll();
processAdapterUpdatesAndSetAnimationFlags();
- final boolean trackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
- if (trackOldChangeHolders && mState.mOldChangedHolders == null) {
+ mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
+ if (mState.mTrackOldChangeHolders && mState.mOldChangedHolders == null) {
mState.mOldChangedHolders = new ArrayMap<>();
}
mItemsAddedOrRemoved = mItemsChanged = false;
@@ -2798,9 +2799,16 @@
ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
holder.getUnmodifiedPayloads());
mState.mPreLayoutHolderMap.put(holder, animationInfo);
- if (trackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
+ if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
&& !holder.shouldIgnore() && !holder.isInvalid()) {
long key = getChangedHolderKey(holder);
+ // This is NOT the only place where a ViewHolder is added to old change holders
+ // list. There is another case where:
+ // * A VH is currently hidden but not deleted
+ // * The hidden item is changed in the adapter
+ // * Layout manager decides to layout the item in the pre-Layout pass (step1)
+ // When this case is detected, RV will un-hide that view and add to the old
+ // change holders list.
mState.mOldChangedHolders.put(key, holder);
}
}
@@ -2836,10 +2844,18 @@
}
if (!found) {
int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder);
- flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
+ boolean wasHidden = viewHolder
+ .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+ if (!wasHidden) {
+ flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
+ }
final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
- appearingViewInfo.put(child, animationInfo);
+ if (wasHidden) {
+ recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
+ } else {
+ appearingViewInfo.put(child, animationInfo);
+ }
}
}
// we don't process disappearing list because they may re-appear in post layout pass.
@@ -2872,7 +2888,7 @@
long key = getChangedHolderKey(holder);
final ItemHolderInfo animationInfo = mItemAnimator
.recordPostLayoutInformation(mState, holder);
- ViewHolder oldChangeViewHolder = trackOldChangeHolders ?
+ ViewHolder oldChangeViewHolder = mState.mTrackOldChangeHolders ?
mState.mOldChangedHolders.get(key) : null;
if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
// run a change animation
@@ -2938,6 +2954,8 @@
mState.mPreviousLayoutItemCount = mState.mItemCount;
mDataSetHasChangedAfterLayout = false;
mState.mRunSimpleAnimations = false;
+ mState.mPreLayoutHolderMap.clear();
+ mState.mPostLayoutHolderMap.clear();
mState.mRunPredictiveAnimations = false;
onExitLayoutOrScroll();
mLayout.mRequestedSimpleAnimations = false;
@@ -2953,6 +2971,22 @@
}
}
+ /**
+ * Records the animation information for a view holder that was bounced from hidden list. It
+ * also clears the bounce back flag.
+ */
+ private void recordAnimationInfoIfBouncedHiddenView(ViewHolder viewHolder,
+ ItemHolderInfo animationInfo) {
+ // looks like this view bounced back from hidden list!
+ viewHolder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+ if (mState.mTrackOldChangeHolders && viewHolder.isUpdated()
+ && !viewHolder.isRemoved() && !viewHolder.shouldIgnore()) {
+ long key = getChangedHolderKey(viewHolder);
+ mState.mOldChangedHolders.put(key, viewHolder);
+ }
+ mState.mPreLayoutHolderMap.put(viewHolder, animationInfo);
+ }
+
private void findMinMaxChildLayoutPositions(int[] into) {
final int count = mChildHelper.getChildCount();
if (count == 0) {
@@ -4466,6 +4500,23 @@
}
}
}
+
+ // This is very ugly but the only place we can grab this information
+ // before the View is rebound and returned to the LayoutManager for post layout ops.
+ // We don't need this in pre-layout since the VH is not updated by the LM.
+ if (fromScrap && !mState.isPreLayout() && holder
+ .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
+ holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+ if (mState.mRunSimpleAnimations) {
+ int changeFlags = ItemAnimator
+ .buildAdapterChangeFlagsForAnimations(holder);
+ changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
+ final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
+ holder, changeFlags, holder.getUnmodifiedPayloads());
+ recordAnimationInfoIfBouncedHiddenView(holder, info);
+ }
+ }
+
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
@@ -4812,8 +4863,20 @@
if (!dryRun) {
View view = mChildHelper.findHiddenNonRemovedView(position, type);
if (view != null) {
- // ending the animation should cause it to get recycled before we reuse it
- mItemAnimator.endAnimation(getChildViewHolder(view));
+ // This View is good to be used. We just need to unhide, detach and move to the
+ // scrap list.
+ final ViewHolder vh = getChildViewHolderInt(view);
+ mChildHelper.unhide(view);
+ int layoutIndex = mChildHelper.indexOfChild(view);
+ if (layoutIndex == RecyclerView.NO_POSITION) {
+ throw new IllegalStateException("layout index should not be -1 after "
+ + "unhiding a view:" + vh);
+ }
+ mChildHelper.detachViewFromParent(layoutIndex);
+ scrapView(view);
+ vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
+ | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
+ return vh;
}
}
@@ -8192,6 +8255,21 @@
*/
static final int FLAG_APPEARED_IN_PRE_LAYOUT = 1 << 12;
+ /**
+ * Used when a ViewHolder starts the layout pass as a hidden ViewHolder but is re-used from
+ * hidden list (as if it was scrap) without being recycled in between.
+ *
+ * When a ViewHolder is hidden, there are 2 paths it can be re-used:
+ * a) Animation ends, view is recycled and used from the recycle pool.
+ * b) LayoutManager asks for the View for that position while the ViewHolder is hidden.
+ *
+ * This flag is used to represent "case b" where the ViewHolder is reused without being
+ * recycled (thus "bounced" from the hidden list). This state requires special handling
+ * because the ViewHolder must be added to pre layout maps for animations as if it was
+ * already there.
+ */
+ static final int FLAG_BOUNCED_FROM_HIDDEN_LIST = 1 << 13;
+
private int mFlags;
private static final List<Object> FULLUPDATE_PAYLOADS = Collections.EMPTY_LIST;
@@ -9344,6 +9422,8 @@
private boolean mRunPredictiveAnimations = false;
+ private boolean mTrackOldChangeHolders = false;
+
State reset() {
mTargetPosition = RecyclerView.NO_POSITION;
if (mData != null) {
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewAnimationsTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewAnimationsTest.java
index 1d095b4..e690f22 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewAnimationsTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewAnimationsTest.java
@@ -630,4 +630,69 @@
}
}
}
+
+ static class LoggingInfo extends RecyclerView.ItemAnimator.ItemHolderInfo {
+ final RecyclerView.ViewHolder viewHolder;
+ @RecyclerView.ItemAnimator.AdapterChanges
+ final int changeFlags;
+ final List<Object> payloads;
+
+ LoggingInfo(RecyclerView.ViewHolder viewHolder, int changeFlags, List<Object> payloads) {
+ this.viewHolder = viewHolder;
+ this.changeFlags = changeFlags;
+ if (payloads != null) {
+ this.payloads = new ArrayList<>();
+ this.payloads.addAll(payloads);
+ } else {
+ this.payloads = null;
+ }
+ setFrom(viewHolder);
+ }
+ }
+
+ static class AnimateChange extends AnimatePersistence {
+
+ final RecyclerView.ViewHolder newHolder;
+
+ public AnimateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
+ LoggingInfo pre, LoggingInfo post) {
+ super(oldHolder, pre, post);
+ this.newHolder = newHolder;
+ }
+ }
+
+ static class AnimatePersistence extends AnimateAppearance {
+
+ public AnimatePersistence(RecyclerView.ViewHolder viewHolder, LoggingInfo pre,
+ LoggingInfo post) {
+ super(viewHolder, pre, post);
+ }
+ }
+
+ static class AnimateAppearance extends AnimateDisappearance {
+
+ final LoggingInfo postInfo;
+
+ public AnimateAppearance(RecyclerView.ViewHolder viewHolder, LoggingInfo pre,
+ LoggingInfo post) {
+ super(viewHolder, pre);
+ this.postInfo = post;
+ }
+ }
+
+ static class AnimateDisappearance extends AnimateLogBase {
+ public AnimateDisappearance(RecyclerView.ViewHolder viewHolder, LoggingInfo pre) {
+ super(viewHolder, pre);
+ }
+ }
+ static class AnimateLogBase {
+
+ final RecyclerView.ViewHolder viewHolder;
+ final LoggingInfo preInfo;
+
+ public AnimateLogBase(RecyclerView.ViewHolder viewHolder, LoggingInfo pre) {
+ this.viewHolder = viewHolder;
+ this.preInfo = pre;
+ }
+ }
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
index 53c3f81..c5f3408 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
@@ -202,19 +202,23 @@
mRecyclerView = null;
}
- void waitForAnimations(int seconds) throws InterruptedException {
- final CountDownLatch latch = new CountDownLatch(2);
- boolean running = mRecyclerView.mItemAnimator
- .isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
- @Override
- public void onAnimationsFinished() {
- latch.countDown();
- }
- });
- if (running) {
- latch.countDown();
- latch.await(seconds, TimeUnit.SECONDS);
- }
+ void waitForAnimations(int seconds) throws Throwable {
+ final CountDownLatch latch = new CountDownLatch(1);
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.mItemAnimator
+ .isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
+ @Override
+ public void onAnimationsFinished() {
+ latch.countDown();
+ }
+ });
+ }
+ });
+
+ assertTrue("animations didn't finish on expected time of " + seconds + " seconds",
+ latch.await(seconds, TimeUnit.SECONDS));
}
public boolean requestFocus(final View view) {
@@ -548,6 +552,7 @@
int mAdapterIndex;
final String mText;
+ int mType = 0;
Item(int adapterIndex, String text) {
mAdapterIndex = adapterIndex;
@@ -584,6 +589,11 @@
}
@Override
+ public int getItemViewType(int position) {
+ return getItemAt(position).mType;
+ }
+
+ @Override
public void onViewAttachedToWindow(TestViewHolder holder) {
super.onViewAttachedToWindow(holder);
mAttachmentCounter.onViewAttached(holder);
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/ItemAnimatorV2ApiTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/ItemAnimatorV2ApiTest.java
index c9192a7..0e4bca7 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/ItemAnimatorV2ApiTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/ItemAnimatorV2ApiTest.java
@@ -440,7 +440,7 @@
@Override
public boolean animateDisappearance(RecyclerView.ViewHolder viewHolder,
ItemHolderInfo preInfo) {
- animateDisappearanceList.add(new AnimateDisappearance(viewHolder, preInfo));
+ animateDisappearanceList.add(new AnimateDisappearance(viewHolder, (LoggingInfo) preInfo));
assertSame(preLayoutInfo.get(viewHolder), preInfo);
dispatchAnimationFinished(viewHolder);
@@ -451,7 +451,7 @@
public boolean animateAppearance(RecyclerView.ViewHolder viewHolder, ItemHolderInfo preInfo,
ItemHolderInfo postInfo) {
animateAppearanceList.add(
- new AnimateAppearance(viewHolder, preInfo, postInfo));
+ new AnimateAppearance(viewHolder, (LoggingInfo) preInfo, (LoggingInfo) postInfo));
assertSame(preLayoutInfo.get(viewHolder), preInfo);
assertSame(postLayoutInfo.get(viewHolder), postInfo);
dispatchAnimationFinished(viewHolder);
@@ -462,7 +462,8 @@
public boolean animatePersistence(RecyclerView.ViewHolder viewHolder,
ItemHolderInfo preInfo,
ItemHolderInfo postInfo) {
- animatePersistenceList.add(new AnimatePersistence(viewHolder, preInfo, postInfo));
+ animatePersistenceList.add(new AnimatePersistence(viewHolder, (LoggingInfo) preInfo,
+ (LoggingInfo) postInfo));
dispatchAnimationFinished(viewHolder);
assertSame(preLayoutInfo.get(viewHolder), preInfo);
assertSame(postLayoutInfo.get(viewHolder), postInfo);
@@ -473,7 +474,8 @@
public boolean animateChange(RecyclerView.ViewHolder oldHolder,
RecyclerView.ViewHolder newHolder, ItemHolderInfo preInfo,
ItemHolderInfo postInfo) {
- animateChangeList.add(new AnimateChange(oldHolder, newHolder, preInfo, postInfo));
+ animateChangeList.add(new AnimateChange(oldHolder, newHolder, (LoggingInfo) preInfo,
+ (LoggingInfo) postInfo));
if (oldHolder != null) {
dispatchAnimationFinished(oldHolder);
assertSame(preLayoutInfo.get(oldHolder), preInfo);
@@ -506,65 +508,6 @@
}
}
- static class LoggingInfo extends RecyclerView.ItemAnimator.ItemHolderInfo {
-
- final RecyclerView.ViewHolder viewHolder;
- @RecyclerView.ItemAnimator.AdapterChanges
- final int changeFlags;
- final List<Object> payloads;
-
- LoggingInfo(RecyclerView.ViewHolder viewHolder, int changeFlags, List<Object> payloads) {
- this.viewHolder = viewHolder;
- this.changeFlags = changeFlags;
- if (payloads != null) {
- this.payloads = new ArrayList<>();
- this.payloads.addAll(payloads);
- } else {
- this.payloads = null;
- }
- setFrom(viewHolder);
- }
- }
-
- static class AnimateChange extends AnimatePersistence {
-
- final RecyclerView.ViewHolder newHolder;
-
- public AnimateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
- Object pre, Object post) {
- super(oldHolder, pre, post);
- this.newHolder = newHolder;
- }
- }
-
- static class AnimatePersistence extends AnimateAppearance {
-
- public AnimatePersistence(RecyclerView.ViewHolder viewHolder, Object pre, Object post) {
- super(viewHolder, pre, post);
- }
- }
-
- static class AnimateAppearance extends AnimateDisappearance {
-
- final LoggingInfo postInfo;
-
- public AnimateAppearance(RecyclerView.ViewHolder viewHolder, Object pre, Object post) {
- super(viewHolder, pre);
- this.postInfo = (LoggingInfo) post;
- }
- }
-
- static class AnimateDisappearance {
-
- final RecyclerView.ViewHolder viewHolder;
- final LoggingInfo preInfo;
-
- public AnimateDisappearance(RecyclerView.ViewHolder viewHolder, Object pre) {
- this.viewHolder = viewHolder;
- this.preInfo = (LoggingInfo) pre;
- }
- }
-
interface CanReUseCallback {
boolean canReUse(RecyclerView.ViewHolder viewHolder);
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LoggingItemAnimator.java b/v7/recyclerview/tests/src/android/support/v7/widget/LoggingItemAnimator.java
index 04031d4..e41286e 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/LoggingItemAnimator.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LoggingItemAnimator.java
@@ -14,6 +14,7 @@
package android.support.v7.widget;
import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -29,8 +30,59 @@
final ArrayList<RecyclerView.ViewHolder> mChangeNewVHs = new ArrayList<RecyclerView.ViewHolder>();
+ List<BaseRecyclerViewAnimationsTest.AnimateAppearance> mAnimateAppearanceList = new ArrayList<>();
+ List<BaseRecyclerViewAnimationsTest.AnimateDisappearance> mAnimateDisappearanceList = new ArrayList<>();
+ List<BaseRecyclerViewAnimationsTest.AnimatePersistence> mAnimatePersistenceList = new ArrayList<>();
+ List<BaseRecyclerViewAnimationsTest.AnimateChange> mAnimateChangeList = new ArrayList<>();
+
CountDownLatch mWaitForPendingAnimations;
+ public boolean contains(RecyclerView.ViewHolder viewHolder,
+ List<? extends BaseRecyclerViewAnimationsTest.AnimateLogBase> list) {
+ for (BaseRecyclerViewAnimationsTest.AnimateLogBase log : list) {
+ if (log.viewHolder == viewHolder) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean animateDisappearance(RecyclerView.ViewHolder viewHolder,
+ ItemHolderInfo preLayoutInfo) {
+ mAnimateDisappearanceList
+ .add(new BaseRecyclerViewAnimationsTest.AnimateDisappearance(viewHolder, null));
+ return super.animateDisappearance(viewHolder, preLayoutInfo);
+ }
+
+ @Override
+ public boolean animateAppearance(RecyclerView.ViewHolder viewHolder,
+ ItemHolderInfo preLayoutInfo,
+ ItemHolderInfo postLayoutInfo) {
+ mAnimateAppearanceList
+ .add(new BaseRecyclerViewAnimationsTest.AnimateAppearance(viewHolder, null, null));
+ return super.animateAppearance(viewHolder, preLayoutInfo, postLayoutInfo);
+ }
+
+ @Override
+ public boolean animatePersistence(RecyclerView.ViewHolder viewHolder,
+ ItemHolderInfo preInfo,
+ ItemHolderInfo postInfo) {
+ mAnimatePersistenceList
+ .add(new BaseRecyclerViewAnimationsTest.AnimatePersistence(viewHolder, null, null));
+ return super.animatePersistence(viewHolder, preInfo, postInfo);
+ }
+
+ @Override
+ public boolean animateChange(RecyclerView.ViewHolder oldHolder,
+ RecyclerView.ViewHolder newHolder, ItemHolderInfo preInfo,
+ ItemHolderInfo postInfo) {
+ mAnimateChangeList
+ .add(new BaseRecyclerViewAnimationsTest.AnimateChange(oldHolder, newHolder, null,
+ null));
+ return super.animateChange(oldHolder, newHolder, preInfo, postInfo);
+ }
+
@Override
public void runPendingAnimations() {
if (mWaitForPendingAnimations != null) {
@@ -84,5 +136,9 @@
mMoveVHs.clear();
mChangeOldVHs.clear();
mChangeNewVHs.clear();
+ mAnimateChangeList.clear();
+ mAnimatePersistenceList.clear();
+ mAnimateAppearanceList.clear();
+ mAnimateDisappearanceList.clear();
}
}
\ No newline at end of file
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
index 85b6a5f..9887d99 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
@@ -17,6 +17,7 @@
package android.support.v7.widget;
import android.graphics.Rect;
+import android.os.Debug;
import android.support.v4.view.ViewCompat;
import android.util.Log;
import android.view.View;
@@ -35,6 +36,310 @@
*/
public class RecyclerViewAnimationsTest extends BaseRecyclerViewAnimationsTest {
+ final List<TestViewHolder> recycledVHs = new ArrayList<>();
+
+ public void testDontLayoutReusedViewWithoutPredictive() throws Throwable {
+ reuseHiddenViewTest(new ReuseTestCallback() {
+ @Override
+ public void postSetup(List<TestViewHolder> recycledList,
+ final TestViewHolder target) throws Throwable {
+ LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+ .getItemAnimator();
+ itemAnimator.reset();
+ mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
+ @Override
+ void beforePreLayout(RecyclerView.Recycler recycler,
+ AnimationLayoutManager lm, RecyclerView.State state) {
+ fail("pre layout is not expected");
+ }
+
+ @Override
+ void beforePostLayout(RecyclerView.Recycler recycler,
+ AnimationLayoutManager layoutManager,
+ RecyclerView.State state) {
+ mLayoutItemCount = 7;
+ View targetView = recycler
+ .getViewForPosition(target.getAdapterPosition());
+ assertSame(targetView, target.itemView);
+ super.beforePostLayout(recycler, layoutManager, state);
+ }
+
+ @Override
+ void afterPostLayout(RecyclerView.Recycler recycler,
+ AnimationLayoutManager layoutManager,
+ RecyclerView.State state) {
+ super.afterPostLayout(recycler, layoutManager, state);
+ assertNull("test sanity. this view should not be re-laid out in post "
+ + "layout", target.itemView.getParent());
+ }
+ };
+ mLayoutManager.expectLayouts(1);
+ mLayoutManager.requestSimpleAnimationsInNextLayout();
+ requestLayoutOnUIThread(mRecyclerView);
+ mLayoutManager.waitForLayout(2);
+ checkForMainThreadException();
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+ // This is a LayoutManager problem if it asked for the view but didn't properly
+ // lay it out. It will move to disappearance
+ assertTrue(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+ waitForAnimations(5);
+ assertTrue(recycledVHs.contains(target));
+ }
+ });
+ }
+
+ public void testDontLayoutReusedViewWithPredictive() throws Throwable {
+ reuseHiddenViewTest(new ReuseTestCallback() {
+ @Override
+ public void postSetup(List<TestViewHolder> recycledList,
+ final TestViewHolder target) throws Throwable {
+ LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+ .getItemAnimator();
+ itemAnimator.reset();
+ mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
+ @Override
+ void beforePreLayout(RecyclerView.Recycler recycler,
+ AnimationLayoutManager lm, RecyclerView.State state) {
+ mLayoutItemCount = 9;
+ super.beforePreLayout(recycler, lm, state);
+ }
+
+ @Override
+ void beforePostLayout(RecyclerView.Recycler recycler,
+ AnimationLayoutManager layoutManager,
+ RecyclerView.State state) {
+ mLayoutItemCount = 7;
+ super.beforePostLayout(recycler, layoutManager, state);
+ }
+
+ @Override
+ void afterPostLayout(RecyclerView.Recycler recycler,
+ AnimationLayoutManager layoutManager,
+ RecyclerView.State state) {
+ super.afterPostLayout(recycler, layoutManager, state);
+ assertNull("test sanity. this view should not be re-laid out in post "
+ + "layout", target.itemView.getParent());
+ }
+ };
+ mLayoutManager.expectLayouts(2);
+ mTestAdapter.deleteAndNotify(1, 1);
+ mLayoutManager.waitForLayout(2);
+ checkForMainThreadException();
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+ // This is a LayoutManager problem if it asked for the view but didn't properly
+ // lay it out. It will move to disappearance.
+ assertTrue(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+ waitForAnimations(5);
+ assertTrue(recycledVHs.contains(target));
+ }
+ });
+ }
+
+ public void testReuseHiddenViewWithoutPredictive() throws Throwable {
+ reuseHiddenViewTest(new ReuseTestCallback() {
+ @Override
+ public void postSetup(List<TestViewHolder> recycledList,
+ TestViewHolder target) throws Throwable {
+ LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+ .getItemAnimator();
+ itemAnimator.reset();
+ mLayoutManager.expectLayouts(1);
+ mLayoutManager.requestSimpleAnimationsInNextLayout();
+ mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
+ requestLayoutOnUIThread(mRecyclerView);
+ mLayoutManager.waitForLayout(2);
+ waitForAnimations(5);
+ assertTrue(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+ assertFalse(recycledVHs.contains(target));
+ }
+ });
+ }
+
+ public void testReuseHiddenViewWithoutAnimations() throws Throwable {
+ reuseHiddenViewTest(new ReuseTestCallback() {
+ @Override
+ public void postSetup(List<TestViewHolder> recycledList,
+ TestViewHolder target) throws Throwable {
+ LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+ .getItemAnimator();
+ itemAnimator.reset();
+ mLayoutManager.expectLayouts(1);
+ mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
+ requestLayoutOnUIThread(mRecyclerView);
+ mLayoutManager.waitForLayout(2);
+ waitForAnimations(5);
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+ assertFalse(recycledVHs.contains(target));
+ }
+ });
+ }
+
+ public void testReuseHiddenViewWithPredictive() throws Throwable {
+ reuseHiddenViewTest(new ReuseTestCallback() {
+ @Override
+ public void postSetup(List<TestViewHolder> recycledList,
+ TestViewHolder target) throws Throwable {
+ // it should move to change scrap and then show up from there
+ LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+ .getItemAnimator();
+ itemAnimator.reset();
+ mLayoutManager.expectLayouts(2);
+ mTestAdapter.deleteAndNotify(2, 1);
+ mLayoutManager.waitForLayout(2);
+ waitForAnimations(5);
+ // This LM does not layout the additional item so it does predictive wrong.
+ // We should still handle it and animate persistence for this item
+ assertTrue(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+ assertTrue(itemAnimator.mMoveVHs.contains(target));
+ assertFalse(recycledVHs.contains(target));
+ }
+ });
+ }
+
+ public void testReuseHiddenViewWithProperPredictive() throws Throwable {
+ reuseHiddenViewTest(new ReuseTestCallback() {
+ @Override
+ public void postSetup(List<TestViewHolder> recycledList,
+ TestViewHolder target) throws Throwable {
+ // it should move to change scrap and then show up from there
+ LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+ .getItemAnimator();
+ itemAnimator.reset();
+ mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
+ @Override
+ void beforePreLayout(RecyclerView.Recycler recycler,
+ AnimationLayoutManager lm, RecyclerView.State state) {
+ mLayoutItemCount = 9;
+ super.beforePreLayout(recycler, lm, state);
+ }
+
+ @Override
+ void afterPreLayout(RecyclerView.Recycler recycler,
+ AnimationLayoutManager layoutManager,
+ RecyclerView.State state) {
+ mLayoutItemCount = 8;
+ super.afterPreLayout(recycler, layoutManager, state);
+ }
+ };
+
+ mLayoutManager.expectLayouts(2);
+ mTestAdapter.deleteAndNotify(2, 1);
+ mLayoutManager.waitForLayout(2);
+ waitForAnimations(5);
+ // This LM implements predictive animations properly by requesting target view
+ // in pre-layout.
+ assertTrue(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+ assertTrue(itemAnimator.mMoveVHs.contains(target));
+ assertFalse(recycledVHs.contains(target));
+ }
+ });
+ }
+
+ public void testDontReuseHiddenViewOnInvalidate() throws Throwable {
+ reuseHiddenViewTest(new ReuseTestCallback() {
+ @Override
+ public void postSetup(List<TestViewHolder> recycledList,
+ TestViewHolder target) throws Throwable {
+ // it should move to change scrap and then show up from there
+ LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+ .getItemAnimator();
+ itemAnimator.reset();
+ mLayoutManager.expectLayouts(1);
+ mTestAdapter.dispatchDataSetChanged();
+ mLayoutManager.waitForLayout(2);
+ waitForAnimations(5);
+ assertFalse(mRecyclerView.getItemAnimator().isRunning());
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+ assertTrue(recycledVHs.contains(target));
+ }
+ });
+ }
+
+ public void testDontReuseOnTypeChange() throws Throwable {
+ reuseHiddenViewTest(new ReuseTestCallback() {
+ @Override
+ public void postSetup(List<TestViewHolder> recycledList,
+ TestViewHolder target) throws Throwable {
+ // it should move to change scrap and then show up from there
+ LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
+ .getItemAnimator();
+ itemAnimator.reset();
+ mLayoutManager.expectLayouts(1);
+ target.mBoundItem.mType += 2;
+ mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
+ mTestAdapter.changeAndNotify(target.getAdapterPosition(), 1);
+ requestLayoutOnUIThread(mRecyclerView);
+ mLayoutManager.waitForLayout(2);
+
+ assertTrue(itemAnimator.mChangeOldVHs.contains(target));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
+ assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
+ assertTrue(mRecyclerView.mChildHelper.isHidden(target.itemView));
+ assertFalse(recycledVHs.contains(target));
+ waitForAnimations(5);
+ assertTrue(recycledVHs.contains(target));
+ }
+ });
+ }
+
+ interface ReuseTestCallback {
+
+ void postSetup(List<TestViewHolder> recycledList, TestViewHolder target) throws Throwable;
+ }
+
+ @Override
+ protected RecyclerView.ItemAnimator createItemAnimator() {
+ return new LoggingItemAnimator();
+ }
+
+ public void reuseHiddenViewTest(ReuseTestCallback callback) throws Throwable {
+ TestAdapter adapter = new TestAdapter(10) {
+ @Override
+ public void onViewRecycled(TestViewHolder holder) {
+ super.onViewRecycled(holder);
+ recycledVHs.add(holder);
+ }
+ };
+ setupBasic(10, 0, 10, adapter);
+ mRecyclerView.setItemViewCacheSize(0);
+ TestViewHolder target = (TestViewHolder) mRecyclerView.findViewHolderForAdapterPosition(9);
+ mRecyclerView.getItemAnimator().setAddDuration(1000);
+ mRecyclerView.getItemAnimator().setRemoveDuration(1000);
+ mRecyclerView.getItemAnimator().setChangeDuration(1000);
+ mRecyclerView.getItemAnimator().setMoveDuration(1000);
+ mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 8;
+ mLayoutManager.expectLayouts(2);
+ adapter.deleteAndNotify(2, 1);
+ mLayoutManager.waitForLayout(2);
+ // test sanity, make sure target is hidden now
+ assertTrue("test sanity", mRecyclerView.mChildHelper.isHidden(target.itemView));
+ callback.postSetup(recycledVHs, target);
+ // TODO TEST ITEM INVALIDATION OR TYPE CHANGE IN BETWEEN
+ // TODO TEST ITEM IS RECEIVED FROM RECYCLER BUT NOT RE-ADDED
+ // TODO TEST ITEM ANIMATOR IS CALLED TO GET NEW INFORMATION ABOUT LOCATION
+
+ }
+
public void testDetachBeforeAnimations() throws Throwable {
setupBasic(10, 0, 5);
final RecyclerView rv = mRecyclerView;
@@ -76,7 +381,7 @@
mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
- RecyclerView.State state) {
+ RecyclerView.State state) {
if (view == targetChild[0]) {
outRect.set(10, 20, 30, 40);
} else {
@@ -230,7 +535,6 @@
mLayoutManager.waitForLayout(2);
-
}
public void testAddRemoveSamePass() throws Throwable {
@@ -326,7 +630,7 @@
changeAnimTest(false, false, true, false);
}
- public void testChangeAnimations() throws Throwable {
+ public void testChangeAnimations() throws Throwable {
final boolean[] booleans = {true, false};
for (boolean supportsChange : booleans) {
for (boolean changeType : booleans) {
@@ -341,7 +645,7 @@
}
public void changeAnimTest(final boolean supportsChangeAnim, final boolean changeType,
- final boolean hasStableIds, final boolean deleteSomeItems) throws Throwable {
+ final boolean hasStableIds, final boolean deleteSomeItems) throws Throwable {
final int changedIndex = 3;
final int defaultType = 1;
final AtomicInteger changedIndexNewType = new AtomicInteger(defaultType);
@@ -376,7 +680,7 @@
};
testAdapter.setHasStableIds(hasStableIds);
setupBasic(testAdapter.getItemCount(), 0, 10, testAdapter);
- ((SimpleItemAnimator)mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(
+ ((SimpleItemAnimator) mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(
supportsChangeAnim);
final RecyclerView.ViewHolder toBeChangedVH =
@@ -435,7 +739,7 @@
if (list1.size() != list2.size()) {
return false;
}
- for (int i= 0; i < list1.size(); i++) {
+ for (int i = 0; i < list1.size(); i++) {
if (!list1.get(i).equals(list2.get(i))) {
return false;
}
@@ -444,8 +748,8 @@
}
private void testChangeWithPayload(final boolean supportsChangeAnim,
- Object[][] notifyPayloads, Object[][] expectedPayloadsInOnBind)
- throws Throwable {
+ Object[][] notifyPayloads, Object[][] expectedPayloadsInOnBind)
+ throws Throwable {
final List<Object> expectedPayloads = new ArrayList<Object>();
final int changedIndex = 3;
TestAdapter testAdapter = new TestAdapter(10) {
@@ -476,11 +780,11 @@
};
testAdapter.setHasStableIds(false);
setupBasic(testAdapter.getItemCount(), 0, 10, testAdapter);
- ((SimpleItemAnimator)mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(
+ ((SimpleItemAnimator) mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(
supportsChangeAnim);
int numTests = notifyPayloads.length;
- for (int i= 0; i < numTests; i++) {
+ for (int i = 0; i < numTests; i++) {
mLayoutManager.expectLayouts(1);
expectedPayloads.clear();
for (int j = 0; j < expectedPayloadsInOnBind[i].length; j++) {
@@ -496,45 +800,46 @@
}
});
mLayoutManager.waitForLayout(2);
+ checkForMainThreadException();
}
}
- public void testCrossFadingChangeAnimationWithPayload() throws Throwable {
+ public void testCrossFadingChangeAnimationWithPayload() throws Throwable {
// for crossfading change animation, will receive EMPTY payload in onBindViewHolder
testChangeWithPayload(true,
new Object[][]{
- new Object[]{"abc"},
- new Object[]{"abc", null, "cdf"},
- new Object[]{"abc", null},
- new Object[]{null, "abc"},
- new Object[]{"abc", "cdf"}
+ new Object[]{"abc"},
+ new Object[]{"abc", null, "cdf"},
+ new Object[]{"abc", null},
+ new Object[]{null, "abc"},
+ new Object[]{"abc", "cdf"}
},
new Object[][]{
- new Object[0],
- new Object[0],
- new Object[0],
- new Object[0],
- new Object[0]
+ new Object[0],
+ new Object[0],
+ new Object[0],
+ new Object[0],
+ new Object[0]
});
}
- public void testNoChangeAnimationWithPayload() throws Throwable {
+ public void testNoChangeAnimationWithPayload() throws Throwable {
// for Change Animation disabled, payload should match the payloads unless
// null payload is fired.
testChangeWithPayload(false,
new Object[][]{
- new Object[]{"abc"},
- new Object[]{"abc", null, "cdf"},
- new Object[]{"abc", null},
- new Object[]{null, "abc"},
- new Object[]{"abc", "cdf"}
+ new Object[]{"abc"},
+ new Object[]{"abc", null, "cdf"},
+ new Object[]{"abc", null},
+ new Object[]{null, "abc"},
+ new Object[]{"abc", "cdf"}
},
new Object[][]{
- new Object[]{"abc"},
- new Object[0],
- new Object[0],
- new Object[0],
- new Object[]{"abc", "cdf"}
+ new Object[]{"abc"},
+ new Object[0],
+ new Object[0],
+ new Object[0],
+ new Object[]{"abc", "cdf"}
});
}
@@ -570,7 +875,7 @@
});
// now keep adding children to trigger more children being created etc.
- for (int i = 0; i < 100; i ++) {
+ for (int i = 0; i < 100; i++) {
adapter.addAndNotify(15, 1);
Thread.sleep(50);
}
@@ -618,19 +923,19 @@
adapter.setHasStableIds(true);
initialSet.addAll(adapter.mItems);
positionStatesTest(itemCount, 5, 5, adapter, new AdapterOps() {
- @Override
- void onRun(TestAdapter testAdapter) throws Throwable {
- Item item5 = adapter.mItems.get(5);
- Item item6 = adapter.mItems.get(6);
- item5.mAdapterIndex = 6;
- item6.mAdapterIndex = 5;
- adapter.mItems.remove(5);
- adapter.mItems.add(6, item5);
- adapter.dispatchDataSetChanged();
- //hacky, we support only 1 layout pass
- mLayoutManager.layoutLatch.countDown();
- }
- }, PositionConstraint.scrap(6, -1, 5), PositionConstraint.scrap(5, -1, 6),
+ @Override
+ void onRun(TestAdapter testAdapter) throws Throwable {
+ Item item5 = adapter.mItems.get(5);
+ Item item6 = adapter.mItems.get(6);
+ item5.mAdapterIndex = 6;
+ item6.mAdapterIndex = 5;
+ adapter.mItems.remove(5);
+ adapter.mItems.add(6, item5);
+ adapter.dispatchDataSetChanged();
+ //hacky, we support only 1 layout pass
+ mLayoutManager.layoutLatch.countDown();
+ }
+ }, PositionConstraint.scrap(6, -1, 5), PositionConstraint.scrap(5, -1, 6),
PositionConstraint.scrap(7, -1, 7), PositionConstraint.scrap(8, -1, 8),
PositionConstraint.scrap(9, -1, 9));
// now mix items.
@@ -682,7 +987,7 @@
itemViewTypeQueries.contains(i));
if (adapter.hasStableIds()) {
assertTrue("getItemId for existing item " + i
- + " should be called when adapter has stable ids",
+ + " should be called when adapter has stable ids",
itemIdQueries.contains(i));
}
}
@@ -1035,35 +1340,35 @@
public void testAddDelete2() throws Throwable {
positionStatesTest(5, 0, 5, new AdapterOps() {
- // 0 1 2 3 4
- // 0 1 2 a b 3 4
- // 0 1 b 3 4
- // pre: 0 1 2 3 4
- // pre w/ adap: 0 1 2 b 3 4
- @Override
- void onRun(TestAdapter adapter) throws Throwable {
- adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{2, -2});
- }
- }, PositionConstraint.scrap(2, 2, -1), PositionConstraint.scrap(1, 1, 1),
+ // 0 1 2 3 4
+ // 0 1 2 a b 3 4
+ // 0 1 b 3 4
+ // pre: 0 1 2 3 4
+ // pre w/ adap: 0 1 2 b 3 4
+ @Override
+ void onRun(TestAdapter adapter) throws Throwable {
+ adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{2, -2});
+ }
+ }, PositionConstraint.scrap(2, 2, -1), PositionConstraint.scrap(1, 1, 1),
PositionConstraint.scrap(3, 3, 3)
);
}
public void testAddDelete1() throws Throwable {
positionStatesTest(5, 0, 5, new AdapterOps() {
- // 0 1 2 3 4
- // 0 1 2 a b 3 4
- // 0 2 a b 3 4
- // 0 c d 2 a b 3 4
- // 0 c d 2 a 4
- // c d 2 a 4
- // pre: 0 1 2 3 4
- @Override
- void onRun(TestAdapter adapter) throws Throwable {
- adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{1, -1},
- new int[]{1, 2}, new int[]{5, -2}, new int[]{0, -1});
- }
- }, PositionConstraint.scrap(0, 0, -1), PositionConstraint.scrap(1, 1, -1),
+ // 0 1 2 3 4
+ // 0 1 2 a b 3 4
+ // 0 2 a b 3 4
+ // 0 c d 2 a b 3 4
+ // 0 c d 2 a 4
+ // c d 2 a 4
+ // pre: 0 1 2 3 4
+ @Override
+ void onRun(TestAdapter adapter) throws Throwable {
+ adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{1, -1},
+ new int[]{1, 2}, new int[]{5, -2}, new int[]{0, -1});
+ }
+ }, PositionConstraint.scrap(0, 0, -1), PositionConstraint.scrap(1, 1, -1),
PositionConstraint.scrap(2, 2, 2), PositionConstraint.scrap(3, 3, -1),
PositionConstraint.scrap(4, 4, 4), PositionConstraint.adapter(0),
PositionConstraint.adapter(1), PositionConstraint.adapter(3)
@@ -1072,12 +1377,12 @@
public void testAddSameIndexTwice() throws Throwable {
positionStatesTest(12, 2, 7, new AdapterOps() {
- @Override
- void onRun(TestAdapter adapter) throws Throwable {
- adapter.addAndNotify(new int[]{1, 2}, new int[]{5, 1}, new int[]{5, 1},
- new int[]{11, 1});
- }
- }, PositionConstraint.adapterScrap(0, 0), PositionConstraint.adapterScrap(1, 3),
+ @Override
+ void onRun(TestAdapter adapter) throws Throwable {
+ adapter.addAndNotify(new int[]{1, 2}, new int[]{5, 1}, new int[]{5, 1},
+ new int[]{11, 1});
+ }
+ }, PositionConstraint.adapterScrap(0, 0), PositionConstraint.adapterScrap(1, 3),
PositionConstraint.scrap(2, 2, 4), PositionConstraint.scrap(3, 3, 7),
PositionConstraint.scrap(4, 4, 8), PositionConstraint.scrap(7, 7, 12),
PositionConstraint.scrap(8, 8, 13)
@@ -1086,12 +1391,12 @@
public void testDeleteTwice() throws Throwable {
positionStatesTest(12, 2, 7, new AdapterOps() {
- @Override
- void onRun(TestAdapter adapter) throws Throwable {
- adapter.deleteAndNotify(new int[]{0, 1}, new int[]{1, 1}, new int[]{7, 1},
- new int[]{0, 1});// delete item ids 0,2,9,1
- }
- }, PositionConstraint.scrap(2, 0, -1), PositionConstraint.scrap(3, 1, 0),
+ @Override
+ void onRun(TestAdapter adapter) throws Throwable {
+ adapter.deleteAndNotify(new int[]{0, 1}, new int[]{1, 1}, new int[]{7, 1},
+ new int[]{0, 1});// delete item ids 0,2,9,1
+ }
+ }, PositionConstraint.scrap(2, 0, -1), PositionConstraint.scrap(3, 1, 0),
PositionConstraint.scrap(4, 2, 1), PositionConstraint.scrap(5, 3, 2),
PositionConstraint.scrap(6, 4, 3), PositionConstraint.scrap(8, 6, 5),
PositionConstraint.adapterScrap(7, 6), PositionConstraint.adapterScrap(8, 7)
@@ -1103,10 +1408,11 @@
int firstLayoutItemCount, AdapterOps adapterChanges,
final PositionConstraint... constraints) throws Throwable {
positionStatesTest(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null,
- adapterChanges, constraints);
+ adapterChanges, constraints);
}
+
public void positionStatesTest(int itemCount, int firstLayoutStartIndex,
- int firstLayoutItemCount,TestAdapter adapter, AdapterOps adapterChanges,
+ int firstLayoutItemCount, TestAdapter adapter, AdapterOps adapterChanges,
final PositionConstraint... constraints) throws Throwable {
setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, adapter);
mLayoutManager.expectLayouts(2);
@@ -1125,7 +1431,8 @@
= collectPositions(lm.mRecyclerView, recycler, state, ids);
StringBuilder positionLog = new StringBuilder("\nPosition logs:\n");
for (Map.Entry<Integer, CollectPositionResult> entry : positions.entrySet()) {
- positionLog.append(entry.getKey()).append(":").append(entry.getValue()).append("\n");
+ positionLog.append(entry.getKey()).append(":").append(entry.getValue())
+ .append("\n");
}
for (PositionConstraint constraint : constraints) {
if (constraint.mPreLayoutPos != -1) {
@@ -1170,7 +1477,8 @@
public void testAddThenRecycleRemovedView() throws Throwable {
setupBasic(10);
final AtomicInteger step = new AtomicInteger(0);
- final List<RecyclerView.ViewHolder> animateRemoveList = new ArrayList<RecyclerView.ViewHolder>();
+ final List<RecyclerView.ViewHolder> animateRemoveList
+ = new ArrayList<RecyclerView.ViewHolder>();
DefaultItemAnimator animator = new DefaultItemAnimator() {
@Override
public boolean animateRemove(RecyclerView.ViewHolder holder) {