Merge "Switch how destroyHardwareResources works"
diff --git a/Android.mk b/Android.mk
index 43dbef6..e68b310 100644
--- a/Android.mk
+++ b/Android.mk
@@ -306,6 +306,7 @@
 	core/java/android/service/wallpaper/IWallpaperService.aidl \
 	core/java/android/service/chooser/IChooserTargetService.aidl \
 	core/java/android/service/chooser/IChooserTargetResult.aidl \
+	core/java/android/text/ITextClassificationService.aidl \
 	core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl\
 	core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl\
 	core/java/android/view/accessibility/IAccessibilityManager.aidl \
diff --git a/api/current.txt b/api/current.txt
index c7fcc26..805586e 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -5423,6 +5423,7 @@
     method public boolean canShowBadge();
     method public int describeContents();
     method public void enableVibration(boolean);
+    method public android.media.AudioAttributes getAudioAttributes();
     method public java.lang.String getGroup();
     method public java.lang.String getId();
     method public int getImportance();
@@ -5436,7 +5437,7 @@
     method public void setLights(boolean);
     method public void setLockscreenVisibility(int);
     method public void setShowBadge(boolean);
-    method public void setSound(android.net.Uri);
+    method public void setSound(android.net.Uri, android.media.AudioAttributes);
     method public void setVibrationPattern(long[]);
     method public boolean shouldShowLights();
     method public boolean shouldVibrate();
diff --git a/api/system-current.txt b/api/system-current.txt
index 02432f0..64e9571 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -5599,6 +5599,7 @@
     method public boolean canShowBadge();
     method public int describeContents();
     method public void enableVibration(boolean);
+    method public android.media.AudioAttributes getAudioAttributes();
     method public java.lang.String getGroup();
     method public java.lang.String getId();
     method public int getImportance();
@@ -5617,7 +5618,7 @@
     method public void setLights(boolean);
     method public void setLockscreenVisibility(int);
     method public void setShowBadge(boolean);
-    method public void setSound(android.net.Uri);
+    method public void setSound(android.net.Uri, android.media.AudioAttributes);
     method public void setVibrationPattern(long[]);
     method public boolean shouldShowLights();
     method public boolean shouldVibrate();
@@ -5628,6 +5629,7 @@
     field public static final java.lang.String DEFAULT_CHANNEL_ID = "miscellaneous";
     field public static final int[] LOCKABLE_FIELDS;
     field public static final int USER_LOCKED_ALLOWED = 64; // 0x40
+    field public static final int USER_LOCKED_AUDIO_ATTRIBUTES = 256; // 0x100
     field public static final int USER_LOCKED_IMPORTANCE = 4; // 0x4
     field public static final int USER_LOCKED_LIGHTS = 8; // 0x8
     field public static final int USER_LOCKED_PRIORITY = 1; // 0x1
diff --git a/api/test-current.txt b/api/test-current.txt
index b599c1c2..a32300e 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -5433,6 +5433,7 @@
     method public boolean canShowBadge();
     method public int describeContents();
     method public void enableVibration(boolean);
+    method public android.media.AudioAttributes getAudioAttributes();
     method public java.lang.String getGroup();
     method public java.lang.String getId();
     method public int getImportance();
@@ -5446,7 +5447,7 @@
     method public void setLights(boolean);
     method public void setLockscreenVisibility(int);
     method public void setShowBadge(boolean);
-    method public void setSound(android.net.Uri);
+    method public void setSound(android.net.Uri, android.media.AudioAttributes);
     method public void setVibrationPattern(long[]);
     method public boolean shouldShowLights();
     method public boolean shouldVibrate();
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index 26ec418..58ff496 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -22,6 +22,7 @@
 
 import android.annotation.SystemApi;
 import android.app.NotificationManager;
+import android.media.AudioAttributes;
 import android.net.Uri;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -55,8 +56,9 @@
     private static final String ATT_VIBRATION = "vibration";
     private static final String ATT_VIBRATION_ENABLED = "vibration_enabled";
     private static final String ATT_SOUND = "sound";
-    //TODO: add audio attributes support
-    private static final String ATT_AUDIO_ATTRIBUTES = "audio_attributes";
+    private static final String ATT_USAGE = "usage";
+    private static final String ATT_FLAGS = "flags";
+    private static final String ATT_CONTENT_TYPE = "content_type";
     private static final String ATT_SHOW_BADGE = "show_badge";
     private static final String ATT_USER_LOCKED = "locked";
     private static final String ATT_GROUP = "group";
@@ -109,6 +111,12 @@
      * @hide
      */
     @SystemApi
+    public static final int USER_LOCKED_AUDIO_ATTRIBUTES = 0x00000100;
+
+    /**
+     * @hide
+     */
+    @SystemApi
     public static final int[] LOCKABLE_FIELDS = new int[] {
             USER_LOCKED_PRIORITY,
             USER_LOCKED_VISIBILITY,
@@ -117,7 +125,8 @@
             USER_LOCKED_VIBRATION,
             USER_LOCKED_SOUND,
             USER_LOCKED_ALLOWED,
-            USER_LOCKED_SHOW_BADGE
+            USER_LOCKED_SHOW_BADGE,
+            USER_LOCKED_AUDIO_ATTRIBUTES
     };
 
 
@@ -141,6 +150,7 @@
     private boolean mShowBadge = DEFAULT_SHOW_BADGE;
     private boolean mDeleted = DEFAULT_DELETED;
     private String mGroup;
+    private AudioAttributes mAudioAttributes = Notification.AUDIO_ATTRIBUTES_DEFAULT;
 
     /**
      * Creates a notification channel.
@@ -183,6 +193,7 @@
         } else {
             mGroup = null;
         }
+        mAudioAttributes = in.readInt() > 0 ? AudioAttributes.CREATOR.createFromParcel(in) : null;
     }
 
     @Override
@@ -215,6 +226,12 @@
         } else {
             dest.writeByte((byte) 0);
         }
+        if (mAudioAttributes != null) {
+            dest.writeInt(1);
+            mAudioAttributes.writeToParcel(dest, 0);
+        } else {
+            dest.writeInt(0);
+        }
     }
 
     /**
@@ -275,6 +292,9 @@
      *
      * Group information is only used for presentation, not for behavior.
      *
+     * Only modifiable before the channel is submitted to
+     * {@link NotificationManager#notify(String, int, Notification)}.
+     *
      * @param groupId the id of a group created by
      * {@link NotificationManager#createNotificationChannelGroup(NotificationChannelGroup)}.
      */
@@ -293,18 +313,23 @@
     }
 
     /**
-     * Sets the sound that should be played for notifications posted to this channel if
-     * the notifications don't supply a sound. Only modifiable before the channel is submitted
-     * to the NotificationManager.
+     * Sets the sound that should be played for notifications posted to this channel and its
+     * audio attributes.
+     *
+     * Only modifiable before the channel is submitted to
+     * {@link NotificationManager#notify(String, int, Notification)}.
      */
-    public void setSound(Uri sound) {
+    public void setSound(Uri sound, AudioAttributes audioAttributes) {
         this.mSound = sound;
+        this.mAudioAttributes = audioAttributes;
     }
 
     /**
      * Sets whether notifications posted to this channel should display notification lights,
-     * on devices that support that feature. Only modifiable before the channel is submitted to
-     * the NotificationManager.
+     * on devices that support that feature.
+     *
+     * Only modifiable before the channel is submitted to
+     * {@link NotificationManager#notify(String, int, Notification)}.
      */
     public void setLights(boolean lights) {
         this.mLights = lights;
@@ -312,16 +337,20 @@
 
     /**
      * Sets whether notification posted to this channel should vibrate. The vibration pattern can
-     * be set with {@link #setVibrationPattern(long[])}. Only modifiable before the channel is
-     * submitted to the NotificationManager.
+     * be set with {@link #setVibrationPattern(long[])}.
+     *
+     * Only modifiable before the channel is submitted to
+     * {@link NotificationManager#notify(String, int, Notification)}.
      */
     public void enableVibration(boolean vibration) {
         this.mVibrationEnabled = vibration;
     }
 
     /**
-     * Sets whether notification posted to this channel should vibrate. Only modifiable before the
-     * channel is submitted to the NotificationManager.
+     * Sets whether notification posted to this channel should vibrate.
+     *
+     * Only modifiable before the channel is submitted to
+     * {@link NotificationManager#notify(String, int, Notification)}.
      */
     public void setVibrationPattern(long[] vibrationPattern) {
         this.mVibration = vibrationPattern;
@@ -365,6 +394,13 @@
     }
 
     /**
+     * Returns the audio attributes for sound played by notifications posted to this channel.
+     */
+    public AudioAttributes getAudioAttributes() {
+        return mAudioAttributes;
+    }
+
+    /**
      * Returns whether notifications posted to this channel trigger notification lights.
      */
     public boolean shouldShowLights() {
@@ -438,7 +474,7 @@
         setBypassDnd(Notification.PRIORITY_DEFAULT
                 != safeInt(parser, ATT_PRIORITY, Notification.PRIORITY_DEFAULT));
         setLockscreenVisibility(safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY));
-        setSound(safeUri(parser, ATT_SOUND));
+        setSound(safeUri(parser, ATT_SOUND), safeAudioAttributes(parser));
         setLights(safeBool(parser, ATT_LIGHTS, false));
         enableVibration(safeBool(parser, ATT_VIBRATION_ENABLED, false));
         setVibrationPattern(safeLongArray(parser, ATT_VIBRATION, null));
@@ -471,6 +507,12 @@
         if (getSound() != null) {
             out.attribute(null, ATT_SOUND, getSound().toString());
         }
+        if (getAudioAttributes() != null) {
+            out.attribute(null, ATT_USAGE, Integer.toString(getAudioAttributes().getUsage()));
+            out.attribute(null, ATT_CONTENT_TYPE,
+                    Integer.toString(getAudioAttributes().getContentType()));
+            out.attribute(null, ATT_FLAGS, Integer.toString(getAudioAttributes().getFlags()));
+        }
         if (shouldShowLights()) {
             out.attribute(null, ATT_LIGHTS, Boolean.toString(shouldShowLights()));
         }
@@ -517,6 +559,12 @@
         if (getSound() != null) {
             record.put(ATT_SOUND, getSound().toString());
         }
+        if (getAudioAttributes() != null) {
+            record.put(ATT_USAGE, Integer.toString(getAudioAttributes().getUsage()));
+            record.put(ATT_CONTENT_TYPE,
+                    Integer.toString(getAudioAttributes().getContentType()));
+            record.put(ATT_FLAGS, Integer.toString(getAudioAttributes().getFlags()));
+        }
         record.put(ATT_LIGHTS, Boolean.toString(shouldShowLights()));
         record.put(ATT_VIBRATION_ENABLED, Boolean.toString(shouldVibrate()));
         record.put(ATT_USER_LOCKED, Integer.toString(getUserLockedFields()));
@@ -527,6 +575,18 @@
         return record;
     }
 
+    private static AudioAttributes safeAudioAttributes(XmlPullParser parser) {
+        int usage = safeInt(parser, ATT_USAGE, AudioAttributes.USAGE_NOTIFICATION);
+        int contentType = safeInt(parser, ATT_CONTENT_TYPE,
+                AudioAttributes.CONTENT_TYPE_SONIFICATION);
+        int flags = safeInt(parser, ATT_FLAGS, 0);
+        return new AudioAttributes.Builder()
+                .setUsage(usage)
+                .setContentType(contentType)
+                .setFlags(flags)
+                .build();
+    }
+
     private static Uri safeUri(XmlPullParser parser, String att) {
         final String val = parser.getAttributeValue(null, att);
         return val == null ? null : Uri.parse(val);
@@ -618,7 +678,11 @@
             return false;
         }
         if (!Arrays.equals(mVibration, that.mVibration)) return false;
-        return getGroup() != null ? getGroup().equals(that.getGroup()) : that.getGroup() == null;
+        if (getGroup() != null ? !getGroup().equals(that.getGroup()) : that.getGroup() != null) {
+            return false;
+        }
+        return getAudioAttributes() != null ? getAudioAttributes().equals(that.getAudioAttributes())
+                : that.getAudioAttributes() == null;
 
     }
 
@@ -637,6 +701,7 @@
         result = 31 * result + (mShowBadge ? 1 : 0);
         result = 31 * result + (isDeleted() ? 1 : 0);
         result = 31 * result + (getGroup() != null ? getGroup().hashCode() : 0);
+        result = 31 * result + (getAudioAttributes() != null ? getAudioAttributes().hashCode() : 0);
         return result;
     }
 
@@ -656,6 +721,7 @@
                 ", mShowBadge=" + mShowBadge +
                 ", mDeleted=" + mDeleted +
                 ", mGroup='" + mGroup + '\'' +
+                ", mAudioAttributes=" + mAudioAttributes +
                 '}';
     }
 }
diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java
index 885b42f3..4534767 100644
--- a/core/java/android/bluetooth/BluetoothAdapter.java
+++ b/core/java/android/bluetooth/BluetoothAdapter.java
@@ -1184,6 +1184,25 @@
     }
 
     /**
+     * Get the end time of the latest remote device discovery process.
+     * @return the latest time that the bluetooth adapter was/will be in discovery mode,
+     * in milliseconds since the epoch.
+     * This time can be in the future if {@link #startDiscovery()} has been called recently.
+     * @hide
+     */
+    public long getDiscoveryEndMillis() {
+        try {
+            mServiceLock.readLock().lock();
+            if (mService != null) return mService.getDiscoveryEndMillis();
+        } catch (RemoteException e) {
+            Log.e(TAG, "", e);
+        } finally {
+            mServiceLock.readLock().unlock();
+        }
+        return -1;
+    }
+
+    /**
      * Start the remote device discovery process.
      * <p>The discovery process usually involves an inquiry scan of about 12
      * seconds, followed by a page scan of each new device to retrieve its
diff --git a/core/java/android/bluetooth/IBluetooth.aidl b/core/java/android/bluetooth/IBluetooth.aidl
index 7c5458b..53fef2a 100644
--- a/core/java/android/bluetooth/IBluetooth.aidl
+++ b/core/java/android/bluetooth/IBluetooth.aidl
@@ -52,6 +52,7 @@
     boolean startDiscovery();
     boolean cancelDiscovery();
     boolean isDiscovering();
+    long getDiscoveryEndMillis();
 
     int getAdapterConnectionState();
     int getProfileConnectionState(int profile);
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 61531ae..4cf65ab 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -490,6 +490,7 @@
      */
     boolean performDexOpt(String packageName, boolean checkProfiles,
             int compileReason, boolean force);
+
     /**
      * Ask the package manager to perform a dex-opt with the given compiler filter.
      *
@@ -500,6 +501,16 @@
             String targetCompilerFilter, boolean force);
 
     /**
+     * Ask the package manager to perform a dex-opt with the given compiler filter on the
+     * secondary dex files belonging to the given package.
+     *
+     * Note: exposed only for the shell command to allow moving packages explicitly to a
+     *       definite state.
+     */
+    boolean performDexOptSecondary(String packageName,
+            String targetCompilerFilter, boolean force);
+
+    /**
      * Ask the package manager to dump profiles associated with a package.
      */
     void dumpProfiles(String packageName);
@@ -507,6 +518,18 @@
     void forceDexOpt(String packageName);
 
     /**
+     * Execute the background dexopt job immediately.
+     */
+    boolean runBackgroundDexoptJob();
+
+    /**
+     * Reconcile the information we have about the secondary dex files belonging to
+     * {@code packagName} and the actual dex files. For all dex files that were
+     * deleted, update the internal records and delete the generated oat files.
+     */
+    void reconcileSecondaryDexFiles(String packageName);
+
+    /**
      * Update status of external media on the package manager to scan and
      * install packages installed on the external media. Like say the
      * StorageManagerService uses this to call into the package manager to update
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index f1bffd3..f7ebf99a 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -4128,6 +4128,7 @@
             EPHEMERAL_SETTINGS.add(FONT_SCALE);
             EPHEMERAL_SETTINGS.add(HAPTIC_FEEDBACK_ENABLED);
             EPHEMERAL_SETTINGS.add(TIME_12_24);
+            EPHEMERAL_SETTINGS.add(SOUND_EFFECTS_ENABLED);
         }
 
         /**
diff --git a/core/java/android/text/ITextClassificationService.aidl b/core/java/android/text/ITextClassificationService.aidl
new file mode 100644
index 0000000..a73dbf0
--- /dev/null
+++ b/core/java/android/text/ITextClassificationService.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import android.os.ParcelFileDescriptor;
+
+/**
+ * Interface to the text classification service, which grants access to the text classification
+ * LSTM model file.
+ * {@hide}
+ */
+interface ITextClassificationService {
+
+    /**
+     * Request a file descriptor with read-only access to the LSTM model file.
+     * This file descriptor should be closed after the client is done with it.
+     */
+    ParcelFileDescriptor getModelFileFd();
+}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index e8535cdb..597c051 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -24768,6 +24768,13 @@
     }
 
     /**
+     * @hide Binary compatibility stub. To be removed when we finalize O APIs.
+     */
+    public void setTooltip(@Nullable CharSequence tooltipText) {
+        setTooltipText(tooltipText);
+    }
+
+    /**
      * Returns the view's tooltip text.
      *
      * @return the tooltip text
@@ -24777,6 +24784,14 @@
         return mTooltipInfo != null ? mTooltipInfo.mTooltipText : null;
     }
 
+    /**
+     * @hide Binary compatibility stub. To be removed when we finalize O APIs.
+     */
+    @Nullable
+    public CharSequence getTooltip() {
+        return getTooltipText();
+    }
+
     private boolean showTooltip(int x, int y, boolean fromLongClick) {
         if (mAttachInfo == null) {
             return false;
diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java
index 46324a3..5199b26 100644
--- a/core/java/android/widget/PopupWindow.java
+++ b/core/java/android/widget/PopupWindow.java
@@ -232,7 +232,7 @@
                         mDecorView.getLayoutParams();
 
                 updateAboveAnchor(findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff,
-                        p.width, p.height, mAnchoredGravity));
+                        p.width, p.height, mAnchoredGravity, false));
                 update(p.x, p.y, -1, -1, true);
             }
         }
@@ -1237,7 +1237,7 @@
         preparePopup(p);
 
         final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff,
-                p.width, p.height, gravity);
+                p.width, p.height, gravity, mAllowScrollingAnchorParent);
         updateAboveAnchor(aboveAnchor);
         p.accessibilityIdOfAnchor = (anchor != null) ? anchor.getAccessibilityViewId() : -1;
 
@@ -1529,10 +1529,12 @@
      * @param xOffset absolute horizontal offset from the left of the anchor
      * @param yOffset absolute vertical offset from the top of the anchor
      * @param gravity horizontal gravity specifying popup alignment
+     * @param allowScroll whether the anchor view's parent may be scrolled
+     *                    when the popup window doesn't fit on screen
      * @return true if the popup is translated upwards to fit on screen
      */
     private boolean findDropDownPosition(View anchor, WindowManager.LayoutParams outParams,
-            int xOffset, int yOffset, int width, int height, int gravity) {
+            int xOffset, int yOffset, int width, int height, int gravity, boolean allowScroll) {
         final int anchorHeight = anchor.getHeight();
         final int anchorWidth = anchor.getWidth();
         if (mOverlapAnchor) {
@@ -1586,7 +1588,7 @@
             final int scrollY = anchor.getScrollY();
             final Rect r = new Rect(scrollX, scrollY, scrollX + width + xOffset,
                     scrollY + height + anchorHeight + yOffset);
-            if (mAllowScrollingAnchorParent && anchor.requestRectangleOnScreen(r, true)) {
+            if (allowScroll && anchor.requestRectangleOnScreen(r, true)) {
                 // Reset for the new anchor position.
                 anchor.getLocationInWindow(drawingLocation);
                 outParams.x = drawingLocation[0] + xOffset;
@@ -2182,15 +2184,19 @@
         }
 
         final boolean aboveAnchor = findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff,
-                width, height, gravity);
+                width, height, gravity, mAllowScrollingAnchorParent);
         updateAboveAnchor(aboveAnchor);
 
         final boolean paramsChanged = oldGravity != p.gravity || oldX != p.x || oldY != p.y
                 || oldWidth != p.width || oldHeight != p.height;
-        // If width and mWidth were both < 0 then we have a MATCH_PARENT/WRAP_CONTENT case.
-        // findDropDownPosition will have resolved this to absolute values,
-        // but we don't want to update mWidth/mHeight to these absolute values.
-        update(p.x, p.y, width < 0 ? width : p.width, height < 0 ? height : p.height, paramsChanged);
+
+        // If width and mWidth were both < 0 then we have a MATCH_PARENT or
+        // WRAP_CONTENT case. findDropDownPosition will have resolved this to
+        // absolute values, but we don't want to update mWidth/mHeight to these
+        // absolute values.
+        final int newWidth = width < 0 ? width : p.width;
+        final int newHeight = height < 0 ? height : p.height;
+        update(p.x, p.y, newWidth, newHeight, paramsChanged);
     }
 
     /**
diff --git a/core/res/res/raw/accessibility_gestures.bin b/core/res/res/raw/accessibility_gestures.bin
deleted file mode 100644
index acd7993..0000000
--- a/core/res/res/raw/accessibility_gestures.bin
+++ /dev/null
Binary files differ
diff --git a/core/res/res/values-mcc208-mnc10/config.xml b/core/res/res/values-mcc208-mnc10/config.xml
index d3640e5..3ed7818 100644
--- a/core/res/res/values-mcc208-mnc10/config.xml
+++ b/core/res/res/values-mcc208-mnc10/config.xml
@@ -31,28 +31,4 @@
         <item>[ApnSettingV3]INTERNET NRJ,internetnrj,,,,,,,,,208,10,,DUN,,,true,0,,,,,,,gid,4E</item>
     </string-array>
 
-    <string-array translatable="false" name="config_operatorConsideredNonRoaming">
-        <item>21401</item>
-        <item>21402</item>
-        <item>21403</item>
-        <item>21404</item>
-        <item>21405</item>
-        <item>21406</item>
-        <item>21407</item>
-        <item>21408</item>
-        <item>21409</item>
-        <item>21410</item>
-        <item>21411</item>
-        <item>21412</item>
-        <item>21413</item>
-        <item>21414</item>
-        <item>21415</item>
-        <item>21416</item>
-        <item>21417</item>
-        <item>21418</item>
-        <item>21419</item>
-        <item>21420</item>
-        <item>21421</item>
-    </string-array>
-
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 554e123..25ebb87 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1453,7 +1453,6 @@
 
   <java-symbol type="raw" name="color_fade_vert" />
   <java-symbol type="raw" name="color_fade_frag" />
-  <java-symbol type="raw" name="accessibility_gestures" />
   <java-symbol type="raw" name="loaderror" />
   <java-symbol type="raw" name="nodomain" />
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java
index 115c622..2fb6843 100755
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java
@@ -135,6 +135,10 @@
         mAdapter.setDiscoverableTimeout(timeout);
     }
 
+    public long getDiscoveryEndMillis() {
+        return mAdapter.getDiscoveryEndMillis();
+    }
+
     public void setName(String name) {
         mAdapter.setName(name);
     }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityGestureDetector.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityGestureDetector.java
index 582b19b..b95d2e6 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityGestureDetector.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityGestureDetector.java
@@ -16,19 +16,18 @@
 
 package com.android.server.accessibility;
 
+import android.accessibilityservice.AccessibilityService;
 import android.content.Context;
 import android.gesture.Gesture;
-import android.gesture.GestureLibraries;
-import android.gesture.GestureLibrary;
 import android.gesture.GesturePoint;
 import android.gesture.GestureStore;
 import android.gesture.GestureStroke;
 import android.gesture.Prediction;
+import android.graphics.PointF;
 import android.util.Slog;
 import android.util.TypedValue;
 import android.view.GestureDetector;
 import android.view.MotionEvent;
-import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
 
 import com.android.internal.R;
@@ -47,6 +46,49 @@
     // Tag for logging received events.
     private static final String LOG_TAG = "AccessibilityGestureDetector";
 
+    // Constants for sampling motion event points.
+    // We sample based on a minimum distance between points, primarily to improve accuracy by
+    // reducing noisy minor changes in direction.
+    private static final float MIN_INCHES_BETWEEN_SAMPLES = 0.1f;
+    private final float mMinPixelsBetweenSamplesX;
+    private final float mMinPixelsBetweenSamplesY;
+
+    // Constants for separating gesture segments
+    private static final float ANGLE_THRESHOLD = 0.0f;
+
+    // Constants for line segment directions
+    private static final int LEFT = 0;
+    private static final int RIGHT = 1;
+    private static final int UP = 2;
+    private static final int DOWN = 3;
+    private static final int[][] DIRECTIONS_TO_GESTURE_ID = {
+        {
+            AccessibilityService.GESTURE_SWIPE_LEFT,
+            AccessibilityService.GESTURE_SWIPE_LEFT_AND_RIGHT,
+            AccessibilityService.GESTURE_SWIPE_LEFT_AND_UP,
+            AccessibilityService.GESTURE_SWIPE_LEFT_AND_DOWN
+        },
+        {
+            AccessibilityService.GESTURE_SWIPE_RIGHT_AND_LEFT,
+            AccessibilityService.GESTURE_SWIPE_RIGHT,
+            AccessibilityService.GESTURE_SWIPE_RIGHT_AND_UP,
+            AccessibilityService.GESTURE_SWIPE_RIGHT_AND_DOWN
+        },
+        {
+            AccessibilityService.GESTURE_SWIPE_UP_AND_LEFT,
+            AccessibilityService.GESTURE_SWIPE_UP_AND_RIGHT,
+            AccessibilityService.GESTURE_SWIPE_UP,
+            AccessibilityService.GESTURE_SWIPE_UP_AND_DOWN
+        },
+        {
+            AccessibilityService.GESTURE_SWIPE_DOWN_AND_LEFT,
+            AccessibilityService.GESTURE_SWIPE_DOWN_AND_RIGHT,
+            AccessibilityService.GESTURE_SWIPE_DOWN_AND_UP,
+            AccessibilityService.GESTURE_SWIPE_DOWN
+        }
+    };
+
+
     /**
      * Listener functions are called as a result of onMoveEvent().  The current
      * MotionEvent in the context of these functions is the event passed into
@@ -102,10 +144,8 @@
     }
 
     private final Listener mListener;
-    private final GestureDetector mGestureDetector;
-
-    // The library for gesture detection.
-    private final GestureLibrary mGestureLibrary;
+    private final Context mContext;  // Retained for on-demand construction of GestureDetector.
+    protected GestureDetector mGestureDetector;  // Double-tap detector. Visible for test.
 
     // Indicates that a single tap has occurred.
     private boolean mFirstTapDetected;
@@ -168,28 +208,26 @@
     // movement when gesturing, and touch exploring.  Based on user testing,
     // all gestures started with the initial movement taking less than 100ms.
     // When touch exploring, the first movement almost always takes longer than
-    // 200ms.  From this data, 200ms seems the best value to decide what
-    // kind of interaction it is.
-    private static final long CANCEL_ON_PAUSE_THRESHOLD_NOT_STARTED_MS = 200;
+    // 200ms.
+    private static final long CANCEL_ON_PAUSE_THRESHOLD_NOT_STARTED_MS = 150;
 
     // Time threshold used to determine if a gesture should be cancelled.  If
-    // the finger pauses for longer than this delay, the ongoing gesture is
+    // the finger takes more than this time to move 1cm, the ongoing gesture is
     // cancelled.
-    private static final long CANCEL_ON_PAUSE_THRESHOLD_STARTED_MS = 500;
+    private static final long CANCEL_ON_PAUSE_THRESHOLD_STARTED_MS = 300;
 
     AccessibilityGestureDetector(Context context, Listener listener) {
         mListener = listener;
-
-        mGestureDetector = new GestureDetector(context, this);
-        mGestureDetector.setOnDoubleTapListener(this);
-
-        mGestureLibrary = GestureLibraries.fromRawResource(context, R.raw.accessibility_gestures);
-        mGestureLibrary.setOrientationStyle(8 /* GestureStore.ORIENTATION_SENSITIVE_8 */);
-        mGestureLibrary.setSequenceType(GestureStore.SEQUENCE_SENSITIVE);
-        mGestureLibrary.load();
+        mContext = context;
 
         mGestureDetectionThreshold = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1,
                 context.getResources().getDisplayMetrics()) * GESTURE_CONFIRM_MM;
+
+        // Calculate minimum gesture velocity
+        final float pixelsPerInchX = context.getResources().getDisplayMetrics().xdpi;
+        final float pixelsPerInchY = context.getResources().getDisplayMetrics().ydpi;
+        mMinPixelsBetweenSamplesX = MIN_INCHES_BETWEEN_SAMPLES * pixelsPerInchX;
+        mMinPixelsBetweenSamplesY = MIN_INCHES_BETWEEN_SAMPLES * pixelsPerInchY;
     }
 
     /**
@@ -205,6 +243,18 @@
      * @return true if the event is consumed, else false
      */
     public boolean onMotionEvent(MotionEvent event, int policyFlags) {
+
+        // Construct GestureDetector double-tap detector on demand, so that testable sub-class
+        // can use mock GestureDetector.
+        // TODO: Break the circular dependency between GestureDetector's constructor and
+        // AccessibilityGestureDetector's constructor. Construct GestureDetector in TouchExplorer,
+        // using a GestureDetector listener owned by TouchExplorer, which passes double-tap state
+        // information to AccessibilityGestureDetector.
+        if (mGestureDetector == null) {
+            mGestureDetector = new GestureDetector(mContext, this);
+            mGestureDetector.setOnDoubleTapListener(this);
+        }
+
         final float x = event.getX();
         final float y = event.getY();
         final long time = event.getEventTime();
@@ -267,7 +317,7 @@
 
                     final float dX = Math.abs(x - mPreviousGestureX);
                     final float dY = Math.abs(y - mPreviousGestureY);
-                    if (dX >= TOUCH_TOLERANCE || dY >= TOUCH_TOLERANCE) {
+                    if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) {
                         mPreviousGestureX = x;
                         mPreviousGestureY = y;
                         mStrokeBuffer.add(new GesturePoint(x, y, time));
@@ -280,8 +330,11 @@
                     return finishDoubleTap(event, policyFlags);
                 }
                 if (mGestureStarted) {
-                    mStrokeBuffer.add(new GesturePoint(x, y, time));
-
+                    final float dX = Math.abs(x - mPreviousGestureX);
+                    final float dY = Math.abs(y - mPreviousGestureY);
+                    if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) {
+                        mStrokeBuffer.add(new GesturePoint(x, y, time));
+                    }
                     return recognizeGesture(event, policyFlags);
                 }
                 break;
@@ -397,30 +450,154 @@
         mStrokeBuffer.clear();
     }
 
+    /**
+     * Looks at the sequence of motions in mStrokeBuffer, classifies the gesture, then calls
+     * Listener callbacks for success or failure.
+     *
+     * @param event The raw motion event to pass to the listener callbacks.
+     * @param policyFlags Policy flags for the event.
+     *
+     * @return true if the event is consumed, else false
+     */
     private boolean recognizeGesture(MotionEvent event, int policyFlags) {
-        Gesture gesture = new Gesture();
-        gesture.addStroke(new GestureStroke(mStrokeBuffer));
-
-        ArrayList<Prediction> predictions = mGestureLibrary.recognize(gesture);
-        if (!predictions.isEmpty()) {
-            Prediction bestPrediction = predictions.get(0);
-            if (bestPrediction.score >= MIN_PREDICTION_SCORE) {
-                if (DEBUG) {
-                    Slog.i(LOG_TAG, "gesture: " + bestPrediction.name + " score: "
-                            + bestPrediction.score);
-                }
-                try {
-                    final int gestureId = Integer.parseInt(bestPrediction.name);
-                    return mListener.onGestureCompleted(gestureId);
-                } catch (NumberFormatException nfe) {
-                    Slog.w(LOG_TAG, "Non numeric gesture id:" + bestPrediction.name);
-                }
-            }
+        if (mStrokeBuffer.size() < 2) {
+            return mListener.onGestureCancelled(event, policyFlags);
         }
 
+        // Look at mStrokeBuffer and extract 2 line segments, delimited by near-perpendicular
+        // direction change.
+        // Method: for each sampled motion event, check the angle of the most recent motion vector
+        // versus the preceding motion vector, and segment the line if the angle is about
+        // 90 degrees.
+
+        ArrayList<PointF> path = new ArrayList<>();
+        PointF lastDelimiter = new PointF(mStrokeBuffer.get(0).x, mStrokeBuffer.get(0).y);
+        path.add(lastDelimiter);
+
+        float dX = 0;  // Sum of unit vectors from last delimiter to each following point
+        float dY = 0;
+        int count = 0;  // Number of points since last delimiter
+        float length = 0;  // Vector length from delimiter to most recent point
+
+        PointF next = new PointF();
+        for (int i = 1; i < mStrokeBuffer.size(); ++i) {
+            next = new PointF(mStrokeBuffer.get(i).x, mStrokeBuffer.get(i).y);
+            if (count > 0) {
+                // Average of unit vectors from delimiter to following points
+                float currentDX = dX / count;
+                float currentDY = dY / count;
+
+                // newDelimiter is a possible new delimiter, based on a vector with length from
+                // the last delimiter to the previous point, but in the direction of the average
+                // unit vector from delimiter to previous points.
+                // Using the averaged vector has the effect of "squaring off the curve",
+                // creating a sharper angle between the last motion and the preceding motion from
+                // the delimiter. In turn, this sharper angle achieves the splitting threshold
+                // even in a gentle curve.
+                PointF newDelimiter = new PointF(length * currentDX + lastDelimiter.x,
+                    length * currentDY + lastDelimiter.y);
+
+                // Unit vector from newDelimiter to the most recent point
+                float nextDX = next.x - newDelimiter.x;
+                float nextDY = next.y - newDelimiter.y;
+                float nextLength = (float) Math.sqrt(nextDX * nextDX + nextDY * nextDY);
+                nextDX = nextDX / nextLength;
+                nextDY = nextDY / nextLength;
+
+                // Compare the initial motion direction to the most recent motion direction,
+                // and segment the line if direction has changed by about 90 degrees.
+                float dot = currentDX * nextDX + currentDY * nextDY;
+                if (dot < ANGLE_THRESHOLD) {
+                    path.add(newDelimiter);
+                    lastDelimiter = newDelimiter;
+                    dX = 0;
+                    dY = 0;
+                    count = 0;
+                }
+            }
+
+            // Vector from last delimiter to most recent point
+            float currentDX = next.x - lastDelimiter.x;
+            float currentDY = next.y - lastDelimiter.y;
+            length = (float) Math.sqrt(currentDX * currentDX + currentDY * currentDY);
+
+            // Increment sum of unit vectors from delimiter to each following point
+            count = count + 1;
+            dX = dX + currentDX / length;
+            dY = dY + currentDY / length;
+        }
+
+        path.add(next);
+        Slog.i(LOG_TAG, "path=" + path.toString());
+
+        // Classify line segments, and call Listener callbacks.
+        return recognizeGesturePath(event, policyFlags, path);
+    }
+
+    /**
+     * Classifies a pair of line segments, by direction.
+     * Calls Listener callbacks for success or failure.
+     *
+     * @param event The raw motion event to pass to the listener's onGestureCanceled method.
+     * @param policyFlags Policy flags for the event.
+     * @param path A sequence of motion line segments derived from motion points in mStrokeBuffer.
+     *
+     * @return true if the event is consumed, else false
+     */
+    private boolean recognizeGesturePath(MotionEvent event, int policyFlags,
+            ArrayList<PointF> path) {
+
+        if (path.size() == 2) {
+            PointF start = path.get(0);
+            PointF end = path.get(1);
+
+            float dX = end.x - start.x;
+            float dY = end.y - start.y;
+            int direction = toDirection(dX, dY);
+            switch (direction) {
+                case LEFT:
+                    return mListener.onGestureCompleted(AccessibilityService.GESTURE_SWIPE_LEFT);
+                case RIGHT:
+                    return mListener.onGestureCompleted(AccessibilityService.GESTURE_SWIPE_RIGHT);
+                case UP:
+                    return mListener.onGestureCompleted(AccessibilityService.GESTURE_SWIPE_UP);
+                case DOWN:
+                    return mListener.onGestureCompleted(AccessibilityService.GESTURE_SWIPE_DOWN);
+                default:
+                    // Do nothing.
+            }
+
+        } else if (path.size() == 3) {
+            PointF start = path.get(0);
+            PointF mid = path.get(1);
+            PointF end = path.get(2);
+
+            float dX0 = mid.x - start.x;
+            float dY0 = mid.y - start.y;
+
+            float dX1 = end.x - mid.x;
+            float dY1 = end.y - mid.y;
+
+            int segmentDirection0 = toDirection(dX0, dY0);
+            int segmentDirection1 = toDirection(dX1, dY1);
+            int gestureId = DIRECTIONS_TO_GESTURE_ID[segmentDirection0][segmentDirection1];
+            return mListener.onGestureCompleted(gestureId);
+        }
+        // else if (path.size() < 2 || 3 < path.size()) then no gesture recognized.
         return mListener.onGestureCancelled(event, policyFlags);
     }
 
+    /** Maps a vector to a dominant direction in set {LEFT, RIGHT, UP, DOWN}. */
+    private static int toDirection(float dX, float dY) {
+        if (Math.abs(dX) > Math.abs(dY)) {
+            // Horizontal
+            return (dX < 0) ? LEFT : RIGHT;
+        } else {
+            // Vertical
+            return (dY < 0) ? UP : DOWN;
+        }
+    }
+
     private MotionEvent mapSecondPointerToFirstPointer(MotionEvent event) {
         // Only map basic events when two fingers are down.
         if (event.getPointerCount() != 2 ||
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index 5739693..ed2da68 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -205,18 +205,26 @@
 
     private AudioAttributes calculateAttributes() {
         final Notification n = sbn.getNotification();
-        AudioAttributes attributes = Notification.AUDIO_ATTRIBUTES_DEFAULT;
+        AudioAttributes attributes = getChannel().getAudioAttributes();
+        if (attributes == null) {
+            attributes = Notification.AUDIO_ATTRIBUTES_DEFAULT;
+        }
 
-        if (n.audioAttributes != null) {
-            // prefer audio attributes to stream type
-            attributes = n.audioAttributes;
-        } else if (n.audioStreamType >= 0 && n.audioStreamType < AudioSystem.getNumStreamTypes()) {
-            // the stream type is valid, use it
-            attributes = new AudioAttributes.Builder()
-                    .setInternalLegacyStreamType(n.audioStreamType)
-                    .build();
-        } else if (n.audioStreamType != AudioSystem.STREAM_DEFAULT) {
-            Log.w(TAG, String.format("Invalid stream type: %d", n.audioStreamType));
+        if (mPreChannelsNotification
+                && (getChannel().getUserLockedFields()
+                & NotificationChannel.USER_LOCKED_SOUND) == 0) {
+            if (n.audioAttributes != null) {
+                // prefer audio attributes to stream type
+                attributes = n.audioAttributes;
+            } else if (n.audioStreamType >= 0
+                    && n.audioStreamType < AudioSystem.getNumStreamTypes()) {
+                // the stream type is valid, use it
+                attributes = new AudioAttributes.Builder()
+                        .setInternalLegacyStreamType(n.audioStreamType)
+                        .build();
+            } else if (n.audioStreamType != AudioSystem.STREAM_DEFAULT) {
+                Log.w(TAG, String.format("Invalid stream type: %d", n.audioStreamType));
+            }
         }
         return attributes;
     }
diff --git a/services/core/java/com/android/server/notification/RankingHelper.java b/services/core/java/com/android/server/notification/RankingHelper.java
index 5b6ac69..8176e5d 100644
--- a/services/core/java/com/android/server/notification/RankingHelper.java
+++ b/services/core/java/com/android/server/notification/RankingHelper.java
@@ -569,7 +569,7 @@
             channel.setBypassDnd(updatedChannel.canBypassDnd());
         }
         if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_SOUND) == 0) {
-            channel.setSound(updatedChannel.getSound());
+            channel.setSound(updatedChannel.getSound(), updatedChannel.getAudioAttributes());
         }
         if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_VIBRATION) == 0) {
             channel.enableVibration(updatedChannel.shouldVibrate());
diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptService.java b/services/core/java/com/android/server/pm/BackgroundDexOptService.java
index 601a219..66977d6 100644
--- a/services/core/java/com/android/server/pm/BackgroundDexOptService.java
+++ b/services/core/java/com/android/server/pm/BackgroundDexOptService.java
@@ -30,10 +30,13 @@
 import android.os.BatteryManager;
 import android.os.Environment;
 import android.os.ServiceManager;
+import android.os.SystemProperties;
 import android.os.storage.StorageManager;
 import android.util.ArraySet;
 import android.util.Log;
 
+import com.android.server.pm.dex.DexManager;
+
 import java.io.File;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.TimeUnit;
@@ -59,21 +62,33 @@
             "android",
             BackgroundDexOptService.class.getName());
 
+    // Possible return codes of individual optimization steps.
+
+    // Optimizations finished. All packages were processed.
+    private static final int OPTIMIZE_PROCESSED = 0;
+    // Optimizations should continue. Issued after checking the scheduler, disk space or battery.
+    private static final int OPTIMIZE_CONTINUE = 1;
+    // Optimizations should be aborted. Job scheduler requested it.
+    private static final int OPTIMIZE_ABORT_BY_JOB_SCHEDULER = 2;
+    // Optimizations should be aborted. No space left on device.
+    private static final int OPTIMIZE_ABORT_NO_SPACE_LEFT = 3;
+
     /**
      * Set of failed packages remembered across job runs.
      */
-    static final ArraySet<String> sFailedPackageNames = new ArraySet<String>();
+    static final ArraySet<String> sFailedPackageNamesPrimary = new ArraySet<String>();
+    static final ArraySet<String> sFailedPackageNamesSecondary = new ArraySet<String>();
 
     /**
      * Atomics set to true if the JobScheduler requests an abort.
      */
-    final AtomicBoolean mAbortPostBootUpdate = new AtomicBoolean(false);
-    final AtomicBoolean mAbortIdleOptimization = new AtomicBoolean(false);
+    private final AtomicBoolean mAbortPostBootUpdate = new AtomicBoolean(false);
+    private final AtomicBoolean mAbortIdleOptimization = new AtomicBoolean(false);
 
     /**
      * Atomic set to true if one job should exit early because another job was started.
      */
-    final AtomicBoolean mExitPostBootUpdate = new AtomicBoolean(false);
+    private final AtomicBoolean mExitPostBootUpdate = new AtomicBoolean(false);
 
     private final File mDataDir = Environment.getDataDirectory();
 
@@ -104,8 +119,11 @@
         // The idle maintanance job skips packages which previously failed to
         // compile. The given package has changed and may successfully compile
         // now. Remove it from the list of known failing packages.
-        synchronized (sFailedPackageNames) {
-            sFailedPackageNames.remove(packageName);
+        synchronized (sFailedPackageNamesPrimary) {
+            sFailedPackageNamesPrimary.remove(packageName);
+        }
+        synchronized (sFailedPackageNamesSecondary) {
+            sFailedPackageNamesSecondary.remove(packageName);
         }
     }
 
@@ -124,9 +142,9 @@
         return (100 * level / scale);
     }
 
-    private long getLowStorageThreshold() {
+    private long getLowStorageThreshold(Context context) {
         @SuppressWarnings("deprecation")
-        final long lowThreshold = StorageManager.from(this).getStorageLowBytes(mDataDir);
+        final long lowThreshold = StorageManager.from(context).getStorageLowBytes(mDataDir);
         if (lowThreshold == 0) {
             Log.e(TAG, "Invalid low storage threshold");
         }
@@ -155,7 +173,7 @@
         // Load low battery threshold from the system config. This is a 0-100 integer.
         final int lowBatteryThreshold = getResources().getInteger(
                 com.android.internal.R.integer.config_lowBatteryWarningLevel);
-        final long lowThreshold = getLowStorageThreshold();
+        final long lowThreshold = getLowStorageThreshold(this);
 
         mAbortPostBootUpdate.set(false);
 
@@ -206,61 +224,123 @@
         new Thread("BackgroundDexOptService_IdleOptimization") {
             @Override
             public void run() {
-                idleOptimization(jobParams, pm, pkgs);
+                int result = idleOptimization(pm, pkgs, BackgroundDexOptService.this);
+                if (result != OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
+                    Log.w(TAG, "Idle optimizations aborted because of space constraints.");
+                    // If we didn't abort we ran to completion (or stopped because of space).
+                    // Abandon our timeslice and do not reschedule.
+                    jobFinished(jobParams, /* reschedule */ false);
+                }
             }
         }.start();
         return true;
     }
 
-    private void idleOptimization(JobParameters jobParams, PackageManagerService pm,
-            ArraySet<String> pkgs) {
+    // Optimize the given packages and return the optimization result (one of the OPTIMIZE_* codes).
+    private int idleOptimization(PackageManagerService pm, ArraySet<String> pkgs, Context context) {
         Log.i(TAG, "Performing idle optimizations");
         // If post-boot update is still running, request that it exits early.
         mExitPostBootUpdate.set(true);
-
         mAbortIdleOptimization.set(false);
 
-        final long lowThreshold = getLowStorageThreshold();
-        for (String pkg : pkgs) {
-            if (mAbortIdleOptimization.get()) {
-                // JobScheduler requested an early abort.
-                return;
+        long lowStorageThreshold = getLowStorageThreshold(context);
+        // Optimize primary apks.
+        int result = optimizePackages(pm, pkgs, lowStorageThreshold, /*is_for_primary_dex*/ true,
+                sFailedPackageNamesPrimary);
+
+        if (result == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
+            return result;
+        }
+
+        if (SystemProperties.getBoolean("dalvik.vm.deopt.secondary", false)) {
+            result = reconcileSecondaryDexFiles(pm.getDexManager());
+            if (result == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
+                return result;
             }
 
-            synchronized (sFailedPackageNames) {
-                if (sFailedPackageNames.contains(pkg)) {
+            result = optimizePackages(pm, pkgs, lowStorageThreshold, /*is_for_primary_dex*/ false,
+                    sFailedPackageNamesSecondary);
+        }
+        return result;
+    }
+
+    private int optimizePackages(PackageManagerService pm, ArraySet<String> pkgs,
+            long lowStorageThreshold, boolean is_for_primary_dex,
+            ArraySet<String> failedPackageNames) {
+        for (String pkg : pkgs) {
+            int abort_code = abortIdleOptimizations(lowStorageThreshold);
+            if (abort_code != OPTIMIZE_CONTINUE) {
+                return abort_code;
+            }
+
+            synchronized (failedPackageNames) {
+                if (failedPackageNames.contains(pkg)) {
                     // Skip previously failing package
                     continue;
+                } else {
+                    // Conservatively add package to the list of failing ones in case performDexOpt
+                    // never returns.
+                    failedPackageNames.add(pkg);
                 }
             }
 
-            long usableSpace = mDataDir.getUsableSpace();
-            if (usableSpace < lowThreshold) {
-                // Rather bail than completely fill up the disk.
-                Log.w(TAG, "Aborting background dex opt job due to low storage: " +
-                        usableSpace);
-                break;
-            }
-
-            // Conservatively add package to the list of failing ones in case performDexOpt
-            // never returns.
-            synchronized (sFailedPackageNames) {
-                sFailedPackageNames.add(pkg);
-            }
             // Optimize package if needed. Note that there can be no race between
             // concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized.
-            if (pm.performDexOpt(pkg,
-                    /* checkProfiles */ true,
-                    PackageManagerService.REASON_BACKGROUND_DEXOPT,
-                    /* force */ false)) {
+            boolean success = is_for_primary_dex
+                    ? pm.performDexOpt(pkg,
+                            /* checkProfiles */ true,
+                            PackageManagerService.REASON_BACKGROUND_DEXOPT,
+                            /* force */ false)
+                    : pm.performDexOptSecondary(pkg,
+                            PackageManagerServiceCompilerMapping.getFullCompilerFilter(),
+                            /* force */ true);
+            if (success) {
                 // Dexopt succeeded, remove package from the list of failing ones.
-                synchronized (sFailedPackageNames) {
-                    sFailedPackageNames.remove(pkg);
+                synchronized (failedPackageNames) {
+                    failedPackageNames.remove(pkg);
                 }
             }
         }
-        // Ran to completion, so we abandon our timeslice and do not reschedule.
-        jobFinished(jobParams, /* reschedule */ false);
+        return OPTIMIZE_PROCESSED;
+    }
+
+    private int reconcileSecondaryDexFiles(DexManager dm) {
+        // TODO(calin): should we blacklist packages for which we fail to reconcile?
+        for (String p : dm.getAllPackagesWithSecondaryDexFiles()) {
+            if (mAbortIdleOptimization.get()) {
+                return OPTIMIZE_ABORT_BY_JOB_SCHEDULER;
+            }
+            dm.reconcileSecondaryDexFiles(p);
+        }
+        return OPTIMIZE_PROCESSED;
+    }
+
+    // Evaluate whether or not idle optimizations should continue.
+    private int abortIdleOptimizations(long lowStorageThreshold) {
+        if (mAbortIdleOptimization.get()) {
+            // JobScheduler requested an early abort.
+            return OPTIMIZE_ABORT_BY_JOB_SCHEDULER;
+        }
+        long usableSpace = mDataDir.getUsableSpace();
+        if (usableSpace < lowStorageThreshold) {
+            // Rather bail than completely fill up the disk.
+            Log.w(TAG, "Aborting background dex opt job due to low storage: " + usableSpace);
+            return OPTIMIZE_ABORT_NO_SPACE_LEFT;
+        }
+
+        return OPTIMIZE_CONTINUE;
+    }
+
+    /**
+     * Execute the idle optimizations immediately.
+     */
+    public static boolean runIdleOptimizationsNow(PackageManagerService pm, Context context) {
+        // Create a new object to make sure we don't interfere with the scheduled jobs.
+        // Note that this may still run at the same time with the job scheduled by the
+        // JobScheduler but the scheduler will not be able to cancel it.
+        BackgroundDexOptService bdos = new BackgroundDexOptService();
+        int result = bdos.idleOptimization(pm, pm.getOptimizablePackages(), context);
+        return result == OPTIMIZE_PROCESSED;
     }
 
     @Override
@@ -281,7 +361,7 @@
         }
 
         final ArraySet<String> pkgs = pm.getOptimizablePackages();
-        if (pkgs == null || pkgs.isEmpty()) {
+        if (pkgs.isEmpty()) {
             if (DEBUG_DEXOPT) {
                 Log.i(TAG, "No packages to optimize");
             }
diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java
index fc66bb3..449d808 100644
--- a/services/core/java/com/android/server/pm/Installer.java
+++ b/services/core/java/com/android/server/pm/Installer.java
@@ -50,6 +50,14 @@
     public static final int DEXOPT_BOOTCOMPLETE   = 1 << 4;
     /** Hint that the dexopt type is profile-guided. */
     public static final int DEXOPT_PROFILE_GUIDED = 1 << 5;
+    /** The compilation is for a secondary dex file. */
+    public static final int DEXOPT_SECONDARY_DEX  = 1 << 6;
+    /** Ignore the result of dexoptNeeded and force compilation. */
+    public static final int DEXOPT_FORCE          = 1 << 7;
+    /** Indicates that the dex file passed to dexopt in on CE storage. */
+    public static final int DEXOPT_STORAGE_CE     = 1 << 8;
+    /** Indicates that the dex file passed to dexopt in on DE storage. */
+    public static final int DEXOPT_STORAGE_DE     = 1 << 9;
 
     // NOTE: keep in sync with installd
     public static final int FLAG_CLEAR_CACHE_ONLY = 1 << 8;
@@ -434,6 +442,20 @@
         }
     }
 
+    public boolean reconcileSecondaryDexFile(String apkPath, String packageName, int uid,
+            String[] isas, @Nullable String volumeUuid, int flags) throws InstallerException {
+        for (int i = 0; i < isas.length; i++) {
+            assertValidInstructionSet(isas[i]);
+        }
+        if (!checkBeforeRemote()) return false;
+        try {
+            return mInstalld.reconcileSecondaryDexFile(apkPath, packageName, uid, isas,
+                    volumeUuid, flags);
+        } catch (Exception e) {
+            throw InstallerException.from(e);
+        }
+    }
+
     public void invalidateMounts() throws InstallerException {
         if (!checkBeforeRemote()) return;
         try {
diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
index 8e201ac..db712ae 100644
--- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java
+++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
@@ -19,6 +19,7 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageParser;
 import android.os.Environment;
 import android.os.PowerManager;
@@ -35,6 +36,7 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 
 import dalvik.system.DexFile;
 
@@ -43,6 +45,10 @@
 import static com.android.server.pm.Installer.DEXOPT_PROFILE_GUIDED;
 import static com.android.server.pm.Installer.DEXOPT_PUBLIC;
 import static com.android.server.pm.Installer.DEXOPT_SAFEMODE;
+import static com.android.server.pm.Installer.DEXOPT_SECONDARY_DEX;
+import static com.android.server.pm.Installer.DEXOPT_FORCE;
+import static com.android.server.pm.Installer.DEXOPT_STORAGE_CE;
+import static com.android.server.pm.Installer.DEXOPT_STORAGE_DE;
 import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
 import static com.android.server.pm.InstructionSets.getDexCodeInstructionSets;
 
@@ -52,13 +58,13 @@
 /**
  * Helper class for running dexopt command on packages.
  */
-class PackageDexOptimizer {
+public class PackageDexOptimizer {
     private static final String TAG = "PackageManager.DexOptimizer";
     static final String OAT_DIR_NAME = "oat";
     // TODO b/19550105 Remove error codes and use exceptions
-    static final int DEX_OPT_SKIPPED = 0;
-    static final int DEX_OPT_PERFORMED = 1;
-    static final int DEX_OPT_FAILED = -1;
+    public static final int DEX_OPT_SKIPPED = 0;
+    public static final int DEX_OPT_PERFORMED = 1;
+    public static final int DEX_OPT_FAILED = -1;
 
     private final Installer mInstaller;
     private final Object mInstallLock;
@@ -100,6 +106,9 @@
             return DEX_OPT_SKIPPED;
         }
         synchronized (mInstallLock) {
+            // During boot the system doesn't need to instantiate and obtain a wake lock.
+            // PowerManager might not be ready, but that doesn't mean that we can't proceed with
+            // dexopt.
             final boolean useLock = mSystemReady;
             if (useLock) {
                 mDexoptWakeLock.setWorkSource(new WorkSource(pkg.applicationInfo.uid));
@@ -130,9 +139,11 @@
         final List<String> paths = pkg.getAllCodePathsExcludingResourceOnly();
         final int sharedGid = UserHandle.getSharedAppGid(pkg.applicationInfo.uid);
 
-        final String compilerFilter = getRealCompilerFilter(pkg, targetCompilerFilter);
+        final String compilerFilter = getRealCompilerFilter(pkg.applicationInfo,
+                targetCompilerFilter, isUsedByOtherApps(pkg));
         final boolean profileUpdated = checkForProfileUpdates &&
                 isProfileUpdated(pkg, sharedGid, compilerFilter);
+
         // TODO(calin,jeffhao): shared library paths should be adjusted to include previous code
         // paths (b/34169257).
         final String sharedLibrariesPath = getSharedLibrariesPath(sharedLibraries);
@@ -201,6 +212,79 @@
     }
 
     /**
+     * Performs dexopt on the secondary dex {@code path} belonging to the app {@code info}.
+     *
+     * @return
+     *      DEX_OPT_FAILED if there was any exception during dexopt
+     *      DEX_OPT_PERFORMED if dexopt was performed successfully on the given path.
+     * NOTE that DEX_OPT_PERFORMED for secondary dex files includes the case when the dex file
+     * didn't need an update. That's because at the moment we don't get more than success/failure
+     * from installd.
+     *
+     * TODO(calin): Consider adding return codes to installd dexopt invocation (rather than
+     * throwing exceptions). Or maybe make a separate call to installd to get DexOptNeeded, though
+     * that seems wasteful.
+     */
+    public int dexOptSecondaryDexPath(ApplicationInfo info, String path, Set<String> isas,
+            String compilerFilter, boolean isUsedByOtherApps) {
+        synchronized (mInstallLock) {
+            // During boot the system doesn't need to instantiate and obtain a wake lock.
+            // PowerManager might not be ready, but that doesn't mean that we can't proceed with
+            // dexopt.
+            final boolean useLock = mSystemReady;
+            if (useLock) {
+                mDexoptWakeLock.setWorkSource(new WorkSource(info.uid));
+                mDexoptWakeLock.acquire();
+            }
+            try {
+                return dexOptSecondaryDexPathLI(info, path, isas, compilerFilter,
+                        isUsedByOtherApps);
+            } finally {
+                if (useLock) {
+                    mDexoptWakeLock.release();
+                }
+            }
+        }
+    }
+
+    @GuardedBy("mInstallLock")
+    private int dexOptSecondaryDexPathLI(ApplicationInfo info, String path, Set<String> isas,
+            String compilerFilter, boolean isUsedByOtherApps) {
+        int dexoptFlags = getDexFlags(info, compilerFilter) | DEXOPT_SECONDARY_DEX;
+        // Check the app storage and add the appropriate flags.
+        if (info.dataDir.equals(info.deviceProtectedDataDir)) {
+            dexoptFlags |= DEXOPT_STORAGE_DE;
+        } else if (info.dataDir.equals(info.credentialProtectedDataDir)) {
+            dexoptFlags |= DEXOPT_STORAGE_CE;
+        } else {
+            Slog.e(TAG, "Could not infer CE/DE storage for package " + info.packageName);
+            return DEX_OPT_FAILED;
+        }
+        compilerFilter = getRealCompilerFilter(info, compilerFilter, isUsedByOtherApps);
+        Log.d(TAG, "Running dexopt on: " + path
+                + " pkg=" + info.packageName + " isa=" + isas
+                + " dexoptFlags=" + printDexoptFlags(dexoptFlags)
+                + " target-filter=" + compilerFilter);
+
+        try {
+            for (String isa : isas) {
+                // Reuse the same dexopt path as for the primary apks. We don't need all the
+                // arguments as some (dexopNeeded and oatDir) will be computed by installd because
+                // system server cannot read untrusted app content.
+                // TODO(calin): maybe add a separate call.
+                mInstaller.dexopt(path, info.uid, info.packageName, isa, /*dexoptNeeded*/ 0,
+                        /*oatDir*/ null, dexoptFlags,
+                        compilerFilter, info.volumeUuid, /*sharedLibrariesPath*/ null);
+            }
+
+            return DEX_OPT_PERFORMED;
+        } catch (InstallerException e) {
+            Slog.w(TAG, "Failed to dexopt", e);
+            return DEX_OPT_FAILED;
+        }
+    }
+
+    /**
      * Adjust the given dexopt-needed value. Can be overridden to influence the decision to
      * optimize or not (and in what way).
      */
@@ -246,8 +330,9 @@
      * The target filter will be updated if the package code is used by other apps
      * or if it has the safe mode flag set.
      */
-    private String getRealCompilerFilter(PackageParser.Package pkg, String targetCompilerFilter) {
-        int flags = pkg.applicationInfo.flags;
+    private String getRealCompilerFilter(ApplicationInfo info, String targetCompilerFilter,
+            boolean isUsedByOtherApps) {
+        int flags = info.flags;
         boolean vmSafeMode = (flags & ApplicationInfo.FLAG_VM_SAFE_MODE) != 0;
         if (vmSafeMode) {
             // For the compilation, it doesn't really matter what we return here because installd
@@ -259,7 +344,7 @@
             return getNonProfileGuidedCompilerFilter(targetCompilerFilter);
         }
 
-        if (isProfileGuidedCompilerFilter(targetCompilerFilter) && isUsedByOtherApps(pkg)) {
+        if (isProfileGuidedCompilerFilter(targetCompilerFilter) && isUsedByOtherApps) {
             // If the dex files is used by other apps, we cannot use profile-guided compilation.
             return getNonProfileGuidedCompilerFilter(targetCompilerFilter);
         }
@@ -272,12 +357,16 @@
      * filter.
      */
     private int getDexFlags(PackageParser.Package pkg, String compilerFilter) {
-        int flags = pkg.applicationInfo.flags;
+        return getDexFlags(pkg.applicationInfo, compilerFilter);
+    }
+
+    private int getDexFlags(ApplicationInfo info, String compilerFilter) {
+        int flags = info.flags;
         boolean vmSafeMode = (flags & ApplicationInfo.FLAG_VM_SAFE_MODE) != 0;
         boolean debuggable = (flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
         // Profile guide compiled oat files should not be public.
         boolean isProfileGuidedFilter = isProfileGuidedCompilerFilter(compilerFilter);
-        boolean isPublic = !pkg.isForwardLocked() && !isProfileGuidedFilter;
+        boolean isPublic = !info.isForwardLocked() && !isProfileGuidedFilter;
         int profileFlag = isProfileGuidedFilter ? DEXOPT_PROFILE_GUIDED : 0;
         int dexFlags =
                 (isPublic ? DEXOPT_PUBLIC : 0)
@@ -437,6 +526,19 @@
         if ((flags & DEXOPT_SAFEMODE) == DEXOPT_SAFEMODE) {
             flagsList.add("safemode");
         }
+        if ((flags & DEXOPT_SECONDARY_DEX) == DEXOPT_SECONDARY_DEX) {
+            flagsList.add("secondary");
+        }
+        if ((flags & DEXOPT_FORCE) == DEXOPT_FORCE) {
+            flagsList.add("force");
+        }
+        if ((flags & DEXOPT_STORAGE_CE) == DEXOPT_STORAGE_CE) {
+            flagsList.add("storage_ce");
+        }
+        if ((flags & DEXOPT_STORAGE_DE) == DEXOPT_STORAGE_DE) {
+            flagsList.add("storage_de");
+        }
+
         return String.join(",", flagsList);
     }
 
@@ -461,5 +563,12 @@
             // TODO: The return value is wrong when patchoat is needed.
             return DexFile.DEX2OAT_FROM_SCRATCH;
         }
+
+        @Override
+        protected int adjustDexoptFlags(int flags) {
+            // Add DEXOPT_FORCE flag to signal installd that it should force compilation
+            // and discard dexoptanalyzer result.
+            return flags | DEXOPT_FORCE;
+        }
     }
 }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index b0512d4..4a426bd 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -2227,7 +2227,7 @@
         mInstaller = installer;
         mPackageDexOptimizer = new PackageDexOptimizer(installer, mInstallLock, context,
                 "*dexopt*");
-        mDexManager = new DexManager();
+        mDexManager = new DexManager(this, mPackageDexOptimizer, installer, mInstallLock);
         mMoveCallbacks = new MoveCallbacks(FgThread.get().getLooper());
 
         mOnPermissionChangeListeners = new OnPermissionChangeListeners(
@@ -8226,6 +8226,39 @@
                 targetCompilerFilter, getOrCreateCompilerPackageStats(p));
     }
 
+    // Performs dexopt on the used secondary dex files belonging to the given package.
+    // Returns true if all dex files were process successfully (which could mean either dexopt or
+    // skip). Returns false if any of the files caused errors.
+    @Override
+    public boolean performDexOptSecondary(String packageName, String compilerFilter,
+            boolean force) {
+        return mDexManager.dexoptSecondaryDex(packageName, compilerFilter, force);
+    }
+
+    /**
+     * Reconcile the information we have about the secondary dex files belonging to
+     * {@code packagName} and the actual dex files. For all dex files that were
+     * deleted, update the internal records and delete the generated oat files.
+     */
+    @Override
+    public void reconcileSecondaryDexFiles(String packageName) {
+        mDexManager.reconcileSecondaryDexFiles(packageName);
+    }
+
+    // TODO(calin): this is only needed for BackgroundDexOptService. Find a cleaner way to inject
+    // a reference there.
+    /*package*/ DexManager getDexManager() {
+        return mDexManager;
+    }
+
+    /**
+     * Execute the background dexopt job immediately.
+     */
+    @Override
+    public boolean runBackgroundDexoptJob() {
+        return BackgroundDexOptService.runIdleOptimizationsNow(this, mContext);
+    }
+
     List<PackageParser.Package> findSharedNonSystemLibraries(PackageParser.Package p) {
         if (p.usesLibraries != null || p.usesOptionalLibraries != null
                 || p.usesStaticLibraries != null) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceCompilerMapping.java b/services/core/java/com/android/server/pm/PackageManagerServiceCompilerMapping.java
index 8a3f48e..9c9a671 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceCompilerMapping.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceCompilerMapping.java
@@ -23,7 +23,7 @@
 /**
  * Manage (retrieve) mappings from compilation reason to compilation filter.
  */
-class PackageManagerServiceCompilerMapping {
+public class PackageManagerServiceCompilerMapping {
     // Names for compilation reasons.
     static final String REASON_STRINGS[] = {
             "first-boot", "boot", "install", "bg-dexopt", "ab-ota", "nsys-library", "shared-apk",
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 2f8d749..1203e4d 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -118,6 +118,10 @@
                     return runInstallWrite();
                 case "compile":
                     return runCompile();
+                case "reconcile-secondary-dex-files":
+                    return runreconcileSecondaryDexFiles();
+                case "bg-dexopt-job":
+                    return runDexoptJob();
                 case "dump-profiles":
                     return runDumpProfiles();
                 case "list":
@@ -306,6 +310,7 @@
         String compilerFilter = null;
         String compilationReason = null;
         String checkProfilesRaw = null;
+        boolean secondaryDex = false;
 
         String opt;
         while ((opt = getNextOption()) != null) {
@@ -333,6 +338,9 @@
                     clearProfileData = true;
                     compilationReason = "install";
                     break;
+                case "--secondary-dex":
+                    secondaryDex = true;
+                    break;
                 default:
                     pw.println("Error: Unknown option: " + opt);
                     return 1;
@@ -405,8 +413,11 @@
                 mInterface.clearApplicationProfileData(packageName);
             }
 
-            boolean result = mInterface.performDexOptMode(packageName,
-                    checkProfiles, targetCompilerFilter, forceCompilation);
+            boolean result = secondaryDex
+                    ? mInterface.performDexOptSecondary(packageName,
+                            targetCompilerFilter, forceCompilation)
+                    : mInterface.performDexOptMode(packageName,
+                            checkProfiles, targetCompilerFilter, forceCompilation);
             if (!result) {
                 failedPackages.add(packageName);
             }
@@ -434,6 +445,17 @@
         }
     }
 
+    private int runreconcileSecondaryDexFiles() throws RemoteException {
+        String packageName = getNextArg();
+        mInterface.reconcileSecondaryDexFiles(packageName);
+        return 0;
+    }
+
+    private int runDexoptJob() throws RemoteException {
+        boolean result = mInterface.runBackgroundDexoptJob();
+        return result ? 0 : -1;
+    }
+
     private int runDumpProfiles() throws RemoteException {
         String packageName = getNextArg();
         mInterface.dumpProfiles(packageName);
@@ -1515,6 +1537,13 @@
         }
         pw.println("      --reset: restore package to its post-install state");
         pw.println("      --check-prof (true | false): look at profiles when doing dexopt?");
+        pw.println("      --secondary-dex: compile app secondary dex files");
+        pw.println("  bg-dexopt-job");
+        pw.println("    Execute the background optimizations immediately.");
+        pw.println("    Note that the command only runs the background optimizer logic. It may");
+        pw.println("    overlap with the actual job but the job scheduler will not be able to");
+        pw.println("    cancel it. It will also run even if the device is not in the idle");
+        pw.println("    maintenance mode.");
         pw.println("  list features");
         pw.println("    Prints all features of the system.");
         pw.println("  list instrumentation [-f] [TARGET-PACKAGE]");
@@ -1539,6 +1568,8 @@
         pw.println("      -u: also include uninstalled packages");
         pw.println("      --uid UID: filter to only show packages with the given UID");
         pw.println("      --user USER_ID: only list packages belonging to the given user");
+        pw.println("  reconcile-secondary-dex-files TARGET-PACKAGE");
+        pw.println("    Reconciles the package secondary dex files with the generated oat files.");
         pw.println("  list permission-groups");
         pw.println("    Prints all known permission groups.");
         pw.println("  list permissions [-g] [-f] [-d] [-u] [GROUP]");
diff --git a/services/core/java/com/android/server/pm/dex/DexManager.java b/services/core/java/com/android/server/pm/dex/DexManager.java
index e809213..00f3711 100644
--- a/services/core/java/com/android/server/pm/dex/DexManager.java
+++ b/services/core/java/com/android/server/pm/dex/DexManager.java
@@ -16,13 +16,21 @@
 
 package com.android.server.pm.dex;
 
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageParser;
-import android.content.pm.ApplicationInfo;
+import android.os.RemoteException;
+import android.os.storage.StorageManager;
 
 import android.util.Slog;
 
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.pm.Installer;
+import com.android.server.pm.Installer.InstallerException;
+import com.android.server.pm.PackageDexOptimizer;
 import com.android.server.pm.PackageManagerServiceUtils;
+import com.android.server.pm.PackageManagerServiceCompilerMapping;
 
 import java.io.File;
 import java.io.IOException;
@@ -32,6 +40,9 @@
 import java.util.Map;
 import java.util.Set;
 
+import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo;
+import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
+
 /**
  * This class keeps track of how dex files are used.
  * Every time it gets a notification about a dex file being loaded it tracks
@@ -54,15 +65,26 @@
     // encode and save the dex usage data.
     private final PackageDexUsage mPackageDexUsage;
 
+    private final IPackageManager mPackageManager;
+    private final PackageDexOptimizer mPackageDexOptimizer;
+    private final Object mInstallLock;
+    @GuardedBy("mInstallLock")
+    private final Installer mInstaller;
+
     // Possible outcomes of a dex search.
     private static int DEX_SEARCH_NOT_FOUND = 0;  // dex file not found
     private static int DEX_SEARCH_FOUND_PRIMARY = 1;  // dex file is the primary/base apk
     private static int DEX_SEARCH_FOUND_SPLIT = 2;  // dex file is a split apk
     private static int DEX_SEARCH_FOUND_SECONDARY = 3;  // dex file is a secondary dex
 
-    public DexManager() {
+    public DexManager(IPackageManager pms, PackageDexOptimizer pdo,
+            Installer installer, Object installLock) {
       mPackageCodeLocationsCache = new HashMap<>();
       mPackageDexUsage = new PackageDexUsage();
+      mPackageManager = pms;
+      mPackageDexOptimizer = pdo;
+      mInstaller = installer;
+      mInstallLock = installLock;
     }
 
     /**
@@ -199,11 +221,144 @@
      * Get the package dex usage for the given package name.
      * @return the package data or null if there is no data available for this package.
      */
-    public PackageDexUsage.PackageUseInfo getPackageUseInfo(String packageName) {
+    public PackageUseInfo getPackageUseInfo(String packageName) {
         return mPackageDexUsage.getPackageUseInfo(packageName);
     }
 
     /**
+     * Perform dexopt on the package {@code packageName} secondary dex files.
+     * @return true if all secondary dex files were processed successfully (compiled or skipped
+     *         because they don't need to be compiled)..
+     */
+    public boolean dexoptSecondaryDex(String packageName, String compilerFilter, boolean force) {
+        // Select the dex optimizer based on the force parameter.
+        // Forced compilation is done through ForcedUpdatePackageDexOptimizer which will adjust
+        // the necessary dexopt flags to make sure that compilation is not skipped. This avoid
+        // passing the force flag through the multitude of layers.
+        // Note: The force option is rarely used (cmdline input for testing, mostly), so it's OK to
+        //       allocate an object here.
+        PackageDexOptimizer pdo = force
+                ? new PackageDexOptimizer.ForcedUpdatePackageDexOptimizer(mPackageDexOptimizer)
+                : mPackageDexOptimizer;
+        PackageUseInfo useInfo = getPackageUseInfo(packageName);
+        if (useInfo == null || useInfo.getDexUseInfoMap().isEmpty()) {
+            if (DEBUG) {
+                Slog.d(TAG, "No secondary dex use for package:" + packageName);
+            }
+            // Nothing to compile, return true.
+            return true;
+        }
+        boolean success = true;
+        for (Map.Entry<String, DexUseInfo> entry : useInfo.getDexUseInfoMap().entrySet()) {
+            String dexPath = entry.getKey();
+            DexUseInfo dexUseInfo = entry.getValue();
+            PackageInfo pkg = null;
+            try {
+                pkg = mPackageManager.getPackageInfo(packageName, /*flags*/0,
+                    dexUseInfo.getOwnerUserId());
+            } catch (RemoteException e) {
+                throw new AssertionError(e);
+            }
+            // It may be that the package gets uninstalled while we try to compile its
+            // secondary dex files. If that's the case, just ignore.
+            // Note that we don't break the entire loop because the package might still be
+            // installed for other users.
+            if (pkg == null) {
+                Slog.d(TAG, "Could not find package when compiling secondary dex " + packageName
+                        + " for user " + dexUseInfo.getOwnerUserId());
+                mPackageDexUsage.removeUserPackage(packageName, dexUseInfo.getOwnerUserId());
+                continue;
+            }
+            int result = pdo.dexOptSecondaryDexPath(pkg.applicationInfo, dexPath,
+                    dexUseInfo.getLoaderIsas(), compilerFilter, dexUseInfo.isUsedByOtherApps());
+            success = success && (result != PackageDexOptimizer.DEX_OPT_FAILED);
+        }
+        return success;
+    }
+
+    /**
+     * Reconcile the information we have about the secondary dex files belonging to
+     * {@code packagName} and the actual dex files. For all dex files that were
+     * deleted, update the internal records and delete any generated oat files.
+     */
+    public void reconcileSecondaryDexFiles(String packageName) {
+        PackageUseInfo useInfo = getPackageUseInfo(packageName);
+        if (useInfo == null || useInfo.getDexUseInfoMap().isEmpty()) {
+            if (DEBUG) {
+                Slog.d(TAG, "No secondary dex use for package:" + packageName);
+            }
+            // Nothing to reconcile.
+            return;
+        }
+        Set<String> dexFilesToRemove = new HashSet<>();
+        boolean updated = false;
+        for (Map.Entry<String, DexUseInfo> entry : useInfo.getDexUseInfoMap().entrySet()) {
+            String dexPath = entry.getKey();
+            DexUseInfo dexUseInfo = entry.getValue();
+            PackageInfo pkg = null;
+            try {
+                // Note that we look for the package in the PackageManager just to be able
+                // to get back the real app uid and its storage kind. These are only used
+                // to perform extra validation in installd.
+                // TODO(calin): maybe a bit overkill.
+                pkg = mPackageManager.getPackageInfo(packageName, /*flags*/0,
+                    dexUseInfo.getOwnerUserId());
+            } catch (RemoteException ignore) {
+                // Can't happen, DexManager is local.
+            }
+            if (pkg == null) {
+                // It may be that the package was uninstalled while we process the secondary
+                // dex files.
+                Slog.d(TAG, "Could not find package when compiling secondary dex " + packageName
+                        + " for user " + dexUseInfo.getOwnerUserId());
+                // Update the usage and continue, another user might still have the package.
+                updated = mPackageDexUsage.removeUserPackage(
+                        packageName, dexUseInfo.getOwnerUserId()) || updated;
+                continue;
+            }
+            ApplicationInfo info = pkg.applicationInfo;
+            int flags = 0;
+            if (info.dataDir.equals(info.deviceProtectedDataDir)) {
+                flags |= StorageManager.FLAG_STORAGE_DE;
+            } else if (info.dataDir.equals(info.credentialProtectedDataDir)) {
+                flags |= StorageManager.FLAG_STORAGE_CE;
+            } else {
+                Slog.e(TAG, "Could not infer CE/DE storage for package " + info.packageName);
+                updated = mPackageDexUsage.removeUserPackage(
+                        packageName, dexUseInfo.getOwnerUserId()) || updated;
+                continue;
+            }
+
+            boolean dexStillExists = true;
+            synchronized(mInstallLock) {
+                try {
+                    String[] isas = dexUseInfo.getLoaderIsas().toArray(new String[0]);
+                    dexStillExists = mInstaller.reconcileSecondaryDexFile(dexPath, packageName,
+                            pkg.applicationInfo.uid, isas, pkg.applicationInfo.volumeUuid, flags);
+                } catch (InstallerException e) {
+                    Slog.e(TAG, "Got InstallerException when reconciling dex " + dexPath +
+                            " : " + e.getMessage());
+                }
+            }
+            if (!dexStillExists) {
+                updated = mPackageDexUsage.removeDexFile(
+                        packageName, dexPath, dexUseInfo.getOwnerUserId()) || updated;
+            }
+
+        }
+        if (updated) {
+            mPackageDexUsage.maybeWriteAsync();
+        }
+    }
+
+    /**
+     * Return all packages that contain records of secondary dex files.
+     */
+    public Set<String> getAllPackagesWithSecondaryDexFiles() {
+        return mPackageDexUsage.getAllPackagesWithSecondaryDexFiles();
+    }
+
+    /**
      * Retrieves the package which owns the given dexPath.
      */
     private DexSearchResult getDexPackage(
diff --git a/services/core/java/com/android/server/pm/dex/PackageDexUsage.java b/services/core/java/com/android/server/pm/dex/PackageDexUsage.java
index 10384a2..3693bce0 100644
--- a/services/core/java/com/android/server/pm/dex/PackageDexUsage.java
+++ b/services/core/java/com/android/server/pm/dex/PackageDexUsage.java
@@ -376,12 +376,86 @@
         }
     }
 
+    /**
+     * Remove all the records about package {@code packageName} belonging to user {@code userId}.
+     * @return true if the record was found and actually deleted,
+     *         false if the record doesn't exist
+     */
+    public boolean removeUserPackage(String packageName, int userId) {
+        synchronized (mPackageUseInfoMap) {
+            PackageUseInfo packageUseInfo = mPackageUseInfoMap.get(packageName);
+            if (packageUseInfo == null) {
+                return false;
+            }
+            boolean updated = false;
+            Iterator<Map.Entry<String, DexUseInfo>> dIt =
+                            packageUseInfo.mDexUseInfoMap.entrySet().iterator();
+            while (dIt.hasNext()) {
+                DexUseInfo dexUseInfo = dIt.next().getValue();
+                if (dexUseInfo.mOwnerUserId == userId) {
+                    dIt.remove();
+                    updated = true;
+                }
+            }
+            return updated;
+        }
+    }
+
+    /**
+     * Remove the secondary dex file record belonging to the package {@code packageName}
+     * and user {@code userId}.
+     * @return true if the record was found and actually deleted,
+     *         false if the record doesn't exist
+     */
+    public boolean removeDexFile(String packageName, String dexFile, int userId) {
+        synchronized (mPackageUseInfoMap) {
+            PackageUseInfo packageUseInfo = mPackageUseInfoMap.get(packageName);
+            if (packageUseInfo == null) {
+                return false;
+            }
+            return removeDexFile(packageUseInfo, dexFile, userId);
+        }
+    }
+
+    private boolean removeDexFile(PackageUseInfo packageUseInfo, String dexFile, int userId) {
+        DexUseInfo dexUseInfo = packageUseInfo.mDexUseInfoMap.get(dexFile);
+        if (dexUseInfo == null) {
+            return false;
+        }
+        if (dexUseInfo.mOwnerUserId == userId) {
+            packageUseInfo.mDexUseInfoMap.remove(dexFile);
+            return true;
+        }
+        return false;
+    }
+
     public PackageUseInfo getPackageUseInfo(String packageName) {
         synchronized (mPackageUseInfoMap) {
-            return mPackageUseInfoMap.get(packageName);
+            PackageUseInfo useInfo = mPackageUseInfoMap.get(packageName);
+            // The useInfo contains a map for secondary dex files which could be modified
+            // concurrently after this method returns and thus outside the locking we do here.
+            // (i.e. the map is updated when new class loaders are created, which can happen anytime
+            // after this method returns)
+            // Make a defensive copy to be sure we don't get concurrent modifications.
+            return useInfo == null ? null : new PackageUseInfo(useInfo);
         }
     }
 
+    /**
+     * Return all packages that contain records of secondary dex files.
+     */
+    public Set<String> getAllPackagesWithSecondaryDexFiles() {
+        Set<String> packages = new HashSet<>();
+        synchronized (mPackageUseInfoMap) {
+            for (Map.Entry<String, PackageUseInfo> entry : mPackageUseInfoMap.entrySet()) {
+                if (!entry.getValue().mDexUseInfoMap.isEmpty()) {
+                    packages.add(entry.getKey());
+                }
+            }
+        }
+        return packages;
+    }
+
     public void clear() {
         synchronized (mPackageUseInfoMap) {
             mPackageUseInfoMap.clear();
diff --git a/services/core/java/com/android/server/text/TextClassificationService.java b/services/core/java/com/android/server/text/TextClassificationService.java
new file mode 100644
index 0000000..9358238
--- /dev/null
+++ b/services/core/java/com/android/server/text/TextClassificationService.java
@@ -0,0 +1,71 @@
+/*
+ * 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.text;
+
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.text.ITextClassificationService;
+import android.util.Slog;
+
+import com.android.server.SystemService;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+/**
+ * Text classification service.
+ * This is used to provide access to the text classification LSTM model file.
+ */
+public class TextClassificationService extends ITextClassificationService.Stub {
+
+    private static final String LOG_TAG = "TextClassificationService";
+
+    public static final class Lifecycle extends SystemService {
+
+        private TextClassificationService mService;
+
+        public Lifecycle(Context context) {
+            super(context);
+            mService = new TextClassificationService();
+        }
+
+        @Override
+        public void onStart() {
+            try {
+                publishBinderService(Context.TEXT_CLASSIFICATION_SERVICE, mService);
+            } catch (Throwable t) {
+                // Starting this service is not critical to the running of this device and should
+                // therefore not crash the device. If it fails, log the error and continue.
+                Slog.e(LOG_TAG, "Could not start the TextClassificationService.", t);
+            }
+        }
+    }
+
+    @Override
+    public synchronized ParcelFileDescriptor getModelFileFd() throws RemoteException {
+        try {
+            return ParcelFileDescriptor.open(
+                    new File("/etc/assistant/smart-selection.model"),
+                    ParcelFileDescriptor.MODE_READ_ONLY);
+        } catch (Throwable t) {
+            Slog.e(LOG_TAG, "Error retrieving an fd to the text classification model file.", t);
+            throw new RemoteException(t.getMessage());
+        }
+    }
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index c3ef23b..712441c 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -100,6 +100,7 @@
 import com.android.server.statusbar.StatusBarManagerService;
 import com.android.server.storage.DeviceStorageMonitorService;
 import com.android.server.telecom.TelecomLoaderService;
+import com.android.server.text.TextClassificationService;
 import com.android.server.trust.TrustManagerService;
 import com.android.server.tv.TvInputManagerService;
 import com.android.server.tv.TvRemoteService;
@@ -951,6 +952,12 @@
                 traceEnd();
             }
 
+            if (!disableNonCoreServices) {
+                traceBeginAndSlog("StartTextClassificationService");
+                mSystemServiceManager.startService(TextClassificationService.Lifecycle.class);
+                traceEnd();
+            }
+
             if (!disableNetwork) {
                 traceBeginAndSlog("StartNetworkScoreService");
                 try {
diff --git a/services/tests/notification/src/com/android/server/notification/BuzzBeepBlinkTest.java b/services/tests/notification/src/com/android/server/notification/BuzzBeepBlinkTest.java
index 468a26b..b8655607 100644
--- a/services/tests/notification/src/com/android/server/notification/BuzzBeepBlinkTest.java
+++ b/services/tests/notification/src/com/android/server/notification/BuzzBeepBlinkTest.java
@@ -85,6 +85,10 @@
             300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400,
             300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400 };
     private static final Uri CUSTOM_SOUND = Settings.System.DEFAULT_ALARM_ALERT_URI;
+    private static final AudioAttributes CUSTOM_ATTRIBUTES = new AudioAttributes.Builder()
+            .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
+            .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+            .build();
     private static final int CUSTOM_LIGHT_COLOR = Color.BLACK;
     private static final int CUSTOM_LIGHT_ON = 10000;
     private static final int CUSTOM_LIGHT_OFF = 10000;
@@ -200,10 +204,11 @@
         if (noisy) {
             if (defaultSound) {
                 defaults |= Notification.DEFAULT_SOUND;
-                channel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
+                channel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI,
+                        Notification.AUDIO_ATTRIBUTES_DEFAULT);
             } else {
                 builder.setSound(CUSTOM_SOUND);
-                channel.setSound(CUSTOM_SOUND);
+                channel.setSound(CUSTOM_SOUND, CUSTOM_ATTRIBUTES);
             }
         }
         if (buzzy) {
@@ -521,6 +526,8 @@
 
         verify(mVibrator, times(1)).vibrate(anyInt(), anyString(), eq(FALLBACK_VIBRATION),
                 eq(-1), (AudioAttributes) anyObject());
+        verify(mRingtonePlayer, never()).playAsync
+                (anyObject(), anyObject(), anyBoolean(), anyObject());
     }
 
     @Test
diff --git a/services/tests/notification/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/notification/src/com/android/server/notification/NotificationRecordTest.java
index 15dcc26..2ab1f30 100644
--- a/services/tests/notification/src/com/android/server/notification/NotificationRecordTest.java
+++ b/services/tests/notification/src/com/android/server/notification/NotificationRecordTest.java
@@ -118,6 +118,7 @@
                 defaults |= Notification.DEFAULT_SOUND;
             } else {
                 builder.setSound(CUSTOM_SOUND, CUSTOM_ATTRIBUTES);
+                channel.setSound(CUSTOM_SOUND, CUSTOM_ATTRIBUTES);
             }
         }
         if (buzzy) {
@@ -150,29 +151,31 @@
 
     @Test
     public void testSound_default_preUpgradeUsesNotification() throws Exception {
-        defaultChannel.setSound(null);
+        defaultChannel.setSound(null, null);
         // pre upgrade, default sound.
         StatusBarNotification sbn = getNotification(true /*preO */, true /* noisy */,
                 true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */);
 
         NotificationRecord record = new NotificationRecord(mMockContext, sbn, defaultChannel);
         assertEquals(Settings.System.DEFAULT_NOTIFICATION_URI, record.getSound());
+        assertEquals(Notification.AUDIO_ATTRIBUTES_DEFAULT, record.getAudioAttributes());
     }
 
     @Test
     public void testSound_custom_preUpgradeUsesNotification() throws Exception {
-        defaultChannel.setSound(null);
+        defaultChannel.setSound(null, null);
         // pre upgrade, custom sound.
         StatusBarNotification sbn = getNotification(true /*preO */, true /* noisy */,
                 false /* defaultSound */, false /* buzzy */, false /* defaultBuzz */);
 
         NotificationRecord record = new NotificationRecord(mMockContext, sbn, defaultChannel);
         assertEquals(CUSTOM_SOUND, record.getSound());
+        assertEquals(CUSTOM_ATTRIBUTES, record.getAudioAttributes());
     }
 
     @Test
     public void testSound_default_userLocked_preUpgrade() throws Exception {
-        defaultChannel.setSound(CUSTOM_SOUND);
+        defaultChannel.setSound(CUSTOM_SOUND, CUSTOM_ATTRIBUTES);
         defaultChannel.lockFields(NotificationChannel.USER_LOCKED_SOUND);
         // pre upgrade, default sound.
         StatusBarNotification sbn = getNotification(true /*preO */, true /* noisy */,
@@ -180,17 +183,19 @@
 
         NotificationRecord record = new NotificationRecord(mMockContext, sbn, defaultChannel);
         assertEquals(CUSTOM_SOUND, record.getSound());
+        assertEquals(CUSTOM_ATTRIBUTES, record.getAudioAttributes());
     }
 
     @Test
     public void testSound_default_upgradeUsesChannel() throws Exception {
-        channel.setSound(CUSTOM_SOUND);
+        channel.setSound(CUSTOM_SOUND, CUSTOM_ATTRIBUTES);
         // post upgrade, default sound.
         StatusBarNotification sbn = getNotification(false /*preO */, true /* noisy */,
                 true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */);
 
         NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
         assertEquals(CUSTOM_SOUND, record.getSound());
+        assertEquals(CUSTOM_ATTRIBUTES, record.getAudioAttributes());
     }
 
     @Test
@@ -239,28 +244,6 @@
     }
 
     @Test
-    public void testAudioAttributes_preUpgrade() throws Exception {
-        defaultChannel.setSound(null);
-        // pre upgrade, default sound.
-        StatusBarNotification sbn = getNotification(true /*preO */, true /* noisy */,
-                false /* defaultSound */, false /* buzzy */, false /* defaultBuzz */);
-
-        NotificationRecord record = new NotificationRecord(mMockContext, sbn, defaultChannel);
-        assertEquals(CUSTOM_ATTRIBUTES, record.getAudioAttributes());
-    }
-
-    @Test
-    public void testAudioAttributes_upgrade() throws Exception {
-        channel.setSound(null);
-        // post upgrade, default sound.
-        StatusBarNotification sbn = getNotification(true /*preO */, true /* noisy */,
-                false /* defaultSound */, false /* buzzy */, false /* defaultBuzz */);
-
-        NotificationRecord record = new NotificationRecord(mMockContext, sbn, defaultChannel);
-        assertEquals(CUSTOM_ATTRIBUTES, record.getAudioAttributes());
-    }
-
-    @Test
     public void testImportance_preUpgrade() throws Exception {
         StatusBarNotification sbn = getNotification(true /*preO */, true /* noisy */,
                 true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */);
diff --git a/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java b/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java
index b53ec45..9fa46d1 100644
--- a/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java
+++ b/services/tests/notification/src/com/android/server/notification/RankingHelperTest.java
@@ -36,6 +36,7 @@
 import android.app.NotificationManager;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.media.AudioAttributes;
 import android.net.Uri;
 import android.os.Build;
 import android.os.UserHandle;
@@ -66,9 +67,12 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class RankingHelperTest {
-    @Mock NotificationUsageStats mUsageStats;
-    @Mock RankingHandler handler;
-    @Mock PackageManager mPm;
+    @Mock
+    NotificationUsageStats mUsageStats;
+    @Mock
+    RankingHandler handler;
+    @Mock
+    PackageManager mPm;
 
     private Notification mNotiGroupGSortA;
     private Notification mNotiGroupGSortB;
@@ -85,6 +89,7 @@
     private final int uid = 0;
     private final String pkg2 = "pkg2";
     private final int uid2 = 1111111;
+    private AudioAttributes mAudioAttributes;
 
     private Context getContext() {
         return InstrumentationRegistry.getTargetContext();
@@ -96,7 +101,7 @@
         UserHandle user = UserHandle.ALL;
 
         mHelper = new RankingHelper(getContext(), mPm, handler, mUsageStats,
-                new String[] {ImportanceExtractor.class.getName()});
+                new String[]{ImportanceExtractor.class.getName()});
 
         mNotiGroupGSortA = new Notification.Builder(getContext())
                 .setContentTitle("A")
@@ -143,6 +148,12 @@
                 "package", "package", 1, null, 0, 0, mNotiNoGroupSortA, user,
                 null, System.currentTimeMillis()), getDefaultChannel());
 
+        mAudioAttributes = new AudioAttributes.Builder()
+                .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
+                .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+                .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+                .build();
+
         final ApplicationInfo legacy = new ApplicationInfo();
         legacy.targetSdkVersion = Build.VERSION_CODES.N_MR1;
         final ApplicationInfo upgrade = new ApplicationInfo();
@@ -150,7 +161,8 @@
         try {
             when(mPm.getApplicationInfoAsUser(eq(pkg), anyInt(), anyInt())).thenReturn(legacy);
             when(mPm.getApplicationInfoAsUser(eq(pkg2), anyInt(), anyInt())).thenReturn(upgrade);
-        } catch (PackageManager.NameNotFoundException e) {}
+        } catch (PackageManager.NameNotFoundException e) {
+        }
     }
 
     private NotificationChannel getDefaultChannel() {
@@ -187,6 +199,7 @@
         assertEquals(expected.canBypassDnd(), actual.canBypassDnd());
         assertTrue(Arrays.equals(expected.getVibrationPattern(), actual.getVibrationPattern()));
         assertEquals(expected.getGroup(), actual.getGroup());
+        assertEquals(expected.getAudioAttributes(), actual.getAudioAttributes());
     }
 
     @Test
@@ -246,13 +259,13 @@
                 new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH);
         NotificationChannel channel2 =
                 new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
-        channel2.setSound(new Uri.Builder().scheme("test").build());
+        channel2.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes);
         channel2.setLights(true);
         channel2.setBypassDnd(true);
         channel2.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
         channel2.enableVibration(true);
         channel2.setGroup(ncg.getId());
-        channel2.setVibrationPattern(new long[] {100, 67, 145, 156});
+        channel2.setVibrationPattern(new long[]{100, 67, 145, 156});
 
         mHelper.createNotificationChannelGroup(pkg, uid, ncg, true);
         mHelper.createNotificationChannel(pkg, uid, channel1, true);
@@ -304,13 +317,13 @@
                 pkg, uid, NotificationChannel.DEFAULT_CHANNEL_ID, false);
         assertEquals(NotificationManager.IMPORTANCE_UNSPECIFIED, updated.getImportance());
         assertFalse(updated.canBypassDnd());
-        assertEquals(NotificationManager.VISIBILITY_NO_OVERRIDE,updated.getLockscreenVisibility());
+        assertEquals(NotificationManager.VISIBILITY_NO_OVERRIDE, updated.getLockscreenVisibility());
         assertEquals(0, updated.getUserLockedFields());
     }
 
     @Test
     public void testChannelXml_defaultChannelUpdatedApp_userSettings() throws Exception {
-         NotificationChannel channel1 =
+        NotificationChannel channel1 =
                 new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_MIN);
         mHelper.createNotificationChannel(pkg, uid, channel1, true);
 
@@ -335,15 +348,16 @@
     @Test
     public void testChannelXml_upgradeCreateDefaultChannel() throws Exception {
         final String preupgradeXml = "<ranking version=\"1\">\n"
-             + "<package name=\"" + pkg + "\" importance=\"" + NotificationManager.IMPORTANCE_HIGH
-            + "\" priority=\"" + Notification.PRIORITY_MAX + "\" visibility=\""
-            + Notification.VISIBILITY_SECRET + "\"" +" uid=\"" + uid + "\" />\n"
-            + "<package name=\"" + pkg2 + "\" uid=\"" + uid2 + "\" visibility=\""
-            + Notification.VISIBILITY_PRIVATE + "\" />\n"
-            + "</ranking>";
+                + "<package name=\"" + pkg + "\" importance=\""
+                + NotificationManager.IMPORTANCE_HIGH
+                + "\" priority=\"" + Notification.PRIORITY_MAX + "\" visibility=\""
+                + Notification.VISIBILITY_SECRET + "\"" + " uid=\"" + uid + "\" />\n"
+                + "<package name=\"" + pkg2 + "\" uid=\"" + uid2 + "\" visibility=\""
+                + Notification.VISIBILITY_PRIVATE + "\" />\n"
+                + "</ranking>";
         XmlPullParser parser = Xml.newPullParser();
         parser.setInput(new BufferedInputStream(new ByteArrayInputStream(preupgradeXml.getBytes())),
-            null);
+                null);
         parser.nextTag();
         mHelper.readXml(parser, false);
 
@@ -353,8 +367,8 @@
         assertTrue(updated1.canBypassDnd());
         assertEquals(Notification.VISIBILITY_SECRET, updated1.getLockscreenVisibility());
         assertEquals(NotificationChannel.USER_LOCKED_IMPORTANCE
-            | NotificationChannel.USER_LOCKED_PRIORITY
-            | NotificationChannel.USER_LOCKED_VISIBILITY, updated1.getUserLockedFields());
+                | NotificationChannel.USER_LOCKED_PRIORITY
+                | NotificationChannel.USER_LOCKED_VISIBILITY, updated1.getUserLockedFields());
 
         final NotificationChannel updated2 = mHelper.getNotificationChannel(
                 pkg2, uid2, NotificationChannel.DEFAULT_CHANNEL_ID, false);
@@ -382,14 +396,14 @@
     public void testUpdate_userLockedImportance() throws Exception {
         // all fields locked by user
         final NotificationChannel channel =
-            new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
+                new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
         channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
 
         mHelper.createNotificationChannel(pkg, uid, channel, false);
 
         // same id, try to update
         final NotificationChannel channel2 =
-            new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_HIGH);
+                new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_HIGH);
 
         mHelper.updateNotificationChannelFromAssistant(pkg, uid, channel2);
 
@@ -401,7 +415,7 @@
     public void testUpdate_userLockedVisibility() throws Exception {
         // all fields locked by user
         final NotificationChannel channel =
-            new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
+                new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
         channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
         channel.lockFields(NotificationChannel.USER_LOCKED_VISIBILITY);
 
@@ -409,7 +423,7 @@
 
         // same id, try to update
         final NotificationChannel channel2 =
-            new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_HIGH);
+                new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_HIGH);
         channel2.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
 
         mHelper.updateNotificationChannelFromAssistant(pkg, uid, channel2);
@@ -422,7 +436,7 @@
     public void testUpdate_userLockedVibration() throws Exception {
         // all fields locked by user
         final NotificationChannel channel =
-            new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
+                new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
         channel.setLights(false);
         channel.lockFields(NotificationChannel.USER_LOCKED_VIBRATION);
 
@@ -430,9 +444,9 @@
 
         // same id, try to update
         final NotificationChannel channel2 =
-            new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_HIGH);
+                new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_HIGH);
         channel2.enableVibration(true);
-        channel2.setVibrationPattern(new long[] {100});
+        channel2.setVibrationPattern(new long[]{100});
 
         mHelper.updateNotificationChannelFromAssistant(pkg, uid, channel2);
 
@@ -444,7 +458,7 @@
     public void testUpdate_userLockedLights() throws Exception {
         // all fields locked by user
         final NotificationChannel channel =
-            new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
+                new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
         channel.setLights(false);
         channel.lockFields(NotificationChannel.USER_LOCKED_LIGHTS);
 
@@ -452,7 +466,7 @@
 
         // same id, try to update
         final NotificationChannel channel2 =
-            new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_HIGH);
+                new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_HIGH);
         channel2.setLights(true);
 
         mHelper.updateNotificationChannelFromAssistant(pkg, uid, channel2);
@@ -465,7 +479,7 @@
     public void testUpdate_userLockedPriority() throws Exception {
         // all fields locked by user
         final NotificationChannel channel =
-            new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
+                new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
         channel.setBypassDnd(true);
         channel.lockFields(NotificationChannel.USER_LOCKED_PRIORITY);
 
@@ -473,7 +487,7 @@
 
         // same id, try to update all fields
         final NotificationChannel channel2 =
-            new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_HIGH);
+                new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_HIGH);
         channel2.setBypassDnd(false);
 
         mHelper.updateNotificationChannelFromAssistant(pkg, uid, channel2);
@@ -486,16 +500,16 @@
     public void testUpdate_userLockedRingtone() throws Exception {
         // all fields locked by user
         final NotificationChannel channel =
-            new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
-        channel.setSound(new Uri.Builder().scheme("test").build());
+                new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
+        channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes);
         channel.lockFields(NotificationChannel.USER_LOCKED_SOUND);
 
         mHelper.createNotificationChannel(pkg, uid, channel, false);
 
         // same id, try to update all fields
         final NotificationChannel channel2 =
-            new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_HIGH);
-        channel2.setSound(new Uri.Builder().scheme("test2").build());
+                new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_HIGH);
+        channel2.setSound(new Uri.Builder().scheme("test2").build(), mAudioAttributes);
 
         mHelper.updateNotificationChannelFromAssistant(pkg, uid, channel2);
 
@@ -527,7 +541,7 @@
         // no fields locked by user
         final NotificationChannel channel =
                 new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
-        channel.setSound(new Uri.Builder().scheme("test").build());
+        channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes);
         channel.setLights(true);
         channel.setBypassDnd(true);
         channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
@@ -537,7 +551,7 @@
         // same id, try to update all fields
         final NotificationChannel channel2 =
                 new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_HIGH);
-        channel2.setSound(new Uri.Builder().scheme("test2").build());
+        channel2.setSound(new Uri.Builder().scheme("test2").build(), mAudioAttributes);
         channel2.setLights(false);
         channel2.setBypassDnd(false);
         channel2.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
@@ -559,7 +573,7 @@
     public void testCreateChannel_CannotChangeHiddenFields() throws Exception {
         final NotificationChannel channel =
                 new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
-        channel.setSound(new Uri.Builder().scheme("test").build());
+        channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes);
         channel.setLights(true);
         channel.setBypassDnd(true);
         channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
@@ -586,7 +600,7 @@
     public void testCreateChannel_CannotChangeHiddenFieldsAssistant() throws Exception {
         final NotificationChannel channel =
                 new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
-        channel.setSound(new Uri.Builder().scheme("test").build());
+        channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes);
         channel.setLights(true);
         channel.setBypassDnd(true);
         channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
@@ -613,12 +627,12 @@
     public void testGetDeletedChannel() throws Exception {
         NotificationChannel channel =
                 new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
-        channel.setSound(new Uri.Builder().scheme("test").build());
+        channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes);
         channel.setLights(true);
         channel.setBypassDnd(true);
         channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
         channel.enableVibration(true);
-        channel.setVibrationPattern(new long[] {100, 67, 145, 156});
+        channel.setVibrationPattern(new long[]{100, 67, 145, 156});
 
         mHelper.createNotificationChannel(pkg, uid, channel, true);
         mHelper.deleteNotificationChannel(pkg, uid, channel.getId());
@@ -639,12 +653,12 @@
         Map<String, NotificationChannel> channelMap = new HashMap<>();
         NotificationChannel channel =
                 new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
-        channel.setSound(new Uri.Builder().scheme("test").build());
+        channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes);
         channel.setLights(true);
         channel.setBypassDnd(true);
         channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
         channel.enableVibration(true);
-        channel.setVibrationPattern(new long[] {100, 67, 145, 156});
+        channel.setVibrationPattern(new long[]{100, 67, 145, 156});
         channelMap.put(channel.getId(), channel);
         NotificationChannel channel2 =
                 new NotificationChannel("id4", "a", NotificationManager.IMPORTANCE_HIGH);
@@ -665,7 +679,7 @@
         }
 
         // Returns deleted channels too
-        channels =  mHelper.getNotificationChannels(pkg, uid, true).getList();
+        channels = mHelper.getNotificationChannels(pkg, uid, true).getList();
         assertEquals(3, channels.size());               // Includes default channel
         for (NotificationChannel nc : channels) {
             if (!NotificationChannel.DEFAULT_CHANNEL_ID.equals(nc.getId())) {
@@ -682,7 +696,7 @@
 
         mHelper.deleteNotificationChannel(pkg, uid, channel.getId());
 
-        channel.setSound(new Uri.Builder().scheme("test").build());
+        channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes);
         try {
             mHelper.updateNotificationChannel(pkg, uid, channel);
             fail("Updated deleted channel");
@@ -700,7 +714,7 @@
 
     @Test
     public void testCreateDeletedChannel() throws Exception {
-        long[] vibration = new long[] {100, 67, 145, 156};
+        long[] vibration = new long[]{100, 67, 145, 156};
         NotificationChannel channel =
                 new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
         channel.setVibrationPattern(vibration);
@@ -710,7 +724,7 @@
 
         NotificationChannel newChannel = new NotificationChannel(
                 channel.getId(), channel.getName(), NotificationManager.IMPORTANCE_HIGH);
-        newChannel.setVibrationPattern(new long[] {100});
+        newChannel.setVibrationPattern(new long[]{100});
 
         mHelper.createNotificationChannel(pkg, uid, newChannel, true);
 
@@ -721,7 +735,7 @@
 
     @Test
     public void testCreateChannel_alreadyExists() throws Exception {
-        long[] vibration = new long[] {100, 67, 145, 156};
+        long[] vibration = new long[]{100, 67, 145, 156};
         NotificationChannel channel =
                 new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_LOW);
         channel.setVibrationPattern(vibration);
@@ -730,7 +744,7 @@
 
         NotificationChannel newChannel = new NotificationChannel(
                 channel.getId(), channel.getName(), NotificationManager.IMPORTANCE_HIGH);
-        newChannel.setVibrationPattern(new long[] {100});
+        newChannel.setVibrationPattern(new long[]{100});
 
         mHelper.createNotificationChannel(pkg, uid, newChannel, true);
 
@@ -816,7 +830,8 @@
         try {
             mHelper.createNotificationChannel(pkg, uid, channel1, true);
             fail("Created a channel with a bad group");
-        } catch (IllegalArgumentException e) {}
+        } catch (IllegalArgumentException e) {
+        }
     }
 
     @Test
@@ -862,11 +877,11 @@
         List<NotificationChannelGroup> actual =
                 mHelper.getNotificationChannelGroups(pkg, uid, true).getList();
         assertEquals(3, actual.size());
-        for (NotificationChannelGroup group: actual) {
+        for (NotificationChannelGroup group : actual) {
             if (group.getId() == null) {
                 assertEquals(2, group.getChannels().size()); // misc channel too
                 assertTrue(channel3.getId().equals(group.getChannels().get(0).getId())
-                || channel3.getId().equals(group.getChannels().get(1).getId()));
+                        || channel3.getId().equals(group.getChannels().get(1).getId()));
             } else if (group.getId().equals(ncg.getId())) {
                 assertEquals(2, group.getChannels().size());
                 if (group.getChannels().get(0).getId().equals(channel1.getId())) {
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityGestureDetectorTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityGestureDetectorTest.java
new file mode 100644
index 0000000..d0c2b52
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityGestureDetectorTest.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility;
+
+import static junit.framework.TestCase.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verify;
+
+import android.accessibilityservice.AccessibilityService;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.os.Looper;
+import android.util.DisplayMetrics;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import java.util.ArrayList;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+
+/**
+ * Tests for AccessibilityGestureDetector
+ */
+public class AccessibilityGestureDetectorTest {
+
+    // Constants for testRecognizeGesturePath()
+    private static final PointF PATH_START = new PointF(300f, 300f);
+    private static final int PATH_STEP_PIXELS = 200;
+    private static final long PATH_STEP_MILLISEC = 100;
+
+    /**
+     * AccessibilitGestureDetector that can mock double-tap detector.
+     */
+    private class AccessibilityGestureDetectorTestable extends AccessibilityGestureDetector {
+        public AccessibilityGestureDetectorTestable(Context context, Listener listener) {
+            super(context, listener);
+        }
+
+        protected void setDoubleTapDetector(GestureDetector gestureDetector) {
+            mGestureDetector = gestureDetector;
+            mGestureDetector.setOnDoubleTapListener(this);
+        }
+    }
+
+
+    // Data used by all tests
+    private AccessibilityGestureDetectorTestable mDetector;
+    private AccessibilityGestureDetector.Listener mResultListener;
+
+
+    @BeforeClass
+    public static void oneTimeInitialization() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+    }
+
+    @Before
+    public void setUp() {
+        // Construct a mock Context.
+        DisplayMetrics displayMetricsMock = mock(DisplayMetrics.class);
+        displayMetricsMock.xdpi = 500;
+        displayMetricsMock.ydpi = 500;
+        Resources mockResources = mock(Resources.class);
+        when(mockResources.getDisplayMetrics()).thenReturn(displayMetricsMock);
+        Context contextMock = mock(Context.class);
+        when(contextMock.getMainLooper()).thenReturn(Looper.myLooper());
+        when(contextMock.getResources()).thenReturn(mockResources);
+
+        // Construct a testable AccessibilityGestureDetector.
+        mResultListener = mock(AccessibilityGestureDetector.Listener.class);
+        mDetector = new AccessibilityGestureDetectorTestable(contextMock, mResultListener);
+        GestureDetector doubleTapDetectorMock = mock(GestureDetector.class);
+        mDetector.setDoubleTapDetector(doubleTapDetectorMock);
+    }
+
+
+    @Test
+    public void testRecognizeGesturePath() {
+        final int d = 1000;  // Length of each segment in the test gesture, in pixels.
+
+        testPath(p(-d, +0), AccessibilityService.GESTURE_SWIPE_LEFT);
+        testPath(p(+d, +0), AccessibilityService.GESTURE_SWIPE_RIGHT);
+        testPath(p(+0, -d), AccessibilityService.GESTURE_SWIPE_UP);
+        testPath(p(+0, +d), AccessibilityService.GESTURE_SWIPE_DOWN);
+
+        testPath(p(-d, +0), p((-d - d), +0), AccessibilityService.GESTURE_SWIPE_LEFT);
+        testPath(p(-d, +0), p(+0, +0), AccessibilityService.GESTURE_SWIPE_LEFT_AND_RIGHT);
+        testPath(p(-d, +0), p(-d, -d), AccessibilityService.GESTURE_SWIPE_LEFT_AND_UP);
+        testPath(p(-d, +0), p(-d, +d), AccessibilityService.GESTURE_SWIPE_LEFT_AND_DOWN);
+
+        testPath(p(+d, +0), p(+0, +0), AccessibilityService.GESTURE_SWIPE_RIGHT_AND_LEFT);
+        testPath(p(+d, +0), p((+d + d), +0), AccessibilityService.GESTURE_SWIPE_RIGHT);
+        testPath(p(+d, +0), p(+d, -d), AccessibilityService.GESTURE_SWIPE_RIGHT_AND_UP);
+        testPath(p(+d, +0), p(+d, +d), AccessibilityService.GESTURE_SWIPE_RIGHT_AND_DOWN);
+
+        testPath(p(+0, -d), p(-d, -d), AccessibilityService.GESTURE_SWIPE_UP_AND_LEFT);
+        testPath(p(+0, -d), p(+d, -d), AccessibilityService.GESTURE_SWIPE_UP_AND_RIGHT);
+        testPath(p(+0, -d), p(+0, (-d - d)), AccessibilityService.GESTURE_SWIPE_UP);
+        testPath(p(+0, -d), p(+0, +0), AccessibilityService.GESTURE_SWIPE_UP_AND_DOWN);
+
+        testPath(p(+0, +d), p(-d, +d), AccessibilityService.GESTURE_SWIPE_DOWN_AND_LEFT);
+        testPath(p(+0, +d), p(+d, +d), AccessibilityService.GESTURE_SWIPE_DOWN_AND_RIGHT);
+        testPath(p(+0, +d), p(+0, +0), AccessibilityService.GESTURE_SWIPE_DOWN_AND_UP);
+        testPath(p(+0, +d), p(+0, (+d + d)), AccessibilityService.GESTURE_SWIPE_DOWN);
+    }
+
+    /** Convenient short alias to make a Point. */
+    private static Point p(int x, int y) {
+        return new Point(x, y);
+    }
+
+    /** Test recognizing path from PATH_START to PATH_START+delta. */
+    private void testPath(Point delta, int gestureId) {
+        ArrayList<PointF> path = new ArrayList<>();
+        path.add(PATH_START);
+
+        PointF segmentEnd = new PointF(PATH_START.x + delta.x, PATH_START.y + delta.y);
+        fillPath(PATH_START, segmentEnd, path);
+
+        testPath(path, gestureId);
+    }
+
+    /** Test recognizing path from PATH_START to PATH_START+delta1 to PATH_START+delta2. */
+    private void testPath(Point delta1, Point delta2, int gestureId) {
+        ArrayList<PointF> path = new ArrayList<>();
+        path.add(PATH_START);
+
+        PointF startPlusDelta1 = new PointF(PATH_START.x + delta1.x, PATH_START.y + delta1.y);
+        fillPath(PATH_START, startPlusDelta1, path);
+
+        PointF startPlusDelta2 = new PointF(PATH_START.x + delta2.x, PATH_START.y + delta2.y);
+        fillPath(startPlusDelta1, startPlusDelta2, path);
+
+        testPath(path, gestureId);
+    }
+
+    /** Fill in movement points from start to end, appending points to path. */
+    private void fillPath(PointF start, PointF end, ArrayList<PointF> path) {
+        // Calculate number of path steps needed.
+        float deltaX = end.x - start.x;
+        float deltaY = end.y - start.y;
+        float distance = (float) Math.hypot(deltaX, deltaY);
+        float numSteps = distance / (float) PATH_STEP_PIXELS;
+        float stepX = (float) deltaX / numSteps;
+        float stepY = (float) deltaY / numSteps;
+
+        // For each path step from start (non-inclusive) to end ... add a motion point.
+        for (int step = 1; step < numSteps; ++step) {
+            path.add(new PointF(
+                (start.x + (stepX * (float) step)),
+                (start.y + (stepY * (float) step))));
+        }
+    }
+
+    /** Test recognizing a path made of motion event points. */
+    private void testPath(ArrayList<PointF> path, int gestureId) {
+        // Clear last recognition result.
+        reset(mResultListener);
+
+        int policyFlags = 0;
+        long eventDownTimeMs = 0;
+        long eventTimeMs = eventDownTimeMs;
+
+        // For each path point...
+        for (int pointIndex = 0; pointIndex < path.size(); ++pointIndex) {
+
+            // Create motion event.
+            PointF point = path.get(pointIndex);
+            int action = MotionEvent.ACTION_MOVE;
+            if (pointIndex == 0) {
+                action = MotionEvent.ACTION_DOWN;
+            } else if (pointIndex == path.size() - 1) {
+                action = MotionEvent.ACTION_UP;
+            }
+            MotionEvent event = MotionEvent.obtain(eventDownTimeMs, eventTimeMs, action,
+                    point.x, point.y, 0);
+
+            // Send event.
+            mDetector.onMotionEvent(event, policyFlags);
+            eventTimeMs += PATH_STEP_MILLISEC;
+        }
+
+        // Check that correct gesture was recognized.
+        verify(mResultListener).onGestureCompleted(gestureId);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
index 2d07e65..90a2ec0 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
@@ -73,8 +73,7 @@
         mInvalidIsa = new TestData("INVALID", "INVALID_ISA", mUser0);
         mDoesNotExist = new TestData("DOES.NOT.EXIST", isa, mUser1);
 
-
-        mDexManager = new DexManager();
+        mDexManager = new DexManager(null, null, null, null);
 
         // Foo and Bar are available to user0.
         // Only Bar is available to user1;
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/PackageDexUsageTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/PackageDexUsageTests.java
index 5a42841..19e0bcf 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/PackageDexUsageTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/PackageDexUsageTests.java
@@ -256,6 +256,32 @@
         assertNull(mPackageDexUsage.getPackageUseInfo(mFooBaseUser0.mPackageName));
     }
 
+    @Test
+    public void testRemoveUserPackage() {
+        // Record Bar secondaries for two different users.
+        assertTrue(record(mBarSecondary1User0));
+        assertTrue(record(mBarSecondary2User1));
+
+        // Remove user 0 files.
+        assertTrue(mPackageDexUsage.removeUserPackage(mBarSecondary1User0.mPackageName,
+                mBarSecondary1User0.mOwnerUserId));
+        // Assert that only user 1 files are there.
+        assertPackageDexUsage(null, mBarSecondary2User1);
+    }
+
+    @Test
+    public void testRemoveDexFile() {
+        // Record Bar secondaries for two different users.
+        assertTrue(record(mBarSecondary1User0));
+        assertTrue(record(mBarSecondary2User1));
+
+        // Remove mBarSecondary1User0 file.
+        assertTrue(mPackageDexUsage.removeDexFile(mBarSecondary1User0.mPackageName,
+                mBarSecondary1User0.mDexFile, mBarSecondary1User0.mOwnerUserId));
+        // Assert that only user 1 files are there.
+        assertPackageDexUsage(null, mBarSecondary2User1);
+    }
+
     private void assertPackageDexUsage(TestData primary, TestData... secondaries) {
         String packageName = primary == null ? secondaries[0].mPackageName : primary.mPackageName;
         boolean primaryUsedByOtherApps = primary == null ? false : primary.mUsedByOtherApps;