Merge "Revert "Import translations. DO NOT MERGE""
diff --git "a/\135" "b/\135"
new file mode 100644
index 0000000..5619151
--- /dev/null
+++ "b/\135"
@@ -0,0 +1,12 @@
+NetworkNotificationManager: logging improvements
+
+TODO: squash me
+# Please enter the commit message for your changes. Lines starting
+# with '#' will be ignored, and an empty message aborts the commit.
+# On branch notification_tagging
+# Your branch is ahead of 'goog/master' by 2 commits.
+#   (use "git push" to publish your local commits)
+#
+# Changes to be committed:
+#	modified:   services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
+#
diff --git a/api/current.txt b/api/current.txt
index bab071b..5020a425 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -2804,9 +2804,14 @@
 
   public static class GestureDescription.StrokeDescription {
     ctor public GestureDescription.StrokeDescription(android.graphics.Path, long, long);
+    ctor public GestureDescription.StrokeDescription(android.graphics.Path, long, long, int, boolean);
+    method public int getContinuedStrokeId();
     method public long getDuration();
+    method public int getId();
     method public android.graphics.Path getPath();
     method public long getStartTime();
+    method public boolean isContinued();
+    field public static final int INVALID_STROKE_ID = -1; // 0xffffffff
   }
 
 }
@@ -6185,6 +6190,7 @@
     field public static final deprecated java.lang.String EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME = "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME";
     field public static final java.lang.String EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM = "android.app.extra.PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM";
     field public static final java.lang.String EXTRA_PROVISIONING_EMAIL_ADDRESS = "android.app.extra.PROVISIONING_EMAIL_ADDRESS";
+    field public static final java.lang.String EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION = "android.app.extra.PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION";
     field public static final java.lang.String EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED = "android.app.extra.PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED";
     field public static final java.lang.String EXTRA_PROVISIONING_LOCALE = "android.app.extra.PROVISIONING_LOCALE";
     field public static final java.lang.String EXTRA_PROVISIONING_LOCAL_TIME = "android.app.extra.PROVISIONING_LOCAL_TIME";
@@ -8816,6 +8822,7 @@
     field public static final java.lang.String CATEGORY_SELECTED_ALTERNATIVE = "android.intent.category.SELECTED_ALTERNATIVE";
     field public static final java.lang.String CATEGORY_TAB = "android.intent.category.TAB";
     field public static final java.lang.String CATEGORY_TEST = "android.intent.category.TEST";
+    field public static final java.lang.String CATEGORY_TYPED_OPENABLE = "android.intent.category.TYPED_OPENABLE";
     field public static final java.lang.String CATEGORY_UNIT_TEST = "android.intent.category.UNIT_TEST";
     field public static final java.lang.String CATEGORY_VOICE = "android.intent.category.VOICE";
     field public static final android.os.Parcelable.Creator<android.content.Intent> CREATOR;
@@ -29681,6 +29688,7 @@
     field public static final java.lang.String DISALLOW_ADD_USER = "no_add_user";
     field public static final java.lang.String DISALLOW_ADJUST_VOLUME = "no_adjust_volume";
     field public static final java.lang.String DISALLOW_APPS_CONTROL = "no_control_apps";
+    field public static final java.lang.String DISALLOW_BLUETOOTH = "no_bluetooth";
     field public static final java.lang.String DISALLOW_CONFIG_BLUETOOTH = "no_config_bluetooth";
     field public static final java.lang.String DISALLOW_CONFIG_CELL_BROADCASTS = "no_config_cell_broadcasts";
     field public static final java.lang.String DISALLOW_CONFIG_CREDENTIALS = "no_config_credentials";
@@ -37260,6 +37268,7 @@
     field public static final java.lang.String KEY_HIDE_SIM_LOCK_SETTINGS_BOOL = "hide_sim_lock_settings_bool";
     field public static final java.lang.String KEY_IGNORE_SIM_NETWORK_LOCKED_EVENTS_BOOL = "ignore_sim_network_locked_events_bool";
     field public static final java.lang.String KEY_IMS_DTMF_TONE_DELAY_INT = "ims_dtmf_tone_delay_int";
+    field public static final java.lang.String KEY_MDN_IS_ADDITIONAL_VOICEMAIL_NUMBER_BOOL = "mdn_is_additional_voicemail_number_bool";
     field public static final java.lang.String KEY_MMS_ALIAS_ENABLED_BOOL = "aliasEnabled";
     field public static final java.lang.String KEY_MMS_ALIAS_MAX_CHARS_INT = "aliasMaxChars";
     field public static final java.lang.String KEY_MMS_ALIAS_MIN_CHARS_INT = "aliasMinChars";
@@ -44384,7 +44393,7 @@
     field public static final int FLAG_ALT_FOCUSABLE_IM = 131072; // 0x20000
     field public static final deprecated int FLAG_BLUR_BEHIND = 4; // 0x4
     field public static final int FLAG_DIM_BEHIND = 2; // 0x2
-    field public static final int FLAG_DISMISS_KEYGUARD = 4194304; // 0x400000
+    field public static final deprecated int FLAG_DISMISS_KEYGUARD = 4194304; // 0x400000
     field public static final deprecated int FLAG_DITHER = 4096; // 0x1000
     field public static final int FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = -2147483648; // 0x80000000
     field public static final int FLAG_FORCE_NOT_FULLSCREEN = 2048; // 0x800
diff --git a/api/system-current.txt b/api/system-current.txt
index 4a2f2d5..f23c1fc 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -2918,9 +2918,14 @@
 
   public static class GestureDescription.StrokeDescription {
     ctor public GestureDescription.StrokeDescription(android.graphics.Path, long, long);
+    ctor public GestureDescription.StrokeDescription(android.graphics.Path, long, long, int, boolean);
+    method public int getContinuedStrokeId();
     method public long getDuration();
+    method public int getId();
     method public android.graphics.Path getPath();
     method public long getStartTime();
+    method public boolean isContinued();
+    field public static final int INVALID_STROKE_ID = -1; // 0xffffffff
   }
 
 }
@@ -6374,6 +6379,7 @@
     field public static final deprecated java.lang.String EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME = "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME";
     field public static final java.lang.String EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM = "android.app.extra.PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM";
     field public static final java.lang.String EXTRA_PROVISIONING_EMAIL_ADDRESS = "android.app.extra.PROVISIONING_EMAIL_ADDRESS";
+    field public static final java.lang.String EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION = "android.app.extra.PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION";
     field public static final java.lang.String EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED = "android.app.extra.PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED";
     field public static final java.lang.String EXTRA_PROVISIONING_LOCALE = "android.app.extra.PROVISIONING_LOCALE";
     field public static final java.lang.String EXTRA_PROVISIONING_LOCAL_TIME = "android.app.extra.PROVISIONING_LOCAL_TIME";
@@ -9176,6 +9182,7 @@
     field public static final java.lang.String CATEGORY_SELECTED_ALTERNATIVE = "android.intent.category.SELECTED_ALTERNATIVE";
     field public static final java.lang.String CATEGORY_TAB = "android.intent.category.TAB";
     field public static final java.lang.String CATEGORY_TEST = "android.intent.category.TEST";
+    field public static final java.lang.String CATEGORY_TYPED_OPENABLE = "android.intent.category.TYPED_OPENABLE";
     field public static final java.lang.String CATEGORY_UNIT_TEST = "android.intent.category.UNIT_TEST";
     field public static final java.lang.String CATEGORY_VOICE = "android.intent.category.VOICE";
     field public static final android.os.Parcelable.Creator<android.content.Intent> CREATOR;
@@ -32296,6 +32303,7 @@
     field public static final java.lang.String DISALLOW_ADD_USER = "no_add_user";
     field public static final java.lang.String DISALLOW_ADJUST_VOLUME = "no_adjust_volume";
     field public static final java.lang.String DISALLOW_APPS_CONTROL = "no_control_apps";
+    field public static final java.lang.String DISALLOW_BLUETOOTH = "no_bluetooth";
     field public static final java.lang.String DISALLOW_CONFIG_BLUETOOTH = "no_config_bluetooth";
     field public static final java.lang.String DISALLOW_CONFIG_CELL_BROADCASTS = "no_config_cell_broadcasts";
     field public static final java.lang.String DISALLOW_CONFIG_CREDENTIALS = "no_config_credentials";
@@ -35638,6 +35646,7 @@
     field public static final int WIFI_SLEEP_POLICY_DEFAULT = 0; // 0x0
     field public static final int WIFI_SLEEP_POLICY_NEVER = 2; // 0x2
     field public static final int WIFI_SLEEP_POLICY_NEVER_WHILE_PLUGGED = 1; // 0x1
+    field public static final java.lang.String WIFI_WAKEUP_ENABLED = "wifi_wakeup_enabled";
     field public static final java.lang.String WIFI_WATCHDOG_ON = "wifi_watchdog_on";
     field public static final java.lang.String WINDOW_ANIMATION_SCALE = "window_animation_scale";
   }
@@ -40330,6 +40339,7 @@
     field public static final java.lang.String KEY_HIDE_SIM_LOCK_SETTINGS_BOOL = "hide_sim_lock_settings_bool";
     field public static final java.lang.String KEY_IGNORE_SIM_NETWORK_LOCKED_EVENTS_BOOL = "ignore_sim_network_locked_events_bool";
     field public static final java.lang.String KEY_IMS_DTMF_TONE_DELAY_INT = "ims_dtmf_tone_delay_int";
+    field public static final java.lang.String KEY_MDN_IS_ADDITIONAL_VOICEMAIL_NUMBER_BOOL = "mdn_is_additional_voicemail_number_bool";
     field public static final java.lang.String KEY_MMS_ALIAS_ENABLED_BOOL = "aliasEnabled";
     field public static final java.lang.String KEY_MMS_ALIAS_MAX_CHARS_INT = "aliasMaxChars";
     field public static final java.lang.String KEY_MMS_ALIAS_MIN_CHARS_INT = "aliasMinChars";
@@ -47539,7 +47549,7 @@
     field public static final int FLAG_ALT_FOCUSABLE_IM = 131072; // 0x20000
     field public static final deprecated int FLAG_BLUR_BEHIND = 4; // 0x4
     field public static final int FLAG_DIM_BEHIND = 2; // 0x2
-    field public static final int FLAG_DISMISS_KEYGUARD = 4194304; // 0x400000
+    field public static final deprecated int FLAG_DISMISS_KEYGUARD = 4194304; // 0x400000
     field public static final deprecated int FLAG_DITHER = 4096; // 0x1000
     field public static final int FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = -2147483648; // 0x80000000
     field public static final int FLAG_FORCE_NOT_FULLSCREEN = 2048; // 0x800
diff --git a/api/test-current.txt b/api/test-current.txt
index b888911..810680d 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -2804,9 +2804,14 @@
 
   public static class GestureDescription.StrokeDescription {
     ctor public GestureDescription.StrokeDescription(android.graphics.Path, long, long);
+    ctor public GestureDescription.StrokeDescription(android.graphics.Path, long, long, int, boolean);
+    method public int getContinuedStrokeId();
     method public long getDuration();
+    method public int getId();
     method public android.graphics.Path getPath();
     method public long getStartTime();
+    method public boolean isContinued();
+    field public static final int INVALID_STROKE_ID = -1; // 0xffffffff
   }
 
 }
@@ -6207,6 +6212,7 @@
     field public static final deprecated java.lang.String EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME = "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME";
     field public static final java.lang.String EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM = "android.app.extra.PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM";
     field public static final java.lang.String EXTRA_PROVISIONING_EMAIL_ADDRESS = "android.app.extra.PROVISIONING_EMAIL_ADDRESS";
+    field public static final java.lang.String EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION = "android.app.extra.PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION";
     field public static final java.lang.String EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED = "android.app.extra.PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED";
     field public static final java.lang.String EXTRA_PROVISIONING_LOCALE = "android.app.extra.PROVISIONING_LOCALE";
     field public static final java.lang.String EXTRA_PROVISIONING_LOCAL_TIME = "android.app.extra.PROVISIONING_LOCAL_TIME";
@@ -8841,6 +8847,7 @@
     field public static final java.lang.String CATEGORY_SELECTED_ALTERNATIVE = "android.intent.category.SELECTED_ALTERNATIVE";
     field public static final java.lang.String CATEGORY_TAB = "android.intent.category.TAB";
     field public static final java.lang.String CATEGORY_TEST = "android.intent.category.TEST";
+    field public static final java.lang.String CATEGORY_TYPED_OPENABLE = "android.intent.category.TYPED_OPENABLE";
     field public static final java.lang.String CATEGORY_UNIT_TEST = "android.intent.category.UNIT_TEST";
     field public static final java.lang.String CATEGORY_VOICE = "android.intent.category.VOICE";
     field public static final android.os.Parcelable.Creator<android.content.Intent> CREATOR;
@@ -29770,6 +29777,7 @@
     field public static final java.lang.String DISALLOW_ADD_USER = "no_add_user";
     field public static final java.lang.String DISALLOW_ADJUST_VOLUME = "no_adjust_volume";
     field public static final java.lang.String DISALLOW_APPS_CONTROL = "no_control_apps";
+    field public static final java.lang.String DISALLOW_BLUETOOTH = "no_bluetooth";
     field public static final java.lang.String DISALLOW_CONFIG_BLUETOOTH = "no_config_bluetooth";
     field public static final java.lang.String DISALLOW_CONFIG_CELL_BROADCASTS = "no_config_cell_broadcasts";
     field public static final java.lang.String DISALLOW_CONFIG_CREDENTIALS = "no_config_credentials";
@@ -37356,6 +37364,7 @@
     field public static final java.lang.String KEY_HIDE_SIM_LOCK_SETTINGS_BOOL = "hide_sim_lock_settings_bool";
     field public static final java.lang.String KEY_IGNORE_SIM_NETWORK_LOCKED_EVENTS_BOOL = "ignore_sim_network_locked_events_bool";
     field public static final java.lang.String KEY_IMS_DTMF_TONE_DELAY_INT = "ims_dtmf_tone_delay_int";
+    field public static final java.lang.String KEY_MDN_IS_ADDITIONAL_VOICEMAIL_NUMBER_BOOL = "mdn_is_additional_voicemail_number_bool";
     field public static final java.lang.String KEY_MMS_ALIAS_ENABLED_BOOL = "aliasEnabled";
     field public static final java.lang.String KEY_MMS_ALIAS_MAX_CHARS_INT = "aliasMaxChars";
     field public static final java.lang.String KEY_MMS_ALIAS_MIN_CHARS_INT = "aliasMinChars";
@@ -44640,7 +44649,7 @@
     field public static final int FLAG_ALT_FOCUSABLE_IM = 131072; // 0x20000
     field public static final deprecated int FLAG_BLUR_BEHIND = 4; // 0x4
     field public static final int FLAG_DIM_BEHIND = 2; // 0x2
-    field public static final int FLAG_DISMISS_KEYGUARD = 4194304; // 0x400000
+    field public static final deprecated int FLAG_DISMISS_KEYGUARD = 4194304; // 0x400000
     field public static final deprecated int FLAG_DITHER = 4096; // 0x1000
     field public static final int FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = -2147483648; // 0x80000000
     field public static final int FLAG_FORCE_NOT_FULLSCREEN = 2048; // 0x800
diff --git a/core/java/android/accessibilityservice/GestureDescription.java b/core/java/android/accessibilityservice/GestureDescription.java
index d9b03fa..c9da152 100644
--- a/core/java/android/accessibilityservice/GestureDescription.java
+++ b/core/java/android/accessibilityservice/GestureDescription.java
@@ -23,10 +23,6 @@
 import android.graphics.RectF;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.view.InputDevice;
-import android.view.MotionEvent;
-import android.view.MotionEvent.PointerCoords;
-import android.view.MotionEvent.PointerProperties;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -128,9 +124,14 @@
         for (int i = 0; i < mStrokes.size(); i++) {
             StrokeDescription strokeDescription = mStrokes.get(i);
             if (strokeDescription.hasPointForTime(time)) {
-                touchPoints[numPointsFound].mPathIndex = i;
-                touchPoints[numPointsFound].mIsStartOfPath = (time == strokeDescription.mStartTime);
-                touchPoints[numPointsFound].mIsEndOfPath = (time == strokeDescription.mEndTime);
+                touchPoints[numPointsFound].mStrokeId = strokeDescription.getId();
+                touchPoints[numPointsFound].mContinuedStrokeId =
+                        strokeDescription.getContinuedStrokeId();
+                touchPoints[numPointsFound].mIsStartOfPath =
+                        (strokeDescription.getContinuedStrokeId() < 0)
+                                && (time == strokeDescription.mStartTime);
+                touchPoints[numPointsFound].mIsEndOfPath = !strokeDescription.isContinued()
+                        && (time == strokeDescription.mEndTime);
                 strokeDescription.getPosForTime(time, mTempPos);
                 touchPoints[numPointsFound].mX = Math.round(mTempPos[0]);
                 touchPoints[numPointsFound].mY = Math.round(mTempPos[1]);
@@ -196,6 +197,10 @@
      * Immutable description of stroke that can be part of a gesture.
      */
     public static class StrokeDescription {
+        public static final int INVALID_STROKE_ID = -1;
+
+        static int sIdCounter;
+
         Path mPath;
         long mStartTime;
         long mEndTime;
@@ -203,6 +208,9 @@
         private PathMeasure mPathMeasure;
         // The tap location is only set for zero-length paths
         float[] mTapLocation;
+        int mId;
+        boolean mContinued;
+        int mContinuedStrokeId;
 
         /**
          * @param path The path to follow. Must have exactly one contour. The bounds of the path
@@ -216,6 +224,32 @@
         public StrokeDescription(@NonNull Path path,
                 @IntRange(from = 0) long startTime,
                 @IntRange(from = 0) long duration) {
+            this(path, startTime, duration, INVALID_STROKE_ID, false);
+        }
+
+        /**
+         * @param path The path to follow. Must have exactly one contour. The bounds of the path
+         * must not be negative. The path must not be empty. If the path has zero length
+         * (for example, a single {@code moveTo()}), the stroke is a touch that doesn't move.
+         * @param startTime The time, in milliseconds, from the time the gesture starts to the
+         * time the stroke should start. Must not be negative.
+         * @param duration The duration, in milliseconds, the stroke takes to traverse the path.
+         * Must be positive.
+         * @param continuedStrokeId The ID of the stroke that this stroke continues, or
+         * {@link #INVALID_STROKE_ID} if it continues no stroke. The stroke it
+         * continues must have its isContinued flag set to {@code true} and must be in the
+         * gesture dispatched immediately before the one containing this stroke.
+         * @param isContinued {@code true} if this stroke will be continued by one in the
+         * next gesture {@code false} otherwise. Continued strokes keep their pointers down when
+         * the gesture completes.
+         */
+        public StrokeDescription(@NonNull Path path,
+                @IntRange(from = 0) long startTime,
+                @IntRange(from = 0) long duration,
+                @IntRange(from = 0) int continuedStrokeId,
+                boolean isContinued) {
+            mContinued = isContinued;
+            mContinuedStrokeId = continuedStrokeId;
             if (duration <= 0) {
                 throw new IllegalArgumentException("Duration must be positive");
             }
@@ -252,6 +286,7 @@
             mStartTime = startTime;
             mEndTime = startTime + duration;
             mTimeToLengthConversion = getLength() / duration;
+            mId = sIdCounter++;
         }
 
         /**
@@ -281,6 +316,34 @@
             return mEndTime - mStartTime;
         }
 
+        /**
+         * Get the stroke's ID. The ID is used when a stroke is to be continued by another
+         * stroke in a future gesture.
+         *
+         * @return the ID of this stroke
+         */
+        public int getId() {
+            return mId;
+        }
+
+        /**
+         * Check if this stroke is marked to continue in the next gesture.
+         *
+         * @return {@code true} if the stroke is to be continued.
+         */
+        public boolean isContinued() {
+            return mContinued;
+        }
+
+        /**
+         * Get the ID of the stroke that this one will continue.
+         *
+         * @return The ID of the stroke that this stroke continues, or 0 if no such stroke exists.
+         */
+        public int getContinuedStrokeId() {
+            return mContinuedStrokeId;
+        }
+
         float getLength() {
             return mPathMeasure.getLength();
         }
@@ -314,11 +377,12 @@
         private static final int FLAG_IS_START_OF_PATH = 0x01;
         private static final int FLAG_IS_END_OF_PATH = 0x02;
 
-        int mPathIndex;
-        boolean mIsStartOfPath;
-        boolean mIsEndOfPath;
-        float mX;
-        float mY;
+        public int mStrokeId;
+        public int mContinuedStrokeId;
+        public boolean mIsStartOfPath;
+        public boolean mIsEndOfPath;
+        public float mX;
+        public float mY;
 
         public TouchPoint() {
         }
@@ -328,7 +392,8 @@
         }
 
         public TouchPoint(Parcel parcel) {
-            mPathIndex = parcel.readInt();
+            mStrokeId = parcel.readInt();
+            mContinuedStrokeId = parcel.readInt();
             int startEnd = parcel.readInt();
             mIsStartOfPath = (startEnd & FLAG_IS_START_OF_PATH) != 0;
             mIsEndOfPath = (startEnd & FLAG_IS_END_OF_PATH) != 0;
@@ -336,8 +401,9 @@
             mY = parcel.readFloat();
         }
 
-        void copyFrom(TouchPoint other) {
-            mPathIndex = other.mPathIndex;
+        public void copyFrom(TouchPoint other) {
+            mStrokeId = other.mStrokeId;
+            mContinuedStrokeId = other.mContinuedStrokeId;
             mIsStartOfPath = other.mIsStartOfPath;
             mIsEndOfPath = other.mIsEndOfPath;
             mX = other.mX;
@@ -351,7 +417,8 @@
 
         @Override
         public void writeToParcel(Parcel dest, int flags) {
-            dest.writeInt(mPathIndex);
+            dest.writeInt(mStrokeId);
+            dest.writeInt(mContinuedStrokeId);
             int startEnd = mIsStartOfPath ? FLAG_IS_START_OF_PATH : 0;
             startEnd |= mIsEndOfPath ? FLAG_IS_END_OF_PATH : 0;
             dest.writeInt(startEnd);
@@ -426,30 +493,15 @@
     }
 
     /**
-     * Class to convert a GestureDescription to a series of MotionEvents.
+     * Class to convert a GestureDescription to a series of GestureSteps.
      *
      * @hide
      */
     public static class MotionEventGenerator {
-        /**
-         * Constants used to initialize all MotionEvents
-         */
-        private static final int EVENT_META_STATE = 0;
-        private static final int EVENT_BUTTON_STATE = 0;
-        private static final int EVENT_DEVICE_ID = 0;
-        private static final int EVENT_EDGE_FLAGS = 0;
-        private static final int EVENT_SOURCE = InputDevice.SOURCE_TOUCHSCREEN;
-        private static final int EVENT_FLAGS = 0;
-        private static final float EVENT_X_PRECISION = 1;
-        private static final float EVENT_Y_PRECISION = 1;
-
         /* Lazily-created scratch memory for processing touches */
         private static TouchPoint[] sCurrentTouchPoints;
-        private static TouchPoint[] sLastTouchPoints;
-        private static PointerCoords[] sPointerCoords;
-        private static PointerProperties[] sPointerProps;
 
-        static List<GestureStep> getGestureStepsFromGestureDescription(
+        public static List<GestureStep> getGestureStepsFromGestureDescription(
                 GestureDescription description, int sampleTimeMs) {
             final List<GestureStep> gestureSteps = new ArrayList<>();
 
@@ -474,31 +526,6 @@
             return gestureSteps;
         }
 
-        public static List<MotionEvent> getMotionEventsFromGestureSteps(List<GestureStep> steps) {
-            final List<MotionEvent> motionEvents = new ArrayList<>();
-
-            // Number of points in last touch event
-            int lastTouchPointSize = 0;
-            TouchPoint[] lastTouchPoints;
-
-            for (int i = 0; i < steps.size(); i++) {
-                GestureStep step = steps.get(i);
-                int currentTouchPointSize = step.numTouchPoints;
-                lastTouchPoints = getLastTouchPoints(
-                        Math.max(lastTouchPointSize, currentTouchPointSize));
-
-                appendMoveEventIfNeeded(motionEvents, lastTouchPoints, lastTouchPointSize,
-                        step.touchPoints, currentTouchPointSize, step.timeSinceGestureStart);
-                lastTouchPointSize = appendUpEvents(motionEvents, lastTouchPoints,
-                        lastTouchPointSize, step.touchPoints, currentTouchPointSize,
-                        step.timeSinceGestureStart);
-                lastTouchPointSize = appendDownEvents(motionEvents, lastTouchPoints,
-                        lastTouchPointSize, step.touchPoints, currentTouchPointSize,
-                        step.timeSinceGestureStart);
-            }
-            return motionEvents;
-        }
-
         private static TouchPoint[] getCurrentTouchPoints(int requiredCapacity) {
             if ((sCurrentTouchPoints == null) || (sCurrentTouchPoints.length < requiredCapacity)) {
                 sCurrentTouchPoints = new TouchPoint[requiredCapacity];
@@ -508,133 +535,5 @@
             }
             return sCurrentTouchPoints;
         }
-
-        private static TouchPoint[] getLastTouchPoints(int requiredCapacity) {
-            if ((sLastTouchPoints == null) || (sLastTouchPoints.length < requiredCapacity)) {
-                sLastTouchPoints = new TouchPoint[requiredCapacity];
-                for (int i = 0; i < requiredCapacity; i++) {
-                    sLastTouchPoints[i] = new TouchPoint();
-                }
-            }
-            return sLastTouchPoints;
-        }
-
-        private static PointerCoords[] getPointerCoords(int requiredCapacity) {
-            if ((sPointerCoords == null) || (sPointerCoords.length < requiredCapacity)) {
-                sPointerCoords = new PointerCoords[requiredCapacity];
-                for (int i = 0; i < requiredCapacity; i++) {
-                    sPointerCoords[i] = new PointerCoords();
-                }
-            }
-            return sPointerCoords;
-        }
-
-        private static PointerProperties[] getPointerProps(int requiredCapacity) {
-            if ((sPointerProps == null) || (sPointerProps.length < requiredCapacity)) {
-                sPointerProps = new PointerProperties[requiredCapacity];
-                for (int i = 0; i < requiredCapacity; i++) {
-                    sPointerProps[i] = new PointerProperties();
-                }
-            }
-            return sPointerProps;
-        }
-
-        private static void appendMoveEventIfNeeded(List<MotionEvent> motionEvents,
-                TouchPoint[] lastTouchPoints, int lastTouchPointsSize,
-                TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
-            /* Look for pointers that have moved */
-            boolean moveFound = false;
-            for (int i = 0; i < currentTouchPointsSize; i++) {
-                int lastPointsIndex = findPointByPathIndex(lastTouchPoints, lastTouchPointsSize,
-                        currentTouchPoints[i].mPathIndex);
-                if (lastPointsIndex >= 0) {
-                    moveFound |= (lastTouchPoints[lastPointsIndex].mX != currentTouchPoints[i].mX)
-                            || (lastTouchPoints[lastPointsIndex].mY != currentTouchPoints[i].mY);
-                    lastTouchPoints[lastPointsIndex].copyFrom(currentTouchPoints[i]);
-                }
-            }
-
-            if (moveFound) {
-                long downTime = motionEvents.get(motionEvents.size() - 1).getDownTime();
-                motionEvents.add(obtainMotionEvent(downTime, currentTime, MotionEvent.ACTION_MOVE,
-                        lastTouchPoints, lastTouchPointsSize));
-            }
-        }
-
-        private static int appendUpEvents(List<MotionEvent> motionEvents,
-                TouchPoint[] lastTouchPoints, int lastTouchPointsSize,
-                TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
-            /* Look for a pointer at the end of its path */
-            for (int i = 0; i < currentTouchPointsSize; i++) {
-                if (currentTouchPoints[i].mIsEndOfPath) {
-                    int indexOfUpEvent = findPointByPathIndex(lastTouchPoints, lastTouchPointsSize,
-                            currentTouchPoints[i].mPathIndex);
-                    if (indexOfUpEvent < 0) {
-                        continue; // Should not happen
-                    }
-                    long downTime = motionEvents.get(motionEvents.size() - 1).getDownTime();
-                    int action = (lastTouchPointsSize == 1) ? MotionEvent.ACTION_UP
-                            : MotionEvent.ACTION_POINTER_UP;
-                    action |= indexOfUpEvent << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
-                    motionEvents.add(obtainMotionEvent(downTime, currentTime, action,
-                            lastTouchPoints, lastTouchPointsSize));
-                    /* Remove this point from lastTouchPoints */
-                    for (int j = indexOfUpEvent; j < lastTouchPointsSize - 1; j++) {
-                        lastTouchPoints[j].copyFrom(lastTouchPoints[j+1]);
-                    }
-                    lastTouchPointsSize--;
-                }
-            }
-            return lastTouchPointsSize;
-        }
-
-        private static int appendDownEvents(List<MotionEvent> motionEvents,
-                TouchPoint[] lastTouchPoints, int lastTouchPointsSize,
-                TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
-            /* Look for a pointer that is just starting */
-            for (int i = 0; i < currentTouchPointsSize; i++) {
-                if (currentTouchPoints[i].mIsStartOfPath) {
-                    /* Add the point to last coords and use the new array to generate the event */
-                    lastTouchPoints[lastTouchPointsSize++].copyFrom(currentTouchPoints[i]);
-                    int action = (lastTouchPointsSize == 1) ? MotionEvent.ACTION_DOWN
-                            : MotionEvent.ACTION_POINTER_DOWN;
-                    long downTime = (action == MotionEvent.ACTION_DOWN) ? currentTime :
-                            motionEvents.get(motionEvents.size() - 1).getDownTime();
-                    action |= i << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
-                    motionEvents.add(obtainMotionEvent(downTime, currentTime, action,
-                            lastTouchPoints, lastTouchPointsSize));
-                }
-            }
-            return lastTouchPointsSize;
-        }
-
-        private static MotionEvent obtainMotionEvent(long downTime, long eventTime, int action,
-                TouchPoint[] touchPoints, int touchPointsSize) {
-            PointerCoords[] pointerCoords = getPointerCoords(touchPointsSize);
-            PointerProperties[] pointerProperties = getPointerProps(touchPointsSize);
-            for (int i = 0; i < touchPointsSize; i++) {
-                pointerProperties[i].id = touchPoints[i].mPathIndex;
-                pointerProperties[i].toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
-                pointerCoords[i].clear();
-                pointerCoords[i].pressure = 1.0f;
-                pointerCoords[i].size = 1.0f;
-                pointerCoords[i].x = touchPoints[i].mX;
-                pointerCoords[i].y = touchPoints[i].mY;
-            }
-            return MotionEvent.obtain(downTime, eventTime, action, touchPointsSize,
-                    pointerProperties, pointerCoords, EVENT_META_STATE, EVENT_BUTTON_STATE,
-                    EVENT_X_PRECISION, EVENT_Y_PRECISION, EVENT_DEVICE_ID, EVENT_EDGE_FLAGS,
-                    EVENT_SOURCE, EVENT_FLAGS);
-        }
-
-        private static int findPointByPathIndex(TouchPoint[] touchPoints, int touchPointsSize,
-                int pathIndex) {
-            for (int i = 0; i < touchPointsSize; i++) {
-                if (touchPoints[i].mPathIndex == pathIndex) {
-                    return i;
-                }
-            }
-            return -1;
-        }
     }
 }
diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java
index 6e2c464..f29e576 100644
--- a/core/java/android/app/Dialog.java
+++ b/core/java/android/app/Dialog.java
@@ -200,6 +200,7 @@
             @Nullable Message cancelCallback) {
         this(context);
         mCancelable = cancelable;
+        updateWindowForCancelable();
         mCancelMessage = cancelCallback;
     }
 
@@ -207,6 +208,7 @@
             @Nullable OnCancelListener cancelListener) {
         this(context);
         mCancelable = cancelable;
+        updateWindowForCancelable();
         setOnCancelListener(cancelListener);
     }
 
@@ -1187,6 +1189,7 @@
      */
     public void setCancelable(boolean flag) {
         mCancelable = flag;
+        updateWindowForCancelable();
     }
 
     /**
@@ -1200,6 +1203,7 @@
     public void setCanceledOnTouchOutside(boolean cancel) {
         if (cancel && !mCancelable) {
             mCancelable = true;
+            updateWindowForCancelable();
         }
         
         mWindow.setCloseOnTouchOutside(cancel);
@@ -1351,4 +1355,8 @@
             }
         }
     }
+
+    private void updateWindowForCancelable() {
+        mWindow.setCloseOnSwipeEnabled(mCancelable);
+    }
 }
diff --git a/core/java/android/app/admin/ConnectEvent.java b/core/java/android/app/admin/ConnectEvent.java
index c1646bb..b6b14f2 100644
--- a/core/java/android/app/admin/ConnectEvent.java
+++ b/core/java/android/app/admin/ConnectEvent.java
@@ -31,6 +31,7 @@
     /** The destination port number. */
     private final int port;
 
+    /** @hide */
     public ConnectEvent(String ipAddress, int port, String packageName, long timestamp) {
         super(packageName, timestamp);
         this.ipAddress = ipAddress;
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 39c8b79..5600da6 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -151,6 +151,7 @@
      * <li>{@link #EXTRA_PROVISIONING_LOGO_URI}, optional</li>
      * <li>{@link #EXTRA_PROVISIONING_MAIN_COLOR}, optional</li>
      * <li>{@link #EXTRA_PROVISIONING_SKIP_USER_CONSENT}, optional</li>
+     * <li>{@link #EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION}, optional</li>
      * </ul>
      *
      * <p>When managed provisioning has completed, broadcasts are sent to the application specified
@@ -513,6 +514,19 @@
         = "android.app.extra.PROVISIONING_ACCOUNT_TO_MIGRATE";
 
     /**
+     * Boolean extra that is used in conjunction with
+     * {@link #EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE}. If it's set to {@code true}, the account will
+     * not be removed from the primary user after it is migrated to the newly created user or
+     * profile.
+     *
+     * <p> Defaults to {@code false}
+     *
+     * <p> Use with {@link #ACTION_PROVISION_MANAGED_PROFILE}.
+     */
+    public static final String EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION
+            = "android.app.extra.PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION";
+
+    /**
      * A String extra that, holds the email address of the account which a managed profile is
      * created for. Used with {@link #ACTION_PROVISION_MANAGED_PROFILE} and
      * {@link DeviceAdminReceiver#ACTION_PROFILE_PROVISIONING_COMPLETE}.
diff --git a/core/java/android/app/admin/DnsEvent.java b/core/java/android/app/admin/DnsEvent.java
index 63ea8db..30e107c 100644
--- a/core/java/android/app/admin/DnsEvent.java
+++ b/core/java/android/app/admin/DnsEvent.java
@@ -37,6 +37,7 @@
      */
     private final int ipAddressesCount;
 
+    /** @hide */
     public DnsEvent(String hostname, String[] ipAddresses, int ipAddressesCount,
             String packageName, long timestamp) {
         super(packageName, timestamp);
diff --git a/core/java/android/app/admin/NetworkEvent.java b/core/java/android/app/admin/NetworkEvent.java
index 1dbff20..0de2665 100644
--- a/core/java/android/app/admin/NetworkEvent.java
+++ b/core/java/android/app/admin/NetworkEvent.java
@@ -16,6 +16,7 @@
 
 package android.app.admin;
 
+import android.content.pm.PackageManager;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.ParcelFormatException;
@@ -35,21 +36,29 @@
     /** The timestamp of the event being reported in milliseconds. */
     long timestamp;
 
-    protected NetworkEvent() {
+    /** @hide */
+    NetworkEvent() {
         //empty constructor
     }
 
-    protected NetworkEvent(String packageName, long timestamp) {
+    /** @hide */
+    NetworkEvent(String packageName, long timestamp) {
         this.packageName = packageName;
         this.timestamp = timestamp;
     }
 
-    /** Returns the package name of the UID that performed the query. */
+    /**
+     * Returns the package name of the UID that performed the query, as returned by
+     * {@link PackageManager#getNameForUid}.
+     */
     public String getPackageName() {
         return packageName;
     }
 
-    /** Returns the timestamp of the event being reported in milliseconds. */
+    /**
+     * Returns the timestamp of the event being reported in milliseconds, the difference between
+     * the time the event was reported and midnight, January 1, 1970 UTC.
+     */
     public long getTimestamp() {
         return timestamp;
     }
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index a5d7999..cda8176 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -3483,6 +3483,21 @@
     public static final String CATEGORY_OPENABLE = "android.intent.category.OPENABLE";
 
     /**
+     * Used to indicate that an intent filter can accept files which are not necessarily
+     * openable by {@link ContentResolver#openFileDescriptor(Uri, String)}, but
+     * at least streamable via
+     * {@link ContentResolver#openTypedAssetFileDescriptor(Uri, String, Bundle)}
+     * using one of the stream types exposed via
+     * {@link ContentResolver#getStreamTypes(Uri, String)}.
+     *
+     * @see #ACTION_SEND
+     * @see #ACTION_SEND_MULTIPLE
+     */
+    @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+    public static final String CATEGORY_TYPED_OPENABLE  =
+            "android.intent.category.TYPED_OPENABLE";
+
+    /**
      * To be used as code under test for framework instrumentation tests.
      */
     public static final String CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST =
diff --git a/core/java/android/net/metrics/NetworkEvent.java b/core/java/android/net/metrics/NetworkEvent.java
index 3b3fa69..0667495 100644
--- a/core/java/android/net/metrics/NetworkEvent.java
+++ b/core/java/android/net/metrics/NetworkEvent.java
@@ -42,6 +42,15 @@
     public static final int NETWORK_DISCONNECTED         = 7;
 
     /** {@hide} */
+    public static final int NETWORK_FIRST_VALIDATION_SUCCESS      = 8;
+    /** {@hide} */
+    public static final int NETWORK_REVALIDATION_SUCCESS          = 9;
+    /** {@hide} */
+    public static final int NETWORK_FIRST_VALIDATION_PORTAL_FOUND = 10;
+    /** {@hide} */
+    public static final int NETWORK_REVALIDATION_PORTAL_FOUND     = 11;
+
+    /** {@hide} */
     @IntDef(value = {
             NETWORK_CONNECTED,
             NETWORK_VALIDATED,
@@ -50,6 +59,10 @@
             NETWORK_LINGER,
             NETWORK_UNLINGER,
             NETWORK_DISCONNECTED,
+            NETWORK_FIRST_VALIDATION_SUCCESS,
+            NETWORK_REVALIDATION_SUCCESS,
+            NETWORK_FIRST_VALIDATION_PORTAL_FOUND,
+            NETWORK_REVALIDATION_PORTAL_FOUND,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface EventType {}
diff --git a/core/java/android/net/metrics/ValidationProbeEvent.java b/core/java/android/net/metrics/ValidationProbeEvent.java
index 1a31b56..a724ec1 100644
--- a/core/java/android/net/metrics/ValidationProbeEvent.java
+++ b/core/java/android/net/metrics/ValidationProbeEvent.java
@@ -44,10 +44,8 @@
     public static final int DNS_FAILURE = 0;
     public static final int DNS_SUCCESS = 1;
 
-    /** {@hide} */
-    @IntDef(value = {PROBE_DNS, PROBE_HTTP, PROBE_HTTPS, PROBE_PAC})
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface ProbeType {}
+    private static final int FIRST_VALIDATION  = 1 << 8;
+    private static final int REVALIDATION      = 2 << 8;
 
     /** {@hide} */
     @IntDef(value = {DNS_FAILURE, DNS_SUCCESS})
@@ -56,12 +54,17 @@
 
     public final int netId;
     public final long durationMs;
-    public final @ProbeType int probeType;
+    // probeType byte format (MSB to LSB):
+    // byte 0: unused
+    // byte 1: unused
+    // byte 2: 0 = UNKNOWN, 1 = FIRST_VALIDATION, 2 = REVALIDATION
+    // byte 3: PROBE_* constant
+    public final int probeType;
     public final @ReturnCode int returnCode;
 
     /** {@hide} */
     public ValidationProbeEvent(
-            int netId, long durationMs, @ProbeType int probeType, @ReturnCode int returnCode) {
+            int netId, long durationMs, int probeType, @ReturnCode int returnCode) {
         this.netId = netId;
         this.durationMs = durationMs;
         this.probeType = probeType;
@@ -100,8 +103,18 @@
     };
 
     /** @hide */
+    public static int makeProbeType(int probeType, boolean firstValidation) {
+        return (probeType & 0xff) | (firstValidation ? FIRST_VALIDATION : REVALIDATION);
+    }
+
+    /** @hide */
     public static String getProbeName(int probeType) {
-        return Decoder.constants.get(probeType, "PROBE_???");
+        return Decoder.constants.get(probeType & 0xff, "PROBE_???");
+    }
+
+    /** @hide */
+    public static String getValidationStage(int probeType) {
+        return Decoder.constants.get(probeType & 0xff00, "UNKNOWN");
     }
 
     public static void logEvent(int netId, long durationMs, int probeType, int returnCode) {
@@ -109,12 +122,13 @@
 
     @Override
     public String toString() {
-        return String.format("ValidationProbeEvent(%d, %s:%d, %dms)",
-                netId, getProbeName(probeType), returnCode, durationMs);
+        return String.format("ValidationProbeEvent(%d, %s:%d %s, %dms)", netId,
+                getProbeName(probeType), returnCode, getValidationStage(probeType), durationMs);
     }
 
     final static class Decoder {
         static final SparseArray<String> constants = MessageUtils.findMessageNames(
-                new Class[]{ValidationProbeEvent.class}, new String[]{"PROBE_"});
+                new Class[]{ValidationProbeEvent.class},
+                new String[]{"PROBE_", "FIRST_", "REVALIDATION"});
     }
 }
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index 85f999b..e15f086 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -1297,6 +1297,29 @@
     }
 
     /**
+     * Flatten a {@code List} containing arbitrary {@code Parcelable} objects into this parcel
+     * at the current position. They can later be retrieved using
+     * {@link #readParcelableList(List, ClassLoader)} if required.
+     *
+     * @see #readParcelableList(List, ClassLoader)
+     * @hide
+     */
+    public final <T extends Parcelable> void writeParcelableList(List<T> val, int flags) {
+        if (val == null) {
+            writeInt(-1);
+            return;
+        }
+
+        int N = val.size();
+        int i=0;
+        writeInt(N);
+        while (i < N) {
+            writeParcelable(val.get(i), flags);
+            i++;
+        }
+    }
+
+    /**
      * Flatten a heterogeneous array containing a particular object type into
      * the parcel, at
      * the current dataPosition() and growing dataCapacity() if needed.  The
@@ -2244,9 +2267,6 @@
      * Read into the given List items IBinder objects that were written with
      * {@link #writeBinderList} at the current dataPosition().
      *
-     * @return A newly created ArrayList containing strings with the same data
-     *         as those that were previously written.
-     *
      * @see #writeBinderList
      */
     public final void readBinderList(List<IBinder> list) {
@@ -2265,6 +2285,34 @@
     }
 
     /**
+     * Read the list of {@code Parcelable} objects at the current data position into the
+     * given {@code list}. The contents of the {@code list} are replaced. If the serialized
+     * list was {@code null}, {@code list} is cleared.
+     *
+     * @see #writeParcelableList(List, int)
+     * @hide
+     */
+    public final <T extends Parcelable> void readParcelableList(List<T> list, ClassLoader cl) {
+        final int N = readInt();
+        if (N == -1) {
+            list.clear();
+            return;
+        }
+
+        final int M = list.size();
+        int i = 0;
+        for (; i < M && i < N; i++) {
+            list.set(i, (T) readParcelable(cl));
+        }
+        for (; i<N; i++) {
+            list.add((T) readParcelable(cl));
+        }
+        for (; i<M; i++) {
+            list.remove(N);
+        }
+    }
+
+    /**
      * Read and return a new array containing a particular object type from
      * the parcel at the current dataPosition().  Returns null if the
      * previously written array was null.  The array <em>must</em> have
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 4ab5d65..0d3b328 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -187,6 +187,8 @@
      * Specifies if a user is disallowed from configuring bluetooth.
      * This does <em>not</em> restrict the user from turning bluetooth on or off.
      * The default value is <code>false</code>.
+     * <p>This restriction doesn't prevent the user from using bluetooth. For disallowing usage of
+     * bluetooth completely on the device, use {@link #DISALLOW_BLUETOOTH}.
      * <p>This restriction has no effect in a managed profile.
      *
      * <p>Key for user restrictions.
@@ -198,6 +200,20 @@
     public static final String DISALLOW_CONFIG_BLUETOOTH = "no_config_bluetooth";
 
     /**
+     * Specifies if bluetooth is disallowed on the device.
+     *
+     * <p> This restriction can only be set by the device owner and the profile owner on the
+     * primary user and it applies globally - i.e. it disables bluetooth on the entire device.
+     * <p>The default value is <code>false</code>.
+     * <p>Key for user restrictions.
+     * <p>Type: Boolean
+     * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
+     * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
+     * @see #getUserRestrictions()
+     */
+    public static final String DISALLOW_BLUETOOTH = "no_bluetooth";
+
+    /**
      * Specifies if a user is disallowed from transferring files over
      * USB. This can only be set by device owners and profile owners on the primary user.
      * The default value is <code>false</code>.
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 0946906..b5c7f7b 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7741,6 +7741,13 @@
        public static final String WIFI_SCAN_ALWAYS_AVAILABLE =
                 "wifi_scan_always_enabled";
 
+        /**
+         * Value to specify if Wi-Fi Wakeup feature is enabled.
+         * @hide
+         */
+        @SystemApi
+        public static final String WIFI_WAKEUP_ENABLED = "wifi_wakeup_enabled";
+
        /**
         * Settings to allow BLE scans to be enabled even when Bluetooth is turned off for
         * connectivity.
diff --git a/core/java/android/util/jar/StrictJarVerifier.java b/core/java/android/util/jar/StrictJarVerifier.java
index cb71ecc..debc170 100644
--- a/core/java/android/util/jar/StrictJarVerifier.java
+++ b/core/java/android/util/jar/StrictJarVerifier.java
@@ -17,7 +17,7 @@
 
 package android.util.jar;
 
-import java.io.ByteArrayInputStream;
+import android.util.apk.ApkSignatureSchemeV2Verifier;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
@@ -33,13 +33,9 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.Set;
 import java.util.StringTokenizer;
 import java.util.jar.Attributes;
 import java.util.jar.JarFile;
-import android.util.ArraySet;
-import android.util.apk.ApkSignatureSchemeV2Verifier;
-import libcore.io.Base64;
 import sun.security.jca.Providers;
 import sun.security.pkcs.PKCS7;
 import sun.security.pkcs.SignerInfo;
@@ -139,7 +135,7 @@
          */
         void verify() {
             byte[] d = digest.digest();
-            if (!MessageDigest.isEqual(d, Base64.decode(hash))) {
+            if (!verifyMessageDigest(d, hash)) {
                 throw invalidDigest(JarFile.MANIFEST_NAME, name, name);
             }
             verifiedEntries.put(name, certChains);
@@ -490,12 +486,22 @@
                 md.update(data, start, end - start);
             }
             byte[] b = md.digest();
-            byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);
-            return MessageDigest.isEqual(b, Base64.decode(hashBytes));
+            byte[] encodedHashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);
+            return verifyMessageDigest(b, encodedHashBytes);
         }
         return ignorable;
     }
 
+    private static boolean verifyMessageDigest(byte[] expected, byte[] encodedActual) {
+        byte[] actual;
+        try {
+            actual = java.util.Base64.getDecoder().decode(encodedActual);
+        } catch (IllegalArgumentException e) {
+            return false;
+        }
+        return MessageDigest.isEqual(expected, actual);
+    }
+
     /**
      * Returns all of the {@link java.security.cert.Certificate} chains that
      * were used to verify the signature on the JAR entry called
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index 2c13831..e04c613 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -301,6 +301,7 @@
     private boolean mDestroyed;
 
     private boolean mOverlayWithDecorCaptionEnabled = false;
+    private boolean mCloseOnSwipeEnabled = false;
 
     // The current window attributes.
     private final WindowManager.LayoutParams mWindowAttributes =
@@ -2209,4 +2210,21 @@
      * @hide
      */
     public abstract void reportActivityRelaunched();
+
+    /**
+     * Called to set flag to check if the close on swipe is enabled. This will only function if
+     * FEATURE_SWIPE_TO_DISMISS has been set.
+     * @hide
+     */
+    public void setCloseOnSwipeEnabled(boolean closeOnSwipeEnabled) {
+        mCloseOnSwipeEnabled = closeOnSwipeEnabled;
+    }
+
+    /**
+     * @return {@code true} if the close on swipe is enabled.
+     * @hide
+     */
+    public boolean isCloseOnSwipeEnabled() {
+        return mCloseOnSwipeEnabled;
+    }
 }
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index b8408dd..aa7631d 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -922,8 +922,11 @@
          * unlock credential) than the user will still need to confirm it before
          * seeing this window, unless {@link #FLAG_SHOW_WHEN_LOCKED} has
          * also been set.
-         * @see KeyguardManager#dismissKeyguard
+         * @deprecated Use {@link #FLAG_SHOW_WHEN_LOCKED} or {@link KeyguardManager#dismissKeyguard}
+         * instead. The Keyguard should never be dismissed automatically repeatedly as it also
+         * guards against unintentional touches.
          */
+        @Deprecated
         public static final int FLAG_DISMISS_KEYGUARD = 0x00400000;
 
         /** Window flag: when set the window will accept for touch events
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 2a004cfb..8c682b9 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -2502,6 +2502,7 @@
         // System.out.println("Features: 0x" + Integer.toHexString(features));
         if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
             layoutResource = R.layout.screen_swipe_dismiss;
+            setCloseOnSwipeEnabled(true);
         } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
             if (mIsFloating) {
                 TypedValue res = new TypedValue();
@@ -2573,7 +2574,7 @@
         }
 
         if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
-            registerSwipeCallbacks();
+            registerSwipeCallbacks(contentParent);
         }
 
         // Remaining setup -- of background and title -- that only applies
@@ -2987,9 +2988,12 @@
         return (mRightIconView = (ImageView)findViewById(R.id.right_icon));
     }
 
-    private void registerSwipeCallbacks() {
-        SwipeDismissLayout swipeDismiss =
-                (SwipeDismissLayout) findViewById(R.id.content);
+    private void registerSwipeCallbacks(ViewGroup contentParent) {
+        if (!(contentParent instanceof SwipeDismissLayout)) {
+            Log.w(TAG, "contentParent is not a SwipeDismissLayout: " + contentParent);
+            return;
+        }
+        SwipeDismissLayout swipeDismiss = (SwipeDismissLayout) contentParent;
         swipeDismiss.setOnDismissedListener(new SwipeDismissLayout.OnDismissedListener() {
             @Override
             public void onDismissed(SwipeDismissLayout layout) {
@@ -3028,6 +3032,16 @@
                 });
     }
 
+    /** @hide */
+    @Override
+    public void setCloseOnSwipeEnabled(boolean closeOnSwipeEnabled) {
+        if (hasFeature(Window.FEATURE_SWIPE_TO_DISMISS) // swipe-to-dismiss feature is requested
+                && mContentParent instanceof SwipeDismissLayout) { // check casting mContentParent
+            ((SwipeDismissLayout) mContentParent).setDismissable(closeOnSwipeEnabled);
+        }
+        super.setCloseOnSwipeEnabled(closeOnSwipeEnabled);
+    }
+
     /**
      * Helper method for calling the {@link Callback#onPanelClosed(int, Menu)}
      * callback. This method will grab whatever extra state is needed for the
diff --git a/core/java/com/android/internal/widget/SwipeDismissLayout.java b/core/java/com/android/internal/widget/SwipeDismissLayout.java
index d88f479..31e67bd 100644
--- a/core/java/com/android/internal/widget/SwipeDismissLayout.java
+++ b/core/java/com/android/internal/widget/SwipeDismissLayout.java
@@ -110,6 +110,8 @@
 
     private float mLastX;
 
+    private boolean mDismissable = true;
+
     public SwipeDismissLayout(Context context) {
         super(context);
         init(context);
@@ -166,6 +168,10 @@
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (!mDismissable) {
+            return super.onInterceptTouchEvent(ev);
+        }
+
         // offset because the view is translated during swipe
         ev.offsetLocation(mTranslationX, 0);
 
@@ -225,7 +231,7 @@
 
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
-        if (mVelocityTracker == null) {
+        if (mVelocityTracker == null || !mDismissable) {
             return super.onTouchEvent(ev);
         }
         // offset because the view is translated during swipe
@@ -363,4 +369,13 @@
 
         return checkV && v.canScrollHorizontally((int) -dx);
     }
+
+    public void setDismissable(boolean dismissable) {
+        if (!dismissable && mDismissable) {
+            cancel();
+            resetMembers();
+        }
+
+        mDismissable = dismissable;
+    }
 }
diff --git a/core/jni/android_os_HwBinder.cpp b/core/jni/android_os_HwBinder.cpp
index 10090a1..23a988a 100644
--- a/core/jni/android_os_HwBinder.cpp
+++ b/core/jni/android_os_HwBinder.cpp
@@ -25,6 +25,8 @@
 
 #include <JNIHelp.h>
 #include <android/hidl/manager/1.0/IServiceManager.h>
+#include <android/hidl/base/1.0/IBase.h>
+#include <android/hidl/base/1.0/IHwBase.h>
 #include <android_runtime/AndroidRuntime.h>
 #include <hidl/ServiceManagement.h>
 #include <hidl/Status.h>
@@ -239,11 +241,11 @@
     using android::hidl::manager::V1_0::IServiceManager;
 
     sp<hardware::IBinder> binder = JHwBinder::GetNativeContext(env, thiz);
-
+    sp<hidl::base::V1_0::IBase> base = hidl::base::V1_0::IHwBase::asInterface(binder);
     bool ok = hardware::defaultServiceManager()->add(
                 interfaceChain,
                 serviceName,
-                binder);
+                base);
 
     env->ReleaseStringUTFChars(serviceNameObj, serviceName);
     serviceName = NULL;
@@ -289,8 +291,10 @@
     hardware::defaultServiceManager()->get(
             ifaceName,
             serviceName,
-            [&service](sp<hardware::IBinder> out) {
-                service = out;
+            [&service](sp<hidl::base::V1_0::IBase> out) {
+                service = hardware::toBinder<
+                        hidl::base::V1_0::IBase, hidl::base::V1_0::IHwBase
+                    >(out);
             });
 
     env->ReleaseStringUTFChars(ifaceNameObj, ifaceName);
diff --git a/core/res/res/layout/date_picker_material.xml b/core/res/res/layout/date_picker_material.xml
index 763f2a4..dd8a45d2 100644
--- a/core/res/res/layout/date_picker_material.xml
+++ b/core/res/res/layout/date_picker_material.xml
@@ -25,10 +25,16 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content" />
 
-    <include
-        layout="@layout/date_picker_view_animator_material"
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="1" />
+    <ScrollView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:fadeScrollbars="false"
+        android:scrollIndicators="top|bottom">
+        <include
+            layout="@layout/date_picker_view_animator_material"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1" />
+    </ScrollView>
 
 </LinearLayout>
diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java
index bbb7184..52d5b7ca 100644
--- a/media/java/android/media/ExifInterface.java
+++ b/media/java/android/media/ExifInterface.java
@@ -2765,23 +2765,28 @@
                         tag != null ? tag.name : null, dataFormat, numberOfComponents));
             }
 
-            if (tag == null || dataFormat <= 0 ||
-                    dataFormat >= IFD_FORMAT_BYTES_PER_FORMAT.length) {
-                // Skip if the parsed tag number is not defined or invalid data format.
-                if (DEBUG) {
-                    if (tag == null) {
-                        Log.w(TAG, "Skip tag entry since tag number is not defined: " + tagNumber);
-                    } else {
-                        Log.w(TAG, "Skip tag entry since data format is invalid: " + dataFormat);
-                    }
+            long byteCount = 0;
+            boolean valid = false;
+            if (tag == null) {
+                Log.w(TAG, "Skip the tag entry since tag number is not defined: " + tagNumber);
+            } else if (dataFormat <= 0 || dataFormat >= IFD_FORMAT_BYTES_PER_FORMAT.length) {
+                Log.w(TAG, "Skip the tag entry since data format is invalid: " + dataFormat);
+            } else {
+                byteCount = (long) numberOfComponents * IFD_FORMAT_BYTES_PER_FORMAT[dataFormat];
+                if (byteCount < 0 || byteCount > Integer.MAX_VALUE) {
+                    Log.w(TAG, "Skip the tag entry since the number of components is invalid: "
+                            + numberOfComponents);
+                } else {
+                    valid = true;
                 }
+            }
+            if (!valid) {
                 dataInputStream.seek(nextEntryOffset);
                 continue;
             }
 
             // Read a value from data field or seek to the value offset which is stored in data
             // field if the size of the entry value is bigger than 4.
-            int byteCount = numberOfComponents * IFD_FORMAT_BYTES_PER_FORMAT[dataFormat];
             if (byteCount > 4) {
                 int offset = dataInputStream.readInt();
                 if (DEBUG) {
@@ -2871,7 +2876,7 @@
                 continue;
             }
 
-            byte[] bytes = new byte[byteCount];
+            byte[] bytes = new byte[(int) byteCount];
             dataInputStream.readFully(bytes);
             ExifAttribute attribute = new ExifAttribute(dataFormat, numberOfComponents, bytes);
             mAttributes[ifdType].put(tag.name, attribute);
diff --git a/packages/PrintRecommendationService/res/values/donottranslate.xml b/packages/PrintRecommendationService/res/values/donottranslate.xml
index e9b97a3..2cce56d8a 100644
--- a/packages/PrintRecommendationService/res/values/donottranslate.xml
+++ b/packages/PrintRecommendationService/res/values/donottranslate.xml
@@ -32,13 +32,6 @@
         <item>Hewlett Packard</item>
     </string-array>
 
-    <!-- Samsung plugin -->
-    <string-array name="known_print_vendor_info_for_samsung" translatable="false">
-        <item>com.sec.app.samsungprintservice</item>
-        <item>Samsung Electronics</item>
-        <item>Samsung</item>
-    </string-array>
-
     <!-- Xerox plugin -->
     <string-array name="known_print_vendor_info_for_xerox" translatable="false">
         <item>com.xerox.printservice</item>
@@ -49,6 +42,8 @@
     <array name="known_print_plugin_vendors" translatable="false">
         <item>@array/known_print_vendor_info_for_mopria</item>
         <item>@array/known_print_vendor_info_for_hp</item>
-        <item>@array/known_print_vendor_info_for_samsung</item>
     </array>
+
+    <!-- Samsung plugin -->
+    <string name="plugin_package_samsung">com.sec.app.samsungprintservice</string>
 </resources>
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java
index 3eedefd..d048396 100644
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java
+++ b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java
@@ -78,7 +78,7 @@
 
         try {
             mPlugins.add(new RemotePrintServicePlugin(new SamsungRecommendationPlugin(this), this,
-                    false));
+                    true));
         } catch (Exception e) {
             Log.e(LOG_TAG, "Could not initiate " + getString(R.string.plugin_vendor_samsung) +
                     " plugin", e);
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/mdnsFilter/MDNSFilterPlugin.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/mdnsFilter/MDNSFilterPlugin.java
index a2c0485..d60a25f 100644
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/mdnsFilter/MDNSFilterPlugin.java
+++ b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/mdnsFilter/MDNSFilterPlugin.java
@@ -16,30 +16,52 @@
 
 package com.android.printservice.recommendation.plugin.mdnsFilter;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.StringRes;
 import android.content.Context;
-import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
-import android.util.Log;
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.Preconditions;
+import android.annotation.NonNull;
+import android.annotation.StringRes;
+
 import com.android.printservice.recommendation.PrintServicePlugin;
-import com.android.printservice.recommendation.util.DiscoveryListenerMultiplexer;
-import com.android.printservice.recommendation.util.NsdResolveQueue;
+import com.android.printservice.recommendation.util.MDNSFilteredDiscovery;
+import com.android.printservice.recommendation.util.MDNSUtils;
 
 import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 /**
  * A plugin listening for mDNS results and only adding the ones that {@link
  * MDNSUtils#isVendorPrinter match} configured list
  */
-public class MDNSFilterPlugin implements PrintServicePlugin, NsdManager.DiscoveryListener {
-    private static final String LOG_TAG = "MDNSFilterPlugin";
+public class MDNSFilterPlugin implements PrintServicePlugin {
 
-    private static final String PRINTER_SERVICE_TYPE = "_ipp._tcp";
+    /** The mDNS service types supported */
+    private static final Set<String> PRINTER_SERVICE_TYPES = new HashSet<String>() {{
+        add("_ipp._tcp");
+    }};
+
+    /**
+     * The printer filter for {@link MDNSFilteredDiscovery} passing only mDNS results
+     * that {@link MDNSUtils#isVendorPrinter match} configured list
+     */
+    private static class VendorNameFilter implements MDNSFilteredDiscovery.PrinterFilter {
+        /** mDNS names handled by the print service this plugin is for */
+        private final @NonNull Set<String> mMDNSNames;
+
+        /**
+         * Filter constructor
+         *
+         * @param vendorNames The vendor names to pass
+         */
+        VendorNameFilter(@NonNull Set<String> vendorNames) {
+            mMDNSNames = new HashSet<>(vendorNames);
+        }
+
+        @Override
+        public boolean matchesCriteria(NsdServiceInfo nsdServiceInfo) {
+            return MDNSUtils.isVendorPrinter(nsdServiceInfo, mMDNSNames);
+        }
+    }
 
     /** Name of the print service this plugin is for */
     private final @StringRes int mName;
@@ -47,26 +69,8 @@
     /** Package name of the print service this plugin is for */
     private final @NonNull CharSequence mPackageName;
 
-    /** mDNS names handled by the print service this plugin is for */
-    private final @NonNull HashSet<String> mMDNSNames;
-
-    /** Printer identifiers of the mPrinters found. */
-    @GuardedBy("mLock")
-    private final @NonNull HashSet<String> mPrinters;
-
-    /** Context of the user of this plugin */
-    private final @NonNull Context mContext;
-
-    /**
-     * Call back to report the number of mPrinters found.
-     *
-     * We assume that {@link #start} and {@link #stop} are never called in parallel, hence it is
-     * safe to not synchronize access to this field.
-     */
-    private @Nullable PrinterDiscoveryCallback mCallback;
-
-    /** Queue used to resolve nsd infos */
-    private final @NonNull NsdResolveQueue mResolveQueue;
+    /** The mDNS filtered discovery */
+    private final MDNSFilteredDiscovery mMDNSFilteredDiscovery;
 
     /**
      * Create new stub that assumes that a print service can be used to print on all mPrinters
@@ -79,16 +83,11 @@
      */
     public MDNSFilterPlugin(@NonNull Context context, @NonNull String name,
             @NonNull CharSequence packageName, @NonNull List<String> mDNSNames) {
-        mContext = Preconditions.checkNotNull(context, "context");
-        mName = mContext.getResources().getIdentifier(Preconditions.checkStringNotEmpty(name,
-                "name"), null, "com.android.printservice.recommendation");
-        mPackageName = Preconditions.checkStringNotEmpty(packageName);
-        mMDNSNames = new HashSet<>(Preconditions
-                .checkCollectionNotEmpty(Preconditions.checkCollectionElementsNotNull(mDNSNames,
-                        "mDNSNames"), "mDNSNames"));
-
-        mResolveQueue = NsdResolveQueue.getInstance();
-        mPrinters = new HashSet<>();
+        mName = context.getResources().getIdentifier(name, null,
+                "com.android.printservice.recommendation");
+        mPackageName = packageName;
+        mMDNSFilteredDiscovery = new MDNSFilteredDiscovery(context, PRINTER_SERVICE_TYPES,
+                new VendorNameFilter(new HashSet<>(mDNSNames)));
     }
 
     @Override
@@ -96,18 +95,9 @@
         return mPackageName;
     }
 
-    /**
-     * @return The NDS manager
-     */
-    private NsdManager getNDSManager() {
-        return (NsdManager) mContext.getSystemService(Context.NSD_SERVICE);
-    }
-
     @Override
     public void start(@NonNull PrinterDiscoveryCallback callback) throws Exception {
-        mCallback = callback;
-
-        DiscoveryListenerMultiplexer.addListener(getNDSManager(), PRINTER_SERVICE_TYPE, this);
+        mMDNSFilteredDiscovery.start(callback);
     }
 
     @Override
@@ -117,82 +107,6 @@
 
     @Override
     public void stop() throws Exception {
-        mCallback.onChanged(0);
-        mCallback = null;
-
-        DiscoveryListenerMultiplexer.removeListener(getNDSManager(), this);
-    }
-
-    @Override
-    public void onStartDiscoveryFailed(String serviceType, int errorCode) {
-        Log.w(LOG_TAG, "Failed to start network discovery for type " + serviceType + ": "
-                + errorCode);
-    }
-
-    @Override
-    public void onStopDiscoveryFailed(String serviceType, int errorCode) {
-        Log.w(LOG_TAG, "Failed to stop network discovery for type " + serviceType + ": "
-                + errorCode);
-    }
-
-    @Override
-    public void onDiscoveryStarted(String serviceType) {
-        // empty
-    }
-
-    @Override
-    public void onDiscoveryStopped(String serviceType) {
-        mPrinters.clear();
-    }
-
-    @Override
-    public void onServiceFound(NsdServiceInfo serviceInfo) {
-        mResolveQueue.resolve(getNDSManager(), serviceInfo,
-                new NsdManager.ResolveListener() {
-            @Override
-            public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
-                Log.w(LOG_TAG, "Service found: could not resolve " + serviceInfo + ": " +
-                        errorCode);
-            }
-
-            @Override
-            public void onServiceResolved(NsdServiceInfo serviceInfo) {
-                if (MDNSUtils.isVendorPrinter(serviceInfo, mMDNSNames)) {
-                    if (mCallback != null) {
-                        boolean added = mPrinters.add(serviceInfo.getHost().getHostAddress());
-
-                        if (added) {
-                            mCallback.onChanged(mPrinters.size());
-                        }
-                    }
-                }
-            }
-        });
-    }
-
-    @Override
-    public void onServiceLost(NsdServiceInfo serviceInfo) {
-        mResolveQueue.resolve(getNDSManager(), serviceInfo,
-                new NsdManager.ResolveListener() {
-            @Override
-            public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
-                Log.w(LOG_TAG, "Service lost: Could not resolve " + serviceInfo + ": "
-                        + errorCode);
-            }
-
-            @Override
-            public void onServiceResolved(NsdServiceInfo serviceInfo) {
-                if (MDNSUtils.isVendorPrinter(serviceInfo, mMDNSNames)) {
-                    if (mCallback != null) {
-                        boolean removed = mPrinters
-                                .remove(serviceInfo.getHost().getHostAddress());
-
-                        if (removed) {
-                            mCallback.onChanged(mPrinters.size());
-                        }
-                    }
-                }
-            }
-        });
+        mMDNSFilteredDiscovery.stop();
     }
 }
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/MDnsUtils.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/MDnsUtils.java
deleted file mode 100644
index 963e09b..0000000
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/MDnsUtils.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.printservice.recommendation.plugin.samsung;
-
-import android.net.nsd.NsdServiceInfo;
-import android.text.TextUtils;
-
-import java.nio.charset.StandardCharsets;
-import java.util.Locale;
-import java.util.Map;
-
-public class MDnsUtils {
-    public static final String ATTRIBUTE__TY = "ty";
-    public static final String ATTRIBUTE__PRODUCT = "product";
-    public static final String ATTRIBUTE__USB_MFG = "usb_MFG";
-    public static final String ATTRIBUTE__MFG = "mfg";
-
-    public static String getString(byte[] value) {
-        if (value != null) return new String(value,StandardCharsets.UTF_8);
-        return null;
-    }
-
-    public static boolean isVendorPrinter(NsdServiceInfo networkDevice, String[] vendorValues) {
-
-        Map<String,byte[]> attributes = networkDevice.getAttributes();
-        String product = getString(attributes.get(ATTRIBUTE__PRODUCT));
-        String ty = getString(attributes.get(ATTRIBUTE__TY));
-        String usbMfg = getString(attributes.get(ATTRIBUTE__USB_MFG));
-        String mfg = getString(attributes.get(ATTRIBUTE__MFG));
-        return containsVendor(product, vendorValues) || containsVendor(ty, vendorValues) || containsVendor(usbMfg, vendorValues) || containsVendor(mfg, vendorValues);
-
-    }
-
-    public static String getVendor(NsdServiceInfo networkDevice) {
-        String vendor;
-
-        Map<String,byte[]> attributes = networkDevice.getAttributes();
-        vendor = getString(attributes.get(ATTRIBUTE__MFG));
-        if (!TextUtils.isEmpty(vendor)) return vendor;
-        vendor = getString(attributes.get(ATTRIBUTE__USB_MFG));
-        if (!TextUtils.isEmpty(vendor)) return vendor;
-
-        return null;
-    }
-
-    private static boolean containsVendor(String container, String[] vendorValues) {
-        if ((container == null) || (vendorValues == null)) return false;
-        for (String value : vendorValues) {
-            if (containsString(container, value)
-                || containsString(container.toLowerCase(Locale.US), value.toLowerCase(Locale.US))
-                || containsString(container.toUpperCase(Locale.US), value.toUpperCase(Locale.US)))
-                return true;
-        }
-        return false;
-    }
-
-    private static boolean containsString(String container, String contained) {
-        return (container != null) && (contained != null) && (container.equalsIgnoreCase(contained) || container.contains(contained + " "));
-    }
-}
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/PrinterFilterMopria.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/PrinterFilterMopria.java
new file mode 100644
index 0000000..d03bb1d
--- /dev/null
+++ b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/PrinterFilterMopria.java
@@ -0,0 +1,63 @@
+/*
+ * (c) Copyright 2016 Samsung Electronics
+ * (c) Copyright 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.printservice.recommendation.plugin.samsung;
+
+import android.net.nsd.NsdServiceInfo;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.printservice.recommendation.util.MDNSFilteredDiscovery;
+import com.android.printservice.recommendation.util.MDNSUtils;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Printer filter for Mopria printer models supported by the print service plugin
+ */
+class PrinterFilterMopria implements MDNSFilteredDiscovery.PrinterFilter {
+    private static final String TAG = "PrinterFilterMopria";
+
+    static final Set<String> MOPRIA_MDNS_SERVICES = new HashSet<String>() {{
+        add("_ipp._tcp");
+        add("_ipps._tcp");
+    }};
+
+    private static final String PDL__PDF = "application/pdf";
+    private static final String PDL__PCLM = "application/PCLm";
+    private static final String PDL__PWG_RASTER = "image/pwg-raster";
+
+    private static final String PDL_ATTRIBUTE = "pdl";
+
+    @Override
+    public boolean matchesCriteria(NsdServiceInfo nsdServiceInfo) {
+        if (!MDNSUtils.isSupportedServiceType(nsdServiceInfo, MOPRIA_MDNS_SERVICES)) {
+            return false;
+        }
+
+        String pdls = MDNSUtils.getString(nsdServiceInfo.getAttributes().get(PDL_ATTRIBUTE));
+        boolean isMatch = !TextUtils.isEmpty(pdls)
+                && (pdls.contains(PDL__PDF)
+                || pdls.contains(PDL__PCLM)
+                || pdls.contains(PDL__PWG_RASTER));
+
+        if (isMatch) {
+            Log.d(TAG, "Mopria printer found: " + nsdServiceInfo.getServiceName());
+        }
+        return isMatch;
+    }
+}
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/PrinterFilterSamsung.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/PrinterFilterSamsung.java
new file mode 100644
index 0000000..5b049ef
--- /dev/null
+++ b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/PrinterFilterSamsung.java
@@ -0,0 +1,117 @@
+/*
+ * (c) Copyright 2016 Samsung Electronics
+ * (c) Copyright 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.printservice.recommendation.plugin.samsung;
+
+import android.net.nsd.NsdServiceInfo;
+import android.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.printservice.recommendation.util.MDNSFilteredDiscovery;
+import com.android.printservice.recommendation.util.MDNSUtils;
+
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Printer filter for Samsung printer models supported by the print service plugin
+ */
+class PrinterFilterSamsung implements MDNSFilteredDiscovery.PrinterFilter {
+    private static final String TAG = "PrinterFilterSamsung";
+
+    static final Set<String> SAMSUNG_MDNS_SERVICES = new HashSet<String>() {{
+        add("_pdl-datastream._tcp");
+    }};
+
+    private static final String[] NOT_SUPPORTED_MODELS = new String[]{
+            "SCX-5x15",
+            "SF-555P",
+            "CF-555P",
+            "SCX-4x16",
+            "SCX-4214F",
+            "CLP-500",
+            "CJX-",
+            "MJC-"
+    };
+    private static final String ATTR_USB_MFG = "usb_MFG";
+    private static final String ATTR_MFG = "mfg";
+    private static final String ATTR_USB_MDL = "usb_MDL";
+    private static final String ATTR_MDL = "mdl";
+    private static final String ATTR_PRODUCT = "product";
+    private static final String ATTR_TY = "ty";
+
+    private static Set<String> SAMUNG_VENDOR_SET = new HashSet<String>() {{
+        add("samsung");
+    }};
+
+    @Override
+    public boolean matchesCriteria(NsdServiceInfo nsdServiceInfo) {
+        if (!MDNSUtils.isSupportedServiceType(nsdServiceInfo, SAMSUNG_MDNS_SERVICES)) {
+            return false;
+        }
+
+        if (!MDNSUtils.isVendorPrinter(nsdServiceInfo, SAMUNG_VENDOR_SET)) {
+            return false;
+        }
+
+        String modelName = getSamsungModelName(nsdServiceInfo);
+        if (modelName != null && isSupportedSamsungModel(modelName)) {
+            Log.d(TAG, "Samsung printer found: " + nsdServiceInfo.getServiceName());
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isSupportedSamsungModel(String model) {
+        if (!TextUtils.isEmpty(model)) {
+            String modelToUpper = model.toUpperCase(Locale.US);
+            for (String unSupportedPrinter : NOT_SUPPORTED_MODELS) {
+                if (modelToUpper.contains(unSupportedPrinter)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    private String getSamsungModelName(@NonNull NsdServiceInfo resolvedDevice) {
+        Map<String,byte[]> attributes = resolvedDevice.getAttributes();
+        String usb_mfg = MDNSUtils.getString(attributes.get(ATTR_USB_MFG));
+        if (TextUtils.isEmpty(usb_mfg)) {
+            usb_mfg = MDNSUtils.getString(attributes.get(ATTR_MFG));
+        }
+
+        String usb_mdl = MDNSUtils.getString(attributes.get(ATTR_USB_MDL));
+        if (TextUtils.isEmpty(usb_mdl)) {
+            usb_mdl = MDNSUtils.getString(attributes.get(ATTR_MDL));
+        }
+
+        String modelName;
+        if (!TextUtils.isEmpty(usb_mfg) && !TextUtils.isEmpty(usb_mdl)) {
+            modelName = usb_mfg.trim() + " " + usb_mdl.trim();
+        } else {
+            modelName = MDNSUtils.getString(attributes.get(ATTR_PRODUCT));
+            if (TextUtils.isEmpty(modelName)) {
+                modelName = MDNSUtils.getString(attributes.get(ATTR_TY));
+            }
+        }
+
+        return modelName;
+    }
+}
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/PrinterHashMap.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/PrinterHashMap.java
deleted file mode 100644
index 032fe22..0000000
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/PrinterHashMap.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.printservice.recommendation.plugin.samsung;
-
-import android.net.nsd.NsdServiceInfo;
-
-import java.util.HashMap;
-
-final class PrinterHashMap extends HashMap<String, NsdServiceInfo> {
-    public static String getKey(NsdServiceInfo serviceInfo) {
-        return serviceInfo.getServiceName();
-    }
-    public NsdServiceInfo addPrinter(NsdServiceInfo device) {
-        return put(getKey(device), device);
-    }
-    public NsdServiceInfo removePrinter(NsdServiceInfo device) {
-        return remove(getKey(device));
-    }
-}
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/SamsungRecommendationPlugin.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/SamsungRecommendationPlugin.java
index e5b8a0f..eeb5122 100644
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/SamsungRecommendationPlugin.java
+++ b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/SamsungRecommendationPlugin.java
@@ -1,102 +1,69 @@
-/*

-(c) Copyright 2016 Samsung Electronics..

-

-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.android.printservice.recommendation.plugin.samsung;

-

-import android.content.Context;

-import android.net.nsd.NsdServiceInfo;

-import android.text.TextUtils;

-

-import java.util.Locale;

-import java.util.Map;

-

-import com.android.printservice.recommendation.R;

-

-public class SamsungRecommendationPlugin extends ServiceRecommendationPlugin {

-

-    private static final String TAG = "SamsungRecommendation";

-

-    private static final String ATTR_USB_MFG = "usb_MFG";

-    private static final String ATTR_MFG = "mfg";

-    private static final String ATTR_USB_MDL = "usb_MDL";

-    private static final String ATTR_MDL = "mdl";

-    private static final String ATTR_PRODUCT = "product";

-    private static final String ATTR_TY = "ty";

-

-    private static String[] mNotSupportedDevices = new String[]{

-            "SCX-5x15",

-            "SF-555P",

-            "CF-555P",

-            "SCX-4x16",

-            "SCX-4214F",

-            "CLP-500",

-            "CJX-",

-            "MJC-"

-    };

-

-    private static boolean isSupportedModel(String model) {

-        if (!TextUtils.isEmpty(model)) {

-            String modelToUpper = model.toUpperCase(Locale.US);

-            for (String unSupportedPrinter : mNotSupportedDevices) {

-                if (modelToUpper.contains(unSupportedPrinter)) {

-                    return  false;

-                }

-            }

-        }

-        return true;

-    }

-

-    public SamsungRecommendationPlugin(Context context) {

-        super(context, R.string.plugin_vendor_samsung, new VendorInfo(context.getResources(), R.array.known_print_vendor_info_for_samsung), new String[]{"_pdl-datastream._tcp"});

-    }

-

-    @Override

-    public boolean matchesCriteria(String vendor, NsdServiceInfo nsdServiceInfo) {

-        if (!TextUtils.equals(vendor, mVendorInfo.mVendorID)) return false;

-

-        String modelName = getModelName(nsdServiceInfo);

-        if (modelName != null) {

-            return (isSupportedModel(modelName));

-        }

-        return false;

-    }

-

-    private String getModelName(NsdServiceInfo resolvedDevice) {

-        Map<String,byte[]> attributes = resolvedDevice.getAttributes();

-        String usb_mfg = MDnsUtils.getString(attributes.get(ATTR_USB_MFG));

-        if (TextUtils.isEmpty(usb_mfg)) {

-            usb_mfg = MDnsUtils.getString(attributes.get(ATTR_MFG));

-        }

-

-        String usb_mdl = MDnsUtils.getString(attributes.get(ATTR_USB_MDL));

-        if (TextUtils.isEmpty(usb_mdl)) {

-            usb_mdl = MDnsUtils.getString(attributes.get(ATTR_MDL));

-        }

-

-        String modelName = null;

-        if (!TextUtils.isEmpty(usb_mfg) && !TextUtils.isEmpty(usb_mdl)) {

-            modelName = usb_mfg.trim() + " " + usb_mdl.trim();

-        } else {

-            modelName = MDnsUtils.getString(attributes.get(ATTR_PRODUCT));

-            if (TextUtils.isEmpty(modelName)) {

-                modelName = MDnsUtils.getString(attributes.get(ATTR_TY));

-            }

-        }

-

-        return modelName;

-    }

-}

+/*
+ * (c) Copyright 2016 Samsung Electronics
+ * (c) Copyright 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.printservice.recommendation.plugin.samsung;
+
+import android.content.Context;
+import android.net.nsd.NsdServiceInfo;
+import android.annotation.NonNull;
+
+import com.android.printservice.recommendation.PrintServicePlugin;
+import com.android.printservice.recommendation.R;
+import com.android.printservice.recommendation.util.MDNSFilteredDiscovery;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class SamsungRecommendationPlugin implements PrintServicePlugin {
+    private static final Set<String> ALL_MDNS_SERVICES = new HashSet<String>() {{
+        addAll(PrinterFilterMopria.MOPRIA_MDNS_SERVICES);
+        addAll(PrinterFilterSamsung.SAMSUNG_MDNS_SERVICES);
+    }};
+
+    private final @NonNull Context mContext;
+    private final @NonNull MDNSFilteredDiscovery mMDNSFilteredDiscovery;
+
+    private final @NonNull PrinterFilterSamsung mPrinterFilterSamsung = new PrinterFilterSamsung();
+    private final @NonNull PrinterFilterMopria mPrinterFilterMopria = new PrinterFilterMopria();
+
+    public SamsungRecommendationPlugin(@NonNull Context context) {
+        mContext = context;
+        mMDNSFilteredDiscovery = new MDNSFilteredDiscovery(context, ALL_MDNS_SERVICES,
+                (NsdServiceInfo nsdServiceInfo) ->
+                        mPrinterFilterSamsung.matchesCriteria(nsdServiceInfo) ||
+                                mPrinterFilterMopria.matchesCriteria(nsdServiceInfo));
+    }
+
+    @Override
+    public int getName() {
+        return R.string.plugin_vendor_samsung;
+    }
+
+    @Override
+    public @NonNull CharSequence getPackageName() {
+        return mContext.getString(R.string.plugin_package_samsung);
+    }
+
+    @Override
+    public void start(@NonNull PrinterDiscoveryCallback callback) throws Exception {
+        mMDNSFilteredDiscovery.start(callback);
+    }
+
+    @Override
+    public void stop() throws Exception {
+        mMDNSFilteredDiscovery.stop();
+    }
+}
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/ServiceListener.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/ServiceListener.java
deleted file mode 100644
index 7bb83c9..0000000
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/ServiceListener.java
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.printservice.recommendation.plugin.samsung;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.net.nsd.NsdManager;
-import android.net.nsd.NsdServiceInfo;
-import android.text.TextUtils;
-import android.util.Pair;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import com.android.printservice.recommendation.R;
-import com.android.printservice.recommendation.util.DiscoveryListenerMultiplexer;
-
-public class ServiceListener implements ServiceResolveQueue.ResolveCallback {
-
-    private final NsdManager mNSDManager;
-    private final Map<String, VendorInfo> mVendorInfoHashMap;
-    private final String[] mServiceType;
-    private final Observer mObserver;
-    private final ServiceResolveQueue mResolveQueue;
-    private List<NsdManager.DiscoveryListener> mListeners = new ArrayList<>();
-    public HashMap<String, PrinterHashMap> mVendorHashMap = new HashMap<>();
-
-    public interface Observer {
-        boolean matchesCriteria(String vendor, NsdServiceInfo serviceInfo);
-        void dataSetChanged();
-    }
-
-    public ServiceListener(Context context, Observer observer, String[] serviceTypes) {
-        mObserver = observer;
-        mServiceType = serviceTypes;
-        mNSDManager = (NsdManager)context.getSystemService(Context.NSD_SERVICE);
-        mResolveQueue = ServiceResolveQueue.getInstance(mNSDManager);
-
-        Map<String, VendorInfo> vendorInfoMap = new HashMap<>();
-        TypedArray testArray = context.getResources().obtainTypedArray(R.array.known_print_plugin_vendors);
-        for(int i = 0; i < testArray.length(); i++) {
-            int arrayID = testArray.getResourceId(i, 0);
-            if (arrayID != 0) {
-                VendorInfo info = new VendorInfo(context.getResources(), arrayID);
-                vendorInfoMap.put(info.mVendorID, info);
-                vendorInfoMap.put(info.mPackageName, info);
-            }
-        }
-        testArray.recycle();
-        mVendorInfoHashMap = vendorInfoMap;
-    }
-
-    @Override
-    public void serviceResolved(NsdServiceInfo nsdServiceInfo) {
-        printerFound(nsdServiceInfo);
-    }
-
-    private synchronized void printerFound(NsdServiceInfo nsdServiceInfo) {
-        if (nsdServiceInfo == null) return;
-        if (TextUtils.isEmpty(PrinterHashMap.getKey(nsdServiceInfo))) return;
-        String vendor = MDnsUtils.getVendor(nsdServiceInfo);
-        if (vendor == null) vendor = "";
-        for(Map.Entry<String,VendorInfo> entry : mVendorInfoHashMap.entrySet()) {
-            for(String vendorValues : entry.getValue().mDNSValues) {
-                if (vendor.equalsIgnoreCase(vendorValues)) {
-                    vendor = entry.getValue().mVendorID;
-                    break;
-                }
-            }
-            // intentional pointer check
-            //noinspection StringEquality
-            if ((vendor != entry.getValue().mVendorID) &&
-                    MDnsUtils.isVendorPrinter(nsdServiceInfo, entry.getValue().mDNSValues)) {
-                vendor = entry.getValue().mVendorID;
-            }
-            // intentional pointer check
-            //noinspection StringEquality
-            if (vendor == entry.getValue().mVendorID) break;
-        }
-
-        if (TextUtils.isEmpty(vendor)) {
-            return;
-        }
-
-        if (!mObserver.matchesCriteria(vendor, nsdServiceInfo))
-            return;
-        boolean mapsChanged;
-
-        PrinterHashMap vendorHash = mVendorHashMap.get(vendor);
-        if (vendorHash == null) {
-            vendorHash = new PrinterHashMap();
-        }
-        mapsChanged = (vendorHash.addPrinter(nsdServiceInfo) == null);
-        mVendorHashMap.put(vendor, vendorHash);
-
-        if (mapsChanged) {
-            mObserver.dataSetChanged();
-        }
-    }
-
-    private synchronized void printerRemoved(NsdServiceInfo nsdServiceInfo) {
-        boolean wasRemoved = false;
-        Set<String> vendors = mVendorHashMap.keySet();
-        for(String vendor : vendors) {
-            PrinterHashMap map = mVendorHashMap.get(vendor);
-            wasRemoved |= (map.removePrinter(nsdServiceInfo) != null);
-            if (map.isEmpty()) wasRemoved |= (mVendorHashMap.remove(vendor) != null);
-        }
-        if (wasRemoved) {
-            mObserver.dataSetChanged();
-        }
-    }
-
-    public void start() {
-        stop();
-        for(final String service :mServiceType) {
-            NsdManager.DiscoveryListener listener = new NsdManager.DiscoveryListener() {
-                @Override
-                public void onStartDiscoveryFailed(String s, int i) {
-
-                }
-
-                @Override
-                public void onStopDiscoveryFailed(String s, int i) {
-
-                }
-
-                @Override
-                public void onDiscoveryStarted(String s) {
-
-                }
-
-                @Override
-                public void onDiscoveryStopped(String s) {
-
-                }
-
-                @Override
-                public void onServiceFound(NsdServiceInfo nsdServiceInfo) {
-                    mResolveQueue.queueRequest(nsdServiceInfo, ServiceListener.this);
-                }
-
-                @Override
-                public void onServiceLost(NsdServiceInfo nsdServiceInfo) {
-                    mResolveQueue.removeRequest(nsdServiceInfo, ServiceListener.this);
-                    printerRemoved(nsdServiceInfo);
-                }
-            };
-            DiscoveryListenerMultiplexer.addListener(mNSDManager, service, listener);
-            mListeners.add(listener);
-        }
-    }
-
-    public void stop() {
-        for(NsdManager.DiscoveryListener listener : mListeners) {
-            DiscoveryListenerMultiplexer.removeListener(mNSDManager, listener);
-        }
-        mVendorHashMap.clear();
-        mListeners.clear();
-    }
-
-    public Pair<Integer, Integer> getCount() {
-        int count = 0;
-        for (PrinterHashMap map : mVendorHashMap.values()) {
-            count += map.size();
-        }
-        return Pair.create(mVendorHashMap.size(), count);
-    }
-}
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/ServiceRecommendationPlugin.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/ServiceRecommendationPlugin.java
deleted file mode 100644
index 9d15f30..0000000
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/ServiceRecommendationPlugin.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.printservice.recommendation.plugin.samsung;
-
-import android.content.Context;
-import android.net.nsd.NsdManager;
-import android.net.nsd.NsdServiceInfo;
-import android.annotation.NonNull;
-import android.text.TextUtils;
-import com.android.printservice.recommendation.PrintServicePlugin;
-
-public abstract class ServiceRecommendationPlugin implements PrintServicePlugin, ServiceListener.Observer {
-
-    protected static final String PDL_ATTRIBUTE = "pdl";
-
-    protected final Object mLock = new Object();
-    protected PrinterDiscoveryCallback mCallback = null;
-    protected final ServiceListener mListener;
-    protected final NsdManager mNSDManager;
-    protected final VendorInfo mVendorInfo;
-    private final int mVendorStringID;
-
-    protected ServiceRecommendationPlugin(Context context, int vendorStringID, VendorInfo vendorInfo, String[] services) {
-        mNSDManager = (NsdManager)context.getSystemService(Context.NSD_SERVICE);
-        mVendorStringID = vendorStringID;
-        mVendorInfo = vendorInfo;
-        mListener = new ServiceListener(context, this, services);
-    }
-
-    @Override
-    public int getName() {
-        return mVendorStringID;
-    }
-
-    @NonNull
-    @Override
-    public CharSequence getPackageName() {
-        return mVendorInfo.mPackageName;
-    }
-
-    @Override
-    public void start(@NonNull PrinterDiscoveryCallback callback) throws Exception {
-        synchronized (mLock) {
-            mCallback = callback;
-        }
-        mListener.start();
-    }
-
-    @Override
-    public void stop() throws Exception {
-        synchronized (mLock) {
-            mCallback = null;
-        }
-        mListener.stop();
-    }
-
-    @Override
-    public void dataSetChanged() {
-        synchronized (mLock) {
-            if (mCallback != null) mCallback.onChanged(getCount());
-        }
-    }
-
-    @Override
-    public boolean matchesCriteria(String vendor, NsdServiceInfo nsdServiceInfo) {
-        return TextUtils.equals(vendor, mVendorInfo.mVendorID);
-    }
-
-    public int getCount() {
-        return mListener.getCount().second;
-    }
-}
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/ServiceResolveQueue.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/ServiceResolveQueue.java
deleted file mode 100644
index e5691b7..0000000
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/ServiceResolveQueue.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.printservice.recommendation.plugin.samsung;
-
-import android.net.nsd.NsdManager;
-import android.net.nsd.NsdServiceInfo;
-import android.util.Pair;
-import com.android.printservice.recommendation.util.NsdResolveQueue;
-
-import java.util.LinkedList;
-
-final class ServiceResolveQueue {
-
-    private final NsdManager mNsdManager;
-    private final LinkedList<Pair<NsdServiceInfo, ResolveCallback>> mQueue = new LinkedList<>();
-    private final Object mLock = new Object();
-
-    private static Object sLock = new Object();
-    private static ServiceResolveQueue sInstance = null;
-    private final NsdResolveQueue mNsdResolveQueue;
-    private Pair<NsdServiceInfo, ResolveCallback> mCurrentRequest = null;
-
-    public static void createInstance(NsdManager nsdManager) {
-        if (sInstance == null) sInstance = new ServiceResolveQueue(nsdManager);
-    }
-
-    public static ServiceResolveQueue getInstance(NsdManager nsdManager) {
-        synchronized (sLock) {
-            createInstance(nsdManager);
-            return sInstance;
-        }
-    }
-
-    public static void destroyInstance() {
-        sInstance = null;
-    }
-
-    public interface ResolveCallback {
-        void serviceResolved(NsdServiceInfo nsdServiceInfo);
-    }
-
-    public ServiceResolveQueue(NsdManager nsdManager) {
-        mNsdManager = nsdManager;
-        mNsdResolveQueue = NsdResolveQueue.getInstance();
-    }
-
-    public void queueRequest(NsdServiceInfo serviceInfo, ResolveCallback callback) {
-        synchronized (mLock) {
-            Pair<NsdServiceInfo, ResolveCallback> newRequest = Pair.create(serviceInfo, callback);
-            if (mQueue.contains(newRequest)) return;
-            mQueue.add(newRequest);
-            makeNextRequest();
-        }
-    }
-
-    public void removeRequest(NsdServiceInfo serviceInfo, ResolveCallback callback) {
-        synchronized (mLock) {
-            Pair<NsdServiceInfo, ResolveCallback> newRequest = Pair.create(serviceInfo, callback);
-            mQueue.remove(newRequest);
-            if ((mCurrentRequest != null) && newRequest.equals(mCurrentRequest)) mCurrentRequest = null;
-        }
-    }
-
-    private void makeNextRequest() {
-        synchronized (mLock) {
-            if (mCurrentRequest != null) return;
-            if (mQueue.isEmpty()) return;
-            mCurrentRequest = mQueue.removeFirst();
-            mNsdResolveQueue.resolve(mNsdManager, mCurrentRequest.first,
-                    new NsdManager.ResolveListener() {
-                        @Override
-                        public void onResolveFailed(NsdServiceInfo nsdServiceInfo, int i) {
-                            synchronized (mLock) {
-                                if (mCurrentRequest != null) mQueue.add(mCurrentRequest);
-                                makeNextRequest();
-                            }
-                        }
-
-                        @Override
-                        public void onServiceResolved(NsdServiceInfo nsdServiceInfo) {
-                            synchronized (mLock) {
-                                if (mCurrentRequest != null) {
-                                    mCurrentRequest.second.serviceResolved(nsdServiceInfo);
-                                    mCurrentRequest = null;
-                                }
-                                makeNextRequest();
-                            }
-                        }
-                    });
-
-        }
-    }
-
-
-}
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/VendorInfo.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/VendorInfo.java
deleted file mode 100644
index 0ebb4e4..0000000
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/VendorInfo.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.printservice.recommendation.plugin.samsung;
-
-import android.content.res.Resources;
-
-import java.util.Arrays;
-
-public final class VendorInfo {
-
-    public final String mPackageName;
-    public final String mVendorID;
-    public final String[] mDNSValues;
-    public final int mID;
-
-    public VendorInfo(Resources resources, int vendor_info_id) {
-        mID = vendor_info_id;
-        String[] data = resources.getStringArray(vendor_info_id);
-        if ((data == null) || (data.length < 2)) {
-            data = new String[] { null, null };
-        }
-        mPackageName = data[0];
-        mVendorID = data[1];
-        mDNSValues = (data.length > 2) ? Arrays.copyOfRange(data, 2, data.length) : new String[]{};
-    }
-}
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/util/MDNSFilteredDiscovery.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/util/MDNSFilteredDiscovery.java
new file mode 100644
index 0000000..c5dbc8c
--- /dev/null
+++ b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/util/MDNSFilteredDiscovery.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.printservice.recommendation.util;
+
+import android.content.Context;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+import com.android.printservice.recommendation.PrintServicePlugin;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A discovery listening for mDNS results and only adding the ones that {@link
+ * PrinterFilter#matchesCriteria match} configured list
+ */
+public class MDNSFilteredDiscovery implements NsdManager.DiscoveryListener  {
+    private static final String LOG_TAG = "MDNSFilteredDiscovery";
+
+    /**
+     * mDNS service filter interface.
+     * Implement {@link PrinterFilter#matchesCriteria} to filter out supported services
+     */
+    public interface PrinterFilter {
+        /**
+         * Main filter method. Should return true if mDNS service is supported
+         * by the print service plugin
+         *
+         * @param nsdServiceInfo The service info to check
+         *
+         * @return True if service is supported by the print service plugin
+         */
+        boolean matchesCriteria(NsdServiceInfo nsdServiceInfo);
+    }
+
+    /** Printer identifiers of the mPrinters found. */
+    @GuardedBy("mLock")
+    private final @NonNull HashSet<String> mPrinters;
+
+    /** Service types discovered by this plugin */
+    private final @NonNull HashSet<String> mServiceTypes;
+
+    /** Context of the user of this plugin */
+    private final @NonNull Context mContext;
+
+    /** mDNS services filter */
+    private final @NonNull PrinterFilter mPrinterFilter;
+
+    /**
+     * Call back to report the number of mPrinters found.
+     *
+     * We assume that {@link #start} and {@link #stop} are never called in parallel, hence it is
+     * safe to not synchronize access to this field.
+     */
+    private @Nullable PrintServicePlugin.PrinterDiscoveryCallback mCallback;
+
+    /** Queue used to resolve nsd infos */
+    private final @NonNull NsdResolveQueue mResolveQueue;
+
+    /**
+     * Create new stub that assumes that a print service can be used to print on all mPrinters
+     * matching some mDNS names.
+     *
+     * @param context       The context the plugin runs in
+     * @param serviceTypes  The mDNS service types to listen to.
+     * @param printerFilter The filter for mDNS services
+     */
+    public MDNSFilteredDiscovery(@NonNull Context context,
+            @NonNull Set<String> serviceTypes,
+            @NonNull PrinterFilter printerFilter) {
+        mContext = Preconditions.checkNotNull(context, "context");
+        mServiceTypes = new HashSet<>(Preconditions
+                .checkCollectionNotEmpty(Preconditions.checkCollectionElementsNotNull(serviceTypes,
+                        "serviceTypes"), "serviceTypes"));
+        mPrinterFilter = Preconditions.checkNotNull(printerFilter, "printerFilter");
+
+        mResolveQueue = NsdResolveQueue.getInstance();
+        mPrinters = new HashSet<>();
+    }
+
+    /**
+     * @return The NDS manager
+     */
+    private NsdManager getNDSManager() {
+        return (NsdManager) mContext.getSystemService(Context.NSD_SERVICE);
+    }
+
+    /**
+     * Start the discovery.
+     *
+     * @param callback Callbacks used by this plugin.
+     */
+    public void start(@NonNull PrintServicePlugin.PrinterDiscoveryCallback callback) {
+        mCallback = callback;
+        mCallback.onChanged(mPrinters.size());
+
+        for (String serviceType : mServiceTypes) {
+            DiscoveryListenerMultiplexer.addListener(getNDSManager(), serviceType, this);
+        }
+    }
+
+    /**
+     * Stop the discovery. This can only return once the plugin is completely finished and cleaned up.
+     */
+    public void stop() {
+        mCallback.onChanged(0);
+        mCallback = null;
+
+        for (int i = 0; i < mServiceTypes.size(); ++i) {
+            DiscoveryListenerMultiplexer.removeListener(getNDSManager(), this);
+        }
+    }
+
+    /**
+     *
+     * @return The number of discovered printers
+     */
+    public int getCount() {
+        return mPrinters.size();
+    }
+
+    @Override
+    public void onStartDiscoveryFailed(String serviceType, int errorCode) {
+        Log.w(LOG_TAG, "Failed to start network discovery for type " + serviceType + ": "
+                + errorCode);
+    }
+
+    @Override
+    public void onStopDiscoveryFailed(String serviceType, int errorCode) {
+        Log.w(LOG_TAG, "Failed to stop network discovery for type " + serviceType + ": "
+                + errorCode);
+    }
+
+    @Override
+    public void onDiscoveryStarted(String serviceType) {
+        // empty
+    }
+
+    @Override
+    public void onDiscoveryStopped(String serviceType) {
+        mPrinters.clear();
+    }
+
+    @Override
+    public void onServiceFound(NsdServiceInfo serviceInfo) {
+        mResolveQueue.resolve(getNDSManager(), serviceInfo,
+                new NsdManager.ResolveListener() {
+                    @Override
+                    public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
+                        Log.w(LOG_TAG, "Service found: could not resolve " + serviceInfo + ": " +
+                                errorCode);
+                    }
+
+                    @Override
+                    public void onServiceResolved(NsdServiceInfo serviceInfo) {
+                        if (mPrinterFilter.matchesCriteria(serviceInfo)) {
+                            if (mCallback != null) {
+                                boolean added = mPrinters.add(serviceInfo.getHost().getHostAddress());
+                                if (added) {
+                                    mCallback.onChanged(mPrinters.size());
+                                }
+                            }
+                        }
+                    }
+                });
+    }
+
+    @Override
+    public void onServiceLost(NsdServiceInfo serviceInfo) {
+        mResolveQueue.resolve(getNDSManager(), serviceInfo,
+                new NsdManager.ResolveListener() {
+                    @Override
+                    public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
+                        Log.w(LOG_TAG, "Service lost: Could not resolve " + serviceInfo + ": "
+                                + errorCode);
+                    }
+
+                    @Override
+                    public void onServiceResolved(NsdServiceInfo serviceInfo) {
+                        if (mPrinterFilter.matchesCriteria(serviceInfo)) {
+                            if (mCallback != null) {
+                                boolean removed = mPrinters
+                                        .remove(serviceInfo.getHost().getHostAddress());
+
+                                if (removed) {
+                                    mCallback.onChanged(mPrinters.size());
+                                }
+                            }
+                        }
+                    }
+                });
+    }
+}
\ No newline at end of file
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/mdnsFilter/MDNSUtils.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/util/MDNSUtils.java
similarity index 75%
rename from packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/mdnsFilter/MDNSUtils.java
rename to packages/PrintRecommendationService/src/com/android/printservice/recommendation/util/MDNSUtils.java
index 4c27a47..a6df3c8 100644
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/mdnsFilter/MDNSUtils.java
+++ b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/util/MDNSUtils.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package com.android.printservice.recommendation.plugin.mdnsFilter;
+package com.android.printservice.recommendation.util;
 
 import android.annotation.NonNull;
 import android.net.nsd.NsdServiceInfo;
@@ -27,12 +27,15 @@
 /**
  * Utils for dealing with mDNS attributes
  */
-class MDNSUtils {
+public class MDNSUtils {
     public static final String ATTRIBUTE_TY = "ty";
     public static final String ATTRIBUTE_PRODUCT = "product";
     public static final String ATTRIBUTE_USB_MFG = "usb_mfg";
     public static final String ATTRIBUTE_MFG = "mfg";
 
+    private MDNSUtils() {
+    }
+
     /**
      * Check if the service has any of a set of vendor names.
      *
@@ -95,4 +98,35 @@
     private static boolean containsString(@NonNull String container, @NonNull String contained) {
         return container.equalsIgnoreCase(contained) || container.contains(contained + " ");
     }
+
+    /**
+     * Return String from mDNS attribute byte array
+     *
+     * @param value the byte array with string data
+     *
+     * @return constructed string
+     */
+    public static String getString(byte[] value) {
+        if (value != null) return new String(value, StandardCharsets.UTF_8);
+        return null;
+    }
+
+    /**
+     * Check if service has a type of supported types set
+     *
+     * @param serviceInfo   The service
+     * @param serviceTypes  The supported service types set
+     *
+     * @return true if service has a type of supported types set
+     */
+    public static boolean isSupportedServiceType(@NonNull NsdServiceInfo serviceInfo,
+            @NonNull Set<String> serviceTypes) {
+        String curType = serviceInfo.getServiceType().toLowerCase();
+        for (String type : serviceTypes) {
+            if (curType.contains(type.toLowerCase())) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml
index bb85de2..7206127 100644
--- a/packages/SettingsProvider/res/values/defaults.xml
+++ b/packages/SettingsProvider/res/values/defaults.xml
@@ -51,6 +51,7 @@
     <bool name="def_wifi_on">false</bool>
     <!-- 0 == never, 1 == only when plugged in, 2 == always -->
     <integer name="def_wifi_sleep_policy">2</integer>
+    <bool name="def_wifi_wakeup_enabled">false</bool>
     <bool name="def_networks_available_notification_on">true</bool>
 
     <bool name="def_backup_enabled">false</bool>
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java
index 09879d8..f653371 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java
@@ -44,7 +44,7 @@
 
         void abortCurrentGesture();
 
-        void setLandscape(boolean landscape);
+        void setVertical(boolean vertical);
 
         void setCarMode(boolean carMode);
     }
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index a6604cb..29b5b62 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -129,9 +129,6 @@
 
     <dimen name="navigation_key_padding">0dp</dimen>
 
-    <dimen name="navigation_key_width_sw600dp_land">162dp</dimen>
-    <dimen name="navigation_key_padding_sw600dp_land">42dp</dimen>
-
     <!-- The width of the view containing the menu/ime navigation bar icons -->
     <dimen name="navigation_extra_key_width">36dp</dimen>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index bfc26e3..af1fc59 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1126,6 +1126,7 @@
         <item></item> <!-- STREAM_SYSTEM_ENFORCED -->
         <item></item> <!-- STREAM_DTMF -->
         <item></item> <!-- STREAM_TTS -->
+        <item>Accessibility</item> <!-- STREAM_ACCESSIBILITY -->
     </string-array>
 
     <string name="volume_stream_muted" translatable="false">%s silent</string>
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 07e1193..c19e806 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -1798,7 +1798,6 @@
                 // (like recents). Temporary enable/disable (e.g. the "back" button) are
                 // done in KeyguardHostView.
                 flags |= StatusBarManager.DISABLE_RECENT;
-                flags |= StatusBarManager.DISABLE_SEARCH;
             }
             if (isShowingAndNotOccluded()) {
                 flags |= StatusBarManager.DISABLE_HOME;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java
index f6fe176..c9f7456 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java
@@ -40,6 +40,7 @@
     private int mImageResource = -1;
     private Drawable mImageDrawable;
     private View mCurrentView;
+    private boolean mVertical;
 
     public ButtonDispatcher(int id) {
         mId = id;
@@ -49,13 +50,6 @@
         mViews.clear();
     }
 
-    void addView(View view, boolean landscape) {
-        addView(view);
-        if (view instanceof ButtonInterface) {
-            ((ButtonInterface) view).setLandscape(landscape);
-        }
-    }
-
     void addView(View view) {
         mViews.add(view);
         view.setOnClickListener(mClickListener);
@@ -75,6 +69,10 @@
         } else if (mImageDrawable != null) {
             ((ButtonInterface) view).setImageDrawable(mImageDrawable);
         }
+
+        if (view instanceof  ButtonInterface) {
+            ((ButtonInterface) view).setVertical(mVertical);
+        }
     }
 
     public int getId() {
@@ -186,4 +184,15 @@
         }
     }
 
+    public void setVertical(boolean vertical) {
+        mVertical = vertical;
+        final int N = mViews.size();
+        for (int i = 0; i < N; i++) {
+            final View view = mViews.get(i);
+            if (view instanceof ButtonInterface) {
+                ((ButtonInterface) view).setVertical(vertical);
+            }
+        }
+    }
+
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
index c420927..b2b093c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
@@ -17,12 +17,14 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.Configuration;
-import android.content.res.Resources;
 import android.util.AttributeSet;
 import android.util.SparseArray;
+import android.view.Display;
+import android.view.Display.Mode;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.WindowManager;
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 import android.widget.Space;
@@ -72,12 +74,13 @@
 
     protected FrameLayout mRot0;
     protected FrameLayout mRot90;
+    private boolean isRot0Landscape;
 
     private SparseArray<ButtonDispatcher> mButtonDispatchers;
     private String mCurrentLayout;
 
-    private View mLastRot0;
-    private View mLastRot90;
+    private View mLastPortrait;
+    private View mLastLandscape;
 
     private boolean mAlternativeOrder;
 
@@ -85,6 +88,10 @@
         super(context, attrs);
         mDensity = context.getResources().getConfiguration().densityDpi;
         createInflaters();
+        Display display = ((WindowManager)
+                context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
+        Mode displayMode = display.getMode();
+        isRot0Landscape = displayMode.getPhysicalWidth() > displayMode.getPhysicalHeight();
     }
 
     private void createInflaters() {
@@ -215,17 +222,17 @@
         String[] center = sets[1].split(BUTTON_SEPARATOR);
         String[] end = sets[2].split(BUTTON_SEPARATOR);
         // Inflate these in start to end order or accessibility traversal will be messed up.
-        inflateButtons(start, (ViewGroup) mRot0.findViewById(R.id.ends_group), false);
-        inflateButtons(start, (ViewGroup) mRot90.findViewById(R.id.ends_group), true);
+        inflateButtons(start, (ViewGroup) mRot0.findViewById(R.id.ends_group), isRot0Landscape);
+        inflateButtons(start, (ViewGroup) mRot90.findViewById(R.id.ends_group), !isRot0Landscape);
 
-        inflateButtons(center, (ViewGroup) mRot0.findViewById(R.id.center_group), false);
-        inflateButtons(center, (ViewGroup) mRot90.findViewById(R.id.center_group), true);
+        inflateButtons(center, (ViewGroup) mRot0.findViewById(R.id.center_group), isRot0Landscape);
+        inflateButtons(center, (ViewGroup) mRot90.findViewById(R.id.center_group), !isRot0Landscape);
 
         addGravitySpacer((LinearLayout) mRot0.findViewById(R.id.ends_group));
         addGravitySpacer((LinearLayout) mRot90.findViewById(R.id.ends_group));
 
-        inflateButtons(end, (ViewGroup) mRot0.findViewById(R.id.ends_group), false);
-        inflateButtons(end, (ViewGroup) mRot90.findViewById(R.id.ends_group), true);
+        inflateButtons(end, (ViewGroup) mRot0.findViewById(R.id.ends_group), isRot0Landscape);
+        inflateButtons(end, (ViewGroup) mRot90.findViewById(R.id.ends_group), !isRot0Landscape);
     }
 
     private void addGravitySpacer(LinearLayout layout) {
@@ -234,7 +241,7 @@
 
     private void inflateButtons(String[] buttons, ViewGroup parent, boolean landscape) {
         for (int i = 0; i < buttons.length; i++) {
-            inflateButton(buttons[i], parent, landscape, i);
+            inflateButton(buttons[i], parent, landscape);
         }
     }
 
@@ -247,8 +254,7 @@
     }
 
     @Nullable
-    protected View inflateButton(String buttonSpec, ViewGroup parent, boolean landscape,
-            int indexInParent) {
+    protected View inflateButton(String buttonSpec, ViewGroup parent, boolean landscape) {
         LayoutInflater inflater = landscape ? mLandscapeInflater : mLayoutInflater;
         float size = extractSize(buttonSpec);
         View v = createView(buttonSpec, parent, inflater, landscape);
@@ -259,15 +265,15 @@
             params.width = (int) (params.width * size);
         }
         parent.addView(v);
-        addToDispatchers(v, landscape);
-        View lastView = landscape ? mLastRot90 : mLastRot0;
+        addToDispatchers(v);
+        View lastView = landscape ? mLastLandscape : mLastPortrait;
         if (lastView != null) {
             v.setAccessibilityTraversalAfter(lastView.getId());
         }
         if (landscape) {
-            mLastRot90 = v;
+            mLastLandscape = v;
         } else {
-            mLastRot0 = v;
+            mLastPortrait = v;
         }
         return v;
     }
@@ -283,19 +289,10 @@
         }
         if (HOME.equals(button)) {
             v = inflater.inflate(R.layout.home, parent, false);
-            if (landscape && isSw600Dp()) {
-                setupLandButton(v);
-            }
         } else if (BACK.equals(button)) {
             v = inflater.inflate(R.layout.back, parent, false);
-            if (landscape && isSw600Dp()) {
-                setupLandButton(v);
-            }
         } else if (RECENT.equals(button)) {
             v = inflater.inflate(R.layout.recent_apps, parent, false);
-            if (landscape && isSw600Dp()) {
-                setupLandButton(v);
-            }
         } else if (MENU_IME.equals(button)) {
             v = inflater.inflate(R.layout.menu_ime, parent, false);
         } else if (NAVSPACE.equals(button)) {
@@ -348,37 +345,22 @@
         return buttonSpec.substring(0, buttonSpec.indexOf(SIZE_MOD_START));
     }
 
-    private void addToDispatchers(View v, boolean landscape) {
+    private void addToDispatchers(View v) {
         if (mButtonDispatchers != null) {
             final int indexOfKey = mButtonDispatchers.indexOfKey(v.getId());
             if (indexOfKey >= 0) {
-                mButtonDispatchers.valueAt(indexOfKey).addView(v, landscape);
+                mButtonDispatchers.valueAt(indexOfKey).addView(v);
             } else if (v instanceof ViewGroup) {
                 final ViewGroup viewGroup = (ViewGroup)v;
                 final int N = viewGroup.getChildCount();
                 for (int i = 0; i < N; i++) {
-                    addToDispatchers(viewGroup.getChildAt(i), landscape);
+                    addToDispatchers(viewGroup.getChildAt(i));
                 }
             }
         }
     }
 
-    private boolean isSw600Dp() {
-        Configuration configuration = mContext.getResources().getConfiguration();
-        return (configuration.smallestScreenWidthDp >= 600);
-    }
 
-    /**
-     * This manually sets the width of sw600dp landscape buttons because despite
-     * overriding the configuration from the overridden resources aren't loaded currently.
-     */
-    private void setupLandButton(View v) {
-        Resources res = mContext.getResources();
-        v.getLayoutParams().width = res.getDimensionPixelOffset(
-                R.dimen.navigation_key_width_sw600dp_land);
-        int padding = res.getDimensionPixelOffset(R.dimen.navigation_key_padding_sw600dp_land);
-        v.setPadding(padding, v.getPaddingTop(), padding, v.getPaddingBottom());
-    }
 
     private void clearViews() {
         if (mButtonDispatchers != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index a0ea9bf..7023324 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -590,6 +590,8 @@
 
         updateTaskSwitchHelper();
         setNavigationIconHints(mNavigationIconHints, true);
+
+        getHomeButton().setVertical(mVertical);
     }
 
     public void onKeyguardOccludedChanged(boolean keyguardOccluded) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java
index bcc5a3f..ae59315 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java
@@ -275,7 +275,7 @@
     }
 
     @Override
-    public void setLandscape(boolean landscape) {
+    public void setVertical(boolean vertical) {
         //no op
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java
index ec0e4cd..bb5632b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java
@@ -78,6 +78,7 @@
         AudioSystem.STREAM_SYSTEM_ENFORCED,
         AudioSystem.STREAM_TTS,
         AudioSystem.STREAM_VOICE_CALL,
+        AudioSystem.STREAM_ACCESSIBILITY,
     };
 
     private final HandlerThread mWorkerThread;
@@ -119,7 +120,26 @@
         mObserver = new SettingObserver(mWorker);
         mObserver.init();
         mReceiver.init();
-        mStreamTitles = mContext.getResources().getStringArray(R.array.volume_stream_titles);
+        final String[] titles =
+                mContext.getResources().getStringArray(R.array.volume_stream_titles);
+        if (STREAMS.length == titles.length) {
+            mStreamTitles = titles;
+        } else if (STREAMS.length > titles.length) {
+            Log.e(TAG, String.format("Missing stream titles (found %d, expected %d): "
+                    + " invalid resources for volume_stream_titles",
+                    titles.length, STREAMS.length));
+            mStreamTitles = new String[STREAMS.length];
+            System.arraycopy(titles, 0, mStreamTitles, 0, titles.length);
+            for (int i = titles.length ; i < STREAMS.length ; i++) {
+                mStreamTitles[i] = "";
+            }
+        } else { // STREAMS.length < titles.length
+            Log.e(TAG, String.format("Too many stream titles (found %d, expected %d): "
+                    + " invalid resources for volume_stream_titles",
+                    titles.length, STREAMS.length));
+            mStreamTitles = new String[STREAMS.length];
+            System.arraycopy(titles, 0, mStreamTitles, 0, STREAMS.length);
+        }
         mVibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
         mHasVibrator = mVibrator != null && mVibrator.hasVibrator();
     }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index ab111a0..df71ced 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -2843,15 +2843,8 @@
                     }
                     if (mMotionEventInjector != null) {
                         List<GestureDescription.GestureStep> steps = gestureSteps.getList();
-                        List<MotionEvent> events = GestureDescription.MotionEventGenerator
-                                .getMotionEventsFromGestureSteps(steps);
-                        // Confirm that the motion events end with an UP event.
-                        if (events.get(events.size() - 1).getAction() == MotionEvent.ACTION_UP) {
-                            mMotionEventInjector.injectEvents(events, mServiceInterface, sequence);
-                            return;
-                        } else {
-                            Slog.e(LOG_TAG, "Gesture is not well-formed");
-                        }
+                         mMotionEventInjector.injectEvents(steps, mServiceInterface, sequence);
+                         return;
                     } else {
                         Slog.e(LOG_TAG, "MotionEventInjector installation timed out");
                     }
diff --git a/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java b/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java
index 8042ddb..48041ad 100644
--- a/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java
+++ b/services/accessibility/java/com/android/server/accessibility/MotionEventInjector.java
@@ -16,49 +16,67 @@
 
 package com.android.server.accessibility;
 
+import android.accessibilityservice.GestureDescription;
+import android.accessibilityservice.GestureDescription.GestureStep;
+import android.accessibilityservice.GestureDescription.TouchPoint;
 import android.accessibilityservice.IAccessibilityServiceClient;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.util.IntArray;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseIntArray;
 import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.WindowManagerPolicy;
 import android.view.accessibility.AccessibilityEvent;
 import com.android.internal.os.SomeArgs;
-import com.android.server.accessibility.AccessibilityManagerService.Service;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
  * Injects MotionEvents to permit {@code AccessibilityService}s to touch the screen on behalf of
  * users.
- *
+ * <p>
  * All methods except {@code injectEvents} must be called only from the main thread.
  */
 public class MotionEventInjector implements EventStreamTransformation, Handler.Callback {
     private static final String LOG_TAG = "MotionEventInjector";
     private static final int MESSAGE_SEND_MOTION_EVENT = 1;
     private static final int MESSAGE_INJECT_EVENTS = 2;
-    private static final int MAX_POINTERS = 11; // Non-binding maximum
+
+    /**
+     * Constants used to initialize all MotionEvents
+     */
+    private static final int EVENT_META_STATE = 0;
+    private static final int EVENT_BUTTON_STATE = 0;
+    private static final int EVENT_DEVICE_ID = 0;
+    private static final int EVENT_EDGE_FLAGS = 0;
+    private static final int EVENT_SOURCE = InputDevice.SOURCE_TOUCHSCREEN;
+    private static final int EVENT_FLAGS = 0;
+    private static final float EVENT_X_PRECISION = 1;
+    private static final float EVENT_Y_PRECISION = 1;
+
+    private static MotionEvent.PointerCoords[] sPointerCoords;
+    private static MotionEvent.PointerProperties[] sPointerProps;
 
     private final Handler mHandler;
     private final SparseArray<Boolean> mOpenGesturesInProgress = new SparseArray<>();
 
-    // These two arrays must be the same length
-    private MotionEvent.PointerProperties[] mPointerProperties =
-            new MotionEvent.PointerProperties[MAX_POINTERS];
-    private MotionEvent.PointerCoords[] mPointerCoords =
-            new MotionEvent.PointerCoords[MAX_POINTERS];
     private EventStreamTransformation mNext;
     private IAccessibilityServiceClient mServiceInterfaceForCurrentGesture;
-    private int mSequenceForCurrentGesture;
-    private int mSourceOfInjectedGesture = InputDevice.SOURCE_UNKNOWN;
+    private IntArray mSequencesInProgress = new IntArray(5);
     private boolean mIsDestroyed = false;
+    private TouchPoint[] mLastTouchPoints;
+    private int mNumLastTouchPoints;
+    private long mDownTime;
+    private long mLastScheduledEventTime;
+    private SparseIntArray mStrokeIdToPointerId = new SparseIntArray(5);
 
     /**
      * @param looper A looper on the main thread to use for dispatching new events
@@ -75,18 +93,18 @@
     }
 
     /**
-     * Schedule a series of events for injection. These events must comprise a complete, valid
-     * sequence. All gestures currently in progress will be cancelled, and all {@code downTime}
-     * and {@code eventTime} fields will be offset by the current time.
+     * Schedule a gesture for injection. The gesture is defined by a set of {@code GestureStep}s,
+     * from which {@code MotionEvent}s will be derived. All gestures currently in progress will be
+     * cancelled.
      *
-     * @param events The events to inject. Must all be from the same source.
+     * @param gestureSteps The gesture steps to inject.
      * @param serviceInterface The interface to call back with a result when the gesture is
      * either complete or cancelled.
      */
-    public void injectEvents(List<MotionEvent> events,
+    public void injectEvents(List<GestureStep> gestureSteps,
             IAccessibilityServiceClient serviceInterface, int sequence) {
         SomeArgs args = SomeArgs.obtain();
-        args.arg1 = events;
+        args.arg1 = gestureSteps;
         args.arg2 = serviceInterface;
         args.argi1 = sequence;
         mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_INJECT_EVENTS, args));
@@ -138,7 +156,7 @@
     public boolean handleMessage(Message message) {
         if (message.what == MESSAGE_INJECT_EVENTS) {
             SomeArgs args = (SomeArgs) message.obj;
-            injectEventsMainThread((List<MotionEvent>) args.arg1,
+            injectEventsMainThread((List<GestureStep>) args.arg1,
                     (IAccessibilityServiceClient) args.arg2, args.argi1);
             args.recycle();
             return true;
@@ -148,16 +166,16 @@
             return false;
         }
         MotionEvent motionEvent = (MotionEvent) message.obj;
-        sendMotionEventToNext(motionEvent, motionEvent,
-                WindowManagerPolicy.FLAG_PASS_TO_USER);
-        // If the message queue is now empty, then this gesture is complete
-        if (!mHandler.hasMessages(MESSAGE_SEND_MOTION_EVENT)) {
-            notifyService(true);
+        sendMotionEventToNext(motionEvent, motionEvent, WindowManagerPolicy.FLAG_PASS_TO_USER);
+        boolean isEndOfSequence = message.arg1 != 0;
+        if (isEndOfSequence) {
+            notifyService(mServiceInterfaceForCurrentGesture, mSequencesInProgress.get(0), true);
+            mSequencesInProgress.remove(0);
         }
         return true;
     }
 
-    private void injectEventsMainThread(List<MotionEvent> events,
+    private void injectEventsMainThread(List<GestureStep> gestureSteps,
             IAccessibilityServiceClient serviceInterface, int sequence) {
         if (mIsDestroyed) {
             try {
@@ -168,48 +186,110 @@
             }
             return;
         }
-        cancelAnyPendingInjectedEvents();
-        mSourceOfInjectedGesture = events.get(0).getSource();
-        cancelAnyGestureInProgress(mSourceOfInjectedGesture);
-        mServiceInterfaceForCurrentGesture = serviceInterface;
-        mSequenceForCurrentGesture = sequence;
+
         if (mNext == null) {
-            notifyService(false);
+            notifyService(serviceInterface, sequence, false);
             return;
         }
 
-        long startTime = SystemClock.uptimeMillis();
+        boolean continuingGesture = newGestureTriesToContinueOldOne(gestureSteps);
+
+        if (continuingGesture) {
+            if ((serviceInterface != mServiceInterfaceForCurrentGesture)
+                    || !prepareToContinueOldGesture(gestureSteps)) {
+                cancelAnyPendingInjectedEvents();
+                notifyService(serviceInterface, sequence, false);
+                return;
+            }
+        }
+        if (!continuingGesture) {
+            cancelAnyPendingInjectedEvents();
+            // Injected gestures have been canceled, but real gestures still need cancelling
+            cancelAnyGestureInProgress(EVENT_SOURCE);
+        }
+        mServiceInterfaceForCurrentGesture = serviceInterface;
+
+        long currentTime = SystemClock.uptimeMillis();
+        List<MotionEvent> events = getMotionEventsFromGestureSteps(gestureSteps,
+                (mSequencesInProgress.size() == 0) ? currentTime : mLastScheduledEventTime);
+        if (events.isEmpty()) {
+            notifyService(serviceInterface, sequence, false);
+            return;
+        }
+        mSequencesInProgress.add(sequence);
+
         for (int i = 0; i < events.size(); i++) {
             MotionEvent event = events.get(i);
-            int numPointers = event.getPointerCount();
-            if (numPointers > mPointerCoords.length) {
-                mPointerCoords = new MotionEvent.PointerCoords[numPointers];
-                mPointerProperties = new MotionEvent.PointerProperties[numPointers];
-            }
-            for (int j = 0; j < numPointers; j++) {
-                if (mPointerCoords[j] == null) {
-                    mPointerCoords[j] = new MotionEvent.PointerCoords();
-                    mPointerProperties[j] = new MotionEvent.PointerProperties();
-                }
-                event.getPointerCoords(j, mPointerCoords[j]);
-                event.getPointerProperties(j, mPointerProperties[j]);
-            }
-
-            /*
-             * MotionEvent doesn't have a setEventTime() method (it carries around history data,
-             * which could become inconsistent), so we need to obtain a new one.
-             */
-            MotionEvent offsetEvent = MotionEvent.obtain(startTime + event.getDownTime(),
-                    startTime + event.getEventTime(), event.getAction(), numPointers,
-                    mPointerProperties, mPointerCoords, event.getMetaState(),
-                    event.getButtonState(), event.getXPrecision(), event.getYPrecision(),
-                    event.getDeviceId(), event.getEdgeFlags(), event.getSource(),
-                    event.getFlags());
-            Message message = mHandler.obtainMessage(MESSAGE_SEND_MOTION_EVENT, offsetEvent);
-            mHandler.sendMessageDelayed(message, event.getEventTime());
+            int isEndOfSequence = (i == events.size() - 1) ? 1 : 0;
+            Message message = mHandler.obtainMessage(
+                    MESSAGE_SEND_MOTION_EVENT, isEndOfSequence, 0, event);
+            mLastScheduledEventTime = event.getEventTime();
+            mHandler.sendMessageDelayed(message, Math.max(0, event.getEventTime() - currentTime));
         }
     }
 
+    private boolean newGestureTriesToContinueOldOne(List<GestureStep> gestureSteps) {
+        if (gestureSteps.isEmpty()) {
+            return false;
+        }
+        GestureStep firstStep = gestureSteps.get(0);
+        for (int i = 0; i < firstStep.numTouchPoints; i++) {
+            if (!firstStep.touchPoints[i].mIsStartOfPath) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * A gesture can only continue a gesture if it contains intermediate points that continue
+     * each continued stroke of the last gesture, and no extra points.
+     *
+     * @param gestureSteps The steps of the new gesture
+     * @return {@code true} if the new gesture could continue the last one dispatched. {@code false}
+     * otherwise.
+     */
+    private boolean prepareToContinueOldGesture(List<GestureStep> gestureSteps) {
+        if (gestureSteps.isEmpty() || (mLastTouchPoints == null) || (mNumLastTouchPoints == 0)) {
+            return false;
+        }
+        GestureStep firstStep = gestureSteps.get(0);
+        // Make sure all of the continuing paths match up
+        int numContinuedStrokes = 0;
+        for (int i = 0; i < firstStep.numTouchPoints; i++) {
+            TouchPoint touchPoint = firstStep.touchPoints[i];
+            if (!touchPoint.mIsStartOfPath) {
+                int continuedPointerId = mStrokeIdToPointerId
+                        .get(touchPoint.mContinuedStrokeId, -1);
+                if (continuedPointerId == -1) {
+                    return false;
+                }
+                mStrokeIdToPointerId.put(touchPoint.mStrokeId, continuedPointerId);
+                int lastPointIndex = findPointByStrokeId(
+                        mLastTouchPoints, mNumLastTouchPoints, touchPoint.mContinuedStrokeId);
+                if (lastPointIndex < 0) {
+                    return false;
+                }
+                if (mLastTouchPoints[lastPointIndex].mIsEndOfPath
+                        || (mLastTouchPoints[lastPointIndex].mX != touchPoint.mX)
+                        || (mLastTouchPoints[lastPointIndex].mY != touchPoint.mY)) {
+                    return false;
+                }
+                // Update the last touch point to match the continuation, so the gestures will
+                // line up
+                mLastTouchPoints[lastPointIndex].mStrokeId = touchPoint.mStrokeId;
+            }
+            numContinuedStrokes++;
+        }
+        // Make sure we didn't miss any paths
+        for (int i = 0; i < mNumLastTouchPoints; i++) {
+            if (!mLastTouchPoints[i].mIsEndOfPath) {
+                numContinuedStrokes--;
+            }
+        }
+        return numContinuedStrokes == 0;
+    }
+
     private void sendMotionEventToNext(MotionEvent event, MotionEvent rawEvent,
             int policyFlags) {
         if (mNext != null) {
@@ -228,7 +308,7 @@
         if ((mNext != null) && mOpenGesturesInProgress.get(source, false)) {
             long now = SystemClock.uptimeMillis();
             MotionEvent cancelEvent =
-                    MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
+                    obtainMotionEvent(now, now, MotionEvent.ACTION_CANCEL, getLastTouchPoints(), 1);
             sendMotionEventToNext(cancelEvent, cancelEvent,
                     WindowManagerPolicy.FLAG_PASS_TO_USER);
             mOpenGesturesInProgress.put(source, false);
@@ -237,19 +317,187 @@
 
     private void cancelAnyPendingInjectedEvents() {
         if (mHandler.hasMessages(MESSAGE_SEND_MOTION_EVENT)) {
-            cancelAnyGestureInProgress(mSourceOfInjectedGesture);
             mHandler.removeMessages(MESSAGE_SEND_MOTION_EVENT);
-            notifyService(false);
+            cancelAnyGestureInProgress(EVENT_SOURCE);
+            for (int i = mSequencesInProgress.size() - 1; i >= 0; i--) {
+                notifyService(mServiceInterfaceForCurrentGesture,
+                        mSequencesInProgress.get(i), false);
+                mSequencesInProgress.remove(i);
+            }
+        } else if (mNumLastTouchPoints != 0) {
+            // An injected gesture is in progress and waiting for a continuation. Cancel it.
+            cancelAnyGestureInProgress(EVENT_SOURCE);
         }
+        mNumLastTouchPoints = 0;
+        mStrokeIdToPointerId.clear();
     }
 
-    private void notifyService(boolean success) {
+    private void notifyService(IAccessibilityServiceClient service, int sequence, boolean success) {
         try {
-            mServiceInterfaceForCurrentGesture.onPerformGestureResult(
-                    mSequenceForCurrentGesture, success);
+            service.onPerformGestureResult(sequence, success);
         } catch (RemoteException re) {
             Slog.e(LOG_TAG, "Error sending motion event injection status to "
                     + mServiceInterfaceForCurrentGesture, re);
         }
     }
+
+    private List<MotionEvent> getMotionEventsFromGestureSteps(
+            List<GestureStep> steps, long startTime) {
+        final List<MotionEvent> motionEvents = new ArrayList<>();
+
+        TouchPoint[] lastTouchPoints = getLastTouchPoints();
+
+        for (int i = 0; i < steps.size(); i++) {
+            GestureDescription.GestureStep step = steps.get(i);
+            int currentTouchPointSize = step.numTouchPoints;
+            if (currentTouchPointSize > lastTouchPoints.length) {
+                mNumLastTouchPoints = 0;
+                motionEvents.clear();
+                return motionEvents;
+            }
+
+            appendMoveEventIfNeeded(motionEvents, step.touchPoints, currentTouchPointSize,
+                    startTime + step.timeSinceGestureStart);
+            appendUpEvents(motionEvents, step.touchPoints, currentTouchPointSize,
+                    startTime + step.timeSinceGestureStart);
+            appendDownEvents(motionEvents, step.touchPoints, currentTouchPointSize,
+                    startTime + step.timeSinceGestureStart);
+        }
+        return motionEvents;
+    }
+
+    private TouchPoint[] getLastTouchPoints() {
+        if (mLastTouchPoints == null) {
+            int capacity = GestureDescription.getMaxStrokeCount();
+            mLastTouchPoints = new TouchPoint[capacity];
+            for (int i = 0; i < capacity; i++) {
+                mLastTouchPoints[i] = new GestureDescription.TouchPoint();
+            }
+        }
+        return mLastTouchPoints;
+    }
+
+    private void appendMoveEventIfNeeded(List<MotionEvent> motionEvents,
+            TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
+            /* Look for pointers that have moved */
+        boolean moveFound = false;
+        TouchPoint[] lastTouchPoints = getLastTouchPoints();
+        for (int i = 0; i < currentTouchPointsSize; i++) {
+            int lastPointsIndex = findPointByStrokeId(lastTouchPoints, mNumLastTouchPoints,
+                    currentTouchPoints[i].mStrokeId);
+            if (lastPointsIndex >= 0) {
+                moveFound |= (lastTouchPoints[lastPointsIndex].mX != currentTouchPoints[i].mX)
+                        || (lastTouchPoints[lastPointsIndex].mY != currentTouchPoints[i].mY);
+                lastTouchPoints[lastPointsIndex].copyFrom(currentTouchPoints[i]);
+            }
+        }
+
+        if (moveFound) {
+            motionEvents.add(obtainMotionEvent(mDownTime, currentTime, MotionEvent.ACTION_MOVE,
+                    lastTouchPoints, mNumLastTouchPoints));
+        }
+    }
+
+    private void appendUpEvents(List<MotionEvent> motionEvents,
+            TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
+        /* Look for a pointer at the end of its path */
+        TouchPoint[] lastTouchPoints = getLastTouchPoints();
+        for (int i = 0; i < currentTouchPointsSize; i++) {
+            if (currentTouchPoints[i].mIsEndOfPath) {
+                int indexOfUpEvent = findPointByStrokeId(lastTouchPoints, mNumLastTouchPoints,
+                        currentTouchPoints[i].mStrokeId);
+                if (indexOfUpEvent < 0) {
+                    continue; // Should not happen
+                }
+                int action = (mNumLastTouchPoints == 1) ? MotionEvent.ACTION_UP
+                        : MotionEvent.ACTION_POINTER_UP;
+                action |= indexOfUpEvent << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+                motionEvents.add(obtainMotionEvent(mDownTime, currentTime, action,
+                        lastTouchPoints, mNumLastTouchPoints));
+                    /* Remove this point from lastTouchPoints */
+                for (int j = indexOfUpEvent; j < mNumLastTouchPoints - 1; j++) {
+                    lastTouchPoints[j].copyFrom(mLastTouchPoints[j + 1]);
+                }
+                mNumLastTouchPoints--;
+                if (mNumLastTouchPoints == 0) {
+                    mStrokeIdToPointerId.clear();
+                }
+            }
+        }
+    }
+
+    private void appendDownEvents(List<MotionEvent> motionEvents,
+            TouchPoint[] currentTouchPoints, int currentTouchPointsSize, long currentTime) {
+        /* Look for a pointer that is just starting */
+        TouchPoint[] lastTouchPoints = getLastTouchPoints();
+        for (int i = 0; i < currentTouchPointsSize; i++) {
+            if (currentTouchPoints[i].mIsStartOfPath) {
+                /* Add the point to last coords and use the new array to generate the event */
+                lastTouchPoints[mNumLastTouchPoints++].copyFrom(currentTouchPoints[i]);
+                int action = (mNumLastTouchPoints == 1) ? MotionEvent.ACTION_DOWN
+                        : MotionEvent.ACTION_POINTER_DOWN;
+                if (action == MotionEvent.ACTION_DOWN) {
+                    mDownTime = currentTime;
+                }
+                action |= i << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+                motionEvents.add(obtainMotionEvent(mDownTime, currentTime, action,
+                        lastTouchPoints, mNumLastTouchPoints));
+            }
+        }
+    }
+
+    private MotionEvent obtainMotionEvent(long downTime, long eventTime, int action,
+            TouchPoint[] touchPoints, int touchPointsSize) {
+        if ((sPointerCoords == null) || (sPointerCoords.length < touchPointsSize)) {
+            sPointerCoords = new MotionEvent.PointerCoords[touchPointsSize];
+            for (int i = 0; i < touchPointsSize; i++) {
+                sPointerCoords[i] = new MotionEvent.PointerCoords();
+            }
+        }
+        if ((sPointerProps == null) || (sPointerProps.length < touchPointsSize)) {
+            sPointerProps = new MotionEvent.PointerProperties[touchPointsSize];
+            for (int i = 0; i < touchPointsSize; i++) {
+                sPointerProps[i] = new MotionEvent.PointerProperties();
+            }
+        }
+        for (int i = 0; i < touchPointsSize; i++) {
+            int pointerId = mStrokeIdToPointerId.get(touchPoints[i].mStrokeId, -1);
+            if (pointerId == -1) {
+                pointerId = getUnusedPointerId();
+                mStrokeIdToPointerId.put(touchPoints[i].mStrokeId, pointerId);
+            }
+            sPointerProps[i].id = pointerId;
+            sPointerProps[i].toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
+            sPointerCoords[i].clear();
+            sPointerCoords[i].pressure = 1.0f;
+            sPointerCoords[i].size = 1.0f;
+            sPointerCoords[i].x = touchPoints[i].mX;
+            sPointerCoords[i].y = touchPoints[i].mY;
+        }
+        return MotionEvent.obtain(downTime, eventTime, action, touchPointsSize,
+                sPointerProps, sPointerCoords, EVENT_META_STATE, EVENT_BUTTON_STATE,
+                EVENT_X_PRECISION, EVENT_Y_PRECISION, EVENT_DEVICE_ID, EVENT_EDGE_FLAGS,
+                EVENT_SOURCE, EVENT_FLAGS);
+    }
+
+    private static int findPointByStrokeId(TouchPoint[] touchPoints, int touchPointsSize,
+            int strokeId) {
+        for (int i = 0; i < touchPointsSize; i++) {
+            if (touchPoints[i].mStrokeId == strokeId) {
+                return i;
+            }
+        }
+        return -1;
+    }
+    private int getUnusedPointerId() {
+        int MAX_POINTER_ID = 10;
+        int pointerId = 0;
+        while (mStrokeIdToPointerId.indexOfValue(pointerId) >= 0) {
+            pointerId++;
+            if (pointerId >= MAX_POINTER_ID) {
+                return MAX_POINTER_ID;
+            }
+        }
+        return pointerId;
+    }
 }
diff --git a/services/core/java/com/android/server/BluetoothManagerService.java b/services/core/java/com/android/server/BluetoothManagerService.java
index f7068cf..b0c5603 100644
--- a/services/core/java/com/android/server/BluetoothManagerService.java
+++ b/services/core/java/com/android/server/BluetoothManagerService.java
@@ -42,6 +42,7 @@
 import android.database.ContentObserver;
 import android.os.Binder;
 import android.os.Build;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -52,6 +53,8 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.os.UserManagerInternal;
+import android.os.UserManagerInternal.UserRestrictionsListener;
 import android.provider.Settings;
 import android.provider.Settings.SettingNotFoundException;
 import android.util.Slog;
@@ -177,6 +180,24 @@
         }
     };
 
+    private final UserRestrictionsListener mUserRestrictionsListener =
+            new UserRestrictionsListener() {
+        @Override
+        public void onUserRestrictionsChanged(int userId, Bundle newRestrictions,
+                Bundle prevRestrictions) {
+            final boolean bluetoothDisallowed =
+                    newRestrictions.getBoolean(UserManager.DISALLOW_BLUETOOTH);
+            if ((mEnable || mEnableExternal) && bluetoothDisallowed) {
+                try {
+                    disable(null, true);
+                } catch (RemoteException e) {
+                    Slog.w(TAG, "Exception when disabling Bluetooth from UserRestrictionsListener",
+                            e);
+                }
+            }
+        }
+    };
+
     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -634,6 +655,13 @@
 
     public boolean enableNoAutoConnect()
     {
+        if (isBluetoothDisallowed()) {
+            if (DBG) {
+                Slog.d(TAG, "enableNoAutoConnect(): not enabling - bluetooth disallowed");
+            }
+            return false;
+        }
+
         mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
                                                 "Need BLUETOOTH ADMIN permission");
 
@@ -659,6 +687,13 @@
         final int callingUid = Binder.getCallingUid();
         final boolean callerSystem = UserHandle.getAppId(callingUid) == Process.SYSTEM_UID;
 
+        if (isBluetoothDisallowed()) {
+            if (DBG) {
+                Slog.d(TAG,"enable(): not enabling - bluetooth disallowed");
+            }
+            return false;
+        }
+
         if (!callerSystem) {
             if (!checkIfCallerIsForegroundUser()) {
                 Slog.w(TAG, "enable(): not allowed for non-active and non system user");
@@ -872,6 +907,12 @@
      */
     public void handleOnBootPhase() {
         if (DBG) Slog.d(TAG, "Bluetooth boot completed");
+        UserManagerInternal userManagerInternal =
+                LocalServices.getService(UserManagerInternal.class);
+        userManagerInternal.addUserRestrictionsListener(mUserRestrictionsListener);
+        if (isBluetoothDisallowed()) {
+            return;
+        }
         if (mEnableExternal && isBluetoothPersistedStateOnBluetooth()) {
             if (DBG) Slog.d(TAG, "Auto-enabling Bluetooth.");
             sendEnableMsg(mQuietEnableExternal);
@@ -1916,6 +1957,16 @@
         }
     }
 
+    private boolean isBluetoothDisallowed() {
+        long callingIdentity = Binder.clearCallingIdentity();
+        try {
+            return mContext.getSystemService(UserManager.class)
+                    .hasUserRestriction(UserManager.DISALLOW_BLUETOOTH, UserHandle.SYSTEM);
+        } finally {
+            Binder.restoreCallingIdentity(callingIdentity);
+        }
+    }
+
     @Override
     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index b2adc73..d94d3cd 100644
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -1830,11 +1830,10 @@
             // If keyguard is showing, nothing is visible, except if we are able to dismiss Keyguard
             // right away.
             return shouldBeVisible && mStackSupervisor.mKeyguardController
-                    .canShowActivityWhileKeyguardShowing(dismissKeyguard);
+                    .canShowActivityWhileKeyguardShowing(r, dismissKeyguard);
         } else if (keyguardLocked) {
-
-            // Show when locked windows above keyguard.
-            return shouldBeVisible && showWhenLocked;
+            return shouldBeVisible && mStackSupervisor.mKeyguardController.canShowWhileOccluded(
+                    dismissKeyguard, showWhenLocked);
         } else {
             return shouldBeVisible;
         }
diff --git a/services/core/java/com/android/server/am/KeyguardController.java b/services/core/java/com/android/server/am/KeyguardController.java
index 5e02597..19bf536 100644
--- a/services/core/java/com/android/server/am/KeyguardController.java
+++ b/services/core/java/com/android/server/am/KeyguardController.java
@@ -57,6 +57,7 @@
     private boolean mKeyguardShowing;
     private boolean mKeyguardGoingAway;
     private boolean mOccluded;
+    private boolean mDismissalRequested;
     private ActivityRecord mDismissingKeyguardActivity;
     private int mBeforeUnoccludeTransit;
     private int mVisibilityTransactionDepth;
@@ -95,9 +96,7 @@
         mKeyguardShowing = showing;
         if (showing) {
             mKeyguardGoingAway = false;
-
-            // Allow an activity to redismiss Keyguard.
-            mDismissingKeyguardActivity = null;
+            mDismissalRequested = false;
         }
         mStackSupervisor.ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
         mService.updateSleepIfNeededLocked();
@@ -183,8 +182,20 @@
      * @return True if we may show an activity while Keyguard is showing because we are in the
      *         process of dismissing it anyways, false otherwise.
      */
-    boolean canShowActivityWhileKeyguardShowing(boolean dismissKeyguard) {
-        return dismissKeyguard && canDismissKeyguard();
+    boolean canShowActivityWhileKeyguardShowing(ActivityRecord r, boolean dismissKeyguard) {
+
+        // Allow to show it when we are about to dismiss Keyguard. This isn't allowed if r is
+        // already the dismissing activity, in which case we don't allow it to repeatedly dismiss
+        // Keyguard.
+        return dismissKeyguard && canDismissKeyguard() &&
+                (mDismissalRequested || r != mDismissingKeyguardActivity);
+    }
+
+    /**
+     * @return True if we may show an activity while Keyguard is occluded, false otherwise.
+     */
+    boolean canShowWhileOccluded(boolean dismissKeyguard, boolean showWhenLocked) {
+        return showWhenLocked || dismissKeyguard && !mWindowManager.isKeyguardSecure();
     }
 
     private void visibilitiesUpdated() {
@@ -199,7 +210,14 @@
 
             // Only the very top activity may control occluded state
             if (stackNdx == topStackNdx) {
-                mOccluded = stack.topActivityOccludesKeyguard();
+
+                // A dismissing activity occludes Keyguard in the insecure case for legacy reasons.
+                final ActivityRecord topDismissing = stack.getTopDismissingKeyguardActivity();
+                mOccluded = stack.topActivityOccludesKeyguard()
+                        || (topDismissing != null
+                                && stack.topRunningActivityLocked() == topDismissing
+                                && canShowWhileOccluded(true /* dismissKeyguard */,
+                                        false /* showWhenLocked */));
             }
             if (mDismissingKeyguardActivity == null
                     && stack.getTopDismissingKeyguardActivity() != null) {
@@ -239,8 +257,13 @@
      * Called when somebody might want to dismiss the Keyguard.
      */
     private void handleDismissKeyguard() {
-        if (mDismissingKeyguardActivity != null) {
+        // We only allow dismissing Keyguard via the flag when Keyguard is secure for legacy
+        // reasons, because that's how apps used to dismiss Keyguard in the secure case. In the
+        // insecure case, we actually show it on top of the lockscreen. See #canShowWhileOccluded.
+        if (!mOccluded && mDismissingKeyguardActivity != null
+                && mWindowManager.isKeyguardSecure()) {
             mWindowManager.dismissKeyguard(null /* callback */);
+            mDismissalRequested = true;
 
             // If we are about to unocclude the Keyguard, but we can dismiss it without security,
             // we immediately dismiss the Keyguard so the activity gets shown without a flicker.
@@ -296,6 +319,7 @@
         pw.println(prefix + "  mKeyguardGoingAway=" + mKeyguardGoingAway);
         pw.println(prefix + "  mOccluded=" + mOccluded);
         pw.println(prefix + "  mDismissingKeyguardActivity=" + mDismissingKeyguardActivity);
+        pw.println(prefix + "  mDismissalRequested=" + mDismissalRequested);
         pw.println(prefix + "  mVisibilityTransactionDepth=" + mVisibilityTransactionDepth);
     }
 }
diff --git a/services/core/java/com/android/server/connectivity/NetworkMonitor.java b/services/core/java/com/android/server/connectivity/NetworkMonitor.java
index aa66917..5e98859 100644
--- a/services/core/java/com/android/server/connectivity/NetworkMonitor.java
+++ b/services/core/java/com/android/server/connectivity/NetworkMonitor.java
@@ -97,6 +97,24 @@
     private static final int SOCKET_TIMEOUT_MS = 10000;
     private static final int PROBE_TIMEOUT_MS  = 3000;
 
+    static enum EvaluationResult {
+        VALIDATED(true),
+        CAPTIVE_PORTAL(false);
+        final boolean isValidated;
+        EvaluationResult(boolean isValidated) {
+            this.isValidated = isValidated;
+        }
+    }
+
+    static enum ValidationStage {
+        FIRST_VALIDATION(true),
+        REVALIDATION(false);
+        final boolean isFirstValidation;
+        ValidationStage(boolean isFirstValidation) {
+            this.isFirstValidation = isFirstValidation;
+        }
+    }
+
     public static final String ACTION_NETWORK_CONDITIONS_MEASURED =
             "android.net.conn.NETWORK_CONDITIONS_MEASURED";
     public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type";
@@ -216,6 +234,8 @@
     protected boolean mIsCaptivePortalCheckEnabled;
 
     private boolean mUseHttps;
+    // The total number of captive portal detection attempts for this NetworkMonitor instance.
+    private int mValidations = 0;
 
     // Set if the user explicitly selected "Do not use this network" in captive portal sign-in app.
     private boolean mUserDoesNotWant = false;
@@ -290,6 +310,10 @@
         return validationLogs.readOnlyLocalLog();
     }
 
+    private ValidationStage validationStage() {
+        return 0 == mValidations ? ValidationStage.FIRST_VALIDATION : ValidationStage.REVALIDATION;
+    }
+
     // DefaultState is the parent of all States.  It exists only to handle CMD_* messages but
     // does not entail any real state (hence no enter() or exit() routines).
     private class DefaultState extends State {
@@ -366,9 +390,11 @@
     private class ValidatedState extends State {
         @Override
         public void enter() {
-            maybeLogEvaluationResult(NetworkEvent.NETWORK_VALIDATED);
+            maybeLogEvaluationResult(
+                    networkEventType(validationStage(), EvaluationResult.VALIDATED));
             mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED,
                     NETWORK_TEST_RESULT_VALID, mNetworkAgentInfo.network.netId, null));
+            mValidations++;
         }
 
         @Override
@@ -584,7 +610,8 @@
 
         @Override
         public void enter() {
-            maybeLogEvaluationResult(NetworkEvent.NETWORK_CAPTIVE_PORTAL_FOUND);
+            maybeLogEvaluationResult(
+                    networkEventType(validationStage(), EvaluationResult.CAPTIVE_PORTAL));
             // Don't annoy user with sign-in notifications.
             if (mDontDisplaySigninNotification) return;
             // Create a CustomIntentReceiver that sends us a
@@ -604,6 +631,7 @@
             // Retest for captive portal occasionally.
             sendMessageDelayed(CMD_CAPTIVE_PORTAL_RECHECK, 0 /* no UID */,
                     CAPTIVE_PORTAL_REEVALUATE_DELAY_MS);
+            mValidations++;
         }
 
         @Override
@@ -679,48 +707,13 @@
 
         long startTime = SystemClock.elapsedRealtime();
 
-        // Pre-resolve the captive portal server host so we can log it.
-        // Only do this if HttpURLConnection is about to, to avoid any potentially
-        // unnecessary resolution.
-        String hostToResolve = null;
+        final CaptivePortalProbeResult result;
         if (pacUrl != null) {
-            hostToResolve = pacUrl.getHost();
-        } else if (proxyInfo != null) {
-            hostToResolve = proxyInfo.getHost();
-        } else {
-            hostToResolve = httpUrl.getHost();
-        }
-
-        if (!TextUtils.isEmpty(hostToResolve)) {
-            String probeName = ValidationProbeEvent.getProbeName(ValidationProbeEvent.PROBE_DNS);
-            final Stopwatch dnsTimer = new Stopwatch().start();
-            int dnsResult;
-            long dnsLatency;
-            try {
-                InetAddress[] addresses = mNetworkAgentInfo.network.getAllByName(hostToResolve);
-                dnsResult = ValidationProbeEvent.DNS_SUCCESS;
-                dnsLatency = dnsTimer.stop();
-                final StringBuffer connectInfo = new StringBuffer(", " + hostToResolve + "=");
-                for (InetAddress address : addresses) {
-                    connectInfo.append(address.getHostAddress());
-                    if (address != addresses[addresses.length-1]) connectInfo.append(",");
-                }
-                validationLog(probeName + " OK " + dnsLatency + "ms" + connectInfo);
-            } catch (UnknownHostException e) {
-                dnsResult = ValidationProbeEvent.DNS_FAILURE;
-                dnsLatency = dnsTimer.stop();
-                validationLog(probeName + " FAIL " + dnsLatency + "ms, " + hostToResolve);
-            }
-            logValidationProbe(dnsLatency, ValidationProbeEvent.PROBE_DNS, dnsResult);
-        }
-
-        CaptivePortalProbeResult result;
-        if (pacUrl != null) {
-            result = sendHttpProbe(pacUrl, ValidationProbeEvent.PROBE_PAC);
+            result = sendDnsAndHttpProbes(null, pacUrl, ValidationProbeEvent.PROBE_PAC);
         } else if (mUseHttps) {
-            result = sendParallelHttpProbes(httpsUrl, httpUrl, fallbackUrl);
+            result = sendParallelHttpProbes(proxyInfo, httpsUrl, httpUrl, fallbackUrl);
         } else {
-            result = sendHttpProbe(httpUrl, ValidationProbeEvent.PROBE_HTTP);
+            result = sendDnsAndHttpProbes(proxyInfo, httpUrl, ValidationProbeEvent.PROBE_HTTP);
         }
 
         long endTime = SystemClock.elapsedRealtime();
@@ -733,8 +726,50 @@
     }
 
     /**
-     * Do a URL fetch on a known server to see if we get the data we expect.
-     * Returns HTTP response code.
+     * Do a DNS resolution and URL fetch on a known web server to see if we get the data we expect.
+     * @return a CaptivePortalProbeResult inferred from the HTTP response.
+     */
+    private CaptivePortalProbeResult sendDnsAndHttpProbes(ProxyInfo proxy, URL url, int probeType) {
+        // Pre-resolve the captive portal server host so we can log it.
+        // Only do this if HttpURLConnection is about to, to avoid any potentially
+        // unnecessary resolution.
+        final String host = (proxy != null) ? proxy.getHost() : url.getHost();
+        sendDnsProbe(host);
+        return sendHttpProbe(url, probeType);
+    }
+
+    /** Do a DNS resolution of the given server. */
+    private void sendDnsProbe(String host) {
+        if (TextUtils.isEmpty(host)) {
+            return;
+        }
+
+        final String name = ValidationProbeEvent.getProbeName(ValidationProbeEvent.PROBE_DNS);
+        final Stopwatch watch = new Stopwatch().start();
+        int result;
+        String connectInfo;
+        try {
+            InetAddress[] addresses = mNetworkAgentInfo.network.getAllByName(host);
+            result = ValidationProbeEvent.DNS_SUCCESS;
+            StringBuffer buffer = new StringBuffer(host).append("=");
+            for (InetAddress address : addresses) {
+                buffer.append(address.getHostAddress());
+                if (address != addresses[addresses.length-1]) buffer.append(",");
+            }
+            connectInfo = buffer.toString();
+        } catch (UnknownHostException e) {
+            result = ValidationProbeEvent.DNS_FAILURE;
+            connectInfo = host;
+        }
+        final long latency = watch.stop();
+        String resultString = (ValidationProbeEvent.DNS_SUCCESS == result) ? "OK" : "FAIL";
+        validationLog(String.format("%s %s %dms, %s", name, resultString, latency, connectInfo));
+        logValidationProbe(latency, ValidationProbeEvent.PROBE_DNS, result);
+    }
+
+    /**
+     * Do a URL fetch on a known web server to see if we get the data we expect.
+     * @return a CaptivePortalProbeResult inferred from the HTTP response.
      */
     @VisibleForTesting
     protected CaptivePortalProbeResult sendHttpProbe(URL url, int probeType) {
@@ -801,7 +836,7 @@
     }
 
     private CaptivePortalProbeResult sendParallelHttpProbes(
-            URL httpsUrl, URL httpUrl, URL fallbackUrl) {
+            ProxyInfo proxy, URL httpsUrl, URL httpUrl, URL fallbackUrl) {
         // Number of probes to wait for. If a probe completes with a conclusive answer
         // it shortcuts the latch immediately by forcing the count to 0.
         final CountDownLatch latch = new CountDownLatch(2);
@@ -821,9 +856,10 @@
             @Override
             public void run() {
                 if (mIsHttps) {
-                    mResult = sendHttpProbe(httpsUrl, ValidationProbeEvent.PROBE_HTTPS);
+                    mResult =
+                            sendDnsAndHttpProbes(proxy, httpsUrl, ValidationProbeEvent.PROBE_HTTPS);
                 } else {
-                    mResult = sendHttpProbe(httpUrl, ValidationProbeEvent.PROBE_HTTP);
+                    mResult = sendDnsAndHttpProbes(proxy, httpUrl, ValidationProbeEvent.PROBE_HTTP);
                 }
                 if ((mIsHttps && mResult.isSuccessful()) || (!mIsHttps && mResult.isPortal())) {
                     // Stop waiting immediately if https succeeds or if http finds a portal.
@@ -974,6 +1010,22 @@
         mMetricsLog.log(new NetworkEvent(mNetId, evtype));
     }
 
+    private int networkEventType(ValidationStage s, EvaluationResult r) {
+        if (s.isFirstValidation) {
+            if (r.isValidated) {
+                return NetworkEvent.NETWORK_FIRST_VALIDATION_SUCCESS;
+            } else {
+                return NetworkEvent.NETWORK_FIRST_VALIDATION_PORTAL_FOUND;
+            }
+        } else {
+            if (r.isValidated) {
+                return NetworkEvent.NETWORK_REVALIDATION_SUCCESS;
+            } else {
+                return NetworkEvent.NETWORK_REVALIDATION_PORTAL_FOUND;
+            }
+        }
+    }
+
     private void maybeLogEvaluationResult(int evtype) {
         if (mEvaluationTimer.isRunning()) {
             mMetricsLog.log(new NetworkEvent(mNetId, evtype, mEvaluationTimer.stop()));
@@ -982,6 +1034,8 @@
     }
 
     private void logValidationProbe(long durationMs, int probeType, int probeResult) {
+        probeType =
+                ValidationProbeEvent.makeProbeType(probeType, validationStage().isFirstValidation);
         mMetricsLog.log(new ValidationProbeEvent(mNetId, durationMs, probeType, probeResult));
     }
 }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index dc25ce4..dd410e2 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -9086,6 +9086,13 @@
                 PackageParser.PermissionGroup pg = pkg.permissionGroups.get(i);
                 PackageParser.PermissionGroup cur = mPermissionGroups.get(pg.info.name);
                 final String curPackageName = cur == null ? null : cur.info.packageName;
+                // Dont allow ephemeral apps to define new permission groups.
+                if (pkg.applicationInfo.isEphemeralApp()) {
+                    Slog.w(TAG, "Permission group " + pg.info.name + " from package "
+                            + pg.info.packageName
+                            + " ignored: ephemeral apps cannot define new permission groups.");
+                    continue;
+                }
                 final boolean isPackageUpdate = pg.info.packageName.equals(curPackageName);
                 if (cur == null || isPackageUpdate) {
                     mPermissionGroups.put(pg.info.name, pg);
@@ -9124,6 +9131,14 @@
             for (i=0; i<N; i++) {
                 PackageParser.Permission p = pkg.permissions.get(i);
 
+                // Dont allow ephemeral apps to define new permissions.
+                if (pkg.applicationInfo.isEphemeralApp()) {
+                    Slog.w(TAG, "Permission " + p.info.name + " from package "
+                            + p.info.packageName
+                            + " ignored: ephemeral apps cannot define new permissions.");
+                    continue;
+                }
+
                 // Assume by default that we did not install this permission into the system.
                 p.info.flags &= ~PermissionInfo.FLAG_INSTALLED;
 
diff --git a/services/core/java/com/android/server/pm/ShortcutPackageInfo.java b/services/core/java/com/android/server/pm/ShortcutPackageInfo.java
index 4de15de..e5a2f5a 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackageInfo.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackageInfo.java
@@ -23,7 +23,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.backup.BackupUtils;
 
-import libcore.io.Base64;
 import libcore.util.HexEncoding;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -33,6 +32,7 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Base64;
 
 /**
  * Package information used by {@link android.content.pm.ShortcutManager} for backup / restore.
@@ -161,7 +161,8 @@
 
         for (int i = 0; i < mSigHashes.size(); i++) {
             out.startTag(null, TAG_SIGNATURE);
-            ShortcutService.writeAttr(out, ATTR_SIGNATURE_HASH, Base64.encode(mSigHashes.get(i)));
+            final String encoded = Base64.getEncoder().encodeToString(mSigHashes.get(i));
+            ShortcutService.writeAttr(out, ATTR_SIGNATURE_HASH, encoded);
             out.endTag(null, TAG_SIGNATURE);
         }
         out.endTag(null, TAG_ROOT);
@@ -196,7 +197,9 @@
                     case TAG_SIGNATURE: {
                         final String hash = ShortcutService.parseStringAttribute(
                                 parser, ATTR_SIGNATURE_HASH);
-                        hashes.add(Base64.decode(hash.getBytes()));
+                        // Throws IllegalArgumentException if hash is invalid base64 data
+                        final byte[] decoded = Base64.getDecoder().decode(hash);
+                        hashes.add(decoded);
                         continue;
                     }
                 }
diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
index 9fe0922..7ec3c19 100644
--- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
+++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
@@ -71,6 +71,7 @@
             UserManager.DISALLOW_SHARE_LOCATION,
             UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
             UserManager.DISALLOW_CONFIG_BLUETOOTH,
+            UserManager.DISALLOW_BLUETOOTH,
             UserManager.DISALLOW_USB_FILE_TRANSFER,
             UserManager.DISALLOW_CONFIG_CREDENTIALS,
             UserManager.DISALLOW_REMOVE_USER,
@@ -117,6 +118,7 @@
      * User restrictions that can not be set by profile owners.
      */
     private static final Set<String> DEVICE_OWNER_ONLY_RESTRICTIONS = Sets.newArraySet(
+            UserManager.DISALLOW_BLUETOOTH,
             UserManager.DISALLOW_USB_FILE_TRANSFER,
             UserManager.DISALLOW_CONFIG_TETHERING,
             UserManager.DISALLOW_NETWORK_RESET,
diff --git a/services/net/java/android/net/apf/ApfFilter.java b/services/net/java/android/net/apf/ApfFilter.java
index a8356dc..83001df 100644
--- a/services/net/java/android/net/apf/ApfFilter.java
+++ b/services/net/java/android/net/apf/ApfFilter.java
@@ -194,8 +194,10 @@
             { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
 
     private static final int ICMP6_TYPE_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN;
-    private static final int ICMP6_NEIGHBOR_ANNOUNCEMENT = 136;
+    private static final int ICMP6_ROUTER_SOLICITATION = 133;
     private static final int ICMP6_ROUTER_ADVERTISEMENT = 134;
+    private static final int ICMP6_NEIGHBOR_SOLICITATION = 135;
+    private static final int ICMP6_NEIGHBOR_ANNOUNCEMENT = 136;
 
     // NOTE: this must be added to the IPv4 header length in IPV4_HEADER_SIZE_MEMORY_SLOT
     private static final int UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 2;
@@ -805,6 +807,8 @@
         //   if it's multicast and we're dropping multicast:
         //     drop
         //   pass
+        // if it's ICMPv6 RS to any:
+        //   drop
         // if it's ICMPv6 NA to ff02::1:
         //   drop
 
@@ -829,10 +833,12 @@
 
         // Add unsolicited multicast neighbor announcements filter
         String skipUnsolicitedMulticastNALabel = "skipUnsolicitedMulticastNA";
-        // If not neighbor announcements, skip unsolicited multicast NA filter
         gen.addLoad8(Register.R0, ICMP6_TYPE_OFFSET);
+        // Drop all router solicitations (b/32833400)
+        gen.addJumpIfR0Equals(ICMP6_ROUTER_SOLICITATION, gen.DROP_LABEL);
+        // If not neighbor announcements, skip filter.
         gen.addJumpIfR0NotEquals(ICMP6_NEIGHBOR_ANNOUNCEMENT, skipUnsolicitedMulticastNALabel);
-        // If to ff02::1, drop
+        // If to ff02::1, drop.
         // TODO: Drop only if they don't contain the address of on-link neighbours.
         gen.addLoadImmediate(Register.R0, IPV6_DEST_ADDR_OFFSET);
         gen.addJumpIfBytesNotEqual(Register.R0, IPV6_ALL_NODES_ADDRESS,
@@ -852,6 +858,7 @@
      * <li>Pass all non-ICMPv6 IPv6 packets,
      * <li>Pass all non-IPv4 and non-IPv6 packets,
      * <li>Drop IPv6 ICMPv6 NAs to ff02::1.
+     * <li>Drop IPv6 ICMPv6 RSs.
      * <li>Let execution continue off the end of the program for IPv6 ICMPv6 packets. This allows
      *     insertion of RA filters here, or if there aren't any, just passes the packets.
      * </ul>
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/GestureDescriptionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/GestureDescriptionTest.java
new file mode 100644
index 0000000..b876a5f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/GestureDescriptionTest.java
@@ -0,0 +1,420 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility;
+
+import static android.accessibilityservice.GestureDescription.StrokeDescription.INVALID_STROKE_ID;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.everyItem;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.accessibilityservice.GestureDescription;
+import android.accessibilityservice.GestureDescription.GestureStep;
+import android.accessibilityservice.GestureDescription.MotionEventGenerator;
+import android.accessibilityservice.GestureDescription.StrokeDescription;
+import android.graphics.Path;
+import android.graphics.PointF;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Test;
+
+import java.util.List;
+
+import static junit.framework.TestCase.assertEquals;
+
+/**
+ * Tests for GestureDescription
+ */
+public class GestureDescriptionTest {
+    @Test
+    public void testGestureShorterThanSampleRate_producesStartAndEnd() {
+        PointF click = new PointF(10, 20);
+        Path clickPath = new Path();
+        clickPath.moveTo(click.x, click.y);
+        StrokeDescription clickStroke = new StrokeDescription(clickPath, 0, 10);
+        GestureDescription.Builder clickBuilder = new GestureDescription.Builder();
+        clickBuilder.addStroke(clickStroke);
+        GestureDescription clickGesture = clickBuilder.build();
+
+        List<GestureStep> clickGestureSteps = MotionEventGenerator
+                .getGestureStepsFromGestureDescription(clickGesture, 100);
+
+        assertEquals(2, clickGestureSteps.size());
+        assertThat(clickGestureSteps.get(0), allOf(numTouchPointsIs(1), numStartsOfStroke(1),
+                numEndsOfStroke(0), hasPoint(click)));
+        assertThat(clickGestureSteps.get(1), allOf(numTouchPointsIs(1), numStartsOfStroke(0),
+                numEndsOfStroke(1), hasPoint(click)));
+    }
+
+    @Test
+    public void testSwipe_shouldContainEvenlySpacedPoints() {
+        int samplePeriod = 10;
+        int numSamples = 5;
+        float stepX = 2;
+        float stepY = 3;
+        PointF start = new PointF(10, 20);
+        PointF end = new PointF(10 + numSamples * stepX, 20 + numSamples * stepY);
+
+        GestureDescription swipe =
+                createSwipe(start.x, start.y, end.x, end.y, numSamples * samplePeriod);
+        List<GestureStep> swipeGestureSteps = MotionEventGenerator
+                .getGestureStepsFromGestureDescription(swipe, samplePeriod);
+        assertEquals(numSamples + 1, swipeGestureSteps.size());
+
+        assertThat(swipeGestureSteps.get(0), allOf(numTouchPointsIs(1), numStartsOfStroke(1),
+                numEndsOfStroke(0), hasPoint(start)));
+        assertThat(swipeGestureSteps.get(numSamples), allOf(numTouchPointsIs(1),
+                numStartsOfStroke(0), numEndsOfStroke(1), hasPoint(end)));
+
+        for (int i = 1; i < numSamples; ++i) {
+            PointF interpPoint = new PointF(start.x + stepX * i, start.y + stepY * i);
+            assertThat(swipeGestureSteps.get(i), allOf(numTouchPointsIs(1),
+                    numStartsOfStroke(0), numEndsOfStroke(0), hasPoint(interpPoint)));
+        }
+    }
+
+    @Test
+    public void testSwipeWithNonIntegerValues_shouldRound() {
+        int strokeTime = 10;
+
+        GestureDescription swipe = createSwipe(10.1f, 20.6f, 11.9f, 22.1f, strokeTime);
+        List<GestureStep> swipeGestureSteps = MotionEventGenerator
+                .getGestureStepsFromGestureDescription(swipe, strokeTime);
+        assertEquals(2, swipeGestureSteps.size());
+        assertThat(swipeGestureSteps.get(0), hasPoint(new PointF(10, 21)));
+        assertThat(swipeGestureSteps.get(1), hasPoint(new PointF(12, 22)));
+    }
+
+    @Test
+    public void testPathsWithOverlappingTiming_produceCorrectSteps() {
+        // There are 4 paths
+        // 0: an L-shaped path that starts first
+        // 1: a swipe that starts in the middle of the L-shaped path and ends when the L ends
+        // 2: a swipe that starts at the same time as #1 but extends past the end of the L
+        // 3: a swipe that starts when #3 ends
+        PointF path0Start = new PointF(100, 150);
+        PointF path0Turn = new PointF(100, 200);
+        PointF path0End = new PointF(250, 200);
+        int path0StartTime = 0;
+        int path0EndTime = 100;
+        int path0Duration = path0EndTime - path0StartTime;
+        Path path0 = new Path();
+        path0.moveTo(path0Start.x, path0Start.y);
+        path0.lineTo(path0Turn.x, path0Turn.y);
+        path0.lineTo(path0End.x, path0End.y);
+        StrokeDescription path0Stroke = new StrokeDescription(path0, path0StartTime, path0Duration);
+
+        PointF path1Start = new PointF(300, 350);
+        PointF path1End = new PointF(300, 400);
+        int path1StartTime = 50;
+        int path1EndTime = path0EndTime;
+        StrokeDescription path1Stroke = createSwipeStroke(
+                path1Start.x, path1Start.y, path1End.x, path1End.y, path1StartTime, path1EndTime);
+
+        PointF path2Start = new PointF(400, 450);
+        PointF path2End = new PointF(400, 500);
+        int path2StartTime = 50;
+        int path2EndTime = 150;
+        StrokeDescription path2Stroke = createSwipeStroke(
+                path2Start.x, path2Start.y, path2End.x, path2End.y, path2StartTime, path2EndTime);
+
+        PointF path3Start = new PointF(500, 550);
+        PointF path3End = new PointF(500, 600);
+        int path3StartTime = path2EndTime;
+        int path3EndTime = 200;
+        StrokeDescription path3Stroke = createSwipeStroke(
+                path3Start.x, path3Start.y, path3End.x, path3End.y, path3StartTime, path3EndTime);
+
+        int deltaT = 12; // Force samples to happen on extra boundaries
+        GestureDescription.Builder builder = new GestureDescription.Builder();
+        builder.addStroke(path0Stroke);
+        builder.addStroke(path1Stroke);
+        builder.addStroke(path2Stroke);
+        builder.addStroke(path3Stroke);
+        List<GestureStep> steps = MotionEventGenerator
+                .getGestureStepsFromGestureDescription(builder.build(), deltaT);
+
+        long start = 0;
+        assertThat(steps.get(0), allOf(numStartsOfStroke(1), numEndsOfStroke(0), isAtTime(start),
+                numTouchPointsIs(1), hasPoint(path0Start)));
+        assertThat(steps.get(1), allOf(numTouchPointsIs(1), noStartsOrEnds(),
+                isAtTime(start + deltaT)));
+        assertThat(steps.get(2), allOf(numTouchPointsIs(1), isAtTime(start + deltaT * 2)));
+        assertThat(steps.get(3), allOf(numTouchPointsIs(1), isAtTime(start + deltaT * 3)));
+        assertThat(steps.get(4), allOf(numTouchPointsIs(1), isAtTime(start + deltaT * 4)));
+
+        assertThat(steps.get(5), allOf(numTouchPointsIs(3), numStartsOfStroke(2),
+                numEndsOfStroke(0), isAtTime(path1StartTime), hasPoint(path1Start),
+                hasPoint(path2Start)));
+
+        start = path1StartTime;
+        assertThat(steps.get(6), allOf(numTouchPointsIs(3), isAtTime(start + deltaT * 1)));
+        assertThat(steps.get(7), allOf(noStartsOrEnds(), isAtTime(start + deltaT * 2)));
+        assertThat(steps.get(8), allOf(numTouchPointsIs(3), isAtTime(start + deltaT * 3)));
+        assertThat(steps.get(9), allOf(noStartsOrEnds(), isAtTime(start + deltaT * 4)));
+
+        assertThat(steps.get(10), allOf(numTouchPointsIs(3), numStartsOfStroke(0),
+                numEndsOfStroke(2), isAtTime(path0EndTime), hasPoint(path0End),
+                hasPoint(path1End)));
+
+        start = path0EndTime;
+        assertThat(steps.get(11), allOf(numTouchPointsIs(1), isAtTime(start + deltaT * 1)));
+        assertThat(steps.get(12), allOf(noStartsOrEnds(), isAtTime(start + deltaT * 2)));
+        assertThat(steps.get(13), allOf(numTouchPointsIs(1), isAtTime(start + deltaT * 3)));
+        assertThat(steps.get(14), allOf(noStartsOrEnds(), isAtTime(start + deltaT * 4)));
+
+        assertThat(steps.get(15), allOf(numTouchPointsIs(2), numStartsOfStroke(1),
+                numEndsOfStroke(1), isAtTime(path2EndTime), hasPoint(path2End),
+                hasPoint(path3Start)));
+
+        start = path2EndTime;
+        assertThat(steps.get(16), allOf(numTouchPointsIs(1), isAtTime(start + deltaT * 1)));
+        assertThat(steps.get(17), allOf(noStartsOrEnds(), isAtTime(start + deltaT * 2)));
+        assertThat(steps.get(18), allOf(numTouchPointsIs(1), isAtTime(start + deltaT * 3)));
+        assertThat(steps.get(19), allOf(noStartsOrEnds(), isAtTime(start + deltaT * 4)));
+
+        assertThat(steps.get(20), allOf(numTouchPointsIs(1), numStartsOfStroke(0),
+                numEndsOfStroke(1), isAtTime(path3EndTime), hasPoint(path3End)));
+    }
+
+    @Test
+    public void testMaxTouchpoints_shouldHaveValidCoords() {
+        GestureDescription.Builder maxPointBuilder = new GestureDescription.Builder();
+        PointF baseStartPoint = new PointF(100, 100);
+        PointF baseEndPoint = new PointF(100, 200);
+        int xStep = 10;
+        int samplePeriod = 15;
+        int numSamples = 2;
+        int numPoints = GestureDescription.getMaxStrokeCount();
+        for (int i = 0; i < numPoints; i++) {
+            Path path = new Path();
+            path.moveTo(baseStartPoint.x + i * xStep, baseStartPoint.y);
+            path.lineTo(baseEndPoint.x + i * xStep, baseEndPoint.y);
+            maxPointBuilder.addStroke(new StrokeDescription(path, 0, samplePeriod * numSamples));
+        }
+
+        List<GestureStep> steps = MotionEventGenerator
+                .getGestureStepsFromGestureDescription(maxPointBuilder.build(), samplePeriod);
+        assertEquals(3, steps.size());
+
+        assertThat(steps.get(0), allOf(numTouchPointsIs(numPoints), numStartsOfStroke(numPoints),
+                numEndsOfStroke(0), isAtTime(0)));
+        assertThat(steps.get(1), allOf(numTouchPointsIs(numPoints), numStartsOfStroke(0),
+                numEndsOfStroke(0), isAtTime(samplePeriod)));
+        assertThat(steps.get(2), allOf(numTouchPointsIs(numPoints), numStartsOfStroke(0),
+                numEndsOfStroke(numPoints), isAtTime(samplePeriod * 2)));
+
+        PointF baseMidPoint = new PointF((baseStartPoint.x + baseEndPoint.x) / 2,
+                (baseStartPoint.y + baseEndPoint.y) / 2);
+        for (int i = 0; i < numPoints; i++) {
+            assertThat(steps.get(0),
+                    hasPoint(new PointF(baseStartPoint.x + i * xStep, baseStartPoint.y)));
+            assertThat(steps.get(1),
+                    hasPoint(new PointF(baseMidPoint.x + i * xStep, baseMidPoint.y)));
+            assertThat(steps.get(2),
+                    hasPoint(new PointF(baseEndPoint.x + i * xStep, baseEndPoint.y)));
+        }
+    }
+
+    @Test
+    public void testGetGestureSteps_touchPointsHaveStrokeId() {
+        StrokeDescription swipeStroke = createSwipeStroke(10, 20, 30, 40, 0, 100);
+        GestureDescription swipe = new GestureDescription.Builder().addStroke(swipeStroke).build();
+        List<GestureStep> swipeGestureSteps = MotionEventGenerator
+                .getGestureStepsFromGestureDescription(swipe, 10);
+
+        assertThat(swipeGestureSteps, everyItem(hasStrokeId(swipeStroke.getId())));
+    }
+
+    @Test
+    public void testGetGestureSteps_continuedStroke_hasNoEndPoint() {
+        Path swipePath = new Path();
+        swipePath.moveTo(10, 20);
+        swipePath.lineTo(30, 40);
+        StrokeDescription stroke1 =
+                new StrokeDescription(swipePath, 0, 100, 0, true);
+        GestureDescription gesture = new GestureDescription.Builder().addStroke(stroke1).build();
+        List<GestureStep> steps = MotionEventGenerator
+                .getGestureStepsFromGestureDescription(gesture, 10);
+
+        assertThat(steps, everyItem(numEndsOfStroke(0)));
+    }
+
+    @Test
+    public void testGetGestureSteps_continuingStroke_hasNoStartPointAndHasContinuedId() {
+        Path swipePath = new Path();
+        swipePath.moveTo(10, 20);
+        swipePath.lineTo(30, 40);
+        StrokeDescription stroke1 =
+                new StrokeDescription(swipePath, 0, 100, INVALID_STROKE_ID, true);
+        StrokeDescription stroke2 =
+                new StrokeDescription(swipePath, 0, 100, stroke1.getId(), false);
+        GestureDescription gesture = new GestureDescription.Builder().addStroke(stroke2).build();
+        List<GestureStep> steps = MotionEventGenerator
+                .getGestureStepsFromGestureDescription(gesture, 10);
+
+        assertThat(steps, everyItem(
+                allOf(continuesStrokeId(stroke1.getId()), numStartsOfStroke(0))));
+    }
+
+    private GestureDescription createSwipe(
+            float startX, float startY, float endX, float endY, long duration) {
+        GestureDescription.Builder swipeBuilder = new GestureDescription.Builder();
+        swipeBuilder.addStroke(createSwipeStroke(startX, startY, endX, endY, 0, duration));
+        return swipeBuilder.build();
+    }
+
+    private StrokeDescription createSwipeStroke(
+            float startX, float startY, float endX, float endY, long startTime, long endTime) {
+        Path swipePath = new Path();
+        swipePath.moveTo(startX, startY);
+        swipePath.lineTo(endX, endY);
+        StrokeDescription swipeStroke =
+                new StrokeDescription(swipePath, startTime, endTime - startTime);
+        return swipeStroke;
+    }
+
+    Matcher<GestureStep> numTouchPointsIs(final int numTouchPoints) {
+        return new TypeSafeMatcher<GestureStep>() {
+            @Override
+            protected boolean matchesSafely(GestureStep gestureStep) {
+                return gestureStep.numTouchPoints == numTouchPoints;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Has " + numTouchPoints + " touch point(s)");
+            }
+        };
+    }
+
+    Matcher<GestureStep> numStartsOfStroke(final int numStarts) {
+        return new TypeSafeMatcher<GestureStep>() {
+            @Override
+            protected boolean matchesSafely(GestureStep gestureStep) {
+                int numStartsFound = 0;
+                for (int i = 0; i < gestureStep.numTouchPoints; i++) {
+                    if (gestureStep.touchPoints[i].mIsStartOfPath) {
+                        numStartsFound++;
+                    }
+                }
+                return numStartsFound == numStarts;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Starts " + numStarts + " stroke(s)");
+            }
+        };
+    }
+
+    Matcher<GestureStep> numEndsOfStroke(final int numEnds) {
+        return new TypeSafeMatcher<GestureStep>() {
+            @Override
+            protected boolean matchesSafely(GestureStep gestureStep) {
+                int numEndsFound = 0;
+                for (int i = 0; i < gestureStep.numTouchPoints; i++) {
+                    if (gestureStep.touchPoints[i].mIsEndOfPath) {
+                        numEndsFound++;
+                    }
+                }
+                return numEndsFound == numEnds;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Ends " + numEnds + " stroke(s)");
+            }
+        };
+    }
+
+    Matcher<GestureStep> hasPoint(final PointF point) {
+        return new TypeSafeMatcher<GestureStep>() {
+            @Override
+            protected boolean matchesSafely(GestureStep gestureStep) {
+                for (int i = 0; i < gestureStep.numTouchPoints; i++) {
+                    if ((gestureStep.touchPoints[i].mX == point.x)
+                            && (gestureStep.touchPoints[i].mY == point.y)) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Has at least one point at " + point);
+            }
+        };
+    }
+
+    Matcher<GestureStep> hasStrokeId(final int strokeId) {
+        return new TypeSafeMatcher<GestureStep>() {
+            @Override
+            protected boolean matchesSafely(GestureStep gestureStep) {
+                for (int i = 0; i < gestureStep.numTouchPoints; i++) {
+                    if (gestureStep.touchPoints[i].mStrokeId == strokeId) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Has at least one point with stroke id " + strokeId);
+            }
+        };
+    }
+
+    Matcher<GestureStep> continuesStrokeId(final int strokeId) {
+        return new TypeSafeMatcher<GestureStep>() {
+            @Override
+            protected boolean matchesSafely(GestureStep gestureStep) {
+                for (int i = 0; i < gestureStep.numTouchPoints; i++) {
+                    if (gestureStep.touchPoints[i].mContinuedStrokeId == strokeId) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Continues stroke id " + strokeId);
+            }
+        };
+    }
+
+    Matcher<GestureStep> isAtTime(final long time) {
+        return new TypeSafeMatcher<GestureStep>() {
+            @Override
+            protected boolean matchesSafely(GestureStep gestureStep) {
+                return gestureStep.timeSinceGestureStart == time;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Is at time " + time);
+            }
+        };
+    }
+
+    Matcher<GestureStep> noStartsOrEnds() {
+        return allOf(numStartsOfStroke(0), numEndsOfStroke(0));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java b/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java
index d5305d9..73344e0 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/MotionEventInjectorTest.java
@@ -16,9 +16,17 @@
 
 package com.android.server.accessibility;
 
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
+import static android.view.WindowManagerPolicy.FLAG_PASS_TO_USER;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.everyItem;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.argThat;
 import static org.mockito.Matchers.eq;
@@ -29,7 +37,10 @@
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
 
+import android.accessibilityservice.GestureDescription.GestureStep;
+import android.accessibilityservice.GestureDescription.TouchPoint;
 import android.accessibilityservice.IAccessibilityServiceClient;
+import android.graphics.Point;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -40,11 +51,15 @@
 import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
-import android.view.WindowManagerPolicy;
+
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 import android.view.accessibility.AccessibilityEvent;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -58,30 +73,59 @@
 @RunWith(AndroidJUnit4.class)
 public class MotionEventInjectorTest {
     private static final String LOG_TAG = "MotionEventInjectorTest";
-    private static final int CLICK_X = 100;
-    private static final int CLICK_Y_START = 200;
-    private static final int CLICK_Y_END = 201;
-    private static final int CLICK_DURATION = 10;
-    private static final int SEQUENCE = 50;
+    private static final Matcher<MotionEvent> IS_ACTION_DOWN =
+            new MotionEventActionMatcher(ACTION_DOWN);
+    private static final Matcher<MotionEvent> IS_ACTION_POINTER_DOWN =
+            new MotionEventActionMatcher(MotionEvent.ACTION_POINTER_DOWN);
+    private static final Matcher<MotionEvent> IS_ACTION_UP =
+            new MotionEventActionMatcher(ACTION_UP);
+    private static final Matcher<MotionEvent> IS_ACTION_POINTER_UP =
+            new MotionEventActionMatcher(MotionEvent.ACTION_POINTER_UP);
+    private static final Matcher<MotionEvent> IS_ACTION_CANCEL =
+            new MotionEventActionMatcher(MotionEvent.ACTION_CANCEL);
+    private static final Matcher<MotionEvent> IS_ACTION_MOVE =
+            new MotionEventActionMatcher(MotionEvent.ACTION_MOVE);
 
-    private static final int SECOND_CLICK_X = 1000;
-    private static final int SECOND_CLICK_Y = 2000;
-    private static final int SECOND_SEQUENCE = 51;
+    private static final Point LINE_START = new Point(100, 200);
+    private static final Point LINE_END = new Point(100, 300);
+    private static final int LINE_DURATION = 100;
+    private static final int LINE_SEQUENCE = 50;
+
+    private static final Point CLICK_POINT = new Point(1000, 2000);
+    private static final int CLICK_DURATION = 10;
+    private static final int CLICK_SEQUENCE = 51;
 
     private static final int MOTION_EVENT_SOURCE = InputDevice.SOURCE_TOUCHSCREEN;
     private static final int OTHER_EVENT_SOURCE = InputDevice.SOURCE_MOUSE;
 
+    private static final Point CONTINUED_LINE_START = new Point(500, 300);
+    private static final Point CONTINUED_LINE_MID1 = new Point(500, 400);
+    private static final Point CONTINUED_LINE_MID2 = new Point(600, 300);
+    private static final Point CONTINUED_LINE_END = new Point(600, 400);
+    private static final int CONTINUED_LINE_STROKE_ID_1 = 100;
+    private static final int CONTINUED_LINE_STROKE_ID_2 = 101;
+    private static final int CONTINUED_LINE_INTERVAL = 100;
+    private static final int CONTINUED_LINE_SEQUENCE_1 = 52;
+    private static final int CONTINUED_LINE_SEQUENCE_2 = 53;
+
     MotionEventInjector mMotionEventInjector;
     IAccessibilityServiceClient mServiceInterface;
-    List<MotionEvent> mClickList = new ArrayList<>();
-    List<MotionEvent> mSecondClickList = new ArrayList<>();
+    List<GestureStep> mLineList = new ArrayList<>();
+    List<GestureStep> mClickList = new ArrayList<>();
+    List<GestureStep> mContinuedLineList1 = new ArrayList<>();
+    List<GestureStep> mContinuedLineList2 = new ArrayList<>();
+
+    MotionEvent mClickDownEvent;
+    MotionEvent mClickUpEvent;
+
     ArgumentCaptor<MotionEvent> mCaptor1 = ArgumentCaptor.forClass(MotionEvent.class);
     ArgumentCaptor<MotionEvent> mCaptor2 = ArgumentCaptor.forClass(MotionEvent.class);
     MessageCapturingHandler mMessageCapturingHandler;
-    MotionEventMatcher mClickEvent0Matcher;
-    MotionEventMatcher mClickEvent1Matcher;
-    MotionEventMatcher mClickEvent2Matcher;
-    MotionEventMatcher mSecondClickEvent0Matcher;
+    Matcher<MotionEvent> mIsLineStart;
+    Matcher<MotionEvent> mIsLineMiddle;
+    Matcher<MotionEvent> mIsLineEnd;
+    Matcher<MotionEvent> mIsClickDown;
+    Matcher<MotionEvent> mIsClickUp;
 
     @BeforeClass
     public static void oneTimeInitialization() {
@@ -99,226 +143,191 @@
             }
         });
         mMotionEventInjector = new MotionEventInjector(mMessageCapturingHandler);
-        mClickList.add(
-                MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, CLICK_X, CLICK_Y_START, 0));
-        mClickList.add(MotionEvent.obtain(
-                0, CLICK_DURATION, MotionEvent.ACTION_MOVE, CLICK_X, CLICK_Y_END, 0));
-        mClickList.add(MotionEvent.obtain(
-                0, CLICK_DURATION, MotionEvent.ACTION_UP, CLICK_X, CLICK_Y_END, 0));
-        for (int i = 0; i < mClickList.size(); i++) {
-            mClickList.get(i).setSource(MOTION_EVENT_SOURCE);
-        }
-
-        mClickEvent0Matcher = new MotionEventMatcher(mClickList.get(0));
-        mClickEvent1Matcher = new MotionEventMatcher(mClickList.get(1));
-        mClickEvent2Matcher = new MotionEventMatcher(mClickList.get(2));
-
-        mSecondClickList.add(MotionEvent.obtain(
-                0, 0, MotionEvent.ACTION_DOWN, SECOND_CLICK_X, SECOND_CLICK_Y, 0));
-        mSecondClickList.add(MotionEvent.obtain(
-                0, CLICK_DURATION, MotionEvent.ACTION_MOVE, SECOND_CLICK_X, CLICK_Y_END, 0));
-        mSecondClickList.add(MotionEvent.obtain(
-                0, CLICK_DURATION, MotionEvent.ACTION_UP, SECOND_CLICK_X, CLICK_Y_END, 0));
-        for (int i = 0; i < mSecondClickList.size(); i++) {
-            mSecondClickList.get(i).setSource(MOTION_EVENT_SOURCE);
-        }
-
-        mSecondClickEvent0Matcher = new MotionEventMatcher(mSecondClickList.get(0));
-
         mServiceInterface = mock(IAccessibilityServiceClient.class);
+
+        mLineList = createSimpleGestureFromPoints(0, 0, false, LINE_DURATION, LINE_START, LINE_END);
+        mClickList = createSimpleGestureFromPoints(
+                0, 0, false, CLICK_DURATION, CLICK_POINT, CLICK_POINT);
+        mContinuedLineList1 = createSimpleGestureFromPoints(CONTINUED_LINE_STROKE_ID_1, 0, true,
+                CONTINUED_LINE_INTERVAL, CONTINUED_LINE_START, CONTINUED_LINE_MID1);
+        mContinuedLineList2 = createSimpleGestureFromPoints(CONTINUED_LINE_STROKE_ID_2,
+                CONTINUED_LINE_STROKE_ID_1, false, CONTINUED_LINE_INTERVAL, CONTINUED_LINE_MID1,
+                CONTINUED_LINE_MID2, CONTINUED_LINE_END);
+
+        mClickDownEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, CLICK_POINT.x, CLICK_POINT.y, 0);
+        mClickDownEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+        mClickUpEvent = MotionEvent.obtain(0, CLICK_DURATION, ACTION_UP, CLICK_POINT.x,
+                CLICK_POINT.y, 0);
+        mClickUpEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+
+        mIsLineStart = allOf(IS_ACTION_DOWN, isAtPoint(LINE_START), hasStandardInitialization(),
+                hasTimeFromDown(0));
+        mIsLineMiddle = allOf(IS_ACTION_MOVE, isAtPoint(LINE_END), hasStandardInitialization(),
+                hasTimeFromDown(LINE_DURATION));
+        mIsLineEnd = allOf(IS_ACTION_UP, isAtPoint(LINE_END), hasStandardInitialization(),
+                hasTimeFromDown(LINE_DURATION));
+        mIsClickDown = allOf(IS_ACTION_DOWN, isAtPoint(CLICK_POINT), hasStandardInitialization(),
+                hasTimeFromDown(0));
+        mIsClickUp = allOf(IS_ACTION_UP, isAtPoint(CLICK_POINT), hasStandardInitialization(),
+                hasTimeFromDown(CLICK_DURATION));
     }
 
     @Test
     public void testInjectEvents_shouldEmergeInOrderWithCorrectTiming() throws RemoteException {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
         verifyNoMoreInteractions(next);
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
 
-        verify(next).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(),
-                eq(WindowManagerPolicy.FLAG_PASS_TO_USER));
-        long gestureStart = mCaptor1.getValue().getDownTime();
-        mClickEvent0Matcher.offsetTimesBy(gestureStart);
-        mClickEvent1Matcher.offsetTimesBy(gestureStart);
-        mClickEvent2Matcher.offsetTimesBy(gestureStart);
-
-        verify(next).onMotionEvent(argThat(mClickEvent0Matcher), argThat(mClickEvent0Matcher),
-                eq(WindowManagerPolicy.FLAG_PASS_TO_USER));
+        verify(next).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), eq(FLAG_PASS_TO_USER));
+        verify(next).onMotionEvent(argThat(mIsLineStart), argThat(mIsLineStart),
+                eq(FLAG_PASS_TO_USER));
         verifyNoMoreInteractions(next);
         reset(next);
 
+        Matcher<MotionEvent> hasRightDownTime = hasDownTime(mCaptor1.getValue().getDownTime());
+
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
-        verify(next).onMotionEvent(argThat(mClickEvent1Matcher), argThat(mClickEvent1Matcher),
-                eq(WindowManagerPolicy.FLAG_PASS_TO_USER));
+        verify(next).onMotionEvent(argThat(allOf(mIsLineMiddle, hasRightDownTime)),
+                argThat(allOf(mIsLineMiddle, hasRightDownTime)), eq(FLAG_PASS_TO_USER));
         verifyNoMoreInteractions(next);
         reset(next);
 
         verifyZeroInteractions(mServiceInterface);
 
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
-        verify(next).onMotionEvent(argThat(mClickEvent2Matcher), argThat(mClickEvent2Matcher),
-                eq(WindowManagerPolicy.FLAG_PASS_TO_USER));
+        verify(next).onMotionEvent(argThat(allOf(mIsLineEnd, hasRightDownTime)),
+                argThat(allOf(mIsLineEnd, hasRightDownTime)), eq(FLAG_PASS_TO_USER));
         verifyNoMoreInteractions(next);
-        reset(next);
 
-        verify(mServiceInterface).onPerformGestureResult(SEQUENCE, true);
+        verify(mServiceInterface).onPerformGestureResult(LINE_SEQUENCE, true);
         verifyNoMoreInteractions(mServiceInterface);
     }
 
     @Test
-    public void testInjectEvents_eventWithManyPointers_shouldNotCrash() {
-        int manyPointersCount = 20;
-        MotionEvent.PointerCoords[] pointerCoords =
-                new MotionEvent.PointerCoords[manyPointersCount];
-        MotionEvent.PointerProperties[] pointerProperties =
-                new MotionEvent.PointerProperties[manyPointersCount];
-        for (int i = 0; i < manyPointersCount; i++) {
-            pointerProperties[i] = new MotionEvent.PointerProperties();
-            pointerProperties[i].id = i;
-            pointerProperties[i].toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
-            pointerCoords[i] = new MotionEvent.PointerCoords();
-            pointerCoords[i].clear();
-            pointerCoords[i].pressure = 1.0f;
-            pointerCoords[i].size = 1.0f;
-            pointerCoords[i].x = i;
-            pointerCoords[i].y = i;
+    public void testInjectEvents_gestureWithTooManyPoints_shouldNotCrash() throws  Exception {
+        int tooManyPointsCount = 20;
+        TouchPoint[] startTouchPoints = new TouchPoint[tooManyPointsCount];
+        TouchPoint[] endTouchPoints = new TouchPoint[tooManyPointsCount];
+        for (int i = 0; i < tooManyPointsCount; i++) {
+            startTouchPoints[i] = new TouchPoint();
+            startTouchPoints[i].mIsStartOfPath = true;
+            startTouchPoints[i].mX = i;
+            startTouchPoints[i].mY = i;
+            endTouchPoints[i] = new TouchPoint();
+            endTouchPoints[i].mIsEndOfPath = true;
+            endTouchPoints[i].mX = i;
+            endTouchPoints[i].mY = i;
         }
-        List<MotionEvent> events = new ArrayList<>();
-        events.add(MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, manyPointersCount,
-                pointerProperties, pointerCoords, 0, 0,
-                1.0f, 1.0f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0));
-        events.add(MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, manyPointersCount,
-                pointerProperties, pointerCoords, 0, 0,
-                1.0f, 1.0f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0));
-        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mMotionEventInjector.injectEvents(events, mServiceInterface, SEQUENCE);
+        List<GestureStep> events = Arrays.asList(
+                new GestureStep(0, tooManyPointsCount, startTouchPoints),
+                new GestureStep(CLICK_DURATION, tooManyPointsCount, endTouchPoints));
+        attachMockNext(mMotionEventInjector);
+        injectEventsSync(events, mServiceInterface, CLICK_SEQUENCE);
         mMessageCapturingHandler.sendAllMessages();
-        verify(next, times(2)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-        assertEquals(MotionEvent.ACTION_DOWN, mCaptor1.getAllValues().get(0).getActionMasked());
-        assertEquals(MotionEvent.ACTION_UP, mCaptor1.getAllValues().get(1).getActionMasked());
+        verify(mServiceInterface).onPerformGestureResult(eq(CLICK_SEQUENCE), anyBoolean());
     }
 
     @Test
     public void testRegularEvent_afterGestureComplete_shouldPassToNext() {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
         mMessageCapturingHandler.sendAllMessages(); // Send all motion events
         reset(next);
-        mMotionEventInjector.onMotionEvent(mSecondClickList.get(0), mClickList.get(0), 0);
-        verify(next).onMotionEvent(argThat(mSecondClickEvent0Matcher),
-                argThat(mClickEvent0Matcher), eq(0));
+        mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0);
+        verify(next).onMotionEvent(argThat(mIsClickDown), argThat(mIsClickDown), eq(0));
     }
 
     @Test
     public void testInjectEvents_withRealGestureUnderway_shouldCancelRealAndPassInjected() {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mMotionEventInjector.onMotionEvent(mClickList.get(0), mClickList.get(0), 0);
-        mMotionEventInjector.injectEvents(mSecondClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+        mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0);
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
 
         verify(next, times(2)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-        assertTrue(mClickEvent0Matcher.matches(mCaptor1.getAllValues().get(0)));
-        assertEquals(MotionEvent.ACTION_CANCEL, mCaptor1.getAllValues().get(1).getActionMasked());
+        assertThat(mCaptor1.getAllValues().get(0), mIsClickDown);
+        assertThat(mCaptor1.getAllValues().get(1), IS_ACTION_CANCEL);
         reset(next);
 
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
-        verify(next).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(),
-                eq(WindowManagerPolicy.FLAG_PASS_TO_USER));
-        long gestureStart = mCaptor1.getValue().getDownTime();
-        mSecondClickEvent0Matcher.offsetTimesBy(gestureStart);
-
-        verify(next).onMotionEvent(argThat(mSecondClickEvent0Matcher),
-                argThat(mSecondClickEvent0Matcher), eq(WindowManagerPolicy.FLAG_PASS_TO_USER));
+        verify(next).onMotionEvent(
+                argThat(mIsLineStart), argThat(mIsLineStart), eq(FLAG_PASS_TO_USER));
     }
 
     @Test
     public void testInjectEvents_withRealMouseGestureUnderway_shouldContinueRealAndPassInjected() {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        MotionEvent mouseEvent = MotionEvent.obtain(mClickList.get(0));
+        MotionEvent mouseEvent = MotionEvent.obtain(mClickDownEvent);
         mouseEvent.setSource(InputDevice.SOURCE_MOUSE);
-        MotionEventMatcher mouseEventMatcher = new MotionEventMatcher(mouseEvent);
+        MotionEventMatcher isMouseEvent = new MotionEventMatcher(mouseEvent);
         mMotionEventInjector.onMotionEvent(mouseEvent, mouseEvent, 0);
-        mMotionEventInjector.injectEvents(mSecondClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
 
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
         verify(next, times(2)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-        assertTrue(mouseEventMatcher.matches(mCaptor1.getAllValues().get(0)));
-        mSecondClickEvent0Matcher.offsetTimesBy(mCaptor1.getAllValues().get(1).getDownTime());
-        assertTrue(mSecondClickEvent0Matcher.matches(mCaptor1.getAllValues().get(1)));
+        assertThat(mCaptor1.getAllValues().get(0), isMouseEvent);
+        assertThat(mCaptor1.getAllValues().get(1), mIsLineStart);
     }
 
     @Test
     public void testInjectEvents_withRealGestureFinished_shouldJustPassInjected() {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mMotionEventInjector.onMotionEvent(mClickList.get(0), mClickList.get(0), 0);
-        mMotionEventInjector.onMotionEvent(mClickList.get(1), mClickList.get(1), 0);
-        mMotionEventInjector.onMotionEvent(mClickList.get(2), mClickList.get(2), 0);
+        mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0);
+        mMotionEventInjector.onMotionEvent(mClickUpEvent, mClickUpEvent, 0);
 
-        mMotionEventInjector.injectEvents(mSecondClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
-        verify(next, times(3)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-
-        assertTrue(mClickEvent0Matcher.matches(mCaptor1.getAllValues().get(0)));
-        assertTrue(mClickEvent1Matcher.matches(mCaptor1.getAllValues().get(1)));
-        assertTrue(mClickEvent2Matcher.matches(mCaptor1.getAllValues().get(2)));
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
+        verify(next, times(2)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        assertThat(mCaptor1.getAllValues().get(0), mIsClickDown);
+        assertThat(mCaptor1.getAllValues().get(1), mIsClickUp);
         reset(next);
 
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
-        verify(next).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(),
-                eq(WindowManagerPolicy.FLAG_PASS_TO_USER));
-        mSecondClickEvent0Matcher.offsetTimesBy(mCaptor1.getValue().getDownTime());
-        verify(next).onMotionEvent(argThat(mSecondClickEvent0Matcher),
-                argThat(mSecondClickEvent0Matcher), eq(WindowManagerPolicy.FLAG_PASS_TO_USER));
+        verify(next).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), eq(FLAG_PASS_TO_USER));
+        verify(next).onMotionEvent(
+                argThat(mIsLineStart), argThat(mIsLineStart), eq(FLAG_PASS_TO_USER));
     }
 
     @Test
     public void testOnMotionEvents_openInjectedGestureInProgress_shouldCancelAndNotifyAndPassReal()
             throws RemoteException {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
-
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
-        mMotionEventInjector.onMotionEvent(mSecondClickList.get(0), mSecondClickList.get(0), 0);
+        mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0);
 
         verify(next, times(3)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-        mClickEvent0Matcher.offsetTimesBy(mCaptor1.getAllValues().get(0).getDownTime());
-        assertTrue(mClickEvent0Matcher.matches(mCaptor1.getAllValues().get(0)));
-        assertEquals(MotionEvent.ACTION_CANCEL, mCaptor1.getAllValues().get(1).getActionMasked());
-        assertTrue(mSecondClickEvent0Matcher.matches(mCaptor1.getAllValues().get(2)));
-        verify(mServiceInterface).onPerformGestureResult(SEQUENCE, false);
+        assertThat(mCaptor1.getAllValues().get(0), mIsLineStart);
+        assertThat(mCaptor1.getAllValues().get(1), IS_ACTION_CANCEL);
+        assertThat(mCaptor1.getAllValues().get(2), mIsClickDown);
+        verify(mServiceInterface).onPerformGestureResult(LINE_SEQUENCE, false);
     }
 
     @Test
     public void testOnMotionEvents_closedInjectedGestureInProgress_shouldOnlyNotifyAndPassReal()
             throws RemoteException {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mClickList.add(MotionEvent.obtain(2 * CLICK_DURATION, 2 * CLICK_DURATION,
-                MotionEvent.ACTION_DOWN, CLICK_X, CLICK_Y_START, 0));
-        mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+        // Tack a click down to the end of the line
+        TouchPoint clickTouchPoint = new TouchPoint();
+        clickTouchPoint.mIsStartOfPath = true;
+        clickTouchPoint.mX = CLICK_POINT.x;
+        clickTouchPoint.mY = CLICK_POINT.y;
+        mLineList.add(new GestureStep(0, 1, new TouchPoint[] {clickTouchPoint}));
+
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
 
         // Send 3 motion events, leaving the extra down in the queue
         mMessageCapturingHandler.sendOneMessage();
         mMessageCapturingHandler.sendOneMessage();
         mMessageCapturingHandler.sendOneMessage();
 
-        mMotionEventInjector.onMotionEvent(mSecondClickList.get(0), mClickList.get(0), 0);
+        mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0);
 
         verify(next, times(4)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-        long gestureStart = mCaptor1.getAllValues().get(0).getDownTime();
-        mClickEvent0Matcher.offsetTimesBy(gestureStart);
-        mClickEvent1Matcher.offsetTimesBy(gestureStart);
-        mClickEvent2Matcher.offsetTimesBy(gestureStart);
-        assertTrue(mClickEvent0Matcher.matches(mCaptor1.getAllValues().get(0)));
-        assertTrue(mClickEvent1Matcher.matches(mCaptor1.getAllValues().get(1)));
-        assertTrue(mClickEvent2Matcher.matches(mCaptor1.getAllValues().get(2)));
-        assertTrue(mSecondClickEvent0Matcher.matches(mCaptor1.getAllValues().get(3)));
-
-        verify(mServiceInterface).onPerformGestureResult(SEQUENCE, false);
+        assertThat(mCaptor1.getAllValues().get(0), mIsLineStart);
+        assertThat(mCaptor1.getAllValues().get(1), mIsLineMiddle);
+        assertThat(mCaptor1.getAllValues().get(2), mIsLineEnd);
+        assertThat(mCaptor1.getAllValues().get(3), mIsClickDown);
+        verify(mServiceInterface).onPerformGestureResult(LINE_SEQUENCE, false);
         assertFalse(mMessageCapturingHandler.hasMessages());
     }
 
@@ -326,105 +335,327 @@
     public void testInjectEvents_openInjectedGestureInProgress_shouldCancelAndNotifyAndPassNew()
             throws RemoteException {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
 
-        mMotionEventInjector.injectEvents(mSecondClickList, mServiceInterface, SECOND_SEQUENCE);
-        mMessageCapturingHandler.sendLastMessage(); // Process the second event injection
+        injectEventsSync(mClickList, mServiceInterface, CLICK_SEQUENCE);
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
 
-        verify(mServiceInterface, times(1)).onPerformGestureResult(SEQUENCE, false);
+        verify(mServiceInterface, times(1)).onPerformGestureResult(LINE_SEQUENCE, false);
         verify(next, times(3)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-        mClickEvent0Matcher.offsetTimesBy(mCaptor1.getAllValues().get(0).getDownTime());
-        assertTrue(mClickEvent0Matcher.matches(mCaptor1.getAllValues().get(0)));
-        assertEquals(MotionEvent.ACTION_CANCEL, mCaptor1.getAllValues().get(1).getActionMasked());
-        mSecondClickEvent0Matcher.offsetTimesBy(mCaptor1.getAllValues().get(2).getDownTime());
-        assertTrue(mSecondClickEvent0Matcher.matches(mCaptor1.getAllValues().get(2)));
+        assertThat(mCaptor1.getAllValues().get(0), mIsLineStart);
+        assertThat(mCaptor1.getAllValues().get(1), IS_ACTION_CANCEL);
+        assertThat(mCaptor1.getAllValues().get(2), mIsClickDown);
     }
 
     @Test
     public void testInjectEvents_closedInjectedGestureInProgress_shouldOnlyNotifyAndPassNew()
             throws RemoteException {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        MotionEvent newEvent = MotionEvent.obtain(2 * CLICK_DURATION, 2 * CLICK_DURATION,
-                MotionEvent.ACTION_DOWN, CLICK_X, CLICK_Y_START, 0);
-        newEvent.setSource(mClickList.get(0).getSource());
-        mClickList.add(newEvent);
-        mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+        // Tack a click down to the end of the line
+        TouchPoint clickTouchPoint = new TouchPoint();
+        clickTouchPoint.mIsStartOfPath = true;
+        clickTouchPoint.mX = CLICK_POINT.x;
+        clickTouchPoint.mY = CLICK_POINT.y;
+        mLineList.add(new GestureStep(0, 1, new TouchPoint[] {clickTouchPoint}));
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
 
         // Send 3 motion events, leaving newEvent in the queue
         mMessageCapturingHandler.sendOneMessage();
         mMessageCapturingHandler.sendOneMessage();
         mMessageCapturingHandler.sendOneMessage();
 
-        mMotionEventInjector.injectEvents(mSecondClickList, mServiceInterface, SECOND_SEQUENCE);
-        mMessageCapturingHandler.sendLastMessage(); // Process the event injection
+        injectEventsSync(mClickList, mServiceInterface, CLICK_SEQUENCE);
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
 
-        verify(mServiceInterface).onPerformGestureResult(SEQUENCE, false);
+        verify(mServiceInterface).onPerformGestureResult(LINE_SEQUENCE, false);
         verify(next, times(4)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-        long gestureStart = mCaptor1.getAllValues().get(0).getDownTime();
-        mClickEvent0Matcher.offsetTimesBy(gestureStart);
-        mClickEvent1Matcher.offsetTimesBy(gestureStart);
-        mClickEvent2Matcher.offsetTimesBy(gestureStart);
-        assertTrue(mClickEvent0Matcher.matches(mCaptor1.getAllValues().get(0)));
-        assertTrue(mClickEvent1Matcher.matches(mCaptor1.getAllValues().get(1)));
-        assertTrue(mClickEvent2Matcher.matches(mCaptor1.getAllValues().get(2)));
-        mSecondClickEvent0Matcher.offsetTimesBy(mCaptor1.getAllValues().get(3).getDownTime());
-        assertTrue(mSecondClickEvent0Matcher.matches(mCaptor1.getAllValues().get(3)));
+        assertThat(mCaptor1.getAllValues().get(0), mIsLineStart);
+        assertThat(mCaptor1.getAllValues().get(1), mIsLineMiddle);
+        assertThat(mCaptor1.getAllValues().get(2), mIsLineEnd);
+        assertThat(mCaptor1.getAllValues().get(3), mIsClickDown);
+    }
+
+    @Test
+    public void testContinuedGesture_continuationArrivesAfterDispatched_gestureCompletes()
+            throws Exception {
+        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+        injectEventsSync(mContinuedLineList1, mServiceInterface, CONTINUED_LINE_SEQUENCE_1);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_1, true);
+        injectEventsSync(mContinuedLineList2, mServiceInterface, CONTINUED_LINE_SEQUENCE_2);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_2, true);
+        verify(next, times(5)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        List<MotionEvent> events = mCaptor1.getAllValues();
+        long downTime = events.get(0).getDownTime();
+        assertThat(events.get(0), allOf(isAtPoint(CONTINUED_LINE_START), IS_ACTION_DOWN,
+                hasEventTime(downTime)));
+        assertThat(events, everyItem(hasDownTime(downTime)));
+        assertThat(events.get(1), allOf(isAtPoint(CONTINUED_LINE_MID1), IS_ACTION_MOVE,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL)));
+        // Timing will restart when the gesture continues
+        long secondSequenceStart = events.get(2).getEventTime();
+        assertTrue(secondSequenceStart > events.get(1).getEventTime());
+        assertThat(events.get(2), allOf(isAtPoint(CONTINUED_LINE_MID2), IS_ACTION_MOVE));
+        assertThat(events.get(3), allOf(isAtPoint(CONTINUED_LINE_END), IS_ACTION_MOVE,
+                hasEventTime(secondSequenceStart + CONTINUED_LINE_INTERVAL)));
+        assertThat(events.get(4), allOf(isAtPoint(CONTINUED_LINE_END), IS_ACTION_UP,
+                hasEventTime(secondSequenceStart + CONTINUED_LINE_INTERVAL)));
+    }
+
+    @Test
+    public void testContinuedGesture_withTwoTouchPoints_gestureCompletes()
+            throws Exception {
+        // Run one point through the continued line backwards
+        int backLineId1 = 30;
+        int backLineId2 = 30;
+        List<GestureStep> continuedBackLineList1 = createSimpleGestureFromPoints(backLineId1, 0,
+                true, CONTINUED_LINE_INTERVAL, CONTINUED_LINE_END, CONTINUED_LINE_MID2);
+        List<GestureStep> continuedBackLineList2 = createSimpleGestureFromPoints(backLineId2,
+                backLineId1, false, CONTINUED_LINE_INTERVAL, CONTINUED_LINE_MID2,
+                CONTINUED_LINE_MID1, CONTINUED_LINE_START);
+        List<GestureStep> combinedLines1 = combineGestureSteps(
+                mContinuedLineList1, continuedBackLineList1);
+        List<GestureStep> combinedLines2 = combineGestureSteps(
+                mContinuedLineList2, continuedBackLineList2);
+
+        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+        injectEventsSync(combinedLines1, mServiceInterface, CONTINUED_LINE_SEQUENCE_1);
+        injectEventsSync(combinedLines2, mServiceInterface, CONTINUED_LINE_SEQUENCE_2);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_1, true);
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_2, true);
+        verify(next, times(7)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        List<MotionEvent> events = mCaptor1.getAllValues();
+        long downTime = events.get(0).getDownTime();
+        assertThat(events.get(0), allOf(
+                anyOf(isAtPoint(CONTINUED_LINE_END), isAtPoint(CONTINUED_LINE_START)),
+                IS_ACTION_DOWN, hasEventTime(downTime)));
+        assertThat(events, everyItem(hasDownTime(downTime)));
+        assertThat(events.get(1), allOf(containsPoints(CONTINUED_LINE_START, CONTINUED_LINE_END),
+                IS_ACTION_POINTER_DOWN, hasEventTime(downTime)));
+        assertThat(events.get(2), allOf(containsPoints(CONTINUED_LINE_MID1, CONTINUED_LINE_MID2),
+                IS_ACTION_MOVE, hasEventTime(downTime + CONTINUED_LINE_INTERVAL)));
+        assertThat(events.get(3), allOf(containsPoints(CONTINUED_LINE_MID1, CONTINUED_LINE_MID2),
+                IS_ACTION_MOVE, hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 2)));
+        assertThat(events.get(4), allOf(containsPoints(CONTINUED_LINE_START, CONTINUED_LINE_END),
+                IS_ACTION_MOVE, hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 3)));
+        assertThat(events.get(5), allOf(containsPoints(CONTINUED_LINE_START, CONTINUED_LINE_END),
+                IS_ACTION_POINTER_UP, hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 3)));
+        assertThat(events.get(6), allOf(
+                anyOf(isAtPoint(CONTINUED_LINE_END), isAtPoint(CONTINUED_LINE_START)),
+                IS_ACTION_UP, hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 3)));
+    }
+
+
+    @Test
+    public void testContinuedGesture_continuationArrivesWhileDispatching_gestureCompletes()
+            throws Exception {
+        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+        injectEventsSync(mContinuedLineList1, mServiceInterface, CONTINUED_LINE_SEQUENCE_1);
+        mMessageCapturingHandler.sendOneMessage(); // Send a motion event
+        injectEventsSync(mContinuedLineList2, mServiceInterface, CONTINUED_LINE_SEQUENCE_2);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_1, true);
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_2, true);
+        verify(next, times(5)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        List<MotionEvent> events = mCaptor1.getAllValues();
+        long downTime = events.get(0).getDownTime();
+        assertThat(events.get(0), allOf(isAtPoint(CONTINUED_LINE_START), IS_ACTION_DOWN,
+                hasEventTime(downTime)));
+        assertThat(events, everyItem(hasDownTime(downTime)));
+        assertThat(events.get(1), allOf(isAtPoint(CONTINUED_LINE_MID1), IS_ACTION_MOVE,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL)));
+        assertThat(events.get(2), allOf(isAtPoint(CONTINUED_LINE_MID2), IS_ACTION_MOVE,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 2)));
+        assertThat(events.get(3), allOf(isAtPoint(CONTINUED_LINE_END), IS_ACTION_MOVE,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 3)));
+        assertThat(events.get(4), allOf(isAtPoint(CONTINUED_LINE_END), IS_ACTION_UP,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 3)));
+    }
+
+    @Test
+    public void testContinuedGesture_twoContinuationsArriveWhileDispatching_gestureCompletes()
+            throws Exception {
+        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+        injectEventsSync(mContinuedLineList1, mServiceInterface, CONTINUED_LINE_SEQUENCE_1);
+        // Continue line again
+        List<GestureStep> continuedLineList2 = createSimpleGestureFromPoints(
+                CONTINUED_LINE_STROKE_ID_2, CONTINUED_LINE_STROKE_ID_1, true,
+                CONTINUED_LINE_INTERVAL, CONTINUED_LINE_MID1,
+                CONTINUED_LINE_MID2, CONTINUED_LINE_END);
+        // Finish line by backtracking
+        int strokeId3 = CONTINUED_LINE_STROKE_ID_2 + 1;
+        int sequence3 = CONTINUED_LINE_SEQUENCE_2 + 1;
+        List<GestureStep> continuedLineList3 = createSimpleGestureFromPoints(strokeId3,
+                CONTINUED_LINE_STROKE_ID_2, false, CONTINUED_LINE_INTERVAL, CONTINUED_LINE_END,
+                CONTINUED_LINE_MID2);
+
+        mMessageCapturingHandler.sendOneMessage(); // Send a motion event
+        injectEventsSync(continuedLineList2, mServiceInterface, CONTINUED_LINE_SEQUENCE_2);
+        injectEventsSync(continuedLineList3, mServiceInterface, sequence3);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_1, true);
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_2, true);
+        verify(mServiceInterface).onPerformGestureResult(sequence3, true);
+        verify(next, times(6)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        List<MotionEvent> events = mCaptor1.getAllValues();
+        long downTime = events.get(0).getDownTime();
+        assertThat(events.get(0), allOf(isAtPoint(CONTINUED_LINE_START), IS_ACTION_DOWN,
+                hasEventTime(downTime)));
+        assertThat(events, everyItem(hasDownTime(downTime)));
+        assertThat(events.get(1), allOf(isAtPoint(CONTINUED_LINE_MID1), IS_ACTION_MOVE,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL)));
+        assertThat(events.get(2), allOf(isAtPoint(CONTINUED_LINE_MID2), IS_ACTION_MOVE,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 2)));
+        assertThat(events.get(3), allOf(isAtPoint(CONTINUED_LINE_END), IS_ACTION_MOVE,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 3)));
+        assertThat(events.get(4), allOf(isAtPoint(CONTINUED_LINE_MID2), IS_ACTION_MOVE,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 4)));
+        assertThat(events.get(5), allOf(isAtPoint(CONTINUED_LINE_MID2), IS_ACTION_UP,
+                hasEventTime(downTime + CONTINUED_LINE_INTERVAL * 4)));
+    }
+
+    @Test
+    public void testContinuedGesture_nonContinuingGestureArrivesDuringDispatch_gestureCanceled()
+            throws Exception {
+        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+        injectEventsSync(mContinuedLineList1, mServiceInterface, CONTINUED_LINE_SEQUENCE_1);
+        mMessageCapturingHandler.sendOneMessage(); // Send a motion event
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_1, false);
+        verify(mServiceInterface).onPerformGestureResult(LINE_SEQUENCE, true);
+        verify(next, times(5)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        List<MotionEvent> events = mCaptor1.getAllValues();
+        assertThat(events.get(0), allOf(isAtPoint(CONTINUED_LINE_START), IS_ACTION_DOWN));
+        assertThat(events.get(1), IS_ACTION_CANCEL);
+        assertThat(events.get(2), allOf(isAtPoint(LINE_START), IS_ACTION_DOWN));
+        assertThat(events.get(3), allOf(isAtPoint(LINE_END), IS_ACTION_MOVE));
+        assertThat(events.get(4), allOf(isAtPoint(LINE_END), IS_ACTION_UP));
+    }
+
+    @Test
+    public void testContinuedGesture_nonContinuingGestureArrivesAfterDispatch_gestureCanceled()
+            throws Exception {
+        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+        injectEventsSync(mContinuedLineList1, mServiceInterface, CONTINUED_LINE_SEQUENCE_1);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_1, true);
+        verify(mServiceInterface).onPerformGestureResult(LINE_SEQUENCE, true);
+        verify(next, times(6)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        List<MotionEvent> events = mCaptor1.getAllValues();
+        assertThat(events.get(0), allOf(isAtPoint(CONTINUED_LINE_START), IS_ACTION_DOWN));
+        assertThat(events.get(1), allOf(isAtPoint(CONTINUED_LINE_MID1), IS_ACTION_MOVE));
+        assertThat(events.get(2), IS_ACTION_CANCEL);
+        assertThat(events.get(3), allOf(isAtPoint(LINE_START), IS_ACTION_DOWN));
+        assertThat(events.get(4), allOf(isAtPoint(LINE_END), IS_ACTION_MOVE));
+        assertThat(events.get(5), allOf(isAtPoint(LINE_END), IS_ACTION_UP));
+    }
+
+    @Test
+    public void testContinuedGesture_misMatchedContinuationArrives_bothGesturesCanceled()
+            throws Exception {
+        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+        injectEventsSync(mContinuedLineList1, mServiceInterface, CONTINUED_LINE_SEQUENCE_1);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_1, true);
+        List<GestureStep> discontinuousGesture = mContinuedLineList2
+                .subList(1, mContinuedLineList2.size());
+        injectEventsSync(discontinuousGesture, mServiceInterface, CONTINUED_LINE_SEQUENCE_2);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_2, false);
+        verify(next, times(3)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        List<MotionEvent> events = mCaptor1.getAllValues();
+        assertThat(events.get(0), allOf(isAtPoint(CONTINUED_LINE_START), IS_ACTION_DOWN));
+        assertThat(events.get(1), allOf(isAtPoint(CONTINUED_LINE_MID1), IS_ACTION_MOVE));
+        assertThat(events.get(2), allOf(isAtPoint(CONTINUED_LINE_MID1), IS_ACTION_CANCEL));
+    }
+
+    @Test
+    public void testContinuedGesture_continuationArrivesFromOtherService_bothGesturesCanceled()
+            throws Exception {
+        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+        IAccessibilityServiceClient otherService = mock(IAccessibilityServiceClient.class);
+        injectEventsSync(mContinuedLineList1, mServiceInterface, CONTINUED_LINE_SEQUENCE_1);
+        mMessageCapturingHandler.sendOneMessage(); // Send a motion events
+        injectEventsSync(mContinuedLineList2, otherService, CONTINUED_LINE_SEQUENCE_2);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_1, false);
+        verify(otherService).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_2, false);
+        verify(next, times(2)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        List<MotionEvent> events = mCaptor1.getAllValues();
+        assertThat(events.get(0), allOf(isAtPoint(CONTINUED_LINE_START), IS_ACTION_DOWN));
+        assertThat(events.get(1), IS_ACTION_CANCEL);
+    }
+
+    @Test
+    public void testContinuedGesture_realGestureArrivesInBetween_getsCanceled()
+            throws Exception {
+        EventStreamTransformation next = attachMockNext(mMotionEventInjector);
+        injectEventsSync(mContinuedLineList1, mServiceInterface, CONTINUED_LINE_SEQUENCE_1);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_1, true);
+
+        mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0);
+
+        injectEventsSync(mContinuedLineList2, mServiceInterface, CONTINUED_LINE_SEQUENCE_2);
+        mMessageCapturingHandler.sendAllMessages(); // Send all motion events
+        verify(mServiceInterface).onPerformGestureResult(CONTINUED_LINE_SEQUENCE_2, false);
+        verify(next, times(4)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
+        List<MotionEvent> events = mCaptor1.getAllValues();
+        assertThat(events.get(0), allOf(isAtPoint(CONTINUED_LINE_START), IS_ACTION_DOWN));
+        assertThat(events.get(1), allOf(isAtPoint(CONTINUED_LINE_MID1), IS_ACTION_MOVE));
+        assertThat(events.get(2), IS_ACTION_CANCEL);
+        assertThat(events.get(3), allOf(isAtPoint(CLICK_POINT), IS_ACTION_DOWN));
     }
 
     @Test
     public void testClearEvents_realGestureInProgress_shouldForgetAboutGesture() {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mMotionEventInjector.onMotionEvent(mClickList.get(0), mClickList.get(0), 0);
+        mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0);
         mMotionEventInjector.clearEvents(MOTION_EVENT_SOURCE);
-        mMotionEventInjector.injectEvents(mSecondClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
 
         verify(next, times(2)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-        assertTrue(mClickEvent0Matcher.matches(mCaptor1.getAllValues().get(0)));
-        mSecondClickEvent0Matcher.offsetTimesBy(mCaptor1.getAllValues().get(1).getDownTime());
-        assertTrue(mSecondClickEvent0Matcher.matches(mCaptor1.getAllValues().get(1)));
+        assertThat(mCaptor1.getAllValues().get(0), mIsClickDown);
+        assertThat(mCaptor1.getAllValues().get(1), mIsLineStart);
     }
 
     @Test
     public void testClearEventsOnOtherSource_realGestureInProgress_shouldNotForgetAboutGesture() {
         EventStreamTransformation next = attachMockNext(mMotionEventInjector);
-        mMotionEventInjector.onMotionEvent(mClickList.get(0), mClickList.get(0), 0);
+        mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0);
         mMotionEventInjector.clearEvents(OTHER_EVENT_SOURCE);
-        mMotionEventInjector.injectEvents(mSecondClickList, mServiceInterface, SECOND_SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
         mMessageCapturingHandler.sendOneMessage(); // Send a motion event
 
         verify(next, times(3)).onMotionEvent(mCaptor1.capture(), mCaptor2.capture(), anyInt());
-        assertTrue(mClickEvent0Matcher.matches(mCaptor1.getAllValues().get(0)));
-        assertEquals(MotionEvent.ACTION_CANCEL, mCaptor1.getAllValues().get(1).getActionMasked());
-        mSecondClickEvent0Matcher.offsetTimesBy(mCaptor1.getAllValues().get(2).getDownTime());
-        assertTrue(mSecondClickEvent0Matcher.matches(mCaptor1.getAllValues().get(2)));
+        assertThat(mCaptor1.getAllValues().get(0), mIsClickDown);
+        assertThat(mCaptor1.getAllValues().get(1), IS_ACTION_CANCEL);
+        assertThat(mCaptor1.getAllValues().get(2), mIsLineStart);
     }
 
     @Test
     public void testOnDestroy_shouldCancelGestures() throws RemoteException {
         mMotionEventInjector.onDestroy();
-        mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
-        verify(mServiceInterface).onPerformGestureResult(SEQUENCE, false);
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
+        verify(mServiceInterface).onPerformGestureResult(LINE_SEQUENCE, false);
     }
 
     @Test
     public void testInjectEvents_withNoNext_shouldCancel() throws RemoteException {
-        mMotionEventInjector.injectEvents(mClickList, mServiceInterface, SEQUENCE);
-        mMessageCapturingHandler.sendOneMessage(); // Process the event injection
-        verify(mServiceInterface).onPerformGestureResult(SEQUENCE, false);
+        injectEventsSync(mLineList, mServiceInterface, LINE_SEQUENCE);
+        verify(mServiceInterface).onPerformGestureResult(LINE_SEQUENCE, false);
     }
 
     @Test
     public void testOnMotionEvent_withNoNext_shouldNotCrash() {
-        mMotionEventInjector.onMotionEvent(mClickList.get(0), mClickList.get(0), 0);
+        mMotionEventInjector.onMotionEvent(mClickDownEvent, mClickDownEvent, 0);
     }
 
     @Test
@@ -455,6 +686,52 @@
         mMotionEventInjector.onAccessibilityEvent(event);
     }
 
+    private void injectEventsSync(List<GestureStep> gestureSteps,
+            IAccessibilityServiceClient serviceInterface, int sequence) {
+        mMotionEventInjector.injectEvents(gestureSteps, serviceInterface, sequence);
+        // Dispatch the message sent by the injector. Our simple handler doesn't guarantee stuff
+        // happens in order.
+        mMessageCapturingHandler.sendLastMessage();
+    }
+
+    private List<GestureStep> createSimpleGestureFromPoints(int strokeId, int continuedStrokeId,
+            boolean continued, long interval, Point... points) {
+        List<GestureStep> gesture = new ArrayList<>(points.length);
+        TouchPoint[] touchPoints = new TouchPoint[1];
+        touchPoints[0] = new TouchPoint();
+        for (int i = 0; i < points.length; i++) {
+            touchPoints[0].mX = points[i].x;
+            touchPoints[0].mY = points[i].y;
+            touchPoints[0].mIsStartOfPath = ((i == 0) && (continuedStrokeId <= 0));
+            touchPoints[0].mContinuedStrokeId = continuedStrokeId;
+            touchPoints[0].mStrokeId = strokeId;
+            touchPoints[0].mIsEndOfPath = ((i == points.length - 1) && !continued);
+            gesture.add(new GestureStep(interval * i, 1, touchPoints));
+        }
+        return gesture;
+    }
+
+    List<GestureStep> combineGestureSteps(List<GestureStep> list1, List<GestureStep> list2) {
+        assertEquals(list1.size(), list2.size());
+        List<GestureStep> gesture = new ArrayList<>(list1.size());
+        for (int i = 0; i < list1.size(); i++) {
+            int numPoints1 = list1.get(i).numTouchPoints;
+            int numPoints2 = list2.get(i).numTouchPoints;
+            TouchPoint[] touchPoints = new TouchPoint[numPoints1 + numPoints2];
+            for (int j = 0; j < numPoints1; j++) {
+                touchPoints[j] = new TouchPoint();
+                touchPoints[j].copyFrom(list1.get(i).touchPoints[j]);
+            }
+            for (int j = 0; j < numPoints2; j++) {
+                touchPoints[numPoints1 + j] = new TouchPoint();
+                touchPoints[numPoints1 + j].copyFrom(list2.get(i).touchPoints[j]);
+            }
+            gesture.add(new GestureStep(list1.get(i).timeSinceGestureStart,
+                    numPoints1 + numPoints2, touchPoints));
+        }
+        return gesture;
+    }
+
     private EventStreamTransformation attachMockNext(MotionEventInjector motionEventInjector) {
         EventStreamTransformation next = mock(EventStreamTransformation.class);
         motionEventInjector.setNext(next);
@@ -506,4 +783,126 @@
             return false;
         }
     }
+
+    private static class MotionEventActionMatcher extends TypeSafeMatcher<MotionEvent> {
+        int mAction;
+
+        MotionEventActionMatcher(int action) {
+            super();
+            mAction = action;
+        }
+
+        @Override
+        protected boolean matchesSafely(MotionEvent motionEvent) {
+            return motionEvent.getActionMasked() == mAction;
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText("Matching to action " + mAction);
+        }
+    }
+
+    private static TypeSafeMatcher<MotionEvent> isAtPoint(final Point point) {
+        return new TypeSafeMatcher<MotionEvent>() {
+            @Override
+            protected boolean matchesSafely(MotionEvent event) {
+                return ((event.getX() == point.x) && (event.getY() == point.y));
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Is at point " + point);
+            }
+        };
+    }
+
+    private static TypeSafeMatcher<MotionEvent> containsPoints(final Point... points) {
+        return new TypeSafeMatcher<MotionEvent>() {
+            @Override
+            protected boolean matchesSafely(MotionEvent event) {
+                MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+                for (int i = 0; i < points.length; i++) {
+                    boolean havePoint = false;
+                    for (int j = 0; j < points.length; j++) {
+                        event.getPointerCoords(j, coords);
+                        if ((points[i].x == coords.x) && (points[i].y == coords.y)) {
+                            havePoint = true;
+                        }
+                    }
+                    if (!havePoint) {
+                        return false;
+                    }
+                }
+                return true;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Contains points " + points);
+            }
+        };
+    }
+
+    private static TypeSafeMatcher<MotionEvent> hasDownTime(final long downTime) {
+        return new TypeSafeMatcher<MotionEvent>() {
+            @Override
+            protected boolean matchesSafely(MotionEvent event) {
+                return event.getDownTime() == downTime;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Down time = " + downTime);
+            }
+        };
+    }
+
+    private static TypeSafeMatcher<MotionEvent> hasEventTime(final long eventTime) {
+        return new TypeSafeMatcher<MotionEvent>() {
+            @Override
+            protected boolean matchesSafely(MotionEvent event) {
+                return event.getEventTime() == eventTime;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Event time = " + eventTime);
+            }
+        };
+    }
+
+    private static TypeSafeMatcher<MotionEvent> hasTimeFromDown(final long timeFromDown) {
+        return new TypeSafeMatcher<MotionEvent>() {
+            @Override
+            protected boolean matchesSafely(MotionEvent event) {
+                return (event.getEventTime() - event.getDownTime()) == timeFromDown;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Time from down to event times = " + timeFromDown);
+            }
+        };
+    }
+
+    private static TypeSafeMatcher<MotionEvent> hasStandardInitialization() {
+        return new TypeSafeMatcher<MotionEvent>() {
+            @Override
+            protected boolean matchesSafely(MotionEvent event) {
+                return (0 == event.getActionIndex()) && (0 == event.getDeviceId())
+                        && (0 == event.getEdgeFlags()) && (0 == event.getFlags())
+                        && (0 == event.getMetaState()) && (0F == event.getOrientation())
+                        && (0F == event.getTouchMajor()) && (0F == event.getTouchMinor())
+                        && (1F == event.getXPrecision()) && (1F == event.getYPrecision())
+                        && (1 == event.getPointerCount()) && (1F == event.getPressure())
+                        && (InputDevice.SOURCE_TOUCHSCREEN == event.getSource());
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("Has standard values for all parameters");
+            }
+        };
+    }
 }
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index d7f96a9..a0f5217 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -73,6 +73,15 @@
             KEY_IGNORE_SIM_NETWORK_LOCKED_EVENTS_BOOL = "ignore_sim_network_locked_events_bool";
 
     /**
+     * When checking if a given number is the voicemail number, if this flag is true
+     * then in addition to comparing the given number to the voicemail number, we also compare it
+     * to the mdn. If this flag is false, the given number is only compared to the voicemail number.
+     * By default this value is false.
+     */
+    public static final String KEY_MDN_IS_ADDITIONAL_VOICEMAIL_NUMBER_BOOL =
+            "mdn_is_additional_voicemail_number_bool";
+
+    /**
      * Flag indicating whether the Phone app should provide a "Dismiss" button on the SIM network
      * unlock screen. The default value is true. If set to false, there will be *no way* to dismiss
      * the SIM network unlock screen if you don't enter the correct unlock code. (One important
@@ -1148,6 +1157,7 @@
 
         sDefaults.putBoolean(KEY_CARRIER_VOLTE_PROVISIONED_BOOL, false);
         sDefaults.putBoolean(KEY_IGNORE_SIM_NETWORK_LOCKED_EVENTS_BOOL, false);
+        sDefaults.putBoolean(KEY_MDN_IS_ADDITIONAL_VOICEMAIL_NUMBER_BOOL, false);
         sDefaults.putBoolean(KEY_OPERATOR_SELECTION_EXPAND_BOOL, true);
         sDefaults.putBoolean(KEY_PREFER_2G_BOOL, true);
         sDefaults.putBoolean(KEY_SHOW_APN_SETTING_CDMA_BOOL, false);
diff --git a/telephony/java/android/telephony/PhoneNumberUtils.java b/telephony/java/android/telephony/PhoneNumberUtils.java
index 152b868..6f51c6e 100644
--- a/telephony/java/android/telephony/PhoneNumberUtils.java
+++ b/telephony/java/android/telephony/PhoneNumberUtils.java
@@ -29,6 +29,7 @@
 import android.location.CountryDetector;
 import android.net.Uri;
 import android.os.SystemProperties;
+import android.os.PersistableBundle;
 import android.provider.Contacts;
 import android.provider.ContactsContract;
 import android.telecom.PhoneAccount;
@@ -2106,7 +2107,7 @@
      *   number provided by the RIL and SIM card. The caller must have
      *   the READ_PHONE_STATE credential.
      *
-     * @param context a non-null {@link Context}.
+     * @param context {@link Context}.
      * @param subId the subscription id of the SIM.
      * @param number the number to look up.
      * @return true if the number is in the list of voicemail. False
@@ -2115,25 +2116,54 @@
      * @hide
      */
     public static boolean isVoiceMailNumber(Context context, int subId, String number) {
-        String vmNumber;
+        String vmNumber, mdn;
         try {
             final TelephonyManager tm;
             if (context == null) {
                 tm = TelephonyManager.getDefault();
+                if (DBG) log("isVoiceMailNumber: default tm");
             } else {
                 tm = TelephonyManager.from(context);
+                if (DBG) log("isVoiceMailNumber: tm from context");
             }
             vmNumber = tm.getVoiceMailNumber(subId);
+            mdn = tm.getLine1Number(subId);
+            if (DBG) log("isVoiceMailNumber: mdn=" + mdn + ", vmNumber=" + vmNumber
+                    + ", number=" + number);
         } catch (SecurityException ex) {
+            if (DBG) log("isVoiceMailNumber: SecurityExcpetion caught");
             return false;
         }
         // Strip the separators from the number before comparing it
         // to the list.
         number = extractNetworkPortionAlt(number);
+        if (TextUtils.isEmpty(number)) {
+            if (DBG) log("isVoiceMailNumber: number is empty after stripping");
+            return false;
+        }
 
-        // compare tolerates null so we need to make sure that we
-        // don't return true when both are null.
-        return !TextUtils.isEmpty(number) && compare(number, vmNumber);
+        // check if the carrier considers MDN to be an additional voicemail number
+        boolean compareWithMdn = false;
+        if (context != null) {
+            CarrierConfigManager configManager = (CarrierConfigManager)
+                    context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+            if (configManager != null) {
+                PersistableBundle b = configManager.getConfigForSubId(subId);
+                if (b != null) {
+                    compareWithMdn = b.getBoolean(CarrierConfigManager.
+                            KEY_MDN_IS_ADDITIONAL_VOICEMAIL_NUMBER_BOOL);
+                    if (DBG) log("isVoiceMailNumber: compareWithMdn=" + compareWithMdn);
+                }
+            }
+        }
+
+        if (compareWithMdn) {
+            if (DBG) log("isVoiceMailNumber: treating mdn as additional vm number");
+            return compare(number, vmNumber) || compare(number, mdn);
+        } else {
+            if (DBG) log("isVoiceMailNumber: returning regular compare");
+            return compare(number, vmNumber);
+        }
     }
 
     /**
diff --git a/telephony/java/com/android/internal/telephony/CallerInfoAsyncQuery.java b/telephony/java/com/android/internal/telephony/CallerInfoAsyncQuery.java
index d4104bd..20c303e 100644
--- a/telephony/java/com/android/internal/telephony/CallerInfoAsyncQuery.java
+++ b/telephony/java/com/android/internal/telephony/CallerInfoAsyncQuery.java
@@ -438,7 +438,7 @@
         // check to see if these are recognized numbers, and use shortcuts if we can.
         if (PhoneNumberUtils.isLocalEmergencyNumber(context, number)) {
             cw.event = EVENT_EMERGENCY_NUMBER;
-        } else if (PhoneNumberUtils.isVoiceMailNumber(subId, number)) {
+        } else if (PhoneNumberUtils.isVoiceMailNumber(context, subId, number)) {
             cw.event = EVENT_VOICEMAIL_NUMBER;
         } else {
             cw.event = EVENT_NEW_QUERY;
diff --git a/tests/net/java/android/net/apf/ApfTest.java b/tests/net/java/android/net/apf/ApfTest.java
index 51adfe7..ff61754 100644
--- a/tests/net/java/android/net/apf/ApfTest.java
+++ b/tests/net/java/android/net/apf/ApfTest.java
@@ -661,9 +661,13 @@
     // The IPv6 all nodes address ff02::1
     private static final byte[] IPV6_ALL_NODES_ADDRESS =
             { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
+    private static final byte[] IPV6_ALL_ROUTERS_ADDRESS =
+            { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 };
 
     private static final int ICMP6_TYPE_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN;
+    private static final int ICMP6_ROUTER_SOLICITATION = 133;
     private static final int ICMP6_ROUTER_ADVERTISEMENT = 134;
+    private static final int ICMP6_NEIGHBOR_SOLICITATION = 135;
     private static final int ICMP6_NEIGHBOR_ANNOUNCEMENT = 136;
 
     private static final int ICMP6_RA_HEADER_LEN = 16;
@@ -798,6 +802,12 @@
         put(packet, IPV6_DEST_ADDR_OFFSET, IPV6_ALL_NODES_ADDRESS);
         assertDrop(program, packet.array());
 
+        // Verify ICMPv6 RS to any is dropped
+        packet.put(ICMP6_TYPE_OFFSET, (byte)ICMP6_ROUTER_SOLICITATION);
+        assertDrop(program, packet.array());
+        put(packet, IPV6_DEST_ADDR_OFFSET, IPV6_ALL_ROUTERS_ADDRESS);
+        assertDrop(program, packet.array());
+
         apfFilter.shutdown();
     }
 
diff --git a/wifi/java/android/net/wifi/aware/SubscribeConfig.java b/wifi/java/android/net/wifi/aware/SubscribeConfig.java
index bf35445..0fe69a8 100644
--- a/wifi/java/android/net/wifi/aware/SubscribeConfig.java
+++ b/wifi/java/android/net/wifi/aware/SubscribeConfig.java
@@ -405,7 +405,7 @@
          * single match session (corresponding to the same publish action on the
          * peer) reported to the host (using the
          * {@link WifiAwareDiscoverySessionCallback#onServiceDiscovered(WifiAwareManager.PeerHandle,
-         * byte[], byte[])}). The options are: only report the first match and ignore the rest
+         * byte[], List)}). The options are: only report the first match and ignore the rest
          * {@link SubscribeConfig#MATCH_STYLE_FIRST_ONLY} or report every single
          * match {@link SubscribeConfig#MATCH_STYLE_ALL} (the default).
          *
diff --git a/wifi/java/android/net/wifi/aware/WifiAwareCharacteristics.java b/wifi/java/android/net/wifi/aware/WifiAwareCharacteristics.java
index 95d128d..092aa34 100644
--- a/wifi/java/android/net/wifi/aware/WifiAwareCharacteristics.java
+++ b/wifi/java/android/net/wifi/aware/WifiAwareCharacteristics.java
@@ -70,8 +70,8 @@
     /**
      * Returns the maximum length of byte array that can be used to specify a Aware match filter.
      * Restricts the parameters of the
-     * {@link PublishConfig.Builder#setMatchFilter(java.util.List<byte[]>)} and
-     * {@link SubscribeConfig.Builder#setMatchFilter(java.util.List<byte[]>)}.
+     * {@link PublishConfig.Builder#setMatchFilter(java.util.List)} and
+     * {@link SubscribeConfig.Builder#setMatchFilter(java.util.List)}.
      *
      * @return A positive integer, maximum legngth of byte array for Aware discovery match filter.
      */
diff --git a/wifi/java/android/net/wifi/aware/WifiAwareDiscoveryBaseSession.java b/wifi/java/android/net/wifi/aware/WifiAwareDiscoveryBaseSession.java
index 451d8a5..2812ad4 100644
--- a/wifi/java/android/net/wifi/aware/WifiAwareDiscoveryBaseSession.java
+++ b/wifi/java/android/net/wifi/aware/WifiAwareDiscoveryBaseSession.java
@@ -140,7 +140,7 @@
      * Sends a message to the specified destination. Aware messages are transmitted in the context
      * of a discovery session - executed subsequent to a publish/subscribe
      * {@link WifiAwareDiscoverySessionCallback#onServiceDiscovered(WifiAwareManager.PeerHandle,
-     * byte[], java.util.List<byte[]>)} event.
+     * byte[], java.util.List)} event.
      * <p>
      *     Aware messages are not guaranteed delivery. Callbacks on
      *     {@link WifiAwareDiscoverySessionCallback} indicate message was transmitted successfully,
@@ -154,7 +154,7 @@
      *
      * @param peerHandle The peer's handle for the message. Must be a result of an
      * {@link WifiAwareDiscoverySessionCallback#onServiceDiscovered(WifiAwareManager.PeerHandle,
-     * byte[], java.util.List<byte[]>)} or
+     * byte[], java.util.List)} or
      * {@link WifiAwareDiscoverySessionCallback#onMessageReceived(WifiAwareManager.PeerHandle,
      * byte[])} events.
      * @param messageId An arbitrary integer used by the caller to identify the message. The same
@@ -187,7 +187,7 @@
      * Sends a message to the specified destination. Aware messages are transmitted in the context
      * of a discovery session - executed subsequent to a publish/subscribe
      * {@link WifiAwareDiscoverySessionCallback#onServiceDiscovered(WifiAwareManager.PeerHandle,
-     * byte[], java.util.List<byte[]>)} event.
+     * byte[], java.util.List)} event.
      * <p>
      *     Aware messages are not guaranteed delivery. Callbacks on
      *     {@link WifiAwareDiscoverySessionCallback} indicate message was transmitted successfully,
@@ -203,7 +203,7 @@
      *
      * @param peerHandle The peer's handle for the message. Must be a result of an
      * {@link WifiAwareDiscoverySessionCallback#onServiceDiscovered(WifiAwareManager.PeerHandle,
-     * byte[], java.util.List<byte[]>)} or
+     * byte[], java.util.List)} or
      * {@link WifiAwareDiscoverySessionCallback#onMessageReceived(WifiAwareManager.PeerHandle,
      * byte[])} events.
      * @param messageId An arbitrary integer used by the caller to identify the message. The same
@@ -220,7 +220,7 @@
     /**
      * Start a ranging operation with the specified peers. The peer IDs are obtained from an
      * {@link WifiAwareDiscoverySessionCallback#onServiceDiscovered(WifiAwareManager.PeerHandle,
-     * byte[], java.util.List<byte[]>)} or
+     * byte[], java.util.List)} or
      * {@link WifiAwareDiscoverySessionCallback#onMessageReceived(WifiAwareManager.PeerHandle,
      * byte[])} operation - can
      * only range devices which are part of an ongoing discovery session.
@@ -266,7 +266,7 @@
      *
      * @param peerHandle The peer's handle obtained through
      * {@link WifiAwareDiscoverySessionCallback#onServiceDiscovered(WifiAwareManager.PeerHandle,
-     * byte[], java.util.List<byte[]>)} or
+     * byte[], java.util.List)} or
      * {@link WifiAwareDiscoverySessionCallback#onMessageReceived(WifiAwareManager.PeerHandle,
      * byte[])}. On a RESPONDER this value is used to gate the acceptance of a connection request
      *                   from only that peer. A RESPONDER may specified a null - indicating that