Merge "Fix janky browse transition" into mnc-dev
diff --git a/customtabs/Android.mk b/customtabs/Android.mk
index 5e86d24..be4cc43 100644
--- a/customtabs/Android.mk
+++ b/customtabs/Android.mk
@@ -24,6 +24,7 @@
 LOCAL_AIDL_INCLUDES := $LOCAL_PATH/src
 LOCAL_SRC_FILES := $(call all-java-files-under, src) \
     $(call all-Iaidl-files-under, src)
+LOCAL_JAVA_LIBRARIES := android-support-annotations
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
 # API Check
@@ -31,6 +32,6 @@
 support_module := $(LOCAL_MODULE)
 support_module_api_dir := $(LOCAL_PATH)/api
 support_module_src_files := $(LOCAL_SRC_FILES)
-support_module_java_libraries := android-support-customtabs
+support_module_java_libraries := $(LOCAL_JAVA_LIBRARIES)
 support_module_java_packages := android.support.customtabs
 include $(SUPPORT_API_CHECK)
diff --git a/customtabs/api/current.txt b/customtabs/api/current.txt
index 626ba5b..e2dd48b 100644
--- a/customtabs/api/current.txt
+++ b/customtabs/api/current.txt
@@ -2,6 +2,7 @@
 
   public class CustomTabsCallback {
     ctor public CustomTabsCallback();
+    method public void extraCallback(java.lang.String, android.os.Bundle);
     method public void onNavigationEvent(int, android.os.Bundle);
     field public static final int NAVIGATION_FINISHED = 2; // 0x2
     field public static final int NAVIGATION_STARTED = 1; // 0x1
@@ -9,26 +10,46 @@
 
   public class CustomTabsClient {
     method public static boolean bindCustomTabsService(android.content.Context, java.lang.String, android.support.customtabs.CustomTabsServiceConnection);
+    method public android.os.Bundle extraCommand(java.lang.String, android.os.Bundle);
     method public android.support.customtabs.CustomTabsSession newSession(android.support.customtabs.CustomTabsCallback);
     method public boolean warmup(long);
   }
 
-  public class CustomTabsIntent {
-    ctor public CustomTabsIntent();
-    method public static android.content.Intent getViewIntentWithNoSession(java.lang.String, android.net.Uri);
+  public final class CustomTabsIntent {
+    method public void launchUrl(android.app.Activity, android.net.Uri);
     field public static final java.lang.String EXTRA_ACTION_BUTTON_BUNDLE = "android.support.customtabs.extra.ACTION_BUTTON_BUNDLE";
+    field public static final java.lang.String EXTRA_CLOSE_BUTTON_ICON = "android.support.customtabs.extra.CLOSE_BUTTON_ICON";
     field public static final java.lang.String EXTRA_EXIT_ANIMATION_BUNDLE = "android.support.customtabs.extra.EXIT_ANIMATION_BUNDLE";
     field public static final java.lang.String EXTRA_MENU_ITEMS = "android.support.customtabs.extra.MENU_ITEMS";
     field public static final java.lang.String EXTRA_SESSION = "android.support.customtabs.extra.SESSION";
+    field public static final java.lang.String EXTRA_TITLE_VISIBILITY_STATE = "android.support.customtabs.extra.TITLE_VISIBILITY";
     field public static final java.lang.String EXTRA_TOOLBAR_COLOR = "android.support.customtabs.extra.TOOLBAR_COLOR";
     field public static final java.lang.String KEY_ICON = "android.support.customtabs.customaction.ICON";
     field public static final java.lang.String KEY_MENU_ITEM_TITLE = "android.support.customtabs.customaction.MENU_ITEM_TITLE";
     field public static final java.lang.String KEY_PENDING_INTENT = "android.support.customtabs.customaction.PENDING_INTENT";
+    field public static final int NO_TITLE = 0; // 0x0
+    field public static final int SHOW_PAGE_TITLE = 1; // 0x1
+    field public final android.content.Intent intent;
+    field public final android.os.Bundle startAnimationBundle;
+  }
+
+  public static final class CustomTabsIntent.Builder {
+    ctor public CustomTabsIntent.Builder();
+    ctor public CustomTabsIntent.Builder(android.support.customtabs.CustomTabsSession);
+    method public android.support.customtabs.CustomTabsIntent.Builder addMenuItem(java.lang.String, android.app.PendingIntent);
+    method public android.support.customtabs.CustomTabsIntent build();
+    method public android.support.customtabs.CustomTabsIntent.Builder setActionButton(android.graphics.Bitmap, android.app.PendingIntent);
+    method public android.support.customtabs.CustomTabsIntent.Builder setCloseButtonIcon(android.graphics.Bitmap);
+    method public android.support.customtabs.CustomTabsIntent.Builder setExitAnimations(android.content.Context, int, int);
+    method public android.support.customtabs.CustomTabsIntent.Builder setShowTitle(boolean);
+    method public android.support.customtabs.CustomTabsIntent.Builder setStartAnimations(android.content.Context, int, int);
+    method public android.support.customtabs.CustomTabsIntent.Builder setToolbarColor(int);
   }
 
   public abstract class CustomTabsService extends android.app.Service {
     ctor public CustomTabsService();
     method protected boolean cleanUpSession(android.support.customtabs.CustomTabsSessionToken);
+    method protected abstract android.os.Bundle extraCommand(java.lang.String, android.os.Bundle);
     method protected abstract boolean mayLaunchUrl(android.support.customtabs.CustomTabsSessionToken, android.net.Uri, android.os.Bundle, java.util.List<android.os.Bundle>);
     method protected abstract boolean newSession(android.support.customtabs.CustomTabsSessionToken);
     method public android.os.IBinder onBind(android.content.Intent);
@@ -43,8 +64,7 @@
     method public final void onServiceConnected(android.content.ComponentName, android.os.IBinder);
   }
 
-  public class CustomTabsSession {
-    method public android.content.Intent getViewIntent(android.net.Uri);
+  public final class CustomTabsSession {
     method public boolean mayLaunchUrl(android.net.Uri, android.os.Bundle, java.util.List<android.os.Bundle>);
   }
 
diff --git a/customtabs/build.gradle b/customtabs/build.gradle
index fc552c2..66e77b6 100644
--- a/customtabs/build.gradle
+++ b/customtabs/build.gradle
@@ -2,6 +2,10 @@
 
 archivesBaseName = 'customtabs'
 
+dependencies {
+    compile project(':support-annotations')
+}
+
 android {
     compileSdkVersion 'current'
 
@@ -12,6 +16,9 @@
         main.res.srcDir 'res'
         main.assets.srcDir 'assets'
         main.resources.srcDir 'java'
+
+        androidTest.setRoot('tests')
+        androidTest.java.srcDir('tests/src/')
     }
 
     compileOptions {
diff --git a/customtabs/src/android/support/customtabs/CustomTabsCallback.java b/customtabs/src/android/support/customtabs/CustomTabsCallback.java
index 274388b..fa1bf89 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsCallback.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsCallback.java
@@ -39,4 +39,20 @@
      * @param extras Reserved for future use.
      */
     public void onNavigationEvent(int navigationEvent, Bundle extras) {}
+
+    /**
+     * Unsupported callbacks that may be provided by the implementation.
+     *
+     * <p>
+     * <strong>Note:</strong>Clients should <strong>never</strong> rely on this callback to be
+     * called and/or to have a defined behavior, as it is entirely implementation-defined and not
+     * supported.
+     *
+     * <p> This can be used by implementations to add extra callbacks, for testing or experimental
+     * purposes.
+     *
+     * @param callbackName Name of the extra callback.
+     * @param args Arguments for the calback
+     */
+    public void extraCallback(String callbackName, Bundle args) {}
 }
diff --git a/customtabs/src/android/support/customtabs/CustomTabsClient.java b/customtabs/src/android/support/customtabs/CustomTabsClient.java
index 779b2d7..7cc5c76 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsClient.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsClient.java
@@ -91,6 +91,11 @@
             public void onNavigationEvent(int navigationEvent, Bundle extras) {
                 if (callback != null) callback.onNavigationEvent(navigationEvent, extras);
             }
+
+            @Override
+            public void extraCallback(String callbackName, Bundle args) throws RemoteException {
+                if (callback != null) callback.extraCallback(callbackName, args);
+            }
         };
 
         try {
@@ -100,4 +105,12 @@
         }
         return new CustomTabsSession(mService, wrapper, mServiceComponentName);
     }
+
+    public Bundle extraCommand(String commandName, Bundle args) {
+        try {
+            return mService.extraCommand(commandName, args);
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
 }
diff --git a/customtabs/src/android/support/customtabs/CustomTabsIntent.java b/customtabs/src/android/support/customtabs/CustomTabsIntent.java
index e364a2e..6167e80 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsIntent.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsIntent.java
@@ -19,6 +19,7 @@
 import android.app.Activity;
 import android.app.ActivityOptions;
 import android.app.PendingIntent;
+import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.Color;
@@ -26,15 +27,23 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.support.annotation.AnimRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.ColorInt;
+import android.support.annotation.Nullable;
 
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.util.ArrayList;
 
 /**
- * Constants and utilities that will be used for low level control on customizing the UI and
- * functionality of a tab.
+ * Class holding the {@link Intent} and start bundle for a Custom Tabs Activity.
+ *
+ * <p>
+ * <strong>Note:</strong> The constants below are public for the browser implementation's benefit.
+ * You are strongly encouraged to use {@link CustomTabsIntent.Builder}.</p>
  */
-public class CustomTabsIntent {
+public final class CustomTabsIntent {
 
     /**
      * Extra used to match the session. This has to be included in the intent to open in
@@ -51,6 +60,29 @@
             "android.support.customtabs.extra.TOOLBAR_COLOR";
 
     /**
+     * Extra bitmap that specifies the icon of the back button on the toolbar. If the client chooses
+     * not to customize it, a default close button will be used.
+     */
+    public static final String EXTRA_CLOSE_BUTTON_ICON =
+            "android.support.customtabs.extra.CLOSE_BUTTON_ICON";
+
+    /**
+     * Extra (int) that specifies state for showing the page title. Default is {@link #NO_TITLE}.
+     */
+    public static final String EXTRA_TITLE_VISIBILITY_STATE =
+            "android.support.customtabs.extra.TITLE_VISIBILITY";
+
+    /**
+     * Don't show any title. Shows only the domain.
+     */
+    public static final int NO_TITLE = 0;
+
+    /**
+     * Shows the page title and the domain.
+     */
+    public static final int SHOW_PAGE_TITLE = 1;
+
+    /**
      * Bundle used for adding a custom action button to the custom tab toolbar. The client can
      * provide an icon {@link Bitmap} and a {@link PendingIntent} for the button.
      */
@@ -92,41 +124,191 @@
             "android.support.customtabs.extra.EXIT_ANIMATION_BUNDLE";
 
     /**
-     * Convenience method to create a VIEW intent without a session for the given package.
-     * @param packageName The package name to set in the intent.
-     * @param data        The data {@link Uri} to be used in the intent.
-     * @return            The intent with the given package, data and the right session extra.
+     * An {@link Intent} used to start the Custom Tabs Activity.
      */
-    public static Intent getViewIntentWithNoSession(String packageName, Uri data) {
-        Intent intent = new Intent(Intent.ACTION_VIEW, data);
-        intent.setPackage(packageName);
-        Bundle extras = new Bundle();
-        if (!safePutBinder(extras, EXTRA_SESSION, null)) return null;
-        intent.putExtras(extras);
-        return intent;
+    @NonNull public final Intent intent;
+
+    /**
+     * A {@link Bundle} containing the start animation for the Custom Tabs Activity.
+     */
+    @Nullable public final Bundle startAnimationBundle;
+
+    /**
+     * Convenience method to launch a Custom Tabs Activity.
+     * @param context The source Activity.
+     * @param url The URL to load in the Custom Tab.
+     */
+    public void launchUrl(Activity context, Uri url) {
+        intent.setData(url);
+        if (startAnimationBundle != null){
+            context.startActivity(intent, startAnimationBundle);
+        } else {
+            context.startActivity(intent);
+        }
+    }
+
+    private CustomTabsIntent(Intent intent, Bundle startAnimationBundle) {
+        this.intent = intent;
+        this.startAnimationBundle = startAnimationBundle;
     }
 
     /**
-     * A convenience method to handle putting an {@link IBinder} inside a {@link Bundle} for all
-     * Android version.
-     * @param bundle The bundle to insert the {@link IBinder}.
-     * @param key    The key to use while putting the {@link IBinder}.
-     * @param binder The {@link IBinder} to put.
-     * @return       Whether the operation was successful.
+     * Builder class for {@link CustomTabsIntent} objects.
      */
-    static boolean safePutBinder(Bundle bundle, String key, IBinder binder) {
-        try {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
-                bundle.putBinder(key, binder);
-            } else {
-                Method putBinderMethod =
-                        Bundle.class.getMethod("putIBinder", String.class, IBinder.class);
-                putBinderMethod.invoke(bundle, key, binder);
-            }
-        } catch (InvocationTargetException | IllegalAccessException
-                | IllegalArgumentException | NoSuchMethodException e) {
-            return false;
+    public static final class Builder {
+        private final Intent mIntent = new Intent(Intent.ACTION_VIEW);
+        private ArrayList<Bundle> mMenuItems = null;
+        private Bundle mStartAnimationBundle = null;
+
+        /**
+         * Creates a {@link CustomTabsIntent.Builder} object associated with no
+         * {@link CustomTabsSession}.
+         */
+        public Builder() {
+            this(null);
         }
-        return true;
+
+        /**
+         * Creates a {@link CustomTabsIntent.Builder} object associated with a given
+         * {@link CustomTabsSession}.
+         *
+         * Guarantees that the {@link Intent} will be sent to the same component as the one the
+         * session is associated with.
+         *
+         * @param session The session to associate this Builder with.
+         */
+        public Builder(@Nullable CustomTabsSession session) {
+            if (session != null) mIntent.setPackage(session.getComponentName().getPackageName());
+            Bundle bundle = new Bundle();
+            safePutBinder(bundle, EXTRA_SESSION, session == null ? null : session.getBinder());
+            mIntent.putExtras(bundle);
+        }
+
+        /**
+         * Sets the toolbar color.
+         *
+         * @param color {@link Color}
+         */
+        public Builder setToolbarColor(@ColorInt int color) {
+            mIntent.putExtra(EXTRA_TOOLBAR_COLOR, color);
+            return this;
+        }
+
+        /**
+         * Sets the Close button icon for the custom tab.
+         *
+         * @param icon The icon {@link Bitmap}
+         */
+        public Builder setCloseButtonIcon(@NonNull Bitmap icon) {
+            mIntent.putExtra(EXTRA_CLOSE_BUTTON_ICON, icon);
+            return this;
+        }
+
+        /**
+         * Sets whether the title should be shown in the custom tab.
+         *
+         * @param showTitle Whether the title should be shown.
+         */
+        public Builder setShowTitle(boolean showTitle) {
+            mIntent.putExtra(EXTRA_TITLE_VISIBILITY_STATE,
+                    showTitle ? SHOW_PAGE_TITLE : NO_TITLE);
+            return this;
+        }
+
+        /**
+         * Adds a menu item.
+         *
+         * @param label Menu label.
+         * @param pendingIntent Pending intent delivered when the menu item is clicked.
+         */
+        public Builder addMenuItem(@NonNull String label, @NonNull PendingIntent pendingIntent) {
+            if (mMenuItems == null) mMenuItems = new ArrayList<>();
+            Bundle bundle = new Bundle();
+            bundle.putString(KEY_MENU_ITEM_TITLE, label);
+            bundle.putParcelable(KEY_PENDING_INTENT, pendingIntent);
+            mMenuItems.add(bundle);
+            return this;
+        }
+
+        /**
+         * Set the action button.
+         *
+         * @param icon The icon.
+         * @param pendingIntent pending intent delivered when the button is clicked.
+         */
+        public Builder setActionButton(@NonNull Bitmap icon, @NonNull PendingIntent pendingIntent) {
+            Bundle bundle = new Bundle();
+            bundle.putParcelable(KEY_ICON, icon);
+            bundle.putParcelable(KEY_PENDING_INTENT, pendingIntent);
+            mIntent.putExtra(EXTRA_ACTION_BUTTON_BUNDLE, bundle);
+            return this;
+        }
+
+        /**
+         * Sets the start animations,
+         *
+         * @param context Application context.
+         * @param enterResId Resource ID of the "enter" animation for the browser.
+         * @param exitResId Resource ID of the "exit" animation for the application.
+         */
+        public Builder setStartAnimations(
+                @NonNull Context context, @AnimRes int enterResId, @AnimRes int exitResId) {
+            mStartAnimationBundle =
+                    ActivityOptions.makeCustomAnimation(context, enterResId, exitResId).toBundle();
+            return this;
+        }
+
+        /**
+         * Sets the exit animations,
+         *
+         * @param context Application context.
+         * @param enterResId Resource ID of the "enter" animation for the application.
+         * @param exitResId Resource ID of the "exit" animation for the browser.
+         */
+        public Builder setExitAnimations(
+                @NonNull Context context, @AnimRes int enterResId, @AnimRes int exitResId) {
+            Bundle bundle =
+                    ActivityOptions.makeCustomAnimation(context, enterResId, exitResId).toBundle();
+            mIntent.putExtra(EXTRA_EXIT_ANIMATION_BUNDLE, bundle);
+            return this;
+        }
+
+        /**
+         * Combines all the options that have been set and returns a new {@link CustomTabsIntent}
+         * object.
+         */
+        public CustomTabsIntent build() {
+            if (mMenuItems != null) {
+                mIntent.putParcelableArrayListExtra(CustomTabsIntent.EXTRA_MENU_ITEMS, mMenuItems);
+            }
+            return new CustomTabsIntent(mIntent, mStartAnimationBundle);
+        }
+
+        /**
+         * A convenience method to handle putting an {@link IBinder} inside a {@link Bundle} for all
+         * Android version.
+         * @param bundle The bundle to insert the {@link IBinder}.
+         * @param key    The key to use while putting the {@link IBinder}.
+         * @param binder The {@link IBinder} to put.
+         * @return       Whether the operation was successful.
+         */
+        private boolean safePutBinder(Bundle bundle, String key, IBinder binder) {
+            try {
+                // {@link Bundle#putBinder} exists since JB MR2, but we have
+                // {@link Bundle#putIBinder} which is the same method since the dawn of time. Use
+                // reflection when necessary to call it.
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+                    bundle.putBinder(key, binder);
+                } else {
+                    Method putBinderMethod =
+                            Bundle.class.getMethod("putIBinder", String.class, IBinder.class);
+                    putBinderMethod.invoke(bundle, key, binder);
+                }
+            } catch (InvocationTargetException | IllegalAccessException
+                    | IllegalArgumentException | NoSuchMethodException e) {
+                return false;
+            }
+            return true;
+        }
     }
 }
diff --git a/customtabs/src/android/support/customtabs/CustomTabsService.java b/customtabs/src/android/support/customtabs/CustomTabsService.java
index 5f70d88..13c737e 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsService.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsService.java
@@ -43,10 +43,9 @@
              "android.support.customtabs.action.CustomTabsService";
 
      /**
-     * For {@link CustomTabsService#mayLaunchUrl(long, Uri, Bundle, List)} calls that wants to
-     * specify more than one url, this key can be used with
-     * {@link Bundle#putParcelable(String, android.os.Parcelable)}
-     * to insert a new url to each bundle inside list of bundles.
+      * For {@link CustomTabsService#mayLaunchUrl} calls that wants to specify more than one url,
+      * this key can be used with {@link Bundle#putParcelable(String, android.os.Parcelable)}
+      * to insert a new url to each bundle inside list of bundles.
       */
      public static final String KEY_URL =
              "android.support.customtabs.otherurls.URL";
@@ -86,6 +85,11 @@
              return CustomTabsService.this.mayLaunchUrl(
                      new CustomTabsSessionToken(callback), url, extras, otherLikelyBundles);
          }
+
+         @Override
+         public Bundle extraCommand(String commandName, Bundle args) {
+             return CustomTabsService.this.extraCommand(commandName, args);
+         }
      };
 
      @Override
@@ -156,4 +160,20 @@
       */
      protected abstract boolean mayLaunchUrl(CustomTabsSessionToken sessionToken, Uri url,
              Bundle extras, List<Bundle> otherLikelyBundles);
-}
+
+     /**
+      * Unsupported commands that may be provided by the implementation.
+      *
+      * <p>
+      * <strong>Note:</strong>Clients should <strong>never</strong> rely on this method to have a
+      * defined behavior, as it is entirely implementation-defined and not supported.
+      *
+      * <p> This call can be used by implementations to add extra commands, for testing or
+      * experimental purposes.
+      *
+      * @param commandName Name of the extra command to execute.
+      * @param args Arguments for the command
+      * @return The result {@link Bundle}, or null.
+      */
+     protected abstract Bundle extraCommand(String commandName, Bundle args);
+ }
diff --git a/customtabs/src/android/support/customtabs/CustomTabsSession.java b/customtabs/src/android/support/customtabs/CustomTabsSession.java
index f7fa56f..ea4d6c6 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsSession.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsSession.java
@@ -17,28 +17,23 @@
 package android.support.customtabs;
 
 import android.content.ComponentName;
-import android.content.Intent;
 import android.net.Uri;
-import android.os.Build;
 import android.os.Bundle;
-import android.os.IBinder;
 import android.os.RemoteException;
 
-import java.lang.reflect.Method;
 import java.util.List;
 
 /**
  * A class to be used for Custom Tabs related communication. Clients that want to launch Custom Tabs
  * can use this class exclusively to handle all related communication.
  */
-public class CustomTabsSession {
+public final class CustomTabsSession {
     private static final String TAG = "CustomTabsSession";
     private final ICustomTabsService mService;
     private final ICustomTabsCallback mCallback;
     private final ComponentName mComponentName;
 
-    /**@hide*/
-    CustomTabsSession(
+    /* package */ CustomTabsSession(
             ICustomTabsService service, ICustomTabsCallback callback, ComponentName componentName) {
         mService = service;
         mCallback = callback;
@@ -69,22 +64,11 @@
         }
     }
 
-    /**
-     * Convenience method to create a VIEW intent associated with this session with
-     * the right identifier and package name.
-     * @param data        The data {@link Uri} to be used in the intent.
-     * @return            The intent with the package and the session extra set to the corresponding
-     *                    ones for this session.
-     */
-    public Intent getViewIntent(Uri data) {
-        Intent intent = new Intent(Intent.ACTION_VIEW, data);
-        intent.setPackage(mComponentName.getPackageName());
-        Bundle extras = new Bundle();
-        if (!CustomTabsIntent.safePutBinder(
-                extras, CustomTabsIntent.EXTRA_SESSION, mCallback.asBinder())) {
-            return null;
-        }
-        intent.putExtras(extras);
-        return intent;
+    /* package */ IBinder getBinder() {
+        return mCallback.asBinder();
+    }
+
+    /* package */ ComponentName getComponentName() {
+        return mComponentName;
     }
 }
\ No newline at end of file
diff --git a/customtabs/src/android/support/customtabs/CustomTabsSessionToken.java b/customtabs/src/android/support/customtabs/CustomTabsSessionToken.java
index a4481f3..479d1d9 100644
--- a/customtabs/src/android/support/customtabs/CustomTabsSessionToken.java
+++ b/customtabs/src/android/support/customtabs/CustomTabsSessionToken.java
@@ -53,6 +53,11 @@
     }
 
     @Override
+    public int hashCode() {
+        return getCallbackBinder().hashCode();
+    }
+
+    @Override
     public boolean equals(Object o) {
         if (!(o instanceof CustomTabsSessionToken)) return false;
         CustomTabsSessionToken token = (CustomTabsSessionToken) o;
diff --git a/customtabs/src/android/support/customtabs/ICustomTabsCallback.aidl b/customtabs/src/android/support/customtabs/ICustomTabsCallback.aidl
index 83a1002..a467864 100644
--- a/customtabs/src/android/support/customtabs/ICustomTabsCallback.aidl
+++ b/customtabs/src/android/support/customtabs/ICustomTabsCallback.aidl
@@ -24,4 +24,5 @@
  */
 oneway interface ICustomTabsCallback {
     void onNavigationEvent(int navigationEvent, in Bundle extras) = 1;
+    void extraCallback(String callbackName, in Bundle args) = 2;
 }
diff --git a/customtabs/src/android/support/customtabs/ICustomTabsService.aidl b/customtabs/src/android/support/customtabs/ICustomTabsService.aidl
index b7eae55..2565928 100644
--- a/customtabs/src/android/support/customtabs/ICustomTabsService.aidl
+++ b/customtabs/src/android/support/customtabs/ICustomTabsService.aidl
@@ -31,4 +31,5 @@
     boolean newSession(in ICustomTabsCallback callback) = 2;
     boolean mayLaunchUrl(in ICustomTabsCallback callback, in Uri url,
             in Bundle extras, in List<Bundle> otherLikelyBundles) = 3;
+    Bundle extraCommand(String commandName, in Bundle args) = 4;
 }
diff --git a/customtabs/tests/src/android/support/customtabs/CustomTabsIntentTest.java b/customtabs/tests/src/android/support/customtabs/CustomTabsIntentTest.java
new file mode 100644
index 0000000..9440282
--- /dev/null
+++ b/customtabs/tests/src/android/support/customtabs/CustomTabsIntentTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.customtabs;
+
+import android.content.Intent;
+import android.graphics.Color;
+import android.test.AndroidTestCase;
+
+/**
+ * Tests for CustomTabsIntent.
+ */
+public class CustomTabsIntentTest extends AndroidTestCase {
+
+    public void testBareboneCustomTabIntent() {
+        CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder().build();
+        Intent intent = customTabsIntent.intent;
+        assertNotNull(intent);
+        assertNull(customTabsIntent.startAnimationBundle);
+
+        assertEquals(Intent.ACTION_VIEW, intent.getAction());
+        assertTrue(intent.hasExtra(CustomTabsIntent.EXTRA_SESSION));
+        assertNull(intent.getExtras().getBinder(CustomTabsIntent.EXTRA_SESSION));
+        assertNull(intent.getComponent());
+    }
+
+    public void testToolbarColor() {
+        int color = Color.RED;
+        Intent intent = new CustomTabsIntent.Builder().setToolbarColor(color).build().intent;
+        assertTrue(intent.hasExtra(CustomTabsIntent.EXTRA_TOOLBAR_COLOR));
+        assertEquals(color, intent.getIntExtra(CustomTabsIntent.EXTRA_TOOLBAR_COLOR, 0));
+    }
+
+    public void testToolbarColorIsNotAResource() {
+        int colorId = android.R.color.background_dark;
+        int color = getContext().getResources().getColor(colorId);
+        Intent intent = new CustomTabsIntent.Builder().setToolbarColor(colorId).build().intent;
+        assertFalse("The color should not be a resource ID",
+                color == intent.getIntExtra(CustomTabsIntent.EXTRA_TOOLBAR_COLOR, 0));
+        intent = new CustomTabsIntent.Builder().setToolbarColor(color).build().intent;
+        assertEquals(color, intent.getIntExtra(CustomTabsIntent.EXTRA_TOOLBAR_COLOR, 0));
+    }
+}
diff --git a/settings.gradle b/settings.gradle
index 85c9ae1..6efb120 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -42,3 +42,6 @@
 
 include ':support-percent'
 project(':support-percent').projectDir = new File(rootDir, 'percent')
+
+include ':support-customtabs'
+project(':support-customtabs').projectDir = new File(rootDir, 'customtabs')
diff --git a/v4/java/android/support/v4/app/FragmentActivity.java b/v4/java/android/support/v4/app/FragmentActivity.java
index 7809f80..1dd6d01 100644
--- a/v4/java/android/support/v4/app/FragmentActivity.java
+++ b/v4/java/android/support/v4/app/FragmentActivity.java
@@ -382,6 +382,13 @@
     }
 
     /**
+     * Hook in to note that fragment state is no longer saved.
+     */
+    public void onStateNotSaved() {
+        mFragments.noteStateNotSaved();
+    }
+
+    /**
      * Dispatch onResume() to fragments.  Note that for better inter-operation
      * with older versions of the platform, at the point of this call the
      * fragments attached to the activity are <em>not</em> resumed.  This means