am c3eff933: am 70a395c4: (-s ours) am 35f73b6f: am 086adab4: am ed56df6f: am 91df3bc9: am a4bb0b7c: am d5abe7ea: am ccd11ec5: DO NOT MERGE Increase the system-image revision number after opengl bug fix

* commit 'c3eff9333f80fc099308bf5376177e228c9daede':
diff --git a/samples/AccelerometerPlay/_index.jd b/samples/AccelerometerPlay/_index.jd
new file mode 100644
index 0000000..cdca3a6
--- /dev/null
+++ b/samples/AccelerometerPlay/_index.jd
@@ -0,0 +1,6 @@
+page.tags="Sensor", "Games", "Accelerometer"
+sample.group=Sensors
+@jd:body
+
+<p>Example code that shows how to use the device accelerometer in apps and games.</p>
+
diff --git a/samples/ActionBarCompat/_index.html b/samples/ActionBarCompat/_index.html
deleted file mode 100644
index 8808de1..0000000
--- a/samples/ActionBarCompat/_index.html
+++ /dev/null
@@ -1,25 +0,0 @@
-<!--
-  Copyright 2011 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.
-  -->
-
-<p>This sample shows how to use the action bar design pattern on pre-API 11 devices and the built-in
-<a href="../../../reference/android/app/ActionBar.html">ActionBar</a> on devices supporting API
-11 or greater. The example 'compatible' action bar, instantiated on pre-Android 3.0 devices,
-uses the same <a href="../../../guide/topics/resources/menu-resource.html">menu resource</a>-based
-action item definition mechanism as the new framework API, even supporting the
-<code>android:showAsAction</code> attribute to a limited extent.</p>
-
-<img alt="" src="../images/ActionBarCompat1.png" height="320" />
-<img alt="" src="../images/ActionBarCompat2.png" height="320" />
diff --git a/samples/ActionBarCompat/_index.jd b/samples/ActionBarCompat/_index.jd
new file mode 100644
index 0000000..61b5e5e
--- /dev/null
+++ b/samples/ActionBarCompat/_index.jd
@@ -0,0 +1,13 @@
+page.tags="ActionBar", "UI", "Compatibility", "Support Library"
+sample.group=Input
+@jd:body
+
+<p>This sample shows how to use the action bar design pattern on pre-API 11 devices and the built-in
+<a href="../../../reference/android/app/ActionBar.html">ActionBar</a> on devices supporting API
+11 or greater. The example 'compatible' action bar, instantiated on pre-Android 3.0 devices,
+uses the same <a href="../../../guide/topics/resources/menu-resource.html">menu resource</a>-based
+action item definition mechanism as the new framework API, even supporting the
+<code>android:showAsAction</code> attribute to a limited extent.</p>
+
+<img alt="" src="../images/ActionBarCompat1.png" height="320" />
+<img alt="" src="../images/ActionBarCompat2.png" height="320" />
\ No newline at end of file
diff --git a/samples/BluetoothHDP/_index.html b/samples/BluetoothHDP/_index.jd
old mode 100755
new mode 100644
similarity index 93%
rename from samples/BluetoothHDP/_index.html
rename to samples/BluetoothHDP/_index.jd
index 3fb3bab..526e318
--- a/samples/BluetoothHDP/_index.html
+++ b/samples/BluetoothHDP/_index.jd
@@ -1,3 +1,7 @@
+page.tags="Bluetooth", "HDP", "Health"
+sample.group=Connectivity
+@jd:body
+
 <p>A sample application that demonstrates how to communicate with a Bluetooth Health Device Profile (HDP) device.  This feature is available on Android 4.0 (API level 14) or above platforms. The Android Bluetooth Health API lets you create
 applications that use Bluetooth to communicate with health devices that support
 Bluetooth, such as heart-rate monitors, blood meters, thermometers, scales, and
@@ -9,5 +13,4 @@
 
 <p>The application manages connection with Bluetooth HDP-enabled devices.  Possible device type include blood pressure monitor, glucose meter, thermometer, etc.  Upon connection, the application retrieves raw data sent from the device.  Note that in order to interpret the data, a parser that conforms to the IEEE 11073-xxxxx specifications would be required.</p>
 
-<img alt="" src="../images/BluetoothHDP.png" />
-
+<img alt="" src="../images/BluetoothHDP.png" />
\ No newline at end of file
diff --git a/samples/BluetoothLeGatt/_index.jd b/samples/BluetoothLeGatt/_index.jd
new file mode 100644
index 0000000..110ca4a
--- /dev/null
+++ b/samples/BluetoothLeGatt/_index.jd
@@ -0,0 +1,8 @@
+page.tags="Bluetooth", "LE", "SMART"
+sample.group=Connectivity
+@jd:body
+
+<p>A sample application that demonstrates how to communicate with
+a Bluetooth SMART (Low Energy) device.  This feature is available on
+Android 4.3 (API level 14) or above platforms. </p>
+
diff --git a/samples/ControllerSample/AndroidManifest.xml b/samples/ControllerSample/AndroidManifest.xml
new file mode 100644
index 0000000..49b67d7
--- /dev/null
+++ b/samples/ControllerSample/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.controllersample"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+    <uses-permission android:name="android.permission.VIBRATE" />
+
+    <uses-sdk
+        android:minSdkVersion="9"
+        android:targetSdkVersion="18" />
+
+    <application
+        android:allowBackup="true"
+        android:icon="@drawable/ic_launcher"
+        android:label="@string/app_name"
+        android:theme="@style/AppTheme" >
+        <activity
+            android:name=".GameViewActivity"
+            android:label="@string/app_name" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/samples/ControllerSample/libs/android-support-v4.jar b/samples/ControllerSample/libs/android-support-v4.jar
new file mode 100644
index 0000000..65ebaf8
--- /dev/null
+++ b/samples/ControllerSample/libs/android-support-v4.jar
Binary files differ
diff --git a/samples/ControllerSample/proguard-project.txt b/samples/ControllerSample/proguard-project.txt
new file mode 100644
index 0000000..f2fe155
--- /dev/null
+++ b/samples/ControllerSample/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
diff --git a/samples/ControllerSample/project.properties b/samples/ControllerSample/project.properties
new file mode 100644
index 0000000..ce39f2d
--- /dev/null
+++ b/samples/ControllerSample/project.properties
@@ -0,0 +1,14 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-18
diff --git a/samples/ControllerSample/res/drawable-hdpi/ic_launcher.png b/samples/ControllerSample/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..4f421f9
--- /dev/null
+++ b/samples/ControllerSample/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/samples/ControllerSample/res/drawable-mdpi/ic_launcher.png b/samples/ControllerSample/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..38651fd
--- /dev/null
+++ b/samples/ControllerSample/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/samples/ControllerSample/res/drawable-xhdpi/ic_launcher.png b/samples/ControllerSample/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..c6f6d82
--- /dev/null
+++ b/samples/ControllerSample/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/ControllerSample/res/drawable-xxhdpi/ic_launcher.png b/samples/ControllerSample/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..eef1c76
--- /dev/null
+++ b/samples/ControllerSample/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/ControllerSample/res/layout/game_controller_input.xml b/samples/ControllerSample/res/layout/game_controller_input.xml
new file mode 100644
index 0000000..4e4a735
--- /dev/null
+++ b/samples/ControllerSample/res/layout/game_controller_input.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+
+<!-- Game controller input demo. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+
+    <TextView
+        android:id="@+id/description"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="12dip"
+        android:text="@string/game_controller_input_description" />
+
+    <com.example.controllersample.GameView
+        android:id="@+id/game"
+        android:layout_width="match_parent"
+        android:layout_height="0dip"
+        android:layout_margin="15dip"
+        android:layout_weight="1"
+        android:background="#000000" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/ControllerSample/res/values-v11/styles.xml b/samples/ControllerSample/res/values-v11/styles.xml
new file mode 100644
index 0000000..541752f
--- /dev/null
+++ b/samples/ControllerSample/res/values-v11/styles.xml
@@ -0,0 +1,11 @@
+<resources>
+
+    <!--
+        Base application theme for API 11+. This theme completely replaces
+        AppBaseTheme from res/values/styles.xml on API 11+ devices.
+    -->
+    <style name="AppBaseTheme" parent="android:Theme.Holo.Light">
+        <!-- API 11 theme customizations can go here. -->
+    </style>
+
+</resources>
\ No newline at end of file
diff --git a/samples/ControllerSample/res/values-v14/styles.xml b/samples/ControllerSample/res/values-v14/styles.xml
new file mode 100644
index 0000000..f20e015
--- /dev/null
+++ b/samples/ControllerSample/res/values-v14/styles.xml
@@ -0,0 +1,12 @@
+<resources>
+
+    <!--
+        Base application theme for API 14+. This theme completely replaces
+        AppBaseTheme from BOTH res/values/styles.xml and
+        res/values-v11/styles.xml on API 14+ devices.
+    -->
+    <style name="AppBaseTheme" parent="android:Theme.Holo.Light.DarkActionBar">
+        <!-- API 14 theme customizations can go here. -->
+    </style>
+
+</resources>
\ No newline at end of file
diff --git a/samples/ControllerSample/res/values/strings.xml b/samples/ControllerSample/res/values/strings.xml
new file mode 100644
index 0000000..ba8e7d7
--- /dev/null
+++ b/samples/ControllerSample/res/values/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="app_name">Controller Sample</string>
+    <string name="game_controller_input_description">
+        This activity demonstrates how to process input events received from
+        game controllers.  Please connect your game controller now and try
+        moving the joysticks or pressing buttons.  If it helps, try to imagine
+        that you are a lone space cowboy in hot pursuit of the aliens who kidnapped
+        your favorite llama on their way back to Andromeda&#8230;
+    </string>
+</resources>
\ No newline at end of file
diff --git a/samples/ControllerSample/res/values/styles.xml b/samples/ControllerSample/res/values/styles.xml
new file mode 100644
index 0000000..4a10ca4
--- /dev/null
+++ b/samples/ControllerSample/res/values/styles.xml
@@ -0,0 +1,20 @@
+<resources>
+
+    <!--
+        Base application theme, dependent on API level. This theme is replaced
+        by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
+    -->
+    <style name="AppBaseTheme" parent="android:Theme.Light">
+        <!--
+            Theme customizations available in newer API levels can go in
+            res/values-vXX/styles.xml, while customizations related to
+            backward-compatibility can go here.
+        -->
+    </style>
+
+    <!-- Application theme. -->
+    <style name="AppTheme" parent="AppBaseTheme">
+        <!-- All customizations that are NOT specific to a particular API-level can go here. -->
+    </style>
+
+</resources>
\ No newline at end of file
diff --git a/samples/ControllerSample/src/com/example/controllersample/GameView.java b/samples/ControllerSample/src/com/example/controllersample/GameView.java
new file mode 100644
index 0000000..6481a2a
--- /dev/null
+++ b/samples/ControllerSample/src/com/example/controllersample/GameView.java
@@ -0,0 +1,1159 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.controllersample;
+
+import com.example.inputmanagercompat.InputManagerCompat;
+import com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Path;
+import android.os.Build;
+import android.os.SystemClock;
+import android.os.Vibrator;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+/*
+ * A trivial joystick based physics game to demonstrate joystick handling. If
+ * the game controller has a vibrator, then it is used to provide feedback when
+ * a bullet is fired or the ship crashes into an obstacle. Otherwise, the system
+ * vibrator is used for that purpose.
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
+public class GameView extends View implements InputDeviceListener {
+    private static final int MAX_OBSTACLES = 12;
+
+    private static final int DPAD_STATE_LEFT = 1 << 0;
+    private static final int DPAD_STATE_RIGHT = 1 << 1;
+    private static final int DPAD_STATE_UP = 1 << 2;
+    private static final int DPAD_STATE_DOWN = 1 << 3;
+
+    private final Random mRandom;
+    /*
+     * Each ship is created as an event comes in from a new Joystick device
+     */
+    private final SparseArray<Ship> mShips;
+    private final Map<String, Integer> mDescriptorMap;
+    private final List<Bullet> mBullets;
+    private final List<Obstacle> mObstacles;
+
+    private long mLastStepTime;
+    private final InputManagerCompat mInputManager;
+
+    private final float mBaseSpeed;
+
+    private final float mShipSize;
+
+    private final float mBulletSize;
+
+    private final float mMinObstacleSize;
+    private final float mMaxObstacleSize;
+    private final float mMinObstacleSpeed;
+    private final float mMaxObstacleSpeed;
+
+    public GameView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        mRandom = new Random();
+        mShips = new SparseArray<Ship>();
+        mDescriptorMap = new HashMap<String, Integer>();
+        mBullets = new ArrayList<Bullet>();
+        mObstacles = new ArrayList<Obstacle>();
+
+        setFocusable(true);
+        setFocusableInTouchMode(true);
+
+        float baseSize = getContext().getResources().getDisplayMetrics().density * 5f;
+        mBaseSpeed = baseSize * 3;
+
+        mShipSize = baseSize * 3;
+
+        mBulletSize = baseSize;
+
+        mMinObstacleSize = baseSize * 2;
+        mMaxObstacleSize = baseSize * 12;
+        mMinObstacleSpeed = mBaseSpeed;
+        mMaxObstacleSpeed = mBaseSpeed * 3;
+
+        mInputManager = InputManagerCompat.Factory.getInputManager(this.getContext());
+        mInputManager.registerInputDeviceListener(this, null);
+    }
+
+    // Iterate through the input devices, looking for controllers. Create a ship
+    // for every device that reports itself as a gamepad or joystick.
+    void findControllersAndAttachShips() {
+        int[] deviceIds = mInputManager.getInputDeviceIds();
+        for (int deviceId : deviceIds) {
+            InputDevice dev = mInputManager.getInputDevice(deviceId);
+            int sources = dev.getSources();
+            // if the device is a gamepad/joystick, create a ship to represent it
+            if (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
+                    ((sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK)) {
+                // if the device has a gamepad or joystick
+                getShipForId(deviceId);
+            }
+        }
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        int deviceId = event.getDeviceId();
+        if (deviceId != -1) {
+            Ship currentShip = getShipForId(deviceId);
+            if (currentShip.onKeyDown(keyCode, event)) {
+                step(event.getEventTime());
+                return true;
+            }
+        }
+
+        return super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        int deviceId = event.getDeviceId();
+        if (deviceId != -1) {
+            Ship currentShip = getShipForId(deviceId);
+            if (currentShip.onKeyUp(keyCode, event)) {
+                step(event.getEventTime());
+                return true;
+            }
+        }
+
+        return super.onKeyUp(keyCode, event);
+    }
+
+    @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        mInputManager.onGenericMotionEvent(event);
+
+        // Check that the event came from a joystick or gamepad since a generic
+        // motion event could be almost anything. API level 18 adds the useful
+        // event.isFromSource() helper function.
+        int eventSource = event.getSource();
+        if ((((eventSource & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
+                ((eventSource & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK))
+                && event.getAction() == MotionEvent.ACTION_MOVE) {
+            int id = event.getDeviceId();
+            if (-1 != id) {
+                Ship curShip = getShipForId(id);
+                if (curShip.onGenericMotionEvent(event)) {
+                    return true;
+                }
+            }
+        }
+        return super.onGenericMotionEvent(event);
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        // Turn on and off animations based on the window focus.
+        // Alternately, we could update the game state using the Activity
+        // onResume()
+        // and onPause() lifecycle events.
+        if (hasWindowFocus) {
+            mLastStepTime = SystemClock.uptimeMillis();
+            mInputManager.onResume();
+        } else {
+            int numShips = mShips.size();
+            for (int i = 0; i < numShips; i++) {
+                Ship currentShip = mShips.valueAt(i);
+                if (currentShip != null) {
+                    currentShip.setHeading(0, 0);
+                    currentShip.setVelocity(0, 0);
+                    currentShip.mDPadState = 0;
+                }
+            }
+            mInputManager.onPause();
+        }
+
+        super.onWindowFocusChanged(hasWindowFocus);
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+
+        // Reset the game when the view changes size.
+        reset();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        // Update the animation
+        animateFrame();
+
+        // Draw the ships.
+        int numShips = mShips.size();
+        for (int i = 0; i < numShips; i++) {
+            Ship currentShip = mShips.valueAt(i);
+            if (currentShip != null) {
+                currentShip.draw(canvas);
+            }
+        }
+
+        // Draw bullets.
+        int numBullets = mBullets.size();
+        for (int i = 0; i < numBullets; i++) {
+            final Bullet bullet = mBullets.get(i);
+            bullet.draw(canvas);
+        }
+
+        // Draw obstacles.
+        int numObstacles = mObstacles.size();
+        for (int i = 0; i < numObstacles; i++) {
+            final Obstacle obstacle = mObstacles.get(i);
+            obstacle.draw(canvas);
+        }
+    }
+
+    /**
+     * Uses the device descriptor to try to assign the same color to the same
+     * joystick. If there are two joysticks of the same type connected over USB,
+     * or the API is < API level 16, it will be unable to distinguish the two
+     * devices.
+     *
+     * @param shipID
+     * @return
+     */
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+    private Ship getShipForId(int shipID) {
+        Ship currentShip = mShips.get(shipID);
+        if (null == currentShip) {
+
+            // do we know something about this ship already?
+            InputDevice dev = InputDevice.getDevice(shipID);
+            String deviceString = null;
+            Integer shipColor = null;
+            if (null != dev) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+                    deviceString = dev.getDescriptor();
+                } else {
+                    deviceString = dev.getName();
+                }
+                shipColor = mDescriptorMap.get(deviceString);
+            }
+
+            if (null != shipColor) {
+                int color = shipColor;
+                int numShips = mShips.size();
+                // do we already have a ship with this color?
+                for (int i = 0; i < numShips; i++) {
+                    if (mShips.valueAt(i).getColor() == color) {
+                        shipColor = null;
+                        // we won't store this value either --- if the first
+                        // controller gets disconnected/connected, it will get
+                        // the same color.
+                        deviceString = null;
+                    }
+                }
+            }
+            if (null != shipColor) {
+                currentShip = new Ship(shipColor);
+                if (null != deviceString) {
+                    mDescriptorMap.remove(deviceString);
+                }
+            } else {
+                currentShip = new Ship(getNextShipColor());
+            }
+            mShips.append(shipID, currentShip);
+            currentShip.setInputDevice(dev);
+
+            if (null != deviceString) {
+                mDescriptorMap.put(deviceString, currentShip.getColor());
+            }
+        }
+        return currentShip;
+    }
+
+    /**
+     * Remove the ship from the array of active ships by ID.
+     *
+     * @param shipID
+     */
+    private void removeShipForID(int shipID) {
+        mShips.remove(shipID);
+    }
+
+    private void reset() {
+        mShips.clear();
+        mBullets.clear();
+        mObstacles.clear();
+        findControllersAndAttachShips();
+    }
+
+    private void animateFrame() {
+        long currentStepTime = SystemClock.uptimeMillis();
+        step(currentStepTime);
+        invalidate();
+    }
+
+    private void step(long currentStepTime) {
+        float tau = (currentStepTime - mLastStepTime) * 0.001f;
+        mLastStepTime = currentStepTime;
+
+        // Move the ships
+        int numShips = mShips.size();
+        for (int i = 0; i < numShips; i++) {
+            Ship currentShip = mShips.valueAt(i);
+            if (currentShip != null) {
+                currentShip.accelerate(tau);
+                if (!currentShip.step(tau)) {
+                    currentShip.reincarnate();
+                }
+            }
+        }
+
+        // Move the bullets.
+        int numBullets = mBullets.size();
+        for (int i = 0; i < numBullets; i++) {
+            final Bullet bullet = mBullets.get(i);
+            if (!bullet.step(tau)) {
+                mBullets.remove(i);
+                i -= 1;
+                numBullets -= 1;
+            }
+        }
+
+        // Move obstacles.
+        int numObstacles = mObstacles.size();
+        for (int i = 0; i < numObstacles; i++) {
+            final Obstacle obstacle = mObstacles.get(i);
+            if (!obstacle.step(tau)) {
+                mObstacles.remove(i);
+                i -= 1;
+                numObstacles -= 1;
+            }
+        }
+
+        // Check for collisions between bullets and obstacles.
+        for (int i = 0; i < numBullets; i++) {
+            final Bullet bullet = mBullets.get(i);
+            for (int j = 0; j < numObstacles; j++) {
+                final Obstacle obstacle = mObstacles.get(j);
+                if (bullet.collidesWith(obstacle)) {
+                    bullet.destroy();
+                    obstacle.destroy();
+                    break;
+                }
+            }
+        }
+
+        // Check for collisions between the ship and obstacles --- this could
+        // get slow
+        for (int i = 0; i < numObstacles; i++) {
+            final Obstacle obstacle = mObstacles.get(i);
+            for (int j = 0; j < numShips; j++) {
+                Ship currentShip = mShips.valueAt(j);
+                if (currentShip != null) {
+                    if (currentShip.collidesWith(obstacle)) {
+                        currentShip.destroy();
+                        obstacle.destroy();
+                        break;
+                    }
+                }
+            }
+        }
+
+        // Spawn more obstacles offscreen when needed.
+        // Avoid putting them right on top of the ship.
+        int tries = MAX_OBSTACLES - mObstacles.size() + 10;
+        final float minDistance = mShipSize * 4;
+        while (mObstacles.size() < MAX_OBSTACLES && tries-- > 0) {
+            float size = mRandom.nextFloat() * (mMaxObstacleSize - mMinObstacleSize)
+                    + mMinObstacleSize;
+            float positionX, positionY;
+            int edge = mRandom.nextInt(4);
+            switch (edge) {
+                case 0:
+                    positionX = -size;
+                    positionY = mRandom.nextInt(getHeight());
+                    break;
+                case 1:
+                    positionX = getWidth() + size;
+                    positionY = mRandom.nextInt(getHeight());
+                    break;
+                case 2:
+                    positionX = mRandom.nextInt(getWidth());
+                    positionY = -size;
+                    break;
+                default:
+                    positionX = mRandom.nextInt(getWidth());
+                    positionY = getHeight() + size;
+                    break;
+            }
+            boolean positionSafe = true;
+
+            // If the obstacle is too close to any ships, we don't want to
+            // spawn it.
+            for (int i = 0; i < numShips; i++) {
+                Ship currentShip = mShips.valueAt(i);
+                if (currentShip != null) {
+                    if (currentShip.distanceTo(positionX, positionY) < minDistance) {
+                        // try to spawn again
+                        positionSafe = false;
+                        break;
+                    }
+                }
+            }
+
+            // if the position is safe, add the obstacle and reset the retry
+            // counter
+            if (positionSafe) {
+                tries = MAX_OBSTACLES - mObstacles.size() + 10;
+                // we can add the obstacle now since it isn't close to any ships
+                float direction = mRandom.nextFloat() * (float) Math.PI * 2;
+                float speed = mRandom.nextFloat() * (mMaxObstacleSpeed - mMinObstacleSpeed)
+                        + mMinObstacleSpeed;
+                float velocityX = (float) Math.cos(direction) * speed;
+                float velocityY = (float) Math.sin(direction) * speed;
+
+                Obstacle obstacle = new Obstacle();
+                obstacle.setPosition(positionX, positionY);
+                obstacle.setSize(size);
+                obstacle.setVelocity(velocityX, velocityY);
+                mObstacles.add(obstacle);
+            }
+        }
+    }
+
+    private static float pythag(float x, float y) {
+        return (float) Math.sqrt(x * x + y * y);
+    }
+
+    private static int blend(float alpha, int from, int to) {
+        return from + (int) ((to - from) * alpha);
+    }
+
+    private static void setPaintARGBBlend(Paint paint, float alpha,
+            int a1, int r1, int g1, int b1,
+            int a2, int r2, int g2, int b2) {
+        paint.setARGB(blend(alpha, a1, a2), blend(alpha, r1, r2),
+                blend(alpha, g1, g2), blend(alpha, b1, b2));
+    }
+
+    private static float getCenteredAxis(MotionEvent event, InputDevice device,
+            int axis, int historyPos) {
+        final InputDevice.MotionRange range = device.getMotionRange(axis, event.getSource());
+        if (range != null) {
+            final float flat = range.getFlat();
+            final float value = historyPos < 0 ? event.getAxisValue(axis)
+                    : event.getHistoricalAxisValue(axis, historyPos);
+
+            // Ignore axis values that are within the 'flat' region of the
+            // joystick axis center.
+            // A joystick at rest does not always report an absolute position of
+            // (0,0).
+            if (Math.abs(value) > flat) {
+                return value;
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * Any gamepad button + the spacebar or DPAD_CENTER will be used as the fire
+     * key.
+     *
+     * @param keyCode
+     * @return true of it's a fire key.
+     */
+    private static boolean isFireKey(int keyCode) {
+        return KeyEvent.isGamepadButton(keyCode)
+                || keyCode == KeyEvent.KEYCODE_DPAD_CENTER
+                || keyCode == KeyEvent.KEYCODE_SPACE;
+    }
+
+    private abstract class Sprite {
+        protected float mPositionX;
+        protected float mPositionY;
+        protected float mVelocityX;
+        protected float mVelocityY;
+        protected float mSize;
+        protected boolean mDestroyed;
+        protected float mDestroyAnimProgress;
+
+        public void setPosition(float x, float y) {
+            mPositionX = x;
+            mPositionY = y;
+        }
+
+        public void setVelocity(float x, float y) {
+            mVelocityX = x;
+            mVelocityY = y;
+        }
+
+        public void setSize(float size) {
+            mSize = size;
+        }
+
+        public float distanceTo(float x, float y) {
+            return pythag(mPositionX - x, mPositionY - y);
+        }
+
+        public float distanceTo(Sprite other) {
+            return distanceTo(other.mPositionX, other.mPositionY);
+        }
+
+        public boolean collidesWith(Sprite other) {
+            // Really bad collision detection.
+            return !mDestroyed && !other.mDestroyed
+                    && distanceTo(other) <= Math.max(mSize, other.mSize)
+                            + Math.min(mSize, other.mSize) * 0.5f;
+        }
+
+        public boolean isDestroyed() {
+            return mDestroyed;
+        }
+
+        /**
+         * Moves the sprite based on the elapsed time defined by tau.
+         *
+         * @param tau the elapsed time in seconds since the last step
+         * @return false if the sprite is to be removed from the display
+         */
+        public boolean step(float tau) {
+            mPositionX += mVelocityX * tau;
+            mPositionY += mVelocityY * tau;
+
+            if (mDestroyed) {
+                mDestroyAnimProgress += tau / getDestroyAnimDuration();
+                if (mDestroyAnimProgress >= getDestroyAnimCycles()) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        /**
+         * Draws the sprite.
+         *
+         * @param canvas the Canvas upon which to draw the sprite.
+         */
+        public abstract void draw(Canvas canvas);
+
+        /**
+         * Returns the duration of the destruction animation of the sprite in
+         * seconds.
+         *
+         * @return the float duration in seconds of the destruction animation
+         */
+        public abstract float getDestroyAnimDuration();
+
+        /**
+         * Returns the number of cycles to play the destruction animation. A
+         * destruction animation has a duration and a number of cycles to play
+         * it for, so we can have an extended death sequence when a ship or
+         * object is destroyed.
+         *
+         * @return the float number of cycles to play the destruction animation
+         */
+        public abstract float getDestroyAnimCycles();
+
+        protected boolean isOutsidePlayfield() {
+            final int width = GameView.this.getWidth();
+            final int height = GameView.this.getHeight();
+            return mPositionX < 0 || mPositionX >= width
+                    || mPositionY < 0 || mPositionY >= height;
+        }
+
+        protected void wrapAtPlayfieldBoundary() {
+            final int width = GameView.this.getWidth();
+            final int height = GameView.this.getHeight();
+            while (mPositionX <= -mSize) {
+                mPositionX += width + mSize * 2;
+            }
+            while (mPositionX >= width + mSize) {
+                mPositionX -= width + mSize * 2;
+            }
+            while (mPositionY <= -mSize) {
+                mPositionY += height + mSize * 2;
+            }
+            while (mPositionY >= height + mSize) {
+                mPositionY -= height + mSize * 2;
+            }
+        }
+
+        public void destroy() {
+            mDestroyed = true;
+            step(0);
+        }
+    }
+
+    private static int sShipColor = 0;
+
+    /**
+     * Returns the next ship color in the sequence. Very simple. Does not in any
+     * way guarantee that there are not multiple ships with the same color on
+     * the screen.
+     *
+     * @return an int containing the index of the next ship color
+     */
+    private static int getNextShipColor() {
+        int color = sShipColor & 0x07;
+        if (0 == color) {
+            color++;
+            sShipColor++;
+        }
+        sShipColor++;
+        return color;
+    }
+
+    /*
+     * Static constants associated with Ship inner class
+     */
+    private static final long[] sDestructionVibratePattern = new long[] {
+            0, 20, 20, 40, 40, 80, 40, 300
+    };
+
+    private class Ship extends Sprite {
+        private static final float CORNER_ANGLE = (float) Math.PI * 2 / 3;
+        private static final float TO_DEGREES = (float) (180.0 / Math.PI);
+
+        private final float mMaxShipThrust = mBaseSpeed * 0.25f;
+        private final float mMaxSpeed = mBaseSpeed * 12;
+
+        // The ship actually determines the speed of the bullet, not the bullet
+        // itself
+        private final float mBulletSpeed = mBaseSpeed * 12;
+
+        private final Paint mPaint;
+        private final Path mPath;
+        private final int mR, mG, mB;
+        private final int mColor;
+
+        // The current device that is controlling the ship
+        private InputDevice mInputDevice;
+
+        private float mHeadingX;
+        private float mHeadingY;
+        private float mHeadingAngle;
+        private float mHeadingMagnitude;
+
+        private int mDPadState;
+
+        /**
+         * The colorIndex is used to create the color based on the lower three
+         * bits of the value in the current implementation.
+         *
+         * @param colorIndex
+         */
+        public Ship(int colorIndex) {
+            mPaint = new Paint();
+            mPaint.setStyle(Style.FILL);
+
+            setPosition(getWidth() * 0.5f, getHeight() * 0.5f);
+            setVelocity(0, 0);
+            setSize(mShipSize);
+
+            mPath = new Path();
+            mPath.moveTo(0, 0);
+            mPath.lineTo((float) Math.cos(-CORNER_ANGLE) * mSize,
+                    (float) Math.sin(-CORNER_ANGLE) * mSize);
+            mPath.lineTo(mSize, 0);
+            mPath.lineTo((float) Math.cos(CORNER_ANGLE) * mSize,
+                    (float) Math.sin(CORNER_ANGLE) * mSize);
+            mPath.lineTo(0, 0);
+
+            mR = (colorIndex & 0x01) == 0 ? 63 : 255;
+            mG = (colorIndex & 0x02) == 0 ? 63 : 255;
+            mB = (colorIndex & 0x04) == 0 ? 63 : 255;
+
+            mColor = colorIndex;
+        }
+
+        public boolean onKeyUp(int keyCode, KeyEvent event) {
+
+            // Handle keys going up.
+            boolean handled = false;
+            switch (keyCode) {
+                case KeyEvent.KEYCODE_DPAD_LEFT:
+                    setHeadingX(0);
+                    mDPadState &= ~DPAD_STATE_LEFT;
+                    handled = true;
+                    break;
+                case KeyEvent.KEYCODE_DPAD_RIGHT:
+                    setHeadingX(0);
+                    mDPadState &= ~DPAD_STATE_RIGHT;
+                    handled = true;
+                    break;
+                case KeyEvent.KEYCODE_DPAD_UP:
+                    setHeadingY(0);
+                    mDPadState &= ~DPAD_STATE_UP;
+                    handled = true;
+                    break;
+                case KeyEvent.KEYCODE_DPAD_DOWN:
+                    setHeadingY(0);
+                    mDPadState &= ~DPAD_STATE_DOWN;
+                    handled = true;
+                    break;
+                default:
+                    if (isFireKey(keyCode)) {
+                        handled = true;
+                    }
+                    break;
+            }
+            return handled;
+        }
+
+        /*
+         * Firing is a unique case where a ship creates a bullet. A bullet needs
+         * to be created with a position near the ship that is firing with a
+         * velocity that is based upon the speed of the ship.
+         */
+        private void fire() {
+            if (!isDestroyed()) {
+                Bullet bullet = new Bullet();
+                bullet.setPosition(getBulletInitialX(), getBulletInitialY());
+                bullet.setVelocity(getBulletVelocityX(),
+                        getBulletVelocityY());
+                mBullets.add(bullet);
+                vibrateController(20);
+            }
+        }
+
+        public boolean onKeyDown(int keyCode, KeyEvent event) {
+            // Handle DPad keys and fire button on initial down but not on
+            // auto-repeat.
+            boolean handled = false;
+            if (event.getRepeatCount() == 0) {
+                switch (keyCode) {
+                    case KeyEvent.KEYCODE_DPAD_LEFT:
+                        setHeadingX(-1);
+                        mDPadState |= DPAD_STATE_LEFT;
+                        handled = true;
+                        break;
+                    case KeyEvent.KEYCODE_DPAD_RIGHT:
+                        setHeadingX(1);
+                        mDPadState |= DPAD_STATE_RIGHT;
+                        handled = true;
+                        break;
+                    case KeyEvent.KEYCODE_DPAD_UP:
+                        setHeadingY(-1);
+                        mDPadState |= DPAD_STATE_UP;
+                        handled = true;
+                        break;
+                    case KeyEvent.KEYCODE_DPAD_DOWN:
+                        setHeadingY(1);
+                        mDPadState |= DPAD_STATE_DOWN;
+                        handled = true;
+                        break;
+                    default:
+                        if (isFireKey(keyCode)) {
+                            fire();
+                            handled = true;
+                        }
+                        break;
+                }
+            }
+            return handled;
+        }
+
+        /**
+         * Gets the vibrator from the controller if it is present. Note that it
+         * would be easy to get the system vibrator here if the controller one
+         * is not present, but we don't choose to do it in this case.
+         *
+         * @return the Vibrator for the controller, or null if it is not
+         *         present. or the API level cannot support it
+         */
+        @SuppressLint("NewApi")
+        private final Vibrator getVibrator() {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
+                    null != mInputDevice) {
+                return mInputDevice.getVibrator();
+            }
+            return null;
+        }
+
+        private void vibrateController(int time) {
+            Vibrator vibrator = getVibrator();
+            if (null != vibrator) {
+                vibrator.vibrate(time);
+            }
+        }
+
+        private void vibrateController(long[] pattern, int repeat) {
+            Vibrator vibrator = getVibrator();
+            if (null != vibrator) {
+                vibrator.vibrate(pattern, repeat);
+            }
+        }
+
+        /**
+         * The ship directly handles joystick input.
+         *
+         * @param event
+         * @param historyPos
+         */
+        private void processJoystickInput(MotionEvent event, int historyPos) {
+            // Get joystick position.
+            // Many game pads with two joysticks report the position of the
+            // second
+            // joystick
+            // using the Z and RZ axes so we also handle those.
+            // In a real game, we would allow the user to configure the axes
+            // manually.
+            if (null == mInputDevice) {
+                mInputDevice = event.getDevice();
+            }
+            float x = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_X, historyPos);
+            if (x == 0) {
+                x = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_HAT_X, historyPos);
+            }
+            if (x == 0) {
+                x = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_Z, historyPos);
+            }
+
+            float y = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_Y, historyPos);
+            if (y == 0) {
+                y = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_HAT_Y, historyPos);
+            }
+            if (y == 0) {
+                y = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_RZ, historyPos);
+            }
+
+            // Set the ship heading.
+            setHeading(x, y);
+            GameView.this.step(historyPos < 0 ? event.getEventTime() : event
+                    .getHistoricalEventTime(historyPos));
+        }
+
+        public boolean onGenericMotionEvent(MotionEvent event) {
+            if (0 == mDPadState) {
+                // Process all historical movement samples in the batch.
+                final int historySize = event.getHistorySize();
+                for (int i = 0; i < historySize; i++) {
+                    processJoystickInput(event, i);
+                }
+
+                // Process the current movement sample in the batch.
+                processJoystickInput(event, -1);
+            }
+            return true;
+        }
+
+        /**
+         * Set the game controller to be used to control the ship.
+         *
+         * @param dev the input device that will be controlling the ship
+         */
+        public void setInputDevice(InputDevice dev) {
+            mInputDevice = dev;
+        }
+
+        /**
+         * Sets the X component of the joystick heading value, defined by the
+         * platform as being from -1.0 (left) to 1.0 (right). This function is
+         * generally used to change the heading in response to a button-style
+         * DPAD event.
+         *
+         * @param x the float x component of the joystick heading value
+         */
+        public void setHeadingX(float x) {
+            mHeadingX = x;
+            updateHeading();
+        }
+
+        /**
+         * Sets the Y component of the joystick heading value, defined by the
+         * platform as being from -1.0 (top) to 1.0 (bottom). This function is
+         * generally used to change the heading in response to a button-style
+         * DPAD event.
+         *
+         * @param y the float y component of the joystick heading value
+         */
+        public void setHeadingY(float y) {
+            mHeadingY = y;
+            updateHeading();
+        }
+
+        /**
+         * Sets the heading as floating point values returned by a joystick.
+         * These values are normalized by the Android platform to be from -1.0
+         * (left, top) to 1.0 (right, bottom)
+         *
+         * @param x the float x component of the joystick heading value
+         * @param y the float y component of the joystick heading value
+         */
+        public void setHeading(float x, float y) {
+            mHeadingX = x;
+            mHeadingY = y;
+            updateHeading();
+        }
+
+        /**
+         * Converts the heading values from joystick devices to the polar
+         * representation of the heading angle if the magnitude of the heading
+         * is significant (> 0.1f).
+         */
+        private void updateHeading() {
+            mHeadingMagnitude = pythag(mHeadingX, mHeadingY);
+            if (mHeadingMagnitude > 0.1f) {
+                mHeadingAngle = (float) Math.atan2(mHeadingY, mHeadingX);
+            }
+        }
+
+        /**
+         * Bring our ship back to life, stopping the destroy animation.
+         */
+        public void reincarnate() {
+            mDestroyed = false;
+            mDestroyAnimProgress = 0.0f;
+        }
+
+        private float polarX(float radius) {
+            return (float) Math.cos(mHeadingAngle) * radius;
+        }
+
+        private float polarY(float radius) {
+            return (float) Math.sin(mHeadingAngle) * radius;
+        }
+
+        /**
+         * Gets the initial x coordinate for the bullet.
+         *
+         * @return the x coordinate of the bullet adjusted for the position and
+         *         direction of the ship
+         */
+        public float getBulletInitialX() {
+            return mPositionX + polarX(mSize);
+        }
+
+        /**
+         * Gets the initial y coordinate for the bullet.
+         *
+         * @return the y coordinate of the bullet adjusted for the position and
+         *         direction of the ship
+         */
+        public float getBulletInitialY() {
+            return mPositionY + polarY(mSize);
+        }
+
+        /**
+         * Returns the bullet speed Y component.
+         *
+         * @return adjusted Y component bullet speed for the velocity and
+         *         direction of the ship
+         */
+        public float getBulletVelocityY() {
+            return mVelocityY + polarY(mBulletSpeed);
+        }
+
+        /**
+         * Returns the bullet speed X component
+         *
+         * @return adjusted X component bullet speed for the velocity and
+         *         direction of the ship
+         */
+        public float getBulletVelocityX() {
+            return mVelocityX + polarX(mBulletSpeed);
+        }
+
+        /**
+         * Uses the heading magnitude and direction to change the acceleration
+         * of the ship. In theory, this should be scaled according to the
+         * elapsed time.
+         *
+         * @param tau the elapsed time in seconds between the last step
+         */
+        public void accelerate(float tau) {
+            final float thrust = mHeadingMagnitude * mMaxShipThrust;
+            mVelocityX += polarX(thrust) * tau * mMaxSpeed / 4;
+            mVelocityY += polarY(thrust) * tau * mMaxSpeed / 4;
+
+            final float speed = pythag(mVelocityX, mVelocityY);
+            if (speed > mMaxSpeed) {
+                final float scale = mMaxSpeed / speed;
+                mVelocityX = mVelocityX * scale * scale;
+                mVelocityY = mVelocityY * scale * scale;
+            }
+        }
+
+        @Override
+        public boolean step(float tau) {
+            if (!super.step(tau)) {
+                return false;
+            }
+            wrapAtPlayfieldBoundary();
+            return true;
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            setPaintARGBBlend(mPaint, mDestroyAnimProgress - (int) (mDestroyAnimProgress),
+                    255, mR, mG, mB,
+                    0, 255, 0, 0);
+
+            canvas.save(Canvas.MATRIX_SAVE_FLAG);
+            canvas.translate(mPositionX, mPositionY);
+            canvas.rotate(mHeadingAngle * TO_DEGREES);
+            canvas.drawPath(mPath, mPaint);
+            canvas.restore();
+        }
+
+        @Override
+        public float getDestroyAnimDuration() {
+            return 1.0f;
+        }
+
+        @Override
+        public void destroy() {
+            super.destroy();
+            vibrateController(sDestructionVibratePattern, -1);
+        }
+
+        @Override
+        public float getDestroyAnimCycles() {
+            return 5.0f;
+        }
+
+        public int getColor() {
+            return mColor;
+        }
+    }
+
+    private static final Paint mBulletPaint;
+    static {
+        mBulletPaint = new Paint();
+        mBulletPaint.setStyle(Style.FILL);
+    }
+
+    private class Bullet extends Sprite {
+
+        public Bullet() {
+            setSize(mBulletSize);
+        }
+
+        @Override
+        public boolean step(float tau) {
+            if (!super.step(tau)) {
+                return false;
+            }
+            return !isOutsidePlayfield();
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            setPaintARGBBlend(mBulletPaint, mDestroyAnimProgress,
+                    255, 255, 255, 0,
+                    0, 255, 255, 255);
+            canvas.drawCircle(mPositionX, mPositionY, mSize, mBulletPaint);
+        }
+
+        @Override
+        public float getDestroyAnimDuration() {
+            return 0.125f;
+        }
+
+        @Override
+        public float getDestroyAnimCycles() {
+            return 1.0f;
+        }
+
+    }
+
+    private static final Paint mObstaclePaint;
+    static {
+        mObstaclePaint = new Paint();
+        mObstaclePaint.setARGB(255, 127, 127, 255);
+        mObstaclePaint.setStyle(Style.FILL);
+    }
+
+    private class Obstacle extends Sprite {
+
+        @Override
+        public boolean step(float tau) {
+            if (!super.step(tau)) {
+                return false;
+            }
+            wrapAtPlayfieldBoundary();
+            return true;
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            setPaintARGBBlend(mObstaclePaint, mDestroyAnimProgress,
+                    255, 127, 127, 255,
+                    0, 255, 0, 0);
+            canvas.drawCircle(mPositionX, mPositionY,
+                    mSize * (1.0f - mDestroyAnimProgress), mObstaclePaint);
+        }
+
+        @Override
+        public float getDestroyAnimDuration() {
+            return 0.25f;
+        }
+
+        @Override
+        public float getDestroyAnimCycles() {
+            return 1.0f;
+        }
+    }
+
+    /*
+     * When an input device is added, we add a ship based upon the device.
+     * @see
+     * com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener
+     * #onInputDeviceAdded(int)
+     */
+    @Override
+    public void onInputDeviceAdded(int deviceId) {
+        getShipForId(deviceId);
+    }
+
+    /*
+     * This is an unusual case. Input devices don't typically change, but they
+     * certainly can --- for example a device may have different modes. We use
+     * this to make sure that the ship has an up-to-date InputDevice.
+     * @see
+     * com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener
+     * #onInputDeviceChanged(int)
+     */
+    @Override
+    public void onInputDeviceChanged(int deviceId) {
+        Ship ship = getShipForId(deviceId);
+        ship.setInputDevice(InputDevice.getDevice(deviceId));
+    }
+
+    /*
+     * Remove any ship associated with the ID.
+     * @see
+     * com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener
+     * #onInputDeviceRemoved(int)
+     */
+    @Override
+    public void onInputDeviceRemoved(int deviceId) {
+        removeShipForID(deviceId);
+    }
+}
diff --git a/samples/ControllerSample/src/com/example/controllersample/GameViewActivity.java b/samples/ControllerSample/src/com/example/controllersample/GameViewActivity.java
new file mode 100644
index 0000000..aaf8bae
--- /dev/null
+++ b/samples/ControllerSample/src/com/example/controllersample/GameViewActivity.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.controllersample;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class GameViewActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        this.setContentView(R.layout.game_controller_input);
+    }
+
+}
diff --git a/samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerCompat.java b/samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerCompat.java
new file mode 100644
index 0000000..fabc876
--- /dev/null
+++ b/samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerCompat.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.inputmanagercompat;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+
+public interface InputManagerCompat {
+    /**
+     * Gets information about the input device with the specified id.
+     *
+     * @param id The device id
+     * @return The input device or null if not found
+     */
+    public InputDevice getInputDevice(int id);
+
+    /**
+     * Gets the ids of all input devices in the system.
+     *
+     * @return The input device ids.
+     */
+    public int[] getInputDeviceIds();
+
+    /**
+     * Registers an input device listener to receive notifications about when
+     * input devices are added, removed or changed.
+     *
+     * @param listener The listener to register.
+     * @param handler The handler on which the listener should be invoked, or
+     *            null if the listener should be invoked on the calling thread's
+     *            looper.
+     */
+    public void registerInputDeviceListener(InputManagerCompat.InputDeviceListener listener,
+            Handler handler);
+
+    /**
+     * Unregisters an input device listener.
+     *
+     * @param listener The listener to unregister.
+     */
+    public void unregisterInputDeviceListener(InputManagerCompat.InputDeviceListener listener);
+
+    /*
+     * The following three calls are to simulate V16 behavior on pre-Jellybean
+     * devices. If you don't call them, your callback will never be called
+     * pre-API 16.
+     */
+
+    /**
+     * Pass the motion events to the InputManagerCompat. This is used to
+     * optimize for polling for controllers. If you do not pass these events in,
+     * polling will cause regular object creation.
+     *
+     * @param event the motion event from the app
+     */
+    public void onGenericMotionEvent(MotionEvent event);
+
+    /**
+     * Tell the V9 input manager that it should stop polling for disconnected
+     * devices. You can call this during onPause in your activity, although you
+     * might want to call it whenever your game is not active (or whenever you
+     * don't care about being notified of new input devices)
+     */
+    public void onPause();
+
+    /**
+     * Tell the V9 input manager that it should start polling for disconnected
+     * devices. You can call this during onResume in your activity, although you
+     * might want to call it less often (only when the gameplay is actually
+     * active)
+     */
+    public void onResume();
+
+    public interface InputDeviceListener {
+        /**
+         * Called whenever the input manager detects that a device has been
+         * added. This will only be called in the V9 version when a motion event
+         * is detected.
+         *
+         * @param deviceId The id of the input device that was added.
+         */
+        void onInputDeviceAdded(int deviceId);
+
+        /**
+         * Called whenever the properties of an input device have changed since
+         * they were last queried. This will not be called for the V9 version of
+         * the API.
+         *
+         * @param deviceId The id of the input device that changed.
+         */
+        void onInputDeviceChanged(int deviceId);
+
+        /**
+         * Called whenever the input manager detects that a device has been
+         * removed. For the V9 version, this can take some time depending on the
+         * poll rate.
+         *
+         * @param deviceId The id of the input device that was removed.
+         */
+        void onInputDeviceRemoved(int deviceId);
+    }
+
+    /**
+     * Use this to construct a compatible InputManager.
+     */
+    public static class Factory {
+
+        /**
+         * Constructs and returns a compatible InputManger
+         *
+         * @param context the Context that will be used to get the system
+         *            service from
+         * @return a compatible implementation of InputManager
+         */
+        public static InputManagerCompat getInputManager(Context context) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+                return new InputManagerV16(context);
+            } else {
+                return new InputManagerV9();
+            }
+        }
+    }
+}
diff --git a/samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerV16.java b/samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerV16.java
new file mode 100644
index 0000000..d26581e
--- /dev/null
+++ b/samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerV16.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.inputmanagercompat;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.hardware.input.InputManager;
+import android.os.Build;
+import android.os.Handler;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+public class InputManagerV16 implements InputManagerCompat {
+
+    private final InputManager mInputManager;
+    private final Map<InputManagerCompat.InputDeviceListener, V16InputDeviceListener> mListeners;
+
+    public InputManagerV16(Context context) {
+        mInputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
+        mListeners = new HashMap<InputManagerCompat.InputDeviceListener, V16InputDeviceListener>();
+    }
+
+    @Override
+    public InputDevice getInputDevice(int id) {
+        return mInputManager.getInputDevice(id);
+    }
+
+    @Override
+    public int[] getInputDeviceIds() {
+        return mInputManager.getInputDeviceIds();
+    }
+
+    static class V16InputDeviceListener implements InputManager.InputDeviceListener {
+        final InputManagerCompat.InputDeviceListener mIDL;
+
+        public V16InputDeviceListener(InputDeviceListener idl) {
+            mIDL = idl;
+        }
+
+        @Override
+        public void onInputDeviceAdded(int deviceId) {
+            mIDL.onInputDeviceAdded(deviceId);
+        }
+
+        @Override
+        public void onInputDeviceChanged(int deviceId) {
+            mIDL.onInputDeviceChanged(deviceId);
+        }
+
+        @Override
+        public void onInputDeviceRemoved(int deviceId) {
+            mIDL.onInputDeviceRemoved(deviceId);
+        }
+
+    }
+
+    @Override
+    public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) {
+        V16InputDeviceListener v16Listener = new V16InputDeviceListener(listener);
+        mInputManager.registerInputDeviceListener(v16Listener, handler);
+        mListeners.put(listener, v16Listener);
+    }
+
+    @Override
+    public void unregisterInputDeviceListener(InputDeviceListener listener) {
+        V16InputDeviceListener curListener = mListeners.remove(listener);
+        if (null != curListener)
+        {
+            mInputManager.unregisterInputDeviceListener(curListener);
+        }
+
+    }
+
+    @Override
+    public void onGenericMotionEvent(MotionEvent event) {
+        // unused in V16
+    }
+
+    @Override
+    public void onPause() {
+        // unused in V16
+    }
+
+    @Override
+    public void onResume() {
+        // unused in V16
+    }
+
+}
diff --git a/samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerV9.java b/samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerV9.java
new file mode 100644
index 0000000..dcd8988
--- /dev/null
+++ b/samples/ControllerSample/src/com/example/inputmanagercompat/InputManagerV9.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.inputmanagercompat;
+
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayDeque;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Queue;
+
+public class InputManagerV9 implements InputManagerCompat {
+    private static final String LOG_TAG = "InputManagerV9";
+    private static final int MESSAGE_TEST_FOR_DISCONNECT = 101;
+    private static final long CHECK_ELAPSED_TIME = 3000L;
+
+    private static final int ON_DEVICE_ADDED = 0;
+    private static final int ON_DEVICE_CHANGED = 1;
+    private static final int ON_DEVICE_REMOVED = 2;
+
+    private final SparseArray<long[]> mDevices;
+    private final Map<InputDeviceListener, Handler> mListeners;
+    private final Handler mDefaultHandler;
+
+    private static class PollingMessageHandler extends Handler {
+        private final WeakReference<InputManagerV9> mInputManager;
+
+        PollingMessageHandler(InputManagerV9 im) {
+            mInputManager = new WeakReference<InputManagerV9>(im);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            super.handleMessage(msg);
+            switch (msg.what) {
+                case MESSAGE_TEST_FOR_DISCONNECT:
+                    InputManagerV9 imv = mInputManager.get();
+                    if (null != imv) {
+                        long time = SystemClock.elapsedRealtime();
+                        int size = imv.mDevices.size();
+                        for (int i = 0; i < size; i++) {
+                            long[] lastContact = imv.mDevices.valueAt(i);
+                            if (null != lastContact) {
+                                if (time - lastContact[0] > CHECK_ELAPSED_TIME) {
+                                    // check to see if the device has been
+                                    // disconnected
+                                    int id = imv.mDevices.keyAt(i);
+                                    if (null == InputDevice.getDevice(id)) {
+                                        // disconnected!
+                                        imv.notifyListeners(ON_DEVICE_REMOVED, id);
+                                        imv.mDevices.remove(id);
+                                    } else {
+                                        lastContact[0] = time;
+                                    }
+                                }
+                            }
+                        }
+                        sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT,
+                                CHECK_ELAPSED_TIME);
+                    }
+                    break;
+            }
+        }
+
+    }
+
+    public InputManagerV9() {
+        mDevices = new SparseArray<long[]>();
+        mListeners = new HashMap<InputDeviceListener, Handler>();
+        mDefaultHandler = new PollingMessageHandler(this);
+        // as a side-effect, populates our collection of watched
+        // input devices
+        getInputDeviceIds();
+    }
+
+    @Override
+    public InputDevice getInputDevice(int id) {
+        return InputDevice.getDevice(id);
+    }
+
+    @Override
+    public int[] getInputDeviceIds() {
+        // add any hitherto unknown devices to our
+        // collection of watched input devices
+        int[] activeDevices = InputDevice.getDeviceIds();
+        long time = SystemClock.elapsedRealtime();
+        for ( int id : activeDevices ) {
+            long[] lastContact = mDevices.get(id);
+            if ( null == lastContact ) {
+                // we have a new device
+                mDevices.put(id, new long[] { time });
+            }
+        }
+        return activeDevices;
+    }
+
+    @Override
+    public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) {
+        mListeners.remove(listener);
+        if (handler == null) {
+            handler = mDefaultHandler;
+        }
+        mListeners.put(listener, handler);
+    }
+
+    @Override
+    public void unregisterInputDeviceListener(InputDeviceListener listener) {
+        mListeners.remove(listener);
+    }
+
+    private void notifyListeners(int why, int deviceId) {
+        // the state of some device has changed
+        if (!mListeners.isEmpty()) {
+            // yes... this will cause an object to get created... hopefully
+            // it won't happen very often
+            for (InputDeviceListener listener : mListeners.keySet()) {
+                Handler handler = mListeners.get(listener);
+                DeviceEvent odc = DeviceEvent.getDeviceEvent(why, deviceId, listener);
+                handler.post(odc);
+            }
+        }
+    }
+
+    private static class DeviceEvent implements Runnable {
+        private int mMessageType;
+        private int mId;
+        private InputDeviceListener mListener;
+        private static Queue<DeviceEvent> sEventQueue = new ArrayDeque<DeviceEvent>();
+
+        private DeviceEvent() {
+        }
+
+        static DeviceEvent getDeviceEvent(int messageType, int id,
+                InputDeviceListener listener) {
+            DeviceEvent curChanged = sEventQueue.poll();
+            if (null == curChanged) {
+                curChanged = new DeviceEvent();
+            }
+            curChanged.mMessageType = messageType;
+            curChanged.mId = id;
+            curChanged.mListener = listener;
+            return curChanged;
+        }
+
+        @Override
+        public void run() {
+            switch (mMessageType) {
+                case ON_DEVICE_ADDED:
+                    mListener.onInputDeviceAdded(mId);
+                    break;
+                case ON_DEVICE_CHANGED:
+                    mListener.onInputDeviceChanged(mId);
+                    break;
+                case ON_DEVICE_REMOVED:
+                    mListener.onInputDeviceRemoved(mId);
+                    break;
+                default:
+                    Log.e(LOG_TAG, "Unknown Message Type");
+                    break;
+            }
+            // dump this runnable back in the queue
+            sEventQueue.offer(this);
+        }
+    }
+
+    @Override
+    public void onGenericMotionEvent(MotionEvent event) {
+        // detect new devices
+        int id = event.getDeviceId();
+        long[] timeArray = mDevices.get(id);
+        if (null == timeArray) {
+            notifyListeners(ON_DEVICE_ADDED, id);
+            timeArray = new long[1];
+            mDevices.put(id, timeArray);
+        }
+        long time = SystemClock.elapsedRealtime();
+        timeArray[0] = time;
+    }
+
+    @Override
+    public void onPause() {
+        mDefaultHandler.removeMessages(MESSAGE_TEST_FOR_DISCONNECT);
+    }
+
+    @Override
+    public void onResume() {
+        mDefaultHandler.sendEmptyMessage(MESSAGE_TEST_FOR_DISCONNECT);
+    }
+
+}
diff --git a/samples/Support4Demos/AndroidManifest.xml b/samples/Support4Demos/AndroidManifest.xml
index 1b015f1..cc98540 100644
--- a/samples/Support4Demos/AndroidManifest.xml
+++ b/samples/Support4Demos/AndroidManifest.xml
@@ -36,7 +36,8 @@
 
     <application android:label="@string/activity_sample_code"
             android:icon="@drawable/app_sample_code"
-            android:hardwareAccelerated="true">
+            android:hardwareAccelerated="true"
+            android:supportsRtl="true">
 
         <activity android:name="Support4Demos">
             <intent-filter>
@@ -304,6 +305,14 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".widget.ContentLoadingProgressBarActivity"
+                  android:label="@string/content_loading_progress_bar">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="com.example.android.supportv4.SUPPORT4_SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+
         <activity android:name=".widget.SlidingPaneLayoutActivity"
                   android:label="@string/sliding_pane_layout_support">
             <intent-filter>
diff --git a/samples/Support4Demos/res/layout/content_loading_progressbar.xml b/samples/Support4Demos/res/layout/content_loading_progressbar.xml
new file mode 100644
index 0000000..7e7d9e0
--- /dev/null
+++ b/samples/Support4Demos/res/layout/content_loading_progressbar.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <android.support.v4.widget.ContentLoadingProgressBar
+        android:id="@+id/progressbar"
+        style="?android:attr/progressBarStyleLarge"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:visibility="gone" />
+
+    <Button android:id="@+id/show"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/show" />
+
+    <Button android:id="@+id/hide"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/hide" />
+
+    <TextView android:id="@+id/show_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+
+    <TextView android:id="@+id/show_text_done"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+
+    <TextView android:id="@+id/hide_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+
+    <TextView android:id="@+id/hide_text_done"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+
+</LinearLayout>
diff --git a/samples/Support4Demos/res/layout/drawer_layout.xml b/samples/Support4Demos/res/layout/drawer_layout.xml
index 2ab0b1a..375c802 100644
--- a/samples/Support4Demos/res/layout/drawer_layout.xml
+++ b/samples/Support4Demos/res/layout/drawer_layout.xml
@@ -37,15 +37,15 @@
                   android:text="@string/drawer_layout_summary"
                   android:textAppearance="?android:attr/textAppearanceMedium"/>
     </ScrollView>
-    <!-- android:layout_gravity="left" tells DrawerLayout to treat
-         this as a sliding drawer on the left side. The drawer is
-         given a fixed width in dp and extends the full height of
-         the container. A solid background is used for contrast
-         with the content view. -->
-    <ListView android:id="@+id/left_drawer"
+    <!-- android:layout_gravity="start" tells DrawerLayout to treat
+         this as a sliding drawer on the starting side, which is
+         left for left-to-right locales. The drawer is given a fixed
+         width in dp and extends the full height of the container. A
+         solid background is used for contrast with the content view. -->
+    <ListView android:id="@+id/start_drawer"
               android:layout_width="300dp"
               android:layout_height="match_parent"
-              android:layout_gravity="left"
+              android:layout_gravity="start"
               android:background="#ff333333"/>
 </android.support.v4.widget.DrawerLayout>
 
diff --git a/samples/Support4Demos/res/values/strings.xml b/samples/Support4Demos/res/values/strings.xml
index 5860ccc..9949ee0 100644
--- a/samples/Support4Demos/res/values/strings.xml
+++ b/samples/Support4Demos/res/values/strings.xml
@@ -54,6 +54,7 @@
 
     <string name="fragment_dialog_support">Fragment/Dialog</string>
     <string name="show">Show</string>
+    <string name="hide">Hide</string>
 
     <string name="fragment_dialog_or_activity_support">Fragment/Dialog or Activity</string>
     <string name="fragment_dialog_or_activity_msg">Demonstrates the same fragment
@@ -157,7 +158,7 @@
 
     <string name="drawer_layout_support">Widget/Drawer layout</string>
 
-    <string name="drawer_layout_summary">This activity illustrates the use of sliding drawers. The drawer may be pulled out from the left edge with an edge swipe. If this demo is running on Ice Cream Sandwich or newer you may tap the icon at the left side of the action bar to open the drawer as well.</string>
+    <string name="drawer_layout_summary">This activity illustrates the use of sliding drawers. The drawer may be pulled out from the starting edge, which is left on left-to-right locales, with an edge swipe. If this demo is running on Ice Cream Sandwich or newer you may tap the icon at the starting side of the action bar to open the drawer as well.</string>
 
     <string name="drawer_open">Open navigation drawer</string>
     <string name="drawer_close">Close navigation drawer</string>
@@ -166,4 +167,6 @@
 
     <string name="sliding_pane_layout_summary">This activity illustrates the use of sliding panes. The content pane may be slid to one side on narrow devices to reveal the left pane used to select content. Sliding panes can be used to fit a UI intended for wider screens in a smaller space. Tapping the Action Bar\'s Up button at the left side of the bar will navigate up in the hierarchy, represented by the left pane. If you rotate the device to landscape mode, on most devices you will see that both panes fit together side by side with no sliding necessary.</string>
 
+    <!-- ContentLoadingProgressBar -->
+    <string name="content_loading_progress_bar">Widget/Content Loading Progress Bar</string>
 </resources>
diff --git a/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentLayoutSupport.java b/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentLayoutSupport.java
index 8be83a6..1230dd3 100644
--- a/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentLayoutSupport.java
+++ b/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentLayoutSupport.java
@@ -152,14 +152,10 @@
 
                     // Execute a transaction, replacing any existing fragment
                     // with this one inside the frame.
-                    FragmentTransaction ft = getFragmentManager().beginTransaction();
-                    if (index == 0) {
-                        ft.replace(R.id.details, details);
-                    } else {
-                        ft.replace(R.id.a_item, details);
-                    }
-                    ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
-                    ft.commit();
+                    getFragmentManager().beginTransaction()
+                            .replace(R.id.details, details)
+                            .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
+                            .commit();
                 }
 
             } else {
diff --git a/samples/Support4Demos/src/com/example/android/supportv4/widget/ContentLoadingProgressBarActivity.java b/samples/Support4Demos/src/com/example/android/supportv4/widget/ContentLoadingProgressBarActivity.java
new file mode 100644
index 0000000..08c14dc
--- /dev/null
+++ b/samples/Support4Demos/src/com/example/android/supportv4/widget/ContentLoadingProgressBarActivity.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.supportv4.widget;
+
+import android.app.Activity;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.widget.ContentLoadingProgressBar;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+
+import com.example.android.supportv4.R;
+
+/**
+ * Demonstrates how to use the ContentLoadingProgressBar. By default, the
+ * developer should start the ContentLoadingProgressBar with visibility of
+ * "gone" or "invisible". The ContentLoadingProgressBar will be shown after the
+ * default delay for at least a minimum time regardless of when the "hide"
+ * button is pressed.
+ */
+public class ContentLoadingProgressBarActivity extends Activity implements
+        OnClickListener, ViewTreeObserver.OnGlobalLayoutListener {
+
+    private Button mShowButton;
+    private Button mHideButton;
+    private ContentLoadingProgressBar mBar;
+    private long mShowTime;
+    private long mHideTime;
+    private TextView mShowText;
+    private TextView mShowTextDone;
+    private TextView mHideText;
+    private TextView mHideTextDone;
+    private int mLastVisibility;
+    private long mVisibilityChangedTime;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.content_loading_progressbar);
+
+        mBar = (ContentLoadingProgressBar)findViewById(R.id.progressbar);
+        mShowButton = (Button)findViewById(R.id.show);
+        mShowButton.setOnClickListener(this);
+        mHideButton = (Button)findViewById(R.id.hide);
+        mHideButton.setOnClickListener(this);
+
+        mShowText = (TextView)findViewById(R.id.show_text);
+        mShowTextDone = (TextView)findViewById(R.id.show_text_done);
+        mHideText = (TextView)findViewById(R.id.hide_text);
+        mHideTextDone = (TextView)findViewById(R.id.hide_text_done);
+
+        mLastVisibility = mBar.getVisibility();
+
+        mBar.getViewTreeObserver().addOnGlobalLayoutListener(this);
+    }
+
+    @Override
+    public void onClick(View v) {
+        switch (v.getId()) {
+            case R.id.show:
+                mBar.show();
+                mShowTime = System.currentTimeMillis();
+                mShowText.setText("Show clicked at " + mShowTime);
+                break;
+            case R.id.hide:
+                mBar.hide();
+                mHideTime = System.currentTimeMillis();
+                mHideText.setText("Hide clicked at " + mHideTime);
+                break;
+        }
+    }
+
+    @Override
+    public void onGlobalLayout() {
+        final int visibility = mBar.getVisibility();
+
+        if (mLastVisibility != visibility) {
+            if (visibility == View.VISIBLE) {
+                mVisibilityChangedTime = System.currentTimeMillis();
+                mShowTextDone.setText("Shown at "
+                    + (mVisibilityChangedTime - mShowTime));
+            } else {
+                mHideTextDone.setText("Hidden after "
+                    + (System.currentTimeMillis() - mVisibilityChangedTime));
+            }
+            mLastVisibility = mBar.getVisibility();
+        }
+    }
+}
diff --git a/samples/Support4Demos/src/com/example/android/supportv4/widget/DrawerLayoutActivity.java b/samples/Support4Demos/src/com/example/android/supportv4/widget/DrawerLayoutActivity.java
index ce311cc..7b88faa 100644
--- a/samples/Support4Demos/src/com/example/android/supportv4/widget/DrawerLayoutActivity.java
+++ b/samples/Support4Demos/src/com/example/android/supportv4/widget/DrawerLayoutActivity.java
@@ -83,7 +83,7 @@
         setContentView(R.layout.drawer_layout);
 
         mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
-        mDrawer = (ListView) findViewById(R.id.left_drawer);
+        mDrawer = (ListView) findViewById(R.id.start_drawer);
         mContent = (TextView) findViewById(R.id.content_text);
 
         mDrawerLayout.setDrawerListener(new DemoDrawerListener());
diff --git a/samples/Support7Demos/AndroidManifest.xml b/samples/Support7Demos/AndroidManifest.xml
index 1edf3e6..4735433 100644
--- a/samples/Support7Demos/AndroidManifest.xml
+++ b/samples/Support7Demos/AndroidManifest.xml
@@ -21,6 +21,12 @@
      to come from a domain that you own or have control over. -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.example.android.supportv7">
+    <!-- Permission for INTERNET is required for streaming video content
+         from the web, it's not required otherwise. -->
+    <uses-permission android:name="android.permission.INTERNET" />
+    <!-- Permission for SYSTEM_ALERT_WINDOW is only required for emulating
+         remote display using system alert window. -->
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
 
     <uses-sdk android:minSdkVersion="7" android:targetSdkVersion="17" />
 
@@ -42,7 +48,11 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
-        
+        <receiver android:name="com.example.android.supportv7.media.SampleMediaButtonReceiver">
+            <intent-filter>
+                <action android:name="android.intent.action.MEDIA_BUTTON" />
+            </intent-filter>
+        </receiver>
         <!-- MediaRouter Support Samples -->
 
         <activity android:name=".media.SampleMediaRouterActivity"
diff --git a/samples/Support7Demos/res/drawable-hdpi/ic_media_pause.png b/samples/Support7Demos/res/drawable-hdpi/ic_media_pause.png
new file mode 100644
index 0000000..1d465a4
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-hdpi/ic_media_pause.png
Binary files differ
diff --git a/samples/Support7Demos/res/drawable-hdpi/ic_media_play.png b/samples/Support7Demos/res/drawable-hdpi/ic_media_play.png
new file mode 100644
index 0000000..2746d17
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-hdpi/ic_media_play.png
Binary files differ
diff --git a/samples/Support7Demos/res/drawable-hdpi/ic_media_stop.png b/samples/Support7Demos/res/drawable-hdpi/ic_media_stop.png
new file mode 100644
index 0000000..a0ff136
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-hdpi/ic_media_stop.png
Binary files differ
diff --git a/samples/Support7Demos/res/drawable-hdpi/ic_menu_add.png b/samples/Support7Demos/res/drawable-hdpi/ic_menu_add.png
new file mode 100644
index 0000000..444e8a5
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-hdpi/ic_menu_add.png
Binary files differ
diff --git a/samples/Support7Demos/res/drawable-hdpi/ic_menu_delete.png b/samples/Support7Demos/res/drawable-hdpi/ic_menu_delete.png
new file mode 100644
index 0000000..24d8f6a
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-hdpi/ic_menu_delete.png
Binary files differ
diff --git a/samples/Support7Demos/res/drawable-mdpi/ic_media_pause.png b/samples/Support7Demos/res/drawable-mdpi/ic_media_pause.png
new file mode 100644
index 0000000..3e6b2a1
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-mdpi/ic_media_pause.png
Binary files differ
diff --git a/samples/Support7Demos/res/drawable-mdpi/ic_media_play.png b/samples/Support7Demos/res/drawable-mdpi/ic_media_play.png
new file mode 100644
index 0000000..7966bbc
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-mdpi/ic_media_play.png
Binary files differ
diff --git a/samples/Support7Demos/res/drawable-mdpi/ic_media_stop.png b/samples/Support7Demos/res/drawable-mdpi/ic_media_stop.png
new file mode 100644
index 0000000..8ea7efe
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-mdpi/ic_media_stop.png
Binary files differ
diff --git a/samples/Support7Demos/res/drawable-mdpi/ic_menu_add.png b/samples/Support7Demos/res/drawable-mdpi/ic_menu_add.png
new file mode 100644
index 0000000..361c7c4
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-mdpi/ic_menu_add.png
Binary files differ
diff --git a/samples/Support7Demos/res/drawable-mdpi/ic_menu_delete.png b/samples/Support7Demos/res/drawable-mdpi/ic_menu_delete.png
new file mode 100644
index 0000000..e2c8700
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-mdpi/ic_menu_delete.png
Binary files differ
diff --git a/samples/Support7Demos/res/layout/media_item.xml b/samples/Support7Demos/res/layout/media_item.xml
new file mode 100644
index 0000000..e5d6d02
--- /dev/null
+++ b/samples/Support7Demos/res/layout/media_item.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<!-- Layout for list item in Library or Playlist view. Displays ImageButton
+     instead of radio button to the right of the item. -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="horizontal"
+        android:gravity="center_vertical">
+
+    <ImageButton android:id="@+id/item_action"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:minWidth="48dp"
+        android:minHeight="48dp"
+        android:layout_alignParentRight="true"
+        android:layout_centerVertical="true"
+        android:background="@null"/>
+
+    <TextView android:id="@+id/item_text"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:layout_centerVertical="true"
+        android:layout_toLeftOf="@id/item_action"
+        android:layout_gravity="left"
+        android:gravity="left"/>
+</RelativeLayout>
diff --git a/samples/Support7Demos/res/layout/overlay_display_window.xml b/samples/Support7Demos/res/layout/overlay_display_window.xml
new file mode 100644
index 0000000..36b4a0d
--- /dev/null
+++ b/samples/Support7Demos/res/layout/overlay_display_window.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      android:background="#000000">
+    <TextureView android:id="@+id/overlay_display_window_texture"
+               android:layout_width="0px"
+               android:layout_height="0px" />
+    <TextView android:id="@+id/overlay_display_window_title"
+               android:layout_width="wrap_content"
+               android:layout_height="wrap_content"
+               android:layout_gravity="top|center_horizontal" />
+</FrameLayout>
diff --git a/samples/Support7Demos/res/layout/sample_media_controller.xml b/samples/Support7Demos/res/layout/sample_media_controller.xml
new file mode 100644
index 0000000..4335b04
--- /dev/null
+++ b/samples/Support7Demos/res/layout/sample_media_controller.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<!-- Layout for the customized MediaRouteControllerDialog -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+    <LinearLayout android:id="@+id/media_route_info"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_weight="1">
+        <ImageView
+            android:id="@+id/snapshot"
+            android:layout_width="100dp"
+            android:layout_height="100dp"
+            android:scaleType="centerCrop"/>
+        <TextView android:id="@+id/track_info"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:padding="4dp"/>
+    </LinearLayout>
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_weight="0"
+        android:gravity="center">
+        <ImageButton android:id="@+id/pause_resume_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="48dp"
+            android:minHeight="48dp"
+            android:background="@null"
+            android:src="@drawable/ic_media_pause" />
+        <ImageButton android:id="@+id/stop_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="48dp"
+            android:minHeight="48dp"
+            android:background="@null"
+            android:src="@drawable/ic_media_stop" />
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/samples/Support7Demos/res/layout/sample_media_router.xml b/samples/Support7Demos/res/layout/sample_media_router.xml
index a8b08b1..e2f7008 100644
--- a/samples/Support7Demos/res/layout/sample_media_router.xml
+++ b/samples/Support7Demos/res/layout/sample_media_router.xml
@@ -20,45 +20,118 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:orientation="vertical">
-
-    <!-- Message to show to use. -->
-    <TextView android:id="@+id/text"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_weight="0"
-        android:gravity="center_vertical|center_horizontal"
-        android:textAppearance="?android:attr/textAppearanceMedium"
-        android:text="@string/sample_media_router_text"/>
-
-    <!-- Some information about what's going on. -->
-    <TextView android:id="@+id/info"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_weight="0"
-        android:gravity="center_vertical|center_horizontal"
-        android:textAppearance="?android:attr/textAppearanceMedium"/>
-
-    <!-- Some media to play. -->
-    <ListView android:id="@+id/media"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_weight="1"/>
-
-    <!-- Control buttons for the currently selected route. -->
     <LinearLayout
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_weight="0">
-        <Button android:id="@+id/play_button"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_weight="0"
-            android:text="@string/play_button_text" />
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:orientation="vertical">
+        <!-- Tabs for media library, playlist and statistics -->
+        <TabHost android:id="@+id/tabHost"
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent"
+            android:layout_weight="1">
+            <LinearLayout
+                android:orientation="vertical"
+                android:layout_width="fill_parent"
+                android:layout_height="fill_parent">
+                <TabWidget android:id="@android:id/tabs"
+                    android:layout_width="fill_parent"
+                    android:layout_height="wrap_content" />
 
-        <Button android:id="@+id/statistics_button"
+                <FrameLayout android:id="@android:id/tabcontent"
+                    android:layout_width="fill_parent"
+                    android:layout_height="wrap_content">
+                    <LinearLayout android:id="@+id/tab1"
+                        android:layout_width="fill_parent"
+                        android:layout_height="wrap_content"
+                        android:orientation="vertical">
+                        <ListView android:id="@+id/media"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_weight="1" />
+                    </LinearLayout>
+
+                    <LinearLayout android:id="@+id/tab2"
+                        android:layout_width="fill_parent"
+                        android:layout_height="fill_parent"
+                        android:orientation="vertical">
+                        <ListView android:id="@+id/playlist"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_weight="1"/>
+                    </LinearLayout>
+
+                    <LinearLayout android:id="@+id/tab3"
+                        android:layout_width="fill_parent"
+                        android:layout_height="fill_parent"
+                        android:orientation="vertical">
+                        <TextView android:id="@+id/info"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_weight="1"
+                            android:textAppearance="?android:attr/textAppearanceMedium"/>
+                    </LinearLayout>
+                </FrameLayout>
+            </LinearLayout>
+        </TabHost>
+
+        <!-- Control buttons for the currently selected route. -->
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="0">
+
+            <SeekBar android:id="@+id/seekbar"
+                 android:layout_width="fill_parent"
+                 android:layout_height="wrap_content"
+                 style="?android:attr/progressBarStyleHorizontal"
+                 android:max="100"
+                 android:progress="0"
+                 android:layout_gravity="center"
+                 android:layout_weight="1"/>
+
+            <ImageButton android:id="@+id/pause_resume_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="0"
+                android:layout_gravity="right"
+                android:minWidth="48dp"
+                android:minHeight="48dp"
+                android:background="@null"
+                android:src="@drawable/ic_media_pause" />
+
+            <ImageButton android:id="@+id/stop_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="0"
+                android:layout_gravity="right"
+                android:minWidth="48dp"
+                android:minHeight="48dp"
+                android:background="@null"
+                android:src="@drawable/ic_media_stop" />
+        </LinearLayout>
+
+    </LinearLayout>
+
+    <!-- Some content for visual interest in the case where no presentation is showing. -->
+    <FrameLayout android:id="@+id/player"
+        android:background="#ff000000"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1">
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:gravity="center">
+            <SurfaceView android:id="@+id/surface_view"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"/>
+        </LinearLayout>
+        <TextView
+            android:textColor="#ffaaaaaa"
+            android:text="@string/sample_media_route_activity_local"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_weight="0"
-            android:text="@string/statistics_button_text" />
-    </LinearLayout>
+            android:layout_gravity="top|center_horizontal" />
+    </FrameLayout>
 </LinearLayout>
diff --git a/samples/Support7Demos/res/layout/sample_media_router_presentation.xml b/samples/Support7Demos/res/layout/sample_media_router_presentation.xml
new file mode 100644
index 0000000..f029627
--- /dev/null
+++ b/samples/Support7Demos/res/layout/sample_media_router_presentation.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- The content that we show on secondary displays.
+     See corresponding Java code PresentationWithMediaRouterActivity.java. -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#ff000000">
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center">
+        <SurfaceView android:id="@+id/surface_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
+    </LinearLayout>
+    <TextView
+        android:textColor="#ffaaaaaa"
+        android:text="@string/sample_media_route_activity_presentation"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="top|center_horizontal" />
+</FrameLayout>
diff --git a/samples/Support7Demos/res/menu/actions.xml b/samples/Support7Demos/res/menu/actions.xml
index fbae7b1..38d291e 100644
--- a/samples/Support7Demos/res/menu/actions.xml
+++ b/samples/Support7Demos/res/menu/actions.xml
@@ -19,7 +19,7 @@
           android:icon="@android:drawable/ic_menu_search"
           android:title="@string/action_bar_search"
           app:showAsAction="ifRoom"
-          app:actionProviderClass="android.support.v7.widget.SearchActionProvider" />
+          app:actionViewClass="android.support.v7.widget.SearchView" />
     <item android:id="@+id/action_add"
           android:icon="@android:drawable/ic_menu_add"
           android:title="@string/action_bar_add" />
diff --git a/samples/Support7Demos/res/values/arrays.xml b/samples/Support7Demos/res/values/arrays.xml
index f5616a1..8d658eb 100644
--- a/samples/Support7Demos/res/values/arrays.xml
+++ b/samples/Support7Demos/res/values/arrays.xml
@@ -16,18 +16,16 @@
 
 <resources>
     <string-array name="media_names">
-        <item>My favorite video of cats</item>
-        <item>Cats on parade</item>
-        <item>Cats with hats</item>
-        <item>Hats on cats</item>
-        <item>Cats on mats</item>
+        <item>Big Buck Bunny</item>
+        <item>Elephants Dream</item>
+        <item>Sintel</item>
+        <item>Tears of Steel</item>
     </string-array>
 
     <string-array name="media_uris">
-        <item>http://cats.example.com/favorite-cats.mp4</item>
-        <item>http://cats.example.com/cats-on-parade.mp4</item>
-        <item>http://cats.example.com/cats-with-hats.mp4</item>
-        <item>http://cats.example.com/hats-on-cats.mp4</item>
-        <item>http://cats.example.com/cats-on-mats.mp4</item>
+        <item>http://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4</item>
+        <item>http://archive.org/download/ElephantsDream_277/elephant_dreams_640_512kb.mp4</item>
+        <item>http://archive.org/download/Sintel/sintel-2048-stereo_512kb.mp4</item>
+        <item>http://archive.org/download/Tears-of-Steel/tears_of_steel_720p.mp4</item>
     </string-array>
 </resources>
diff --git a/samples/Support7Demos/res/values/strings.xml b/samples/Support7Demos/res/values/strings.xml
index 6a8ac77..1b13623 100644
--- a/samples/Support7Demos/res/values/strings.xml
+++ b/samples/Support7Demos/res/values/strings.xml
@@ -26,12 +26,15 @@
             use MediaRouter from the support library.  Select a route from the action bar.</string>
     <string name="media_route_menu_title">Play on...</string>
 
-    <string name="play_button_text">Play</string>
-    <string name="statistics_button_text">Show Statistics</string>
+    <string name="library_tab_text">Library</string>
+    <string name="playlist_tab_text">Playlist</string>
+    <string name="info_tab_text">Route Info</string>
 
     <string name="sample_media_route_provider_service">Media Route Provider Service Support Library Sample</string>
     <string name="fixed_volume_route_name">Fixed Volume Remote Playback Route</string>
-    <string name="variable_volume_route_name">Variable Volume Remote Playback Route</string>
+    <string name="variable_volume_basic_route_name">Variable Volume (Basic) Remote Playback Route</string>
+    <string name="variable_volume_queuing_route_name">Variable Volume (Queuing) Remote Playback Route</string>
+    <string name="variable_volume_session_route_name">Variable Volume (Session) Remote Playback Route</string>
     <string name="sample_route_description">Sample route from Support7Demos</string>
 
     <!-- GridLayout -->
@@ -88,4 +91,8 @@
     <string name="action_bar_fragment_has_options_menu">Set has options menu to true</string>
     <string name="action_bar_fragment_menu_visibility">Set menu visibility to true</string>
 
+    <string name="sample_media_route_provider_remote">Remote Playback (Simulated)</string>
+    <string name="sample_media_route_activity_local">Local Playback</string>
+    <string name="sample_media_route_activity_presentation">Local Playback on Presentation Display</string>
+
 </resources>
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/app/ActionBarUsage.java b/samples/Support7Demos/src/com/example/android/supportv7/app/ActionBarUsage.java
index d6152cc..6ed59fb 100644
--- a/samples/Support7Demos/src/com/example/android/supportv7/app/ActionBarUsage.java
+++ b/samples/Support7Demos/src/com/example/android/supportv7/app/ActionBarUsage.java
@@ -15,18 +15,19 @@
  */
 package com.example.android.supportv7.app;
 
+import com.example.android.supportv7.R;
+
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.support.v4.view.MenuItemCompat;
-import android.support.v4.widget.SearchViewCompat;
 import android.support.v7.app.ActionBarActivity;
+import android.support.v7.widget.SearchView;
+import android.text.TextUtils;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
-import android.view.View;
 import android.widget.TextView;
 import android.widget.Toast;
-import com.example.android.supportv7.R;
 
 /**
  * This demonstrates idiomatic usage of the Action Bar. The default Honeycomb theme
@@ -49,8 +50,9 @@
     public boolean onCreateOptionsMenu(Menu menu) {
         MenuInflater inflater = getMenuInflater();
         inflater.inflate(R.menu.actions, menu);
-        View searchView = MenuItemCompat.getActionView(menu.findItem(R.id.action_search));
-        SearchViewCompat.setOnQueryTextListener(searchView, mOnQueryTextListener);
+        SearchView searchView = (SearchView) MenuItemCompat
+                .getActionView(menu.findItem(R.id.action_search));
+        searchView.setOnQueryTextListener(mOnQueryTextListener);
         return true;
     }
 
@@ -80,11 +82,11 @@
 
     // The following callbacks are called for the SearchView.OnQueryChangeListener
     // For more about using SearchView, see src/.../view/SearchView1.java and SearchView2.java
-    private final SearchViewCompat.OnQueryTextListenerCompat mOnQueryTextListener =
-            new SearchViewCompat.OnQueryTextListenerCompat() {
+    private final SearchView.OnQueryTextListener mOnQueryTextListener =
+            new SearchView.OnQueryTextListener() {
         @Override
         public boolean onQueryTextChange(String newText) {
-            newText = newText.isEmpty() ? "" : "Query so far: " + newText;
+            newText = TextUtils.isEmpty(newText) ? "" : "Query so far: " + newText;
             mSearchText.setText(newText);
             return true;
         }
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/LocalPlayer.java b/samples/Support7Demos/src/com/example/android/supportv7/media/LocalPlayer.java
new file mode 100644
index 0000000..806df25
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/LocalPlayer.java
@@ -0,0 +1,638 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.supportv7.media;
+
+import android.app.Activity;
+import android.app.Presentation;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.support.v7.media.MediaRouter.RouteInfo;
+import android.support.v7.media.MediaItemStatus;
+import android.util.Log;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+
+import com.example.android.supportv7.R;
+
+import java.io.IOException;
+
+/**
+ * Handles playback of a single media item using MediaPlayer.
+ */
+public abstract class LocalPlayer extends Player implements
+        MediaPlayer.OnPreparedListener,
+        MediaPlayer.OnCompletionListener,
+        MediaPlayer.OnErrorListener,
+        MediaPlayer.OnSeekCompleteListener {
+    private static final String TAG = "LocalPlayer";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final int STATE_IDLE = 0;
+    private static final int STATE_PLAY_PENDING = 1;
+    private static final int STATE_READY = 2;
+    private static final int STATE_PLAYING = 3;
+    private static final int STATE_PAUSED = 4;
+
+    private final Context mContext;
+    private final Handler mHandler = new Handler();
+    private MediaPlayer mMediaPlayer;
+    private int mState = STATE_IDLE;
+    private int mSeekToPos;
+    private int mVideoWidth;
+    private int mVideoHeight;
+    private Surface mSurface;
+    private SurfaceHolder mSurfaceHolder;
+
+    public LocalPlayer(Context context) {
+        mContext = context;
+
+        // reset media player
+        reset();
+    }
+
+    @Override
+    public boolean isRemotePlayback() {
+        return false;
+    }
+
+    @Override
+    public boolean isQueuingSupported() {
+        return false;
+    }
+
+    @Override
+    public void connect(RouteInfo route) {
+        if (DEBUG) {
+            Log.d(TAG, "connecting to: " + route);
+        }
+    }
+
+    @Override
+    public void release() {
+        if (DEBUG) {
+            Log.d(TAG, "releasing");
+        }
+        // release media player
+        if (mMediaPlayer != null) {
+            mMediaPlayer.stop();
+            mMediaPlayer.release();
+            mMediaPlayer = null;
+        }
+    }
+
+    // Player
+    @Override
+    public void play(final PlaylistItem item) {
+        if (DEBUG) {
+            Log.d(TAG, "play: item=" + item);
+        }
+        reset();
+        mSeekToPos = (int)item.getPosition();
+        try {
+            mMediaPlayer.setDataSource(mContext, item.getUri());
+            mMediaPlayer.prepareAsync();
+        } catch (IllegalStateException e) {
+            Log.e(TAG, "MediaPlayer throws IllegalStateException, uri=" + item.getUri());
+        } catch (IOException e) {
+            Log.e(TAG, "MediaPlayer throws IOException, uri=" + item.getUri());
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "MediaPlayer throws IllegalArgumentException, uri=" + item.getUri());
+        } catch (SecurityException e) {
+            Log.e(TAG, "MediaPlayer throws SecurityException, uri=" + item.getUri());
+        }
+        if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) {
+            resume();
+        } else {
+            pause();
+        }
+    }
+
+    @Override
+    public void seek(final PlaylistItem item) {
+        if (DEBUG) {
+            Log.d(TAG, "seek: item=" + item);
+        }
+        int pos = (int)item.getPosition();
+        if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
+            mMediaPlayer.seekTo(pos);
+            mSeekToPos = pos;
+        } else if (mState == STATE_IDLE || mState == STATE_PLAY_PENDING) {
+            // Seek before onPrepared() arrives,
+            // need to performed delayed seek in onPrepared()
+            mSeekToPos = pos;
+        }
+    }
+
+    @Override
+    public void getStatus(final PlaylistItem item, final boolean update) {
+        if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
+            // use mSeekToPos if we're currently seeking (mSeekToPos is reset
+            // when seeking is completed)
+            item.setDuration(mMediaPlayer.getDuration());
+            item.setPosition(mSeekToPos > 0 ?
+                    mSeekToPos : mMediaPlayer.getCurrentPosition());
+            item.setTimestamp(SystemClock.elapsedRealtime());
+        }
+        if (update && mCallback != null) {
+            mCallback.onPlaylistReady();
+        }
+    }
+
+    @Override
+    public void pause() {
+        if (DEBUG) {
+            Log.d(TAG, "pause");
+        }
+        if (mState == STATE_PLAYING) {
+            mMediaPlayer.pause();
+            mState = STATE_PAUSED;
+        }
+    }
+
+    @Override
+    public void resume() {
+        if (DEBUG) {
+            Log.d(TAG, "resume");
+        }
+        if (mState == STATE_READY || mState == STATE_PAUSED) {
+            mMediaPlayer.start();
+            mState = STATE_PLAYING;
+        } else if (mState == STATE_IDLE){
+            mState = STATE_PLAY_PENDING;
+        }
+    }
+
+    @Override
+    public void stop() {
+        if (DEBUG) {
+            Log.d(TAG, "stop");
+        }
+        if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
+            mMediaPlayer.stop();
+            mState = STATE_IDLE;
+        }
+    }
+
+    @Override
+    public void enqueue(final PlaylistItem item) {
+        throw new UnsupportedOperationException("LocalPlayer doesn't support enqueue!");
+    }
+
+    @Override
+    public PlaylistItem remove(String iid) {
+        throw new UnsupportedOperationException("LocalPlayer doesn't support remove!");
+    }
+
+    //MediaPlayer Listeners
+    @Override
+    public void onPrepared(MediaPlayer mp) {
+        if (DEBUG) {
+            Log.d(TAG, "onPrepared");
+        }
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                if (mState == STATE_IDLE) {
+                    mState = STATE_READY;
+                    updateVideoRect();
+                } else if (mState == STATE_PLAY_PENDING) {
+                    mState = STATE_PLAYING;
+                    updateVideoRect();
+                    if (mSeekToPos > 0) {
+                        if (DEBUG) {
+                            Log.d(TAG, "seek to initial pos: " + mSeekToPos);
+                        }
+                        mMediaPlayer.seekTo(mSeekToPos);
+                    }
+                    mMediaPlayer.start();
+                }
+                if (mCallback != null) {
+                    mCallback.onPlaylistChanged();
+                }
+            }
+        });
+    }
+
+    @Override
+    public void onCompletion(MediaPlayer mp) {
+        if (DEBUG) {
+            Log.d(TAG, "onCompletion");
+        }
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                if (mCallback != null) {
+                    mCallback.onCompletion();
+                }
+            }
+        });
+    }
+
+    @Override
+    public boolean onError(MediaPlayer mp, int what, int extra) {
+        if (DEBUG) {
+            Log.d(TAG, "onError");
+        }
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                if (mCallback != null) {
+                    mCallback.onError();
+                }
+            }
+        });
+        // return true so that onCompletion is not called
+        return true;
+    }
+
+    @Override
+    public void onSeekComplete(MediaPlayer mp) {
+        if (DEBUG) {
+            Log.d(TAG, "onSeekComplete");
+        }
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                mSeekToPos = 0;
+                if (mCallback != null) {
+                    mCallback.onPlaylistChanged();
+                }
+            }
+        });
+    }
+
+    protected Context getContext() { return mContext; }
+    protected MediaPlayer getMediaPlayer() { return mMediaPlayer; }
+    protected int getVideoWidth() { return mVideoWidth; }
+    protected int getVideoHeight() { return mVideoHeight; }
+    protected void setSurface(Surface surface) {
+        mSurface = surface;
+        mSurfaceHolder = null;
+        updateSurface();
+    }
+
+    protected void setSurface(SurfaceHolder surfaceHolder) {
+        mSurface = null;
+        mSurfaceHolder = surfaceHolder;
+        updateSurface();
+    }
+
+    protected void removeSurface(SurfaceHolder surfaceHolder) {
+        if (surfaceHolder == mSurfaceHolder) {
+            setSurface((SurfaceHolder)null);
+        }
+    }
+
+    protected void updateSurface() {
+        if (mMediaPlayer == null) {
+            // just return if media player is already gone
+            return;
+        }
+        if (mSurface != null) {
+            // The setSurface API does not exist until V14+.
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+                ICSMediaPlayer.setSurface(mMediaPlayer, mSurface);
+            } else {
+                throw new UnsupportedOperationException("MediaPlayer does not support "
+                        + "setSurface() on this version of the platform.");
+            }
+        } else if (mSurfaceHolder != null) {
+            mMediaPlayer.setDisplay(mSurfaceHolder);
+        } else {
+            mMediaPlayer.setDisplay(null);
+        }
+    }
+
+    protected abstract void updateSize();
+
+    private void reset() {
+        if (mMediaPlayer != null) {
+            mMediaPlayer.stop();
+            mMediaPlayer.release();
+            mMediaPlayer = null;
+        }
+        mMediaPlayer = new MediaPlayer();
+        mMediaPlayer.setOnPreparedListener(this);
+        mMediaPlayer.setOnCompletionListener(this);
+        mMediaPlayer.setOnErrorListener(this);
+        mMediaPlayer.setOnSeekCompleteListener(this);
+        updateSurface();
+        mState = STATE_IDLE;
+        mSeekToPos = 0;
+    }
+
+    private void updateVideoRect() {
+        if (mState != STATE_IDLE && mState != STATE_PLAY_PENDING) {
+            int width = mMediaPlayer.getVideoWidth();
+            int height = mMediaPlayer.getVideoHeight();
+            if (width > 0 && height > 0) {
+                mVideoWidth = width;
+                mVideoHeight = height;
+                updateSize();
+            } else {
+                Log.e(TAG, "video rect is 0x0!");
+                mVideoWidth = mVideoHeight = 0;
+            }
+        }
+    }
+
+    private static final class ICSMediaPlayer {
+        public static final void setSurface(MediaPlayer player, Surface surface) {
+            player.setSurface(surface);
+        }
+    }
+
+    /**
+     * Handles playback of a single media item using MediaPlayer in SurfaceView
+     */
+    public static class SurfaceViewPlayer extends LocalPlayer implements
+            SurfaceHolder.Callback {
+        private static final String TAG = "SurfaceViewPlayer";
+        private RouteInfo mRoute;
+        private final SurfaceView mSurfaceView;
+        private final FrameLayout mLayout;
+        private DemoPresentation mPresentation;
+
+        public SurfaceViewPlayer(Context context) {
+            super(context);
+
+            mLayout = (FrameLayout)((Activity)context).findViewById(R.id.player);
+            mSurfaceView = (SurfaceView)((Activity)context).findViewById(R.id.surface_view);
+
+            // add surface holder callback
+            SurfaceHolder holder = mSurfaceView.getHolder();
+            holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+            holder.addCallback(this);
+        }
+
+        @Override
+        public void connect(RouteInfo route) {
+            super.connect(route);
+            mRoute = route;
+        }
+
+        @Override
+        public void release() {
+            super.release();
+
+            // dismiss presentation display
+            if (mPresentation != null) {
+                Log.i(TAG, "Dismissing presentation because the activity is no longer visible.");
+                mPresentation.dismiss();
+                mPresentation = null;
+            }
+
+            // remove surface holder callback
+            SurfaceHolder holder = mSurfaceView.getHolder();
+            holder.removeCallback(this);
+
+            // hide the surface view when SurfaceViewPlayer is destroyed
+            mSurfaceView.setVisibility(View.GONE);
+            mLayout.setVisibility(View.GONE);
+        }
+
+        @Override
+        public void updatePresentation() {
+            // Get the current route and its presentation display.
+            Display presentationDisplay = mRoute != null ? mRoute.getPresentationDisplay() : null;
+
+            // Dismiss the current presentation if the display has changed.
+            if (mPresentation != null && mPresentation.getDisplay() != presentationDisplay) {
+                Log.i(TAG, "Dismissing presentation because the current route no longer "
+                        + "has a presentation display.");
+                mPresentation.dismiss();
+                mPresentation = null;
+            }
+
+            // Show a new presentation if needed.
+            if (mPresentation == null && presentationDisplay != null) {
+                Log.i(TAG, "Showing presentation on display: " + presentationDisplay);
+                mPresentation = new DemoPresentation(getContext(), presentationDisplay);
+                mPresentation.setOnDismissListener(mOnDismissListener);
+                try {
+                    mPresentation.show();
+                } catch (WindowManager.InvalidDisplayException ex) {
+                    Log.w(TAG, "Couldn't show presentation!  Display was removed in "
+                              + "the meantime.", ex);
+                    mPresentation = null;
+                }
+            }
+
+            updateContents();
+        }
+
+        // SurfaceHolder.Callback
+        @Override
+        public void surfaceChanged(SurfaceHolder holder, int format,
+                int width, int height) {
+            if (DEBUG) {
+                Log.d(TAG, "surfaceChanged: " + width + "x" + height);
+            }
+            setSurface(holder);
+        }
+
+        @Override
+        public void surfaceCreated(SurfaceHolder holder) {
+            if (DEBUG) {
+                Log.d(TAG, "surfaceCreated");
+            }
+            setSurface(holder);
+            updateSize();
+        }
+
+        @Override
+        public void surfaceDestroyed(SurfaceHolder holder) {
+            if (DEBUG) {
+                Log.d(TAG, "surfaceDestroyed");
+            }
+            removeSurface(holder);
+        }
+
+        @Override
+        protected void updateSize() {
+            int width = getVideoWidth();
+            int height = getVideoHeight();
+            if (width > 0 && height > 0) {
+                if (mPresentation == null) {
+                    int surfaceWidth = mLayout.getWidth();
+                    int surfaceHeight = mLayout.getHeight();
+
+                    // Calculate the new size of mSurfaceView, so that video is centered
+                    // inside the framelayout with proper letterboxing/pillarboxing
+                    ViewGroup.LayoutParams lp = mSurfaceView.getLayoutParams();
+                    if (surfaceWidth * height < surfaceHeight * width) {
+                        // Black bars on top&bottom, mSurfaceView has full layout width,
+                        // while height is derived from video's aspect ratio
+                        lp.width = surfaceWidth;
+                        lp.height = surfaceWidth * height / width;
+                    } else {
+                        // Black bars on left&right, mSurfaceView has full layout height,
+                        // while width is derived from video's aspect ratio
+                        lp.width = surfaceHeight * width / height;
+                        lp.height = surfaceHeight;
+                    }
+                    Log.i(TAG, "video rect is " + lp.width + "x" + lp.height);
+                    mSurfaceView.setLayoutParams(lp);
+                } else {
+                    mPresentation.updateSize(width, height);
+                }
+            }
+        }
+
+        private void updateContents() {
+            // Show either the content in the main activity or the content in the presentation
+            if (mPresentation != null) {
+                mLayout.setVisibility(View.GONE);
+                mSurfaceView.setVisibility(View.GONE);
+            } else {
+                mLayout.setVisibility(View.VISIBLE);
+                mSurfaceView.setVisibility(View.VISIBLE);
+            }
+        }
+
+        // Listens for when presentations are dismissed.
+        private final DialogInterface.OnDismissListener mOnDismissListener =
+                new DialogInterface.OnDismissListener() {
+            @Override
+            public void onDismiss(DialogInterface dialog) {
+                if (dialog == mPresentation) {
+                    Log.i(TAG, "Presentation dismissed.");
+                    mPresentation = null;
+                    updateContents();
+                }
+            }
+        };
+
+        // Presentation
+        private final class DemoPresentation extends Presentation {
+            private SurfaceView mPresentationSurfaceView;
+
+            public DemoPresentation(Context context, Display display) {
+                super(context, display);
+            }
+
+            @Override
+            protected void onCreate(Bundle savedInstanceState) {
+                // Be sure to call the super class.
+                super.onCreate(savedInstanceState);
+
+                // Inflate the layout.
+                setContentView(R.layout.sample_media_router_presentation);
+
+                // Set up the surface view.
+                mPresentationSurfaceView = (SurfaceView)findViewById(R.id.surface_view);
+                SurfaceHolder holder = mPresentationSurfaceView.getHolder();
+                holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+                holder.addCallback(SurfaceViewPlayer.this);
+                Log.i(TAG, "Presentation created");
+            }
+
+            public void updateSize(int width, int height) {
+                int surfaceHeight = getWindow().getDecorView().getHeight();
+                int surfaceWidth = getWindow().getDecorView().getWidth();
+                ViewGroup.LayoutParams lp = mPresentationSurfaceView.getLayoutParams();
+                if (surfaceWidth * height < surfaceHeight * width) {
+                    lp.width = surfaceWidth;
+                    lp.height = surfaceWidth * height / width;
+                } else {
+                    lp.width = surfaceHeight * width / height;
+                    lp.height = surfaceHeight;
+                }
+                Log.i(TAG, "Presentation video rect is " + lp.width + "x" + lp.height);
+                mPresentationSurfaceView.setLayoutParams(lp);
+            }
+        }
+    }
+
+    /**
+     * Handles playback of a single media item using MediaPlayer in
+     * OverlayDisplayWindow.
+     */
+    public static class OverlayPlayer extends LocalPlayer implements
+            OverlayDisplayWindow.OverlayWindowListener {
+        private static final String TAG = "OverlayPlayer";
+        private final OverlayDisplayWindow mOverlay;
+
+        public OverlayPlayer(Context context) {
+            super(context);
+
+            mOverlay = OverlayDisplayWindow.create(getContext(),
+                    getContext().getResources().getString(
+                            R.string.sample_media_route_provider_remote),
+                    1024, 768, Gravity.CENTER);
+
+            mOverlay.setOverlayWindowListener(this);
+        }
+
+        @Override
+        public void connect(RouteInfo route) {
+            super.connect(route);
+            mOverlay.show();
+        }
+
+        @Override
+        public void release() {
+            super.release();
+            mOverlay.dismiss();
+        }
+
+        @Override
+        protected void updateSize() {
+            int width = getVideoWidth();
+            int height = getVideoHeight();
+            if (width > 0 && height > 0) {
+                mOverlay.updateAspectRatio(width, height);
+            }
+        }
+
+        // OverlayDisplayWindow.OverlayWindowListener
+        @Override
+        public void onWindowCreated(Surface surface) {
+            setSurface(surface);
+        }
+
+        @Override
+        public void onWindowCreated(SurfaceHolder surfaceHolder) {
+            setSurface(surfaceHolder);
+        }
+
+        @Override
+        public void onWindowDestroyed() {
+            setSurface((SurfaceHolder)null);
+        }
+
+        @Override
+        public Bitmap getSnapshot() {
+            return mOverlay.getSnapshot();
+        }
+    }
+}
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/OverlayDisplayWindow.java b/samples/Support7Demos/src/com/example/android/supportv7/media/OverlayDisplayWindow.java
new file mode 100644
index 0000000..80cc77d
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/OverlayDisplayWindow.java
@@ -0,0 +1,476 @@
+/*
+ * Copyright (C) 2012 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 com.example.android.supportv7.media;
+import com.example.android.supportv7.R;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.graphics.SurfaceTexture;
+import android.hardware.display.DisplayManager;
+import android.os.Build;
+import android.util.Log;
+import android.view.Display;
+import android.util.DisplayMetrics;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.View;
+import android.view.Surface;
+import android.view.WindowManager;
+import android.view.TextureView.SurfaceTextureListener;
+import android.widget.TextView;
+
+/**
+ * Manages an overlay display window, used for simulating remote playback.
+ */
+public abstract class OverlayDisplayWindow {
+    private static final String TAG = "OverlayDisplayWindow";
+    private static final boolean DEBUG = false;
+
+    private static final float WINDOW_ALPHA = 0.8f;
+    private static final float INITIAL_SCALE = 0.5f;
+    private static final float MIN_SCALE = 0.3f;
+    private static final float MAX_SCALE = 1.0f;
+
+    protected final Context mContext;
+    protected final String mName;
+    protected final int mWidth;
+    protected final int mHeight;
+    protected final int mGravity;
+    protected OverlayWindowListener mListener;
+
+    protected OverlayDisplayWindow(Context context, String name,
+            int width, int height, int gravity) {
+        mContext = context;
+        mName = name;
+        mWidth = width;
+        mHeight = height;
+        mGravity = gravity;
+    }
+
+    public static OverlayDisplayWindow create(Context context, String name,
+            int width, int height, int gravity) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+            return new JellybeanMr1Impl(context, name, width, height, gravity);
+        } else {
+            return new LegacyImpl(context, name, width, height, gravity);
+        }
+    }
+
+    public void setOverlayWindowListener(OverlayWindowListener listener) {
+        mListener = listener;
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
+    public abstract void show();
+
+    public abstract void dismiss();
+
+    public abstract void updateAspectRatio(int width, int height);
+
+    public abstract Bitmap getSnapshot();
+
+    // Watches for significant changes in the overlay display window lifecycle.
+    public interface OverlayWindowListener {
+        public void onWindowCreated(Surface surface);
+        public void onWindowCreated(SurfaceHolder surfaceHolder);
+        public void onWindowDestroyed();
+    }
+
+    /**
+     * Implementation for older versions.
+     */
+    private static final class LegacyImpl extends OverlayDisplayWindow {
+        private final WindowManager mWindowManager;
+
+        private boolean mWindowVisible;
+        private SurfaceView mSurfaceView;
+
+        public LegacyImpl(Context context, String name,
+                int width, int height, int gravity) {
+            super(context, name, width, height, gravity);
+
+            mWindowManager = (WindowManager)context.getSystemService(
+                    Context.WINDOW_SERVICE);
+        }
+
+        @Override
+        public void show() {
+            if (!mWindowVisible) {
+                mSurfaceView = new SurfaceView(mContext);
+
+                Display display = mWindowManager.getDefaultDisplay();
+
+                WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+                        WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+                params.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+                        | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
+                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+                params.alpha = WINDOW_ALPHA;
+                params.gravity = Gravity.LEFT | Gravity.BOTTOM;
+                params.setTitle(mName);
+
+                int width = (int)(display.getWidth() * INITIAL_SCALE);
+                int height = (int)(display.getHeight() * INITIAL_SCALE);
+                if (mWidth > mHeight) {
+                    height = mHeight * width / mWidth;
+                } else {
+                    width = mWidth * height / mHeight;
+                }
+                params.width = width;
+                params.height = height;
+
+                mWindowManager.addView(mSurfaceView, params);
+                mWindowVisible = true;
+
+                SurfaceHolder holder = mSurfaceView.getHolder();
+                holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+                mListener.onWindowCreated(holder);
+            }
+        }
+
+        @Override
+        public void dismiss() {
+            if (mWindowVisible) {
+                mListener.onWindowDestroyed();
+
+                mWindowManager.removeView(mSurfaceView);
+                mWindowVisible = false;
+            }
+        }
+
+        @Override
+        public void updateAspectRatio(int width, int height) {
+        }
+
+        @Override
+        public Bitmap getSnapshot() {
+            return null;
+        }
+    }
+
+    /**
+     * Implementation for API version 17+.
+     */
+    private static final class JellybeanMr1Impl extends OverlayDisplayWindow {
+        // When true, disables support for moving and resizing the overlay.
+        // The window is made non-touchable, which makes it possible to
+        // directly interact with the content underneath.
+        private static final boolean DISABLE_MOVE_AND_RESIZE = false;
+
+        private final DisplayManager mDisplayManager;
+        private final WindowManager mWindowManager;
+
+        private final Display mDefaultDisplay;
+        private final DisplayMetrics mDefaultDisplayMetrics = new DisplayMetrics();
+
+        private View mWindowContent;
+        private WindowManager.LayoutParams mWindowParams;
+        private TextureView mTextureView;
+        private TextView mNameTextView;
+
+        private GestureDetector mGestureDetector;
+        private ScaleGestureDetector mScaleGestureDetector;
+
+        private boolean mWindowVisible;
+        private int mWindowX;
+        private int mWindowY;
+        private float mWindowScale;
+
+        private float mLiveTranslationX;
+        private float mLiveTranslationY;
+        private float mLiveScale = 1.0f;
+
+        public JellybeanMr1Impl(Context context, String name,
+                int width, int height, int gravity) {
+            super(context, name, width, height, gravity);
+
+            mDisplayManager = (DisplayManager)context.getSystemService(
+                    Context.DISPLAY_SERVICE);
+            mWindowManager = (WindowManager)context.getSystemService(
+                    Context.WINDOW_SERVICE);
+
+            mDefaultDisplay = mWindowManager.getDefaultDisplay();
+            updateDefaultDisplayInfo();
+
+            createWindow();
+        }
+
+        @Override
+        public void show() {
+            if (!mWindowVisible) {
+                mDisplayManager.registerDisplayListener(mDisplayListener, null);
+                if (!updateDefaultDisplayInfo()) {
+                    mDisplayManager.unregisterDisplayListener(mDisplayListener);
+                    return;
+                }
+
+                clearLiveState();
+                updateWindowParams();
+                mWindowManager.addView(mWindowContent, mWindowParams);
+                mWindowVisible = true;
+            }
+        }
+
+        @Override
+        public void dismiss() {
+            if (mWindowVisible) {
+                mDisplayManager.unregisterDisplayListener(mDisplayListener);
+                mWindowManager.removeView(mWindowContent);
+                mWindowVisible = false;
+            }
+        }
+
+        @Override
+        public void updateAspectRatio(int width, int height) {
+            if (mWidth * height < mHeight * width) {
+                mTextureView.getLayoutParams().width = mWidth;
+                mTextureView.getLayoutParams().height = mWidth * height / width;
+            } else {
+                mTextureView.getLayoutParams().width = mHeight * width / height;
+                mTextureView.getLayoutParams().height = mHeight;
+            }
+            relayout();
+        }
+
+        @Override
+        public Bitmap getSnapshot() {
+            return mTextureView.getBitmap();
+        }
+
+        private void relayout() {
+            if (mWindowVisible) {
+                updateWindowParams();
+                mWindowManager.updateViewLayout(mWindowContent, mWindowParams);
+            }
+        }
+
+        private boolean updateDefaultDisplayInfo() {
+            mDefaultDisplay.getMetrics(mDefaultDisplayMetrics);
+            return true;
+        }
+
+        private void createWindow() {
+            LayoutInflater inflater = LayoutInflater.from(mContext);
+
+            mWindowContent = inflater.inflate(
+                    R.layout.overlay_display_window, null);
+            mWindowContent.setOnTouchListener(mOnTouchListener);
+
+            mTextureView = (TextureView)mWindowContent.findViewById(
+                    R.id.overlay_display_window_texture);
+            mTextureView.setPivotX(0);
+            mTextureView.setPivotY(0);
+            mTextureView.getLayoutParams().width = mWidth;
+            mTextureView.getLayoutParams().height = mHeight;
+            mTextureView.setOpaque(false);
+            mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
+
+            mNameTextView = (TextView)mWindowContent.findViewById(
+                    R.id.overlay_display_window_title);
+            mNameTextView.setText(mName);
+
+            mWindowParams = new WindowManager.LayoutParams(
+                    WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+            mWindowParams.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+                    | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
+                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                    | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                    | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
+            if (DISABLE_MOVE_AND_RESIZE) {
+                mWindowParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+            }
+            mWindowParams.alpha = WINDOW_ALPHA;
+            mWindowParams.gravity = Gravity.TOP | Gravity.LEFT;
+            mWindowParams.setTitle(mName);
+
+            mGestureDetector = new GestureDetector(mContext, mOnGestureListener);
+            mScaleGestureDetector = new ScaleGestureDetector(mContext, mOnScaleGestureListener);
+
+            // Set the initial position and scale.
+            // The position and scale will be clamped when the display is first shown.
+            mWindowX = (mGravity & Gravity.LEFT) == Gravity.LEFT ?
+                    0 : mDefaultDisplayMetrics.widthPixels;
+            mWindowY = (mGravity & Gravity.TOP) == Gravity.TOP ?
+                    0 : mDefaultDisplayMetrics.heightPixels;
+            Log.d(TAG, mDefaultDisplayMetrics.toString());
+            mWindowScale = INITIAL_SCALE;
+
+            // calculate and save initial settings
+            updateWindowParams();
+            saveWindowParams();
+        }
+
+        private void updateWindowParams() {
+            float scale = mWindowScale * mLiveScale;
+            scale = Math.min(scale, (float)mDefaultDisplayMetrics.widthPixels / mWidth);
+            scale = Math.min(scale, (float)mDefaultDisplayMetrics.heightPixels / mHeight);
+            scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale));
+
+            float offsetScale = (scale / mWindowScale - 1.0f) * 0.5f;
+            int width = (int)(mWidth * scale);
+            int height = (int)(mHeight * scale);
+            int x = (int)(mWindowX + mLiveTranslationX - width * offsetScale);
+            int y = (int)(mWindowY + mLiveTranslationY - height * offsetScale);
+            x = Math.max(0, Math.min(x, mDefaultDisplayMetrics.widthPixels - width));
+            y = Math.max(0, Math.min(y, mDefaultDisplayMetrics.heightPixels - height));
+
+            if (DEBUG) {
+                Log.d(TAG, "updateWindowParams: scale=" + scale
+                        + ", offsetScale=" + offsetScale
+                        + ", x=" + x + ", y=" + y
+                        + ", width=" + width + ", height=" + height);
+            }
+
+            mTextureView.setScaleX(scale);
+            mTextureView.setScaleY(scale);
+
+            mTextureView.setTranslationX(
+                    (mWidth - mTextureView.getLayoutParams().width) * scale / 2);
+            mTextureView.setTranslationY(
+                    (mHeight - mTextureView.getLayoutParams().height) * scale / 2);
+
+            mWindowParams.x = x;
+            mWindowParams.y = y;
+            mWindowParams.width = width;
+            mWindowParams.height = height;
+        }
+
+        private void saveWindowParams() {
+            mWindowX = mWindowParams.x;
+            mWindowY = mWindowParams.y;
+            mWindowScale = mTextureView.getScaleX();
+            clearLiveState();
+        }
+
+        private void clearLiveState() {
+            mLiveTranslationX = 0f;
+            mLiveTranslationY = 0f;
+            mLiveScale = 1.0f;
+        }
+
+        private final DisplayManager.DisplayListener mDisplayListener =
+                new DisplayManager.DisplayListener() {
+            @Override
+            public void onDisplayAdded(int displayId) {
+            }
+
+            @Override
+            public void onDisplayChanged(int displayId) {
+                if (displayId == mDefaultDisplay.getDisplayId()) {
+                    if (updateDefaultDisplayInfo()) {
+                        relayout();
+                    } else {
+                        dismiss();
+                    }
+                }
+            }
+
+            @Override
+            public void onDisplayRemoved(int displayId) {
+                if (displayId == mDefaultDisplay.getDisplayId()) {
+                    dismiss();
+                }
+            }
+        };
+
+        private final SurfaceTextureListener mSurfaceTextureListener =
+                new SurfaceTextureListener() {
+            @Override
+            public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture,
+                    int width, int height) {
+                if (mListener != null) {
+                    mListener.onWindowCreated(new Surface(surfaceTexture));
+                }
+            }
+
+            @Override
+            public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+                if (mListener != null) {
+                    mListener.onWindowDestroyed();
+                }
+                return true;
+            }
+
+            @Override
+            public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture,
+                    int width, int height) {
+            }
+
+            @Override
+            public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
+            }
+        };
+
+        private final View.OnTouchListener mOnTouchListener = new View.OnTouchListener() {
+            @Override
+            public boolean onTouch(View view, MotionEvent event) {
+                // Work in screen coordinates.
+                final float oldX = event.getX();
+                final float oldY = event.getY();
+                event.setLocation(event.getRawX(), event.getRawY());
+
+                mGestureDetector.onTouchEvent(event);
+                mScaleGestureDetector.onTouchEvent(event);
+
+                switch (event.getActionMasked()) {
+                    case MotionEvent.ACTION_UP:
+                    case MotionEvent.ACTION_CANCEL:
+                        saveWindowParams();
+                        break;
+                }
+
+                // Revert to window coordinates.
+                event.setLocation(oldX, oldY);
+                return true;
+            }
+        };
+
+        private final GestureDetector.OnGestureListener mOnGestureListener =
+                new GestureDetector.SimpleOnGestureListener() {
+            @Override
+            public boolean onScroll(MotionEvent e1, MotionEvent e2,
+                    float distanceX, float distanceY) {
+                mLiveTranslationX -= distanceX;
+                mLiveTranslationY -= distanceY;
+                relayout();
+                return true;
+            }
+        };
+
+        private final ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener =
+                new ScaleGestureDetector.SimpleOnScaleGestureListener() {
+            @Override
+            public boolean onScale(ScaleGestureDetector detector) {
+                mLiveScale *= detector.getScaleFactor();
+                relayout();
+                return true;
+            }
+        };
+    }
+}
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/Player.java b/samples/Support7Demos/src/com/example/android/supportv7/media/Player.java
new file mode 100644
index 0000000..32b1285
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/Player.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.supportv7.media;
+
+import android.net.Uri;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaRouter.RouteInfo;
+
+/**
+ * Abstraction of common playback operations of media items, such as play,
+ * seek, etc. Used by PlaybackManager as a backend to handle actual playback
+ * of media items.
+ */
+public abstract class Player {
+    protected Callback mCallback;
+
+    public abstract boolean isRemotePlayback();
+    public abstract boolean isQueuingSupported();
+
+    public abstract void connect(RouteInfo route);
+    public abstract void release();
+
+    // basic operations that are always supported
+    public abstract void play(final PlaylistItem item);
+    public abstract void seek(final PlaylistItem item);
+    public abstract void getStatus(final PlaylistItem item, final boolean update);
+    public abstract void pause();
+    public abstract void resume();
+    public abstract void stop();
+
+    // advanced queuing (enqueue & remove) are only supported
+    // if isQueuingSupported() returns true
+    public abstract void enqueue(final PlaylistItem item);
+    public abstract PlaylistItem remove(String iid);
+
+    // track info for current media item
+    public void updateTrackInfo() {}
+    public String getDescription() { return ""; }
+    public Bitmap getSnapshot() { return null; }
+
+    // presentation display
+    public void updatePresentation() {}
+
+    public void setCallback(Callback callback) {
+        mCallback = callback;
+    }
+
+    public static Player create(Context context, RouteInfo route) {
+        Player player;
+        if (route != null && route.supportsControlCategory(
+                MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
+            player = new RemotePlayer(context);
+        } else if (route != null) {
+            player = new LocalPlayer.SurfaceViewPlayer(context);
+        } else {
+            player = new LocalPlayer.OverlayPlayer(context);
+        }
+        player.connect(route);
+        return player;
+    }
+
+    public interface Callback {
+        void onError();
+        void onCompletion();
+        void onPlaylistChanged();
+        void onPlaylistReady();
+    }
+}
\ No newline at end of file
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/PlaylistItem.java b/samples/Support7Demos/src/com/example/android/supportv7/media/PlaylistItem.java
new file mode 100644
index 0000000..9a12d59
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/PlaylistItem.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.supportv7.media;
+
+import android.app.PendingIntent;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.support.v7.media.MediaItemStatus;
+
+/**
+ * PlaylistItem helps keep track of the current status of an media item.
+ */
+final class PlaylistItem {
+    // immutables
+    private final String mSessionId;
+    private final String mItemId;
+    private final Uri mUri;
+    private final String mMime;
+    private final PendingIntent mUpdateReceiver;
+    // changeable states
+    private int mPlaybackState = MediaItemStatus.PLAYBACK_STATE_PENDING;
+    private long mContentPosition;
+    private long mContentDuration;
+    private long mTimestamp;
+    private String mRemoteItemId;
+
+    public PlaylistItem(String qid, String iid, Uri uri, String mime, PendingIntent pi) {
+        mSessionId = qid;
+        mItemId = iid;
+        mUri = uri;
+        mMime = mime;
+        mUpdateReceiver = pi;
+        setTimestamp(SystemClock.elapsedRealtime());
+    }
+
+    public void setRemoteItemId(String riid) {
+        mRemoteItemId = riid;
+    }
+
+    public void setState(int state) {
+        mPlaybackState = state;
+    }
+
+    public void setPosition(long pos) {
+        mContentPosition = pos;
+    }
+
+    public void setTimestamp(long ts) {
+        mTimestamp = ts;
+    }
+
+    public void setDuration(long duration) {
+        mContentDuration = duration;
+    }
+
+    public String getSessionId() {
+        return mSessionId;
+    }
+
+    public String getItemId() {
+        return mItemId;
+    }
+
+    public String getRemoteItemId() {
+        return mRemoteItemId;
+    }
+
+    public Uri getUri() {
+        return mUri;
+    }
+
+    public PendingIntent getUpdateReceiver() {
+        return mUpdateReceiver;
+    }
+
+    public int getState() {
+        return mPlaybackState;
+    }
+
+    public long getPosition() {
+        return mContentPosition;
+    }
+
+    public long getDuration() {
+        return mContentDuration;
+    }
+
+    public long getTimestamp() {
+        return mTimestamp;
+    }
+
+    public MediaItemStatus getStatus() {
+        return new MediaItemStatus.Builder(mPlaybackState)
+            .setContentPosition(mContentPosition)
+            .setContentDuration(mContentDuration)
+            .setTimestamp(mTimestamp)
+            .build();
+    }
+
+    @Override
+    public String toString() {
+        String state[] = {
+            "PENDING",
+            "PLAYING",
+            "PAUSED",
+            "BUFFERING",
+            "FINISHED",
+            "CANCELED",
+            "INVALIDATED",
+            "ERROR"
+        };
+        return "[" + mSessionId + "|" + mItemId + "|"
+            + (mRemoteItemId != null ? mRemoteItemId : "-") + "|"
+            + state[mPlaybackState] + "] " + mUri.toString();
+    }
+}
\ No newline at end of file
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/RemotePlayer.java b/samples/Support7Demos/src/com/example/android/supportv7/media/RemotePlayer.java
new file mode 100644
index 0000000..5020c37
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/RemotePlayer.java
@@ -0,0 +1,488 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.supportv7.media;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v7.media.MediaItemStatus;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaRouter.ControlRequestCallback;
+import android.support.v7.media.MediaRouter.RouteInfo;
+import android.support.v7.media.MediaSessionStatus;
+import android.support.v7.media.RemotePlaybackClient;
+import android.support.v7.media.RemotePlaybackClient.ItemActionCallback;
+import android.support.v7.media.RemotePlaybackClient.SessionActionCallback;
+import android.support.v7.media.RemotePlaybackClient.StatusCallback;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Handles playback of media items using a remote route.
+ *
+ * This class is used as a backend by PlaybackManager to feed media items to
+ * the remote route. When the remote route doesn't support queuing, media items
+ * are fed one-at-a-time; otherwise media items are enqueued to the remote side.
+ */
+public class RemotePlayer extends Player {
+    private static final String TAG = "RemotePlayer";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    private Context mContext;
+    private RouteInfo mRoute;
+    private boolean mEnqueuePending;
+    private String mTrackInfo = "";
+    private Bitmap mSnapshot;
+    private List<PlaylistItem> mTempQueue = new ArrayList<PlaylistItem>();
+
+    private RemotePlaybackClient mClient;
+    private StatusCallback mStatusCallback = new StatusCallback() {
+        @Override
+        public void onItemStatusChanged(Bundle data,
+                String sessionId, MediaSessionStatus sessionStatus,
+                String itemId, MediaItemStatus itemStatus) {
+            logStatus("onItemStatusChanged", sessionId, sessionStatus, itemId, itemStatus);
+            if (mCallback != null) {
+                if (itemStatus.getPlaybackState() ==
+                        MediaItemStatus.PLAYBACK_STATE_FINISHED) {
+                    mCallback.onCompletion();
+                } else if (itemStatus.getPlaybackState() ==
+                        MediaItemStatus.PLAYBACK_STATE_ERROR) {
+                    mCallback.onError();
+                }
+            }
+        }
+
+        @Override
+        public void onSessionStatusChanged(Bundle data,
+                String sessionId, MediaSessionStatus sessionStatus) {
+            logStatus("onSessionStatusChanged", sessionId, sessionStatus, null, null);
+            if (mCallback != null) {
+                mCallback.onPlaylistChanged();
+            }
+        }
+
+        @Override
+        public void onSessionChanged(String sessionId) {
+            if (DEBUG) {
+                Log.d(TAG, "onSessionChanged: sessionId=" + sessionId);
+            }
+        }
+    };
+
+    public RemotePlayer(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    public boolean isRemotePlayback() {
+        return true;
+    }
+
+    @Override
+    public boolean isQueuingSupported() {
+        return mClient.isQueuingSupported();
+    }
+
+    @Override
+    public void connect(RouteInfo route) {
+        mRoute = route;
+        mClient = new RemotePlaybackClient(mContext, route);
+        mClient.setStatusCallback(mStatusCallback);
+
+        if (DEBUG) {
+            Log.d(TAG, "connected to: " + route
+                    + ", isRemotePlaybackSupported: " + mClient.isRemotePlaybackSupported()
+                    + ", isQueuingSupported: "+ mClient.isQueuingSupported());
+        }
+    }
+
+    @Override
+    public void release() {
+        mClient.release();
+
+        if (DEBUG) {
+            Log.d(TAG, "released.");
+        }
+    }
+
+    // basic playback operations that are always supported
+    @Override
+    public void play(final PlaylistItem item) {
+        if (DEBUG) {
+            Log.d(TAG, "play: item=" + item);
+        }
+        mClient.play(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() {
+            @Override
+            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
+                    String itemId, MediaItemStatus itemStatus) {
+                logStatus("play: succeeded", sessionId, sessionStatus, itemId, itemStatus);
+                item.setRemoteItemId(itemId);
+                if (item.getPosition() > 0) {
+                    seekInternal(item);
+                }
+                if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
+                    pause();
+                }
+                if (mCallback != null) {
+                    mCallback.onPlaylistChanged();
+                }
+            }
+
+            @Override
+            public void onError(String error, int code, Bundle data) {
+                logError("play: failed", error, code);
+            }
+        });
+    }
+
+    @Override
+    public void seek(final PlaylistItem item) {
+        seekInternal(item);
+    }
+
+    @Override
+    public void getStatus(final PlaylistItem item, final boolean update) {
+        if (!mClient.hasSession() || item.getRemoteItemId() == null) {
+            // if session is not valid or item id not assigend yet.
+            // just return, it's not fatal
+            return;
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "getStatus: item=" + item + ", update=" + update);
+        }
+        mClient.getStatus(item.getRemoteItemId(), null, new ItemActionCallback() {
+            @Override
+            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
+                    String itemId, MediaItemStatus itemStatus) {
+                logStatus("getStatus: succeeded", sessionId, sessionStatus, itemId, itemStatus);
+                int state = itemStatus.getPlaybackState();
+                if (state == MediaItemStatus.PLAYBACK_STATE_PLAYING
+                        || state == MediaItemStatus.PLAYBACK_STATE_PAUSED
+                        || state == MediaItemStatus.PLAYBACK_STATE_PENDING) {
+                    item.setState(state);
+                    item.setPosition(itemStatus.getContentPosition());
+                    item.setDuration(itemStatus.getContentDuration());
+                    item.setTimestamp(itemStatus.getTimestamp());
+                }
+                if (update && mCallback != null) {
+                    mCallback.onPlaylistReady();
+                }
+            }
+
+            @Override
+            public void onError(String error, int code, Bundle data) {
+                logError("getStatus: failed", error, code);
+                if (update && mCallback != null) {
+                    mCallback.onPlaylistReady();
+                }
+            }
+        });
+    }
+
+    @Override
+    public void pause() {
+        if (!mClient.hasSession()) {
+            // ignore if no session
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "pause");
+        }
+        mClient.pause(null, new SessionActionCallback() {
+            @Override
+            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
+                logStatus("pause: succeeded", sessionId, sessionStatus, null, null);
+                if (mCallback != null) {
+                    mCallback.onPlaylistChanged();
+                }
+            }
+
+            @Override
+            public void onError(String error, int code, Bundle data) {
+                logError("pause: failed", error, code);
+            }
+        });
+    }
+
+    @Override
+    public void resume() {
+        if (!mClient.hasSession()) {
+            // ignore if no session
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "resume");
+        }
+        mClient.resume(null, new SessionActionCallback() {
+            @Override
+            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
+                logStatus("resume: succeeded", sessionId, sessionStatus, null, null);
+                if (mCallback != null) {
+                    mCallback.onPlaylistChanged();
+                }
+            }
+
+            @Override
+            public void onError(String error, int code, Bundle data) {
+                logError("resume: failed", error, code);
+            }
+        });
+    }
+
+    @Override
+    public void stop() {
+        if (!mClient.hasSession()) {
+            // ignore if no session
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "stop");
+        }
+        mClient.stop(null, new SessionActionCallback() {
+            @Override
+            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
+                logStatus("stop: succeeded", sessionId, sessionStatus, null, null);
+                if (mClient.isSessionManagementSupported()) {
+                    endSession();
+                }
+                if (mCallback != null) {
+                    mCallback.onPlaylistChanged();
+                }
+            }
+
+            @Override
+            public void onError(String error, int code, Bundle data) {
+                logError("stop: failed", error, code);
+            }
+        });
+    }
+
+    // enqueue & remove are only supported if isQueuingSupported() returns true
+    @Override
+    public void enqueue(final PlaylistItem item) {
+        throwIfQueuingUnsupported();
+
+        if (!mClient.hasSession() && !mEnqueuePending) {
+            mEnqueuePending = true;
+            if (mClient.isSessionManagementSupported()) {
+                startSession(item);
+            } else {
+                enqueueInternal(item);
+            }
+        } else if (mEnqueuePending){
+            mTempQueue.add(item);
+        } else {
+            enqueueInternal(item);
+        }
+    }
+
+    @Override
+    public PlaylistItem remove(String itemId) {
+        throwIfNoSession();
+        throwIfQueuingUnsupported();
+
+        if (DEBUG) {
+            Log.d(TAG, "remove: itemId=" + itemId);
+        }
+        mClient.remove(itemId, null, new ItemActionCallback() {
+            @Override
+            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
+                    String itemId, MediaItemStatus itemStatus) {
+                logStatus("remove: succeeded", sessionId, sessionStatus, itemId, itemStatus);
+            }
+
+            @Override
+            public void onError(String error, int code, Bundle data) {
+                logError("remove: failed", error, code);
+            }
+        });
+
+        return null;
+    }
+
+    @Override
+    public void updateTrackInfo() {
+        // clear stats info first
+        mTrackInfo = "";
+        mSnapshot = null;
+
+        Intent intent = new Intent(SampleMediaRouteProvider.ACTION_GET_TRACK_INFO);
+        intent.addCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE);
+
+        if (mRoute != null && mRoute.supportsControlRequest(intent)) {
+            ControlRequestCallback callback = new ControlRequestCallback() {
+                @Override
+                public void onResult(Bundle data) {
+                    if (DEBUG) {
+                        Log.d(TAG, "getStatistics: succeeded: data=" + data);
+                    }
+                    if (data != null) {
+                        mTrackInfo = data.getString(SampleMediaRouteProvider.TRACK_INFO_DESC);
+                        mSnapshot = data.getParcelable(
+                                SampleMediaRouteProvider.TRACK_INFO_SNAPSHOT);
+                    }
+                }
+
+                @Override
+                public void onError(String error, Bundle data) {
+                    Log.d(TAG, "getStatistics: failed: error=" + error + ", data=" + data);
+                }
+            };
+
+            mRoute.sendControlRequest(intent, callback);
+        }
+    }
+
+    @Override
+    public String getDescription() {
+        return mTrackInfo;
+    }
+
+    @Override
+    public Bitmap getSnapshot() {
+        return mSnapshot;
+    }
+
+    private void enqueueInternal(final PlaylistItem item) {
+        throwIfQueuingUnsupported();
+
+        if (DEBUG) {
+            Log.d(TAG, "enqueue: item=" + item);
+        }
+        mClient.enqueue(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() {
+            @Override
+            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
+                    String itemId, MediaItemStatus itemStatus) {
+                logStatus("enqueue: succeeded", sessionId, sessionStatus, itemId, itemStatus);
+                item.setRemoteItemId(itemId);
+                if (item.getPosition() > 0) {
+                    seekInternal(item);
+                }
+                if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
+                    pause();
+                }
+                if (mEnqueuePending) {
+                    mEnqueuePending = false;
+                    for (PlaylistItem item : mTempQueue) {
+                        enqueueInternal(item);
+                    }
+                    mTempQueue.clear();
+                }
+                if (mCallback != null) {
+                    mCallback.onPlaylistChanged();
+                }
+            }
+
+            @Override
+            public void onError(String error, int code, Bundle data) {
+                logError("enqueue: failed", error, code);
+                if (mCallback != null) {
+                    mCallback.onPlaylistChanged();
+                }
+            }
+        });
+    }
+
+    private void seekInternal(final PlaylistItem item) {
+        throwIfNoSession();
+
+        if (DEBUG) {
+            Log.d(TAG, "seek: item=" + item);
+        }
+        mClient.seek(item.getRemoteItemId(), item.getPosition(), null, new ItemActionCallback() {
+           @Override
+           public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
+                   String itemId, MediaItemStatus itemStatus) {
+               logStatus("seek: succeeded", sessionId, sessionStatus, itemId, itemStatus);
+               if (mCallback != null) {
+                   mCallback.onPlaylistChanged();
+               }
+           }
+
+           @Override
+           public void onError(String error, int code, Bundle data) {
+               logError("seek: failed", error, code);
+           }
+        });
+    }
+
+    private void startSession(final PlaylistItem item) {
+        mClient.startSession(null, new SessionActionCallback() {
+            @Override
+            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
+                logStatus("startSession: succeeded", sessionId, sessionStatus, null, null);
+                enqueueInternal(item);
+            }
+
+            @Override
+            public void onError(String error, int code, Bundle data) {
+                logError("startSession: failed", error, code);
+            }
+        });
+    }
+
+    private void endSession() {
+        mClient.endSession(null, new SessionActionCallback() {
+            @Override
+            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
+                logStatus("endSession: succeeded", sessionId, sessionStatus, null, null);
+            }
+
+            @Override
+            public void onError(String error, int code, Bundle data) {
+                logError("endSession: failed", error, code);
+            }
+        });
+    }
+
+    private void logStatus(String message,
+            String sessionId, MediaSessionStatus sessionStatus,
+            String itemId, MediaItemStatus itemStatus) {
+        if (DEBUG) {
+            String result = "";
+            if (sessionId != null && sessionStatus != null) {
+                result += "sessionId=" + sessionId + ", sessionStatus=" + sessionStatus;
+            }
+            if (itemId != null & itemStatus != null) {
+                result += (result.isEmpty() ? "" : ", ")
+                        + "itemId=" + itemId + ", itemStatus=" + itemStatus;
+            }
+            Log.d(TAG, message + ": " + result);
+        }
+    }
+
+    private void logError(String message, String error, int code) {
+        Log.d(TAG, message + ": error=" + error + ", code=" + code);
+    }
+
+    private void throwIfNoSession() {
+        if (!mClient.hasSession()) {
+            throw new IllegalStateException("Session is invalid");
+        }
+    }
+
+    private void throwIfQueuingUnsupported() {
+        if (!isQueuingSupported()) {
+            throw new UnsupportedOperationException("Queuing is unsupported");
+        }
+    }
+}
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaButtonReceiver.java b/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaButtonReceiver.java
new file mode 100644
index 0000000..c2eec8e
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaButtonReceiver.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.supportv7.media;
+
+import com.example.android.supportv7.R;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.BroadcastReceiver;
+import android.util.Log;
+import android.view.KeyEvent;
+
+/**
+ * Broadcast receiver for handling ACTION_MEDIA_BUTTON.
+ *
+ * This is needed to create the RemoteControlClient for controlling
+ * remote route volume in lock screen. It routes media key events back
+ * to main app activity SampleMediaRouterActivity.
+ */
+public class SampleMediaButtonReceiver extends BroadcastReceiver {
+    private static final String TAG = "SampleMediaButtonReceiver";
+    private static SampleMediaRouterActivity mActivity;
+
+    public static void setActivity(SampleMediaRouterActivity activity) {
+        mActivity = activity;
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (mActivity != null && Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
+            mActivity.handleMediaKey(
+                    (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT));
+        }
+    }
+}
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouteControllerDialog.java b/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouteControllerDialog.java
new file mode 100644
index 0000000..a2cacc3
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouteControllerDialog.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.supportv7.media;
+
+import com.example.android.supportv7.R;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.support.v7.app.MediaRouteControllerDialog;
+import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.media.MediaRouter;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * This class serves as an example on how to customize the media router control
+ * dialog. It is derived from the standard MediaRouteControllerDialog with the
+ * following overrides:
+ *
+ *   1. Shows thumbnail/snapshot of the current item
+ *
+ *   2. For variable volume routes, only allow volume control via Volume Up/Down
+ *      keys (to prevent accidental tapping on the volume adjust seekbar that sets
+ *      volume to maximum)
+ *
+ *   3. Provides transport control buttons (play/pause, stop)
+ */
+public class SampleMediaRouteControllerDialog extends MediaRouteControllerDialog {
+    private static final String TAG = "SampleMediaRouteControllerDialog";
+    private final SampleMediaRouterActivity mActivity;
+    private final SessionManager mSessionManager;
+    private final Player mPlayer;
+    private ImageButton mPauseResumeButton;
+    private ImageButton mStopButton;
+    private ImageView mThumbnail;
+    private TextView mTextView;
+    private LinearLayout mInfoLayout;
+    private LinearLayout mVolumeLayout;
+
+    public SampleMediaRouteControllerDialog(Context context,
+            SessionManager manager, Player player) {
+        super(context);
+        mActivity = (SampleMediaRouterActivity) context;
+        mSessionManager = manager;
+        mPlayer = player;
+    }
+
+    @Override
+    public View onCreateMediaControlView(Bundle savedInstanceState) {
+        // Thumbnail and Track info
+        View v = getLayoutInflater().inflate(R.layout.sample_media_controller, null);
+        mInfoLayout = (LinearLayout)v.findViewById(R.id.media_route_info);
+        mTextView = (TextView)v.findViewById(R.id.track_info);
+        mThumbnail = (ImageView)v.findViewById(R.id.snapshot);
+
+        // Transport controls
+        mPauseResumeButton = (ImageButton)v.findViewById(R.id.pause_resume_button);
+        mPauseResumeButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (mActivity != null) {
+                    mActivity.handleMediaKey(new KeyEvent(KeyEvent.ACTION_DOWN,
+                        KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
+                }
+            }
+        });
+
+        mStopButton = (ImageButton)v.findViewById(R.id.stop_button);
+        mStopButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (mActivity != null) {
+                    mActivity.handleMediaKey(new KeyEvent(KeyEvent.ACTION_DOWN,
+                        KeyEvent.KEYCODE_MEDIA_STOP));
+                }
+            }
+        });
+
+        // update session status (will callback to updateUi at the end)
+        mSessionManager.updateStatus();
+        return v;
+    }
+
+    public void updateUi() {
+        String trackInfo = mPlayer.getDescription();
+        Bitmap snapshot = mPlayer.getSnapshot();
+        if (mPlayer.isRemotePlayback() && !trackInfo.isEmpty() && snapshot != null) {
+            mInfoLayout.setVisibility(View.VISIBLE);
+            mThumbnail.setImageBitmap(snapshot);
+            mTextView.setText(trackInfo);
+        } else {
+            mInfoLayout.setVisibility(View.GONE);
+        }
+        // show pause or resume icon depending on current state
+        mPauseResumeButton.setImageResource(mSessionManager.isPaused() ?
+                R.drawable.ic_media_play : R.drawable.ic_media_pause);
+    }
+}
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouteProvider.java b/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouteProvider.java
index 0d5c21d..8a20564 100644
--- a/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouteProvider.java
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouteProvider.java
@@ -23,20 +23,25 @@
 import android.content.IntentFilter;
 import android.content.IntentFilter.MalformedMimeTypeException;
 import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.media.AudioManager;
 import android.media.MediaRouter;
 import android.net.Uri;
 import android.os.Bundle;
+import android.app.PendingIntent;
 import android.support.v7.media.MediaControlIntent;
 import android.support.v7.media.MediaItemStatus;
 import android.support.v7.media.MediaRouteProvider;
 import android.support.v7.media.MediaRouter.ControlRequestCallback;
 import android.support.v7.media.MediaRouteProviderDescriptor;
 import android.support.v7.media.MediaRouteDescriptor;
+import android.support.v7.media.MediaSessionStatus;
 import android.util.Log;
-import android.widget.Toast;
+import android.view.Gravity;
+import android.view.Surface;
+import android.view.SurfaceHolder;
 
 import java.util.ArrayList;
-import java.util.UUID;
 
 /**
  * Demonstrates how to create a custom media route provider.
@@ -47,7 +52,9 @@
     private static final String TAG = "SampleMediaRouteProvider";
 
     private static final String FIXED_VOLUME_ROUTE_ID = "fixed";
-    private static final String VARIABLE_VOLUME_ROUTE_ID = "variable";
+    private static final String VARIABLE_VOLUME_BASIC_ROUTE_ID = "variable_basic";
+    private static final String VARIABLE_VOLUME_QUEUING_ROUTE_ID = "variable_queuing";
+    private static final String VARIABLE_VOLUME_SESSION_ROUTE_ID = "variable_session";
     private static final int VOLUME_MAX = 10;
 
     /**
@@ -60,39 +67,86 @@
     /**
      * A custom media control intent action for special requests that are
      * supported by this provider's routes.
-     * <p>
-     * This particular request is designed to return a bundle of not very
-     * interesting statistics for demonstration purposes.
      * </p>
      *
-     * @see #DATA_PLAYBACK_COUNT
+     * @see #TRACK_INFO_DESC
+     * @see #TRACK_INFO_SNAPSHOT
      */
-    public static final String ACTION_GET_STATISTICS =
-            "com.example.android.supportv7.media.ACTION_GET_STATISTICS";
+    public static final String ACTION_GET_TRACK_INFO =
+            "com.example.android.supportv7.media.ACTION_GET_TRACK_INFO";
 
     /**
-     * {@link #ACTION_GET_STATISTICS} result data: Number of times the
-     * playback action was invoked.
+     * {@link #ACTION_GET_TRACK_INFO} result data: a string of information about
+     * the currently playing media item
      */
-    public static final String DATA_PLAYBACK_COUNT =
-            "com.example.android.supportv7.media.EXTRA_PLAYBACK_COUNT";
+    public static final String TRACK_INFO_DESC =
+            "com.example.android.supportv7.media.EXTRA_TRACK_INFO_DESC";
 
-    private static final ArrayList<IntentFilter> CONTROL_FILTERS;
+    /**
+     * {@link #ACTION_GET_TRACK_INFO} result data: a bitmap containing a snapshot
+     * of the currently playing media item
+     */
+    public static final String TRACK_INFO_SNAPSHOT =
+            "com.example.android.supportv7.media.EXTRA_TRACK_INFO_SNAPSHOT";
+
+    private static final ArrayList<IntentFilter> CONTROL_FILTERS_BASIC;
+    private static final ArrayList<IntentFilter> CONTROL_FILTERS_QUEUING;
+    private static final ArrayList<IntentFilter> CONTROL_FILTERS_SESSION;
+
     static {
         IntentFilter f1 = new IntentFilter();
         f1.addCategory(CATEGORY_SAMPLE_ROUTE);
-        f1.addAction(ACTION_GET_STATISTICS);
+        f1.addAction(ACTION_GET_TRACK_INFO);
 
         IntentFilter f2 = new IntentFilter();
         f2.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
         f2.addAction(MediaControlIntent.ACTION_PLAY);
         f2.addDataScheme("http");
         f2.addDataScheme("https");
+        f2.addDataScheme("rtsp");
+        f2.addDataScheme("file");
         addDataTypeUnchecked(f2, "video/*");
 
-        CONTROL_FILTERS = new ArrayList<IntentFilter>();
-        CONTROL_FILTERS.add(f1);
-        CONTROL_FILTERS.add(f2);
+        IntentFilter f3 = new IntentFilter();
+        f3.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+        f3.addAction(MediaControlIntent.ACTION_SEEK);
+        f3.addAction(MediaControlIntent.ACTION_GET_STATUS);
+        f3.addAction(MediaControlIntent.ACTION_PAUSE);
+        f3.addAction(MediaControlIntent.ACTION_RESUME);
+        f3.addAction(MediaControlIntent.ACTION_STOP);
+
+        IntentFilter f4 = new IntentFilter();
+        f4.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+        f4.addAction(MediaControlIntent.ACTION_ENQUEUE);
+        f4.addDataScheme("http");
+        f4.addDataScheme("https");
+        f4.addDataScheme("rtsp");
+        f4.addDataScheme("file");
+        addDataTypeUnchecked(f4, "video/*");
+
+        IntentFilter f5 = new IntentFilter();
+        f5.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+        f5.addAction(MediaControlIntent.ACTION_REMOVE);
+
+        IntentFilter f6 = new IntentFilter();
+        f6.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+        f6.addAction(MediaControlIntent.ACTION_START_SESSION);
+        f6.addAction(MediaControlIntent.ACTION_GET_SESSION_STATUS);
+        f6.addAction(MediaControlIntent.ACTION_END_SESSION);
+
+        CONTROL_FILTERS_BASIC = new ArrayList<IntentFilter>();
+        CONTROL_FILTERS_BASIC.add(f1);
+        CONTROL_FILTERS_BASIC.add(f2);
+        CONTROL_FILTERS_BASIC.add(f3);
+
+        CONTROL_FILTERS_QUEUING =
+                new ArrayList<IntentFilter>(CONTROL_FILTERS_BASIC);
+        CONTROL_FILTERS_QUEUING.add(f4);
+        CONTROL_FILTERS_QUEUING.add(f5);
+
+        CONTROL_FILTERS_SESSION =
+                new ArrayList<IntentFilter>(CONTROL_FILTERS_QUEUING);
+        CONTROL_FILTERS_SESSION.add(f6);
     }
 
     private static void addDataTypeUnchecked(IntentFilter filter, String type) {
@@ -104,7 +158,7 @@
     }
 
     private int mVolume = 5;
-    private int mPlaybackCount;
+    private int mEnqueueCount;
 
     public SampleMediaRouteProvider(Context context) {
         super(context);
@@ -124,17 +178,43 @@
                 FIXED_VOLUME_ROUTE_ID,
                 r.getString(R.string.fixed_volume_route_name))
                 .setDescription(r.getString(R.string.sample_route_description))
-                .addControlFilters(CONTROL_FILTERS)
+                .addControlFilters(CONTROL_FILTERS_BASIC)
+                .setPlaybackStream(AudioManager.STREAM_MUSIC)
                 .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
                 .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED)
                 .setVolume(VOLUME_MAX)
                 .build();
 
         MediaRouteDescriptor routeDescriptor2 = new MediaRouteDescriptor.Builder(
-                VARIABLE_VOLUME_ROUTE_ID,
-                r.getString(R.string.variable_volume_route_name))
+                VARIABLE_VOLUME_BASIC_ROUTE_ID,
+                r.getString(R.string.variable_volume_basic_route_name))
                 .setDescription(r.getString(R.string.sample_route_description))
-                .addControlFilters(CONTROL_FILTERS)
+                .addControlFilters(CONTROL_FILTERS_BASIC)
+                .setPlaybackStream(AudioManager.STREAM_MUSIC)
+                .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
+                .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE)
+                .setVolumeMax(VOLUME_MAX)
+                .setVolume(mVolume)
+                .build();
+
+        MediaRouteDescriptor routeDescriptor3 = new MediaRouteDescriptor.Builder(
+                VARIABLE_VOLUME_QUEUING_ROUTE_ID,
+                r.getString(R.string.variable_volume_queuing_route_name))
+                .setDescription(r.getString(R.string.sample_route_description))
+                .addControlFilters(CONTROL_FILTERS_QUEUING)
+                .setPlaybackStream(AudioManager.STREAM_MUSIC)
+                .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
+                .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE)
+                .setVolumeMax(VOLUME_MAX)
+                .setVolume(mVolume)
+                .build();
+
+        MediaRouteDescriptor routeDescriptor4 = new MediaRouteDescriptor.Builder(
+                VARIABLE_VOLUME_SESSION_ROUTE_ID,
+                r.getString(R.string.variable_volume_session_route_name))
+                .setDescription(r.getString(R.string.sample_route_description))
+                .addControlFilters(CONTROL_FILTERS_SESSION)
+                .setPlaybackStream(AudioManager.STREAM_MUSIC)
                 .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
                 .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE)
                 .setVolumeMax(VOLUME_MAX)
@@ -145,41 +225,58 @@
                 new MediaRouteProviderDescriptor.Builder()
                 .addRoute(routeDescriptor1)
                 .addRoute(routeDescriptor2)
+                .addRoute(routeDescriptor3)
+                .addRoute(routeDescriptor4)
                 .build();
         setDescriptor(providerDescriptor);
     }
 
-    private String generateStreamId() {
-        return UUID.randomUUID().toString();
-    }
-
     private final class SampleRouteController extends MediaRouteProvider.RouteController {
         private final String mRouteId;
+        private final SessionManager mSessionManager = new SessionManager("mrp");
+        private final Player mPlayer;
+        private PendingIntent mSessionReceiver;
 
         public SampleRouteController(String routeId) {
             mRouteId = routeId;
+            mPlayer = Player.create(getContext(), null);
+            mSessionManager.setPlayer(mPlayer);
+            mSessionManager.setCallback(new SessionManager.Callback() {
+                @Override
+                public void onStatusChanged() {
+                }
+
+                @Override
+                public void onItemChanged(PlaylistItem item) {
+                    handleStatusChange(item);
+                }
+            });
+            setVolumeInternal(mVolume);
             Log.d(TAG, mRouteId + ": Controller created");
         }
 
         @Override
         public void onRelease() {
             Log.d(TAG, mRouteId + ": Controller released");
+            mPlayer.release();
         }
 
         @Override
         public void onSelect() {
             Log.d(TAG, mRouteId + ": Selected");
+            mPlayer.connect(null);
         }
 
         @Override
         public void onUnselect() {
             Log.d(TAG, mRouteId + ": Unselected");
+            mPlayer.release();
         }
 
         @Override
         public void onSetVolume(int volume) {
             Log.d(TAG, mRouteId + ": Set volume to " + volume);
-            if (mRouteId.equals(VARIABLE_VOLUME_ROUTE_ID)) {
+            if (!mRouteId.equals(FIXED_VOLUME_ROUTE_ID)) {
                 setVolumeInternal(volume);
             }
         }
@@ -187,70 +284,52 @@
         @Override
         public void onUpdateVolume(int delta) {
             Log.d(TAG, mRouteId + ": Update volume by " + delta);
-            if (mRouteId.equals(VARIABLE_VOLUME_ROUTE_ID)) {
+            if (!mRouteId.equals(FIXED_VOLUME_ROUTE_ID)) {
                 setVolumeInternal(mVolume + delta);
             }
         }
 
-        private void setVolumeInternal(int volume) {
-            if (volume >= 0 && volume <= VOLUME_MAX) {
-                mVolume = volume;
-                Log.d(TAG, mRouteId + ": New volume is " + mVolume);
-                publishRoutes();
-            }
-        }
-
         @Override
         public boolean onControlRequest(Intent intent, ControlRequestCallback callback) {
             Log.d(TAG, mRouteId + ": Received control request " + intent);
-            if (intent.getAction().equals(MediaControlIntent.ACTION_PLAY)
-                    && intent.hasCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
-                    && intent.getData() != null) {
-                mPlaybackCount +=1;
-
-                // TODO: Handle queue ids.
-                Uri uri = intent.getData();
-                long contentPositionMillis = intent.getLongExtra(
-                        MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0);
-                Bundle metadata = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_METADATA);
-                Bundle headers = intent.getBundleExtra(
-                        MediaControlIntent.EXTRA_ITEM_HTTP_HEADERS);
-
-                Log.d(TAG, mRouteId + ": Received play request, uri=" + uri
-                        + ", contentPositionMillis=" + contentPositionMillis
-                        + ", metadata=" + metadata
-                        + ", headers=" + headers);
-
-                if (uri.toString().contains("hats")) {
-                    // Simulate generating an error whenever the uri contains the word 'hats'.
-                    Toast.makeText(getContext(), "Route rejected play request: uri=" + uri
-                            + ", no hats allowed!", Toast.LENGTH_LONG).show();
-                    if (callback != null) {
-                        callback.onError("Simulated error.  No hats allowed!", null);
-                    }
-                } else {
-                    Toast.makeText(getContext(), "Route received play request: uri=" + uri,
-                            Toast.LENGTH_LONG).show();
-                    String streamId = generateStreamId();
-                    if (callback != null) {
-                        MediaItemStatus status = new MediaItemStatus.Builder(
-                                MediaItemStatus.PLAYBACK_STATE_PLAYING)
-                                .setContentPosition(contentPositionMillis)
-                                .build();
-
-                        Bundle result = new Bundle();
-                        result.putString(MediaControlIntent.EXTRA_ITEM_ID, streamId);
-                        result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, status.asBundle());
-                        callback.onResult(result);
-                    }
+            String action = intent.getAction();
+            if (intent.hasCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
+                boolean success = false;
+                if (action.equals(MediaControlIntent.ACTION_PLAY)) {
+                    success = handlePlay(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_ENQUEUE)) {
+                    success = handleEnqueue(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_REMOVE)) {
+                    success = handleRemove(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_SEEK)) {
+                    success = handleSeek(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_GET_STATUS)) {
+                    success = handleGetStatus(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_PAUSE)) {
+                    success = handlePause(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_RESUME)) {
+                    success = handleResume(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_STOP)) {
+                    success = handleStop(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_START_SESSION)) {
+                    success = handleStartSession(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_GET_SESSION_STATUS)) {
+                    success = handleGetSessionStatus(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_END_SESSION)) {
+                    success = handleEndSession(intent, callback);
                 }
-                return true;
+                Log.d(TAG, mSessionManager.toString());
+                return success;
             }
 
-            if (intent.getAction().equals(ACTION_GET_STATISTICS)
+            if (action.equals(ACTION_GET_TRACK_INFO)
                     && intent.hasCategory(CATEGORY_SAMPLE_ROUTE)) {
                 Bundle data = new Bundle();
-                data.putInt(DATA_PLAYBACK_COUNT, mPlaybackCount);
+                PlaylistItem item = mSessionManager.getCurrentItem();
+                if (item != null) {
+                    data.putString(TRACK_INFO_DESC, item.toString());
+                    data.putParcelable(TRACK_INFO_SNAPSHOT, mPlayer.getSnapshot());
+                }
                 if (callback != null) {
                     callback.onResult(data);
                 }
@@ -258,5 +337,278 @@
             }
             return false;
         }
+
+        private void setVolumeInternal(int volume) {
+            if (volume >= 0 && volume <= VOLUME_MAX) {
+                mVolume = volume;
+                Log.d(TAG, mRouteId + ": New volume is " + mVolume);
+                AudioManager audioManager =
+                        (AudioManager)getContext().getSystemService(Context.AUDIO_SERVICE);
+                audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0);
+                publishRoutes();
+            }
+        }
+
+        private boolean handlePlay(Intent intent, ControlRequestCallback callback) {
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            if (sid != null && !sid.equals(mSessionManager.getSessionId())) {
+                Log.d(TAG, "handlePlay fails because of bad sid="+sid);
+                return false;
+            }
+            if (mSessionManager.hasSession()) {
+                mSessionManager.stop();
+            }
+            return handleEnqueue(intent, callback);
+        }
+
+        private boolean handleEnqueue(Intent intent, ControlRequestCallback callback) {
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            if (sid != null && !sid.equals(mSessionManager.getSessionId())) {
+                Log.d(TAG, "handleEnqueue fails because of bad sid="+sid);
+                return false;
+            }
+
+            Uri uri = intent.getData();
+            if (uri == null) {
+                Log.d(TAG, "handleEnqueue fails because of bad uri="+uri);
+                return false;
+            }
+
+            boolean enqueue = intent.getAction().equals(MediaControlIntent.ACTION_ENQUEUE);
+            String mime = intent.getType();
+            long pos = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0);
+            Bundle metadata = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_METADATA);
+            Bundle headers = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_HTTP_HEADERS);
+            PendingIntent receiver = (PendingIntent)intent.getParcelableExtra(
+                    MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER);
+
+            Log.d(TAG, mRouteId + ": Received " + (enqueue?"enqueue":"play") + " request"
+                    + ", uri=" + uri
+                    + ", mime=" + mime
+                    + ", sid=" + sid
+                    + ", pos=" + pos
+                    + ", metadata=" + metadata
+                    + ", headers=" + headers
+                    + ", receiver=" + receiver);
+            PlaylistItem item = mSessionManager.add(uri, mime, receiver);
+            if (callback != null) {
+                if (item != null) {
+                    Bundle result = new Bundle();
+                    result.putString(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId());
+                    result.putString(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId());
+                    result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
+                            item.getStatus().asBundle());
+                    callback.onResult(result);
+                } else {
+                    callback.onError("Failed to open " + uri.toString(), null);
+                }
+            }
+            mEnqueueCount +=1;
+            return true;
+        }
+
+        private boolean handleRemove(Intent intent, ControlRequestCallback callback) {
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            if (sid == null || !sid.equals(mSessionManager.getSessionId())) {
+                return false;
+            }
+
+            String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
+            PlaylistItem item = mSessionManager.remove(iid);
+            if (callback != null) {
+                if (item != null) {
+                    Bundle result = new Bundle();
+                    result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
+                            item.getStatus().asBundle());
+                    callback.onResult(result);
+                } else {
+                    callback.onError("Failed to remove" +
+                            ", sid=" + sid + ", iid=" + iid, null);
+                }
+            }
+            return (item != null);
+        }
+
+        private boolean handleSeek(Intent intent, ControlRequestCallback callback) {
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            if (sid == null || !sid.equals(mSessionManager.getSessionId())) {
+                return false;
+            }
+
+            String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
+            long pos = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0);
+            Log.d(TAG, mRouteId + ": Received seek request, pos=" + pos);
+            PlaylistItem item = mSessionManager.seek(iid, pos);
+            if (callback != null) {
+                if (item != null) {
+                    Bundle result = new Bundle();
+                    result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
+                            item.getStatus().asBundle());
+                    callback.onResult(result);
+                } else {
+                    callback.onError("Failed to seek" +
+                            ", sid=" + sid + ", iid=" + iid + ", pos=" + pos, null);
+                }
+            }
+            return (item != null);
+        }
+
+        private boolean handleGetStatus(Intent intent, ControlRequestCallback callback) {
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
+            Log.d(TAG, mRouteId + ": Received getStatus request, sid=" + sid + ", iid=" + iid);
+            PlaylistItem item = mSessionManager.getStatus(iid);
+            if (callback != null) {
+                if (item != null) {
+                    Bundle result = new Bundle();
+                    result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
+                            item.getStatus().asBundle());
+                    callback.onResult(result);
+                } else {
+                    callback.onError("Failed to get status" +
+                            ", sid=" + sid + ", iid=" + iid, null);
+                }
+            }
+            return (item != null);
+        }
+
+        private boolean handlePause(Intent intent, ControlRequestCallback callback) {
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId());
+            mSessionManager.pause();
+            if (callback != null) {
+                if (success) {
+                    callback.onResult(new Bundle());
+                    handleSessionStatusChange(sid);
+                } else {
+                    callback.onError("Failed to pause, sid=" + sid, null);
+                }
+            }
+            return success;
+        }
+
+        private boolean handleResume(Intent intent, ControlRequestCallback callback) {
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId());
+            mSessionManager.resume();
+            if (callback != null) {
+                if (success) {
+                    callback.onResult(new Bundle());
+                    handleSessionStatusChange(sid);
+                } else {
+                    callback.onError("Failed to resume, sid=" + sid, null);
+                }
+            }
+            return success;
+        }
+
+        private boolean handleStop(Intent intent, ControlRequestCallback callback) {
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId());
+            mSessionManager.stop();
+            if (callback != null) {
+                if (success) {
+                    callback.onResult(new Bundle());
+                    handleSessionStatusChange(sid);
+                } else {
+                    callback.onError("Failed to stop, sid=" + sid, null);
+                }
+            }
+            return success;
+        }
+
+        private boolean handleStartSession(Intent intent, ControlRequestCallback callback) {
+            String sid = mSessionManager.startSession();
+            Log.d(TAG, "StartSession returns sessionId "+sid);
+            if (callback != null) {
+                if (sid != null) {
+                    Bundle result = new Bundle();
+                    result.putString(MediaControlIntent.EXTRA_SESSION_ID, sid);
+                    result.putBundle(MediaControlIntent.EXTRA_SESSION_STATUS,
+                            mSessionManager.getSessionStatus(sid).asBundle());
+                    callback.onResult(result);
+                    mSessionReceiver = (PendingIntent)intent.getParcelableExtra(
+                            MediaControlIntent.EXTRA_SESSION_STATUS_UPDATE_RECEIVER);
+                    handleSessionStatusChange(sid);
+                } else {
+                    callback.onError("Failed to start session.", null);
+                }
+            }
+            return (sid != null);
+        }
+
+        private boolean handleGetSessionStatus(Intent intent, ControlRequestCallback callback) {
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+
+            MediaSessionStatus sessionStatus = mSessionManager.getSessionStatus(sid);
+            if (callback != null) {
+                if (sessionStatus != null) {
+                    Bundle result = new Bundle();
+                    result.putBundle(MediaControlIntent.EXTRA_SESSION_STATUS,
+                            mSessionManager.getSessionStatus(sid).asBundle());
+                    callback.onResult(result);
+                } else {
+                    callback.onError("Failed to get session status, sid=" + sid, null);
+                }
+            }
+            return (sessionStatus != null);
+        }
+
+        private boolean handleEndSession(Intent intent, ControlRequestCallback callback) {
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId())
+                    && mSessionManager.endSession();
+            if (callback != null) {
+                if (success) {
+                    Bundle result = new Bundle();
+                    MediaSessionStatus sessionStatus = new MediaSessionStatus.Builder(
+                            MediaSessionStatus.SESSION_STATE_ENDED).build();
+                    result.putBundle(MediaControlIntent.EXTRA_SESSION_STATUS, sessionStatus.asBundle());
+                    callback.onResult(result);
+                    handleSessionStatusChange(sid);
+                    mSessionReceiver = null;
+                } else {
+                    callback.onError("Failed to end session, sid=" + sid, null);
+                }
+            }
+            return success;
+        }
+
+        private void handleStatusChange(PlaylistItem item) {
+            if (item == null) {
+                item = mSessionManager.getCurrentItem();
+            }
+            if (item != null) {
+                PendingIntent receiver = item.getUpdateReceiver();
+                if (receiver != null) {
+                    Intent intent = new Intent();
+                    intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId());
+                    intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId());
+                    intent.putExtra(MediaControlIntent.EXTRA_ITEM_STATUS,
+                            item.getStatus().asBundle());
+                    try {
+                        receiver.send(getContext(), 0, intent);
+                        Log.d(TAG, mRouteId + ": Sending status update from provider");
+                    } catch (PendingIntent.CanceledException e) {
+                        Log.d(TAG, mRouteId + ": Failed to send status update!");
+                    }
+                }
+            }
+        }
+
+        private void handleSessionStatusChange(String sid) {
+            if (mSessionReceiver != null) {
+                Intent intent = new Intent();
+                intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sid);
+                intent.putExtra(MediaControlIntent.EXTRA_SESSION_STATUS,
+                        mSessionManager.getSessionStatus(sid).asBundle());
+                try {
+                    mSessionReceiver.send(getContext(), 0, intent);
+                    Log.d(TAG, mRouteId + ": Sending session status update from provider");
+                } catch (PendingIntent.CanceledException e) {
+                    Log.d(TAG, mRouteId + ": Failed to send session status update!");
+                }
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouterActivity.java b/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouterActivity.java
index 456bd15..dfa1416 100644
--- a/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouterActivity.java
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouterActivity.java
@@ -18,32 +18,55 @@
 
 import com.example.android.supportv7.R;
 
+import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
+import android.content.res.Resources;
+import android.app.PendingIntent;
+import android.media.AudioManager;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.media.MediaMetadataRetriever;
+import android.media.RemoteControlClient;
 import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.os.Handler;
 import android.os.Bundle;
+import android.os.SystemClock;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.view.MenuItemCompat;
 import android.support.v7.app.ActionBarActivity;
 import android.support.v7.app.MediaRouteActionProvider;
+import android.support.v7.app.MediaRouteControllerDialog;
+import android.support.v7.app.MediaRouteControllerDialogFragment;
 import android.support.v7.app.MediaRouteDiscoveryFragment;
+import android.support.v7.app.MediaRouteDialogFactory;
 import android.support.v7.media.MediaControlIntent;
 import android.support.v7.media.MediaRouter;
 import android.support.v7.media.MediaRouter.Callback;
 import android.support.v7.media.MediaRouter.RouteInfo;
 import android.support.v7.media.MediaRouter.ProviderInfo;
 import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.media.MediaItemStatus;
 import android.util.Log;
+import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
-import android.widget.AdapterView.OnItemClickListener;
+import android.view.ViewGroup;
 import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
 import android.widget.ArrayAdapter;
-import android.widget.Button;
+import android.widget.ImageButton;
 import android.widget.ListView;
 import android.widget.TextView;
-import android.widget.Toast;
+import android.widget.TabHost;
+import android.widget.TabHost.TabSpec;
+import android.widget.TabHost.OnTabChangeListener;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import java.io.File;
 
 /**
  * <h3>Media Router Support Activity</h3>
@@ -55,16 +78,128 @@
  * </p>
  */
 public class SampleMediaRouterActivity extends ActionBarActivity {
-    private static final String TAG = "MediaRouterSupport";
+    private static final String TAG = "SampleMediaRouterActivity";
     private static final String DISCOVERY_FRAGMENT_TAG = "DiscoveryFragment";
 
     private MediaRouter mMediaRouter;
     private MediaRouteSelector mSelector;
-    private ArrayAdapter<MediaItem> mMediaItems;
+    private LibraryAdapter mLibraryItems;
+    private PlaylistAdapter mPlayListItems;
     private TextView mInfoTextView;
-    private ListView mMediaListView;
-    private Button mPlayButton;
-    private Button mStatisticsButton;
+    private ListView mLibraryView;
+    private ListView mPlayListView;
+    private ImageButton mPauseResumeButton;
+    private ImageButton mStopButton;
+    private SeekBar mSeekBar;
+    private boolean mNeedResume;
+    private boolean mSeeking;
+    private SampleMediaRouteControllerDialog mControllerDialog;
+
+    private final Handler mHandler = new Handler();
+    private final Runnable mUpdateSeekRunnable = new Runnable() {
+        @Override
+        public void run() {
+            updateProgress();
+            // update Ui every 1 second
+            mHandler.postDelayed(this, 1000);
+        }
+    };
+
+    private final SessionManager mSessionManager = new SessionManager("app");
+    private Player mPlayer;
+
+    private final MediaRouter.Callback mMediaRouterCB = new MediaRouter.Callback() {
+        // Return a custom callback that will simply log all of the route events
+        // for demonstration purposes.
+        @Override
+        public void onRouteAdded(MediaRouter router, RouteInfo route) {
+            Log.d(TAG, "onRouteAdded: route=" + route);
+        }
+
+        @Override
+        public void onRouteChanged(MediaRouter router, RouteInfo route) {
+            Log.d(TAG, "onRouteChanged: route=" + route);
+        }
+
+        @Override
+        public void onRouteRemoved(MediaRouter router, RouteInfo route) {
+            Log.d(TAG, "onRouteRemoved: route=" + route);
+        }
+
+        @Override
+        public void onRouteSelected(MediaRouter router, RouteInfo route) {
+            Log.d(TAG, "onRouteSelected: route=" + route);
+
+            mPlayer = Player.create(SampleMediaRouterActivity.this, route);
+            mPlayer.updatePresentation();
+            mSessionManager.setPlayer(mPlayer);
+            mSessionManager.unsuspend();
+
+            registerRCC();
+            updateUi();
+        }
+
+        @Override
+        public void onRouteUnselected(MediaRouter router, RouteInfo route) {
+            Log.d(TAG, "onRouteUnselected: route=" + route);
+            unregisterRCC();
+
+            PlaylistItem item = getCheckedPlaylistItem();
+            if (item != null) {
+                long pos = item.getPosition() + (mSessionManager.isPaused() ?
+                        0 : (SystemClock.elapsedRealtime() - item.getTimestamp()));
+                mSessionManager.suspend(pos);
+            }
+            mPlayer.updatePresentation();
+            mPlayer.release();
+            mControllerDialog = null;
+        }
+
+        @Override
+        public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) {
+            Log.d(TAG, "onRouteVolumeChanged: route=" + route);
+        }
+
+        @Override
+        public void onRoutePresentationDisplayChanged(
+                MediaRouter router, RouteInfo route) {
+            Log.d(TAG, "onRoutePresentationDisplayChanged: route=" + route);
+            mPlayer.updatePresentation();
+        }
+
+        @Override
+        public void onProviderAdded(MediaRouter router, ProviderInfo provider) {
+            Log.d(TAG, "onRouteProviderAdded: provider=" + provider);
+        }
+
+        @Override
+        public void onProviderRemoved(MediaRouter router, ProviderInfo provider) {
+            Log.d(TAG, "onRouteProviderRemoved: provider=" + provider);
+        }
+
+        @Override
+        public void onProviderChanged(MediaRouter router, ProviderInfo provider) {
+            Log.d(TAG, "onRouteProviderChanged: provider=" + provider);
+        }
+    };
+
+    private RemoteControlClient mRemoteControlClient;
+    private ComponentName mEventReceiver;
+    private AudioManager mAudioManager;
+    private PendingIntent mMediaPendingIntent;
+    private final OnAudioFocusChangeListener mAfChangeListener =
+            new OnAudioFocusChangeListener() {
+        @Override
+        public void onAudioFocusChange(int focusChange) {
+            if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
+                Log.d(TAG, "onAudioFocusChange: LOSS_TRANSIENT");
+            } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+                Log.d(TAG, "onAudioFocusChange: AUDIOFOCUS_GAIN");
+            } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
+                Log.d(TAG, "onAudioFocusChange: AUDIOFOCUS_LOSS");
+            }
+        }
+    };
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -86,61 +221,288 @@
         // This fragment automatically adds or removes a callback whenever the activity
         // is started or stopped.
         FragmentManager fm = getSupportFragmentManager();
-        if (fm.findFragmentByTag(DISCOVERY_FRAGMENT_TAG) == null) {
-            DiscoveryFragment fragment = new DiscoveryFragment();
+        DiscoveryFragment fragment = (DiscoveryFragment)fm.findFragmentByTag(
+                DISCOVERY_FRAGMENT_TAG);
+        if (fragment == null) {
+            fragment = new DiscoveryFragment(mMediaRouterCB);
             fragment.setRouteSelector(mSelector);
             fm.beginTransaction()
                     .add(fragment, DISCOVERY_FRAGMENT_TAG)
                     .commit();
+        } else {
+            fragment.setCallback(mMediaRouterCB);
+            fragment.setRouteSelector(mSelector);
         }
 
-        // Populate an array adapter with fake media items.
+        // Populate an array adapter with streaming media items.
         String[] mediaNames = getResources().getStringArray(R.array.media_names);
         String[] mediaUris = getResources().getStringArray(R.array.media_uris);
-        mMediaItems = new ArrayAdapter<MediaItem>(this,
-                android.R.layout.simple_list_item_single_choice, android.R.id.text1);
+        mLibraryItems = new LibraryAdapter();
         for (int i = 0; i < mediaNames.length; i++) {
-            mMediaItems.add(new MediaItem(mediaNames[i], Uri.parse(mediaUris[i])));
+            mLibraryItems.add(new MediaItem(
+                    "[streaming] "+mediaNames[i], Uri.parse(mediaUris[i]), "video/mp4"));
         }
 
+        // Scan local external storage directory for media files.
+        File externalDir = Environment.getExternalStorageDirectory();
+        if (externalDir != null) {
+            File list[] = externalDir.listFiles();
+            if (list != null) {
+                for (int i = 0; i < list.length; i++) {
+                    String filename = list[i].getName();
+                    if (filename.matches(".*\\.(m4v|mp4)")) {
+                        mLibraryItems.add(new MediaItem("[local] " + filename,
+                                Uri.fromFile(list[i]), "video/mp4"));
+                    }
+                }
+            }
+        }
+
+        mPlayListItems = new PlaylistAdapter();
+
         // Initialize the layout.
         setContentView(R.layout.sample_media_router);
 
-        mInfoTextView = (TextView)findViewById(R.id.info);
+        TabHost tabHost=(TabHost)findViewById(R.id.tabHost);
+        tabHost.setup();
+        String tabName = getResources().getString(R.string.library_tab_text);
+        TabSpec spec1=tabHost.newTabSpec(tabName);
+        spec1.setContent(R.id.tab1);
+        spec1.setIndicator(tabName);
 
-        mMediaListView = (ListView)findViewById(R.id.media);
-        mMediaListView.setAdapter(mMediaItems);
-        mMediaListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
-        mMediaListView.setOnItemClickListener(new OnItemClickListener() {
+        tabName = getResources().getString(R.string.playlist_tab_text);
+        TabSpec spec2=tabHost.newTabSpec(tabName);
+        spec2.setIndicator(tabName);
+        spec2.setContent(R.id.tab2);
+
+        tabName = getResources().getString(R.string.info_tab_text);
+        TabSpec spec3=tabHost.newTabSpec(tabName);
+        spec3.setIndicator(tabName);
+        spec3.setContent(R.id.tab3);
+
+        tabHost.addTab(spec1);
+        tabHost.addTab(spec2);
+        tabHost.addTab(spec3);
+        tabHost.setOnTabChangedListener(new OnTabChangeListener() {
+            @Override
+            public void onTabChanged(String arg0) {
+                updateUi();
+            }
+        });
+
+        mLibraryView = (ListView) findViewById(R.id.media);
+        mLibraryView.setAdapter(mLibraryItems);
+        mLibraryView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+        mLibraryView.setOnItemClickListener(new OnItemClickListener() {
             @Override
             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                 updateButtons();
             }
         });
 
-        mPlayButton = (Button)findViewById(R.id.play_button);
-        mPlayButton.setOnClickListener(new OnClickListener() {
+        mPlayListView = (ListView) findViewById(R.id.playlist);
+        mPlayListView.setAdapter(mPlayListItems);
+        mPlayListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+        mPlayListView.setOnItemClickListener(new OnItemClickListener() {
             @Override
-            public void onClick(View v) {
-                play();
+            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                updateButtons();
             }
         });
 
-        mStatisticsButton = (Button)findViewById(R.id.statistics_button);
-        mStatisticsButton.setOnClickListener(new OnClickListener() {
+        mInfoTextView = (TextView) findViewById(R.id.info);
+
+        mPauseResumeButton = (ImageButton)findViewById(R.id.pause_resume_button);
+        mPauseResumeButton.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View v) {
-                showStatistics();
+                if (mSessionManager.isPaused()) {
+                    mSessionManager.resume();
+                } else {
+                    mSessionManager.pause();
+                }
             }
         });
+
+        mStopButton = (ImageButton)findViewById(R.id.stop_button);
+        mStopButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                mSessionManager.stop();
+            }
+        });
+
+        mSeekBar = (SeekBar) findViewById(R.id.seekbar);
+        mSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                PlaylistItem item = getCheckedPlaylistItem();
+                if (fromUser && item != null && item.getDuration() > 0) {
+                    long pos = progress * item.getDuration() / 100;
+                    mSessionManager.seek(item.getItemId(), pos);
+                    item.setPosition(pos);
+                    item.setTimestamp(SystemClock.elapsedRealtime());
+                }
+            }
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+                mSeeking = true;
+            }
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+                mSeeking = false;
+                updateUi();
+            }
+        });
+
+        // Schedule Ui update
+        mHandler.postDelayed(mUpdateSeekRunnable, 1000);
+
+        // Build the PendingIntent for the remote control client
+        mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+        mEventReceiver = new ComponentName(getPackageName(),
+                SampleMediaButtonReceiver.class.getName());
+        Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+        mediaButtonIntent.setComponent(mEventReceiver);
+        mMediaPendingIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, 0);
+
+        // Create and register the remote control client
+        registerRCC();
+
+        // Set up playback manager and player
+        mPlayer = Player.create(SampleMediaRouterActivity.this,
+                mMediaRouter.getSelectedRoute());
+        mSessionManager.setPlayer(mPlayer);
+        mSessionManager.setCallback(new SessionManager.Callback() {
+            @Override
+            public void onStatusChanged() {
+                updateUi();
+            }
+
+            @Override
+            public void onItemChanged(PlaylistItem item) {
+            }
+        });
+
+        updateUi();
+    }
+
+    private void registerRCC() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+            // Create the RCC and register with AudioManager and MediaRouter
+            mAudioManager.requestAudioFocus(mAfChangeListener,
+                    AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+            mAudioManager.registerMediaButtonEventReceiver(mEventReceiver);
+            mRemoteControlClient = new RemoteControlClient(mMediaPendingIntent);
+            mAudioManager.registerRemoteControlClient(mRemoteControlClient);
+            mMediaRouter.addRemoteControlClient(mRemoteControlClient);
+            SampleMediaButtonReceiver.setActivity(SampleMediaRouterActivity.this);
+            mRemoteControlClient.setTransportControlFlags(
+                    RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE);
+            mRemoteControlClient.setPlaybackState(
+                    RemoteControlClient.PLAYSTATE_PLAYING);
+        }
+    }
+
+    private void unregisterRCC() {
+        // Unregister the RCC with AudioManager and MediaRouter
+        if (mRemoteControlClient != null) {
+            mRemoteControlClient.setTransportControlFlags(0);
+            mAudioManager.abandonAudioFocus(mAfChangeListener);
+            mAudioManager.unregisterMediaButtonEventReceiver(mEventReceiver);
+            mAudioManager.unregisterRemoteControlClient(mRemoteControlClient);
+            mMediaRouter.removeRemoteControlClient(mRemoteControlClient);
+            SampleMediaButtonReceiver.setActivity(null);
+            mRemoteControlClient = null;
+        }
+    }
+
+    public boolean handleMediaKey(KeyEvent event) {
+        if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+            switch (event.getKeyCode()) {
+                case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+                {
+                    Log.d(TAG, "Received Play/Pause event from RemoteControlClient");
+                    if (mSessionManager.isPaused()) {
+                        mSessionManager.resume();
+                    } else {
+                        mSessionManager.pause();
+                    }
+                    return true;
+                }
+                case KeyEvent.KEYCODE_MEDIA_PLAY:
+                {
+                    Log.d(TAG, "Received Play event from RemoteControlClient");
+                    if (mSessionManager.isPaused()) {
+                        mSessionManager.resume();
+                    }
+                    return true;
+                }
+                case KeyEvent.KEYCODE_MEDIA_PAUSE:
+                {
+                    Log.d(TAG, "Received Pause event from RemoteControlClient");
+                    if (!mSessionManager.isPaused()) {
+                        mSessionManager.pause();
+                    }
+                    return true;
+                }
+                case KeyEvent.KEYCODE_MEDIA_STOP:
+                {
+                    Log.d(TAG, "Received Stop event from RemoteControlClient");
+                    mSessionManager.stop();
+                    return true;
+                }
+                default:
+                    break;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        return handleMediaKey(event) || super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        return handleMediaKey(event) || super.onKeyUp(keyCode, event);
     }
 
     @Override
     public void onStart() {
         // Be sure to call the super class.
         super.onStart();
+    }
 
-        updateRouteDescription();
+    @Override
+    public void onPause() {
+        // pause media player for local playback case only
+        if (!mPlayer.isRemotePlayback() && !mSessionManager.isPaused()) {
+            mNeedResume = true;
+            mSessionManager.pause();
+        }
+        super.onPause();
+    }
+
+    @Override
+    public void onResume() {
+        // resume media player for local playback case only
+        if (!mPlayer.isRemotePlayback() && mNeedResume) {
+            mSessionManager.resume();
+            mNeedResume = false;
+        }
+        super.onResume();
+    }
+
+    @Override
+    public void onDestroy() {
+        // Unregister the remote control client
+        unregisterRCC();
+
+        mSessionManager.stop();
+        mPlayer.release();
+        super.onDestroy();
     }
 
     @Override
@@ -155,192 +517,121 @@
         MediaRouteActionProvider mediaRouteActionProvider =
                 (MediaRouteActionProvider)MenuItemCompat.getActionProvider(mediaRouteMenuItem);
         mediaRouteActionProvider.setRouteSelector(mSelector);
+        mediaRouteActionProvider.setDialogFactory(new MediaRouteDialogFactory() {
+            @Override
+            public MediaRouteControllerDialogFragment onCreateControllerDialogFragment() {
+                return new MediaRouteControllerDialogFragment() {
+                    @Override
+                    public MediaRouteControllerDialog onCreateControllerDialog(
+                            Context context, Bundle savedInstanceState) {
+                        mControllerDialog = new SampleMediaRouteControllerDialog(
+                                context, mSessionManager, mPlayer);
+                        return mControllerDialog;
+                    }
+                };
+            }
+        });
 
         // Return true to show the menu.
         return true;
     }
 
+    private void updateProgress() {
+        // Estimate content position from last status time and elapsed time.
+        // (Note this might be slightly out of sync with remote side, however
+        // it avoids frequent polling the MRP.)
+        int progress = 0;
+        PlaylistItem item = getCheckedPlaylistItem();
+        if (item != null) {
+            int state = item.getState();
+            long duration = item.getDuration();
+            if (duration <= 0) {
+                if (state == MediaItemStatus.PLAYBACK_STATE_PLAYING
+                        || state == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
+                    mSessionManager.updateStatus();
+                }
+            } else {
+                long position = item.getPosition();
+                long timeDelta = mSessionManager.isPaused() ? 0 :
+                        (SystemClock.elapsedRealtime() - item.getTimestamp());
+                progress = (int)(100.0 * (position + timeDelta) / duration);
+            }
+        }
+        mSeekBar.setProgress(progress);
+    }
+
+    private void updateUi() {
+        updatePlaylist();
+        updateRouteDescription();
+        updateButtons();
+        if (mControllerDialog != null) {
+            mControllerDialog.updateUi();
+        }
+    }
+
+    private void updatePlaylist() {
+        mPlayListItems.clear();
+        for (PlaylistItem item : mSessionManager.getPlaylist()) {
+            mPlayListItems.add(item);
+        }
+        mPlayListView.invalidate();
+    }
+
     private void updateRouteDescription() {
         RouteInfo route = mMediaRouter.getSelectedRoute();
-        mInfoTextView.setText("Currently selected route: " + route.getName()
-                + " from provider " + route.getProvider().getPackageName()
-                + ", description: " + route.getDescription());
-        updateButtons();
+        mInfoTextView.setText("Currently selected route:"
+                + "\nName: " + route.getName()
+                + "\nProvider: " + route.getProvider().getPackageName()
+                + "\nDescription: " + route.getDescription());
     }
 
     private void updateButtons() {
         MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute();
-
-        MediaItem item = getCheckedMediaItem();
-        if (item != null) {
-            mPlayButton.setEnabled(route.supportsControlRequest(makePlayIntent(item)));
-        } else {
-            mPlayButton.setEnabled(false);
-        }
-
-        mStatisticsButton.setEnabled(route.supportsControlRequest(makeStatisticsIntent()));
-    }
-
-    private void play() {
-        final MediaItem item = getCheckedMediaItem();
-        if (item == null) {
-            return;
-        }
-
-        MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute();
-        Intent intent = makePlayIntent(item);
-        if (route.supportsControlRequest(intent)) {
-            MediaRouter.ControlRequestCallback callback =
-                    new MediaRouter.ControlRequestCallback() {
-                @Override
-                public void onResult(Bundle data) {
-                    String streamId = data != null ? data.getString(
-                            MediaControlIntent.EXTRA_ITEM_ID) : null;
-
-                    Log.d(TAG, "Play request succeeded: data=" + data + " , streamId=" + streamId);
-                    Toast.makeText(SampleMediaRouterActivity.this,
-                            "Now playing " + item.mName,
-                            Toast.LENGTH_LONG).show();
-                }
-
-                @Override
-                public void onError(String error, Bundle data) {
-                    Log.d(TAG, "Play request failed: error=" + error + ", data=" + data);
-                    Toast.makeText(SampleMediaRouterActivity.this,
-                            "Unable to play " + item.mName + ", error: " + error,
-                            Toast.LENGTH_LONG).show();
-                }
-            };
-
-            Log.d(TAG, "Sending play request: intent=" + intent);
-            route.sendControlRequest(intent, callback);
-        } else {
-            Log.d(TAG, "Play request not supported: intent=" + intent);
-            Toast.makeText(SampleMediaRouterActivity.this,
-                    "Play not supported for " + item.mName, Toast.LENGTH_LONG).show();
+        // show pause or resume icon depending on current state
+        mPauseResumeButton.setImageResource(mSessionManager.isPaused() ?
+                R.drawable.ic_media_play : R.drawable.ic_media_pause);
+        // only enable seek bar when duration is known
+        PlaylistItem item = getCheckedPlaylistItem();
+        mSeekBar.setEnabled(item != null && item.getDuration() > 0);
+        if (mRemoteControlClient != null) {
+            mRemoteControlClient.setPlaybackState(mSessionManager.isPaused() ?
+                    RemoteControlClient.PLAYSTATE_PAUSED :
+                        RemoteControlClient.PLAYSTATE_PLAYING);
         }
     }
 
-    private void showStatistics() {
-        MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute();
-        Intent intent = makeStatisticsIntent();
-        if (route.supportsControlRequest(intent)) {
-            MediaRouter.ControlRequestCallback callback = new MediaRouter.ControlRequestCallback() {
-                @Override
-                public void onResult(Bundle data) {
-                    Log.d(TAG, "Statistics request succeeded: data=" + data);
-                    if (data != null) {
-                        int playbackCount = data.getInt(
-                                SampleMediaRouteProvider.DATA_PLAYBACK_COUNT, -1);
-                        Toast.makeText(SampleMediaRouterActivity.this,
-                                "Total playback count: " + playbackCount,
-                                Toast.LENGTH_LONG).show();
-                    } else {
-                        Toast.makeText(SampleMediaRouterActivity.this,
-                                "Statistics query did not return any data",
-                                Toast.LENGTH_LONG).show();
-                    }
-                }
-
-                @Override
-                public void onError(String error, Bundle data) {
-                    Log.d(TAG, "Statistics request failed: error=" + error + ", data=" + data);
-                    Toast.makeText(SampleMediaRouterActivity.this,
-                            "Unable to query statistics, error: " + error,
-                            Toast.LENGTH_LONG).show();
-                }
-            };
-
-            Log.d(TAG, "Sent statistics request: intent=" + intent);
-            route.sendControlRequest(intent, callback);
-        } else {
-            Log.d(TAG, "Statistics request not supported: intent=" + intent);
-            Toast.makeText(SampleMediaRouterActivity.this,
-                    "Statistics not supported.", Toast.LENGTH_LONG).show();
-        }
-    }
-
-    private Intent makePlayIntent(MediaItem item) {
-        Intent intent = new Intent(MediaControlIntent.ACTION_PLAY);
-        intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
-        intent.setDataAndType(item.mUri, "video/mp4");
-        return intent;
-    }
-
-    private Intent makeStatisticsIntent() {
-        Intent intent = new Intent(SampleMediaRouteProvider.ACTION_GET_STATISTICS);
-        intent.addCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE);
-        return intent;
-    }
-
-    private MediaItem getCheckedMediaItem() {
-        int index = mMediaListView.getCheckedItemPosition();
-        if (index >= 0 && index < mMediaItems.getCount()) {
-            return mMediaItems.getItem(index);
+    private PlaylistItem getCheckedPlaylistItem() {
+        int count = mPlayListView.getCount();
+        int index = mPlayListView.getCheckedItemPosition();
+        if (count > 0) {
+            if (index < 0 || index >= count) {
+                index = 0;
+                mPlayListView.setItemChecked(0, true);
+            }
+            return mPlayListItems.getItem(index);
         }
         return null;
     }
 
-    private final class DiscoveryFragment extends MediaRouteDiscoveryFragment {
+    public static final class DiscoveryFragment extends MediaRouteDiscoveryFragment {
+        private static final String TAG = "DiscoveryFragment";
+        private Callback mCallback;
+
+        public DiscoveryFragment() {
+            mCallback = null;
+        }
+
+        public DiscoveryFragment(Callback cb) {
+            mCallback = cb;
+        }
+
+        public void setCallback(Callback cb) {
+            mCallback = cb;
+        }
+
         @Override
         public Callback onCreateCallback() {
-            // Return a custom callback that will simply log all of the route events
-            // for demonstration purposes.
-            return new MediaRouter.Callback() {
-                @Override
-                public void onRouteAdded(MediaRouter router, RouteInfo route) {
-                    Log.d(TAG, "onRouteAdded: route=" + route);
-                }
-
-                @Override
-                public void onRouteChanged(MediaRouter router, RouteInfo route) {
-                    Log.d(TAG, "onRouteChanged: route=" + route);
-                    updateRouteDescription();
-                }
-
-                @Override
-                public void onRouteRemoved(MediaRouter router, RouteInfo route) {
-                    Log.d(TAG, "onRouteRemoved: route=" + route);
-                }
-
-                @Override
-                public void onRouteSelected(MediaRouter router, RouteInfo route) {
-                    Log.d(TAG, "onRouteSelected: route=" + route);
-                    updateRouteDescription();
-                }
-
-                @Override
-                public void onRouteUnselected(MediaRouter router, RouteInfo route) {
-                    Log.d(TAG, "onRouteUnselected: route=" + route);
-                    updateRouteDescription();
-                }
-
-                @Override
-                public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) {
-                    Log.d(TAG, "onRouteVolumeChanged: route=" + route);
-                }
-
-                @Override
-                public void onRoutePresentationDisplayChanged(
-                        MediaRouter router, RouteInfo route) {
-                    Log.d(TAG, "onRoutePresentationDisplayChanged: route=" + route);
-                }
-
-                @Override
-                public void onProviderAdded(MediaRouter router, ProviderInfo provider) {
-                    Log.d(TAG, "onRouteProviderAdded: provider=" + provider);
-                }
-
-                @Override
-                public void onProviderRemoved(MediaRouter router, ProviderInfo provider) {
-                    Log.d(TAG, "onRouteProviderRemoved: provider=" + provider);
-                }
-
-                @Override
-                public void onProviderChanged(MediaRouter router, ProviderInfo provider) {
-                    Log.d(TAG, "onRouteProviderChanged: provider=" + provider);
-                }
-            };
+            return mCallback;
         }
 
         @Override
@@ -357,10 +648,12 @@
     private static final class MediaItem {
         public final String mName;
         public final Uri mUri;
+        public final String mMime;
 
-        public MediaItem(String name, Uri uri) {
+        public MediaItem(String name, Uri uri, String mime) {
             mName = name;
             mUri = uri;
+            mMime = mime;
         }
 
         @Override
@@ -369,6 +662,76 @@
         }
     }
 
+    private final class LibraryAdapter extends ArrayAdapter<MediaItem> {
+        public LibraryAdapter() {
+            super(SampleMediaRouterActivity.this, R.layout.media_item);
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final View v;
+            if (convertView == null) {
+                v = getLayoutInflater().inflate(R.layout.media_item, null);
+            } else {
+                v = convertView;
+            }
+
+            final MediaItem item = getItem(position);
+
+            TextView tv = (TextView)v.findViewById(R.id.item_text);
+            tv.setText(item.mName);
+
+            ImageButton b = (ImageButton)v.findViewById(R.id.item_action);
+            b.setImageResource(R.drawable.ic_menu_add);
+            b.setTag(item);
+            b.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    if (item != null) {
+                        mSessionManager.add(item.mUri, item.mMime);
+                    }
+                }
+            });
+
+            return v;
+        }
+    }
+
+    private final class PlaylistAdapter extends ArrayAdapter<PlaylistItem> {
+        public PlaylistAdapter() {
+            super(SampleMediaRouterActivity.this, R.layout.media_item);
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final View v;
+            if (convertView == null) {
+                v = getLayoutInflater().inflate(R.layout.media_item, null);
+            } else {
+                v = convertView;
+            }
+
+            final PlaylistItem item = getItem(position);
+
+            TextView tv = (TextView)v.findViewById(R.id.item_text);
+            tv.setText(item.toString());
+
+            ImageButton b = (ImageButton)v.findViewById(R.id.item_action);
+            b.setImageResource(R.drawable.ic_menu_delete);
+            b.setTag(item);
+            b.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    if (item != null) {
+                        mSessionManager.remove(item.getItemId());
+                    }
+                }
+            });
+
+            return v;
+        }
+    }
+
     /**
      * Trivial subclass of this activity used to provide another copy of the
      * same activity using a light theme instead of the dark theme.
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/SessionManager.java b/samples/Support7Demos/src/com/example/android/supportv7/media/SessionManager.java
new file mode 100644
index 0000000..1c00192
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/SessionManager.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.supportv7.media;
+
+import android.app.PendingIntent;
+import android.net.Uri;
+import android.support.v7.media.MediaItemStatus;
+import android.support.v7.media.MediaSessionStatus;
+import android.util.Log;
+
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * SessionManager manages a media session as a queue. It supports common
+ * queuing behaviors such as enqueue/remove of media items, pause/resume/stop,
+ * etc.
+ *
+ * Actual playback of a single media item is abstracted into a Player interface,
+ * and is handled outside this class.
+ */
+public class SessionManager implements Player.Callback {
+    private static final String TAG = "SessionManager";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private String mName;
+    private int mSessionId;
+    private int mItemId;
+    private boolean mPaused;
+    private boolean mSessionValid;
+    private Player mPlayer;
+    private Callback mCallback;
+    private List<PlaylistItem> mPlaylist = new ArrayList<PlaylistItem>();
+
+    public SessionManager(String name) {
+        mName = name;
+    }
+
+    public boolean isPaused() {
+        return hasSession() && mPaused;
+    }
+
+    public boolean hasSession() {
+        return mSessionValid;
+    }
+
+    public String getSessionId() {
+        return mSessionValid ? Integer.toString(mSessionId) : null;
+    }
+
+    public PlaylistItem getCurrentItem() {
+        return mPlaylist.isEmpty() ? null : mPlaylist.get(0);
+    }
+
+    // Returns the cached playlist (note this is not responsible for updating it)
+    public List<PlaylistItem> getPlaylist() {
+        return mPlaylist;
+    }
+
+    // Updates the playlist asynchronously, calls onPlaylistReady() when finished.
+    public void updateStatus() {
+        if (DEBUG) {
+            log("updateStatus");
+        }
+        checkPlayer();
+        // update the statistics first, so that the stats string is valid when
+        // onPlaylistReady() gets called in the end
+        mPlayer.updateTrackInfo();
+
+        if (mPlaylist.isEmpty()) {
+            // If queue is empty, don't forget to call onPlaylistReady()!
+            onPlaylistReady();
+        } else if (mPlayer.isQueuingSupported()) {
+            // If player supports queuing, get status of each item. Player is
+            // responsible to call onPlaylistReady() after last getStatus().
+            // (update=1 requires player to callback onPlaylistReady())
+            for (int i = 0; i < mPlaylist.size(); i++) {
+                PlaylistItem item = mPlaylist.get(i);
+                mPlayer.getStatus(item, (i == mPlaylist.size() - 1) /* update */);
+            }
+        } else {
+            // Otherwise, only need to get status for current item. Player is
+            // responsible to call onPlaylistReady() when finished.
+            mPlayer.getStatus(getCurrentItem(), true /* update */);
+        }
+    }
+
+    public PlaylistItem add(Uri uri, String mime) {
+        return add(uri, mime, null);
+    }
+
+    public PlaylistItem add(Uri uri, String mime, PendingIntent receiver) {
+        if (DEBUG) {
+            log("add: uri=" + uri + ", receiver=" + receiver);
+        }
+        // create new session if needed
+        startSession();
+        checkPlayerAndSession();
+
+        // append new item with initial status PLAYBACK_STATE_PENDING
+        PlaylistItem item = new PlaylistItem(
+                Integer.toString(mSessionId), Integer.toString(mItemId), uri, mime, receiver);
+        mPlaylist.add(item);
+        mItemId++;
+
+        // if player supports queuing, enqueue the item now
+        if (mPlayer.isQueuingSupported()) {
+            mPlayer.enqueue(item);
+        }
+        updatePlaybackState();
+        return item;
+    }
+
+    public PlaylistItem remove(String iid) {
+        if (DEBUG) {
+            log("remove: iid=" + iid);
+        }
+        checkPlayerAndSession();
+        return removeItem(iid, MediaItemStatus.PLAYBACK_STATE_CANCELED);
+    }
+
+    public PlaylistItem seek(String iid, long pos) {
+        if (DEBUG) {
+            log("seek: iid=" + iid +", pos=" + pos);
+        }
+        checkPlayerAndSession();
+        // seeking on pending items are not yet supported
+        checkItemCurrent(iid);
+
+        PlaylistItem item = getCurrentItem();
+        if (pos != item.getPosition()) {
+            item.setPosition(pos);
+            if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
+                    || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
+                mPlayer.seek(item);
+            }
+        }
+        return item;
+    }
+
+    public PlaylistItem getStatus(String iid) {
+        checkPlayerAndSession();
+
+        // This should only be called for local player. Remote player is
+        // asynchronous, need to use updateStatus() instead.
+        if (mPlayer.isRemotePlayback()) {
+            throw new IllegalStateException(
+                    "getStatus should not be called on remote player!");
+        }
+
+        for (PlaylistItem item : mPlaylist) {
+            if (item.getItemId().equals(iid)) {
+                if (item == getCurrentItem()) {
+                    mPlayer.getStatus(item, false);
+                }
+                return item;
+            }
+        }
+        return null;
+    }
+
+    public void pause() {
+        if (DEBUG) {
+            log("pause");
+        }
+        if (!mSessionValid) {
+            return;
+        }
+        checkPlayer();
+        mPaused = true;
+        updatePlaybackState();
+    }
+
+    public void resume() {
+        if (DEBUG) {
+            log("resume");
+        }
+        if (!mSessionValid) {
+            return;
+        }
+        checkPlayer();
+        mPaused = false;
+        updatePlaybackState();
+    }
+
+    public void stop() {
+        if (DEBUG) {
+            log("stop");
+        }
+        if (!mSessionValid) {
+            return;
+        }
+        checkPlayer();
+        mPlayer.stop();
+        mPlaylist.clear();
+        mPaused = false;
+        updateStatus();
+    }
+
+    public String startSession() {
+        if (!mSessionValid) {
+            mSessionId++;
+            mItemId = 0;
+            mPaused = false;
+            mSessionValid = true;
+            return Integer.toString(mSessionId);
+        }
+        return null;
+    }
+
+    public boolean endSession() {
+        if (mSessionValid) {
+            mSessionValid = false;
+            return true;
+        }
+        return false;
+    }
+
+    MediaSessionStatus getSessionStatus(String sid) {
+        int sessionState = (sid != null && sid.equals(mSessionId)) ?
+                MediaSessionStatus.SESSION_STATE_ACTIVE :
+                    MediaSessionStatus.SESSION_STATE_INVALIDATED;
+
+        return new MediaSessionStatus.Builder(sessionState)
+                .setQueuePaused(mPaused)
+                .build();
+    }
+
+    // Suspend the playback manager. Put the current item back into PENDING
+    // state, and remember the current playback position. Called when switching
+    // to a different player (route).
+    public void suspend(long pos) {
+        for (PlaylistItem item : mPlaylist) {
+            item.setRemoteItemId(null);
+            item.setDuration(0);
+        }
+        PlaylistItem item = getCurrentItem();
+        if (DEBUG) {
+            log("suspend: item=" + item + ", pos=" + pos);
+        }
+        if (item != null) {
+            if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
+                    || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
+                item.setState(MediaItemStatus.PLAYBACK_STATE_PENDING);
+                item.setPosition(pos);
+            }
+        }
+    }
+
+    // Unsuspend the playback manager. Restart playback on new player (route).
+    // This will resume playback of current item. Furthermore, if the new player
+    // supports queuing, playlist will be re-established on the remote player.
+    public void unsuspend() {
+        if (DEBUG) {
+            log("unsuspend");
+        }
+        if (mPlayer.isQueuingSupported()) {
+            for (PlaylistItem item : mPlaylist) {
+                mPlayer.enqueue(item);
+            }
+        }
+        updatePlaybackState();
+    }
+
+    // Player.Callback
+    @Override
+    public void onError() {
+        finishItem(true);
+    }
+
+    @Override
+    public void onCompletion() {
+        finishItem(false);
+    }
+
+    @Override
+    public void onPlaylistChanged() {
+        // Playlist has changed, update the cached playlist
+        updateStatus();
+    }
+
+    @Override
+    public void onPlaylistReady() {
+        // Notify activity to update Ui
+        if (mCallback != null) {
+            mCallback.onStatusChanged();
+        }
+    }
+
+    private void log(String message) {
+        Log.d(TAG, mName + ": " + message);
+    }
+
+    private void checkPlayer() {
+        if (mPlayer == null) {
+            throw new IllegalStateException("Player not set!");
+        }
+    }
+
+    private void checkSession() {
+        if (!mSessionValid) {
+            throw new IllegalStateException("Session not set!");
+        }
+    }
+
+    private void checkPlayerAndSession() {
+        checkPlayer();
+        checkSession();
+    }
+
+    private void checkItemCurrent(String iid) {
+        PlaylistItem item = getCurrentItem();
+        if (item == null || !item.getItemId().equals(iid)) {
+            throw new IllegalArgumentException("Item is not current!");
+        }
+    }
+
+    private void updatePlaybackState() {
+        PlaylistItem item = getCurrentItem();
+        if (item != null) {
+            if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PENDING) {
+                item.setState(mPaused ? MediaItemStatus.PLAYBACK_STATE_PAUSED
+                        : MediaItemStatus.PLAYBACK_STATE_PLAYING);
+                if (!mPlayer.isQueuingSupported()) {
+                    mPlayer.play(item);
+                }
+            } else if (mPaused && item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) {
+                mPlayer.pause();
+                item.setState(MediaItemStatus.PLAYBACK_STATE_PAUSED);
+            } else if (!mPaused && item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
+                mPlayer.resume();
+                item.setState(MediaItemStatus.PLAYBACK_STATE_PLAYING);
+            }
+            // notify client that item playback status has changed
+            if (mCallback != null) {
+                mCallback.onItemChanged(item);
+            }
+        }
+        updateStatus();
+    }
+
+    private PlaylistItem removeItem(String iid, int state) {
+        checkPlayerAndSession();
+        List<PlaylistItem> queue =
+                new ArrayList<PlaylistItem>(mPlaylist.size());
+        PlaylistItem found = null;
+        for (PlaylistItem item : mPlaylist) {
+            if (iid.equals(item.getItemId())) {
+                if (mPlayer.isQueuingSupported()) {
+                    mPlayer.remove(item.getRemoteItemId());
+                } else if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
+                        || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED){
+                    mPlayer.stop();
+                }
+                item.setState(state);
+                found = item;
+                // notify client that item is now removed
+                if (mCallback != null) {
+                    mCallback.onItemChanged(found);
+                }
+            } else {
+                queue.add(item);
+            }
+        }
+        if (found != null) {
+            mPlaylist = queue;
+            updatePlaybackState();
+        } else {
+            log("item not found");
+        }
+        return found;
+    }
+
+    private void finishItem(boolean error) {
+        PlaylistItem item = getCurrentItem();
+        if (item != null) {
+            removeItem(item.getItemId(), error ?
+                    MediaItemStatus.PLAYBACK_STATE_ERROR :
+                        MediaItemStatus.PLAYBACK_STATE_FINISHED);
+            updateStatus();
+        }
+    }
+
+    // set the Player that this playback manager will interact with
+    public void setPlayer(Player player) {
+        mPlayer = player;
+        checkPlayer();
+        mPlayer.setCallback(this);
+    }
+
+    // provide a callback interface to tell the UI when significant state changes occur
+    public void setCallback(Callback callback) {
+        mCallback = callback;
+    }
+
+    @Override
+    public String toString() {
+        String result = "Media Queue: ";
+        if (!mPlaylist.isEmpty()) {
+            for (PlaylistItem item : mPlaylist) {
+                result += "\n" + item.toString();
+            }
+        } else {
+            result += "<empty>";
+        }
+        return result;
+    }
+
+    public interface Callback {
+        void onStatusChanged();
+        void onItemChanged(PlaylistItem item);
+    }
+}
diff --git a/samples/devbytes/animation/CardFlip/AndroidManifest.xml b/samples/devbytes/animation/CardFlip/AndroidManifest.xml
new file mode 100644
index 0000000..d915f3e
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.example.android.cardflip"
+          android:versionCode="1"
+          android:versionName="1.0">
+    <uses-sdk android:minSdkVersion="14"
+              android:targetSdkVersion="17"/>
+    <application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
+        <activity android:name=".CardFlip"
+                  android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/samples/devbytes/animation/CardFlip/res/drawable-hdpi/blue.jpg b/samples/devbytes/animation/CardFlip/res/drawable-hdpi/blue.jpg
new file mode 100644
index 0000000..2f7b786
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/res/drawable-hdpi/blue.jpg
Binary files differ
diff --git a/samples/devbytes/animation/CardFlip/res/drawable-hdpi/ic_launcher.png b/samples/devbytes/animation/CardFlip/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..96a442e
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/CardFlip/res/drawable-hdpi/red.jpg b/samples/devbytes/animation/CardFlip/res/drawable-hdpi/red.jpg
new file mode 100644
index 0000000..f433603
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/res/drawable-hdpi/red.jpg
Binary files differ
diff --git a/samples/devbytes/animation/CardFlip/res/drawable-ldpi/ic_launcher.png b/samples/devbytes/animation/CardFlip/res/drawable-ldpi/ic_launcher.png
new file mode 100644
index 0000000..9923872
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/res/drawable-ldpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/CardFlip/res/drawable-mdpi/ic_launcher.png b/samples/devbytes/animation/CardFlip/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..359047d
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/CardFlip/res/drawable-xhdpi/ic_launcher.png b/samples/devbytes/animation/CardFlip/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..71c6d76
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/CardFlip/res/layout/main.xml b/samples/devbytes/animation/CardFlip/res/layout/main.xml
new file mode 100644
index 0000000..ef23d69
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/res/layout/main.xml
@@ -0,0 +1,19 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/main_relative_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+</RelativeLayout>
\ No newline at end of file
diff --git a/samples/devbytes/animation/CardFlip/res/values/integer.xml b/samples/devbytes/animation/CardFlip/res/values/integer.xml
new file mode 100644
index 0000000..2eb363c
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/res/values/integer.xml
@@ -0,0 +1,20 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+
+    <integer name="vertical_card_magin">30</integer>
+    <integer name="horizontal_card_magin">30</integer>
+
+</resources>
\ No newline at end of file
diff --git a/samples/devbytes/animation/CardFlip/res/values/strings.xml b/samples/devbytes/animation/CardFlip/res/values/strings.xml
new file mode 100644
index 0000000..bd248d1
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/res/values/strings.xml
@@ -0,0 +1,19 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+
+    <string name="app_name">CardFlip</string>
+
+</resources>
diff --git a/samples/devbytes/animation/CardFlip/src/com/example/android/cardflip/CardFlip.java b/samples/devbytes/animation/CardFlip/src/com/example/android/cardflip/CardFlip.java
new file mode 100644
index 0000000..746afec
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/src/com/example/android/cardflip/CardFlip.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.cardflip;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.app.Activity;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ViewTreeObserver;
+import android.widget.RelativeLayout;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This application creates 2 stacks of playing cards. Using fling events,
+ * these cards can be flipped from one stack to another where each flip comes with
+ * an associated animation. The cards can be flipped horizontally from left to right
+ * or right to left depending on which stack the animating card currently belongs to.
+ *
+ * This application demonstrates an animation where a stack of cards can either be
+ * be rotated out or back in about their bottom left corner in a counter-clockwise direction.
+ * Rotate out: Down fling on stack of cards
+ * Rotate in: Up fling on stack of cards
+ * Full rotation: Tap on stack of cards
+ *
+ * Note that in this demo touch events are disabled in the middle of any animation so
+ * only one card can be flipped at a time. When the cards are in a rotated-out
+ * state, no new cards can be rotated to or from that stack. These changes were made to
+ * simplify the code for this demo.
+ */
+
+public class CardFlip extends Activity implements CardFlipListener {
+
+    final static int CARD_PILE_OFFSET = 3;
+    final static int STARTING_NUMBER_CARDS = 15;
+    final static int RIGHT_STACK = 0;
+    final static int LEFT_STACK = 1;
+
+    int mCardWidth = 0;
+    int mCardHeight = 0;
+
+    int mVerticalPadding;
+    int mHorizontalPadding;
+
+    boolean mTouchEventsEnabled = true;
+    boolean[] mIsStackEnabled;
+
+    RelativeLayout mLayout;
+
+    List<ArrayList<CardView>> mStackCards;
+
+    GestureDetector gDetector;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.main);
+
+        mStackCards = new ArrayList<ArrayList<CardView>>();
+        mStackCards.add(new ArrayList<CardView>());
+        mStackCards.add(new ArrayList<CardView>());
+
+        mIsStackEnabled = new boolean[2];
+        mIsStackEnabled[0] = true;
+        mIsStackEnabled[1] = true;
+
+        mVerticalPadding = getResources().getInteger(R.integer.vertical_card_magin);
+        mHorizontalPadding = getResources().getInteger(R.integer.horizontal_card_magin);
+
+        gDetector = new GestureDetector(this, mGestureListener);
+
+        mLayout = (RelativeLayout)findViewById(R.id.main_relative_layout);
+        ViewTreeObserver observer = mLayout.getViewTreeObserver();
+        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+            @Override
+            public void onGlobalLayout() {
+                if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+                    mLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                } else {
+                    mLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+                }
+
+                mCardHeight = mLayout.getHeight();
+                mCardWidth = mLayout.getWidth() / 2;
+
+                for (int x = 0; x < STARTING_NUMBER_CARDS; x++) {
+                    addNewCard(RIGHT_STACK);
+                }
+            }
+        });
+    }
+
+    /**
+     * Adds a new card to the specified stack. Also performs all the necessary layout setup
+     * to place the card in the correct position.
+     */
+    public void addNewCard(int stack) {
+        CardView view = new CardView(this);
+        view.updateTranslation(mStackCards.get(stack).size());
+        view.setCardFlipListener(this);
+        view.setPadding(mHorizontalPadding, mVerticalPadding, mHorizontalPadding, mVerticalPadding);
+
+        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(mCardWidth,
+                mCardHeight);
+        params.topMargin = 0;
+        params.leftMargin = (stack == RIGHT_STACK ? mCardWidth : 0);
+
+        mStackCards.get(stack).add(view);
+        mLayout.addView(view, params);
+    }
+
+    /**
+     * Gesture Detector listens for fling events in order to potentially initiate
+     * a card flip event when a fling event occurs. Also listens for tap events in
+     * order to potentially initiate a full rotation animation.
+     */
+    private GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector
+            .SimpleOnGestureListener() {
+        @Override
+        public boolean onSingleTapUp(MotionEvent motionEvent) {
+            int stack = getStack(motionEvent);
+            rotateCardsFullRotation(stack, CardView.Corner.BOTTOM_LEFT);
+            return true;
+        }
+
+        @Override
+        public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent2, float v,
+                               float v2) {
+            int stack = getStack(motionEvent);
+            ArrayList<CardView> cardStack = mStackCards.get(stack);
+            int size = cardStack.size();
+            if (size > 0) {
+                rotateCardView(cardStack.get(size - 1), stack, v, v2);
+            }
+            return true;
+        }
+    };
+
+    /** Returns the appropriate stack corresponding to the MotionEvent. */
+    public int getStack(MotionEvent ev) {
+        boolean isLeft = ev.getX() <= mCardWidth;
+        return isLeft ? LEFT_STACK : RIGHT_STACK;
+    }
+
+    /**
+     * Uses the stack parameter, along with the velocity values of the fling event
+     * to determine in what direction the card must be flipped. By the same logic, the
+     * new stack that the card belongs to after the animation is also determined
+     * and updated.
+     */
+    public void rotateCardView(final CardView cardView, int stack, float velocityX,
+                               float velocityY) {
+
+        boolean xGreaterThanY = Math.abs(velocityX) > Math.abs(velocityY);
+
+        boolean bothStacksEnabled = mIsStackEnabled[RIGHT_STACK] && mIsStackEnabled[LEFT_STACK];
+
+        ArrayList<CardView>leftStack = mStackCards.get(LEFT_STACK);
+        ArrayList<CardView>rightStack = mStackCards.get(RIGHT_STACK);
+
+        switch (stack) {
+            case RIGHT_STACK:
+                if (velocityX < 0 &&  xGreaterThanY) {
+                    if (!bothStacksEnabled) {
+                        break;
+                    }
+                    mLayout.bringChildToFront(cardView);
+                    mLayout.requestLayout();
+                    rightStack.remove(rightStack.size() - 1);
+                    leftStack.add(cardView);
+                    cardView.flipRightToLeft(leftStack.size() - 1, (int)velocityX);
+                    break;
+                } else if (!xGreaterThanY) {
+                    boolean rotateCardsOut = velocityY > 0;
+                    rotateCards(RIGHT_STACK, CardView.Corner.BOTTOM_LEFT, rotateCardsOut);
+                }
+                break;
+            case LEFT_STACK:
+                if (velocityX > 0 && xGreaterThanY) {
+                    if (!bothStacksEnabled) {
+                        break;
+                    }
+                    mLayout.bringChildToFront(cardView);
+                    mLayout.requestLayout();
+                    leftStack.remove(leftStack.size() - 1);
+                    rightStack.add(cardView);
+                    cardView.flipLeftToRight(rightStack.size() - 1, (int)velocityX);
+                    break;
+                } else if (!xGreaterThanY) {
+                    boolean rotateCardsOut = velocityY > 0;
+                    rotateCards(LEFT_STACK, CardView.Corner.BOTTOM_LEFT, rotateCardsOut);
+                }
+                break;
+            default:
+                break;
+        }
+    }
+
+    @Override
+    public void onCardFlipEnd() {
+        mTouchEventsEnabled = true;
+    }
+
+    @Override
+    public void onCardFlipStart() {
+        mTouchEventsEnabled = false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent me) {
+        if (mTouchEventsEnabled) {
+            return gDetector.onTouchEvent(me);
+        } else {
+            return super.onTouchEvent(me);
+        }
+    }
+
+    /**
+     * Retrieves an animator object for each card in the specified stack that either
+     * rotates it in or out depending on its current state. All of these animations
+     * are then played together.
+     */
+    public void rotateCards (final int stack, CardView.Corner corner,
+                             final boolean isRotatingOut) {
+        List<Animator> animations = new ArrayList<Animator>();
+
+        ArrayList <CardView> cards = mStackCards.get(stack);
+
+        for (int i = 0; i < cards.size(); i++) {
+            CardView cardView = cards.get(i);
+            animations.add(cardView.getRotationAnimator(i, corner, isRotatingOut, false));
+            mLayout.bringChildToFront(cardView);
+        }
+        /** All the cards are being brought to the front in order to guarantee that
+         * the cards being rotated in the current stack will overlay the cards in the
+         * other stack. After the z-ordering of all the cards is updated, a layout must
+         * be requested in order to apply the changes made.*/
+        mLayout.requestLayout();
+
+        AnimatorSet set = new AnimatorSet();
+        set.playTogether(animations);
+        set.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mIsStackEnabled[stack] = !isRotatingOut;
+            }
+        });
+        set.start();
+    }
+
+    /**
+     * Retrieves an animator object for each card in the specified stack to complete a
+     * full revolution around one of its corners, and plays all of them together.
+     */
+    public void rotateCardsFullRotation (int stack, CardView.Corner corner) {
+        List<Animator> animations = new ArrayList<Animator>();
+
+        ArrayList <CardView> cards = mStackCards.get(stack);
+        for (int i = 0; i < cards.size(); i++) {
+            CardView cardView = cards.get(i);
+            animations.add(cardView.getFullRotationAnimator(i, corner, false));
+            mLayout.bringChildToFront(cardView);
+        }
+        /** Same reasoning for bringing cards to front as in rotateCards().*/
+        mLayout.requestLayout();
+
+        mTouchEventsEnabled = false;
+        AnimatorSet set = new AnimatorSet();
+        set.playTogether(animations);
+        set.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mTouchEventsEnabled = true;
+            }
+        });
+        set.start();
+    }
+}
\ No newline at end of file
diff --git a/samples/devbytes/animation/CardFlip/src/com/example/android/cardflip/CardFlipListener.java b/samples/devbytes/animation/CardFlip/src/com/example/android/cardflip/CardFlipListener.java
new file mode 100644
index 0000000..0af6941
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/src/com/example/android/cardflip/CardFlipListener.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.cardflip;
+
+/**
+ * This interface is used to prevent flipping multiple cards at the same time.
+ * These callback methods are used to disable and re-enable touches when a card
+ * flip animation begins and ends respectively.
+ * */
+public interface CardFlipListener {
+    public void onCardFlipEnd();
+    public void onCardFlipStart();
+}
diff --git a/samples/devbytes/animation/CardFlip/src/com/example/android/cardflip/CardView.java b/samples/devbytes/animation/CardFlip/src/com/example/android/cardflip/CardView.java
new file mode 100644
index 0000000..9a3ab71
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/src/com/example/android/cardflip/CardView.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.cardflip;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.Keyframe;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+
+/**
+ * This CardView object is a view which can flip horizontally about its edges,
+ * as well as rotate clockwise or counter-clockwise about any of its corners. In
+ * the middle of a flip animation, this view darkens to imitate a shadow-like effect.
+ *
+ * The key behind the design of this view is the fact that the layout parameters and
+ * the animation properties of this view are updated and reset respectively after
+ * every single animation. Therefore, every consecutive animation that this
+ * view experiences is completely independent of what its prior state was.
+ */
+public class CardView extends ImageView {
+
+    enum Corner {
+        TOP_LEFT,
+        TOP_RIGHT,
+        BOTTOM_LEFT,
+        BOTTOM_RIGHT
+    }
+
+    private final int CAMERA_DISTANCE = 8000;
+    private final int MIN_FLIP_DURATION = 300;
+    private final int VELOCITY_TO_DURATION_CONSTANT = 15;
+    private final int MAX_FLIP_DURATION = 700;
+    private final int ROTATION_PER_CARD = 2;
+    private final int ROTATION_DELAY_PER_CARD = 50;
+    private final int ROTATION_DURATION = 2000;
+    private final int ANTIALIAS_BORDER = 1;
+
+    private BitmapDrawable mFrontBitmapDrawable, mBackBitmapDrawable, mCurrentBitmapDrawable;
+
+    private boolean mIsFrontShowing = true;
+    private boolean mIsHorizontallyFlipped = false;
+
+    private Matrix mHorizontalFlipMatrix;
+
+    private CardFlipListener mCardFlipListener;
+
+    public CardView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public CardView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    /** Loads the bitmap drawables used for the front and back for this card.*/
+    public void init(Context context) {
+        mHorizontalFlipMatrix = new Matrix();
+
+        setCameraDistance(CAMERA_DISTANCE);
+
+        mFrontBitmapDrawable = bitmapWithBorder((BitmapDrawable)getResources()
+                .getDrawable(R.drawable.red));
+        mBackBitmapDrawable = bitmapWithBorder((BitmapDrawable) getResources()
+                .getDrawable(R.drawable.blue));
+
+        updateDrawableBitmap();
+    }
+
+    /**
+     *  Adding a 1 pixel transparent border around the bitmap can be used to
+     *  anti-alias the image as it rotates.
+     */
+    private BitmapDrawable bitmapWithBorder(BitmapDrawable bitmapDrawable) {
+        Bitmap bitmapWithBorder = Bitmap.createBitmap(bitmapDrawable.getIntrinsicWidth() +
+                ANTIALIAS_BORDER * 2, bitmapDrawable.getIntrinsicHeight() + ANTIALIAS_BORDER * 2,
+                Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(bitmapWithBorder);
+        canvas.drawBitmap(bitmapDrawable.getBitmap(), ANTIALIAS_BORDER, ANTIALIAS_BORDER, null);
+        return new BitmapDrawable(getResources(), bitmapWithBorder);
+    }
+
+    /** Initiates a horizontal flip from right to left. */
+    public void flipRightToLeft(int numberInPile, int velocity) {
+        setPivotX(0);
+        flipHorizontally(numberInPile, false, velocity);
+    }
+
+    /** Initiates a horizontal flip from left to right. */
+    public void flipLeftToRight(int numberInPile, int velocity) {
+        setPivotX(getWidth());
+        flipHorizontally(numberInPile, true, velocity);
+    }
+
+    /**
+     * Animates a horizontal (about the y-axis) flip of this card.
+     * @param numberInPile Specifies how many cards are underneath this card in the new
+     *                     pile so as to properly adjust its position offset in the stack.
+     * @param clockwise Specifies whether the horizontal animation is 180 degrees
+     *                  clockwise or 180 degrees counter clockwise.
+     */
+    public void flipHorizontally (int numberInPile, boolean clockwise, int velocity) {
+        toggleFrontShowing();
+
+        PropertyValuesHolder rotation = PropertyValuesHolder.ofFloat(View.ROTATION_Y,
+                clockwise ? 180 : -180);
+
+        PropertyValuesHolder xOffset = PropertyValuesHolder.ofFloat(View.TRANSLATION_X,
+                numberInPile * CardFlip.CARD_PILE_OFFSET);
+        PropertyValuesHolder yOffset = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y,
+                numberInPile * CardFlip.CARD_PILE_OFFSET);
+
+        ObjectAnimator cardAnimator = ObjectAnimator.ofPropertyValuesHolder(this, rotation,
+                xOffset, yOffset);
+        cardAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                if (valueAnimator.getAnimatedFraction() >= 0.5) {
+                    updateDrawableBitmap();
+                }
+            }
+        });
+
+        Keyframe shadowKeyFrameStart = Keyframe.ofFloat(0, 0);
+        Keyframe shadowKeyFrameMid = Keyframe.ofFloat(0.5f, 1);
+        Keyframe shadowKeyFrameEnd = Keyframe.ofFloat(1, 0);
+        PropertyValuesHolder shadowPropertyValuesHolder = PropertyValuesHolder.ofKeyframe
+                ("shadow", shadowKeyFrameStart, shadowKeyFrameMid, shadowKeyFrameEnd);
+        ObjectAnimator colorizer = ObjectAnimator.ofPropertyValuesHolder(this,
+                shadowPropertyValuesHolder);
+
+        mCardFlipListener.onCardFlipStart();
+        AnimatorSet set = new AnimatorSet();
+        int duration = MAX_FLIP_DURATION - Math.abs(velocity) / VELOCITY_TO_DURATION_CONSTANT;
+        duration = duration < MIN_FLIP_DURATION ? MIN_FLIP_DURATION : duration;
+        set.setDuration(duration);
+        set.playTogether(cardAnimator, colorizer);
+        set.setInterpolator(new AccelerateDecelerateInterpolator());
+        set.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                toggleIsHorizontallyFlipped();
+                updateDrawableBitmap();
+                updateLayoutParams();
+                mCardFlipListener.onCardFlipEnd();
+            }
+        });
+        set.start();
+    }
+
+    /** Darkens this ImageView's image by applying a shadow color filter over it. */
+    public void setShadow(float value) {
+        int colorValue = (int)(255 - 200 * value);
+        setColorFilter(Color.rgb(colorValue, colorValue, colorValue),
+                android.graphics.PorterDuff.Mode.MULTIPLY);
+    }
+
+    public void toggleFrontShowing() {
+        mIsFrontShowing = !mIsFrontShowing;
+    }
+
+    public void toggleIsHorizontallyFlipped() {
+        mIsHorizontallyFlipped = !mIsHorizontallyFlipped;
+        invalidate();
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        mHorizontalFlipMatrix.setScale(-1, 1, w / 2, h / 2);
+    }
+
+    /**
+     *  Scale the canvas horizontally about its midpoint in the case that the card
+     *  is in a horizontally flipped state.
+     */
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (mIsHorizontallyFlipped) {
+            canvas.concat(mHorizontalFlipMatrix);
+        }
+        super.onDraw(canvas);
+    }
+
+    /**
+     *  Updates the layout parameters of this view so as to reset the rotationX and
+     *  rotationY parameters, and remain independent of its previous position, while
+     *  also maintaining its current position in the layout.
+     */
+    public void updateLayoutParams () {
+        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams();
+
+        params.leftMargin = (int)(params.leftMargin + ((Math.abs(getRotationY()) % 360) / 180) *
+                (2 * getPivotX () - getWidth()));
+
+        setRotationX(0);
+        setRotationY(0);
+
+        setLayoutParams(params);
+    }
+
+    /**
+     * Toggles the visible bitmap of this view between its front and back drawables
+     * respectively.
+     */
+    public void updateDrawableBitmap () {
+        mCurrentBitmapDrawable = mIsFrontShowing ? mFrontBitmapDrawable : mBackBitmapDrawable;
+        setImageDrawable(mCurrentBitmapDrawable);
+    }
+
+    /**
+     * Sets the appropriate translation of this card depending on how many cards
+     * are in the pile underneath it.
+     */
+    public void updateTranslation (int numInPile) {
+        setTranslationX(CardFlip.CARD_PILE_OFFSET * numInPile);
+        setTranslationY(CardFlip.CARD_PILE_OFFSET * numInPile);
+    }
+
+    /**
+     * Returns a rotation animation which rotates this card by some degree about
+     * one of its corners either in the clockwise or counter-clockwise direction.
+     * Depending on how many cards lie below this one in the stack, this card will
+     * be rotated by a different amount so all the cards are visible when rotated out.
+     */
+    public ObjectAnimator getRotationAnimator (int cardFromTop, Corner corner,
+                                               boolean isRotatingOut, boolean isClockwise) {
+        rotateCardAroundCorner(corner);
+        int rotation = cardFromTop * ROTATION_PER_CARD;
+
+        if (!isClockwise) {
+            rotation = -rotation;
+        }
+
+        if (!isRotatingOut) {
+            rotation = 0;
+        }
+
+        return ObjectAnimator.ofFloat(this, View.ROTATION, rotation);
+    }
+
+    /**
+     * Returns a full rotation animator which rotates this card by 360 degrees
+     * about one of its corners either in the clockwise or counter-clockwise direction.
+     * Depending on how many cards lie below this one in the stack, a different start
+     * delay is applied to the animation so the cards don't all animate at once.
+     */
+    public ObjectAnimator getFullRotationAnimator (int cardFromTop, Corner corner,
+                                                   boolean isClockwise) {
+        final int currentRotation = (int)getRotation();
+
+        rotateCardAroundCorner(corner);
+        int rotation = 360 - currentRotation;
+        rotation =  isClockwise ? rotation : -rotation;
+
+        ObjectAnimator animator = ObjectAnimator.ofFloat(this, View.ROTATION, rotation);
+
+        animator.setStartDelay(ROTATION_DELAY_PER_CARD * cardFromTop);
+        animator.setDuration(ROTATION_DURATION);
+
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                setRotation(currentRotation);
+            }
+        });
+
+        return animator;
+    }
+
+    /**
+     * Sets the appropriate pivot of this card so that it can be rotated about
+     * any one of its four corners.
+     */
+    public void rotateCardAroundCorner(Corner corner) {
+        switch(corner) {
+            case TOP_LEFT:
+                setPivotX(0);
+                setPivotY(0);
+                break;
+            case TOP_RIGHT:
+                setPivotX(getWidth());
+                setPivotY(0);
+                break;
+            case BOTTOM_LEFT:
+                setPivotX(0);
+                setPivotY(getHeight());
+                break;
+            case BOTTOM_RIGHT:
+                setPivotX(getWidth());
+                setPivotY(getHeight());
+                break;
+        }
+    }
+
+    public void setCardFlipListener(CardFlipListener cardFlipListener) {
+        mCardFlipListener = cardFlipListener;
+    }
+
+}
diff --git a/samples/devbytes/animation/ListViewCellInsertion/AndroidManifest.xml b/samples/devbytes/animation/ListViewCellInsertion/AndroidManifest.xml
new file mode 100644
index 0000000..230c2e7
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.example.android.insertingcells"
+          android:versionCode="1"
+          android:versionName="1.0">
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-sdk android:minSdkVersion="14"
+              android:targetSdkVersion="17"/>
+    <application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
+        <activity android:name=".InsertingCells"
+                  android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/samples/devbytes/animation/ListViewCellInsertion/res/drawable-hdpi/border.9.png b/samples/devbytes/animation/ListViewCellInsertion/res/drawable-hdpi/border.9.png
new file mode 100644
index 0000000..f76e008
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/res/drawable-hdpi/border.9.png
Binary files differ
diff --git a/samples/devbytes/animation/ListViewCellInsertion/res/drawable-hdpi/chameleon.jpg b/samples/devbytes/animation/ListViewCellInsertion/res/drawable-hdpi/chameleon.jpg
new file mode 100644
index 0000000..686cc88
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/res/drawable-hdpi/chameleon.jpg
Binary files differ
diff --git a/samples/devbytes/animation/ListViewCellInsertion/res/drawable-hdpi/flower.jpg b/samples/devbytes/animation/ListViewCellInsertion/res/drawable-hdpi/flower.jpg
new file mode 100644
index 0000000..8842483
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/res/drawable-hdpi/flower.jpg
Binary files differ
diff --git a/samples/devbytes/animation/ListViewCellInsertion/res/drawable-hdpi/ic_launcher.png b/samples/devbytes/animation/ListViewCellInsertion/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..96a442e
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/ListViewCellInsertion/res/drawable-hdpi/rock.jpg b/samples/devbytes/animation/ListViewCellInsertion/res/drawable-hdpi/rock.jpg
new file mode 100644
index 0000000..8ea0e85
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/res/drawable-hdpi/rock.jpg
Binary files differ
diff --git a/samples/devbytes/animation/ListViewCellInsertion/res/drawable-ldpi/ic_launcher.png b/samples/devbytes/animation/ListViewCellInsertion/res/drawable-ldpi/ic_launcher.png
new file mode 100644
index 0000000..9923872
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/res/drawable-ldpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/ListViewCellInsertion/res/drawable-mdpi/ic_launcher.png b/samples/devbytes/animation/ListViewCellInsertion/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..359047d
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/ListViewCellInsertion/res/drawable-xhdpi/ic_launcher.png b/samples/devbytes/animation/ListViewCellInsertion/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..71c6d76
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/ListViewCellInsertion/res/layout/activity_main.xml b/samples/devbytes/animation/ListViewCellInsertion/res/layout/activity_main.xml
new file mode 100644
index 0000000..e515309
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/res/layout/activity_main.xml
@@ -0,0 +1,53 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:id="@+id/relative_layout"
+                xmlns:tools="http://schemas.android.com/tools"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:orientation="vertical"
+                tools:context=".MainActivity"
+                android:background="#111">
+
+    <LinearLayout
+            android:id="@+id/linear_layout"
+            android:layout_height="@dimen/cell_height"
+            android:layout_width="match_parent"
+            android:orientation="horizontal">
+
+        <com.example.android.insertingcells.RoundView
+                android:id="@+id/round_view"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"/>
+
+        <Button
+                android:id="@+id/add_row_button"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:text="@string/add_row"
+                android:onClick="addRow"
+                android:layout_weight="2"/>
+
+    </LinearLayout>
+
+    <com.example.android.insertingcells.InsertionListView
+            android:id="@+id/listview"
+            android:layout_below="@id/linear_layout"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
+
+</RelativeLayout>
diff --git a/samples/devbytes/animation/ListViewCellInsertion/res/layout/list_view_item.xml b/samples/devbytes/animation/ListViewCellInsertion/res/layout/list_view_item.xml
new file mode 100644
index 0000000..e984b2e
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/res/layout/list_view_item.xml
@@ -0,0 +1,40 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:id="@+id/item_linear_layout"
+              android:layout_height="match_parent"
+              android:layout_width="match_parent"
+              android:orientation="horizontal"
+              android:background="@drawable/border">
+
+    <ImageView
+            android:id="@+id/image_view"
+            android:layout_height="match_parent"
+            android:layout_width="0dp"
+            android:gravity="center_vertical"
+            android:layout_weight="1"
+            android:scaleType="center"/>
+
+    <TextView
+            android:id="@+id/text_view"
+            android:layout_height="fill_parent"
+            android:layout_width="0dp"
+            android:gravity="center"
+            android:layout_weight="2"
+            android:textStyle="bold"
+            android:textSize="22sp"
+            android:textColor="#ffffff"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/devbytes/animation/ListViewCellInsertion/res/values/dimensions.xml b/samples/devbytes/animation/ListViewCellInsertion/res/values/dimensions.xml
new file mode 100644
index 0000000..a18add3
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/res/values/dimensions.xml
@@ -0,0 +1,21 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+
+    <dimen name="circle_radius">50dp</dimen>
+    <dimen name="cell_height">150dp</dimen>
+
+</resources>
diff --git a/samples/devbytes/animation/ListViewCellInsertion/res/values/strings.xml b/samples/devbytes/animation/ListViewCellInsertion/res/values/strings.xml
new file mode 100644
index 0000000..cb57d69
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/res/values/strings.xml
@@ -0,0 +1,21 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+
+    <string name="app_name">InsertingCells</string>
+    <string name="add_row">Add Row</string>
+
+</resources>
diff --git a/samples/devbytes/animation/ListViewCellInsertion/src/com/example/android/insertingcells/CustomArrayAdapter.java b/samples/devbytes/animation/ListViewCellInsertion/src/com/example/android/insertingcells/CustomArrayAdapter.java
new file mode 100644
index 0000000..870d279
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/src/com/example/android/insertingcells/CustomArrayAdapter.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.insertingcells;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * This custom array adapter is used to populate the ListView in this application.
+ * This adapter also maintains a map of unique stable ids for each object in the data set.
+ * Since this adapter has to support the addition of a new cell to the 1ist index, it also
+ * provides a mechanism to add a stable ID for new data that was recently inserted.
+ */
+public class CustomArrayAdapter extends ArrayAdapter<ListItemObject> {
+
+    HashMap<ListItemObject, Integer> mIdMap = new HashMap<ListItemObject, Integer>();
+    List<ListItemObject> mData;
+    Context mContext;
+    int mLayoutViewResourceId;
+    int mCounter;
+
+    public CustomArrayAdapter(Context context, int layoutViewResourceId,
+                              List <ListItemObject> data) {
+        super(context, layoutViewResourceId, data);
+        mData = data;
+        mContext = context;
+        mLayoutViewResourceId = layoutViewResourceId;
+        updateStableIds();
+    }
+
+    public long getItemId(int position) {
+        ListItemObject item = getItem(position);
+        if (mIdMap.containsKey(item)) {
+            return mIdMap.get(item);
+        }
+        return -1;
+    }
+
+    public void updateStableIds() {
+        mIdMap.clear();
+        mCounter = 0;
+        for (int i = 0; i < mData.size(); ++i) {
+            mIdMap.put(mData.get(i), mCounter++);
+        }
+    }
+
+    public void addStableIdForDataAtPosition(int position) {
+        mIdMap.put(mData.get(position), ++mCounter);
+    }
+
+    @Override
+    public boolean hasStableIds() {
+        return true;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+
+        ListItemObject obj = mData.get(position);
+
+        if(convertView == null) {
+            LayoutInflater inflater = ((Activity)mContext).getLayoutInflater();
+            convertView = inflater.inflate(mLayoutViewResourceId, parent, false);
+        }
+
+        convertView.setLayoutParams(new ListView.LayoutParams(ListView.LayoutParams
+                .MATCH_PARENT, obj.getHeight()));
+
+        ImageView imgView = (ImageView)convertView.findViewById(R.id.image_view);
+        TextView textView = (TextView)convertView.findViewById(R.id.text_view);
+
+        Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),
+                obj.getImgResource(), null);
+
+        textView.setText(obj.getTitle());
+        imgView.setImageBitmap(CustomArrayAdapter.getCroppedBitmap(bitmap));
+
+        return convertView;
+    }
+
+    /**
+     * Returns a circular cropped version of the bitmap passed in.
+     */
+    public static Bitmap getCroppedBitmap(Bitmap bitmap) {
+        Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(),
+                Config.ARGB_8888);
+
+        final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+
+        Canvas canvas = new Canvas(output);
+
+        final Paint paint = new Paint();
+        paint.setAntiAlias(true);
+
+        int halfWidth = bitmap.getWidth() / 2;
+        int halfHeight = bitmap.getHeight() / 2;
+
+        canvas.drawCircle(halfWidth, halfHeight, Math.max(halfWidth, halfHeight), paint);
+
+        paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
+
+        canvas.drawBitmap(bitmap, rect, rect, paint);
+
+        return output;
+    }
+}
\ No newline at end of file
diff --git a/samples/devbytes/animation/ListViewCellInsertion/src/com/example/android/insertingcells/InsertingCells.java b/samples/devbytes/animation/ListViewCellInsertion/src/com/example/android/insertingcells/InsertingCells.java
new file mode 100644
index 0000000..a58dbfb
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/src/com/example/android/insertingcells/InsertingCells.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.insertingcells;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.RelativeLayout;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This application creates a ListView to which new elements can be added from the
+ * top. When a new element is added, it is animated from above the bounds
+ * of the list to the top. When the list is scrolled all the way to the top and a new
+ * element is added, the row animation is accompanied by an image animation that pops
+ * out of the round view and pops into the correct position in the top cell.
+ */
+public class InsertingCells extends Activity implements OnRowAdditionAnimationListener {
+
+    private ListItemObject mValues[];
+
+    private InsertionListView mListView;
+
+    private Button mButton;
+
+    private Integer mItemNum = 0;
+
+    private RoundView mRoundView;
+
+    private int mCellHeight;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        mValues = new ListItemObject[] {
+                new ListItemObject("Chameleon", R.drawable.chameleon, 0),
+                new ListItemObject("Rock", R.drawable.rock, 0),
+                new ListItemObject("Flower", R.drawable.flower, 0),
+        };
+
+        mCellHeight = (int)(getResources().getDimension(R.dimen.cell_height));
+
+        List<ListItemObject> mData = new ArrayList<ListItemObject>();
+        CustomArrayAdapter mAdapter = new CustomArrayAdapter(this, R.layout.list_view_item, mData);
+        RelativeLayout mLayout = (RelativeLayout)findViewById(R.id.relative_layout);
+
+        mRoundView = (RoundView)findViewById(R.id.round_view);
+        mButton = (Button)findViewById(R.id.add_row_button);
+        mListView = (InsertionListView)findViewById(R.id.listview);
+
+        mListView.setAdapter(mAdapter);
+        mListView.setData(mData);
+        mListView.setLayout(mLayout);
+        mListView.setRowAdditionAnimationListener(this);
+    }
+
+    public void addRow(View view) {
+        mButton.setEnabled(false);
+
+        mItemNum++;
+        ListItemObject obj = mValues[mItemNum % mValues.length];
+        final ListItemObject newObj = new ListItemObject(obj.getTitle(), obj.getImgResource(),
+                mCellHeight);
+
+        boolean shouldAnimateInNewImage = mListView.shouldAnimateInNewImage();
+        if (!shouldAnimateInNewImage) {
+            mListView.addRow(newObj);
+            return;
+        }
+
+        mListView.setEnabled(false);
+        ObjectAnimator animator = mRoundView.getScalingAnimator();
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationRepeat(Animator animation) {
+                mListView.addRow(newObj);
+            }
+        });
+        animator.start();
+    }
+
+    @Override
+    public void onRowAdditionAnimationStart() {
+        mButton.setEnabled(false);
+    }
+
+    @Override
+    public void onRowAdditionAnimationEnd() {
+        mButton.setEnabled(true);
+    }
+}
diff --git a/samples/devbytes/animation/ListViewCellInsertion/src/com/example/android/insertingcells/InsertionListView.java b/samples/devbytes/animation/ListViewCellInsertion/src/com/example/android/insertingcells/InsertionListView.java
new file mode 100644
index 0000000..66866ba
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/src/com/example/android/insertingcells/InsertionListView.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.insertingcells;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.TypeEvaluator;
+import android.animation.ValueAnimator;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.view.animation.OvershootInterpolator;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * This ListView displays a set of ListItemObjects. By calling addRow with a new
+ * ListItemObject, it is added to the top of the ListView and the new row is animated
+ * in. If the ListView content is at the top (the scroll offset is 0), the animation of
+ * the new row is accompanied by an extra image animation that pops into place in its
+ * corresponding item in the ListView.
+ */
+public class InsertionListView extends ListView {
+
+    private static final int NEW_ROW_DURATION = 500;
+    private static final int OVERSHOOT_INTERPOLATOR_TENSION = 5;
+
+    private OvershootInterpolator sOvershootInterpolator;
+
+    private RelativeLayout mLayout;
+
+    private Context mContext;
+
+    private OnRowAdditionAnimationListener mRowAdditionAnimationListener;
+
+    private List<ListItemObject> mData;
+    private List<BitmapDrawable> mCellBitmapDrawables;
+
+    public InsertionListView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public InsertionListView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public InsertionListView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context);
+    }
+
+    public void init(Context context) {
+        setDivider(null);
+        mContext = context;
+        mCellBitmapDrawables = new ArrayList<BitmapDrawable>();
+        sOvershootInterpolator = new OvershootInterpolator(OVERSHOOT_INTERPOLATOR_TENSION);
+    }
+
+    /**
+     * Modifies the underlying data set and adapter through the addition of the new object
+     * to the first item of the ListView. The new cell is then animated into place from
+     * above the bounds of the ListView.
+     */
+    public void addRow(ListItemObject newObj) {
+
+        final CustomArrayAdapter adapter = (CustomArrayAdapter)getAdapter();
+
+        /**
+         * Stores the starting bounds and the corresponding bitmap drawables of every
+         * cell present in the ListView before the data set change takes place.
+         */
+        final HashMap<Long, Rect> listViewItemBounds = new HashMap<Long, Rect>();
+        final HashMap<Long, BitmapDrawable> listViewItemDrawables = new HashMap<Long,
+                BitmapDrawable>();
+
+        int firstVisiblePosition = getFirstVisiblePosition();
+        for (int i = 0; i < getChildCount(); ++i) {
+            View child = getChildAt(i);
+            int position = firstVisiblePosition + i;
+            long itemID = adapter.getItemId(position);
+            Rect startRect = new Rect(child.getLeft(), child.getTop(), child.getRight(),
+                    child.getBottom());
+            listViewItemBounds.put(itemID, startRect);
+            listViewItemDrawables.put(itemID, getBitmapDrawableFromView(child));
+        }
+
+        /** Adds the new object to the data set, thereby modifying the adapter,
+         *  as well as adding a stable Id for that specified object.*/
+        mData.add(0, newObj);
+        adapter.addStableIdForDataAtPosition(0);
+        adapter.notifyDataSetChanged();
+
+        final ViewTreeObserver observer = getViewTreeObserver();
+        observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+            public boolean onPreDraw() {
+                observer.removeOnPreDrawListener(this);
+
+                ArrayList<Animator> animations = new ArrayList<Animator>();
+
+                final View newCell = getChildAt(0);
+                final ImageView imgView = (ImageView)newCell.findViewById(R.id.image_view);
+                final ImageView copyImgView = new ImageView(mContext);
+
+                int firstVisiblePosition = getFirstVisiblePosition();
+                final boolean shouldAnimateInNewRow = shouldAnimateInNewRow();
+                final boolean shouldAnimateInImage = shouldAnimateInNewImage();
+
+                if (shouldAnimateInNewRow) {
+                    /** Fades in the text of the first cell. */
+                    TextView textView = (TextView)newCell.findViewById(R.id.text_view);
+                    ObjectAnimator textAlphaAnimator = ObjectAnimator.ofFloat(textView,
+                            View.ALPHA, 0.0f, 1.0f);
+                    animations.add(textAlphaAnimator);
+
+                    /** Animates in the extra hover view corresponding to the image
+                     * in the top row of the ListView. */
+                    if (shouldAnimateInImage) {
+
+                        int width = imgView.getWidth();
+                        int height = imgView.getHeight();
+
+                        Point childLoc = getLocationOnScreen(newCell);
+                        Point layoutLoc = getLocationOnScreen(mLayout);
+
+                        ListItemObject obj = mData.get(0);
+                        Bitmap bitmap = CustomArrayAdapter.getCroppedBitmap(BitmapFactory
+                                .decodeResource(mContext.getResources(), obj.getImgResource(),
+                                        null));
+                        copyImgView.setImageBitmap(bitmap);
+
+                        imgView.setVisibility(View.INVISIBLE);
+
+                        copyImgView.setScaleType(ImageView.ScaleType.CENTER);
+
+                        ObjectAnimator imgViewTranslation = ObjectAnimator.ofFloat(copyImgView,
+                                View.Y, childLoc.y - layoutLoc.y);
+
+                        PropertyValuesHolder imgViewScaleY = PropertyValuesHolder.ofFloat
+                                (View.SCALE_Y, 0, 1.0f);
+                        PropertyValuesHolder imgViewScaleX = PropertyValuesHolder.ofFloat
+                                (View.SCALE_X, 0, 1.0f);
+                        ObjectAnimator imgViewScaleAnimator = ObjectAnimator
+                                .ofPropertyValuesHolder(copyImgView, imgViewScaleX, imgViewScaleY);
+                        imgViewScaleAnimator.setInterpolator(sOvershootInterpolator);
+                        animations.add(imgViewTranslation);
+                        animations.add(imgViewScaleAnimator);
+
+                        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams
+                                (width, height);
+
+                        mLayout.addView(copyImgView, params);
+                    }
+                }
+
+                /** Loops through all the current visible cells in the ListView and animates
+                 * all of them into their post layout positions from their original positions.*/
+                for (int i = 0; i < getChildCount(); i++) {
+                    View child = getChildAt(i);
+                    int position = firstVisiblePosition + i;
+                    long itemId = adapter.getItemId(position);
+                    Rect startRect = listViewItemBounds.get(itemId);
+                    int top = child.getTop();
+                    if (startRect != null) {
+                        /** If the cell was visible before the data set change and
+                         * after the data set change, then animate the cell between
+                         * the two positions.*/
+                        int startTop = startRect.top;
+                        int delta = startTop - top;
+                        ObjectAnimator animation = ObjectAnimator.ofFloat(child,
+                                View.TRANSLATION_Y, delta, 0);
+                        animations.add(animation);
+                    } else {
+                        /** If the cell was not visible (or present) before the data set
+                         * change but is visible after the data set change, then use its
+                         * height to determine the delta by which it should be animated.*/
+                        int childHeight = child.getHeight() + getDividerHeight();
+                        int startTop = top + (i > 0 ? childHeight : -childHeight);
+                        int delta = startTop - top;
+                        ObjectAnimator animation = ObjectAnimator.ofFloat(child,
+                                View.TRANSLATION_Y, delta, 0);
+                        animations.add(animation);
+                    }
+                    listViewItemBounds.remove(itemId);
+                    listViewItemDrawables.remove(itemId);
+                }
+
+                /**
+                 * Loops through all the cells that were visible before the data set
+                 * changed but not after, and keeps track of their corresponding
+                 * drawables. The bounds of each drawable are then animated from the
+                 * original state to the new one (off the screen). By storing all
+                 * the drawables that meet this criteria, they can be redrawn on top
+                 * of the ListView via dispatchDraw as they are animating.
+                 */
+                for (Long itemId: listViewItemBounds.keySet()) {
+                    BitmapDrawable bitmapDrawable = listViewItemDrawables.get(itemId);
+                    Rect startBounds = listViewItemBounds.get(itemId);
+                    bitmapDrawable.setBounds(startBounds);
+
+                    int childHeight = startBounds.bottom - startBounds.top + getDividerHeight();
+                    Rect endBounds = new Rect(startBounds);
+                    endBounds.offset(0, childHeight);
+
+                    ObjectAnimator animation = ObjectAnimator.ofObject(bitmapDrawable,
+                            "bounds", sBoundsEvaluator, startBounds, endBounds);
+                    animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+                        private Rect mLastBound = null;
+                        private Rect mCurrentBound = new Rect();
+                        @Override
+                        public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                            Rect bounds = (Rect)valueAnimator.getAnimatedValue();
+                            mCurrentBound.set(bounds);
+                            if (mLastBound != null) {
+                                mCurrentBound.union(mLastBound);
+                            }
+                            mLastBound = bounds;
+                            invalidate(mCurrentBound);
+                        }
+                    });
+
+                    listViewItemBounds.remove(itemId);
+                    listViewItemDrawables.remove(itemId);
+
+                    mCellBitmapDrawables.add(bitmapDrawable);
+
+                    animations.add(animation);
+                }
+
+                /** Animates all the cells from their old position to their new position
+                 *  at the same time.*/
+                setEnabled(false);
+                mRowAdditionAnimationListener.onRowAdditionAnimationStart();
+                AnimatorSet set = new AnimatorSet();
+                set.setDuration(NEW_ROW_DURATION);
+                set.playTogether(animations);
+                set.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        mCellBitmapDrawables.clear();
+                        imgView.setVisibility(View.VISIBLE);
+                        mLayout.removeView(copyImgView);
+                        mRowAdditionAnimationListener.onRowAdditionAnimationEnd();
+                        setEnabled(true);
+                        invalidate();
+                    }
+                });
+                set.start();
+
+                listViewItemBounds.clear();
+                listViewItemDrawables.clear();
+                return true;
+            }
+        });
+    }
+
+    /**
+     * By overriding dispatchDraw, the BitmapDrawables of all the cells that were on the
+     * screen before (but not after) the layout are drawn and animated off the screen.
+     */
+    @Override
+    protected void dispatchDraw (Canvas canvas) {
+        super.dispatchDraw(canvas);
+        if (mCellBitmapDrawables.size() > 0) {
+            for (BitmapDrawable bitmapDrawable: mCellBitmapDrawables) {
+                bitmapDrawable.draw(canvas);
+            }
+        }
+    }
+
+    public boolean shouldAnimateInNewRow() {
+        int firstVisiblePosition = getFirstVisiblePosition();
+        return (firstVisiblePosition == 0);
+    }
+
+    public boolean shouldAnimateInNewImage() {
+        if (getChildCount() == 0) {
+            return true;
+        }
+        boolean shouldAnimateInNewRow = shouldAnimateInNewRow();
+        View topCell = getChildAt(0);
+        return (shouldAnimateInNewRow && topCell.getTop() == 0);
+    }
+
+    /** Returns a bitmap drawable showing a screenshot of the view passed in. */
+    private BitmapDrawable getBitmapDrawableFromView(View v) {
+        Bitmap bitmap = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas (bitmap);
+        v.draw(canvas);
+        return new BitmapDrawable(getResources(), bitmap);
+    }
+
+    /**
+     * Returns the absolute x,y coordinates of the view relative to the top left
+     * corner of the phone screen.
+     */
+    public Point getLocationOnScreen(View v) {
+        DisplayMetrics dm = new DisplayMetrics();
+        ((Activity)getContext()).getWindowManager().getDefaultDisplay().getMetrics(dm);
+
+        int[] location = new int[2];
+        v.getLocationOnScreen(location);
+
+        return new Point(location[0], location[1]);
+    }
+
+    /** Setter for the underlying data set controlling the adapter. */
+    public void setData(List<ListItemObject> data) {
+        mData = data;
+    }
+
+    /**
+     * Setter for the parent RelativeLayout of this ListView. A reference to this
+     * ViewGroup is required in order to add the custom animated overlaying bitmap
+     * when adding a new row.
+     */
+    public void setLayout(RelativeLayout layout) {
+        mLayout = layout;
+    }
+
+    public void setRowAdditionAnimationListener(OnRowAdditionAnimationListener
+                                                        rowAdditionAnimationListener) {
+        mRowAdditionAnimationListener = rowAdditionAnimationListener;
+    }
+
+    /**
+     * This TypeEvaluator is used to animate the position of a BitmapDrawable
+     * by updating its bounds.
+     */
+    static final TypeEvaluator<Rect> sBoundsEvaluator = new TypeEvaluator<Rect>() {
+        public Rect evaluate(float fraction, Rect startValue, Rect endValue) {
+            return new Rect(interpolate(startValue.left, endValue.left, fraction),
+                    interpolate(startValue.top, endValue.top, fraction),
+                    interpolate(startValue.right, endValue.right, fraction),
+                    interpolate(startValue.bottom, endValue.bottom, fraction));
+        }
+
+        public int interpolate(int start, int end, float fraction) {
+            return (int)(start + fraction * (end - start));
+        }
+    };
+
+}
diff --git a/samples/devbytes/animation/ListViewCellInsertion/src/com/example/android/insertingcells/ListItemObject.java b/samples/devbytes/animation/ListViewCellInsertion/src/com/example/android/insertingcells/ListItemObject.java
new file mode 100644
index 0000000..8ca56f5
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/src/com/example/android/insertingcells/ListItemObject.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.insertingcells;
+
+/**
+ * The data model for every cell in the ListView for this application. This model stores
+ * a title, an image resource and a default cell height for every item in the ListView.
+ */
+public class ListItemObject {
+
+    private String mTitle;
+    private int mImgResource;
+    private int mHeight;
+
+    public ListItemObject(String title, int imgResource, int height) {
+        super();
+        mTitle = title;
+        mImgResource = imgResource;
+        mHeight = height;
+    }
+
+    public String getTitle() {
+        return mTitle;
+    }
+
+    public int getImgResource() {
+        return mImgResource;
+    }
+
+    public int getHeight() {
+        return mHeight;
+    }
+
+}
diff --git a/samples/devbytes/animation/ListViewCellInsertion/src/com/example/android/insertingcells/OnRowAdditionAnimationListener.java b/samples/devbytes/animation/ListViewCellInsertion/src/com/example/android/insertingcells/OnRowAdditionAnimationListener.java
new file mode 100644
index 0000000..f371f3e
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/src/com/example/android/insertingcells/OnRowAdditionAnimationListener.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.insertingcells;
+
+/**
+ * This listener is used to determine when the animation of a new row addition
+ * begins and ends. The primary use of this interface is to create a callback
+ * under which certain elements, such as the listview itself, can be disabled
+ * to prevent unpredictable behaviour during the actual cell animation.
+ */
+public interface OnRowAdditionAnimationListener {
+    public void onRowAdditionAnimationStart();
+    public void onRowAdditionAnimationEnd();
+}
diff --git a/samples/devbytes/animation/ListViewCellInsertion/src/com/example/android/insertingcells/RoundView.java b/samples/devbytes/animation/ListViewCellInsertion/src/com/example/android/insertingcells/RoundView.java
new file mode 100644
index 0000000..320c8b9
--- /dev/null
+++ b/samples/devbytes/animation/ListViewCellInsertion/src/com/example/android/insertingcells/RoundView.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.insertingcells;
+
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * This round view draws a circle from which the image pops out of and into
+ * the corresponding cell in the list.
+ */
+public class RoundView extends View {
+
+    private final int STROKE_WIDTH = 6;
+    private final int RADIUS = 20;
+    private final int ANIMATION_DURATION = 300;
+    private final float SCALE_FACTOR = 0.3f;
+
+    private Paint mPaint;
+
+    public RoundView(Context context) {
+        super(context);
+        init();
+    }
+
+    public RoundView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    public RoundView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init();
+    }
+
+    private void init() {
+        mPaint = new Paint();
+        mPaint.setAntiAlias(true);
+        mPaint.setColor(Color.WHITE);
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setStrokeWidth(STROKE_WIDTH);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        canvas.drawCircle(canvas.getWidth() / 2, canvas.getHeight() / 2,
+                RADIUS, mPaint);
+    }
+
+    public ObjectAnimator getScalingAnimator() {
+        PropertyValuesHolder imgViewScaleY = PropertyValuesHolder.ofFloat(View
+                .SCALE_Y, SCALE_FACTOR);
+        PropertyValuesHolder imgViewScaleX = PropertyValuesHolder.ofFloat(View
+                .SCALE_X, SCALE_FACTOR);
+
+        ObjectAnimator imgViewScaleAnimator = ObjectAnimator
+                .ofPropertyValuesHolder(this, imgViewScaleX, imgViewScaleY);
+        imgViewScaleAnimator.setRepeatCount(1);
+        imgViewScaleAnimator.setRepeatMode(ValueAnimator.REVERSE);
+        imgViewScaleAnimator.setDuration(ANIMATION_DURATION);
+
+        return imgViewScaleAnimator;
+    }
+}
diff --git a/samples/devbytes/animation/ListViewDraggingAnimation/AndroidManifest.xml b/samples/devbytes/animation/ListViewDraggingAnimation/AndroidManifest.xml
new file mode 100644
index 0000000..5702dca
--- /dev/null
+++ b/samples/devbytes/animation/ListViewDraggingAnimation/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.example.android.listviewdragginganimation"
+          android:versionCode="1"
+          android:versionName="1.0">
+    <uses-sdk android:minSdkVersion="11"
+              android:targetSdkVersion="17"/>
+    <application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
+        <activity android:name=".ListViewDraggingAnimation"
+                  android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/samples/devbytes/animation/ListViewDraggingAnimation/res/drawable-hdpi/ic_launcher.png b/samples/devbytes/animation/ListViewDraggingAnimation/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..96a442e
--- /dev/null
+++ b/samples/devbytes/animation/ListViewDraggingAnimation/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/ListViewDraggingAnimation/res/drawable-ldpi/ic_launcher.png b/samples/devbytes/animation/ListViewDraggingAnimation/res/drawable-ldpi/ic_launcher.png
new file mode 100644
index 0000000..9923872
--- /dev/null
+++ b/samples/devbytes/animation/ListViewDraggingAnimation/res/drawable-ldpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/ListViewDraggingAnimation/res/drawable-mdpi/ic_launcher.png b/samples/devbytes/animation/ListViewDraggingAnimation/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..359047d
--- /dev/null
+++ b/samples/devbytes/animation/ListViewDraggingAnimation/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/ListViewDraggingAnimation/res/drawable-xhdpi/ic_launcher.png b/samples/devbytes/animation/ListViewDraggingAnimation/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..71c6d76
--- /dev/null
+++ b/samples/devbytes/animation/ListViewDraggingAnimation/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/ListViewDraggingAnimation/res/layout/activity_list_view.xml b/samples/devbytes/animation/ListViewDraggingAnimation/res/layout/activity_list_view.xml
new file mode 100644
index 0000000..68198bc
--- /dev/null
+++ b/samples/devbytes/animation/ListViewDraggingAnimation/res/layout/activity_list_view.xml
@@ -0,0 +1,29 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/mainLayout"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".ListViewAnimations" >
+
+        <com.example.android.listviewdragginganimation.DynamicListView
+            android:id="@+id/listview"
+            android:background="#0000"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+</RelativeLayout>
diff --git a/samples/devbytes/animation/ListViewDraggingAnimation/res/layout/text_view.xml b/samples/devbytes/animation/ListViewDraggingAnimation/res/layout/text_view.xml
new file mode 100644
index 0000000..d7aeb7a
--- /dev/null
+++ b/samples/devbytes/animation/ListViewDraggingAnimation/res/layout/text_view.xml
@@ -0,0 +1,26 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="#FFF"
+    android:textSize="@dimen/list_text_size"
+    android:gravity="center_vertical"
+    android:paddingLeft="15dp"
+    android:paddingRight="15dp"
+    android:minHeight="@dimen/list_item_height"
+    android:textColor="#000000"
+/>
diff --git a/samples/devbytes/animation/ListViewDraggingAnimation/res/values/dimens.xml b/samples/devbytes/animation/ListViewDraggingAnimation/res/values/dimens.xml
new file mode 100644
index 0000000..d647948
--- /dev/null
+++ b/samples/devbytes/animation/ListViewDraggingAnimation/res/values/dimens.xml
@@ -0,0 +1,20 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+
+    <dimen name="list_text_size">16sp</dimen>
+    <dimen name="list_item_height">48dip</dimen>
+
+</resources>
\ No newline at end of file
diff --git a/samples/devbytes/animation/ListViewDraggingAnimation/res/values/strings.xml b/samples/devbytes/animation/ListViewDraggingAnimation/res/values/strings.xml
new file mode 100644
index 0000000..bbe6f91
--- /dev/null
+++ b/samples/devbytes/animation/ListViewDraggingAnimation/res/values/strings.xml
@@ -0,0 +1,19 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+
+    <string name="app_name">ListViewDraggingAnimation</string>
+
+</resources>
diff --git a/samples/devbytes/animation/ListViewDraggingAnimation/src/com/example/android/listviewdragginganimation/Cheeses.java b/samples/devbytes/animation/ListViewDraggingAnimation/src/com/example/android/listviewdragginganimation/Cheeses.java
new file mode 100644
index 0000000..f5422cf
--- /dev/null
+++ b/samples/devbytes/animation/ListViewDraggingAnimation/src/com/example/android/listviewdragginganimation/Cheeses.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.listviewdragginganimation;
+
+public class Cheeses {
+
+    public static final String[] sCheeseStrings = {
+            "Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi",
+            "Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale",
+            "Aisy Cendre", "Allgauer Emmentaler", "Alverca", "Ambert", "American Cheese",
+            "Ami du Chambertin", "Anejo Enchilado", "Anneau du Vic-Bilh", "Anthoriro", "Appenzell",
+            "Aragon", "Ardi Gasna", "Ardrahan", "Armenian String", "Aromes au Gene de Marc",
+            "Asadero", "Asiago", "Aubisque Pyrenees", "Autun", "Avaxtskyr", "Baby Swiss",
+            "Babybel", "Baguette Laonnaise", "Bakers", "Baladi", "Balaton", "Bandal", "Banon",
+            "Barry's Bay Cheddar", "Basing", "Basket Cheese", "Bath Cheese", "Bavarian Bergkase",
+            "Baylough", "Beaufort", "Beauvoorde", "Beenleigh Blue", "Beer Cheese", "Bel Paese",
+            "Bergader", "Bergere Bleue", "Berkswell", "Beyaz Peynir", "Bierkase", "Bishop Kennedy",
+            "Blarney", "Bleu d'Auvergne", "Bleu de Gex", "Bleu de Laqueuille",
+            "Bleu de Septmoncel", "Bleu Des Causses", "Blue", "Blue Castello", "Blue Rathgore",
+            "Blue Vein (Australian)", "Blue Vein Cheeses", "Bocconcini", "Bocconcini (Australian)",
+            "Boeren Leidenkaas", "Bonchester", "Bosworth", "Bougon", "Boule Du Roves",
+            "Boulette d'Avesnes", "Boursault", "Boursin", "Bouyssou", "Bra", "Braudostur",
+            "Breakfast Cheese", "Brebis du Lavort", "Brebis du Lochois", "Brebis du Puyfaucon",
+            "Bresse Bleu", "Brick", "Brie", "Brie de Meaux", "Brie de Melun", "Brillat-Savarin",
+            "Brin", "Brin d' Amour", "Brin d'Amour", "Brinza (Burduf Brinza)",
+            "Briquette de Brebis", "Briquette du Forez", "Broccio", "Broccio Demi-Affine",
+            "Brousse du Rove", "Bruder Basil", "Brusselae Kaas (Fromage de Bruxelles)", "Bryndza",
+            "Buchette d'Anjou", "Buffalo", "Burgos", "Butte", "Butterkase", "Button (Innes)",
+            "Buxton Blue", "Cabecou", "Caboc", "Cabrales", "Cachaille", "Caciocavallo", "Caciotta",
+            "Caerphilly", "Cairnsmore", "Calenzana", "Cambazola", "Camembert de Normandie",
+            "Canadian Cheddar", "Canestrato", "Cantal", "Caprice des Dieux", "Capricorn Goat",
+            "Capriole Banon", "Carre de l'Est", "Casciotta di Urbino", "Cashel Blue", "Castellano",
+            "Castelleno", "Castelmagno", "Castelo Branco", "Castigliano", "Cathelain",
+            "Celtic Promise", "Cendre d'Olivet", "Cerney", "Chabichou", "Chabichou du Poitou",
+            "Chabis de Gatine", "Chaource", "Charolais", "Chaumes", "Cheddar",
+            "Cheddar Clothbound", "Cheshire", "Chevres", "Chevrotin des Aravis", "Chontaleno",
+            "Civray", "Coeur de Camembert au Calvados", "Coeur de Chevre", "Colby", "Cold Pack",
+            "Comte", "Coolea", "Cooleney", "Coquetdale", "Corleggy", "Cornish Pepper",
+            "Cotherstone", "Cotija", "Cottage Cheese", "Cottage Cheese (Australian)",
+            "Cougar Gold", "Coulommiers", "Coverdale", "Crayeux de Roncq", "Cream Cheese",
+            "Cream Havarti", "Crema Agria", "Crema Mexicana", "Creme Fraiche", "Crescenza",
+            "Croghan", "Crottin de Chavignol", "Crottin du Chavignol", "Crowdie", "Crowley",
+            "Cuajada", "Curd", "Cure Nantais", "Curworthy", "Cwmtawe Pecorino",
+            "Cypress Grove Chevre", "Danablu (Danish Blue)", "Danbo", "Danish Fontina",
+            "Daralagjazsky", "Dauphin", "Delice des Fiouves", "Denhany Dorset Drum", "Derby",
+            "Dessertnyj Belyj", "Devon Blue", "Devon Garland", "Dolcelatte", "Doolin",
+            "Doppelrhamstufel", "Dorset Blue Vinney", "Double Gloucester", "Double Worcester",
+            "Dreux a la Feuille", "Dry Jack", "Duddleswell", "Dunbarra", "Dunlop", "Dunsyre Blue",
+            "Duroblando", "Durrus", "Dutch Mimolette (Commissiekaas)", "Edam", "Edelpilz",
+            "Emental Grand Cru", "Emlett", "Emmental", "Epoisses de Bourgogne", "Esbareich",
+            "Esrom", "Etorki", "Evansdale Farmhouse Brie", "Evora De L'Alentejo", "Exmoor Blue",
+            "Explorateur", "Feta", "Feta (Australian)", "Figue", "Filetta", "Fin-de-Siecle",
+            "Finlandia Swiss", "Finn", "Fiore Sardo", "Fleur du Maquis", "Flor de Guia",
+            "Flower Marie", "Folded", "Folded cheese with mint", "Fondant de Brebis",
+            "Fontainebleau", "Fontal", "Fontina Val d'Aosta", "Formaggio di capra", "Fougerus",
+            "Four Herb Gouda", "Fourme d' Ambert", "Fourme de Haute Loire", "Fourme de Montbrison",
+            "Fresh Jack", "Fresh Mozzarella", "Fresh Ricotta", "Fresh Truffles", "Fribourgeois",
+            "Friesekaas", "Friesian", "Friesla", "Frinault", "Fromage a Raclette", "Fromage Corse",
+            "Fromage de Montagne de Savoie", "Fromage Frais", "Fruit Cream Cheese",
+            "Frying Cheese", "Fynbo", "Gabriel", "Galette du Paludier", "Galette Lyonnaise",
+            "Galloway Goat's Milk Gems", "Gammelost", "Gaperon a l'Ail", "Garrotxa", "Gastanberra",
+            "Geitost", "Gippsland Blue", "Gjetost", "Gloucester", "Golden Cross", "Gorgonzola",
+            "Gornyaltajski", "Gospel Green", "Gouda", "Goutu", "Gowrie", "Grabetto", "Graddost",
+            "Grafton Village Cheddar", "Grana", "Grana Padano", "Grand Vatel",
+            "Grataron d' Areches", "Gratte-Paille", "Graviera", "Greuilh", "Greve",
+            "Gris de Lille", "Gruyere", "Gubbeen", "Guerbigny", "Halloumi",
+            "Halloumy (Australian)", "Haloumi-Style Cheese", "Harbourne Blue", "Havarti",
+            "Heidi Gruyere", "Hereford Hop", "Herrgardsost", "Herriot Farmhouse", "Herve",
+            "Hipi Iti", "Hubbardston Blue Cow", "Hushallsost", "Iberico", "Idaho Goatster",
+            "Idiazabal", "Il Boschetto al Tartufo", "Ile d'Yeu", "Isle of Mull", "Jarlsberg",
+            "Jermi Tortes", "Jibneh Arabieh", "Jindi Brie", "Jubilee Blue", "Juustoleipa",
+            "Kadchgall", "Kaseri", "Kashta", "Kefalotyri", "Kenafa", "Kernhem", "Kervella Affine",
+            "Kikorangi", "King Island Cape Wickham Brie", "King River Gold", "Klosterkaese",
+            "Knockalara", "Kugelkase", "L'Aveyronnais", "L'Ecir de l'Aubrac", "La Taupiniere",
+            "La Vache Qui Rit", "Laguiole", "Lairobell", "Lajta", "Lanark Blue", "Lancashire",
+            "Langres", "Lappi", "Laruns", "Lavistown", "Le Brin", "Le Fium Orbo", "Le Lacandou",
+            "Le Roule", "Leafield", "Lebbene", "Leerdammer", "Leicester", "Leyden", "Limburger",
+            "Lincolnshire Poacher", "Lingot Saint Bousquet d'Orb", "Liptauer", "Little Rydings",
+            "Livarot", "Llanboidy", "Llanglofan Farmhouse", "Loch Arthur Farmhouse",
+            "Loddiswell Avondale", "Longhorn", "Lou Palou", "Lou Pevre", "Lyonnais", "Maasdam",
+            "Macconais", "Mahoe Aged Gouda", "Mahon", "Malvern", "Mamirolle", "Manchego",
+            "Manouri", "Manur", "Marble Cheddar", "Marbled Cheeses", "Maredsous", "Margotin",
+            "Maribo", "Maroilles", "Mascares", "Mascarpone", "Mascarpone (Australian)",
+            "Mascarpone Torta", "Matocq", "Maytag Blue", "Meira", "Menallack Farmhouse",
+            "Menonita", "Meredith Blue", "Mesost", "Metton (Cancoillotte)", "Meyer Vintage Gouda",
+            "Mihalic Peynir", "Milleens", "Mimolette", "Mine-Gabhar", "Mini Baby Bells", "Mixte",
+            "Molbo", "Monastery Cheeses", "Mondseer", "Mont D'or Lyonnais", "Montasio",
+            "Monterey Jack", "Monterey Jack Dry", "Morbier", "Morbier Cru de Montagne",
+            "Mothais a la Feuille", "Mozzarella", "Mozzarella (Australian)",
+            "Mozzarella di Bufala", "Mozzarella Fresh, in water", "Mozzarella Rolls", "Munster",
+            "Murol", "Mycella", "Myzithra", "Naboulsi", "Nantais", "Neufchatel",
+            "Neufchatel (Australian)", "Niolo", "Nokkelost", "Northumberland", "Oaxaca",
+            "Olde York", "Olivet au Foin", "Olivet Bleu", "Olivet Cendre",
+            "Orkney Extra Mature Cheddar", "Orla", "Oschtjepka", "Ossau Fermier", "Ossau-Iraty",
+            "Oszczypek", "Oxford Blue", "P'tit Berrichon", "Palet de Babligny", "Paneer", "Panela",
+            "Pannerone", "Pant ys Gawn", "Parmesan (Parmigiano)", "Parmigiano Reggiano",
+            "Pas de l'Escalette", "Passendale", "Pasteurized Processed", "Pate de Fromage",
+            "Patefine Fort", "Pave d'Affinois", "Pave d'Auge", "Pave de Chirac", "Pave du Berry",
+            "Pecorino", "Pecorino in Walnut Leaves", "Pecorino Romano", "Peekskill Pyramid",
+            "Pelardon des Cevennes", "Pelardon des Corbieres", "Penamellera", "Penbryn",
+            "Pencarreg", "Perail de Brebis", "Petit Morin", "Petit Pardou", "Petit-Suisse",
+            "Picodon de Chevre", "Picos de Europa", "Piora", "Pithtviers au Foin",
+            "Plateau de Herve", "Plymouth Cheese", "Podhalanski", "Poivre d'Ane", "Polkolbin",
+            "Pont l'Eveque", "Port Nicholson", "Port-Salut", "Postel", "Pouligny-Saint-Pierre",
+            "Pourly", "Prastost", "Pressato", "Prince-Jean", "Processed Cheddar", "Provolone",
+            "Provolone (Australian)", "Pyengana Cheddar", "Pyramide", "Quark",
+            "Quark (Australian)", "Quartirolo Lombardo", "Quatre-Vents", "Quercy Petit",
+            "Queso Blanco", "Queso Blanco con Frutas --Pina y Mango", "Queso de Murcia",
+            "Queso del Montsec", "Queso del Tietar", "Queso Fresco", "Queso Fresco (Adobera)",
+            "Queso Iberico", "Queso Jalapeno", "Queso Majorero", "Queso Media Luna",
+            "Queso Para Frier", "Queso Quesadilla", "Rabacal", "Raclette", "Ragusano", "Raschera",
+            "Reblochon", "Red Leicester", "Regal de la Dombes", "Reggianito", "Remedou",
+            "Requeson", "Richelieu", "Ricotta", "Ricotta (Australian)", "Ricotta Salata", "Ridder",
+            "Rigotte", "Rocamadour", "Rollot", "Romano", "Romans Part Dieu", "Roncal", "Roquefort",
+            "Roule", "Rouleau De Beaulieu", "Royalp Tilsit", "Rubens", "Rustinu", "Saaland Pfarr",
+            "Saanenkaese", "Saga", "Sage Derby", "Sainte Maure", "Saint-Marcellin",
+            "Saint-Nectaire", "Saint-Paulin", "Salers", "Samso", "San Simon", "Sancerre",
+            "Sap Sago", "Sardo", "Sardo Egyptian", "Sbrinz", "Scamorza", "Schabzieger", "Schloss",
+            "Selles sur Cher", "Selva", "Serat", "Seriously Strong Cheddar", "Serra da Estrela",
+            "Sharpam", "Shelburne Cheddar", "Shropshire Blue", "Siraz", "Sirene", "Smoked Gouda",
+            "Somerset Brie", "Sonoma Jack", "Sottocenare al Tartufo", "Soumaintrain",
+            "Sourire Lozerien", "Spenwood", "Sraffordshire Organic", "St. Agur Blue Cheese",
+            "Stilton", "Stinking Bishop", "String", "Sussex Slipcote", "Sveciaost", "Swaledale",
+            "Sweet Style Swiss", "Swiss", "Syrian (Armenian String)", "Tala", "Taleggio", "Tamie",
+            "Tasmania Highland Chevre Log", "Taupiniere", "Teifi", "Telemea", "Testouri",
+            "Tete de Moine", "Tetilla", "Texas Goat Cheese", "Tibet", "Tillamook Cheddar",
+            "Tilsit", "Timboon Brie", "Toma", "Tomme Brulee", "Tomme d'Abondance",
+            "Tomme de Chevre", "Tomme de Romans", "Tomme de Savoie", "Tomme des Chouans", "Tommes",
+            "Torta del Casar", "Toscanello", "Touree de L'Aubier", "Tourmalet",
+            "Trappe (Veritable)", "Trois Cornes De Vendee", "Tronchon", "Trou du Cru", "Truffe",
+            "Tupi", "Turunmaa", "Tymsboro", "Tyn Grug", "Tyning", "Ubriaco", "Ulloa",
+            "Vacherin-Fribourgeois", "Valencay", "Vasterbottenost", "Venaco", "Vendomois",
+            "Vieux Corse", "Vignotte", "Vulscombe", "Waimata Farmhouse Blue",
+            "Washed Rind Cheese (Australian)", "Waterloo", "Weichkaese", "Wellington",
+            "Wensleydale", "White Stilton", "Whitestone Farmhouse", "Wigmore", "Woodside Cabecou",
+            "Xanadu", "Xynotyro", "Yarg Cornish", "Yarra Valley Pyramid", "Yorkshire Blue",
+            "Zamorano", "Zanetti Grana Padano", "Zanetti Parmigiano Reggiano"
+    };
+
+}
diff --git a/samples/devbytes/animation/ListViewDraggingAnimation/src/com/example/android/listviewdragginganimation/DynamicListView.java b/samples/devbytes/animation/ListViewDraggingAnimation/src/com/example/android/listviewdragginganimation/DynamicListView.java
new file mode 100644
index 0000000..580fe64
--- /dev/null
+++ b/samples/devbytes/animation/ListViewDraggingAnimation/src/com/example/android/listviewdragginganimation/DynamicListView.java
@@ -0,0 +1,592 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.listviewdragginganimation;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.TypeEvaluator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+
+import java.util.ArrayList;
+
+/**
+ * The dynamic listview is an extension of listview that supports cell dragging
+ * and swapping.
+ *
+ * This layout is in charge of positioning the hover cell in the correct location
+ * on the screen in response to user touch events. It uses the position of the
+ * hover cell to determine when two cells should be swapped. If two cells should
+ * be swapped, all the corresponding data set and layout changes are handled here.
+ *
+ * If no cell is selected, all the touch events are passed down to the listview
+ * and behave normally. If one of the items in the listview experiences a
+ * long press event, the contents of its current visible state are captured as
+ * a bitmap and its visibility is set to INVISIBLE. A hover cell is then created and
+ * added to this layout as an overlaying BitmapDrawable above the listview. Once the
+ * hover cell is translated some distance to signify an item swap, a data set change
+ * accompanied by animation takes place. When the user releases the hover cell,
+ * it animates into its corresponding position in the listview.
+ *
+ * When the hover cell is either above or below the bounds of the listview, this
+ * listview also scrolls on its own so as to reveal additional content.
+ */
+public class DynamicListView extends ListView {
+
+    private final int SMOOTH_SCROLL_AMOUNT_AT_EDGE = 15;
+    private final int MOVE_DURATION = 150;
+    private final int LINE_THICKNESS = 15;
+
+    public ArrayList<String> mCheeseList;
+
+    private int mLastEventY = -1;
+
+    private int mDownY = -1;
+    private int mDownX = -1;
+
+    private int mTotalOffset = 0;
+
+    private boolean mCellIsMobile = false;
+    private boolean mIsMobileScrolling = false;
+    private int mSmoothScrollAmountAtEdge = 0;
+
+    private final int INVALID_ID = -1;
+    private long mAboveItemId = INVALID_ID;
+    private long mMobileItemId = INVALID_ID;
+    private long mBelowItemId = INVALID_ID;
+
+    private BitmapDrawable mHoverCell;
+    private Rect mHoverCellCurrentBounds;
+    private Rect mHoverCellOriginalBounds;
+
+    private final int INVALID_POINTER_ID = -1;
+    private int mActivePointerId = INVALID_POINTER_ID;
+
+    private boolean mIsWaitingForScrollFinish = false;
+    private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
+
+    public DynamicListView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public DynamicListView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context);
+    }
+
+    public DynamicListView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public void init(Context context) {
+        setOnItemLongClickListener(mOnItemLongClickListener);
+        setOnScrollListener(mScrollListener);
+        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+        mSmoothScrollAmountAtEdge = (int)(SMOOTH_SCROLL_AMOUNT_AT_EDGE / metrics.density);
+    }
+
+    /**
+     * Listens for long clicks on any items in the listview. When a cell has
+     * been selected, the hover cell is created and set up.
+     */
+    private AdapterView.OnItemLongClickListener mOnItemLongClickListener =
+            new AdapterView.OnItemLongClickListener() {
+                public boolean onItemLongClick(AdapterView<?> arg0, View arg1, int pos, long id) {
+                    mTotalOffset = 0;
+
+                    int position = pointToPosition(mDownX, mDownY);
+                    int itemNum = position - getFirstVisiblePosition();
+
+                    View selectedView = getChildAt(itemNum);
+                    mMobileItemId = getAdapter().getItemId(position);
+                    mHoverCell = getAndAddHoverView(selectedView);
+                    selectedView.setVisibility(INVISIBLE);
+
+                    mCellIsMobile = true;
+
+                    updateNeighborViewsForID(mMobileItemId);
+
+                    return true;
+                }
+            };
+
+    /**
+     * Creates the hover cell with the appropriate bitmap and of appropriate
+     * size. The hover cell's BitmapDrawable is drawn on top of the bitmap every
+     * single time an invalidate call is made.
+     */
+    private BitmapDrawable getAndAddHoverView(View v) {
+
+        int w = v.getWidth();
+        int h = v.getHeight();
+        int top = v.getTop();
+        int left = v.getLeft();
+
+        Bitmap b = getBitmapWithBorder(v);
+
+        BitmapDrawable drawable = new BitmapDrawable(getResources(), b);
+
+        mHoverCellOriginalBounds = new Rect(left, top, left + w, top + h);
+        mHoverCellCurrentBounds = new Rect(mHoverCellOriginalBounds);
+
+        drawable.setBounds(mHoverCellCurrentBounds);
+
+        return drawable;
+    }
+
+    /** Draws a black border over the screenshot of the view passed in. */
+    private Bitmap getBitmapWithBorder(View v) {
+        Bitmap bitmap = getBitmapFromView(v);
+        Canvas can = new Canvas(bitmap);
+
+        Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+
+        Paint paint = new Paint();
+        paint.setStyle(Paint.Style.STROKE);
+        paint.setStrokeWidth(LINE_THICKNESS);
+        paint.setColor(Color.BLACK);
+
+        can.drawBitmap(bitmap, 0, 0, null);
+        can.drawRect(rect, paint);
+
+        return bitmap;
+    }
+
+    /** Returns a bitmap showing a screenshot of the view passed in. */
+    private Bitmap getBitmapFromView(View v) {
+        Bitmap bitmap = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas (bitmap);
+        v.draw(canvas);
+        return bitmap;
+    }
+
+    /**
+     * Stores a reference to the views above and below the item currently
+     * corresponding to the hover cell. It is important to note that if this
+     * item is either at the top or bottom of the list, mAboveItemId or mBelowItemId
+     * may be invalid.
+     */
+    private void updateNeighborViewsForID(long itemID) {
+        int position = getPositionForID(itemID);
+        StableArrayAdapter adapter = ((StableArrayAdapter)getAdapter());
+        mAboveItemId = adapter.getItemId(position - 1);
+        mBelowItemId = adapter.getItemId(position + 1);
+    }
+
+    /** Retrieves the view in the list corresponding to itemID */
+    public View getViewForID (long itemID) {
+        int firstVisiblePosition = getFirstVisiblePosition();
+        StableArrayAdapter adapter = ((StableArrayAdapter)getAdapter());
+        for(int i = 0; i < getChildCount(); i++) {
+            View v = getChildAt(i);
+            int position = firstVisiblePosition + i;
+            long id = adapter.getItemId(position);
+            if (id == itemID) {
+                return v;
+            }
+        }
+        return null;
+    }
+
+    /** Retrieves the position in the list corresponding to itemID */
+    public int getPositionForID (long itemID) {
+        View v = getViewForID(itemID);
+        if (v == null) {
+            return -1;
+        } else {
+            return getPositionForView(v);
+        }
+    }
+
+    /**
+     *  dispatchDraw gets invoked when all the child views are about to be drawn.
+     *  By overriding this method, the hover cell (BitmapDrawable) can be drawn
+     *  over the listview's items whenever the listview is redrawn.
+     */
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+        if (mHoverCell != null) {
+            mHoverCell.draw(canvas);
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent (MotionEvent event) {
+
+        switch (event.getAction() & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_DOWN:
+                mDownX = (int)event.getX();
+                mDownY = (int)event.getY();
+                mActivePointerId = event.getPointerId(0);
+                break;
+            case MotionEvent.ACTION_MOVE:
+                if (mActivePointerId == INVALID_POINTER_ID) {
+                    break;
+                }
+
+                int pointerIndex = event.findPointerIndex(mActivePointerId);
+
+                mLastEventY = (int) event.getY(pointerIndex);
+                int deltaY = mLastEventY - mDownY;
+
+                if (mCellIsMobile) {
+                    mHoverCellCurrentBounds.offsetTo(mHoverCellOriginalBounds.left,
+                            mHoverCellOriginalBounds.top + deltaY + mTotalOffset);
+                    mHoverCell.setBounds(mHoverCellCurrentBounds);
+                    invalidate();
+
+                    handleCellSwitch();
+
+                    mIsMobileScrolling = false;
+                    handleMobileCellScroll();
+
+                    return false;
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                touchEventsEnded();
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                touchEventsCancelled();
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+                /* If a multitouch event took place and the original touch dictating
+                 * the movement of the hover cell has ended, then the dragging event
+                 * ends and the hover cell is animated to its corresponding position
+                 * in the listview. */
+                pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
+                        MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+                final int pointerId = event.getPointerId(pointerIndex);
+                if (pointerId == mActivePointerId) {
+                    touchEventsEnded();
+                }
+                break;
+            default:
+                break;
+        }
+
+        return super.onTouchEvent(event);
+    }
+
+    /**
+     * This method determines whether the hover cell has been shifted far enough
+     * to invoke a cell swap. If so, then the respective cell swap candidate is
+     * determined and the data set is changed. Upon posting a notification of the
+     * data set change, a layout is invoked to place the cells in the right place.
+     * Using a ViewTreeObserver and a corresponding OnPreDrawListener, we can
+     * offset the cell being swapped to where it previously was and then animate it to
+     * its new position.
+     */
+    private void handleCellSwitch() {
+        final int deltaY = mLastEventY - mDownY;
+        int deltaYTotal = mHoverCellOriginalBounds.top + mTotalOffset + deltaY;
+
+        View belowView = getViewForID(mBelowItemId);
+        View mobileView = getViewForID(mMobileItemId);
+        View aboveView = getViewForID(mAboveItemId);
+
+        boolean isBelow = (belowView != null) && (deltaYTotal > belowView.getTop());
+        boolean isAbove = (aboveView != null) && (deltaYTotal < aboveView.getTop());
+
+        if (isBelow || isAbove) {
+
+            final long switchItemID = isBelow ? mBelowItemId : mAboveItemId;
+            View switchView = isBelow ? belowView : aboveView;
+            final int originalItem = getPositionForView(mobileView);
+
+            if (switchView == null) {
+                updateNeighborViewsForID(mMobileItemId);
+                return;
+            }
+
+            swapElements(mCheeseList, originalItem, getPositionForView(switchView));
+
+            ((BaseAdapter) getAdapter()).notifyDataSetChanged();
+
+            mDownY = mLastEventY;
+
+            final int switchViewStartTop = switchView.getTop();
+
+            mobileView.setVisibility(View.VISIBLE);
+            switchView.setVisibility(View.INVISIBLE);
+
+            updateNeighborViewsForID(mMobileItemId);
+
+            final ViewTreeObserver observer = getViewTreeObserver();
+            observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+                public boolean onPreDraw() {
+                    observer.removeOnPreDrawListener(this);
+
+                    View switchView = getViewForID(switchItemID);
+
+                    mTotalOffset += deltaY;
+
+                    int switchViewNewTop = switchView.getTop();
+                    int delta = switchViewStartTop - switchViewNewTop;
+
+                    switchView.setTranslationY(delta);
+
+                    ObjectAnimator animator = ObjectAnimator.ofFloat(switchView,
+                            View.TRANSLATION_Y, 0);
+                    animator.setDuration(MOVE_DURATION);
+                    animator.start();
+
+                    return true;
+                }
+            });
+        }
+    }
+
+    private void swapElements(ArrayList arrayList, int indexOne, int indexTwo) {
+        Object temp = arrayList.get(indexOne);
+        arrayList.set(indexOne, arrayList.get(indexTwo));
+        arrayList.set(indexTwo, temp);
+    }
+
+
+    /**
+     * Resets all the appropriate fields to a default state while also animating
+     * the hover cell back to its correct location.
+     */
+    private void touchEventsEnded () {
+        final View mobileView = getViewForID(mMobileItemId);
+        if (mCellIsMobile|| mIsWaitingForScrollFinish) {
+            mCellIsMobile = false;
+            mIsWaitingForScrollFinish = false;
+            mIsMobileScrolling = false;
+            mActivePointerId = INVALID_POINTER_ID;
+
+            // If the autoscroller has not completed scrolling, we need to wait for it to
+            // finish in order to determine the final location of where the hover cell
+            // should be animated to.
+            if (mScrollState != OnScrollListener.SCROLL_STATE_IDLE) {
+                mIsWaitingForScrollFinish = true;
+                return;
+            }
+
+            mHoverCellCurrentBounds.offsetTo(mHoverCellOriginalBounds.left, mobileView.getTop());
+
+            ObjectAnimator hoverViewAnimator = ObjectAnimator.ofObject(mHoverCell, "bounds",
+                    sBoundEvaluator, mHoverCellCurrentBounds);
+            hoverViewAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                    invalidate();
+                }
+            });
+            hoverViewAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+                    setEnabled(false);
+                }
+
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mAboveItemId = INVALID_ID;
+                    mMobileItemId = INVALID_ID;
+                    mBelowItemId = INVALID_ID;
+                    mobileView.setVisibility(VISIBLE);
+                    mHoverCell = null;
+                    setEnabled(true);
+                    invalidate();
+                }
+            });
+            hoverViewAnimator.start();
+        } else {
+            touchEventsCancelled();
+        }
+    }
+
+    /**
+     * Resets all the appropriate fields to a default state.
+     */
+    private void touchEventsCancelled () {
+        View mobileView = getViewForID(mMobileItemId);
+        if (mCellIsMobile) {
+            mAboveItemId = INVALID_ID;
+            mMobileItemId = INVALID_ID;
+            mBelowItemId = INVALID_ID;
+            mobileView.setVisibility(VISIBLE);
+            mHoverCell = null;
+            invalidate();
+        }
+        mCellIsMobile = false;
+        mIsMobileScrolling = false;
+        mActivePointerId = INVALID_POINTER_ID;
+    }
+
+    /**
+     * This TypeEvaluator is used to animate the BitmapDrawable back to its
+     * final location when the user lifts his finger by modifying the
+     * BitmapDrawable's bounds.
+     */
+    private final static TypeEvaluator<Rect> sBoundEvaluator = new TypeEvaluator<Rect>() {
+        public Rect evaluate(float fraction, Rect startValue, Rect endValue) {
+            return new Rect(interpolate(startValue.left, endValue.left, fraction),
+                    interpolate(startValue.top, endValue.top, fraction),
+                    interpolate(startValue.right, endValue.right, fraction),
+                    interpolate(startValue.bottom, endValue.bottom, fraction));
+        }
+
+        public int interpolate(int start, int end, float fraction) {
+            return (int)(start + fraction * (end - start));
+        }
+    };
+
+    /**
+     *  Determines whether this listview is in a scrolling state invoked
+     *  by the fact that the hover cell is out of the bounds of the listview;
+     */
+    private void handleMobileCellScroll() {
+        mIsMobileScrolling = handleMobileCellScroll(mHoverCellCurrentBounds);
+    }
+
+    /**
+     * This method is in charge of determining if the hover cell is above
+     * or below the bounds of the listview. If so, the listview does an appropriate
+     * upward or downward smooth scroll so as to reveal new items.
+     */
+    public boolean handleMobileCellScroll(Rect r) {
+        int offset = computeVerticalScrollOffset();
+        int height = getHeight();
+        int extent = computeVerticalScrollExtent();
+        int range = computeVerticalScrollRange();
+        int hoverViewTop = r.top;
+        int hoverHeight = r.height();
+
+        if (hoverViewTop <= 0 && offset > 0) {
+            smoothScrollBy(-mSmoothScrollAmountAtEdge, 0);
+            return true;
+        }
+
+        if (hoverViewTop + hoverHeight >= height && (offset + extent) < range) {
+            smoothScrollBy(mSmoothScrollAmountAtEdge, 0);
+            return true;
+        }
+
+        return false;
+    }
+
+    public void setCheeseList(ArrayList<String> cheeseList) {
+        mCheeseList = cheeseList;
+    }
+
+    /**
+     * This scroll listener is added to the listview in order to handle cell swapping
+     * when the cell is either at the top or bottom edge of the listview. If the hover
+     * cell is at either edge of the listview, the listview will begin scrolling. As
+     * scrolling takes place, the listview continuously checks if new cells became visible
+     * and determines whether they are potential candidates for a cell swap.
+     */
+    private AbsListView.OnScrollListener mScrollListener = new AbsListView.OnScrollListener () {
+
+        private int mPreviousFirstVisibleItem = -1;
+        private int mPreviousVisibleItemCount = -1;
+        private int mCurrentFirstVisibleItem;
+        private int mCurrentVisibleItemCount;
+        private int mCurrentScrollState;
+
+        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+                             int totalItemCount) {
+            mCurrentFirstVisibleItem = firstVisibleItem;
+            mCurrentVisibleItemCount = visibleItemCount;
+
+            mPreviousFirstVisibleItem = (mPreviousFirstVisibleItem == -1) ? mCurrentFirstVisibleItem
+                    : mPreviousFirstVisibleItem;
+            mPreviousVisibleItemCount = (mPreviousVisibleItemCount == -1) ? mCurrentVisibleItemCount
+                    : mPreviousVisibleItemCount;
+
+            checkAndHandleFirstVisibleCellChange();
+            checkAndHandleLastVisibleCellChange();
+
+            mPreviousFirstVisibleItem = mCurrentFirstVisibleItem;
+            mPreviousVisibleItemCount = mCurrentVisibleItemCount;
+        }
+
+        @Override
+        public void onScrollStateChanged(AbsListView view, int scrollState) {
+            mCurrentScrollState = scrollState;
+            mScrollState = scrollState;
+            isScrollCompleted();
+        }
+
+        /**
+         * This method is in charge of invoking 1 of 2 actions. Firstly, if the listview
+         * is in a state of scrolling invoked by the hover cell being outside the bounds
+         * of the listview, then this scrolling event is continued. Secondly, if the hover
+         * cell has already been released, this invokes the animation for the hover cell
+         * to return to its correct position after the listview has entered an idle scroll
+         * state.
+         */
+        private void isScrollCompleted() {
+            if (mCurrentVisibleItemCount > 0 && mCurrentScrollState == SCROLL_STATE_IDLE) {
+                if (mCellIsMobile && mIsMobileScrolling) {
+                    handleMobileCellScroll();
+                } else if (mIsWaitingForScrollFinish) {
+                    touchEventsEnded();
+                }
+            }
+        }
+
+        /**
+         * Determines if the listview scrolled up enough to reveal a new cell at the
+         * top of the list. If so, then the appropriate parameters are updated.
+         */
+        public void checkAndHandleFirstVisibleCellChange() {
+            if (mCurrentFirstVisibleItem != mPreviousFirstVisibleItem) {
+                if (mCellIsMobile && mMobileItemId != INVALID_ID) {
+                    updateNeighborViewsForID(mMobileItemId);
+                    handleCellSwitch();
+                }
+            }
+        }
+
+        /**
+         * Determines if the listview scrolled down enough to reveal a new cell at the
+         * bottom of the list. If so, then the appropriate parameters are updated.
+         */
+        public void checkAndHandleLastVisibleCellChange() {
+            int currentLastVisibleItem = mCurrentFirstVisibleItem + mCurrentVisibleItemCount;
+            int previousLastVisibleItem = mPreviousFirstVisibleItem + mPreviousVisibleItemCount;
+            if (currentLastVisibleItem != previousLastVisibleItem) {
+                if (mCellIsMobile && mMobileItemId != INVALID_ID) {
+                    updateNeighborViewsForID(mMobileItemId);
+                    handleCellSwitch();
+                }
+            }
+        }
+    };
+}
\ No newline at end of file
diff --git a/samples/devbytes/animation/ListViewDraggingAnimation/src/com/example/android/listviewdragginganimation/ListViewDraggingAnimation.java b/samples/devbytes/animation/ListViewDraggingAnimation/src/com/example/android/listviewdragginganimation/ListViewDraggingAnimation.java
new file mode 100644
index 0000000..496e49d
--- /dev/null
+++ b/samples/devbytes/animation/ListViewDraggingAnimation/src/com/example/android/listviewdragginganimation/ListViewDraggingAnimation.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.listviewdragginganimation;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.widget.ListView;
+
+import java.util.ArrayList;
+
+/**
+ * This application creates a listview where the ordering of the data set
+ * can be modified in response to user touch events.
+ *
+ * An item in the listview is selected via a long press event and is then
+ * moved around by tracking and following the movement of the user's finger.
+ * When the item is released, it animates to its new position within the listview.
+ */
+public class ListViewDraggingAnimation extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_list_view);
+
+        ArrayList<String>mCheeseList = new ArrayList<String>();
+        for (int i = 0; i < Cheeses.sCheeseStrings.length; ++i) {
+            mCheeseList.add(Cheeses.sCheeseStrings[i]);
+        }
+
+        StableArrayAdapter adapter = new StableArrayAdapter(this, R.layout.text_view, mCheeseList);
+        DynamicListView listView = (DynamicListView) findViewById(R.id.listview);
+
+        listView.setCheeseList(mCheeseList);
+        listView.setAdapter(adapter);
+        listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+    }
+}
diff --git a/samples/devbytes/animation/ListViewDraggingAnimation/src/com/example/android/listviewdragginganimation/StableArrayAdapter.java b/samples/devbytes/animation/ListViewDraggingAnimation/src/com/example/android/listviewdragginganimation/StableArrayAdapter.java
new file mode 100644
index 0000000..9146cca
--- /dev/null
+++ b/samples/devbytes/animation/ListViewDraggingAnimation/src/com/example/android/listviewdragginganimation/StableArrayAdapter.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.listviewdragginganimation;
+
+import android.content.Context;
+import android.widget.ArrayAdapter;
+
+import java.util.HashMap;
+import java.util.List;
+
+public class StableArrayAdapter extends ArrayAdapter<String> {
+
+    final int INVALID_ID = -1;
+
+    HashMap<String, Integer> mIdMap = new HashMap<String, Integer>();
+
+    public StableArrayAdapter(Context context, int textViewResourceId, List<String> objects) {
+        super(context, textViewResourceId, objects);
+        for (int i = 0; i < objects.size(); ++i) {
+            mIdMap.put(objects.get(i), i);
+        }
+    }
+
+    @Override
+    public long getItemId(int position) {
+        if (position < 0 || position >= mIdMap.size()) {
+            return INVALID_ID;
+        }
+        String item = getItem(position);
+        return mIdMap.get(item);
+    }
+
+    @Override
+    public boolean hasStableIds() {
+        return true;
+    }
+}
diff --git a/samples/devbytes/animation/ListViewExpandingCells/AndroidManifest.xml b/samples/devbytes/animation/ListViewExpandingCells/AndroidManifest.xml
new file mode 100644
index 0000000..1633d01
--- /dev/null
+++ b/samples/devbytes/animation/ListViewExpandingCells/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.example.android.expandingcells"
+          android:versionCode="1"
+          android:versionName="1.0">
+    <uses-sdk android:minSdkVersion="16"
+              android:targetSdkVersion="17"/>
+    <application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
+        <activity android:name=".ExpandingCells"
+                  android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/samples/devbytes/animation/ListViewExpandingCells/res/drawable-hdpi/border.9.png b/samples/devbytes/animation/ListViewExpandingCells/res/drawable-hdpi/border.9.png
new file mode 100644
index 0000000..f76e008
--- /dev/null
+++ b/samples/devbytes/animation/ListViewExpandingCells/res/drawable-hdpi/border.9.png
Binary files differ
diff --git a/samples/devbytes/animation/ListViewExpandingCells/res/drawable-hdpi/chameleon.jpg b/samples/devbytes/animation/ListViewExpandingCells/res/drawable-hdpi/chameleon.jpg
new file mode 100644
index 0000000..686cc88
--- /dev/null
+++ b/samples/devbytes/animation/ListViewExpandingCells/res/drawable-hdpi/chameleon.jpg
Binary files differ
diff --git a/samples/devbytes/animation/ListViewExpandingCells/res/drawable-hdpi/flower.jpg b/samples/devbytes/animation/ListViewExpandingCells/res/drawable-hdpi/flower.jpg
new file mode 100644
index 0000000..8842483
--- /dev/null
+++ b/samples/devbytes/animation/ListViewExpandingCells/res/drawable-hdpi/flower.jpg
Binary files differ
diff --git a/samples/devbytes/animation/ListViewExpandingCells/res/drawable-hdpi/ic_launcher.png b/samples/devbytes/animation/ListViewExpandingCells/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..96a442e
--- /dev/null
+++ b/samples/devbytes/animation/ListViewExpandingCells/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/ListViewExpandingCells/res/drawable-hdpi/rock.jpg b/samples/devbytes/animation/ListViewExpandingCells/res/drawable-hdpi/rock.jpg
new file mode 100644
index 0000000..8ea0e85
--- /dev/null
+++ b/samples/devbytes/animation/ListViewExpandingCells/res/drawable-hdpi/rock.jpg
Binary files differ
diff --git a/samples/devbytes/animation/ListViewExpandingCells/res/drawable-ldpi/ic_launcher.png b/samples/devbytes/animation/ListViewExpandingCells/res/drawable-ldpi/ic_launcher.png
new file mode 100644
index 0000000..9923872
--- /dev/null
+++ b/samples/devbytes/animation/ListViewExpandingCells/res/drawable-ldpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/ListViewExpandingCells/res/drawable-mdpi/ic_launcher.png b/samples/devbytes/animation/ListViewExpandingCells/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..359047d
--- /dev/null
+++ b/samples/devbytes/animation/ListViewExpandingCells/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/ListViewExpandingCells/res/drawable-xhdpi/ic_launcher.png b/samples/devbytes/animation/ListViewExpandingCells/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..71c6d76
--- /dev/null
+++ b/samples/devbytes/animation/ListViewExpandingCells/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/ListViewExpandingCells/res/layout/activity_main.xml b/samples/devbytes/animation/ListViewExpandingCells/res/layout/activity_main.xml
new file mode 100644
index 0000000..d904a58
--- /dev/null
+++ b/samples/devbytes/animation/ListViewExpandingCells/res/layout/activity_main.xml
@@ -0,0 +1,21 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.example.android.expandingcells.ExpandingListView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/main_list_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".MainActivity" />
diff --git a/samples/devbytes/animation/ListViewExpandingCells/res/layout/list_view_item.xml b/samples/devbytes/animation/ListViewExpandingCells/res/layout/list_view_item.xml
new file mode 100644
index 0000000..c2c2209
--- /dev/null
+++ b/samples/devbytes/animation/ListViewExpandingCells/res/layout/list_view_item.xml
@@ -0,0 +1,68 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@drawable/border"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:id="@+id/item_linear_layout"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:orientation="horizontal">
+
+        <ImageView
+            android:id="@+id/image_view"
+            android:layout_height="match_parent"
+            android:layout_width="0dp"
+            android:gravity="center_vertical"
+            android:layout_weight="1"
+            android:scaleType="center"/>
+
+        <TextView
+            android:id="@+id/title_view"
+            android:layout_height="fill_parent"
+            android:layout_width="0dp"
+            android:gravity="center"
+            android:layout_weight="2"
+            android:textStyle="bold"
+            android:textSize="22sp"
+            android:textColor="#ffffff"/>
+
+    </LinearLayout>
+
+    <com.example.android.expandingcells.ExpandingLayout
+            android:id="@+id/expanding_layout"
+            android:layout_height="wrap_content"
+            android:layout_width="match_parent"
+            android:visibility="gone">
+
+        <TextView
+            android:id="@+id/text_view"
+            android:layout_height="match_parent"
+            android:layout_width="match_parent"
+            android:textStyle="bold"
+            android:textSize="22sp"
+            android:textColor="#ffffff"
+            android:gravity="center_horizontal"
+            android:paddingLeft="20dp"
+            android:paddingRight="20dp"
+            android:paddingBottom="20dp"/>
+
+    </com.example.android.expandingcells.ExpandingLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/devbytes/animation/ListViewExpandingCells/res/values/strings.xml b/samples/devbytes/animation/ListViewExpandingCells/res/values/strings.xml
new file mode 100644
index 0000000..1fcee40
--- /dev/null
+++ b/samples/devbytes/animation/ListViewExpandingCells/res/values/strings.xml
@@ -0,0 +1,23 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+
+    <string name="app_name">ExpandingCells</string>
+    <string name="short_lorem_ipsum">Lorem ipsum dolor sit amet, consectetur adipiscing elit</string>
+    <string name="medium_lorem_ipsum">"Pellentesque dictum sit amet sapien in faucibus. Curabitur fermentum, nulla quis placerat imperdiet, est nisi placerat arcu, non ornare erat justo at enim. Nam vitae porttitor sem. Quisque non quam nisi. Proin quis urna id elit ultrices cursus non tempus dolor"</string>
+    <string name="long_lorem_ipsum">"Mauris dapibus convallis massa, vitae ultrices est ultricies ut. Nam porttitor et metus ac bibendum. Nam at justo vitae felis lacinia ultrices laoreet ut arcu. Nam ac purus et turpis convallis mollis. Integer lorem eros, hendrerit imperdiet interdum vitae, sagittis eget ipsum. Donec dignissim tortor at felis fringilla, sed dignissim diam vulputate. Nam sit amet facilisis massa. Suspendisse posuere quam quis augue dapibus venenatis."</string>
+
+</resources>
diff --git a/samples/devbytes/animation/ListViewExpandingCells/src/com/example/android/expandingcells/CustomArrayAdapter.java b/samples/devbytes/animation/ListViewExpandingCells/src/com/example/android/expandingcells/CustomArrayAdapter.java
new file mode 100644
index 0000000..68919a2
--- /dev/null
+++ b/samples/devbytes/animation/ListViewExpandingCells/src/com/example/android/expandingcells/CustomArrayAdapter.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.expandingcells;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.List;
+
+/**
+ * This is a custom array adapter used to populate the listview whose items will
+ * expand to display extra content in addition to the default display.
+ */
+public class CustomArrayAdapter extends ArrayAdapter<ExpandableListItem> {
+
+    private List<ExpandableListItem> mData;
+    private int mLayoutViewResourceId;
+
+    public CustomArrayAdapter(Context context, int layoutViewResourceId,
+                              List<ExpandableListItem> data) {
+        super(context, layoutViewResourceId, data);
+        mData = data;
+        mLayoutViewResourceId = layoutViewResourceId;
+    }
+
+    /**
+     * Populates the item in the listview cell with the appropriate data. This method
+     * sets the thumbnail image, the title and the extra text. This method also updates
+     * the layout parameters of the item's view so that the image and title are centered
+     * in the bounds of the collapsed view, and such that the extra text is not displayed
+     * in the collapsed state of the cell.
+     */
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+
+        final ExpandableListItem object = mData.get(position);
+
+        if(convertView == null) {
+            LayoutInflater inflater = ((Activity) getContext()).getLayoutInflater();
+            convertView = inflater.inflate(mLayoutViewResourceId, parent, false);
+        }
+
+        LinearLayout linearLayout = (LinearLayout)(convertView.findViewById(
+                R.id.item_linear_layout));
+        LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams
+                (AbsListView.LayoutParams.MATCH_PARENT, object.getCollapsedHeight());
+        linearLayout.setLayoutParams(linearLayoutParams);
+
+        ImageView imgView = (ImageView)convertView.findViewById(R.id.image_view);
+        TextView titleView = (TextView)convertView.findViewById(R.id.title_view);
+        TextView textView = (TextView)convertView.findViewById(R.id.text_view);
+
+        titleView.setText(object.getTitle());
+        imgView.setImageBitmap(getCroppedBitmap(BitmapFactory.decodeResource(getContext()
+                .getResources(), object.getImgResource(), null)));
+        textView.setText(object.getText());
+
+        convertView.setLayoutParams(new ListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT,
+                AbsListView.LayoutParams.WRAP_CONTENT));
+
+        ExpandingLayout expandingLayout = (ExpandingLayout)convertView.findViewById(R.id
+                .expanding_layout);
+        expandingLayout.setExpandedHeight(object.getExpandedHeight());
+        expandingLayout.setSizeChangedListener(object);
+
+        if (!object.isExpanded()) {
+            expandingLayout.setVisibility(View.GONE);
+        } else {
+            expandingLayout.setVisibility(View.VISIBLE);
+        }
+
+        return convertView;
+    }
+
+    /**
+     * Crops a circle out of the thumbnail photo.
+     */
+    public Bitmap getCroppedBitmap(Bitmap bitmap) {
+        Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(),
+                Config.ARGB_8888);
+
+        final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+
+        Canvas canvas = new Canvas(output);
+
+        final Paint paint = new Paint();
+        paint.setAntiAlias(true);
+
+        int halfWidth = bitmap.getWidth()/2;
+        int halfHeight = bitmap.getHeight()/2;
+
+        canvas.drawCircle(halfWidth, halfHeight, Math.max(halfWidth, halfHeight), paint);
+
+        paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
+
+        canvas.drawBitmap(bitmap, rect, rect, paint);
+
+        return output;
+    }
+
+
+}
\ No newline at end of file
diff --git a/samples/devbytes/animation/ListViewExpandingCells/src/com/example/android/expandingcells/ExpandableListItem.java b/samples/devbytes/animation/ListViewExpandingCells/src/com/example/android/expandingcells/ExpandableListItem.java
new file mode 100644
index 0000000..1eb4fc0
--- /dev/null
+++ b/samples/devbytes/animation/ListViewExpandingCells/src/com/example/android/expandingcells/ExpandableListItem.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.expandingcells;
+
+/**
+ * This custom object is used to populate the list adapter. It contains a reference
+ * to an image, title, and the extra text to be displayed. Furthermore, it keeps track
+ * of the current state (collapsed/expanded) of the corresponding item in the list,
+ * as well as store the height of the cell in its collapsed state.
+ */
+public class ExpandableListItem implements OnSizeChangedListener {
+
+    private String mTitle;
+    private String mText;
+    private boolean mIsExpanded;
+    private int mImgResource;
+    private int mCollapsedHeight;
+    private int mExpandedHeight;
+
+    public ExpandableListItem(String title, int imgResource, int collapsedHeight, String text) {
+        mTitle = title;
+        mImgResource = imgResource;
+        mCollapsedHeight = collapsedHeight;
+        mIsExpanded = false;
+        mText = text;
+        mExpandedHeight = -1;
+    }
+
+    public boolean isExpanded() {
+        return mIsExpanded;
+    }
+
+    public void setExpanded(boolean isExpanded) {
+        mIsExpanded = isExpanded;
+    }
+
+    public String getTitle() {
+        return mTitle;
+    }
+
+    public int getImgResource() {
+        return mImgResource;
+    }
+
+    public int getCollapsedHeight() {
+        return mCollapsedHeight;
+    }
+
+    public void setCollapsedHeight(int collapsedHeight) {
+        mCollapsedHeight = collapsedHeight;
+    }
+
+    public String getText() {
+        return mText;
+    }
+
+    public void setText(String text) {
+        mText = text;
+    }
+
+    public int getExpandedHeight() {
+        return mExpandedHeight;
+    }
+
+    public void setExpandedHeight(int expandedHeight) {
+        mExpandedHeight = expandedHeight;
+    }
+
+    @Override
+    public void onSizeChanged(int newHeight) {
+        setExpandedHeight(newHeight);
+    }
+}
diff --git a/samples/devbytes/animation/ListViewExpandingCells/src/com/example/android/expandingcells/ExpandingCells.java b/samples/devbytes/animation/ListViewExpandingCells/src/com/example/android/expandingcells/ExpandingCells.java
new file mode 100644
index 0000000..6d6d4f1
--- /dev/null
+++ b/samples/devbytes/animation/ListViewExpandingCells/src/com/example/android/expandingcells/ExpandingCells.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.expandingcells;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This activity creates a listview whose items can be clicked to expand and show
+ * additional content.
+ *
+ * In this specific demo, each item in a listview displays an image and a corresponding
+ * title. These two items are centered in the default (collapsed) state of the listview's
+ * item. When the item is clicked, it expands to display text of some varying length.
+ * The item persists in this expanded state (even if the user scrolls away and then scrolls
+ * back to the same location) until it is clicked again, at which point the cell collapses
+ * back to its default state.
+ */
+public class ExpandingCells extends Activity {
+
+    private final int CELL_DEFAULT_HEIGHT = 200;
+    private final int NUM_OF_CELLS = 30;
+
+    private ExpandingListView mListView;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        ExpandableListItem[] values = new ExpandableListItem[] {
+                new ExpandableListItem("Chameleon", R.drawable.chameleon, CELL_DEFAULT_HEIGHT,
+                        getResources().getString(R.string.short_lorem_ipsum)),
+                new ExpandableListItem("Rock", R.drawable.rock, CELL_DEFAULT_HEIGHT,
+                        getResources().getString(R.string.medium_lorem_ipsum)),
+                new ExpandableListItem("Flower", R.drawable.flower, CELL_DEFAULT_HEIGHT,
+                        getResources().getString(R.string.long_lorem_ipsum)),
+        };
+
+        List<ExpandableListItem> mData = new ArrayList<ExpandableListItem>();
+
+        for (int i = 0; i < NUM_OF_CELLS; i++) {
+            ExpandableListItem obj = values[i % values.length];
+            mData.add(new ExpandableListItem(obj.getTitle(), obj.getImgResource(),
+                    obj.getCollapsedHeight(), obj.getText()));
+        }
+
+        CustomArrayAdapter adapter = new CustomArrayAdapter(this, R.layout.list_view_item, mData);
+
+        mListView = (ExpandingListView)findViewById(R.id.main_list_view);
+        mListView.setAdapter(adapter);
+        mListView.setDivider(null);
+    }
+}
\ No newline at end of file
diff --git a/samples/devbytes/animation/ListViewExpandingCells/src/com/example/android/expandingcells/ExpandingLayout.java b/samples/devbytes/animation/ListViewExpandingCells/src/com/example/android/expandingcells/ExpandingLayout.java
new file mode 100644
index 0000000..5add667
--- /dev/null
+++ b/samples/devbytes/animation/ListViewExpandingCells/src/com/example/android/expandingcells/ExpandingLayout.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.expandingcells;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.RelativeLayout;
+
+/**
+ * This layout is used to contain the extra information that will be displayed
+ * when a certain cell is expanded. The custom relative layout is created in
+ * order to achieve a fading affect of this layout's contents as it is being
+ * expanded or collapsed as opposed to just fading the content in(out) after(before)
+ * the cell expands(collapses).
+ *
+ * During expansion, layout takes place so the full contents of this layout can
+ * be displayed. When the size changes to display the full contents of the layout,
+ * its height is stored. When the view is collapsing, this layout's height becomes 0
+ * since it is no longer in the visible part of the cell.By overriding onMeasure, and
+ * setting the height back to its max height, it is still visible during the collapse
+ * animation, and so, a fade out effect can be achieved.
+ */
+public class ExpandingLayout extends RelativeLayout {
+
+
+    private OnSizeChangedListener mSizeChangedListener;
+    private int mExpandedHeight = -1;
+
+    public ExpandingLayout(Context context) {
+        super(context);
+    }
+
+    public ExpandingLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public ExpandingLayout(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) {
+        if (mExpandedHeight > 0) {
+            heightMeasureSpec = MeasureSpec.makeMeasureSpec(mExpandedHeight, MeasureSpec.AT_MOST);
+        }
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    protected void onSizeChanged (int w, int h, int oldw, int oldh) {
+        mExpandedHeight = h;
+        //Notifies the list data object corresponding to this layout that its size has changed.
+        mSizeChangedListener.onSizeChanged(h);
+    }
+
+    public int getExpandedHeight() {
+        return mExpandedHeight;
+    }
+
+    public void setExpandedHeight(int expandedHeight) {
+        mExpandedHeight = expandedHeight;
+    }
+
+    public void setSizeChangedListener(OnSizeChangedListener listener) {
+        mSizeChangedListener = listener;
+    }
+}
diff --git a/samples/devbytes/animation/ListViewExpandingCells/src/com/example/android/expandingcells/ExpandingListView.java b/samples/devbytes/animation/ListViewExpandingCells/src/com/example/android/expandingcells/ExpandingListView.java
new file mode 100644
index 0000000..0ba5d5f
--- /dev/null
+++ b/samples/devbytes/animation/ListViewExpandingCells/src/com/example/android/expandingcells/ExpandingListView.java
@@ -0,0 +1,540 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.expandingcells;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.ListView;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * A custom listview which supports the preview of extra content corresponding to each cell
+ * by clicking on the cell to hide and show the extra content.
+ */
+public class ExpandingListView extends ListView {
+
+    private boolean mShouldRemoveObserver = false;
+
+    private List<View> mViewsToDraw = new ArrayList<View>();
+
+    private int[] mTranslate;
+
+    public ExpandingListView(Context context) {
+        super(context);
+        init();
+    }
+
+    public ExpandingListView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    public ExpandingListView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init();
+    }
+
+    private void init() {
+        setOnItemClickListener(mItemClickListener);
+    }
+
+    /**
+     * Listens for item clicks and expands or collapses the selected view depending on
+     * its current state.
+     */
+    private AdapterView.OnItemClickListener mItemClickListener = new AdapterView
+            .OnItemClickListener() {
+        @Override
+        public void onItemClick (AdapterView<?> parent, View view, int position, long id) {
+            ExpandableListItem viewObject = (ExpandableListItem)getItemAtPosition(getPositionForView
+                    (view));
+            if (!viewObject.isExpanded()) {
+                expandView(view);
+            } else {
+                collapseView(view);
+            }
+        }
+    };
+
+    /**
+     * Calculates the top and bottom bound changes of the selected item. These values are
+     * also used to move the bounds of the items around the one that is actually being
+     * expanded or collapsed.
+     *
+     * This method can be modified to achieve different user experiences depending
+     * on how you want the cells to expand or collapse. In this specific demo, the cells
+     * always try to expand downwards (leaving top bound untouched), and similarly,
+     * collapse upwards (leaving top bound untouched). If the change in bounds
+     * results in the complete disappearance of a cell, its lower bound is moved is
+     * moved to the top of the screen so as not to hide any additional content that
+     * the user has not interacted with yet. Furthermore, if the collapsed cell is
+     * partially off screen when it is first clicked, it is translated such that its
+     * full contents are visible. Lastly, this behaviour varies slightly near the bottom
+     * of the listview in order to account for the fact that the bottom bounds of the actual
+     * listview cannot be modified.
+     */
+    private int[] getTopAndBottomTranslations(int top, int bottom, int yDelta,
+                                              boolean isExpanding) {
+        int yTranslateTop = 0;
+        int yTranslateBottom = yDelta;
+
+        int height = bottom - top;
+
+        if (isExpanding) {
+            boolean isOverTop = top < 0;
+            boolean isBelowBottom = (top + height + yDelta) > getHeight();
+            if (isOverTop) {
+                yTranslateTop = top;
+                yTranslateBottom = yDelta - yTranslateTop;
+            } else if (isBelowBottom){
+                int deltaBelow = top + height + yDelta - getHeight();
+                yTranslateTop = top - deltaBelow < 0 ? top : deltaBelow;
+                yTranslateBottom = yDelta - yTranslateTop;
+            }
+        } else {
+            int offset = computeVerticalScrollOffset();
+            int range = computeVerticalScrollRange();
+            int extent = computeVerticalScrollExtent();
+            int leftoverExtent = range-offset - extent;
+
+            boolean isCollapsingBelowBottom = (yTranslateBottom > leftoverExtent);
+            boolean isCellCompletelyDisappearing = bottom - yTranslateBottom < 0;
+
+            if (isCollapsingBelowBottom) {
+                yTranslateTop = yTranslateBottom - leftoverExtent;
+                yTranslateBottom = yDelta - yTranslateTop;
+            } else if (isCellCompletelyDisappearing) {
+                yTranslateBottom = bottom;
+                yTranslateTop = yDelta - yTranslateBottom;
+            }
+        }
+
+        return new int[] {yTranslateTop, yTranslateBottom};
+    }
+
+    /**
+     * This method expands the view that was clicked and animates all the views
+     * around it to make room for the expanding view. There are several steps required
+     * to do this which are outlined below.
+     *
+     * 1. Store the current top and bottom bounds of each visible item in the listview.
+     * 2. Update the layout parameters of the selected view. In the context of this
+     *    method, the view should be originally collapsed and set to some custom height.
+     *    The layout parameters are updated so as to wrap the content of the additional
+     *    text that is to be displayed.
+     *
+     * After invoking a layout to take place, the listview will order all the items
+     * such that there is space for each view. This layout will be independent of what
+     * the bounds of the items were prior to the layout so two pre-draw passes will
+     * be made. This is necessary because after the layout takes place, some views that
+     * were visible before the layout may now be off bounds but a reference to these
+     * views is required so the animation completes as intended.
+     *
+     * 3. The first predraw pass will set the bounds of all the visible items to
+     *    their original location before the layout took place and then force another
+     *    layout. Since the bounds of the cells cannot be set directly, the method
+     *    setSelectionFromTop can be used to achieve a very similar effect.
+     * 4. The expanding view's bounds are animated to what the final values should be
+     *    from the original bounds.
+     * 5. The bounds above the expanding view are animated upwards while the bounds
+     *    below the expanding view are animated downwards.
+     * 6. The extra text is faded in as its contents become visible throughout the
+     *    animation process.
+     *
+     * It is important to note that the listview is disabled during the animation
+     * because the scrolling behaviour is unpredictable if the bounds of the items
+     * within the listview are not constant during the scroll.
+     */
+
+    private void expandView(final View view) {
+        final ExpandableListItem viewObject = (ExpandableListItem)getItemAtPosition(getPositionForView
+                (view));
+
+        /* Store the original top and bottom bounds of all the cells.*/
+        final int oldTop = view.getTop();
+        final int oldBottom = view.getBottom();
+
+        final HashMap<View, int[]> oldCoordinates = new HashMap<View, int[]>();
+
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View v = getChildAt(i);
+            v.setHasTransientState(true);
+            oldCoordinates.put(v, new int[] {v.getTop(), v.getBottom()});
+        }
+
+        /* Update the layout so the extra content becomes visible.*/
+        final View expandingLayout = view.findViewById(R.id.expanding_layout);
+        expandingLayout.setVisibility(View.VISIBLE);
+
+        /* Add an onPreDraw Listener to the listview. onPreDraw will get invoked after onLayout
+        * and onMeasure have run but before anything has been drawn. This
+        * means that the final post layout properties for all the items have already been
+        * determined, but still have not been rendered onto the screen.*/
+        final ViewTreeObserver observer = getViewTreeObserver();
+        observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+
+            @Override
+            public boolean onPreDraw() {
+                /* Determine if this is the first or second pass.*/
+                if (!mShouldRemoveObserver) {
+                    mShouldRemoveObserver = true;
+
+                    /* Calculate what the parameters should be for setSelectionFromTop.
+                    * The ListView must be offset in a way, such that after the animation
+                    * takes place, all the cells that remain visible are rendered completely
+                    * by the ListView.*/
+                    int newTop = view.getTop();
+                    int newBottom = view.getBottom();
+
+                    int newHeight = newBottom - newTop;
+                    int oldHeight = oldBottom - oldTop;
+                    int delta = newHeight - oldHeight;
+
+                    mTranslate = getTopAndBottomTranslations(oldTop, oldBottom, delta, true);
+
+                    int currentTop = view.getTop();
+                    int futureTop = oldTop - mTranslate[0];
+
+                    int firstChildStartTop = getChildAt(0).getTop();
+                    int firstVisiblePosition = getFirstVisiblePosition();
+                    int deltaTop = currentTop - futureTop;
+
+                    int i;
+                    int childCount = getChildCount();
+                    for (i = 0; i < childCount; i++) {
+                        View v = getChildAt(i);
+                        int height = v.getBottom() - Math.max(0, v.getTop());
+                        if (deltaTop - height > 0) {
+                            firstVisiblePosition++;
+                            deltaTop -= height;
+                        } else {
+                            break;
+                        }
+                    }
+
+                    if (i > 0) {
+                        firstChildStartTop = 0;
+                    }
+
+                    setSelectionFromTop(firstVisiblePosition, firstChildStartTop - deltaTop);
+
+                    /* Request another layout to update the layout parameters of the cells.*/
+                    requestLayout();
+
+                    /* Return false such that the ListView does not redraw its contents on
+                     * this layout but only updates all the parameters associated with its
+                     * children.*/
+                    return false;
+                }
+
+                /* Remove the predraw listener so this method does not keep getting called. */
+                mShouldRemoveObserver = false;
+                observer.removeOnPreDrawListener(this);
+
+                int yTranslateTop = mTranslate[0];
+                int yTranslateBottom = mTranslate[1];
+
+                ArrayList <Animator> animations = new ArrayList<Animator>();
+
+                int index = indexOfChild(view);
+
+                /* Loop through all the views that were on the screen before the cell was
+                *  expanded. Some cells will still be children of the ListView while
+                *  others will not. The cells that remain children of the ListView
+                *  simply have their bounds animated appropriately. The cells that are no
+                *  longer children of the ListView also have their bounds animated, but
+                *  must also be added to a list of views which will be drawn in dispatchDraw.*/
+                for (View v: oldCoordinates.keySet()) {
+                    int[] old = oldCoordinates.get(v);
+                    v.setTop(old[0]);
+                    v.setBottom(old[1]);
+                    if (v.getParent() == null) {
+                        mViewsToDraw.add(v);
+                        int delta = old[0] < oldTop ? -yTranslateTop : yTranslateBottom;
+                        animations.add(getAnimation(v, delta, delta));
+                    } else {
+                        int i = indexOfChild(v);
+                        if (v != view) {
+                            int delta = i > index ? yTranslateBottom : -yTranslateTop;
+                            animations.add(getAnimation(v, delta, delta));
+                        }
+                        v.setHasTransientState(false);
+                    }
+                }
+
+                /* Adds animation for expanding the cell that was clicked. */
+                animations.add(getAnimation(view, -yTranslateTop, yTranslateBottom));
+
+                /* Adds an animation for fading in the extra content. */
+                animations.add(ObjectAnimator.ofFloat(view.findViewById(R.id.expanding_layout),
+                        View.ALPHA, 0, 1));
+
+                /* Disabled the ListView for the duration of the animation.*/
+                setEnabled(false);
+                setClickable(false);
+
+                /* Play all the animations created above together at the same time. */
+                AnimatorSet s = new AnimatorSet();
+                s.playTogether(animations);
+                s.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        viewObject.setExpanded(true);
+                        setEnabled(true);
+                        setClickable(true);
+                        if (mViewsToDraw.size() > 0) {
+                            for (View v : mViewsToDraw) {
+                                v.setHasTransientState(false);
+                            }
+                        }
+                        mViewsToDraw.clear();
+                    }
+                });
+                s.start();
+                return true;
+            }
+        });
+    }
+
+    /**
+     * By overriding dispatchDraw, we can draw the cells that disappear during the
+     * expansion process. When the cell expands, some items below or above the expanding
+     * cell may be moved off screen and are thus no longer children of the ListView's
+     * layout. By storing a reference to these views prior to the layout, and
+     * guaranteeing that these cells do not get recycled, the cells can be drawn
+     * directly onto the canvas during the animation process. After the animation
+     * completes, the references to the extra views can then be discarded.
+     */
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+
+        if (mViewsToDraw.size() == 0) {
+            return;
+        }
+
+        for (View v: mViewsToDraw) {
+            canvas.translate(0, v.getTop());
+            v.draw(canvas);
+            canvas.translate(0, -v.getTop());
+        }
+    }
+
+    /**
+     * This method collapses the view that was clicked and animates all the views
+     * around it to close around the collapsing view. There are several steps required
+     * to do this which are outlined below.
+     *
+     * 1. Update the layout parameters of the view clicked so as to minimize its height
+     *    to the original collapsed (default) state.
+     * 2. After invoking a layout, the listview will shift all the cells so as to display
+     *    them most efficiently. Therefore, during the first predraw pass, the listview
+     *    must be offset by some amount such that given the custom bound change upon
+     *    collapse, all the cells that need to be on the screen after the layout
+     *    are rendered by the listview.
+     * 3. On the second predraw pass, all the items are first returned to their original
+     *    location (before the first layout).
+     * 4. The collapsing view's bounds are animated to what the final values should be.
+     * 5. The bounds above the collapsing view are animated downwards while the bounds
+     *    below the collapsing view are animated upwards.
+     * 6. The extra text is faded out as its contents become visible throughout the
+     *    animation process.
+     */
+
+     private void collapseView(final View view) {
+        final ExpandableListItem viewObject = (ExpandableListItem)getItemAtPosition
+                (getPositionForView(view));
+
+        /* Store the original top and bottom bounds of all the cells.*/
+        final int oldTop = view.getTop();
+        final int oldBottom = view.getBottom();
+
+        final HashMap<View, int[]> oldCoordinates = new HashMap<View, int[]>();
+
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View v = getChildAt(i);
+            v.setHasTransientState(true);
+            oldCoordinates.put(v, new int [] {v.getTop(), v.getBottom()});
+        }
+
+        /* Update the layout so the extra content becomes invisible.*/
+        view.setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT,
+                 viewObject.getCollapsedHeight()));
+
+         /* Add an onPreDraw listener. */
+        final ViewTreeObserver observer = getViewTreeObserver();
+        observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+
+            @Override
+            public boolean onPreDraw() {
+
+                if (!mShouldRemoveObserver) {
+                    /*Same as for expandingView, the parameters for setSelectionFromTop must
+                    * be determined such that the necessary cells of the ListView are rendered
+                    * and added to it.*/
+                    mShouldRemoveObserver = true;
+
+                    int newTop = view.getTop();
+                    int newBottom = view.getBottom();
+
+                    int newHeight = newBottom - newTop;
+                    int oldHeight = oldBottom - oldTop;
+                    int deltaHeight = oldHeight - newHeight;
+
+                    mTranslate = getTopAndBottomTranslations(oldTop, oldBottom, deltaHeight, false);
+
+                    int currentTop = view.getTop();
+                    int futureTop = oldTop + mTranslate[0];
+
+                    int firstChildStartTop = getChildAt(0).getTop();
+                    int firstVisiblePosition = getFirstVisiblePosition();
+                    int deltaTop = currentTop - futureTop;
+
+                    int i;
+                    int childCount = getChildCount();
+                    for (i = 0; i < childCount; i++) {
+                        View v = getChildAt(i);
+                        int height = v.getBottom() - Math.max(0, v.getTop());
+                        if (deltaTop - height > 0) {
+                            firstVisiblePosition++;
+                            deltaTop -= height;
+                        } else {
+                            break;
+                        }
+                    }
+
+                    if (i > 0) {
+                        firstChildStartTop = 0;
+                    }
+
+                    setSelectionFromTop(firstVisiblePosition, firstChildStartTop - deltaTop);
+
+                    requestLayout();
+
+                    return false;
+                }
+
+                mShouldRemoveObserver = false;
+                observer.removeOnPreDrawListener(this);
+
+                int yTranslateTop = mTranslate[0];
+                int yTranslateBottom = mTranslate[1];
+
+                int index = indexOfChild(view);
+                int childCount = getChildCount();
+                for (int i = 0; i < childCount; i++) {
+                    View v = getChildAt(i);
+                    int [] old = oldCoordinates.get(v);
+                    if (old != null) {
+                        /* If the cell was present in the ListView before the collapse and
+                        * after the collapse then the bounds are reset to their old values.*/
+                        v.setTop(old[0]);
+                        v.setBottom(old[1]);
+                        v.setHasTransientState(false);
+                    } else {
+                        /* If the cell is present in the ListView after the collapse but
+                         * not before the collapse then the bounds are calculated using
+                         * the bottom and top translation of the collapsing cell.*/
+                        int delta = i > index ? yTranslateBottom : -yTranslateTop;
+                        v.setTop(v.getTop() + delta);
+                        v.setBottom(v.getBottom() + delta);
+                    }
+                }
+
+                final View expandingLayout = view.findViewById (R.id.expanding_layout);
+
+                /* Animates all the cells present on the screen after the collapse. */
+                ArrayList <Animator> animations = new ArrayList<Animator>();
+                for (int i = 0; i < childCount; i++) {
+                    View v = getChildAt(i);
+                    if (v != view) {
+                        float diff = i > index ? -yTranslateBottom : yTranslateTop;
+                        animations.add(getAnimation(v, diff, diff));
+                    }
+                }
+
+
+                /* Adds animation for collapsing the cell that was clicked. */
+                animations.add(getAnimation(view, yTranslateTop, -yTranslateBottom));
+
+                /* Adds an animation for fading out the extra content. */
+                animations.add(ObjectAnimator.ofFloat(expandingLayout, View.ALPHA, 1, 0));
+
+                /* Disabled the ListView for the duration of the animation.*/
+                setEnabled(false);
+                setClickable(false);
+
+                /* Play all the animations created above together at the same time. */
+                AnimatorSet s = new AnimatorSet();
+                s.playTogether(animations);
+                s.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        expandingLayout.setVisibility(View.GONE);
+                        view.setLayoutParams(new AbsListView.LayoutParams(AbsListView
+                                .LayoutParams.MATCH_PARENT, AbsListView.LayoutParams.WRAP_CONTENT));
+                        viewObject.setExpanded(false);
+                        setEnabled(true);
+                        setClickable(true);
+                        /* Note that alpha must be set back to 1 in case this view is reused
+                        * by a cell that was expanded, but not yet collapsed, so its state
+                        * should persist in an expanded state with the extra content visible.*/
+                        expandingLayout.setAlpha(1);
+                    }
+                });
+                s.start();
+
+                return true;
+            }
+        });
+    }
+
+    /**
+     * This method takes some view and the values by which its top and bottom bounds
+     * should be changed by. Given these params, an animation which will animate
+     * these bound changes is created and returned.
+     */
+    private Animator getAnimation(final View view, float translateTop, float translateBottom) {
+
+        int top = view.getTop();
+        int bottom = view.getBottom();
+
+        int endTop = (int)(top + translateTop);
+        int endBottom = (int)(bottom + translateBottom);
+
+        PropertyValuesHolder translationTop = PropertyValuesHolder.ofInt("top", top, endTop);
+        PropertyValuesHolder translationBottom = PropertyValuesHolder.ofInt("bottom", bottom,
+                endBottom);
+
+        return ObjectAnimator.ofPropertyValuesHolder(view, translationTop, translationBottom);
+    }
+}
diff --git a/samples/devbytes/animation/ListViewExpandingCells/src/com/example/android/expandingcells/OnSizeChangedListener.java b/samples/devbytes/animation/ListViewExpandingCells/src/com/example/android/expandingcells/OnSizeChangedListener.java
new file mode 100644
index 0000000..ec51950
--- /dev/null
+++ b/samples/devbytes/animation/ListViewExpandingCells/src/com/example/android/expandingcells/OnSizeChangedListener.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.expandingcells;
+
+/**
+ * A listener used to update the list data object when the corresponding expanding
+ * layout experiences a size change.
+ */
+public interface OnSizeChangedListener {
+    public void onSizeChanged(int newHeight);
+}
diff --git a/samples/devbytes/animation/SlidingFragments/AndroidManifest.xml b/samples/devbytes/animation/SlidingFragments/AndroidManifest.xml
new file mode 100644
index 0000000..2e71b6c
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.example.android.slidingfragments"
+          android:versionCode="1"
+          android:versionName="1.0">
+    <uses-sdk android:minSdkVersion="14"
+              android:targetSdkVersion="17"/>
+    <application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
+        <activity android:name=".SlidingFragments"
+                  android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/samples/devbytes/animation/SlidingFragments/res/animator/slide_fragment_in.xml b/samples/devbytes/animation/SlidingFragments/res/animator/slide_fragment_in.xml
new file mode 100644
index 0000000..7fe333b
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/res/animator/slide_fragment_in.xml
@@ -0,0 +1,22 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:valueFrom="0"
+    android:valueTo="@dimen/slide_up_down_fraction"
+    android:propertyName="yFraction"
+    android:valueType="floatType"
+    android:duration="@integer/slide_up_down_duration">
+</objectAnimator>
\ No newline at end of file
diff --git a/samples/devbytes/animation/SlidingFragments/res/animator/slide_fragment_out.xml b/samples/devbytes/animation/SlidingFragments/res/animator/slide_fragment_out.xml
new file mode 100644
index 0000000..4d4134a
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/res/animator/slide_fragment_out.xml
@@ -0,0 +1,22 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:valueFrom="@dimen/slide_up_down_fraction"
+    android:valueTo="0"
+    android:propertyName="yFraction"
+    android:valueType="floatType"
+    android:duration="@integer/slide_up_down_duration" >
+</objectAnimator>
\ No newline at end of file
diff --git a/samples/devbytes/animation/SlidingFragments/res/drawable-hdpi/golden_gate.jpg b/samples/devbytes/animation/SlidingFragments/res/drawable-hdpi/golden_gate.jpg
new file mode 100644
index 0000000..9a3d3fd
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/res/drawable-hdpi/golden_gate.jpg
Binary files differ
diff --git a/samples/devbytes/animation/SlidingFragments/res/drawable-hdpi/ic_launcher.png b/samples/devbytes/animation/SlidingFragments/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..96a442e
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/SlidingFragments/res/drawable-ldpi/ic_launcher.png b/samples/devbytes/animation/SlidingFragments/res/drawable-ldpi/ic_launcher.png
new file mode 100644
index 0000000..9923872
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/res/drawable-ldpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/SlidingFragments/res/drawable-mdpi/ic_launcher.png b/samples/devbytes/animation/SlidingFragments/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..359047d
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/SlidingFragments/res/drawable-xhdpi/ic_launcher.png b/samples/devbytes/animation/SlidingFragments/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..71c6d76
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/animation/SlidingFragments/res/layout/image_fragment.xml b/samples/devbytes/animation/SlidingFragments/res/layout/image_fragment.xml
new file mode 100644
index 0000000..eb555fe
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/res/layout/image_fragment.xml
@@ -0,0 +1,20 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:src="@drawable/golden_gate" >
+</ImageView>
\ No newline at end of file
diff --git a/samples/devbytes/animation/SlidingFragments/res/layout/sliding_fragments_layout.xml b/samples/devbytes/animation/SlidingFragments/res/layout/sliding_fragments_layout.xml
new file mode 100644
index 0000000..a789a77
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/res/layout/sliding_fragments_layout.xml
@@ -0,0 +1,31 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:id="@+id/move_to_back_container">
+
+    <fragment class="com.example.android.slidingfragments.ImageFragment"
+        android:id="@+id/move_fragment"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <View android:id="@+id/dark_hover_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@color/black" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/samples/devbytes/animation/SlidingFragments/res/layout/text_fragment.xml b/samples/devbytes/animation/SlidingFragments/res/layout/text_fragment.xml
new file mode 100644
index 0000000..652acaa
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/res/layout/text_fragment.xml
@@ -0,0 +1,104 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<com.example.android.slidingfragments.FractionalLinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_marginLeft="@dimen/slide_fragment_horizontal_margin"
+    android:layout_marginRight="@dimen/slide_fragment_horizontal_margin"
+    android:orientation="vertical"
+    android:background="@color/white">
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/bridge"
+        android:textColor="#000000"
+        android:textStyle="bold"
+        android:textSize="26sp"
+        android:layout_marginBottom="@dimen/text_vertical_margin"
+        android:layout_marginLeft="@dimen/text_horizontal_margin"
+        android:layout_marginRight="@dimen/text_horizontal_margin" />
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/summary"
+        android:textSize="18sp"
+        android:textColor="#000000"
+        android:layout_marginBottom="@dimen/text_vertical_margin"
+        android:layout_marginLeft="@dimen/text_horizontal_margin"
+        android:layout_marginRight="@dimen/text_horizontal_margin" />
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/length"
+        android:textColor="#000000"
+        android:layout_marginLeft="@dimen/text_horizontal_margin"
+        android:layout_marginRight="@dimen/text_horizontal_margin" />
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/span"
+        android:textColor="#000000"
+        android:layout_marginLeft="@dimen/text_horizontal_margin"
+        android:layout_marginRight="@dimen/text_horizontal_margin" />
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/construction"
+        android:textColor="#000000"
+        android:layout_marginLeft="@dimen/text_horizontal_margin"
+        android:layout_marginRight="@dimen/text_horizontal_margin" />
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/height"
+        android:textColor="#000000"
+        android:layout_marginLeft="@dimen/text_horizontal_margin"
+        android:layout_marginRight="@dimen/text_horizontal_margin" />
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/elevation"
+        android:textColor="#000000"
+        android:layout_marginLeft="@dimen/text_horizontal_margin"
+        android:layout_marginRight="@dimen/text_horizontal_margin" />
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/address"
+        android:textColor="#000000"
+        android:layout_marginLeft="@dimen/text_horizontal_margin"
+        android:layout_marginRight="@dimen/text_horizontal_margin" />
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/photo_credits"
+        android:textColor="#000000"
+        android:textSize="10sp"
+        android:layout_marginTop="@dimen/text_vertical_margin"
+        android:layout_marginLeft="@dimen/text_horizontal_margin"
+        android:layout_marginRight="@dimen/text_horizontal_margin" />
+
+</com.example.android.slidingfragments.FractionalLinearLayout>
\ No newline at end of file
diff --git a/samples/devbytes/animation/SlidingFragments/res/values/colors.xml b/samples/devbytes/animation/SlidingFragments/res/values/colors.xml
new file mode 100644
index 0000000..f5eb07a
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/res/values/colors.xml
@@ -0,0 +1,23 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+
+    <color name="red">#ff0000</color>
+    <color name="black">#000000</color>
+    <color name="white">#ffffff</color>
+    <color name="green">#00ff00</color>
+
+</resources>
\ No newline at end of file
diff --git a/samples/devbytes/animation/SlidingFragments/res/values/dimens.xml b/samples/devbytes/animation/SlidingFragments/res/values/dimens.xml
new file mode 100644
index 0000000..2b378c1
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/res/values/dimens.xml
@@ -0,0 +1,22 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+
+    <dimen name="slide_fragment_horizontal_margin">50dp</dimen>
+    <dimen name="text_vertical_margin">18dp</dimen>
+    <dimen name="text_horizontal_margin">10dp</dimen>
+
+</resources>
diff --git a/samples/devbytes/animation/SlidingFragments/res/values/integers.xml b/samples/devbytes/animation/SlidingFragments/res/values/integers.xml
new file mode 100644
index 0000000..57c19ac
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/res/values/integers.xml
@@ -0,0 +1,22 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+
+    <integer name="slide_up_down_duration">200</integer>
+    <integer name="half_slide_up_down_duration">150</integer>
+    <integer name="slide_up_down_final_value">150</integer>
+
+</resources>
\ No newline at end of file
diff --git a/samples/devbytes/animation/SlidingFragments/res/values/items.xml b/samples/devbytes/animation/SlidingFragments/res/values/items.xml
new file mode 100644
index 0000000..2b7f0b5
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/res/values/items.xml
@@ -0,0 +1,20 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+
+    <item name="slide_up_down_fraction" type="dimen" format="float">0.67</item>
+
+</resources>
\ No newline at end of file
diff --git a/samples/devbytes/animation/SlidingFragments/res/values/strings.xml b/samples/devbytes/animation/SlidingFragments/res/values/strings.xml
new file mode 100644
index 0000000..dbc41da
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/res/values/strings.xml
@@ -0,0 +1,29 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+
+    <string name="app_name">SlidingFragments</string>
+    <string name="photo_credits"><b>Golden Gate Sunset</b> by <b>Romain Guy</b></string>
+    <string name="address"><b>Address:</b> Golden Gate Bridge, San Francisco, CA 94129</string>
+    <string name="elevation"><b>Elevation:</b>  220\' (67 m)</string>
+    <string name="height"><b>Height:</b>  746\' (227 m)</string>
+    <string name="construction"><b>Construction started:</b>  1933</string>
+    <string name="span"><b>Longest span:</b>  4,200\' (1,280 m)</string>
+    <string name="length"><b>Total length:</b>  8,980\' (2,737 m)</string>
+    <string name="summary">"The Golden Gate Bridge is a suspension bridge spanning the Golden Gate, the opening of the San Francisco Bay into the Pacific Ocean."</string>
+    <string name="bridge">Golden Gate Bridge</string>
+
+</resources>
diff --git a/samples/devbytes/animation/SlidingFragments/src/com/example/android/slidingfragments/FractionalLinearLayout.java b/samples/devbytes/animation/SlidingFragments/src/com/example/android/slidingfragments/FractionalLinearLayout.java
new file mode 100644
index 0000000..55f7269
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/src/com/example/android/slidingfragments/FractionalLinearLayout.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.slidingfragments;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+/**
+ * In order to animate the fragment containing text on/off the screen,
+ * it is required that we know the height of the device being used. However,
+ * this can only be determined at runtime, so we cannot specify the required
+ * translation in an xml file. Since FragmentTransaction's setCustomAnimations
+ * method requires an ID of an animation defined via an xml file, this linear
+ * layout was built as a workaround. This custom linear layout is created to specify
+ * the location of the fragment's layout as a fraction of the device's height. By
+ * animating yFraction from 0 to 1, we can animate the fragment from the top of
+ * the screen to the bottom of the screen, regardless of the device's specific size.
+ */
+public class FractionalLinearLayout extends LinearLayout {
+
+    private float mYFraction;
+    private int mScreenHeight;
+
+    public FractionalLinearLayout(Context context) {
+        super(context);
+    }
+
+    public FractionalLinearLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onSizeChanged (int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        mScreenHeight = h;
+        setY(mScreenHeight);
+    }
+
+    public float getYFraction() {
+        return mYFraction;
+    }
+
+    public void setYFraction(float yFraction) {
+        mYFraction = yFraction;
+        setY((mScreenHeight > 0) ? (mScreenHeight - yFraction * mScreenHeight) : 0);
+    }
+}
diff --git a/samples/devbytes/animation/SlidingFragments/src/com/example/android/slidingfragments/ImageFragment.java b/samples/devbytes/animation/SlidingFragments/src/com/example/android/slidingfragments/ImageFragment.java
new file mode 100644
index 0000000..e306c98
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/src/com/example/android/slidingfragments/ImageFragment.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.slidingfragments;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class ImageFragment extends Fragment {
+
+    View.OnClickListener clickListener;
+
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.image_fragment, container, false);
+        view.setOnClickListener(clickListener);
+        return view;
+    }
+
+    public void setClickListener(View.OnClickListener clickListener) {
+        this.clickListener = clickListener;
+    }
+}
diff --git a/samples/devbytes/animation/SlidingFragments/src/com/example/android/slidingfragments/OnTextFragmentAnimationEndListener.java b/samples/devbytes/animation/SlidingFragments/src/com/example/android/slidingfragments/OnTextFragmentAnimationEndListener.java
new file mode 100644
index 0000000..692a7cb
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/src/com/example/android/slidingfragments/OnTextFragmentAnimationEndListener.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.slidingfragments;
+
+/**
+ * This interface is used to inform the main activity when the entry
+ * animation of the text fragment has completed in order to avoid the
+ * start of a new animation before the current one has completed.
+ */
+public interface OnTextFragmentAnimationEndListener {
+    public void onAnimationEnd();
+}
diff --git a/samples/devbytes/animation/SlidingFragments/src/com/example/android/slidingfragments/SlidingFragments.java b/samples/devbytes/animation/SlidingFragments/src/com/example/android/slidingfragments/SlidingFragments.java
new file mode 100644
index 0000000..9bfd8d4
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/src/com/example/android/slidingfragments/SlidingFragments.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.slidingfragments;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.app.Activity;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.os.Bundle;
+import android.view.View;
+
+/**
+ * This application shows a simple technique to animate and overlay two fragments
+ * on top of each other in order to provide a more immersive experience,
+ * as opposed to only having full screen transitions. When additional content
+ * (text) related to the currently displayed content (image) is to be shown,
+ * the currently visible content can be moved into the background instead of
+ * being removed from the screen entirely. This effect can therefore
+ * provide a more natural way of displaying additional information to the user
+ * using a different fragment.
+ *
+ * In this specific demo, tapping on the screen toggles between the two
+ * animated states of the fragment. When the animation is called,
+ * the fragment with an image animates into the background while the fragment
+ * containing text slides up on top of it. When the animation is toggled once
+ * more, the text fragment slides back down and the image fragment regains
+ * focus.
+ */
+public class SlidingFragments extends Activity implements
+        OnTextFragmentAnimationEndListener, FragmentManager.OnBackStackChangedListener {
+
+    ImageFragment mImageFragment;
+    TextFragment mTextFragment;
+    View mDarkHoverView;
+
+    boolean mDidSlideOut = false;
+    boolean mIsAnimating = false;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.sliding_fragments_layout);
+
+        mDarkHoverView = findViewById(R.id.dark_hover_view);
+        mDarkHoverView.setAlpha(0);
+
+        mImageFragment = (ImageFragment) getFragmentManager().findFragmentById(R.id.move_fragment);
+        mTextFragment = new TextFragment();
+
+        getFragmentManager().addOnBackStackChangedListener(this);
+
+        mImageFragment.setClickListener(mClickListener);
+        mTextFragment.setClickListener(mClickListener);
+        mTextFragment.setOnTextFragmentAnimationEnd(this);
+        mDarkHoverView.setOnClickListener(mClickListener);
+
+    }
+
+    View.OnClickListener mClickListener = new View.OnClickListener () {
+        @Override
+        public void onClick(View view) {
+            switchFragments();
+        }
+    };
+
+    /**
+     * This method is used to toggle between the two fragment states by
+     * calling the appropriate animations between them. The entry and exit
+     * animations of the text fragment are specified in R.animator resource
+     * files. The entry and exit animations of the image fragment are
+     * specified in the slideBack and slideForward methods below. The reason
+     * for separating the animation logic in this way is because the translucent
+     * dark hover view must fade in at the same time as the image fragment
+     * animates into the background, which would be difficult to time
+     * properly given that the setCustomAnimations method can only modify the
+     * two fragments in the transaction.
+     */
+    private void switchFragments () {
+        if (mIsAnimating) {
+            return;
+        }
+        mIsAnimating = true;
+        if (mDidSlideOut) {
+            mDidSlideOut = false;
+            getFragmentManager().popBackStack();
+        } else {
+            mDidSlideOut = true;
+
+            AnimatorListener listener = new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator arg0) {
+                    FragmentTransaction transaction = getFragmentManager().beginTransaction();
+                    transaction.setCustomAnimations(R.animator.slide_fragment_in, 0, 0,
+                            R.animator.slide_fragment_out);
+                    transaction.add(R.id.move_to_back_container, mTextFragment);
+                    transaction.addToBackStack(null);
+                    transaction.commit();
+                }
+            };
+            slideBack (listener);
+        }
+    }
+
+    @Override
+    public void onBackStackChanged() {
+        if (!mDidSlideOut) {
+            slideForward(null);
+        }
+
+    }
+
+    /**
+     * This method animates the image fragment into the background by both
+     * scaling and rotating the fragment's view, as well as adding a
+     * translucent dark hover view to inform the user that it is inactive.
+     */
+    public void slideBack(AnimatorListener listener)
+    {
+        View movingFragmentView = mImageFragment.getView();
+
+        PropertyValuesHolder rotateX =  PropertyValuesHolder.ofFloat("rotationX", 40f);
+        PropertyValuesHolder scaleX =  PropertyValuesHolder.ofFloat("scaleX", 0.8f);
+        PropertyValuesHolder scaleY =  PropertyValuesHolder.ofFloat("scaleY", 0.8f);
+        ObjectAnimator movingFragmentAnimator = ObjectAnimator.
+                ofPropertyValuesHolder(movingFragmentView, rotateX, scaleX, scaleY);
+
+        ObjectAnimator darkHoverViewAnimator = ObjectAnimator.
+                ofFloat(mDarkHoverView, "alpha", 0.0f, 0.5f);
+
+        ObjectAnimator movingFragmentRotator = ObjectAnimator.
+                ofFloat(movingFragmentView, "rotationX", 0);
+        movingFragmentRotator.setStartDelay(getResources().
+                getInteger(R.integer.half_slide_up_down_duration));
+
+        AnimatorSet s = new AnimatorSet();
+        s.playTogether(movingFragmentAnimator, darkHoverViewAnimator, movingFragmentRotator);
+        s.addListener(listener);
+        s.start();
+    }
+
+    /**
+     * This method animates the image fragment into the foreground by both
+     * scaling and rotating the fragment's view, while also removing the
+     * previously added translucent dark hover view. Upon the completion of
+     * this animation, the image fragment regains focus since this method is
+     * called from the onBackStackChanged method.
+     */
+    public void slideForward(AnimatorListener listener)
+    {
+        View movingFragmentView = mImageFragment.getView();
+
+        PropertyValuesHolder rotateX =  PropertyValuesHolder.ofFloat("rotationX", 40f);
+        PropertyValuesHolder scaleX =  PropertyValuesHolder.ofFloat("scaleX", 1.0f);
+        PropertyValuesHolder scaleY =  PropertyValuesHolder.ofFloat("scaleY", 1.0f);
+        ObjectAnimator movingFragmentAnimator = ObjectAnimator.
+                ofPropertyValuesHolder(movingFragmentView, rotateX, scaleX, scaleY);
+
+        ObjectAnimator darkHoverViewAnimator = ObjectAnimator.
+                ofFloat(mDarkHoverView, "alpha", 0.5f, 0.0f);
+
+        ObjectAnimator movingFragmentRotator = ObjectAnimator.
+                ofFloat(movingFragmentView, "rotationX", 0);
+        movingFragmentRotator.setStartDelay(
+                getResources().getInteger(R.integer.half_slide_up_down_duration));
+
+        AnimatorSet s = new AnimatorSet();
+        s.playTogether(movingFragmentAnimator, movingFragmentRotator, darkHoverViewAnimator);
+        s.setStartDelay(getResources().getInteger(R.integer.slide_up_down_duration));
+        s.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mIsAnimating = false;
+            }
+        });
+        s.start();
+    }
+
+    public void onAnimationEnd() {
+        mIsAnimating = false;
+    }
+}
diff --git a/samples/devbytes/animation/SlidingFragments/src/com/example/android/slidingfragments/TextFragment.java b/samples/devbytes/animation/SlidingFragments/src/com/example/android/slidingfragments/TextFragment.java
new file mode 100644
index 0000000..f1f245c
--- /dev/null
+++ b/samples/devbytes/animation/SlidingFragments/src/com/example/android/slidingfragments/TextFragment.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.slidingfragments;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorListenerAdapter;
+import android.app.Fragment;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class TextFragment extends Fragment {
+
+    View.OnClickListener clickListener;
+    OnTextFragmentAnimationEndListener mListener;
+
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.text_fragment, container, false);
+        view.setOnClickListener(clickListener);
+        return view;
+    }
+
+    public void setClickListener(View.OnClickListener clickListener) {
+        this.clickListener = clickListener;
+    }
+
+    @Override
+    public Animator onCreateAnimator(int transit, boolean enter, int nextAnim)
+    {
+        int id = enter ? R.animator.slide_fragment_in : R.animator.slide_fragment_out;
+        final Animator anim = AnimatorInflater.loadAnimator(getActivity(), id);
+        if (enter) {
+            anim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mListener.onAnimationEnd();
+                }
+            });
+        }
+        return anim;
+    }
+
+    public void setOnTextFragmentAnimationEnd(OnTextFragmentAnimationEndListener listener)
+    {
+        mListener = listener;
+    }
+
+}
diff --git a/samples/devbytes/graphics/FoldingLayout/AndroidManifest.xml b/samples/devbytes/graphics/FoldingLayout/AndroidManifest.xml
new file mode 100644
index 0000000..13758d7
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.example.android.foldinglayout"
+          android:versionCode="1"
+          android:versionName="1.0">
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-feature android:name="android.hardware.camera" />
+    <uses-sdk android:minSdkVersion="17"
+              android:targetSdkVersion="18"/>
+    <application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
+        <activity android:name=".FoldingLayoutActivity"
+                  android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/samples/devbytes/graphics/FoldingLayout/res/drawable-hdpi/ic_launcher.png b/samples/devbytes/graphics/FoldingLayout/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..96a442e
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/graphics/FoldingLayout/res/drawable-hdpi/image.jpg b/samples/devbytes/graphics/FoldingLayout/res/drawable-hdpi/image.jpg
new file mode 100644
index 0000000..60ce9f2
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/drawable-hdpi/image.jpg
Binary files differ
diff --git a/samples/devbytes/graphics/FoldingLayout/res/drawable-ldpi/ic_launcher.png b/samples/devbytes/graphics/FoldingLayout/res/drawable-ldpi/ic_launcher.png
new file mode 100644
index 0000000..9923872
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/drawable-ldpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/graphics/FoldingLayout/res/drawable-mdpi/ic_launcher.png b/samples/devbytes/graphics/FoldingLayout/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..359047d
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/graphics/FoldingLayout/res/drawable-xhdpi/ic_launcher.png b/samples/devbytes/graphics/FoldingLayout/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..71c6d76
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/graphics/FoldingLayout/res/layout/activity_fold.xml b/samples/devbytes/graphics/FoldingLayout/res/layout/activity_fold.xml
new file mode 100644
index 0000000..9ed3bc5
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/layout/activity_fold.xml
@@ -0,0 +1,41 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <com.example.android.foldinglayout.FoldingLayout
+        android:layout_weight="1"
+        android:id="@+id/fold_view"
+        android:layout_width="match_parent"
+        android:layout_height="0dp">
+
+        <ImageView
+                android:id="@+id/image_view"
+                android:layout_height="match_parent"
+                android:layout_width="match_parent"
+                android:scaleType="fitXY"/>
+
+        </com.example.android.foldinglayout.FoldingLayout>
+
+    <SeekBar
+        android:id="@+id/anchor_seek_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:max="100"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/devbytes/graphics/FoldingLayout/res/layout/spinner.xml b/samples/devbytes/graphics/FoldingLayout/res/layout/spinner.xml
new file mode 100644
index 0000000..c35133d
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/layout/spinner.xml
@@ -0,0 +1,18 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<Spinner xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:entries="@array/num_of_folds_array" />
\ No newline at end of file
diff --git a/samples/devbytes/graphics/FoldingLayout/res/menu/fold.xml b/samples/devbytes/graphics/FoldingLayout/res/menu/fold.xml
new file mode 100644
index 0000000..a0231bd
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/menu/fold.xml
@@ -0,0 +1,41 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+    <item
+        android:id="@+id/toggle_orientation"
+        android:showAsAction="never"
+        android:title="@string/vertical"/>
+
+    <item
+        android:id="@+id/num_of_folds"
+        android:showAsAction="ifRoom"
+        android:actionLayout="@layout/spinner"/>
+
+    <item
+        android:title="@string/sepia_effect_off"
+        android:id="@+id/sepia"
+        android:checkable="true"/>
+
+    <item
+        android:title="@string/camera_feed"
+        android:id="@+id/camera_feed"/>
+
+    <item
+        android:id="@+id/animate_fold"
+        android:showAsAction="never"
+        android:title="@string/animate"/>
+
+</menu>
\ No newline at end of file
diff --git a/samples/devbytes/graphics/FoldingLayout/res/menu/fold_with_bug.xml b/samples/devbytes/graphics/FoldingLayout/res/menu/fold_with_bug.xml
new file mode 100644
index 0000000..44631ee
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/menu/fold_with_bug.xml
@@ -0,0 +1,32 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+    <item
+            android:id="@+id/toggle_orientation"
+            android:showAsAction="never"
+            android:title="@string/vertical"/>
+
+    <item
+            android:id="@+id/num_of_folds"
+            android:showAsAction="ifRoom"
+            android:actionLayout="@layout/spinner"/>
+
+    <item
+            android:id="@+id/animate_fold"
+            android:showAsAction="never"
+            android:title="@string/animate"/>
+
+</menu>
\ No newline at end of file
diff --git a/samples/devbytes/graphics/FoldingLayout/res/values/strings.xml b/samples/devbytes/graphics/FoldingLayout/res/values/strings.xml
new file mode 100644
index 0000000..181b15d
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/values/strings.xml
@@ -0,0 +1,42 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+
+    <string name="app_name">FoldingLayout</string>
+    <string name="title_activity_fold">FoldActivity</string>
+
+    <string name="horizontal">Horizontal</string>
+    <string name="vertical">Vertical</string>
+    <string name="num_of_folds">Number Of Folds</string>
+
+    <string name="animate">Animate</string>
+
+    <string name="camera_feed">Camera Feed</string>
+    <string name="static_image">Static Image</string>
+
+    <string name="sepia_effect_off">Sepia Off</string>
+
+    <string-array name="num_of_folds_array">
+        <item>2</item>
+        <item>3</item>
+        <item>4</item>
+        <item>5</item>
+        <item>6</item>
+        <item>7</item>
+        <item>8</item>
+        <item>1</item>
+    </string-array>
+
+</resources>
diff --git a/samples/devbytes/graphics/FoldingLayout/src/com/example/android/foldinglayout/FoldingLayout.java b/samples/devbytes/graphics/FoldingLayout/src/com/example/android/foldinglayout/FoldingLayout.java
new file mode 100644
index 0000000..8afb27e
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/src/com/example/android/foldinglayout/FoldingLayout.java
@@ -0,0 +1,545 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.foldinglayout;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Rect;
+import android.graphics.Shader.TileMode;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * The folding layout where the number of folds, the anchor point and the
+ * orientation of the fold can be specified. Each of these parameters can
+ * be modified individually and updates and resets the fold to a default
+ * (unfolded) state. The fold factor varies between 0 (completely unfolded
+ * flat image) to 1.0 (completely folded, non-visible image).
+ *
+ * This layout throws an exception if there is more than one child added to the view.
+ * For more complicated view hierarchy's inside the folding layout, the views should all
+ * be nested inside 1 parent layout.
+ *
+ * This layout folds the contents of its child in real time. By applying matrix
+ * transformations when drawing to canvas, the contents of the child may change as
+ * the fold takes place. It is important to note that there are jagged edges about
+ * the perimeter of the layout as a result of applying transformations to a rectangle.
+ * This can be avoided by having the child of this layout wrap its content inside a
+ * 1 pixel transparent border. This will cause an anti-aliasing like effect and smoothen
+ * out the edges.
+ *
+ */
+public class FoldingLayout extends ViewGroup {
+
+    public static enum Orientation {
+        VERTICAL,
+        HORIZONTAL
+    }
+
+    private final String FOLDING_VIEW_EXCEPTION_MESSAGE = "Folding Layout can only 1 child at " +
+            "most";
+
+    private final float SHADING_ALPHA = 0.8f;
+    private final float SHADING_FACTOR = 0.5f;
+    private final int DEPTH_CONSTANT = 1500;
+    private final int NUM_OF_POLY_POINTS = 8;
+
+    private Rect[] mFoldRectArray;
+
+    private Matrix [] mMatrix;
+
+    private Orientation mOrientation = Orientation.HORIZONTAL;
+
+    private float mAnchorFactor = 0;
+    private float mFoldFactor = 0;
+
+    private int mNumberOfFolds = 2;
+
+    private boolean mIsHorizontal = true;
+
+    private int mOriginalWidth = 0;
+    private int mOriginalHeight = 0;
+
+    private float mFoldMaxWidth = 0;
+    private float mFoldMaxHeight = 0;
+    private float mFoldDrawWidth = 0;
+    private float mFoldDrawHeight = 0;
+
+    private boolean mIsFoldPrepared = false;
+    private boolean mShouldDraw = true;
+
+    private Paint mSolidShadow;
+    private Paint mGradientShadow;
+    private LinearGradient mShadowLinearGradient;
+    private Matrix mShadowGradientMatrix;
+
+    private float [] mSrc;
+    private float [] mDst;
+
+    private OnFoldListener mFoldListener;
+
+    private float mPreviousFoldFactor = 0;
+
+    private Bitmap mFullBitmap;
+    private Rect mDstRect;
+
+    public FoldingLayout(Context context) {
+        super(context);
+    }
+
+    public FoldingLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public FoldingLayout(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected boolean addViewInLayout(View child, int index, LayoutParams params,
+                                      boolean preventRequestLayout) {
+        throwCustomException(getChildCount());
+        boolean returnValue = super.addViewInLayout(child, index, params, preventRequestLayout);
+        return returnValue;
+    }
+
+    @Override
+    public void addView(View child, int index, LayoutParams params) {
+        throwCustomException(getChildCount());
+        super.addView(child, index, params);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        View child = getChildAt(0);
+        measureChild(child,widthMeasureSpec, heightMeasureSpec);
+        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        View child = getChildAt(0);
+        child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
+        updateFold();
+    }
+
+    /**
+     * The custom exception to be thrown so as to limit the number of views in this
+     * layout to at most one.
+     */
+    private class NumberOfFoldingLayoutChildrenException extends RuntimeException {
+        public NumberOfFoldingLayoutChildrenException(String message) {
+            super(message);
+        }
+    }
+
+    /** Throws an exception if the number of views added to this layout exceeds one.*/
+    private void throwCustomException (int numOfChildViews) {
+        if (numOfChildViews == 1) {
+            throw new NumberOfFoldingLayoutChildrenException(FOLDING_VIEW_EXCEPTION_MESSAGE);
+        }
+    }
+
+    public void setFoldListener(OnFoldListener foldListener) {
+        mFoldListener = foldListener;
+    }
+
+    /**
+     * Sets the fold factor of the folding view and updates all the corresponding
+     * matrices and values to account for the new fold factor. Once that is complete,
+     * it redraws itself with the new fold. */
+    public void setFoldFactor(float foldFactor) {
+        if (foldFactor != mFoldFactor) {
+            mFoldFactor = foldFactor;
+            calculateMatrices();
+            invalidate();
+        }
+    }
+
+    public void setOrientation(Orientation orientation) {
+        if (orientation != mOrientation) {
+            mOrientation = orientation;
+            updateFold();
+        }
+    }
+
+    public void setAnchorFactor(float anchorFactor) {
+        if (anchorFactor != mAnchorFactor) {
+            mAnchorFactor = anchorFactor;
+            updateFold();
+        }
+    }
+
+    public void setNumberOfFolds(int numberOfFolds) {
+        if (numberOfFolds != mNumberOfFolds) {
+            mNumberOfFolds = numberOfFolds;
+            updateFold();
+        }
+    }
+
+    public float getAnchorFactor() {
+        return mAnchorFactor;
+    }
+
+    public Orientation getOrientation() {
+        return mOrientation;
+    }
+
+    public float getFoldFactor() {
+        return mFoldFactor;
+    }
+
+    public int getNumberOfFolds() {
+        return mNumberOfFolds;
+    }
+
+    private void updateFold() {
+        prepareFold(mOrientation, mAnchorFactor, mNumberOfFolds);
+        calculateMatrices();
+        invalidate();
+    }
+
+    /**
+     * This method is called in order to update the fold's orientation, anchor
+     * point and number of folds. This creates the necessary setup in order to
+     * prepare the layout for a fold with the specified parameters. Some of the
+     * dimensions required for the folding transformation are also acquired here.
+     *
+     * After this method is called, it will be in a completely unfolded state by default.
+     */
+    private void prepareFold(Orientation orientation, float anchorFactor, int numberOfFolds) {
+
+        mSrc = new float[NUM_OF_POLY_POINTS];
+        mDst = new float[NUM_OF_POLY_POINTS];
+
+        mDstRect = new Rect();
+
+        mFoldFactor = 0;
+        mPreviousFoldFactor = 0;
+
+        mIsFoldPrepared = false;
+
+        mSolidShadow = new Paint();
+        mGradientShadow = new Paint();
+
+        mOrientation = orientation;
+        mIsHorizontal = (orientation == Orientation.HORIZONTAL);
+
+        if (mIsHorizontal) {
+            mShadowLinearGradient = new LinearGradient(0, 0, SHADING_FACTOR, 0, Color.BLACK,
+                    Color.TRANSPARENT, TileMode.CLAMP);
+        } else {
+            mShadowLinearGradient = new LinearGradient(0, 0, 0, SHADING_FACTOR, Color.BLACK,
+                    Color.TRANSPARENT, TileMode.CLAMP);
+        }
+
+        mGradientShadow.setStyle(Style.FILL);
+        mGradientShadow.setShader(mShadowLinearGradient);
+        mShadowGradientMatrix = new Matrix();
+
+        mAnchorFactor = anchorFactor;
+        mNumberOfFolds = numberOfFolds;
+
+        mOriginalWidth = getMeasuredWidth();
+        mOriginalHeight = getMeasuredHeight();
+
+        mFoldRectArray = new Rect[mNumberOfFolds];
+        mMatrix = new Matrix [mNumberOfFolds];
+
+        for (int x = 0; x < mNumberOfFolds; x++) {
+            mMatrix[x] = new Matrix();
+        }
+
+        int h = mOriginalHeight;
+        int w = mOriginalWidth;
+
+        if (FoldingLayoutActivity.IS_JBMR2) {
+            mFullBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+            Canvas canvas = new Canvas(mFullBitmap);
+            getChildAt(0).draw(canvas);
+        }
+
+        int delta = Math.round(mIsHorizontal ? ((float) w) / ((float) mNumberOfFolds) :
+                ((float) h) /((float) mNumberOfFolds));
+
+        /* Loops through the number of folds and segments the full layout into a number
+         * of smaller equal components. If the number of folds is odd, then one of the
+         * components will be smaller than all the rest. Note that deltap below handles
+         * the calculation for an odd number of folds.*/
+        for (int x = 0; x < mNumberOfFolds; x++) {
+            if (mIsHorizontal) {
+                int deltap = (x + 1) * delta > w ? w - x * delta : delta;
+                mFoldRectArray[x] = new Rect(x * delta, 0, x * delta + deltap, h);
+            } else {
+                int deltap = (x + 1) * delta > h ? h - x * delta : delta;
+                mFoldRectArray[x] = new Rect(0, x * delta, w, x * delta + deltap);
+            }
+        }
+
+        if (mIsHorizontal) {
+            mFoldMaxHeight = h;
+            mFoldMaxWidth = delta;
+        } else {
+            mFoldMaxHeight = delta;
+            mFoldMaxWidth = w;
+        }
+
+        mIsFoldPrepared = true;
+    }
+
+    /*
+    * Calculates the transformation matrices used to draw each of the separate folding
+    * segments from this view.
+    */
+    private void calculateMatrices() {
+
+        mShouldDraw = true;
+
+        if (!mIsFoldPrepared) {
+            return;
+        }
+
+        /** If the fold factor is 1 than the folding view should not be seen
+         * and the canvas can be left completely empty. */
+        if (mFoldFactor == 1) {
+            mShouldDraw = false;
+            return;
+        }
+
+        if (mFoldFactor == 0 &&  mPreviousFoldFactor > 0) {
+            mFoldListener.onEndFold();
+        }
+
+        if (mPreviousFoldFactor == 0 && mFoldFactor > 0) {
+            mFoldListener.onStartFold();
+        }
+
+        mPreviousFoldFactor = mFoldFactor;
+
+        /* Reset all the transformation matrices back to identity before computing
+         * the new transformation */
+        for (int x = 0; x < mNumberOfFolds; x++) {
+            mMatrix[x].reset();
+        }
+
+        float cTranslationFactor = 1 - mFoldFactor;
+
+        float translatedDistance = mIsHorizontal ? mOriginalWidth * cTranslationFactor :
+                mOriginalHeight * cTranslationFactor;
+
+        float translatedDistancePerFold = Math.round(translatedDistance / mNumberOfFolds);
+
+        /* For an odd number of folds, the rounding error may cause the
+         * translatedDistancePerFold to be grater than the max fold width or height. */
+        mFoldDrawWidth = mFoldMaxWidth < translatedDistancePerFold ?
+                translatedDistancePerFold : mFoldMaxWidth;
+        mFoldDrawHeight = mFoldMaxHeight < translatedDistancePerFold ?
+                translatedDistancePerFold : mFoldMaxHeight;
+
+        float translatedDistanceFoldSquared = translatedDistancePerFold * translatedDistancePerFold;
+
+        /* Calculate the depth of the fold into the screen using pythagorean theorem. */
+        float depth = mIsHorizontal ?
+                (float)Math.sqrt((double)(mFoldDrawWidth * mFoldDrawWidth -
+                        translatedDistanceFoldSquared)) :
+                (float)Math.sqrt((double)(mFoldDrawHeight * mFoldDrawHeight -
+                        translatedDistanceFoldSquared));
+
+        /* The size of some object is always inversely proportional to the distance
+        *  it is away from the viewpoint. The constant can be varied to to affect the
+        *  amount of perspective. */
+        float scaleFactor = DEPTH_CONSTANT / (DEPTH_CONSTANT + depth);
+
+        float scaledWidth, scaledHeight, bottomScaledPoint, topScaledPoint, rightScaledPoint,
+                leftScaledPoint;
+
+        if (mIsHorizontal) {
+            scaledWidth = mFoldDrawWidth * cTranslationFactor;
+            scaledHeight = mFoldDrawHeight * scaleFactor;
+        } else {
+            scaledWidth = mFoldDrawWidth * scaleFactor;
+            scaledHeight = mFoldDrawHeight * cTranslationFactor;
+        }
+
+        topScaledPoint = (mFoldDrawHeight - scaledHeight) / 2.0f;
+        bottomScaledPoint = topScaledPoint + scaledHeight;
+
+        leftScaledPoint = (mFoldDrawWidth - scaledWidth) / 2.0f;
+        rightScaledPoint = leftScaledPoint + scaledWidth;
+
+        float anchorPoint = mIsHorizontal ? mAnchorFactor * mOriginalWidth :
+                mAnchorFactor * mOriginalHeight;
+
+        /* The fold along which the anchor point is located. */
+        float midFold = mIsHorizontal ? (anchorPoint / mFoldDrawWidth) : anchorPoint /
+                mFoldDrawHeight;
+
+        mSrc[0] = 0;
+        mSrc[1] = 0;
+        mSrc[2] = 0;
+        mSrc[3] = mFoldDrawHeight;
+        mSrc[4] = mFoldDrawWidth;
+        mSrc[5] = 0;
+        mSrc[6] = mFoldDrawWidth;
+        mSrc[7] = mFoldDrawHeight;
+
+        /* Computes the transformation matrix for each fold using the values calculated above. */
+        for (int x = 0; x < mNumberOfFolds; x++) {
+
+            boolean isEven = (x % 2 == 0);
+
+            if (mIsHorizontal) {
+                mDst[0] = (anchorPoint > x * mFoldDrawWidth) ? anchorPoint + (x - midFold) *
+                        scaledWidth : anchorPoint - (midFold - x) * scaledWidth;
+                mDst[1] = isEven ? 0 : topScaledPoint;
+                mDst[2] = mDst[0];
+                mDst[3] = isEven ? mFoldDrawHeight: bottomScaledPoint;
+                mDst[4] = (anchorPoint > (x + 1) * mFoldDrawWidth) ? anchorPoint + (x + 1 - midFold)
+                        * scaledWidth : anchorPoint - (midFold - x - 1) * scaledWidth;
+                mDst[5] = isEven ? topScaledPoint : 0;
+                mDst[6] = mDst[4];
+                mDst[7] = isEven ? bottomScaledPoint : mFoldDrawHeight;
+
+            } else {
+                mDst[0] = isEven ? 0 : leftScaledPoint;
+                mDst[1] = (anchorPoint > x * mFoldDrawHeight) ? anchorPoint + (x - midFold) *
+                        scaledHeight : anchorPoint - (midFold - x) * scaledHeight;
+                mDst[2] = isEven ? leftScaledPoint: 0;
+                mDst[3] = (anchorPoint > (x + 1) * mFoldDrawHeight) ? anchorPoint + (x + 1 -
+                        midFold) * scaledHeight : anchorPoint - (midFold - x - 1) * scaledHeight;
+                mDst[4] = isEven ? mFoldDrawWidth : rightScaledPoint;
+                mDst[5] = mDst[1];
+                mDst[6] = isEven ? rightScaledPoint : mFoldDrawWidth;
+                mDst[7] = mDst[3];
+            }
+
+            /* Pixel fractions are present for odd number of folds which need to be
+             * rounded off here.*/
+            for (int y = 0; y < 8; y ++) {
+                mDst[y] = Math.round(mDst[y]);
+            }
+
+            /* If it so happens that any of the folds have reached a point where
+            *  the width or height of that fold is 0, then nothing needs to be
+            *  drawn onto the canvas because the view is essentially completely
+            *  folded.*/
+            if (mIsHorizontal) {
+                if (mDst[4] <= mDst[0] || mDst[6] <= mDst[2]) {
+                    mShouldDraw = false;
+                    return;
+                }
+            } else {
+                if (mDst[3] <= mDst[1] || mDst[7] <= mDst[5]) {
+                    mShouldDraw = false;
+                    return;
+                }
+            }
+
+            /* Sets the shadow and bitmap transformation matrices.*/
+            mMatrix[x].setPolyToPoly(mSrc, 0, mDst, 0, NUM_OF_POLY_POINTS / 2);
+        }
+        /* The shadows on the folds are split into two parts: Solid shadows and gradients.
+         * Every other fold has a solid shadow which overlays the whole fold. Similarly,
+         * the folds in between these alternating folds also have an overlaying shadow.
+         * However, it is a gradient that takes up part of the fold as opposed to a solid
+         * shadow overlaying the whole fold.*/
+
+        /* Solid shadow paint object. */
+        int alpha = (int) (mFoldFactor * 255 * SHADING_ALPHA);
+
+        mSolidShadow.setColor(Color.argb(alpha, 0, 0, 0));
+
+        if (mIsHorizontal) {
+            mShadowGradientMatrix.setScale(mFoldDrawWidth, 1);
+            mShadowLinearGradient.setLocalMatrix(mShadowGradientMatrix);
+        } else {
+            mShadowGradientMatrix.setScale(1, mFoldDrawHeight);
+            mShadowLinearGradient.setLocalMatrix(mShadowGradientMatrix);
+        }
+
+        mGradientShadow.setAlpha(alpha);
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        /** If prepareFold has not been called or if preparation has not completed yet,
+         * then no custom drawing will take place so only need to invoke super's
+         * onDraw and return. */
+        if (!mIsFoldPrepared || mFoldFactor == 0) {
+            super.dispatchDraw(canvas);
+            return;
+        }
+
+        if (!mShouldDraw) {
+            return;
+        }
+
+        Rect src;
+         /* Draws the bitmaps and shadows on the canvas with the appropriate transformations. */
+        for (int x = 0; x < mNumberOfFolds; x++) {
+
+            src = mFoldRectArray[x];
+            /* The canvas is saved and restored for every individual fold*/
+            canvas.save();
+
+            /* Concatenates the canvas with the transformation matrix for the
+             *  the segment of the view corresponding to the actual image being
+             *  displayed. */
+            canvas.concat(mMatrix[x]);
+            if (FoldingLayoutActivity.IS_JBMR2) {
+                mDstRect.set(0, 0, src.width(), src.height());
+                canvas.drawBitmap(mFullBitmap, src, mDstRect, null);
+            } else {
+                /* The same transformation matrix is used for both the shadow and the image
+                 * segment. The canvas is clipped to account for the size of each fold and
+                 * is translated so they are drawn in the right place. The shadow is then drawn on
+                 * top of the different folds using the sametransformation matrix.*/
+                canvas.clipRect(0, 0, src.right - src.left, src.bottom - src.top);
+
+                if (mIsHorizontal) {
+                    canvas.translate(-src.left, 0);
+                } else {
+                    canvas.translate(0, -src.top);
+                }
+
+                super.dispatchDraw(canvas);
+
+                if (mIsHorizontal) {
+                    canvas.translate(src.left, 0);
+                } else {
+                    canvas.translate(0, src.top);
+                }
+            }
+            /* Draws the shadows corresponding to this specific fold. */
+            if (x % 2 == 0) {
+                canvas.drawRect(0, 0, mFoldDrawWidth, mFoldDrawHeight, mSolidShadow);
+            } else {
+                canvas.drawRect(0, 0, mFoldDrawWidth, mFoldDrawHeight, mGradientShadow);
+            }
+
+            canvas.restore();
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/samples/devbytes/graphics/FoldingLayout/src/com/example/android/foldinglayout/FoldingLayoutActivity.java b/samples/devbytes/graphics/FoldingLayout/src/com/example/android/foldinglayout/FoldingLayoutActivity.java
new file mode 100644
index 0000000..1a1033d
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/src/com/example/android/foldinglayout/FoldingLayoutActivity.java
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.foldinglayout;
+
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.app.Activity;
+import android.graphics.Color;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.GestureDetector;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ImageView;
+import android.widget.SeekBar;
+import android.widget.Spinner;
+
+import com.example.android.foldinglayout.FoldingLayout.Orientation;
+
+import java.io.IOException;
+
+/**
+ * This application creates  a paper like folding effect of some view.
+ * The number of folds, orientation (vertical or horizontal) of the fold, and the
+ * anchor point about which the view will fold can be set to achieve different
+ * folding effects.
+ *
+ * Using bitmap and canvas scaling techniques, the foldingLayout can be scaled so as
+ * to depict a paper-like folding effect. The addition of shadows on the separate folds
+ * adds a sense of realism to the visual effect.
+ *
+ * This application shows folding of a TextureView containing a live camera feed,
+ * as well as the folding of an ImageView with a static image. The TextureView experiences
+ * jagged edges as a result of scaling operations on rectangles. The ImageView however
+ * contains a 1 pixel transparent border around its contents which can be used to avoid
+ * this unwanted artifact.
+ */
+public class FoldingLayoutActivity extends Activity {
+
+    private final int ANTIALIAS_PADDING = 1;
+
+    private final int FOLD_ANIMATION_DURATION = 1000;
+
+    /* A bug was introduced in Android 4.3 that ignores changes to the Canvas state
+     * between multiple calls to super.dispatchDraw() when running with hardware acceleration.
+     * To account for this bug, a slightly different approach was taken to fold a
+     * static image whereby a bitmap of the original contents is captured and drawn
+     * in segments onto the canvas. However, this method does not permit the folding
+     * of a TextureView hosting a live camera feed which continuously updates.
+     * Furthermore, the sepia effect was removed from the bitmap variation of the
+     * demo to simplify the logic when running with this workaround."
+     */
+    static final boolean IS_JBMR2 = Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR2;
+
+    private FoldingLayout mFoldLayout;
+    private SeekBar mAnchorSeekBar;
+    private Orientation mOrientation = Orientation.HORIZONTAL;
+
+    private int mTranslation = 0;
+    private int mNumberOfFolds = 2;
+    private int mParentPositionY = -1;
+    private int mTouchSlop = -1;
+
+    private float mAnchorFactor = 0;
+
+    private boolean mDidLoadSpinner = true;
+    private boolean mDidNotStartScroll = true;
+
+    private boolean mIsCameraFeed = false;
+    private boolean mIsSepiaOn = true;
+
+    private GestureDetector mScrollGestureDetector;
+    private ItemSelectedListener mItemSelectedListener;
+
+    private Camera mCamera;
+    private TextureView mTextureView;
+    private ImageView mImageView;
+
+    private Paint mSepiaPaint;
+    private Paint mDefaultPaint;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.activity_fold);
+
+        mImageView = (ImageView)findViewById(R.id.image_view);
+        mImageView.setPadding(ANTIALIAS_PADDING, ANTIALIAS_PADDING, ANTIALIAS_PADDING,
+                ANTIALIAS_PADDING);
+        mImageView.setScaleType(ImageView.ScaleType.FIT_XY);
+        mImageView.setImageDrawable(getResources().getDrawable(R.drawable.image));
+
+        mTextureView = new TextureView(this);
+        mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
+
+        mAnchorSeekBar = (SeekBar)findViewById(R.id.anchor_seek_bar);
+        mFoldLayout = (FoldingLayout)findViewById(R.id.fold_view);
+        mFoldLayout.setBackgroundColor(Color.BLACK);
+        mFoldLayout.setFoldListener(mOnFoldListener);
+
+        mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
+
+        mAnchorSeekBar.setOnSeekBarChangeListener(mSeekBarChangeListener);
+
+        mScrollGestureDetector = new GestureDetector(this, new ScrollGestureDetector());
+        mItemSelectedListener = new ItemSelectedListener();
+
+        mDefaultPaint = new Paint();
+        mSepiaPaint = new Paint();
+
+        ColorMatrix m1 = new ColorMatrix();
+        ColorMatrix m2 = new ColorMatrix();
+        m1.setSaturation(0);
+        m2.setScale(1f, .95f, .82f, 1.0f);
+        m1.setConcat(m2, m1);
+        mSepiaPaint.setColorFilter(new ColorMatrixColorFilter(m1));
+    }
+
+    /**
+     * This listener, along with the setSepiaLayer method below, show a possible use case
+     * of the OnFoldListener provided with the FoldingLayout. This is a fun extra addition
+     * to the demo showing what kind of visual effects can be applied to the child of the
+     * FoldingLayout by setting the layer type to hardware. With a hardware layer type
+     * applied to the child, a paint object can also be applied to the same layer. Using
+     * the concatenation of two different color matrices (above), a color filter was created
+     * which simulates a sepia effect on the layer.*/
+    private OnFoldListener mOnFoldListener =
+            new OnFoldListener() {
+        @Override
+        public void onStartFold() {
+            if (mIsSepiaOn) {
+                setSepiaLayer(mFoldLayout.getChildAt(0), true);
+            }
+        }
+
+        @Override
+        public void onEndFold() {
+            setSepiaLayer(mFoldLayout.getChildAt(0), false);
+        }
+    };
+
+    private void setSepiaLayer (View view, boolean isSepiaLayerOn) {
+        if (!IS_JBMR2) {
+            if (isSepiaLayerOn) {
+                view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+                view.setLayerPaint(mSepiaPaint);
+            } else {
+                view.setLayerPaint(mDefaultPaint);
+            }
+        }
+    }
+
+    /**
+     * Creates a SurfaceTextureListener in order to prepare a TextureView
+     * which displays a live, and continuously updated, feed from the Camera.
+     */
+    private TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView
+            .SurfaceTextureListener() {
+        @Override
+        public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i2) {
+            mCamera = Camera.open();
+
+            if (mCamera == null && Camera.getNumberOfCameras() > 1) {
+                mCamera = mCamera.open(Camera.CameraInfo.CAMERA_FACING_FRONT);
+            }
+
+            if (mCamera == null) {
+                return;
+            }
+
+            try {
+                mCamera.setPreviewTexture(surfaceTexture);
+                mCamera.setDisplayOrientation(90);
+                mCamera.startPreview();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+
+        @Override
+        public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i2) {
+            // Ignored, Camera does all the work for us
+        }
+
+        @Override
+        public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+            if (mCamera != null) {
+                mCamera.stopPreview();
+                mCamera.release();
+            }
+            return true;
+        }
+
+        @Override
+        public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
+            // Invoked every time there's a new Camera preview frame
+        }
+    };
+
+    /**
+     * A listener for scrolling changes in the seekbar. The anchor point of the folding
+     * view is updated every time the seekbar stops tracking touch events. Every time the
+     * anchor point is updated, the folding view is restored to a default unfolded state.
+     */
+    private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener = new SeekBar
+            .OnSeekBarChangeListener() {
+        @Override
+        public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
+        }
+
+        @Override
+        public void onStartTrackingTouch(SeekBar seekBar) {
+        }
+
+        @Override
+        public void onStopTrackingTouch(SeekBar seekBar) {
+            mTranslation = 0;
+            mAnchorFactor = ((float)mAnchorSeekBar.getProgress())/100.0f;
+            mFoldLayout.setAnchorFactor(mAnchorFactor);
+        }
+    };
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        if (IS_JBMR2) {
+            getMenuInflater().inflate(R.menu.fold_with_bug, menu);
+        } else {
+            getMenuInflater().inflate(R.menu.fold, menu);
+        }
+        Spinner s = (Spinner) menu.findItem(R.id.num_of_folds).getActionView();
+        s.setOnItemSelectedListener(mItemSelectedListener);
+        return true;
+    }
+
+    @Override
+    public void onWindowFocusChanged (boolean hasFocus) {
+        super.onWindowFocusChanged(hasFocus);
+
+        int[] loc = new int[2];
+        mFoldLayout.getLocationOnScreen(loc);
+        mParentPositionY = loc[1];
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent me) {
+        return mScrollGestureDetector.onTouchEvent(me);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected (MenuItem item) {
+        switch(item.getItemId()) {
+            case R.id.animate_fold:
+                animateFold();
+                break;
+            case R.id.toggle_orientation:
+                mOrientation = (mOrientation == Orientation.HORIZONTAL) ? Orientation.VERTICAL :
+                        Orientation.HORIZONTAL;
+                item.setTitle((mOrientation == Orientation.HORIZONTAL) ? R.string.vertical :
+                R.string.horizontal);
+                mTranslation = 0;
+                mFoldLayout.setOrientation(mOrientation);
+                break;
+            case R.id.camera_feed:
+                mIsCameraFeed = !mIsCameraFeed;
+                item.setTitle(mIsCameraFeed ? R.string.static_image : R.string.camera_feed);
+                item.setChecked(mIsCameraFeed);
+                if (mIsCameraFeed) {
+                    mFoldLayout.removeView(mImageView);
+                    mFoldLayout.addView(mTextureView, new ViewGroup.LayoutParams(
+                            mFoldLayout.getWidth(), mFoldLayout.getHeight()));
+                } else {
+                    mFoldLayout.removeView(mTextureView);
+                    mFoldLayout.addView(mImageView, new ViewGroup.LayoutParams(
+                            mFoldLayout.getWidth(), mFoldLayout.getHeight()));
+                }
+                mTranslation = 0;
+                break;
+            case R.id.sepia:
+                mIsSepiaOn = !mIsSepiaOn;
+                item.setChecked(!mIsSepiaOn);
+                if (mIsSepiaOn && mFoldLayout.getFoldFactor() != 0) {
+                    setSepiaLayer(mFoldLayout.getChildAt(0), true);
+                } else {
+                    setSepiaLayer(mFoldLayout.getChildAt(0), false);
+                }
+                break;
+            default:
+               break;
+
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    /**
+     * Animates the folding view inwards (to a completely folded state) from its
+     * current state and then back out to its original state.
+     */
+    public void animateFold ()
+    {
+        float foldFactor = mFoldLayout.getFoldFactor();
+
+        ObjectAnimator animator = ObjectAnimator.ofFloat(mFoldLayout, "foldFactor", foldFactor, 1);
+        animator.setRepeatMode(ValueAnimator.REVERSE);
+        animator.setRepeatCount(1);
+        animator.setDuration(FOLD_ANIMATION_DURATION);
+        animator.setInterpolator(new AccelerateInterpolator());
+        animator.start();
+    }
+
+    /**
+     * Listens for selection events of the spinner located on the action bar. Every
+     * time a new value is selected, the number of folds in the folding view is updated
+     * and is also restored to a default unfolded state.
+     */
+    private class ItemSelectedListener implements OnItemSelectedListener {
+        @Override
+        public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
+            mNumberOfFolds = Integer.parseInt(parent.getItemAtPosition(pos).toString());
+            if (mDidLoadSpinner) {
+                mDidLoadSpinner = false;
+            } else {
+                mTranslation = 0;
+                mFoldLayout.setNumberOfFolds(mNumberOfFolds);
+            }
+        }
+
+        @Override
+        public void onNothingSelected(AdapterView<?> arg0) {
+        }
+    }
+
+    /** This class uses user touch events to fold and unfold the folding view. */
+    private class ScrollGestureDetector extends GestureDetector.SimpleOnGestureListener {
+        @Override
+        public boolean onDown (MotionEvent e) {
+            mDidNotStartScroll = true;
+            return true;
+        }
+
+        /**
+         * All the logic here is used to determine by what factor the paper view should
+         * be folded in response to the user's touch events. The logic here uses vertical
+         * scrolling to fold a vertically oriented view and horizontal scrolling to fold
+         * a horizontally oriented fold. Depending on where the anchor point of the fold is,
+         * movements towards or away from the anchor point will either fold or unfold
+         * the paper respectively.
+         *
+         * The translation logic here also accounts for the touch slop when a new user touch
+         * begins, but before a scroll event is first invoked.
+         */
+        @Override
+        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+            int touchSlop = 0;
+            float factor;
+            if (mOrientation == Orientation.VERTICAL) {
+                factor = Math.abs((float)(mTranslation) / (float)(mFoldLayout.getHeight()));
+
+                if (e2.getY() - mParentPositionY <= mFoldLayout.getHeight()
+                        && e2.getY() - mParentPositionY >= 0) {
+                    if ((e2.getY() - mParentPositionY) > mFoldLayout.getHeight() * mAnchorFactor) {
+                        mTranslation -= (int)distanceY;
+                        touchSlop = distanceY < 0 ? -mTouchSlop : mTouchSlop;
+                    } else {
+                        mTranslation += (int)distanceY;
+                        touchSlop = distanceY < 0 ? mTouchSlop : -mTouchSlop;
+                    }
+                    mTranslation = mDidNotStartScroll ? mTranslation + touchSlop : mTranslation;
+
+                    if (mTranslation < -mFoldLayout.getHeight()) {
+                        mTranslation = -mFoldLayout.getHeight();
+                    }
+                }
+            } else {
+                factor = Math.abs(((float)mTranslation) / ((float) mFoldLayout.getWidth()));
+
+                if (e2.getRawX() > mFoldLayout.getWidth() * mAnchorFactor) {
+                    mTranslation -= (int)distanceX;
+                    touchSlop = distanceX < 0 ? -mTouchSlop : mTouchSlop;
+                } else {
+                    mTranslation += (int)distanceX;
+                    touchSlop = distanceX < 0 ? mTouchSlop : -mTouchSlop;
+                }
+                mTranslation = mDidNotStartScroll ? mTranslation + touchSlop : mTranslation;
+
+                if (mTranslation < -mFoldLayout.getWidth()) {
+                    mTranslation = -mFoldLayout.getWidth();
+                }
+            }
+
+            mDidNotStartScroll = false;
+
+            if (mTranslation > 0) {
+                mTranslation = 0;
+            }
+
+            mFoldLayout.setFoldFactor(factor);
+
+            return true;
+        }
+    }
+}
\ No newline at end of file
diff --git a/samples/devbytes/graphics/FoldingLayout/src/com/example/android/foldinglayout/OnFoldListener.java b/samples/devbytes/graphics/FoldingLayout/src/com/example/android/foldinglayout/OnFoldListener.java
new file mode 100644
index 0000000..a305568
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/src/com/example/android/foldinglayout/OnFoldListener.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.foldinglayout;
+
+/**
+ * This interface listens for when the folding layout begins folding (enters
+ * a folded state from a completely unfolded state), or ends folding (enters a
+ * completely unfolded state from a folded state).
+ */
+public interface OnFoldListener {
+    public void onStartFold();
+    public void onEndFold();
+}
diff --git a/samples/devbytes/graphics/ImagePixelization/AndroidManifest.xml b/samples/devbytes/graphics/ImagePixelization/AndroidManifest.xml
new file mode 100644
index 0000000..7bbb4f6
--- /dev/null
+++ b/samples/devbytes/graphics/ImagePixelization/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.example.android.imagepixelization"
+          android:versionCode="1"
+          android:versionName="1.0">
+    <uses-sdk android:minSdkVersion="11"
+              android:targetSdkVersion="17"/>
+    <application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
+        <activity android:name=".ImagePixelization"
+                  android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/samples/devbytes/graphics/ImagePixelization/res/drawable-hdpi/ic_launcher.png b/samples/devbytes/graphics/ImagePixelization/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..96a442e
--- /dev/null
+++ b/samples/devbytes/graphics/ImagePixelization/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/graphics/ImagePixelization/res/drawable-hdpi/image.jpg b/samples/devbytes/graphics/ImagePixelization/res/drawable-hdpi/image.jpg
new file mode 100644
index 0000000..00bdbba
--- /dev/null
+++ b/samples/devbytes/graphics/ImagePixelization/res/drawable-hdpi/image.jpg
Binary files differ
diff --git a/samples/devbytes/graphics/ImagePixelization/res/drawable-ldpi/ic_launcher.png b/samples/devbytes/graphics/ImagePixelization/res/drawable-ldpi/ic_launcher.png
new file mode 100644
index 0000000..9923872
--- /dev/null
+++ b/samples/devbytes/graphics/ImagePixelization/res/drawable-ldpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/graphics/ImagePixelization/res/drawable-mdpi/ic_launcher.png b/samples/devbytes/graphics/ImagePixelization/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..359047d
--- /dev/null
+++ b/samples/devbytes/graphics/ImagePixelization/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/graphics/ImagePixelization/res/drawable-xhdpi/ic_launcher.png b/samples/devbytes/graphics/ImagePixelization/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..71c6d76
--- /dev/null
+++ b/samples/devbytes/graphics/ImagePixelization/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/devbytes/graphics/ImagePixelization/res/layout/activity_image_pixelization.xml b/samples/devbytes/graphics/ImagePixelization/res/layout/activity_image_pixelization.xml
new file mode 100644
index 0000000..b3e30a8
--- /dev/null
+++ b/samples/devbytes/graphics/ImagePixelization/res/layout/activity_image_pixelization.xml
@@ -0,0 +1,42 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:tools="http://schemas.android.com/tools"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:paddingLeft="@dimen/activity_horizontal_margin"
+                android:paddingRight="@dimen/activity_horizontal_margin"
+                android:paddingTop="@dimen/activity_vertical_margin"
+                android:orientation="vertical">
+
+        <ImageView
+                android:id="@+id/pixelView"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_alignParentTop="true"
+                android:layout_above="@+id/seekbar"
+                android:scaleType="fitXY"/>
+
+        <SeekBar
+                android:id="@+id/seekbar"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_margin="10dp"
+                android:max="@integer/seek_max"
+                android:progress="0"
+                android:layout_alignParentBottom = "true"/>
+
+</RelativeLayout>
+
diff --git a/samples/devbytes/graphics/ImagePixelization/res/menu/image_pixelization.xml b/samples/devbytes/graphics/ImagePixelization/res/menu/image_pixelization.xml
new file mode 100644
index 0000000..e6a8d66
--- /dev/null
+++ b/samples/devbytes/graphics/ImagePixelization/res/menu/image_pixelization.xml
@@ -0,0 +1,31 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+    <item   android:id="@+id/builtin_pixelation_checkbox"
+            android:showAsAction="never"
+            android:title="@string/builtin_pixelization"
+            android:checkable="true" />
+
+    <item   android:id="@+id/checkbox"
+            android:showAsAction="never"
+            android:title="@string/async_task"
+            android:checkable="true" />
+
+    <item   android:id="@+id/animate"
+            android:showAsAction="never"
+            android:title="@string/animate_pixelation"/>
+
+</menu>
\ No newline at end of file
diff --git a/samples/devbytes/graphics/ImagePixelization/res/values/dimens.xml b/samples/devbytes/graphics/ImagePixelization/res/values/dimens.xml
new file mode 100644
index 0000000..09d93f5
--- /dev/null
+++ b/samples/devbytes/graphics/ImagePixelization/res/values/dimens.xml
@@ -0,0 +1,20 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
+
+</resources>
diff --git a/samples/devbytes/graphics/ImagePixelization/res/values/integers.xml b/samples/devbytes/graphics/ImagePixelization/res/values/integers.xml
new file mode 100644
index 0000000..98c1438
--- /dev/null
+++ b/samples/devbytes/graphics/ImagePixelization/res/values/integers.xml
@@ -0,0 +1,19 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+
+     <integer name="seek_max">1000</integer>
+
+</resources>
diff --git a/samples/devbytes/graphics/ImagePixelization/res/values/strings.xml b/samples/devbytes/graphics/ImagePixelization/res/values/strings.xml
new file mode 100644
index 0000000..35a87c8
--- /dev/null
+++ b/samples/devbytes/graphics/ImagePixelization/res/values/strings.xml
@@ -0,0 +1,22 @@
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+
+    <string name="app_name" >ImagePixelization</string>
+    <string name="animate_pixelation">Animate</string>
+    <string name="async_task">Using AyncTask</string>
+    <string name="builtin_pixelization">Built-in Pixelization</string>
+
+</resources>
diff --git a/samples/devbytes/graphics/ImagePixelization/src/com/example/android/imagepixelization/ImagePixelization.java b/samples/devbytes/graphics/ImagePixelization/src/com/example/android/imagepixelization/ImagePixelization.java
new file mode 100644
index 0000000..e31d95d
--- /dev/null
+++ b/samples/devbytes/graphics/ImagePixelization/src/com/example/android/imagepixelization/ImagePixelization.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.android.imagepixelization;
+
+import android.animation.ObjectAnimator;
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.graphics.drawable.BitmapDrawable;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.animation.LinearInterpolator;
+import android.widget.ImageView;
+import android.widget.SeekBar;
+
+import java.util.Arrays;
+
+/**
+ * This application shows three different graphics/animation concepts.
+ *
+ * A pixelization effect is applied to an image with varying pixelization
+ * factors to achieve an image that is pixelized to varying degrees. In
+ * order to optimize the amount of image processing performed on the image
+ * being pixelized, the pixelization effect only takes place if a predefined
+ * amount of time has elapsed since the main image was last pixelized. The
+ * effect is also applied when the user stops moving the seekbar.
+ *
+ * This application also shows how to use a ValueAnimator to achieve a
+ * smooth self-animating seekbar.
+ *
+ * Lastly, this application shows a use case of AsyncTask where some
+ * computation heavy processing can be moved onto a background thread,
+ * so as to keep the UI completely responsive to user input.
+ */
+public class ImagePixelization extends Activity {
+
+    final private static int SEEKBAR_ANIMATION_DURATION = 10000;
+    final private static int TIME_BETWEEN_TASKS = 400;
+    final private static int SEEKBAR_STOP_CHANGE_DELTA = 5;
+    final private static float PROGRESS_TO_PIXELIZATION_FACTOR = 4000.0f;
+
+    Bitmap mImageBitmap;
+    ImageView mImageView;
+    SeekBar mSeekBar;
+    boolean mIsChecked = false;
+    boolean mIsBuiltinPixelizationChecked = false;
+    int mLastProgress = 0;
+    long mLastTime = 0;
+    Bitmap mPixelatedBitmap;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_image_pixelization);
+
+        mImageView = (ImageView) findViewById(R.id.pixelView);
+        mSeekBar = (SeekBar)findViewById(R.id.seekbar);
+
+        mImageBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image);
+        mImageView.setImageBitmap(mImageBitmap);
+
+        mSeekBar.setOnSeekBarChangeListener(mOnSeekBarChangeListener);
+    }
+
+    private SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener =
+            new SeekBar.OnSeekBarChangeListener() {
+
+        @Override
+        public void onStopTrackingTouch(SeekBar seekBar) {
+            if (Math.abs(mSeekBar.getProgress() - mLastProgress) > SEEKBAR_STOP_CHANGE_DELTA) {
+                invokePixelization();
+            }
+        }
+
+        @Override
+        public void onStartTrackingTouch(SeekBar seekBar) {
+        }
+
+        @Override
+        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+            checkIfShouldPixelize();
+        }
+    };
+
+    /**
+     * Checks if enough time has elapsed since the last pixelization call was invoked.
+     * This prevents too many pixelization processes from being invoked at the same time
+     * while previous ones have not yet completed.
+     */
+    public void checkIfShouldPixelize() {
+        if ((System.currentTimeMillis() - mLastTime) > TIME_BETWEEN_TASKS) {
+            invokePixelization();
+        }
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.image_pixelization, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected (MenuItem item) {
+        switch (item.getItemId()){
+            case R.id.animate:
+                ObjectAnimator animator = ObjectAnimator.ofInt(mSeekBar, "progress", 0,
+                        mSeekBar.getMax());
+                animator.setInterpolator(new LinearInterpolator());
+                animator.setDuration(SEEKBAR_ANIMATION_DURATION);
+                animator.start();
+                break;
+            case R.id.checkbox:
+                if (mIsChecked) {
+                    item.setChecked(false);
+                    mIsChecked = false;
+                } else {
+                    item.setChecked(true);
+                    mIsChecked = true;
+                }
+                break;
+            case R.id.builtin_pixelation_checkbox:
+                mIsBuiltinPixelizationChecked = !mIsBuiltinPixelizationChecked;
+                item.setChecked(mIsBuiltinPixelizationChecked);
+                break;
+            default:
+                break;
+        }
+        return true;
+    }
+
+    /**
+     * A simple pixelization algorithm. This uses a box blur algorithm where all the
+     * pixels within some region are averaged, and that average pixel value is then
+     * applied to all the pixels within that region. A higher pixelization factor
+     * imposes a smaller number of regions of greater size. Similarly, a smaller
+     * pixelization factor imposes a larger number of regions of smaller size.
+     */
+    public BitmapDrawable customImagePixelization(float pixelizationFactor, Bitmap bitmap) {
+
+        int width = bitmap.getWidth();
+        int height = bitmap.getHeight();
+
+        if (mPixelatedBitmap == null || !(width == mPixelatedBitmap.getWidth() && height ==
+                mPixelatedBitmap.getHeight())) {
+            mPixelatedBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        }
+
+        int xPixels = (int) (pixelizationFactor * ((float)width));
+        xPixels = xPixels > 0 ? xPixels : 1;
+        int yPixels = (int)  (pixelizationFactor * ((float)height));
+        yPixels = yPixels > 0 ? yPixels : 1;
+        int pixel = 0, red = 0, green = 0, blue = 0, numPixels = 0;
+
+        int[] bitmapPixels = new int[width * height];
+        bitmap.getPixels(bitmapPixels, 0, width, 0, 0, width, height);
+
+        int[] pixels = new int[yPixels * xPixels];
+
+        int maxX, maxY;
+
+        for (int y = 0; y < height; y+=yPixels) {
+            for (int x = 0; x < width; x+=xPixels) {
+
+                numPixels = red = green = blue = 0;
+
+                maxX = Math.min(x + xPixels, width);
+                maxY = Math.min(y + yPixels, height);
+
+                for (int i = x; i < maxX; i++) {
+                    for (int j = y; j < maxY; j++) {
+                        pixel = bitmapPixels[j * width + i];
+                        red += Color.red(pixel);
+                        green += Color.green(pixel);
+                        blue += Color.blue(pixel);
+                        numPixels ++;
+                    }
+                }
+
+                pixel = Color.rgb(red / numPixels, green / numPixels, blue / numPixels);
+
+                Arrays.fill(pixels, pixel);
+
+                int w = Math.min(xPixels, width - x);
+                int h = Math.min(yPixels, height - y);
+
+                mPixelatedBitmap.setPixels(pixels, 0 , w, x , y, w, h);
+            }
+        }
+
+        return new BitmapDrawable(getResources(), mPixelatedBitmap);
+    }
+
+    /**
+     * This method of image pixelization utilizes the bitmap scaling operations built
+     * into the framework. By downscaling the bitmap and upscaling it back to its
+     * original size (while setting the filter flag to false), the same effect can be
+     * achieved with much better performance.
+     */
+    public BitmapDrawable builtInPixelization(float pixelizationFactor, Bitmap bitmap) {
+
+        int width = bitmap.getWidth();
+        int height = bitmap.getHeight();
+
+        int downScaleFactorWidth = (int)(pixelizationFactor * width);
+        downScaleFactorWidth = downScaleFactorWidth > 0 ? downScaleFactorWidth : 1;
+        int downScaleFactorHeight = (int)(pixelizationFactor * height);
+        downScaleFactorHeight = downScaleFactorHeight > 0 ? downScaleFactorHeight : 1;
+
+        int downScaledWidth =  width / downScaleFactorWidth;
+        int downScaledHeight = height / downScaleFactorHeight;
+
+        Bitmap pixelatedBitmap = Bitmap.createScaledBitmap(bitmap, downScaledWidth,
+                downScaledHeight, false);
+
+        /* Bitmap's createScaledBitmap method has a filter parameter that can be set to either
+         * true or false in order to specify either bilinear filtering or point sampling
+         * respectively when the bitmap is scaled up or now.
+         *
+         * Similarly, a BitmapDrawable also has a flag to specify the same thing. When the
+         * BitmapDrawable is applied to an ImageView that has some scaleType, the filtering
+         * flag is taken into consideration. However, for optimization purposes, this flag was
+         * ignored in BitmapDrawables before Jelly Bean MR1.
+         *
+         * Here, it is important to note that prior to JBMR1, two bitmap scaling operations
+         * are required to achieve the pixelization effect. Otherwise, a BitmapDrawable
+         * can be created corresponding to the downscaled bitmap such that when it is
+         * upscaled to fit the ImageView, the upscaling operation is a lot faster since
+         * it uses internal optimizations to fit the ImageView.
+         * */
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+            BitmapDrawable bitmapDrawable = new BitmapDrawable(getResources(), pixelatedBitmap);
+            bitmapDrawable.setFilterBitmap(false);
+            return bitmapDrawable;
+        } else {
+            Bitmap upscaled = Bitmap.createScaledBitmap(pixelatedBitmap, width, height, false);
+            return new BitmapDrawable(getResources(), upscaled);
+        }
+    }
+
+    /**
+     * Invokes pixelization either on the main thread or on a background thread
+     * depending on whether or not the checkbox was checked.
+     */
+    public void invokePixelization () {
+        mLastTime = System.currentTimeMillis();
+        mLastProgress = mSeekBar.getProgress();
+        if (mIsChecked) {
+            PixelizeImageAsyncTask asyncPixelateTask = new PixelizeImageAsyncTask();
+            asyncPixelateTask.execute(mSeekBar.getProgress() / PROGRESS_TO_PIXELIZATION_FACTOR,
+                    mImageBitmap);
+        } else {
+            mImageView.setImageDrawable(pixelizeImage(mSeekBar.getProgress()
+                    / PROGRESS_TO_PIXELIZATION_FACTOR, mImageBitmap));
+        }
+    }
+
+    /**
+     *  Selects either the custom pixelization algorithm that sets and gets bitmap
+     *  pixels manually or the one that uses built-in bitmap operations.
+     */
+    public BitmapDrawable pixelizeImage(float pixelizationFactor, Bitmap bitmap) {
+        if (mIsBuiltinPixelizationChecked) {
+            return builtInPixelization(pixelizationFactor, bitmap);
+        } else {
+            return customImagePixelization(pixelizationFactor, bitmap);
+        }
+    }
+
+    /**
+     * Implementation of the AsyncTask class showing how to run the
+     * pixelization algorithm in the background, and retrieving the
+     * pixelated image from the resulting operation.
+     */
+    private class PixelizeImageAsyncTask extends AsyncTask<Object, Void, BitmapDrawable> {
+
+        @Override
+        protected BitmapDrawable doInBackground(Object... params) {
+            float pixelizationFactor = (Float)params[0];
+            Bitmap originalBitmap = (Bitmap)params[1];
+            return pixelizeImage(pixelizationFactor, originalBitmap);
+        }
+
+        @Override
+        protected void onPostExecute(BitmapDrawable result) {
+            mImageView.setImageDrawable(result);
+        }
+
+        @Override
+        protected void onPreExecute() {
+
+        }
+
+        @Override
+        protected void onProgressUpdate(Void... values) {
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/scripts/app_engine_server/memcache_zipserve.py b/scripts/app_engine_server/memcache_zipserve.py
index 9c6f42e..4efd984 100644
--- a/scripts/app_engine_server/memcache_zipserve.py
+++ b/scripts/app_engine_server/memcache_zipserve.py
@@ -94,7 +94,7 @@
   source file again. It also uses considerably fewer CPU cycles.
   """
   zipfile_cache = {}                # class cache of source zip files
-  MAX_AGE = 600                     # max client-side cache lifetime
+  MAX_AGE = 43200                   # max client-side cache lifetime, in seconds
   PUBLIC = True                     # public cache setting
   CACHE_PREFIX = 'cache://'         # memcache key prefix for actual URLs
   NEG_CACHE_PREFIX = 'noncache://'  # memcache key prefix for non-existant URL
diff --git a/scripts/app_engine_server/redirects.yaml b/scripts/app_engine_server/redirects.yaml
index 465d692..4f83713 100644
--- a/scripts/app_engine_server/redirects.yaml
+++ b/scripts/app_engine_server/redirects.yaml
@@ -77,11 +77,21 @@
   type: permanent
   comment: Redirect sdk reference to new location
 
-# new one works
 - src: /sdk/compatibility-library.html
-  dst: /tools/extras/support-library.html
+  dst: /tools/support-library/index.html
   type: permanent
-  comment: Redirect sdk reference to new location
+
+- src: /tools/extras/support-library.html
+  dst: /tools/support-library/index.html
+  type: permanent
+
+- src: /training/basics/fragments/support-lib.html
+  dst: /tools/support-library/setup.html
+  type: permanent
+
+- src: /training/id-auth/.*
+  dst: /google/play-services/auth.html
+  type: permanent
 
 # new one works
 - src: /sdk/eclipse-adt.html
@@ -279,11 +289,6 @@
   type: permanent
   comment: Open Accessory Protocol content has moved to source.android.com.
 
-- src: /tools/extras/support-library.html
-  dst: /tools/support-library/index.html
-  type: permanent
-  comment: moved Support Library doc to its own directory
-
 - src: /guide/topics/usb
   dst: /guide/topics/connectivity/usb
   type: permanent
@@ -561,6 +566,10 @@
 
 # ------------------- TRAINING -------------------
 
+- src: /guide/topics/ui/layout/tabs.html
+  dst: /training/implementing-navigation/lateral.html
+  type: permanent
+
 - src: /training/cloudsync/aesync.html
   dst: /google/gcm/index.html
   type: permanent