Merge "DirectReply: Close if touching outside status bar" into nyc-mr2-dev
diff --git a/Android.mk b/Android.mk
index d813c91..730eeb1 100644
--- a/Android.mk
+++ b/Android.mk
@@ -202,6 +202,7 @@
 	core/java/android/net/IIpConnectivityMetrics.aidl \
 	core/java/android/net/IEthernetManager.aidl \
 	core/java/android/net/IEthernetServiceListener.aidl \
+	core/java/android/net/INetdEventCallback.aidl \
 	core/java/android/net/INetworkManagementEventObserver.aidl \
 	core/java/android/net/INetworkPolicyListener.aidl \
 	core/java/android/net/INetworkPolicyManager.aidl \
@@ -574,6 +575,9 @@
 	frameworks/base/graphics/java/android/graphics/drawable/Icon.aidl \
 	frameworks/base/core/java/android/accounts/AuthenticatorDescription.aidl \
 	frameworks/base/core/java/android/accounts/Account.aidl \
+	frameworks/base/core/java/android/app/admin/ConnectEvent.aidl \
+	frameworks/base/core/java/android/app/admin/DnsEvent.aidl \
+	frameworks/base/core/java/android/app/admin/NetworkEvent.aidl \
 	frameworks/base/core/java/android/app/admin/SystemUpdatePolicy.aidl \
 	frameworks/base/core/java/android/print/PrintDocumentInfo.aidl \
 	frameworks/base/core/java/android/print/PageRange.aidl \
diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java
index 9c17780..72ccf72 100644
--- a/core/java/android/app/Dialog.java
+++ b/core/java/android/app/Dialog.java
@@ -185,6 +185,11 @@
         mWindow = w;
         w.setCallback(this);
         w.setOnWindowDismissedCallback(this);
+        w.setOnWindowSwipeDismissedCallback(() -> {
+            if (mCancelable) {
+                cancel();
+            }
+        });
         w.setWindowManager(mWindowManager, null, null);
         w.setGravity(Gravity.CENTER);
 
diff --git a/core/java/android/app/SharedPreferencesImpl.java b/core/java/android/app/SharedPreferencesImpl.java
index c1180e2..f273cd8 100644
--- a/core/java/android/app/SharedPreferencesImpl.java
+++ b/core/java/android/app/SharedPreferencesImpl.java
@@ -26,6 +26,8 @@
 import android.util.Log;
 
 import com.google.android.collect.Maps;
+
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.XmlUtils;
 
 import dalvik.system.BlockGuard;
@@ -72,6 +74,14 @@
     private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
             new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
 
+    /** Current memory state (always increasing) */
+    @GuardedBy("this")
+    private long mCurrentMemoryStateGeneration;
+
+    /** Latest memory state that was committed to disk */
+    @GuardedBy("mWritingToDiskLock")
+    private long mDiskStateGeneration;
+
     SharedPreferencesImpl(File file, int mode) {
         mFile = file;
         mBackupFile = makeBackupFile(file);
@@ -289,7 +299,7 @@
 
     // Return value from EditorImpl#commitToMemory()
     private static class MemoryCommitResult {
-        public boolean changesMade;  // any keys different?
+        public long memoryStateGeneration;
         public List<String> keysModified;  // may be null
         public Set<OnSharedPreferenceChangeListener> listeners;  // may be null
         public Map<?, ?> mapToWriteToDisk;
@@ -412,9 +422,11 @@
                 }
 
                 synchronized (this) {
+                    boolean changesMade = false;
+
                     if (mClear) {
                         if (!mMap.isEmpty()) {
-                            mcr.changesMade = true;
+                            changesMade = true;
                             mMap.clear();
                         }
                         mClear = false;
@@ -441,13 +453,19 @@
                             mMap.put(k, v);
                         }
 
-                        mcr.changesMade = true;
+                        changesMade = true;
                         if (hasListeners) {
                             mcr.keysModified.add(k);
                         }
                     }
 
                     mModified.clear();
+
+                    if (changesMade) {
+                        mCurrentMemoryStateGeneration++;
+                    }
+
+                    mcr.memoryStateGeneration = mCurrentMemoryStateGeneration;
                 }
             }
             return mcr;
@@ -509,10 +527,12 @@
      */
     private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                   final Runnable postWriteRunnable) {
+        final boolean isFromSyncCommit = (postWriteRunnable == null);
+
         final Runnable writeToDiskRunnable = new Runnable() {
                 public void run() {
                     synchronized (mWritingToDiskLock) {
-                        writeToFile(mcr);
+                        writeToFile(mcr, isFromSyncCommit);
                     }
                     synchronized (SharedPreferencesImpl.this) {
                         mDiskWritesInFlight--;
@@ -523,8 +543,6 @@
                 }
             };
 
-        final boolean isFromSyncCommit = (postWriteRunnable == null);
-
         // Typical #commit() path with fewer allocations, doing a write on
         // the current thread.
         if (isFromSyncCommit) {
@@ -538,6 +556,10 @@
             }
         }
 
+        if (DEBUG) {
+            Log.d(TAG, "added " + mcr.memoryStateGeneration + " -> " + mFile.getName());
+        }
+
         QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
     }
 
@@ -565,17 +587,34 @@
     }
 
     // Note: must hold mWritingToDiskLock
-    private void writeToFile(MemoryCommitResult mcr) {
+    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
         // Rename the current file so it may be used as a backup during the next read
         if (mFile.exists()) {
-            if (!mcr.changesMade) {
-                // If the file already exists, but no changes were
-                // made to the underlying map, it's wasteful to
-                // re-write the file.  Return as if we wrote it
-                // out.
+            boolean needsWrite = false;
+
+            // Only need to write if the disk state is older than this commit
+            if (mDiskStateGeneration < mcr.memoryStateGeneration) {
+                if (isFromSyncCommit) {
+                    needsWrite = true;
+                } else {
+                    synchronized (this) {
+                        // No need to persist intermediate states. Just wait for the latest state to
+                        // be persisted.
+                        if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
+                            needsWrite = true;
+                        }
+                    }
+                }
+            }
+
+            if (!needsWrite) {
+                if (DEBUG) {
+                    Log.d(TAG, "skipped " + mcr.memoryStateGeneration + " -> " + mFile.getName());
+                }
                 mcr.setDiskWriteResult(true);
                 return;
             }
+
             if (!mBackupFile.exists()) {
                 if (!mFile.renameTo(mBackupFile)) {
                     Log.e(TAG, "Couldn't rename file " + mFile
@@ -599,6 +638,11 @@
             }
             XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
             FileUtils.sync(str);
+
+            if (DEBUG) {
+                Log.d(TAG, "wrote " + mcr.memoryStateGeneration + " -> " + mFile.getName());
+            }
+
             str.close();
             ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
             try {
@@ -612,7 +656,11 @@
             }
             // Writing was successful, delete the backup file if there is one.
             mBackupFile.delete();
+
+            mDiskStateGeneration = mcr.memoryStateGeneration;
+
             mcr.setDiskWriteResult(true);
+
             return;
         } catch (XmlPullParserException e) {
             Log.w(TAG, "writeToFile: Got exception:", e);
diff --git a/core/java/android/app/admin/ConnectEvent.aidl b/core/java/android/app/admin/ConnectEvent.aidl
new file mode 100644
index 0000000..bab40f5
--- /dev/null
+++ b/core/java/android/app/admin/ConnectEvent.aidl
@@ -0,0 +1,21 @@
+/*
+ * 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.app.admin;
+
+/** {@hide} */
+parcelable ConnectEvent;
+
diff --git a/core/java/android/app/admin/ConnectEvent.java b/core/java/android/app/admin/ConnectEvent.java
new file mode 100644
index 0000000..b6b14f2
--- /dev/null
+++ b/core/java/android/app/admin/ConnectEvent.java
@@ -0,0 +1,93 @@
+/*
+ * 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.app.admin;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A class that represents a connect library call event.
+ * @hide
+ */
+public final class ConnectEvent extends NetworkEvent implements Parcelable {
+
+    /** The destination IP address. */
+    private final String ipAddress;
+
+    /** The destination port number. */
+    private final int port;
+
+    /** @hide */
+    public ConnectEvent(String ipAddress, int port, String packageName, long timestamp) {
+        super(packageName, timestamp);
+        this.ipAddress = ipAddress;
+        this.port = port;
+    }
+
+    private ConnectEvent(Parcel in) {
+        this.ipAddress = in.readString();
+        this.port = in.readInt();
+        this.packageName = in.readString();
+        this.timestamp = in.readLong();
+    }
+
+    public String getIpAddress() {
+        return ipAddress;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("ConnectEvent(%s, %d, %d, %s)", ipAddress, port, timestamp,
+                packageName);
+    }
+
+    public static final Parcelable.Creator<ConnectEvent> CREATOR
+            = new Parcelable.Creator<ConnectEvent>() {
+        @Override
+        public ConnectEvent createFromParcel(Parcel in) {
+            if (in.readInt() != PARCEL_TOKEN_CONNECT_EVENT) {
+                return null;
+            }
+            return new ConnectEvent(in);
+        }
+
+        @Override
+        public ConnectEvent[] newArray(int size) {
+            return new ConnectEvent[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        // write parcel token first
+        out.writeInt(PARCEL_TOKEN_CONNECT_EVENT);
+        out.writeString(ipAddress);
+        out.writeInt(port);
+        out.writeString(packageName);
+        out.writeLong(timestamp);
+    }
+}
+
diff --git a/core/java/android/app/admin/DeviceAdminReceiver.java b/core/java/android/app/admin/DeviceAdminReceiver.java
index dd70b5d..cbd5a6d 100644
--- a/core/java/android/app/admin/DeviceAdminReceiver.java
+++ b/core/java/android/app/admin/DeviceAdminReceiver.java
@@ -276,6 +276,36 @@
             = "android.app.action.SECURITY_LOGS_AVAILABLE";
 
     /**
+     * Broadcast action: notify that a new batch of network logs is ready to be collected.
+     * @see DeviceAdminReceiver#onNetworkLogsAvailable
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_NETWORK_LOGS_AVAILABLE
+            = "android.app.action.NETWORK_LOGS_AVAILABLE";
+
+    /**
+     * A {@code long} containing a token of the current batch of network logs, that has to be used
+     * to retrieve the batch of logs by the device owner.
+     *
+     * @see #ACTION_NETWORK_LOGS_AVAILABLE
+     * @see DevicePolicyManager#retrieveNetworkLogs
+     * @hide
+     */
+    public static final String EXTRA_NETWORK_LOGS_TOKEN =
+            "android.app.extra.EXTRA_NETWORK_LOGS_TOKEN";
+
+    /**
+     * An {@code int} count representing a total count of network logs inside the current batch of
+     * network logs.
+     *
+     * @see #ACTION_NETWORK_LOGS_AVAILABLE
+     * @hide
+     */
+    public static final String EXTRA_NETWORK_LOGS_COUNT =
+            "android.app.extra.EXTRA_NETWORK_LOGS_COUNT";
+
+    /**
      * A string containing the SHA-256 hash of the bugreport file.
      *
      * @see #ACTION_BUGREPORT_SHARE
@@ -635,6 +665,25 @@
     }
 
     /**
+     * Called each time a new batch of network logs can be retrieved. This callback method will only
+     * ever be called when network logging is enabled. The logs can only be retrieved while network
+     * logging is enabled.
+     *
+     * <p>This callback is only applicable to device owners.
+     *
+     * @param context The running context as per {@link #onReceive}.
+     * @param intent The received intent as per {@link #onReceive}.
+     * @param batchToken The token representing the current batch of network logs.
+     * @param networkLogsCount The total count of events in the current batch of network logs.
+     * @see DevicePolicyManager#retrieveNetworkLogs(ComponentName)
+     *
+     * @hide
+     */
+    public void onNetworkLogsAvailable(Context context, Intent intent, long batchToken,
+            int networkLogsCount) {
+    }
+
+    /**
      * Intercept standard device administrator broadcasts.  Implementations
      * should not override this method; it is better to implement the
      * convenience callbacks for each action.
@@ -688,6 +737,10 @@
             onBugreportFailed(context, intent, failureCode);
         } else if (ACTION_SECURITY_LOGS_AVAILABLE.equals(action)) {
             onSecurityLogsAvailable(context, intent);
+        } else if (ACTION_NETWORK_LOGS_AVAILABLE.equals(action)) {
+            long batchToken = intent.getLongExtra(EXTRA_NETWORK_LOGS_TOKEN, -1);
+            int networkLogsCount = intent.getIntExtra(EXTRA_NETWORK_LOGS_COUNT, 0);
+            onNetworkLogsAvailable(context, intent, batchToken, networkLogsCount);
         }
     }
 }
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index a2f9bdd..1912437 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -25,6 +25,7 @@
 import android.annotation.SystemApi;
 import android.annotation.UserIdInt;
 import android.app.Activity;
+import android.app.admin.NetworkEvent;
 import android.app.admin.SecurityLog.SecurityEvent;
 import android.content.ComponentName;
 import android.content.Context;
@@ -949,6 +950,15 @@
             = "android.app.action.SET_NEW_PARENT_PROFILE_PASSWORD";
 
     /**
+     * Broadcast action: Tell the status bar to open the device monitoring dialog, e.g. when
+     * Network logging was enabled and the user tapped the notification.
+     * <p class="note">This is a protected intent that can only be sent by the system.</p>
+     * @hide
+     */
+    public static final String ACTION_SHOW_DEVICE_MONITORING_DIALOG
+            = "android.app.action.SHOW_DEVICE_MONITORING_DIALOG";
+
+    /**
      * Flag used by {@link #addCrossProfileIntentFilter} to allow activities in
      * the parent profile to access intents sent from the managed profile.
      * That is, when an app in the managed profile calls
@@ -6609,4 +6619,81 @@
             throw re.rethrowFromSystemServer();
         }
     }
+
+    /**
+     * Called by a device owner to control the network logging feature. Logging can only be
+     * enabled on single user devices where the sole user is managed by the device owner. If a new
+     * user is added on the device, logging is disabled.
+     *
+     * <p> Network logs contain DNS lookup and connect() library call events.
+     *
+     * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+     * @param enabled whether network logging should be enabled or not.
+     * @throws {@link SecurityException} if {@code admin} is not a device owner.
+     * @see #retrieveNetworkLogs
+     *
+     * @hide
+     */
+    public void setNetworkLoggingEnabled(@NonNull ComponentName admin, boolean enabled) {
+        throwIfParentInstance("setNetworkLoggingEnabled");
+        try {
+            mService.setNetworkLoggingEnabled(admin, enabled);
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return whether network logging is enabled by a device owner.
+     *
+     * @param admin Which {@link DeviceAdminReceiver} this request is associated with. Can only
+     * be {@code null} if the caller has MANAGE_USERS permission.
+     * @return {@code true} if network logging is enabled by device owner, {@code false} otherwise.
+     * @throws {@link SecurityException} if {@code admin} is not a device owner and caller has
+     * no MANAGE_USERS permission
+     *
+     * @hide
+     */
+    public boolean isNetworkLoggingEnabled(@Nullable ComponentName admin) {
+        throwIfParentInstance("isNetworkLoggingEnabled");
+        try {
+            return mService.isNetworkLoggingEnabled(admin);
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Called by device owner to retrieve the most recent batch of network logging events.
+     * A device owner has to provide a batchToken provided as part of
+     * {@link DeviceAdminReceiver#onNetworkLogsAvailable} callback. If the token doesn't match the
+     * token of the most recent available batch of logs, {@code null} will be returned.
+     *
+     * <p> {@link NetworkEvent} can be one of {@link DnsEvent} or {@link ConnectEvent}.
+     *
+     * <p> The list of network events is sorted chronologically, and contains at most 1200 events.
+     *
+     * <p> Access to the logs is rate limited and this method will only return a new batch of logs
+     * after the device device owner has been notified via
+     * {@link DeviceAdminReceiver#onNetworkLogsAvailable}.
+     *
+     * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+     * @param batchToken A token of the batch to retrieve
+     * @return A new batch of network logs which is a list of {@link NetworkEvent}. Returns
+     *        {@code null} if the batch represented by batchToken is no longer available or if
+     *        logging is disabled.
+     * @throws {@link SecurityException} if {@code admin} is not a device owner.
+     * @see DeviceAdminReceiver#onNetworkLogsAvailable
+     *
+     * @hide
+     */
+    public @Nullable List<NetworkEvent> retrieveNetworkLogs(@NonNull ComponentName admin,
+            long batchToken) {
+        throwIfParentInstance("retrieveNetworkLogs");
+        try {
+            return mService.retrieveNetworkLogs(admin, batchToken);
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/core/java/android/app/admin/DnsEvent.aidl b/core/java/android/app/admin/DnsEvent.aidl
new file mode 100644
index 0000000..6da962a
--- /dev/null
+++ b/core/java/android/app/admin/DnsEvent.aidl
@@ -0,0 +1,21 @@
+/*
+ * 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.app.admin;
+
+/** {@hide} */
+parcelable DnsEvent;
+
diff --git a/core/java/android/app/admin/DnsEvent.java b/core/java/android/app/admin/DnsEvent.java
new file mode 100644
index 0000000..4ff8e1c
--- /dev/null
+++ b/core/java/android/app/admin/DnsEvent.java
@@ -0,0 +1,124 @@
+/*
+ * 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.app.admin;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A class that represents a DNS lookup event.
+ * @hide
+ */
+public final class DnsEvent extends NetworkEvent implements Parcelable {
+
+    /** The hostname that was looked up. */
+    private final String hostname;
+
+    /** Contains (possibly a subset of) the IP addresses returned. */
+    private final String[] ipAddresses;
+
+    /**
+     * The number of IP addresses returned from the DNS lookup event. May be different from the
+     * length of ipAddresses if there were too many addresses to log.
+     */
+    private final int ipAddressesCount;
+
+    /** @hide */
+    public DnsEvent(String hostname, String[] ipAddresses, int ipAddressesCount,
+            String packageName, long timestamp) {
+        super(packageName, timestamp);
+        this.hostname = hostname;
+        this.ipAddresses = ipAddresses;
+        this.ipAddressesCount = ipAddressesCount;
+    }
+
+    private DnsEvent(Parcel in) {
+        this.hostname = in.readString();
+        this.ipAddresses = in.createStringArray();
+        this.ipAddressesCount = in.readInt();
+        this.packageName = in.readString();
+        this.timestamp = in.readLong();
+    }
+
+    /** Returns the hostname that was looked up. */
+    public String getHostname() {
+        return hostname;
+    }
+
+    /** Returns (possibly a subset of) the IP addresses returned. */
+    public String[] getIpAddresses() {
+        return ipAddresses;
+    }
+
+    /**
+     * Returns the number of IP addresses returned from the DNS lookup event. May be different from
+     * the length of ipAddresses if there were too many addresses to log.
+     */
+    public int getIpAddressesCount() {
+        return ipAddressesCount;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        if (ipAddresses != null) {
+            for (int i = 0; i < ipAddresses.length; i++) {
+                sb.append(ipAddresses[i]);
+                if (i < ipAddresses.length - 1) {
+                    sb.append(" ");
+                }
+            }
+        } else {
+            sb.append("NONE");
+        }
+        return String.format("DnsEvent(%s, %s, %d, %d, %s)", hostname, sb.toString(),
+                ipAddressesCount, timestamp, packageName);
+    }
+
+    public static final Parcelable.Creator<DnsEvent> CREATOR
+            = new Parcelable.Creator<DnsEvent>() {
+        @Override
+        public DnsEvent createFromParcel(Parcel in) {
+            if (in.readInt() != PARCEL_TOKEN_DNS_EVENT) {
+                return null;
+            }
+            return new DnsEvent(in);
+        }
+
+        @Override
+        public DnsEvent[] newArray(int size) {
+            return new DnsEvent[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        // write parcel token first
+        out.writeInt(PARCEL_TOKEN_DNS_EVENT);
+        out.writeString(hostname);
+        out.writeStringArray(ipAddresses);
+        out.writeInt(ipAddressesCount);
+        out.writeString(packageName);
+        out.writeLong(timestamp);
+    }
+}
+
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index f39cb5a..f0710ec 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -17,6 +17,7 @@
 
 package android.app.admin;
 
+import android.app.admin.NetworkEvent;
 import android.app.admin.SystemUpdatePolicy;
 import android.content.ComponentName;
 import android.content.Intent;
@@ -311,4 +312,8 @@
 
     void setBackupServiceEnabled(in ComponentName admin, boolean enabled);
     boolean isBackupServiceEnabled(in ComponentName admin);
+
+    void setNetworkLoggingEnabled(in ComponentName admin, boolean enabled);
+    boolean isNetworkLoggingEnabled(in ComponentName admin);
+    List<NetworkEvent> retrieveNetworkLogs(in ComponentName admin, long batchToken);
 }
diff --git a/core/java/android/app/admin/NetworkEvent.aidl b/core/java/android/app/admin/NetworkEvent.aidl
new file mode 100644
index 0000000..5fa5dbf
--- /dev/null
+++ b/core/java/android/app/admin/NetworkEvent.aidl
@@ -0,0 +1,21 @@
+/*
+ * 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.app.admin;
+
+/** {@hide} */
+parcelable NetworkEvent;
+
diff --git a/core/java/android/app/admin/NetworkEvent.java b/core/java/android/app/admin/NetworkEvent.java
new file mode 100644
index 0000000..0de2665
--- /dev/null
+++ b/core/java/android/app/admin/NetworkEvent.java
@@ -0,0 +1,97 @@
+/*
+ * 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.app.admin;
+
+import android.content.pm.PackageManager;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.ParcelFormatException;
+
+/**
+ * An abstract class that represents a network event.
+ * @hide
+ */
+public abstract class NetworkEvent implements Parcelable {
+
+    static final int PARCEL_TOKEN_DNS_EVENT = 1;
+    static final int PARCEL_TOKEN_CONNECT_EVENT = 2;
+
+    /** The package name of the UID that performed the query. */
+    String packageName;
+
+    /** The timestamp of the event being reported in milliseconds. */
+    long timestamp;
+
+    /** @hide */
+    NetworkEvent() {
+        //empty constructor
+    }
+
+    /** @hide */
+    NetworkEvent(String packageName, long timestamp) {
+        this.packageName = packageName;
+        this.timestamp = timestamp;
+    }
+
+    /**
+     * Returns the package name of the UID that performed the query, as returned by
+     * {@link PackageManager#getNameForUid}.
+     */
+    public String getPackageName() {
+        return packageName;
+    }
+
+    /**
+     * Returns the timestamp of the event being reported in milliseconds, the difference between
+     * the time the event was reported and midnight, January 1, 1970 UTC.
+     */
+    public long getTimestamp() {
+        return timestamp;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Parcelable.Creator<NetworkEvent> CREATOR
+            = new Parcelable.Creator<NetworkEvent>() {
+        public NetworkEvent createFromParcel(Parcel in) {
+            final int initialPosition = in.dataPosition();
+            final int parcelToken = in.readInt();
+            // we need to move back to the position from before we read parcelToken
+            in.setDataPosition(initialPosition);
+            switch (parcelToken) {
+                case PARCEL_TOKEN_DNS_EVENT:
+                    return DnsEvent.CREATOR.createFromParcel(in);
+                case PARCEL_TOKEN_CONNECT_EVENT:
+                    return ConnectEvent.CREATOR.createFromParcel(in);
+                default:
+                    throw new ParcelFormatException("Unexpected NetworkEvent token in parcel: "
+                            + parcelToken);
+            }
+        }
+
+        public NetworkEvent[] newArray(int size) {
+            return new NetworkEvent[size];
+        }
+    };
+
+    @Override
+    public abstract void writeToParcel(Parcel out, int flags);
+}
+
diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java
index 542b06b..5ced03b 100644
--- a/core/java/android/bluetooth/BluetoothAdapter.java
+++ b/core/java/android/bluetooth/BluetoothAdapter.java
@@ -201,6 +201,23 @@
     public static final int STATE_BLE_TURNING_OFF = 16;
 
     /**
+     * Human-readable string helper for AdapterState
+     * @hide
+     */
+    public static String nameForState(@AdapterState int state) {
+        switch(state) {
+            case STATE_OFF: return "OFF";
+            case STATE_TURNING_ON: return "TURNING_ON";
+            case STATE_ON: return "ON";
+            case STATE_TURNING_OFF: return "TURNING_OFF";
+            case STATE_BLE_TURNING_ON: return "BLE_TURNING_ON";
+            case STATE_BLE_ON: return "BLE_ON";
+            case STATE_BLE_TURNING_OFF: return "BLE_TURNING_OFF";
+            default: return "?!?!? (" + state + ")";
+        }
+    }
+
+    /**
      * Activity Action: Show a system activity that requests discoverable mode.
      * This activity will also request the user to turn on Bluetooth if it
      * is not currently enabled.
@@ -662,15 +679,8 @@
     @SystemApi
     public boolean isLeEnabled() {
        final int state = getLeState();
-       if (state == BluetoothAdapter.STATE_ON) {
-           if (DBG) Log.d (TAG, "STATE_ON");
-       } else if (state == BluetoothAdapter.STATE_BLE_ON) {
-           if (DBG) Log.d (TAG, "STATE_BLE_ON");
-       } else {
-           if (DBG) Log.d (TAG, "STATE_OFF");
-           return false;
-       }
-       return true;
+       if (DBG) Log.d(TAG, "isLeEnabled(): " + BluetoothAdapter.nameForState(state));
+       return (state == BluetoothAdapter.STATE_ON || state == BluetoothAdapter.STATE_BLE_ON);
     }
 
     /**
@@ -835,10 +845,10 @@
         if (state == BluetoothAdapter.STATE_BLE_ON
             || state == BluetoothAdapter.STATE_BLE_TURNING_ON
             || state == BluetoothAdapter.STATE_BLE_TURNING_OFF) {
-            if (VDBG) Log.d(TAG, "Consider internal state as OFF");
+            if (VDBG) Log.d(TAG, "Consider " + BluetoothAdapter.nameForState(state) + " state as OFF");
             state = BluetoothAdapter.STATE_OFF;
         }
-        if (VDBG) Log.d(TAG, "" + hashCode() + ": getState(). Returning " + state);
+        if (VDBG) Log.d(TAG, "" + hashCode() + ": getState(). Returning " + BluetoothAdapter.nameForState(state));
         return state;
     }
 
@@ -875,12 +885,12 @@
             mServiceLock.readLock().unlock();
         }
 
-        if (VDBG) Log.d(TAG,"getLeState() returning " + state);
+        if (VDBG) Log.d(TAG,"getLeState() returning " + BluetoothAdapter.nameForState(state));
         return state;
     }
 
     boolean getLeAccess() {
-        if(getLeState() == STATE_ON)
+        if (getLeState() == STATE_ON)
             return true;
 
         else if (getLeState() == STATE_BLE_ON)
@@ -918,8 +928,8 @@
      */
     @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
     public boolean enable() {
-        if (isEnabled() == true) {
-            if (DBG) Log.d(TAG, "enable(): BT is already enabled..!");
+        if (isEnabled()) {
+            if (DBG) Log.d(TAG, "enable(): BT already enabled!");
             return true;
         }
         try {
@@ -1540,8 +1550,9 @@
                     }
                 }
             }
-        } catch (RemoteException e) {Log.e(TAG, "getSupportedProfiles:", e);}
-
+        } catch (RemoteException e) {
+          Log.e(TAG, "getSupportedProfiles:", e);
+        }
         return supportedProfiles;
     }
 
@@ -1892,34 +1903,6 @@
      * @hide
      */
     public Pair<byte[], byte[]> readOutOfBandData() {
-        if (getState() != STATE_ON) return null;
-        //TODO(BT
-        /*
-        try {
-            byte[] hash;
-            byte[] randomizer;
-
-            byte[] ret = null;
-            mServiceLock.readLock().lock();
-            if (mService != null) mService.readOutOfBandData();
-
-            if (ret  == null || ret.length != 32) return null;
-
-            hash = Arrays.copyOfRange(ret, 0, 16);
-            randomizer = Arrays.copyOfRange(ret, 16, 32);
-
-            if (DBG) {
-                Log.d(TAG, "readOutOfBandData:" + Arrays.toString(hash) +
-                  ":" + Arrays.toString(randomizer));
-            }
-            return new Pair<byte[], byte[]>(hash, randomizer);
-
-        } catch (RemoteException e) {
-            Log.e(TAG, "", e);
-        } finally {
-            mServiceLock.readLock().unlock();
-        }
-        */
         return null;
     }
 
@@ -2066,7 +2049,7 @@
                             if (cb != null) {
                                 cb.onBluetoothServiceUp(bluetoothService);
                             } else {
-                                Log.d(TAG, "onBluetoothServiceUp: cb is null!!!");
+                                Log.d(TAG, "onBluetoothServiceUp: cb is null!");
                             }
                         } catch (Exception e) {
                             Log.e(TAG,"",e);
@@ -2094,7 +2077,7 @@
                             if (cb != null) {
                                 cb.onBluetoothServiceDown();
                             } else {
-                                Log.d(TAG, "onBluetoothServiceDown: cb is null!!!");
+                                Log.d(TAG, "onBluetoothServiceDown: cb is null!");
                             }
                         } catch (Exception e) {
                             Log.e(TAG,"",e);
@@ -2104,7 +2087,7 @@
             }
 
             public void onBrEdrDown() {
-                if (DBG) Log.i(TAG, "onBrEdrDown:");
+                if (VDBG) Log.i(TAG, "onBrEdrDown: " + mService);
             }
     };
 
@@ -2115,7 +2098,7 @@
      */
     public boolean enableNoAutoConnect() {
         if (isEnabled() == true){
-            if (DBG) Log.d(TAG, "enableNoAutoConnect(): BT is already enabled..!");
+            if (DBG) Log.d(TAG, "enableNoAutoConnect(): BT already enabled!");
             return true;
         }
         try {
@@ -2155,22 +2138,6 @@
      */
     public boolean changeApplicationBluetoothState(boolean on,
                                                    BluetoothStateChangeCallback callback) {
-        if (callback == null) return false;
-
-        //TODO(BT)
-        /*
-        try {
-            mServiceLock.readLock().lock();
-            if (mService != null) {
-                return mService.changeApplicationBluetoothState(on, new
-                    StateChangeCallbackWrapper(callback), new Binder());
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "changeBluetoothState", e);
-        } finally {
-            mServiceLock.readLock().unlock();
-        }
-        */
         return false;
     }
 
diff --git a/core/java/android/content/BroadcastReceiver.java b/core/java/android/content/BroadcastReceiver.java
index 10e6fb2..a7a8615 100644
--- a/core/java/android/content/BroadcastReceiver.java
+++ b/core/java/android/content/BroadcastReceiver.java
@@ -27,189 +27,25 @@
 import android.util.Slog;
 
 /**
- * Base class for code that will receive intents sent by sendBroadcast().
- *
- * <p>If you don't need to send broadcasts across applications, consider using
- * this class with {@link android.support.v4.content.LocalBroadcastManager} instead
- * of the more general facilities described below.  This will give you a much
- * more efficient implementation (no cross-process communication needed) and allow
- * you to avoid thinking about any security issues related to other applications
- * being able to receive or send your broadcasts.
+ * Base class for code that receives and handles broadcast intents sent by
+ * {@link android.content.Context#sendBroadcast(Intent)}.
  *
  * <p>You can either dynamically register an instance of this class with
  * {@link Context#registerReceiver Context.registerReceiver()}
- * or statically publish an implementation through the
+ * or statically declare an implementation with the
  * {@link android.R.styleable#AndroidManifestReceiver &lt;receiver&gt;}
  * tag in your <code>AndroidManifest.xml</code>.
- * 
- * <p><em><strong>Note:</strong></em>
- * &nbsp;&nbsp;&nbsp;If registering a receiver in your
- * {@link android.app.Activity#onResume() Activity.onResume()}
- * implementation, you should unregister it in 
- * {@link android.app.Activity#onPause() Activity.onPause()}.
- * (You won't receive intents when paused, 
- * and this will cut down on unnecessary system overhead). Do not unregister in 
- * {@link android.app.Activity#onSaveInstanceState(android.os.Bundle) Activity.onSaveInstanceState()},
- * because this won't be called if the user moves back in the history
- * stack.
- * 
- * <p>There are two major classes of broadcasts that can be received:</p>
- * <ul>
- * <li> <b>Normal broadcasts</b> (sent with {@link Context#sendBroadcast(Intent)
- * Context.sendBroadcast}) are completely asynchronous.  All receivers of the
- * broadcast are run in an undefined order, often at the same time.  This is
- * more efficient, but means that receivers cannot use the result or abort
- * APIs included here.
- * <li> <b>Ordered broadcasts</b> (sent with {@link Context#sendOrderedBroadcast(Intent, String)
- * Context.sendOrderedBroadcast}) are delivered to one receiver at a time.
- * As each receiver executes in turn, it can propagate a result to the next
- * receiver, or it can completely abort the broadcast so that it won't be passed
- * to other receivers.  The order receivers run in can be controlled with the
- * {@link android.R.styleable#AndroidManifestIntentFilter_priority
- * android:priority} attribute of the matching intent-filter; receivers with
- * the same priority will be run in an arbitrary order.
- * </ul>
- * 
- * <p>Even in the case of normal broadcasts, the system may in some
- * situations revert to delivering the broadcast one receiver at a time.  In
- * particular, for receivers that may require the creation of a process, only
- * one will be run at a time to avoid overloading the system with new processes.
- * In this situation, however, the non-ordered semantics hold: these receivers still
- * cannot return results or abort their broadcast.</p>
- * 
- * <p>Note that, although the Intent class is used for sending and receiving
- * these broadcasts, the Intent broadcast mechanism here is completely separate
- * from Intents that are used to start Activities with
- * {@link Context#startActivity Context.startActivity()}.
- * There is no way for a BroadcastReceiver
- * to see or capture Intents used with startActivity(); likewise, when
- * you broadcast an Intent, you will never find or start an Activity.
- * These two operations are semantically very different: starting an
- * Activity with an Intent is a foreground operation that modifies what the
- * user is currently interacting with; broadcasting an Intent is a background
- * operation that the user is not normally aware of.
- * 
- * <p>The BroadcastReceiver class (when launched as a component through
- * a manifest's {@link android.R.styleable#AndroidManifestReceiver &lt;receiver&gt;}
- * tag) is an important part of an
- * <a href="{@docRoot}guide/topics/fundamentals.html#lcycles">application's overall lifecycle</a>.</p>
- * 
- * <p>Topics covered here:
- * <ol>
- * <li><a href="#Security">Security</a>
- * <li><a href="#ReceiverLifecycle">Receiver Lifecycle</a>
- * <li><a href="#ProcessLifecycle">Process Lifecycle</a>
- * </ol>
  *
  * <div class="special reference">
  * <h3>Developer Guides</h3>
- * <p>For information about how to use this class to receive and resolve intents, read the
- * <a href="{@docRoot}guide/topics/intents/intents-filters.html">Intents and Intent Filters</a>
- * developer guide.</p>
- * </div>
+ * <p>For more information about using BroadcastReceiver, read the
+ * <a href="{@docRoot}guide/components/broadcasts.html">Broadcasts</a> developer guide.</p></div>
  *
- * <a name="Security"></a>
- * <h3>Security</h3>
- *
- * <p>Receivers used with the {@link Context} APIs are by their nature a
- * cross-application facility, so you must consider how other applications
- * may be able to abuse your use of them.  Some things to consider are:
- *
- * <ul>
- * <li><p>The Intent namespace is global.  Make sure that Intent action names and
- * other strings are written in a namespace you own, or else you may inadvertently
- * conflict with other applications.
- * <li><p>When you use {@link Context#registerReceiver(BroadcastReceiver, IntentFilter)},
- * <em>any</em> application may send broadcasts to that registered receiver.  You can
- * control who can send broadcasts to it through permissions described below.
- * <li><p>When you publish a receiver in your application's manifest and specify
- * intent-filters for it, any other application can send broadcasts to it regardless
- * of the filters you specify.  To prevent others from sending to it, make it
- * unavailable to them with <code>android:exported="false"</code>.
- * <li><p>When you use {@link Context#sendBroadcast(Intent)} or related methods,
- * normally any other application can receive these broadcasts.  You can control who
- * can receive such broadcasts through permissions described below.  Alternatively,
- * starting with {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH}, you
- * can also safely restrict the broadcast to a single application with
- * {@link Intent#setPackage(String) Intent.setPackage}
- * </ul>
- *
- * <p>None of these issues exist when using
- * {@link android.support.v4.content.LocalBroadcastManager}, since intents
- * broadcast it never go outside of the current process.
- *
- * <p>Access permissions can be enforced by either the sender or receiver
- * of a broadcast.
- *
- * <p>To enforce a permission when sending, you supply a non-null
- * <var>permission</var> argument to
- * {@link Context#sendBroadcast(Intent, String)} or
- * {@link Context#sendOrderedBroadcast(Intent, String, BroadcastReceiver, android.os.Handler, int, String, Bundle)}.
- * Only receivers who have been granted this permission
- * (by requesting it with the
- * {@link android.R.styleable#AndroidManifestUsesPermission &lt;uses-permission&gt;}
- * tag in their <code>AndroidManifest.xml</code>) will be able to receive
- * the broadcast.
- *
- * <p>To enforce a permission when receiving, you supply a non-null
- * <var>permission</var> when registering your receiver -- either when calling
- * {@link Context#registerReceiver(BroadcastReceiver, IntentFilter, String, android.os.Handler)}
- * or in the static
- * {@link android.R.styleable#AndroidManifestReceiver &lt;receiver&gt;}
- * tag in your <code>AndroidManifest.xml</code>.  Only broadcasters who have
- * been granted this permission (by requesting it with the
- * {@link android.R.styleable#AndroidManifestUsesPermission &lt;uses-permission&gt;}
- * tag in their <code>AndroidManifest.xml</code>) will be able to send an
- * Intent to the receiver.
- *
- * <p>See the <a href="{@docRoot}guide/topics/security/security.html">Security and Permissions</a>
- * document for more information on permissions and security in general.
- *
- * <a name="ReceiverLifecycle"></a>
- * <h3>Receiver Lifecycle</h3>
- * 
- * <p>A BroadcastReceiver object is only valid for the duration of the call
- * to {@link #onReceive}.  Once your code returns from this function,
- * the system considers the object to be finished and no longer active.
- * 
- * <p>This has important repercussions to what you can do in an
- * {@link #onReceive} implementation: anything that requires asynchronous
- * operation is not available, because you will need to return from the
- * function to handle the asynchronous operation, but at that point the
- * BroadcastReceiver is no longer active and thus the system is free to kill
- * its process before the asynchronous operation completes.
- * 
- * <p>In particular, you may <i>not</i> show a dialog or bind to a service from
- * within a BroadcastReceiver.  For the former, you should instead use the
- * {@link android.app.NotificationManager} API.  For the latter, you can
- * use {@link android.content.Context#startService Context.startService()} to
- * send a command to the service.
- *
- * <a name="ProcessLifecycle"></a>
- * <h3>Process Lifecycle</h3>
- * 
- * <p>A process that is currently executing a BroadcastReceiver (that is,
- * currently running the code in its {@link #onReceive} method) is
- * considered to be a foreground process and will be kept running by the
- * system except under cases of extreme memory pressure.
- * 
- * <p>Once you return from onReceive(), the BroadcastReceiver is no longer
- * active, and its hosting process is only as important as any other application
- * components that are running in it.  This is especially important because if
- * that process was only hosting the BroadcastReceiver (a common case for
- * applications that the user has never or not recently interacted with), then
- * upon returning from onReceive() the system will consider its process
- * to be empty and aggressively kill it so that resources are available for other
- * more important processes.
- * 
- * <p>This means that for longer-running operations you will often use
- * a {@link android.app.Service} in conjunction with a BroadcastReceiver to keep
- * the containing process active for the entire time of your operation.
  */
 public abstract class BroadcastReceiver {
     private PendingResult mPendingResult;
     private boolean mDebugUnregister;
-    
+
     /**
      * State for a result that is pending for a broadcast receiver.  Returned
      * by {@link BroadcastReceiver#goAsync() goAsync()}
@@ -218,7 +54,7 @@
      * terminate; you must call {@link #finish()} once you are done with the
      * broadcast.  This allows you to process the broadcast off of the main
      * thread of your app.
-     * 
+     *
      * <p>Note on threading: the state inside of this class is not itself
      * thread-safe, however you can use it from any thread if you properly
      * sure that you do not have races.  Typically this means you will hand
@@ -232,14 +68,14 @@
         public static final int TYPE_REGISTERED = 1;
         /** @hide */
         public static final int TYPE_UNREGISTERED = 2;
-        
+
         final int mType;
         final boolean mOrderedHint;
         final boolean mInitialStickyHint;
         final IBinder mToken;
         final int mSendingUser;
         final int mFlags;
-        
+
         int mResultCode;
         String mResultData;
         Bundle mResultExtras;
@@ -259,7 +95,7 @@
             mSendingUser = userId;
             mFlags = flags;
         }
-        
+
         /**
          * Version of {@link BroadcastReceiver#setResultCode(int)
          * BroadcastReceiver.setResultCode(int)} for
@@ -331,7 +167,7 @@
             mResultData = data;
             mResultExtras = extras;
         }
-     
+
         /**
          * Version of {@link BroadcastReceiver#getAbortBroadcast()
          * BroadcastReceiver.getAbortBroadcast()} for
@@ -350,7 +186,7 @@
             checkSynchronousHint();
             mAbortBroadcast = true;
         }
-        
+
         /**
          * Version of {@link BroadcastReceiver#clearAbortBroadcast()
          * BroadcastReceiver.clearAbortBroadcast()} for
@@ -359,7 +195,7 @@
         public final void clearAbortBroadcast() {
             mAbortBroadcast = false;
         }
-        
+
         /**
          * Finish the broadcast.  The current result will be sent and the
          * next broadcast will proceed.
@@ -397,14 +233,14 @@
                 sendFinished(mgr);
             }
         }
-        
+
         /** @hide */
         public void setExtrasClassLoader(ClassLoader cl) {
             if (mResultExtras != null) {
                 mResultExtras.setClassLoader(cl);
             }
         }
-        
+
         /** @hide */
         public void sendFinished(IActivityManager am) {
             synchronized (this) {
@@ -412,7 +248,7 @@
                     throw new IllegalStateException("Broadcast already finished");
                 }
                 mFinished = true;
-            
+
                 try {
                     if (mResultExtras != null) {
                         mResultExtras.setAllowFds(false);
@@ -448,7 +284,7 @@
             Log.e("BroadcastReceiver", e.getMessage(), e);
         }
     }
-    
+
     public BroadcastReceiver() {
     }
 
@@ -468,14 +304,15 @@
      *
      * <p><b>If this BroadcastReceiver was launched through a &lt;receiver&gt; tag,
      * then the object is no longer alive after returning from this
-     * function.</b>  This means you should not perform any operations that
-     * return a result to you asynchronously -- in particular, for interacting
-     * with services, you should use
-     * {@link Context#startService(Intent)} instead of
-     * {@link Context#bindService(Intent, ServiceConnection, int)}.  If you wish
-     * to interact with a service that is already running, you can use
-     * {@link #peekService}.
-     * 
+     * function.</b> This means you should not perform any operations that
+     * return a result to you asynchronously. If you need to perform any follow up
+     * background work, schedule a {@link android.app.job.JobService} with
+     * {@link android.app.job.JobScheduler}.
+     *
+     * If you wish to interact with a service that is already running and previously
+     * bound using {@link android.content.Context#bindService(Intent, ServiceConnection, int) bindService()},
+     * you can use {@link #peekService}.
+     *
      * <p>The Intent filters used in {@link android.content.Context#registerReceiver}
      * and in application manifests are <em>not</em> guaranteed to be exclusive. They
      * are hints to the operating system about how to find suitable recipients. It is
@@ -483,7 +320,7 @@
      * resolution.  For this reason, {@link #onReceive(Context, Intent) onReceive()}
      * implementations should respond only to known actions, ignoring any unexpected
      * Intents that they may receive.
-     * 
+     *
      * @param context The Context in which the receiver is running.
      * @param intent The Intent being received.
      */
@@ -496,7 +333,7 @@
      * responsive to the broadcast (finishing it within 10s), but does allow
      * the implementation to move work related to it over to another thread
      * to avoid glitching the main UI thread due to disk IO.
-     * 
+     *
      * @return Returns a {@link PendingResult} representing the result of
      * the active broadcast.  The BroadcastRecord itself is no longer active;
      * all data and other interaction must go through {@link PendingResult}
@@ -508,15 +345,20 @@
         mPendingResult = null;
         return res;
     }
-    
+
     /**
-     * Provide a binder to an already-running service.  This method is synchronous
+     * Provide a binder to an already-bound service.  This method is synchronous
      * and will not start the target service if it is not present, so it is safe
      * to call from {@link #onReceive}.
-     * 
+     *
+     * For peekService() to return a non null {@link android.os.IBinder} interface
+     * the service must have published it before. In other words some component
+     * must have called {@link android.content.Context#bindService(Intent, ServiceConnection, int)} on it.
+     *
      * @param myContext The Context that had been passed to {@link #onReceive(Context, Intent)}
-     * @param service The Intent indicating the service you wish to use.  See {@link
-     * Context#startService(Intent)} for more information.
+     * @param service Identifies the already-bound service you wish to use. See
+     * {@link android.content.Context#bindService(Intent, ServiceConnection, int)}
+     * for more information.
      */
     public IBinder peekService(Context myContext, Intent service) {
         IActivityManager am = ActivityManagerNative.getDefault();
@@ -538,13 +380,13 @@
      * Activity {@link android.app.Activity#RESULT_CANCELED} and
      * {@link android.app.Activity#RESULT_OK} constants, though the
      * actual meaning of this value is ultimately up to the broadcaster.
-     * 
+     *
      * <p class="note">This method does not work with non-ordered broadcasts such
      * as those sent with {@link Context#sendBroadcast(Intent)
      * Context.sendBroadcast}</p>
-     * 
+     *
      * @param code The new result code.
-     * 
+     *
      * @see #setResult(int, String, Bundle)
      */
     public final void setResultCode(int code) {
@@ -554,7 +396,7 @@
 
     /**
      * Retrieve the current result code, as set by the previous receiver.
-     * 
+     *
      * @return int The current result code.
      */
     public final int getResultCode() {
@@ -567,13 +409,13 @@
      * {@link Context#sendOrderedBroadcast(Intent, String)
      * Context.sendOrderedBroadcast}.  This is an arbitrary
      * string whose interpretation is up to the broadcaster.
-     * 
+     *
      * <p><strong>This method does not work with non-ordered broadcasts such
      * as those sent with {@link Context#sendBroadcast(Intent)
      * Context.sendBroadcast}</strong></p>
-     * 
+     *
      * @param data The new result data; may be null.
-     * 
+     *
      * @see #setResult(int, String, Bundle)
      */
     public final void setResultData(String data) {
@@ -584,7 +426,7 @@
     /**
      * Retrieve the current result data, as set by the previous receiver.
      * Often this is null.
-     * 
+     *
      * @return String The current result data; may be null.
      */
     public final String getResultData() {
@@ -599,13 +441,13 @@
      * holding arbitrary data, whose interpretation is up to the
      * broadcaster.  Can be set to null.  Calling this method completely
      * replaces the current map (if any).
-     * 
+     *
      * <p><strong>This method does not work with non-ordered broadcasts such
      * as those sent with {@link Context#sendBroadcast(Intent)
      * Context.sendBroadcast}</strong></p>
-     * 
+     *
      * @param extras The new extra data map; may be null.
-     * 
+     *
      * @see #setResult(int, String, Bundle)
      */
     public final void setResultExtras(Bundle extras) {
@@ -617,11 +459,11 @@
      * Retrieve the current result extra data, as set by the previous receiver.
      * Any changes you make to the returned Map will be propagated to the next
      * receiver.
-     * 
+     *
      * @param makeMap If true then a new empty Map will be made for you if the
      *                current Map is null; if false you should be prepared to
      *                receive a null Map.
-     * 
+     *
      * @return Map The current extras map.
      */
     public final Bundle getResultExtras(boolean makeMap) {
@@ -640,11 +482,11 @@
      * {@link Context#sendOrderedBroadcast(Intent, String)
      * Context.sendOrderedBroadcast}.  All current result data is replaced
      * by the value given to this method.
-     * 
+     *
      * <p><strong>This method does not work with non-ordered broadcasts such
      * as those sent with {@link Context#sendBroadcast(Intent)
      * Context.sendBroadcast}</strong></p>
-     * 
+     *
      * @param code The new result code.  Often uses the
      * Activity {@link android.app.Activity#RESULT_CANCELED} and
      * {@link android.app.Activity#RESULT_OK} constants, though the
@@ -662,11 +504,11 @@
         mPendingResult.mResultData = data;
         mPendingResult.mResultExtras = extras;
     }
- 
+
     /**
      * Returns the flag indicating whether or not this receiver should
      * abort the current broadcast.
-     * 
+     *
      * @return True if the broadcast should be aborted.
      */
     public final boolean getAbortBroadcast() {
@@ -679,10 +521,10 @@
      * {@link Context#sendOrderedBroadcast(Intent, String)
      * Context.sendOrderedBroadcast}.  This will prevent
      * any other broadcast receivers from receiving the broadcast. It will still
-     * call {@link #onReceive} of the BroadcastReceiver that the caller of 
+     * call {@link #onReceive} of the BroadcastReceiver that the caller of
      * {@link Context#sendOrderedBroadcast(Intent, String)
      * Context.sendOrderedBroadcast} passed in.
-     * 
+     *
      * <p><strong>This method does not work with non-ordered broadcasts such
      * as those sent with {@link Context#sendBroadcast(Intent)
      * Context.sendBroadcast}</strong></p>
@@ -691,7 +533,7 @@
         checkSynchronousHint();
         mPendingResult.mAbortBroadcast = true;
     }
-    
+
     /**
      * Clears the flag indicating that this receiver should abort the current
      * broadcast.
@@ -701,7 +543,7 @@
             mPendingResult.mAbortBroadcast = false;
         }
     }
-    
+
     /**
      * Returns true if the receiver is currently processing an ordered
      * broadcast.
@@ -709,7 +551,7 @@
     public final boolean isOrderedBroadcast() {
         return mPendingResult != null ? mPendingResult.mOrderedHint : false;
     }
-    
+
     /**
      * Returns true if the receiver is currently processing the initial
      * value of a sticky broadcast -- that is, the value that was last
@@ -719,7 +561,7 @@
     public final boolean isInitialStickyBroadcast() {
         return mPendingResult != null ? mPendingResult.mInitialStickyHint : false;
     }
-    
+
     /**
      * For internal use, sets the hint about whether this BroadcastReceiver is
      * running in ordered mode.
@@ -727,21 +569,21 @@
     public final void setOrderedHint(boolean isOrdered) {
         // Accidentally left in the SDK.
     }
-    
+
     /**
      * For internal use to set the result data that is active. @hide
      */
     public final void setPendingResult(PendingResult result) {
         mPendingResult = result;
     }
-    
+
     /**
      * For internal use to set the result data that is active. @hide
      */
     public final PendingResult getPendingResult() {
         return mPendingResult;
     }
-    
+
     /** @hide */
     public int getSendingUserId() {
         return mPendingResult.mSendingUser;
@@ -761,19 +603,19 @@
     public final void setDebugUnregister(boolean debug) {
         mDebugUnregister = debug;
     }
-    
+
     /**
      * Return the last value given to {@link #setDebugUnregister}.
      */
     public final boolean getDebugUnregister() {
         return mDebugUnregister;
     }
-    
+
     void checkSynchronousHint() {
         if (mPendingResult == null) {
             throw new IllegalStateException("Call while result is not pending");
         }
-        
+
         // Note that we don't assert when receiving the initial sticky value,
         // since that may have come from an ordered broadcast.  We'll catch
         // them later when the real broadcast happens again.
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 3a2f471..da64c3a 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -2474,9 +2474,8 @@
      * for high frequency calls.
      * </p>
      *
-     * @param service Identifies the service to be started.  The Intent must be either
-     *      fully explicit (supplying a component name) or specify a specific package
-     *      name it is targetted to.  Additional values
+     * @param service Identifies the service to be started.  The Intent must be
+     *      fully explicit (supplying a component name).  Additional values
      *      may be included in the Intent extras to supply arguments along with
      *      this specific start call.
      *
@@ -2554,10 +2553,8 @@
      * {@link #registerReceiver}, since the lifetime of this BroadcastReceiver
      * is tied to another object (the one that registered it).</p>
      *
-     * @param service Identifies the service to connect to.  The Intent may
-     *      specify either an explicit component name, or a logical
-     *      description (action, category, etc) to match an
-     *      {@link IntentFilter} published by a service.
+     * @param service Identifies the service to connect to.  The Intent must
+     *      specify an explicit component name.
      * @param conn Receives information as the service is started and stopped.
      *      This must be a valid ServiceConnection object; it must not be null.
      * @param flags Operation options for the binding.  May be 0,
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index a75f1116..39133b8 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -3291,7 +3291,8 @@
      * Grant a runtime permission to an application which the application does not
      * already have. The permission must have been requested by the application.
      * If the application is not allowed to hold the permission, a {@link
-     * java.lang.SecurityException} is thrown.
+     * java.lang.SecurityException} is thrown. If the package or permission is
+     * invalid, a {@link java.lang.IllegalArgumentException} is thrown.
      * <p>
      * <strong>Note: </strong>Using this API requires holding
      * android.permission.GRANT_REVOKE_PERMISSIONS and if the user id is
@@ -3316,7 +3317,8 @@
      * #grantRuntimePermission(String, String, android.os.UserHandle)}. The
      * permission must have been requested by and granted to the application.
      * If the application is not allowed to hold the permission, a {@link
-     * java.lang.SecurityException} is thrown.
+     * java.lang.SecurityException} is thrown. If the package or permission is
+     * invalid, a {@link java.lang.IllegalArgumentException} is thrown.
      * <p>
      * <strong>Note: </strong>Using this API requires holding
      * android.permission.GRANT_REVOKE_PERMISSIONS and if the user id is
diff --git a/core/java/android/content/pm/PackageManagerInternal.java b/core/java/android/content/pm/PackageManagerInternal.java
index f5bcf64..9bb2128 100644
--- a/core/java/android/content/pm/PackageManagerInternal.java
+++ b/core/java/android/content/pm/PackageManagerInternal.java
@@ -168,4 +168,17 @@
      * @return Whether was launched.
      */
     public abstract boolean wasPackageEverLaunched(String packageName, int userId);
+
+    /**
+     * Retrieve the official name associated with a user id.  This name is
+     * guaranteed to never change, though it is possible for the underlying
+     * user id to be changed.  That is, if you are storing information about
+     * user ids in persistent storage, you should use the string returned
+     * by this function instead of the raw user-id.
+     *
+     * @param uid The user id for which you would like to retrieve a name.
+     * @return Returns a unique name for the given user id, or null if the
+     * user id is not currently assigned.
+     */
+    public abstract String getNameForUid(int uid);
 }
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index 0afb546..2b5afa7 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -87,6 +87,13 @@
      * sent as an extra; it should be consulted to see what kind of
      * connectivity event occurred.
      * <p/>
+     * Apps targeting Android 7.0 (API level 24) and higher do not receive this
+     * broadcast if they declare the broadcast receiver in their manifest. Apps
+     * will still receive broadcasts if they register their
+     * {@link android.content.BroadcastReceiver} with
+     * {@link android.content.Context#registerReceiver Context.registerReceiver()}
+     * and that context is still valid.
+     * <p/>
      * If this is a connection that was the result of failing over from a
      * disconnected network, then the FAILOVER_CONNECTION boolean extra is
      * set to true.
@@ -223,6 +230,13 @@
     public static final String EXTRA_CAPTIVE_PORTAL_URL = "android.net.extra.CAPTIVE_PORTAL_URL";
 
     /**
+     * Key for passing a user agent string to the captive portal login activity.
+     * {@hide}
+     */
+    public static final String EXTRA_CAPTIVE_PORTAL_USER_AGENT =
+            "android.net.extra.CAPTIVE_PORTAL_USER_AGENT";
+
+    /**
      * Broadcast action to indicate the change of data activity status
      * (idle or active) on a network in a recent period.
      * The network becomes active when data transmission is started, or
diff --git a/core/java/android/net/IIpConnectivityMetrics.aidl b/core/java/android/net/IIpConnectivityMetrics.aidl
index d36b766..6f07b31 100644
--- a/core/java/android/net/IIpConnectivityMetrics.aidl
+++ b/core/java/android/net/IIpConnectivityMetrics.aidl
@@ -18,6 +18,7 @@
 
 import android.os.Parcelable;
 import android.net.ConnectivityMetricsEvent;
+import android.net.INetdEventCallback;
 
 /** {@hide} */
 interface IIpConnectivityMetrics {
@@ -27,4 +28,13 @@
      * or -1 if the event was dropped due to rate limiting.
      */
     int logEvent(in ConnectivityMetricsEvent event);
+
+    /**
+     * At most one callback can be registered (by DevicePolicyManager).
+     * @return status {@code true} if registering/unregistering of the callback was successful,
+     *         {@code false} otherwise (might happen if IIpConnectivityMetrics is not available,
+     *         if it happens make sure you call it when the service is up in the caller)
+     */
+    boolean registerNetdEventCallback(in INetdEventCallback callback);
+    boolean unregisterNetdEventCallback();
 }
diff --git a/core/java/android/net/INetdEventCallback.aidl b/core/java/android/net/INetdEventCallback.aidl
new file mode 100644
index 0000000..49436be
--- /dev/null
+++ b/core/java/android/net/INetdEventCallback.aidl
@@ -0,0 +1,47 @@
+/*
+ * 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.net;
+
+/** {@hide} */
+oneway interface INetdEventCallback {
+
+    /**
+     * Reports a single DNS lookup function call.
+     * This method must not block or perform long-running operations.
+     *
+     * @param hostname the name that was looked up.
+     * @param ipAddresses (possibly a subset of) the IP addresses returned.
+     *        At most {@link #DNS_REPORTED_IP_ADDRESSES_LIMIT} addresses are logged.
+     * @param ipAddressesCount the number of IP addresses returned. May be different from the length
+     *        of ipAddresses if there were too many addresses to log.
+     * @param timestamp the timestamp at which the query was reported by netd.
+     * @param uid the UID of the application that performed the query.
+     */
+    void onDnsEvent(String hostname, in String[] ipAddresses, int ipAddressesCount, long timestamp,
+            int uid);
+
+    /**
+     * Reports a single connect library call.
+     * This method must not block or perform long-running operations.
+     *
+     * @param ipAddr destination IP address.
+     * @param port destination port number.
+     * @param timestamp the timestamp at which the call was reported by netd.
+     * @param uid the UID of the application that performed the connection.
+     */
+    void onConnectEvent(String ipAddr, int port, long timestamp, int uid);
+}
diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java
index 35e3065..a677d73 100644
--- a/core/java/android/net/NetworkUtils.java
+++ b/core/java/android/net/NetworkUtils.java
@@ -52,6 +52,17 @@
     public native static void attachRaFilter(FileDescriptor fd, int packetType) throws SocketException;
 
     /**
+     * Attaches a socket filter that accepts L2-L4 signaling traffic required for IP connectivity.
+     *
+     * This includes: all ARP, ICMPv6 RS/RA/NS/NA messages, and DHCPv4 exchanges.
+     *
+     * @param fd the socket's {@link FileDescriptor}.
+     * @param packetType the hardware address type, one of ARPHRD_*.
+     */
+    public native static void attachControlPacketFilter(FileDescriptor fd, int packetType)
+            throws SocketException;
+
+    /**
      * Configures a socket for receiving ICMPv6 router solicitations and sending advertisements.
      * @param fd the socket's {@link FileDescriptor}.
      * @param ifIndex the interface index.
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index a0c2efd..0bb05b5 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -182,7 +182,7 @@
      * New in version 19:
      *   - Wakelock data (wl) gets current and max times.
      */
-    static final String CHECKIN_VERSION = "19";
+    static final String CHECKIN_VERSION = "20";
 
     /**
      * Old version, we hit 9 and ran out of room, need to remove.
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index a36127d..c3ae9d9 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -275,6 +275,7 @@
     private TypedArray mWindowStyle;
     private Callback mCallback;
     private OnWindowDismissedCallback mOnWindowDismissedCallback;
+    private OnWindowSwipeDismissedCallback mOnWindowSwipeDismissedCallback;
     private WindowControllerCallback mWindowControllerCallback;
     private OnRestrictedCaptionAreaChangedListener mOnRestrictedCaptionAreaChangedListener;
     private Rect mRestrictedCaptionAreaRect;
@@ -588,6 +589,18 @@
     }
 
     /** @hide */
+    public interface OnWindowSwipeDismissedCallback {
+        /**
+         * Called when a window is swipe dismissed. This informs the callback that the
+         * window is gone, and it should finish itself.
+         * @param finishTask True if the task should also be finished.
+         * @param suppressWindowTransition True if the resulting exit and enter window transition
+         * animations should be suppressed.
+         */
+        void onWindowSwipeDismissed();
+    }
+
+    /** @hide */
     public interface WindowControllerCallback {
         /**
          * Moves the activity from
@@ -881,6 +894,18 @@
     }
 
     /** @hide */
+    public final void setOnWindowSwipeDismissedCallback(OnWindowSwipeDismissedCallback sdcb) {
+        mOnWindowSwipeDismissedCallback = sdcb;
+    }
+
+    /** @hide */
+    public final void dispatchOnWindowSwipeDismissed() {
+        if (mOnWindowSwipeDismissedCallback != null) {
+            mOnWindowSwipeDismissedCallback.onWindowSwipeDismissed();
+        }
+    }
+
+    /** @hide */
     public final void setWindowControllerCallback(WindowControllerCallback wccb) {
         mWindowControllerCallback = wccb;
     }
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index 2dfa8cd..286f5f7 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -353,7 +353,18 @@
                 return;
             }
             if (!mIsEnabled) {
-                throw new IllegalStateException("Accessibility off. Did you forget to check that?");
+                Looper myLooper = Looper.myLooper();
+                if (myLooper == Looper.getMainLooper()) {
+                    throw new IllegalStateException(
+                            "Accessibility off. Did you forget to check that?");
+                } else {
+                    // If we're not running on the thread with the main looper, it's possible for
+                    // the state of accessibility to change between checking isEnabled and
+                    // calling this method. So just log the error rather than throwing the
+                    // exception.
+                    Log.e(LOG_TAG, "Interrupt called with accessibility disabled");
+                    return;
+                }
             }
             userId = mUserId;
         }
diff --git a/core/java/android/widget/Toast.java b/core/java/android/widget/Toast.java
index a400d90..4d9192b 100644
--- a/core/java/android/widget/Toast.java
+++ b/core/java/android/widget/Toast.java
@@ -100,13 +100,13 @@
      */
     public Toast(Context context) {
         mContext = context;
-        mTN = new TN();
+        mTN = new TN(context.getPackageName());
         mTN.mY = context.getResources().getDimensionPixelSize(
                 com.android.internal.R.dimen.toast_y_offset);
         mTN.mGravity = context.getResources().getInteger(
                 com.android.internal.R.integer.config_toastDefaultGravity);
     }
-    
+
     /**
      * Show the view for the specified duration.
      */
@@ -133,15 +133,9 @@
      * after the appropriate duration.
      */
     public void cancel() {
-        mTN.hide();
-
-        try {
-            getService().cancelToast(mContext.getPackageName(), mTN);
-        } catch (RemoteException e) {
-            // Empty
-        }
+        mTN.cancel();
     }
-    
+
     /**
      * Set the view to show.
      * @see #getView
@@ -331,18 +325,40 @@
         final Runnable mHide = new Runnable() {
             @Override
             public void run() {
-                handleHide();
-                // Don't do this in handleHide() because it is also invoked by handleShow()
-                mNextView = null;
             }
         };
 
         private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
+
+        private static final int SHOW = 0;
+        private static final int HIDE = 1;
+        private static final int CANCEL = 2;
         final Handler mHandler = new Handler() {
             @Override
             public void handleMessage(Message msg) {
-                IBinder token = (IBinder) msg.obj;
-                handleShow(token);
+                switch (msg.what) {
+                    case SHOW: {
+                        IBinder token = (IBinder) msg.obj;
+                        handleShow(token);
+                        break;
+                    }
+                    case HIDE: {
+                        handleHide();
+                        // Don't do this in handleHide() because it is also invoked by handleShow()
+                        mNextView = null;
+                        break;
+                    }
+                    case CANCEL: {
+                        handleHide();
+                        // Don't do this in handleHide() because it is also invoked by handleShow()
+                        mNextView = null;
+                        try {
+                            getService().cancelToast(mPackageName, TN.this);
+                        } catch (RemoteException e) {
+                        }
+                        break;
+                    }
+                }
             }
         };
 
@@ -358,10 +374,12 @@
 
         WindowManager mWM;
 
+        String mPackageName;
+
         static final long SHORT_DURATION_TIMEOUT = 5000;
         static final long LONG_DURATION_TIMEOUT = 1000;
 
-        TN() {
+        TN(String packageName) {
             // XXX This should be changed to use a Dialog, with a Theme.Toast
             // defined that sets up the layout params appropriately.
             final WindowManager.LayoutParams params = mParams;
@@ -374,6 +392,8 @@
             params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                     | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                     | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+
+            mPackageName = packageName;
         }
 
         /**
@@ -382,7 +402,7 @@
         @Override
         public void show(IBinder windowToken) {
             if (localLOGV) Log.v(TAG, "SHOW: " + this);
-            mHandler.obtainMessage(0, windowToken).sendToTarget();
+            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
         }
 
         /**
@@ -391,7 +411,12 @@
         @Override
         public void hide() {
             if (localLOGV) Log.v(TAG, "HIDE: " + this);
-            mHandler.post(mHide);
+            mHandler.obtainMessage(HIDE).sendToTarget();
+        }
+
+        public void cancel() {
+            if (localLOGV) Log.v(TAG, "CANCEL: " + this);
+            mHandler.obtainMessage(CANCEL).sendToTarget();
         }
 
         public void handleShow(IBinder windowToken) {
diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index 3b3344e..da13a04 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -108,7 +108,7 @@
     private static final int MAGIC = 0xBA757475; // 'BATSTATS'
 
     // Current on-disk Parcel version
-    private static final int VERSION = 150 + (USE_OLD_HISTORY ? 1000 : 0);
+    private static final int VERSION = 151 + (USE_OLD_HISTORY ? 1000 : 0);
 
     // Maximum number of items we will record in the history.
     private static final int MAX_HISTORY_ITEMS = 2000;
@@ -1593,7 +1593,7 @@
         @Override
         public void writeToParcel(Parcel out, long elapsedRealtimeUs) {
             super.writeToParcel(out, elapsedRealtimeUs);
-            out.writeLong(mMaxDurationMs);
+            out.writeLong(getMaxDurationMsLocked(elapsedRealtimeUs / 1000));
         }
 
         /**
@@ -1606,7 +1606,7 @@
         @Override
         public void writeSummaryFromParcelLocked(Parcel out, long elapsedRealtimeUs) {
             super.writeSummaryFromParcelLocked(out, elapsedRealtimeUs);
-            out.writeLong(mMaxDurationMs);
+            out.writeLong(getMaxDurationMsLocked(elapsedRealtimeUs / 1000));
         }
 
         /**
@@ -1630,7 +1630,7 @@
         public void onTimeStarted(long elapsedRealtimeUs, long baseUptime, long baseRealtime) {
             super.onTimeStarted(elapsedRealtimeUs, baseUptime, baseRealtime);
             if (mNesting > 0) {
-                mStartTimeMs = mTimeBase.getRealtime(mClocks.elapsedRealtime()*1000) / 1000;
+                mStartTimeMs = baseRealtime / 1000;
             }
         }
 
@@ -1640,10 +1640,11 @@
          * If the timer is running, add the duration into mCurrentDurationMs.
          */
         @Override
-        public void onTimeStopped(long elapsedRealtimeUs, long baseUptime, long baseRealtime) {
-            super.onTimeStopped(elapsedRealtimeUs, baseUptime, baseRealtime);
+        public void onTimeStopped(long elapsedRealtimeUs, long baseUptime, long baseRealtimeUs) {
+            super.onTimeStopped(elapsedRealtimeUs, baseUptime, baseRealtimeUs);
             if (mNesting > 0) {
-                mCurrentDurationMs += (elapsedRealtimeUs / 1000) - mStartTimeMs;
+                // baseRealtimeUs has already been converted to the timebase's realtime.
+                mCurrentDurationMs += (baseRealtimeUs / 1000) - mStartTimeMs;
             }
             mStartTimeMs = -1;
         }
@@ -1658,7 +1659,7 @@
             super.startRunningLocked(elapsedRealtimeMs);
             if (mNesting == 1 && mTimeBase.isRunning()) {
                 // Just started
-                mStartTimeMs = mTimeBase.getRealtime(mClocks.elapsedRealtime()*1000) / 1000;
+                mStartTimeMs = mTimeBase.getRealtime(elapsedRealtimeMs * 1000) / 1000;
             }
         }
 
@@ -1670,8 +1671,7 @@
          */
         @Override
         public void stopRunningLocked(long elapsedRealtimeMs) {
-            super.stopRunningLocked(elapsedRealtimeMs);
-            if (mNesting == 0) {
+            if (mNesting == 1) {
                 final long durationMs = getCurrentDurationMsLocked(elapsedRealtimeMs);
                 if (durationMs > mMaxDurationMs) {
                     mMaxDurationMs = durationMs;
@@ -1679,6 +1679,9 @@
                 mStartTimeMs = -1;
                 mCurrentDurationMs = 0;
             }
+            // super method decrements mNesting, which getCurrentDurationMsLocked relies on,
+            // so call super.stopRunningLocked after calling getCurrentDurationMsLocked.
+            super.stopRunningLocked(elapsedRealtimeMs);
         }
 
         @Override
@@ -1720,11 +1723,9 @@
         @Override
         public long getCurrentDurationMsLocked(long elapsedRealtimeMs) {
             long durationMs = mCurrentDurationMs;
-            if (mNesting > 0) {
-                if (mTimeBase.isRunning()) {
-                    durationMs += (mTimeBase.getRealtime(elapsedRealtimeMs*1000)/1000)
-                            - mStartTimeMs;
-                }
+            if (mNesting > 0 && mTimeBase.isRunning()) {
+                durationMs += (mTimeBase.getRealtime(elapsedRealtimeMs*1000)/1000)
+                        - mStartTimeMs;
             }
             return durationMs;
         }
@@ -2877,8 +2878,22 @@
             mHistoryLastWritten.setTo(mHistoryLastLastWritten);
         }
 
+        boolean recordResetDueToOverflow = false;
         final int dataSize = mHistoryBuffer.dataSize();
-        if (dataSize >= MAX_HISTORY_BUFFER) {
+        if (dataSize >= MAX_MAX_HISTORY_BUFFER*3) {
+            // Clients can't deal with history buffers this large. This only
+            // really happens when the device is on charger and interacted with
+            // for long periods of time, like in retail mode. Since the device is
+            // most likely charged, when unplugged, stats would have reset anyways.
+            // Reset the stats and mark that we overflowed.
+            // b/32540341
+            resetAllStatsLocked();
+
+            // Mark that we want to set *OVERFLOW* event and the RESET:START
+            // events.
+            recordResetDueToOverflow = true;
+
+        } else if (dataSize >= MAX_HISTORY_BUFFER) {
             if (!mHistoryOverflow) {
                 mHistoryOverflow = true;
                 addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_UPDATE, cur);
@@ -2924,9 +2939,12 @@
             return;
         }
 
-        if (dataSize == 0) {
+        if (dataSize == 0 || recordResetDueToOverflow) {
             // The history is currently empty; we need it to start with a time stamp.
             cur.currentTime = System.currentTimeMillis();
+            if (recordResetDueToOverflow) {
+                addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_OVERFLOW, cur);
+            }
             addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_RESET, cur);
         }
         addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_UPDATE, cur);
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 5d25b1b..920a6cb 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -2990,6 +2990,7 @@
         swipeDismiss.setOnDismissedListener(new SwipeDismissLayout.OnDismissedListener() {
             @Override
             public void onDismissed(SwipeDismissLayout layout) {
+                dispatchOnWindowSwipeDismissed();
                 dispatchOnWindowDismissed(false /*finishTask*/, true /*suppressWindowTransition*/);
             }
         });
diff --git a/core/java/com/android/internal/util/TokenBucket.java b/core/java/com/android/internal/util/TokenBucket.java
index effb82ba..a163ceb 100644
--- a/core/java/com/android/internal/util/TokenBucket.java
+++ b/core/java/com/android/internal/util/TokenBucket.java
@@ -33,6 +33,8 @@
  * The available amount of tokens is computed lazily when the bucket state is inspected.
  * Therefore it is purely synchronous and does not involve any asynchronous activity.
  * It is not synchronized in any way and not a thread-safe object.
+ *
+ * {@hide}
  */
 public class TokenBucket {
 
diff --git a/core/java/com/android/internal/widget/SwipeDismissLayout.java b/core/java/com/android/internal/widget/SwipeDismissLayout.java
index bdcfff2..e80d812 100644
--- a/core/java/com/android/internal/widget/SwipeDismissLayout.java
+++ b/core/java/com/android/internal/widget/SwipeDismissLayout.java
@@ -330,18 +330,16 @@
         mVelocityTracker.addMovement(ev);
         mVelocityTracker.computeCurrentVelocity(1000);
         if (!mDismissed) {
-
-            if (deltaX > (getWidth() * DISMISS_MIN_DRAG_WIDTH_RATIO) &&
-                    ev.getRawX() >= mLastX) {
+            if ((deltaX > (getWidth() * DISMISS_MIN_DRAG_WIDTH_RATIO) &&
+                    ev.getRawX() >= mLastX)
+                    || mVelocityTracker.getXVelocity() >= mMinFlingVelocity) {
                 mDismissed = true;
             }
         }
         // Check if the user tried to undo this.
         if (mDismissed && mSwiping) {
-            // Check if the user's finger is actually back
-            if (deltaX < (getWidth() * DISMISS_MIN_DRAG_WIDTH_RATIO) ||
-                    // or user is flinging back left
-                    mVelocityTracker.getXVelocity() < -mMinFlingVelocity) {
+            // Check if the user's finger is actually flinging back to left
+            if (mVelocityTracker.getXVelocity() < -mMinFlingVelocity) {
                 mDismissed = false;
             }
         }
diff --git a/core/jni/android_net_NetUtils.cpp b/core/jni/android_net_NetUtils.cpp
index 679e882..eb105d2 100644
--- a/core/jni/android_net_NetUtils.cpp
+++ b/core/jni/android_net_NetUtils.cpp
@@ -47,28 +47,33 @@
 
 namespace android {
 
+static const uint32_t kEtherTypeOffset = offsetof(ether_header, ether_type);
+static const uint32_t kEtherHeaderLen = sizeof(ether_header);
+static const uint32_t kIPv4Protocol = kEtherHeaderLen + offsetof(iphdr, protocol);
+static const uint32_t kIPv4FlagsOffset = kEtherHeaderLen + offsetof(iphdr, frag_off);
+static const uint32_t kIPv6NextHeader = kEtherHeaderLen + offsetof(ip6_hdr, ip6_nxt);
+static const uint32_t kIPv6PayloadStart = kEtherHeaderLen + sizeof(ip6_hdr);
+static const uint32_t kICMPv6TypeOffset = kIPv6PayloadStart + offsetof(icmp6_hdr, icmp6_type);
+static const uint32_t kUDPSrcPortIndirectOffset = kEtherHeaderLen + offsetof(udphdr, source);
+static const uint32_t kUDPDstPortIndirectOffset = kEtherHeaderLen + offsetof(udphdr, dest);
 static const uint16_t kDhcpClientPort = 68;
 
 static void android_net_utils_attachDhcpFilter(JNIEnv *env, jobject clazz, jobject javaFd)
 {
-    uint32_t ip_offset = sizeof(ether_header);
-    uint32_t proto_offset = ip_offset + offsetof(iphdr, protocol);
-    uint32_t flags_offset = ip_offset + offsetof(iphdr, frag_off);
-    uint32_t dport_indirect_offset = ip_offset + offsetof(udphdr, dest);
     struct sock_filter filter_code[] = {
         // Check the protocol is UDP.
-        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  proto_offset),
+        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  kIPv4Protocol),
         BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    IPPROTO_UDP, 0, 6),
 
         // Check this is not a fragment.
-        BPF_STMT(BPF_LD  | BPF_H    | BPF_ABS, flags_offset),
-        BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K,   0x1fff, 4, 0),
+        BPF_STMT(BPF_LD  | BPF_H    | BPF_ABS, kIPv4FlagsOffset),
+        BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K,   IP_OFFMASK, 4, 0),
 
         // Get the IP header length.
-        BPF_STMT(BPF_LDX | BPF_B    | BPF_MSH, ip_offset),
+        BPF_STMT(BPF_LDX | BPF_B    | BPF_MSH, kEtherHeaderLen),
 
         // Check the destination port.
-        BPF_STMT(BPF_LD  | BPF_H    | BPF_IND, dport_indirect_offset),
+        BPF_STMT(BPF_LD  | BPF_H    | BPF_IND, kUDPDstPortIndirectOffset),
         BPF_JUMP(BPF_JMP | BPF_JEQ  | BPF_K,   kDhcpClientPort, 0, 1),
 
         // Accept or reject.
@@ -96,17 +101,13 @@
         return;
     }
 
-    uint32_t ipv6_offset = sizeof(ether_header);
-    uint32_t ipv6_next_header_offset = ipv6_offset + offsetof(ip6_hdr, ip6_nxt);
-    uint32_t icmp6_offset = ipv6_offset + sizeof(ip6_hdr);
-    uint32_t icmp6_type_offset = icmp6_offset + offsetof(icmp6_hdr, icmp6_type);
     struct sock_filter filter_code[] = {
         // Check IPv6 Next Header is ICMPv6.
-        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  ipv6_next_header_offset),
+        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  kIPv6NextHeader),
         BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    IPPROTO_ICMPV6, 0, 3),
 
         // Check ICMPv6 type is Router Advertisement.
-        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  icmp6_type_offset),
+        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  kICMPv6TypeOffset),
         BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    ND_ROUTER_ADVERT, 0, 1),
 
         // Accept or reject.
@@ -125,6 +126,81 @@
     }
 }
 
+// TODO: Move all this filter code into libnetutils.
+static void android_net_utils_attachControlPacketFilter(
+        JNIEnv *env, jobject clazz, jobject javaFd, jint hardwareAddressType) {
+    if (hardwareAddressType != ARPHRD_ETHER) {
+        jniThrowExceptionFmt(env, "java/net/SocketException",
+                "attachControlPacketFilter only supports ARPHRD_ETHER");
+        return;
+    }
+
+    // Capture all:
+    //     - ARPs
+    //     - DHCPv4 packets
+    //     - Router Advertisements & Solicitations
+    //     - Neighbor Advertisements & Solicitations
+    //
+    // tcpdump:
+    //     arp or
+    //     '(ip and udp port 68)' or
+    //     '(icmp6 and ip6[40] >= 133 and ip6[40] <= 136)'
+    struct sock_filter filter_code[] = {
+        // Load the link layer next payload field.
+        BPF_STMT(BPF_LD  | BPF_H   | BPF_ABS,  kEtherTypeOffset),
+
+        // Accept all ARP.
+        // TODO: Figure out how to better filter ARPs on noisy networks.
+        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ETHERTYPE_ARP, 16, 0),
+
+        // If IPv4:
+        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ETHERTYPE_IP, 0, 9),
+
+        // Check the protocol is UDP.
+        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  kIPv4Protocol),
+        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    IPPROTO_UDP, 0, 14),
+
+        // Check this is not a fragment.
+        BPF_STMT(BPF_LD  | BPF_H    | BPF_ABS, kIPv4FlagsOffset),
+        BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K,   IP_OFFMASK, 12, 0),
+
+        // Get the IP header length.
+        BPF_STMT(BPF_LDX | BPF_B    | BPF_MSH, kEtherHeaderLen),
+
+        // Check the source port.
+        BPF_STMT(BPF_LD  | BPF_H    | BPF_IND, kUDPSrcPortIndirectOffset),
+        BPF_JUMP(BPF_JMP | BPF_JEQ  | BPF_K,   kDhcpClientPort, 8, 0),
+
+        // Check the destination port.
+        BPF_STMT(BPF_LD  | BPF_H    | BPF_IND, kUDPDstPortIndirectOffset),
+        BPF_JUMP(BPF_JMP | BPF_JEQ  | BPF_K,   kDhcpClientPort, 6, 7),
+
+        // IPv6 ...
+        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ETHERTYPE_IPV6, 0, 6),
+        // ... check IPv6 Next Header is ICMPv6 (ignore fragments), ...
+        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  kIPv6NextHeader),
+        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    IPPROTO_ICMPV6, 0, 4),
+        // ... and check the ICMPv6 type is one of RS/RA/NS/NA.
+        BPF_STMT(BPF_LD  | BPF_B   | BPF_ABS,  kICMPv6TypeOffset),
+        BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K,    ND_ROUTER_SOLICIT, 0, 2),
+        BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K,    ND_NEIGHBOR_ADVERT, 1, 0),
+
+        // Accept or reject.
+        BPF_STMT(BPF_RET | BPF_K,              0xffff),
+        BPF_STMT(BPF_RET | BPF_K,              0)
+    };
+    struct sock_fprog filter = {
+        sizeof(filter_code) / sizeof(filter_code[0]),
+        filter_code,
+    };
+
+    int fd = jniGetFDFromFileDescriptor(env, javaFd);
+    if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) {
+        jniThrowExceptionFmt(env, "java/net/SocketException",
+                "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno));
+    }
+}
+
 static void android_net_utils_setupRaSocket(JNIEnv *env, jobject clazz, jobject javaFd,
         jint ifIndex)
 {
@@ -266,6 +342,7 @@
     { "queryUserAccess", "(II)Z", (void*)android_net_utils_queryUserAccess },
     { "attachDhcpFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_attachDhcpFilter },
     { "attachRaFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachRaFilter },
+    { "attachControlPacketFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachControlPacketFilter },
     { "setupRaSocket", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_setupRaSocket },
 };
 
diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp
index 3c72274..2ac4f9d 100644
--- a/core/jni/android_util_Process.cpp
+++ b/core/jni/android_util_Process.cpp
@@ -241,7 +241,7 @@
         t_pri = getpriority(PRIO_PROCESS, t_pid);
 
         if (t_pri <= ANDROID_PRIORITY_AUDIO) {
-            int scheduler = sched_getscheduler(t_pid);
+            int scheduler = sched_getscheduler(t_pid) & ~SCHED_RESET_ON_FORK;
             if ((scheduler == SCHED_FIFO) || (scheduler == SCHED_RR)) {
                 // This task wants to stay in its current audio group so it can keep its budget
                 // don't update its cpuset or cgroup
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index f39fc1a..dc09e64 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -101,6 +101,7 @@
     <protected-broadcast android:name="android.app.action.BUGREPORT_SHARING_DECLINED" />
     <protected-broadcast android:name="android.app.action.BUGREPORT_FAILED" />
     <protected-broadcast android:name="android.app.action.BUGREPORT_SHARE" />
+    <protected-broadcast android:name="android.app.action.SHOW_DEVICE_MONITORING_DIALOG" />
 
     <protected-broadcast android:name="android.appwidget.action.APPWIDGET_UPDATE_OPTIONS" />
     <protected-broadcast android:name="android.appwidget.action.APPWIDGET_DELETED" />
@@ -443,6 +444,7 @@
     <protected-broadcast android:name="com.android.server.Wifi.action.TOGGLE_PNO" />
     <protected-broadcast android:name="intent.action.ACTION_RF_BAND_INFO" />
     <protected-broadcast android:name="android.intent.action.MEDIA_RESOURCE_GRANTED" />
+    <protected-broadcast android:name="android.app.action.NETWORK_LOGS_AVAILABLE" />
     <protected-broadcast android:name="android.app.action.SECURITY_LOGS_AVAILABLE" />
 
     <protected-broadcast android:name="android.app.action.INTERRUPTION_FILTER_CHANGED" />
@@ -3388,10 +3390,13 @@
                  android:permission="android.permission.BIND_JOB_SERVICE" >
         </service>
 
-        <service
-            android:name="com.android.server.pm.BackgroundDexOptService"
-            android:exported="true"
-            android:permission="android.permission.BIND_JOB_SERVICE">
+        <service android:name="com.android.server.pm.BackgroundDexOptService"
+                 android:exported="true"
+                 android:permission="android.permission.BIND_JOB_SERVICE">
+        </service>
+
+        <service android:name="com.android.server.storage.DiskStatsLoggingService"
+                 android:permission="android.permission.BIND_JOB_SERVICE" >
         </service>
 
     </application>
diff --git a/core/res/res/drawable/ic_qs_network_logging.xml b/core/res/res/drawable/ic_qs_network_logging.xml
new file mode 100644
index 0000000..9e082641
--- /dev/null
+++ b/core/res/res/drawable/ic_qs_network_logging.xml
@@ -0,0 +1,29 @@
+<!--
+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.
+-->
+
+<!-- STOPSHIP: Placeholder icon for network logging until the real icon is finalized-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="48dp"
+        android:height="48dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24"
+        android:tint="#4DFFFFFF" >
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M2,24v-4h12v4H2z M2,16v-4h20v4H2z M5,7 12,0 19,7 14,7 14,15 10,15 10,7z"/>
+
+</vector>
diff --git a/core/res/res/values-de/strings.xml b/core/res/res/values-de/strings.xml
index c182831..cff3f38 100644
--- a/core/res/res/values-de/strings.xml
+++ b/core/res/res/values-de/strings.xml
@@ -1208,7 +1208,7 @@
     <string name="permdesc_readInstallSessions" msgid="2049771699626019849">"Ermöglicht der App, Installationssitzungen zu lesen. Dadurch kann sie Details aktiver Paketinstallationen abrufen."</string>
     <string name="permlab_requestInstallPackages" msgid="5782013576218172577">"Installation von Paketen anfordern"</string>
     <string name="permdesc_requestInstallPackages" msgid="5740101072486783082">"Ermöglicht der App, die Installation von Paketen anzufordern"</string>
-    <string name="permlab_requestIgnoreBatteryOptimizations" msgid="8021256345643918264">"Fragen, ob Akku-Leistungsoptimierungen ignoriert werden können"</string>
+    <string name="permlab_requestIgnoreBatteryOptimizations" msgid="8021256345643918264">"fragen, ob Akku-Leistungsoptimierungen ignoriert werden können"</string>
     <string name="permdesc_requestIgnoreBatteryOptimizations" msgid="8359147856007447638">"Erlaubt einer App, nach der Berechtigung zum Ignorieren der Akku-Leistungsoptimierungen zu fragen."</string>
     <string name="tutorial_double_tap_to_zoom_message_short" msgid="1311810005957319690">"Für Zoomeinstellung zweimal berühren"</string>
     <string name="gadget_host_error_inflating" msgid="4882004314906466162">"Widget konnte nicht hinzugefügt werden."</string>
diff --git a/core/res/res/values-gl-rES/strings.xml b/core/res/res/values-gl-rES/strings.xml
index b0aaecf..b85ddf2 100644
--- a/core/res/res/values-gl-rES/strings.xml
+++ b/core/res/res/values-gl-rES/strings.xml
@@ -1208,8 +1208,8 @@
     <string name="permdesc_readInstallSessions" msgid="2049771699626019849">"Permite que unha aplicación consulte as sesións de instalación. Desta forma, pode ver os detalles acerca das instalacións de paquetes activas."</string>
     <string name="permlab_requestInstallPackages" msgid="5782013576218172577">"solicitar instalación de paquetes"</string>
     <string name="permdesc_requestInstallPackages" msgid="5740101072486783082">"Permite a unha aplicación solicitar a instalación dos paquetes."</string>
-    <string name="permlab_requestIgnoreBatteryOptimizations" msgid="8021256345643918264">"solicitar ignorar optimizacións da batería"</string>
-    <string name="permdesc_requestIgnoreBatteryOptimizations" msgid="8359147856007447638">"Permite que unha aplicación solicite permiso para ignorar optimizacións da batería para esa aplicación."</string>
+    <string name="permlab_requestIgnoreBatteryOptimizations" msgid="8021256345643918264">"pedir que se ignore a optimización da batería"</string>
+    <string name="permdesc_requestIgnoreBatteryOptimizations" msgid="8359147856007447638">"Fai que unha aplicación poida solicitar permiso para ignorar as optimizacións da batería."</string>
     <string name="tutorial_double_tap_to_zoom_message_short" msgid="1311810005957319690">"Toca dúas veces para controlar o zoom"</string>
     <string name="gadget_host_error_inflating" msgid="4882004314906466162">"Non se puido engadir o widget."</string>
     <string name="ime_action_go" msgid="8320845651737369027">"Ir"</string>
diff --git a/core/res/res/values-ko/strings.xml b/core/res/res/values-ko/strings.xml
index 40c35fa..e4af8f8 100644
--- a/core/res/res/values-ko/strings.xml
+++ b/core/res/res/values-ko/strings.xml
@@ -248,9 +248,9 @@
     <string name="permgrouplab_location" msgid="7275582855722310164">"위치"</string>
     <string name="permgroupdesc_location" msgid="1346617465127855033">"이 기기의 위치정보에 액세스"</string>
     <string name="permgrouplab_calendar" msgid="5863508437783683902">"캘린더"</string>
-    <string name="permgroupdesc_calendar" msgid="3889615280211184106">"일정에 접근할 수 있도록"</string>
+    <string name="permgroupdesc_calendar" msgid="3889615280211184106">"캘린더에 액세스"</string>
     <string name="permgrouplab_sms" msgid="228308803364967808">"SMS"</string>
-    <string name="permgroupdesc_sms" msgid="4656988620100940350">"문자 메시지를 보내고 확인할 수 있도록"</string>
+    <string name="permgroupdesc_sms" msgid="4656988620100940350">"SMS 메시지 전송 및 보기"</string>
     <string name="permgrouplab_storage" msgid="1971118770546336966">"저장"</string>
     <string name="permgroupdesc_storage" msgid="637758554581589203">"기기 사진, 미디어, 파일 액세스"</string>
     <string name="permgrouplab_microphone" msgid="171539900250043464">"마이크"</string>
@@ -258,7 +258,7 @@
     <string name="permgrouplab_camera" msgid="4820372495894586615">"카메라"</string>
     <string name="permgroupdesc_camera" msgid="3250611594678347720">"사진 및 동영상 촬영"</string>
     <string name="permgrouplab_phone" msgid="5229115638567440675">"전화"</string>
-    <string name="permgroupdesc_phone" msgid="6234224354060641055">"통화 상태를 관리하거나 전화를 걸 수 있도록"</string>
+    <string name="permgroupdesc_phone" msgid="6234224354060641055">"전화 걸기 및 관리"</string>
     <string name="permgrouplab_sensors" msgid="416037179223226722">"신체 센서"</string>
     <string name="permgroupdesc_sensors" msgid="7147968539346634043">"생체 신호에 관한 센서 데이터에 액세스"</string>
     <string name="capability_title_canRetrieveWindowContent" msgid="3901717936930170320">"창 콘텐츠 가져오기"</string>
diff --git a/core/res/res/values-si-rLK/strings.xml b/core/res/res/values-si-rLK/strings.xml
index ecbf649..e7458ae 100644
--- a/core/res/res/values-si-rLK/strings.xml
+++ b/core/res/res/values-si-rLK/strings.xml
@@ -269,8 +269,8 @@
     <string name="capability_desc_canRequestEnhancedWebAccessibility" msgid="7881063961507511765">"යෙදුම් අන්තර්ගතයට ප්‍රවේශ්‍යතාවය වැඩිවන ලෙස සකස් කිරීමට ඇතැම් විට ස්ක්‍රිප්ට් ස්ථාපනය කර ඇත."</string>
     <string name="capability_title_canRequestFilterKeyEvents" msgid="2103440391902412174">"ඔබ ටයිප් කළ පෙළ බලන්න"</string>
     <string name="capability_desc_canRequestFilterKeyEvents" msgid="7463135292204152818">"ණයවරපත් අංක සහ මුරපද වැනි පුද්ගලික දත්ත ඇතුළත් වේ."</string>
-    <string name="capability_title_canControlMagnification" msgid="3593493281059424855">"සංදර්ශන විශාලන මට්ටම පාලනය කිරීම"</string>
-    <string name="capability_desc_canControlMagnification" msgid="4791858203568383773">"සංදර්ශනයේ විශාලන මට්ටම සහ පිහිටීම පාලනය කිරීම."</string>
+    <string name="capability_title_canControlMagnification" msgid="3593493281059424855">"සංදර්ශනයේ විශාලනය පාලනය කරන්න"</string>
+    <string name="capability_desc_canControlMagnification" msgid="4791858203568383773">"සංදර්ශනයේ විශාලන මට්ටම සහ පිහිටීම පාලනය කරන්න."</string>
     <string name="capability_title_canPerformGestures" msgid="7418984730362576862">"අභින සිදු කරන්න"</string>
     <string name="capability_desc_canPerformGestures" msgid="8296373021636981249">"තට්ටු කිරීමට, ස්වයිප් කිරීමට, පින්ච් කිරීමට, සහ වෙනත් අභින සිදු කිරීමට හැකිය."</string>
     <string name="permlab_statusBar" msgid="7417192629601890791">"තත්ව තීරුව අබල කරන්න හෝ වෙනස් කරන්න"</string>
@@ -457,13 +457,13 @@
     <string name="permlab_readSyncSettings" msgid="6201810008230503052">"සමමුහුර්ත සැකසීම් කියවන්න"</string>
     <string name="permdesc_readSyncSettings" msgid="2706745674569678644">"ගිණුම සඳහා සමමුහුර්ත සැකසීම් කියවීමට යෙදුමට අවසර දෙන්න. උදාහරණයක් ලෙස, ගිණුමක් සමඟ පුද්ගල යෙදුම සමමුහුර්ත දැයි මෙයට හඳුනා ගත හැක."</string>
     <string name="permlab_writeSyncSettings" msgid="5408694875793945314">"සමමුහුර්ත කිරීම සක්‍රිය කරන්න සහ අක්‍රිය කරන්න"</string>
-    <string name="permdesc_writeSyncSettings" msgid="8956262591306369868">"ගිණුම සඳහා සමමුහුර්ත සැකසීම් වෙනස් කිරීමට යෙදුමට අවසර දෙන්න. උදාහරණයක් ලෙස, ගිණුම සමඟ පුද්ගල යෙදුම සමමුහුර්ත කිරීම සක්‍රිය කිරීමට භාවිත කල හැක."</string>
+    <string name="permdesc_writeSyncSettings" msgid="8956262591306369868">"ගිණුමක් සඳහා සමමුහුර්ත සැකසීම් විකරණය කිරීමට යෙදුමකට ඉඩ දෙයි. උදාහරණයක් ලෙස, ගිණුමක් සමඟ පුද්ගල යෙදුම සමමුහුර්ත කිරීම සබල කිරීමට මෙය භාවිත කළ හැක."</string>
     <string name="permlab_readSyncStats" msgid="7396577451360202448">"සමමුහුර්ත කිරීමේ සංඛ්‍යාන කියවීම"</string>
     <string name="permdesc_readSyncStats" msgid="1510143761757606156">"සමමුහුර්ත කිරීමේ සිදුවීම් ඉතිහාසය සහ කෙතරම් දත්ත සමමුහුර්ත වී ඇතිදැයි ඇතුලත් ගිණුම සඳහා සමමුහුර්ත කිරීමේ සංඛ්‍යාන කියවීමට යෙදුමට අවසර දෙන්න."</string>
     <string name="permlab_sdcardRead" product="nosdcard" msgid="367275095159405468">"ඔබගේ USB ආචයනය හි අන්තර්ගතය කියවන්න"</string>
     <string name="permlab_sdcardRead" product="default" msgid="2188156462934977940">"ඔබගේ SD කාඩ් පතෙහි අන්තර්ගතය කියවන්න"</string>
     <string name="permdesc_sdcardRead" product="nosdcard" msgid="3446988712598386079">"යෙදුමට ඔබගේ USB ආචයනය අන්තර්ගතය කියවීමට අවසර දෙන්න."</string>
-    <string name="permdesc_sdcardRead" product="default" msgid="2607362473654975411">"යෙදුමට ඔබගේ SD කාඩ් පතින් අන්තර්ගත කියවීමට අවසර දෙන්න."</string>
+    <string name="permdesc_sdcardRead" product="default" msgid="2607362473654975411">"යෙදුමට ඔබගේ SD කාඩ්පතේ අන්තර්ගතය කියවීමට ඉඩ දෙයි."</string>
     <string name="permlab_sdcardWrite" product="nosdcard" msgid="8485979062254666748">"ඔබගේ USB ආචයනයේ අන්තර්ගත වෙනස් කිරීම හෝ මැකීම"</string>
     <string name="permlab_sdcardWrite" product="default" msgid="8805693630050458763">"ඔබගේ SD පතේ අන්තර්ගත වෙනස් කිරීම හෝ මැකීම"</string>
     <string name="permdesc_sdcardWrite" product="nosdcard" msgid="6175406299445710888">"USB ආචයනය වෙත ලිවීමට යෙදුමට අවසර දෙන්න."</string>
@@ -737,7 +737,7 @@
     <!-- String.format failed for translation -->
     <!-- no translation found for keyguard_accessibility_widget_changed (5678624624681400191) -->
     <skip />
-    <string name="keyguard_accessibility_add_widget" msgid="8273277058724924654">"විජටය එකතු කරන්න."</string>
+    <string name="keyguard_accessibility_add_widget" msgid="8273277058724924654">"විජටය එක් කරන්න."</string>
     <string name="keyguard_accessibility_widget_empty_slot" msgid="1281505703307930757">"හිස්"</string>
     <string name="keyguard_accessibility_unlock_area_expanded" msgid="2278106022311170299">"අගුළු අරින ප්‍රදේශය විදහා ඇත."</string>
     <string name="keyguard_accessibility_unlock_area_collapsed" msgid="6366992066936076396">"අගුළු අරින ප්‍රදේශය හැකිලී ඇත."</string>
@@ -950,7 +950,7 @@
     <string name="undo" msgid="7905788502491742328">"අස් කරන්න"</string>
     <string name="redo" msgid="7759464876566803888">"යළි කරන්න"</string>
     <string name="textSelectionCABTitle" msgid="5236850394370820357">"පෙළ තේරීම"</string>
-    <string name="addToDictionary" msgid="4352161534510057874">"ශබ්ද කෝෂයට එකතු කරන්න"</string>
+    <string name="addToDictionary" msgid="4352161534510057874">"ශබ්ද කෝෂයට එක් කරන්න"</string>
     <string name="deleteText" msgid="6979668428458199034">"මකන්න"</string>
     <string name="inputMethod" msgid="1653630062304567879">"ආදාන ක්‍රමය"</string>
     <string name="editTextMenuTitle" msgid="4909135564941815494">"පෙළ ක්‍රියාවන්"</string>
@@ -1286,7 +1286,7 @@
     <string name="sync_undo_deletes" msgid="2941317360600338602">"මැකීම් අස් කරන්න"</string>
     <string name="sync_do_nothing" msgid="3743764740430821845">"දැනට කිසිවක් නොකරන්න"</string>
     <string name="choose_account_label" msgid="5655203089746423927">"ගිණුමක් තෝරන්න"</string>
-    <string name="add_account_label" msgid="2935267344849993553">"ගිණුමක් එකතු කරන්න"</string>
+    <string name="add_account_label" msgid="2935267344849993553">"ගිණුමක් එක් කරන්න"</string>
     <string name="add_account_button_label" msgid="3611982894853435874">"ගිණුමක් එක් කරන්න"</string>
     <string name="number_picker_increment_button" msgid="2412072272832284313">"වැඩි කරන්න"</string>
     <string name="number_picker_decrement_button" msgid="476050778386779067">"අඩු කරන්න"</string>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 1c06cc8..ebfb837 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2620,6 +2620,9 @@
     <!-- Component that is the default launcher when demo mode is enabled. -->
     <string name="config_demoModeLauncherComponent">com.android.retaildemo/.DemoPlayer</string>
 
+    <!-- Hashed password (SHA-256) used to restrict demo mode operation -->
+    <string name="config_demoModePassword" translatable="false"></string>
+
     <!-- Flag indicating whether round icons should be parsed from the application manifest. -->
     <bool name="config_useRoundIcon">false</bool>
 
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 5ae03a9..382f902 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -101,6 +101,8 @@
     <!-- Displayed when the user dialed an MMI code whose function
          could not be performed because FDN is enabled. This will be displayed in a toast. -->
     <string name="mmiFdnError">Operation is restricted to fixed dialing numbers only.</string>
+    <!-- Displayed when a carrier does not support call forwarding queries when roaming. -->
+    <string name="mmiErrorWhileRoaming">Can not change call forwarding settings from your phone while you are roaming.</string>
 
     <!-- Displayed when a phone feature such as call barring was activated. -->
     <string name="serviceEnabled">Service was enabled.</string>
@@ -202,6 +204,12 @@
     <!-- Displayed to tell the user that all service is blocked by access control. -->
     <string name="RestrictedOnAll">All voice/data/SMS services are blocked.</string>
 
+    <!-- Displayed to tell the user that they should switch their network preference. -->
+    <string name="NetworkPreferenceSwitchTitle">Can\u2019t reach network</string>
+    <!-- Displayed to tell the user that they should switch their network preference. -->
+    <string name="NetworkPreferenceSwitchSummary">To improve reception, try changing the type selected at Settings > Cellular networks > Preferred network type."</string>
+
+
     <!-- Displayed to tell the user that peer changed TTY mode -->
     <string name="peerTtyModeFull">Peer requested TTY Mode FULL</string>
     <string name="peerTtyModeHco">Peer requested TTY Mode HCO</string>
@@ -382,6 +390,13 @@
         This indicates that a work profile has been deleted. [CHAR LIMIT=NONE]-->
     <string name="work_profile_deleted_description_dpm_wipe">Your work profile is no longer available on this device.</string>
 
+    <!-- Content title for a notification. This indicates that network logging was activated by
+        a device owner. [CHAR LIMIT=NONE]-->
+    <string name="network_logging_notification_title">Network traffic is being monitored</string>
+    <!-- Content text for a notification. Tapping opens a dialog with more information on network
+        logging. [CHAR LIMIT=NONE]-->
+    <string name="network_logging_notification_text">Tap for more details</string>
+
     <!-- Factory reset warning dialog strings--> <skip />
     <!-- Shows up in the dialog's title to warn about an impeding factory reset. [CHAR LIMIT=NONE] -->
     <string name="factory_reset_warning">Your device will be erased</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 225a20a..d95f6e9 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -486,6 +486,8 @@
   <java-symbol type="string" name="RestrictedOnData" />
   <java-symbol type="string" name="RestrictedOnEmergency" />
   <java-symbol type="string" name="RestrictedOnNormal" />
+  <java-symbol type="string" name="NetworkPreferenceSwitchSummary" />
+  <java-symbol type="string" name="NetworkPreferenceSwitchTitle" />
   <java-symbol type="string" name="SetupCallDefault" />
   <java-symbol type="string" name="accept" />
   <java-symbol type="string" name="accessibility_enabled" />
@@ -725,6 +727,7 @@
   <java-symbol type="string" name="mmiComplete" />
   <java-symbol type="string" name="mmiError" />
   <java-symbol type="string" name="mmiFdnError" />
+  <java-symbol type="string" name="mmiErrorWhileRoaming" />
   <java-symbol type="string" name="month_day_year" />
   <java-symbol type="string" name="more_item_label" />
   <java-symbol type="string" name="needPuk" />
@@ -1106,6 +1109,8 @@
   <java-symbol type="string" name="work_profile_deleted_description" />
   <java-symbol type="string" name="work_profile_deleted_details" />
   <java-symbol type="string" name="work_profile_deleted_description_dpm_wipe" />
+  <java-symbol type="string" name="network_logging_notification_title" />
+  <java-symbol type="string" name="network_logging_notification_text" />
   <java-symbol type="string" name="factory_reset_warning" />
   <java-symbol type="string" name="factory_reset_message" />
   <java-symbol type="string" name="lockscreen_transport_play_description" />
@@ -1113,6 +1118,7 @@
   <java-symbol type="string" name="config_ethernet_tcp_buffers" />
   <java-symbol type="string" name="config_wifi_tcp_buffers" />
   <java-symbol type="string" name="config_demoModeLauncherComponent" />
+  <java-symbol type="string" name="config_demoModePassword" />
   <java-symbol type="string" name="demo_starting_message" />
   <java-symbol type="string" name="demo_restarting_message" />
   <java-symbol type="string" name="conference_call" />
@@ -1217,6 +1223,7 @@
   <java-symbol type="drawable" name="ic_print" />
   <java-symbol type="drawable" name="ic_print_error" />
   <java-symbol type="drawable" name="ic_grayedout_printer" />
+  <java-symbol type="drawable" name="ic_qs_network_logging" />
   <java-symbol type="drawable" name="jog_dial_arrow_long_left_green" />
   <java-symbol type="drawable" name="jog_dial_arrow_long_right_red" />
   <java-symbol type="drawable" name="jog_dial_arrow_short_left_and_right" />
diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryStatsDurationTimerTest.java b/core/tests/coretests/src/com/android/internal/os/BatteryStatsDurationTimerTest.java
index a15e367..f1aeecc 100644
--- a/core/tests/coretests/src/com/android/internal/os/BatteryStatsDurationTimerTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryStatsDurationTimerTest.java
@@ -18,12 +18,9 @@
 import android.os.BatteryStats;
 import android.os.Parcel;
 import android.support.test.filters.SmallTest;
-import android.util.Log;
 
 import junit.framework.TestCase;
 
-import org.mockito.Mockito;
-
 /**
  * Test BatteryStatsImpl.DurationTimer.
  *
@@ -39,7 +36,7 @@
         final BatteryStatsImpl.TimeBase timeBase = new BatteryStatsImpl.TimeBase();
         timeBase.init(clocks.uptimeMillis(), clocks.elapsedRealtime());
 
-        final BatteryStatsImpl.DurationTimer timer = new BatteryStatsImpl.DurationTimer(clocks, 
+        final BatteryStatsImpl.DurationTimer timer = new BatteryStatsImpl.DurationTimer(clocks,
                 null, BatteryStats.WAKE_TYPE_PARTIAL, null, timeBase);
 
         // TimeBase running, timer not running: current and max are 0
@@ -82,15 +79,15 @@
         // Stop the TimeBase. The values should be frozen.
         timeBase.setRunning(false, /* uptimeUs */ 10, /* realtimeUs */ 55000*1000);
         assertTrue(timer.isRunningLocked());
-        assertEquals(28100, timer.getCurrentDurationMsLocked(110100)); // Why 28100 and not 28000?
-        assertEquals(28100, timer.getMaxDurationMsLocked(110101));
+        assertEquals(28000, timer.getCurrentDurationMsLocked(110100));
+        assertEquals(28000, timer.getMaxDurationMsLocked(110101));
 
         // Start the TimeBase. The values should be the old value plus the delta
         // between when the timer restarted and the current time
         timeBase.setRunning(true, /* uptimeUs */ 10, /* realtimeUs */ 220100*1000);
         assertTrue(timer.isRunningLocked());
-        assertEquals(28300, timer.getCurrentDurationMsLocked(220300)); // extra 100 from above??
-        assertEquals(28301, timer.getMaxDurationMsLocked(220301));
+        assertEquals(28200, timer.getCurrentDurationMsLocked(220300));
+        assertEquals(28201, timer.getMaxDurationMsLocked(220301));
     }
 
     @SmallTest
@@ -104,7 +101,7 @@
         final BatteryStatsImpl.TimeBase timeBase = new BatteryStatsImpl.TimeBase();
         timeBase.init(clocks.uptimeMillis(), clocks.elapsedRealtime());
 
-        final BatteryStatsImpl.DurationTimer timer = new BatteryStatsImpl.DurationTimer(clocks, 
+        final BatteryStatsImpl.DurationTimer timer = new BatteryStatsImpl.DurationTimer(clocks,
                 null, BatteryStats.WAKE_TYPE_PARTIAL, null, timeBase);
 
         // Start running on battery.
@@ -124,7 +121,7 @@
         summaryParcel.setDataPosition(0);
 
         // Read summary
-        final BatteryStatsImpl.DurationTimer summary = new BatteryStatsImpl.DurationTimer(clocks, 
+        final BatteryStatsImpl.DurationTimer summary = new BatteryStatsImpl.DurationTimer(clocks,
                 null, BatteryStats.WAKE_TYPE_PARTIAL, null, timeBase);
         summary.startRunningLocked(3100);
         summary.readSummaryFromParcelLocked(summaryParcel);
@@ -138,9 +135,9 @@
         final Parcel fullParcel = Parcel.obtain();
         timer.writeToParcel(fullParcel, 1500*1000);
         fullParcel.setDataPosition(0);
- 
+
         // Read full - Should be the same as the summary as far as DurationTimer is concerned.
-        final BatteryStatsImpl.DurationTimer full = new BatteryStatsImpl.DurationTimer(clocks, 
+        final BatteryStatsImpl.DurationTimer full = new BatteryStatsImpl.DurationTimer(clocks,
                 null, BatteryStats.WAKE_TYPE_PARTIAL, null, timeBase, fullParcel);
         // The new one shouldn't be running, and therefore 0 for current time
         assertFalse(full.isRunningLocked());
diff --git a/docs/__DEPRECATED__DO_NOT_EDIT__.txt b/docs/__DEPRECATED__DO_NOT_EDIT__.txt
new file mode 100644
index 0000000..3f8fc80c
--- /dev/null
+++ b/docs/__DEPRECATED__DO_NOT_EDIT__.txt
@@ -0,0 +1,16 @@
+### DEPRECATED: DO NOT EDIT ###
+
+The source files for developer.android.com are NO LONGER MAINTAINED HERE, as
+of 12/2016. Migration of content was completed on 10/16/2016.
+
+All authoring of content has been moved to Piper (go/dac-source).
+
+Exceptions and Caveats:
+
+- Reference documentation is still maintained via building of .java source files,
+  so you may continue to update JavaDoc comments to update documentation.
+
+- Sample code documentation is not maintained in Piper, but is published from
+  a separate code repository.
+
+For answers to further questions, please email: android-writers@google.com
diff --git a/docs/html/__DEPRECATED__DO_NOT_EDIT__.txt b/docs/html/__DEPRECATED__DO_NOT_EDIT__.txt
new file mode 100644
index 0000000..3f8fc80c
--- /dev/null
+++ b/docs/html/__DEPRECATED__DO_NOT_EDIT__.txt
@@ -0,0 +1,16 @@
+### DEPRECATED: DO NOT EDIT ###
+
+The source files for developer.android.com are NO LONGER MAINTAINED HERE, as
+of 12/2016. Migration of content was completed on 10/16/2016.
+
+All authoring of content has been moved to Piper (go/dac-source).
+
+Exceptions and Caveats:
+
+- Reference documentation is still maintained via building of .java source files,
+  so you may continue to update JavaDoc comments to update documentation.
+
+- Sample code documentation is not maintained in Piper, but is published from
+  a separate code repository.
+
+For answers to further questions, please email: android-writers@google.com
diff --git a/graphics/java/android/graphics/BitmapFactory.java b/graphics/java/android/graphics/BitmapFactory.java
index 447a4c4..4afe438 100644
--- a/graphics/java/android/graphics/BitmapFactory.java
+++ b/graphics/java/android/graphics/BitmapFactory.java
@@ -49,8 +49,8 @@
         /**
          * If set, decode methods that take the Options object will attempt to
          * reuse this bitmap when loading content. If the decode operation
-         * cannot use this bitmap, the decode method will return
-         * <code>null</code> and will throw an IllegalArgumentException. The
+         * cannot use this bitmap, the decode method will throw an
+         * {@link java.lang.IllegalArgumentException}. The
          * current implementation necessitates that the reused bitmap be
          * mutable, and the resulting reused bitmap will continue to remain
          * mutable even when decoding a resource which would normally result in
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index cfbe672..cac9537 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -1404,7 +1404,7 @@
     /**
      * Return the paint's text size.
      *
-     * @return the paint's text size.
+     * @return the paint's text size in pixel units.
      */
     public float getTextSize() {
         return nGetTextSize(mNativePaint);
@@ -1415,7 +1415,7 @@
     /**
      * Set the paint's text size. This value must be > 0
      *
-     * @param textSize set the paint's text size.
+     * @param textSize set the paint's text size in pixel units.
      */
     public void setTextSize(float textSize) {
         nSetTextSize(mNativePaint, textSize);
diff --git a/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java b/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
index 03f0b4d..23a8655 100644
--- a/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
+++ b/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
@@ -63,6 +63,7 @@
     private enum Result { DISMISSED, UNWANTED, WANTED_AS_IS };
 
     private URL mUrl;
+    private String mUserAgent;
     private Network mNetwork;
     private CaptivePortal mCaptivePortal;
     private NetworkCallback mNetworkCallback;
@@ -76,6 +77,8 @@
         mCm = ConnectivityManager.from(this);
         mNetwork = getIntent().getParcelableExtra(ConnectivityManager.EXTRA_NETWORK);
         mCaptivePortal = getIntent().getParcelableExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL);
+        mUserAgent = getIntent().getParcelableExtra(
+                ConnectivityManager.EXTRA_CAPTIVE_PORTAL_USER_AGENT);
         mUrl = getUrl();
         if (mUrl == null) {
             // getUrl() failed to parse the url provided in the intent: bail out in a way that
@@ -252,6 +255,7 @@
     }
 
     private void testForCaptivePortal() {
+        // TODO: reuse NetworkMonitor facilities for consistent captive portal detection.
         new Thread(new Runnable() {
             public void run() {
                 // Give time for captive portal to open.
@@ -262,11 +266,14 @@
                 HttpURLConnection urlConnection = null;
                 int httpResponseCode = 500;
                 try {
-                    urlConnection = (HttpURLConnection) mUrl.openConnection();
+                    urlConnection = (HttpURLConnection) mNetwork.openConnection(mUrl);
                     urlConnection.setInstanceFollowRedirects(false);
                     urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
                     urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
                     urlConnection.setUseCaches(false);
+                    if (mUserAgent != null) {
+                       urlConnection.setRequestProperty("User-Agent", mUserAgent);
+                    }
                     urlConnection.getInputStream();
                     httpResponseCode = urlConnection.getResponseCode();
                 } catch (IOException e) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
index 1922773..a04a930 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
@@ -387,6 +387,12 @@
         }
 
         mNavigator.update();
+        // Causes talkback to announce the activity's new title
+        if (mState.stack.isRecents()) {
+            setTitle(mRoots.getRecentsRoot().title);
+        } else {
+            setTitle(mState.stack.getTitle());
+        }
         invalidateOptionsMenu();
     }
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
index 7186339..233380e 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
@@ -192,7 +192,7 @@
         Intent intent = getIntent();
         return (intent != null && intent.hasExtra(Intent.EXTRA_TITLE))
                 ? intent.getStringExtra(Intent.EXTRA_TITLE)
-                : getTitle().toString();
+                : getString(R.string.downloads_label);
     }
 
     @Override
diff --git a/packages/SettingsLib/res/values/config.xml b/packages/SettingsLib/res/values/config.xml
index 0aa76a0..64f21b5 100755
--- a/packages/SettingsLib/res/values/config.xml
+++ b/packages/SettingsLib/res/values/config.xml
@@ -38,6 +38,12 @@
     <!-- Intent key for package name values -->
     <string name="config_helpIntentNameKey" translatable="false"></string>
 
+    <!-- Intent key for the package name keys -->
+    <string name="config_feedbackIntentExtraKey" translatable="false"></string>
+
+    <!-- Intent key for package name values -->
+    <string name="config_feedbackIntentNameKey" translatable="false"></string>
+
     <!-- The apps that need to be hided when they are disabled -->
     <string-array name="config_hideWhenDisabled_packageNames"></string-array>
 </resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/HelpUtils.java b/packages/SettingsLib/src/com/android/settingslib/HelpUtils.java
index 381f903..e7c8c0b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/HelpUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/HelpUtils.java
@@ -185,12 +185,18 @@
                     {resources.getString(R.string.config_helpPackageNameKey)};
             String[] packageNameValue =
                     {resources.getString(R.string.config_helpPackageNameValue)};
-            String intentExtraKey =
+            String helpIntentExtraKey =
                     resources.getString(R.string.config_helpIntentExtraKey);
-            String intentNameKey =
+            String helpIntentNameKey =
                     resources.getString(R.string.config_helpIntentNameKey);
-            intent.putExtra(intentExtraKey, packageNameKey);
-            intent.putExtra(intentNameKey, packageNameValue);
+            String feedbackIntentExtraKey =
+                    resources.getString(R.string.config_feedbackIntentExtraKey);
+            String feedbackIntentNameKey =
+                    resources.getString(R.string.config_feedbackIntentNameKey);
+            intent.putExtra(helpIntentExtraKey, packageNameKey);
+            intent.putExtra(helpIntentNameKey, packageNameValue);
+            intent.putExtra(feedbackIntentExtraKey, packageNameKey);
+            intent.putExtra(feedbackIntentNameKey, packageNameValue);
         }
         intent.putExtra(EXTRA_THEME, 1 /* Light, dark action bar */);
         TypedArray array = context.obtainStyledAttributes(new int[]{android.R.attr.colorPrimary});
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java
index 234ae71..28575a9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java
@@ -790,16 +790,19 @@
         if (state == DetailedState.CONNECTED) {
             IWifiManager wifiManager = IWifiManager.Stub.asInterface(
                     ServiceManager.getService(Context.WIFI_SERVICE));
-            Network nw;
+            NetworkCapabilities nc = null;
 
             try {
-                nw = wifiManager.getCurrentNetwork();
-            } catch (RemoteException e) {
-                nw = null;
-            }
-            NetworkCapabilities nc = cm.getNetworkCapabilities(nw);
-            if (nc != null && !nc.hasCapability(nc.NET_CAPABILITY_VALIDATED)) {
-                return context.getString(R.string.wifi_connected_no_internet);
+                nc = cm.getNetworkCapabilities(wifiManager.getCurrentNetwork());
+            } catch (RemoteException e) {}
+
+            if (nc != null) {
+                if (nc.hasCapability(nc.NET_CAPABILITY_CAPTIVE_PORTAL)) {
+                    return context.getString(
+                        com.android.internal.R.string.network_available_sign_in);
+                } else if (!nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
+                    return context.getString(R.string.wifi_connected_no_internet);
+                }
             }
         }
         if (state == null) {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/HelpUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/HelpUtilsTest.java
new file mode 100644
index 0000000..5d843c1
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/HelpUtilsTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.settingslib;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link HelpUtils}.
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class HelpUtilsTest {
+    private static final String PACKAGE_NAME_KEY = "package-name-key";
+    private static final String PACKAGE_NAME_VALUE = "package-name-value";
+    private static final String HELP_INTENT_EXTRA_KEY = "help-intent-extra";
+    private static final String HELP_INTENT_NAME_KEY = "help-intent-name";
+    private static final String FEEDBACK_INTENT_EXTRA_KEY = "feedback-intent-extra";
+    private static final String FEEDBACK_INTENT_NAME_KEY = "feedback-intent-name";
+
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mContext.getResources().getString(R.string.config_helpPackageNameKey))
+                .thenReturn(PACKAGE_NAME_KEY);
+        when(mContext.getResources().getString(R.string.config_helpPackageNameValue))
+                .thenReturn(PACKAGE_NAME_VALUE);
+        when(mContext.getResources().getString(R.string.config_helpIntentExtraKey))
+                .thenReturn(HELP_INTENT_EXTRA_KEY);
+        when(mContext.getResources().getString(R.string.config_helpIntentNameKey))
+                .thenReturn(HELP_INTENT_NAME_KEY);
+        when(mContext.getResources().getString(R.string.config_feedbackIntentExtraKey))
+                .thenReturn(FEEDBACK_INTENT_EXTRA_KEY);
+        when(mContext.getResources().getString(R.string.config_feedbackIntentNameKey))
+                .thenReturn(FEEDBACK_INTENT_NAME_KEY);
+
+    }
+
+    @Test
+    public void addIntentParameters_configTrue_argumentTrue() {
+        when(mContext.getResources().getBoolean(R.bool.config_sendPackageName)).thenReturn(true);
+        Intent intent = new Intent();
+
+        HelpUtils.addIntentParameters(
+                mContext, intent, null /* backupContext */, true /* sendPackageName */);
+
+        assertThat(intent.getStringArrayExtra(HELP_INTENT_EXTRA_KEY)).asList()
+                .containsExactly(PACKAGE_NAME_KEY);
+        assertThat(intent.getStringArrayExtra(HELP_INTENT_NAME_KEY)).asList()
+                .containsExactly(PACKAGE_NAME_VALUE);
+        assertThat(intent.getStringArrayExtra(FEEDBACK_INTENT_EXTRA_KEY)).asList()
+                .containsExactly(PACKAGE_NAME_KEY);
+        assertThat(intent.getStringArrayExtra(FEEDBACK_INTENT_NAME_KEY)).asList()
+                .containsExactly(PACKAGE_NAME_VALUE);
+    }
+
+    @Test
+    public void addIntentParameters_configTrue_argumentFalse() {
+        when(mContext.getResources().getBoolean(R.bool.config_sendPackageName)).thenReturn(true);
+        Intent intent = new Intent();
+
+        HelpUtils.addIntentParameters(
+                mContext, intent, null /* backupContext */, false /* sendPackageName */);
+
+        assertThat(intent.hasExtra(HELP_INTENT_EXTRA_KEY)).isFalse();
+        assertThat(intent.hasExtra(HELP_INTENT_NAME_KEY)).isFalse();
+        assertThat(intent.hasExtra(FEEDBACK_INTENT_EXTRA_KEY)).isFalse();
+        assertThat(intent.hasExtra(FEEDBACK_INTENT_NAME_KEY)).isFalse();
+    }
+
+    @Test
+    public void addIntentParameters_configFalse_argumentTrue() {
+        when(mContext.getResources().getBoolean(R.bool.config_sendPackageName)).thenReturn(false);
+        Intent intent = new Intent();
+
+        HelpUtils.addIntentParameters(
+                mContext, intent, null /* backupContext */, true /* sendPackageName */);
+
+        assertThat(intent.hasExtra(HELP_INTENT_EXTRA_KEY)).isFalse();
+        assertThat(intent.hasExtra(HELP_INTENT_NAME_KEY)).isFalse();
+        assertThat(intent.hasExtra(FEEDBACK_INTENT_EXTRA_KEY)).isFalse();
+        assertThat(intent.hasExtra(FEEDBACK_INTENT_NAME_KEY)).isFalse();
+    }
+
+    @Test
+    public void addIntentParameters_configFalse_argumentFalse() {
+        when(mContext.getResources().getBoolean(R.bool.config_sendPackageName)).thenReturn(false);
+        Intent intent = new Intent();
+
+        HelpUtils.addIntentParameters(
+                mContext, intent, null /* backupContext */, false /* sendPackageName */);
+
+        assertThat(intent.hasExtra(HELP_INTENT_EXTRA_KEY)).isFalse();
+        assertThat(intent.hasExtra(HELP_INTENT_NAME_KEY)).isFalse();
+        assertThat(intent.hasExtra(FEEDBACK_INTENT_EXTRA_KEY)).isFalse();
+        assertThat(intent.hasExtra(FEEDBACK_INTENT_NAME_KEY)).isFalse();
+    }
+}
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index ba845bd..a74fbf8 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -267,22 +267,6 @@
             </intent-filter>
         </activity>
 
-        <activity android:name=".recents.grid.RecentsGridActivity"
-                  android:label="@string/accessibility_desc_recent_apps"
-                  android:exported="false"
-                  android:launchMode="singleInstance"
-                  android:excludeFromRecents="true"
-                  android:stateNotNeeded="true"
-                  android:resumeWhilePausing="true"
-                  android:screenOrientation="behind"
-                  android:resizeableActivity="true"
-                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
-                  android:theme="@style/RecentsTheme.Wallpaper">
-            <intent-filter>
-                <action android:name="com.android.systemui.recents.TOGGLE_RECENTS" />
-            </intent-filter>
-        </activity>
-
         <activity android:name=".recents.tv.RecentsTvActivity"
                   android:label="@string/accessibility_desc_recent_apps"
                   android:exported="false"
diff --git a/packages/SystemUI/res/drawable/ic_qs_network_logging.xml b/packages/SystemUI/res/drawable/ic_qs_network_logging.xml
new file mode 100644
index 0000000..2cce532
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_qs_network_logging.xml
@@ -0,0 +1,29 @@
+<!--
+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.
+-->
+
+<!-- Placeholder icon for network logging until the real icon is finalized-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="12.0dp"
+        android:height="12.0dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"
+        android:tint="#4DFFFFFF" >
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M2,24v-4h12v4H2z M2,16v-4h20v4H2z M5,7 12,0 19,7 14,7 14,15 10,15 10,7z"/>
+
+</vector>
diff --git a/packages/SystemUI/res/layout-sw600dp/recents_grid.xml b/packages/SystemUI/res/layout-sw600dp/recents_grid.xml
deleted file mode 100644
index 07ec237..0000000
--- a/packages/SystemUI/res/layout-sw600dp/recents_grid.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical"
-    android:id="@+id/recents_container">
-    <include layout="@layout/recents_stack_action_button" />
-    <FrameLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:id="@+id/recents_view"
-        android:layout_marginLeft="12dp"
-        android:layout_marginTop="10dp"
-        android:layout_marginRight="12dp"
-        android:layout_marginBottom="5dp"
-        android:gravity="center">
-    </FrameLayout>
-</LinearLayout>
-
diff --git a/packages/SystemUI/res/layout/quick_settings_footer.xml b/packages/SystemUI/res/layout/quick_settings_footer.xml
index 53baf74..8667a5a 100644
--- a/packages/SystemUI/res/layout/quick_settings_footer.xml
+++ b/packages/SystemUI/res/layout/quick_settings_footer.xml
@@ -39,4 +39,16 @@
         android:src="@drawable/ic_qs_vpn"
         android:visibility="invisible" />
 
-</RelativeLayout>
\ No newline at end of file
+    <!-- Only shown if both images are visible -->
+    <ImageView
+        android:id="@+id/footer_icon2"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerVertical="true"
+        android:layout_marginEnd="8dp"
+        android:layout_toStartOf="@id/footer_icon"
+        android:contentDescription="@null"
+        android:src="@drawable/ic_qs_network_logging"
+        android:visibility="invisible" />
+
+</RelativeLayout>
diff --git a/packages/SystemUI/res/layout/quick_settings_footer_dialog.xml b/packages/SystemUI/res/layout/quick_settings_footer_dialog.xml
new file mode 100644
index 0000000..2ba04fd
--- /dev/null
+++ b/packages/SystemUI/res/layout/quick_settings_footer_dialog.xml
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/scrollView"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:clipToPadding="false">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingTop="?android:attr/dialogPreferredPadding"
+        android:paddingRight="?android:attr/dialogPreferredPadding"
+        android:paddingLeft="?android:attr/dialogPreferredPadding"
+        android:paddingBottom="?android:attr/dialogPreferredPadding"
+        android:orientation="vertical">
+        <TextView
+            android:id="@+id/device_owner_warning"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            style="@android:style/TextAppearance.Material.Subhead"
+            android:textColor="?android:attr/textColorPrimaryInverse"
+        />
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal">
+            <ImageView
+                android:id="@+id/vpn_icon"
+                android:layout_width="@dimen/qs_footer_dialog_icon_size"
+                android:layout_height="wrap_content"
+                android:paddingTop="?android:attr/dialogPreferredPadding"
+                android:layout_marginStart="@dimen/qs_footer_dialog_icon_margin"
+                android:layout_marginEnd="@dimen/qs_footer_dialog_icon_margin"
+                android:scaleType="fitCenter"
+                android:src="@drawable/ic_qs_vpn"
+                android:tint="?android:attr/textColorPrimaryInverse"
+                android:adjustViewBounds="true"/>
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical">
+                <TextView
+                    android:id="@+id/vpn_subtitle"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:paddingTop="?android:attr/dialogPreferredPadding"
+                    android:text="@string/monitoring_subtitle_vpn"
+                    style="@android:style/TextAppearance.Material.Title"
+                    android:textColor="?android:attr/textColorPrimaryInverse"
+                />
+                <TextView
+                    android:id="@+id/vpn_warning"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:text="@null"
+                    style="@android:style/TextAppearance.Material.Subhead"
+                    android:textColor="?android:attr/textColorPrimaryInverse"
+                />
+            </LinearLayout>
+        </LinearLayout>
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal">
+            <ImageView
+                android:id="@+id/network_logging_icon"
+                android:layout_width="@dimen/qs_footer_dialog_icon_size"
+                android:layout_height="wrap_content"
+                android:paddingTop="?android:attr/dialogPreferredPadding"
+                android:layout_marginStart="@dimen/qs_footer_dialog_icon_margin"
+                android:layout_marginEnd="@dimen/qs_footer_dialog_icon_margin"
+                android:scaleType="fitCenter"
+                android:src="@drawable/ic_qs_network_logging"
+                android:tint="?android:attr/textColorPrimaryInverse"
+                android:alpha="@dimen/qs_footer_dialog_network_logging_icon_alpha"
+                android:adjustViewBounds="true"/>
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical">
+                <TextView
+                    android:id="@+id/network_logging_subtitle"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:paddingTop="?android:attr/dialogPreferredPadding"
+                    android:text="@string/monitoring_subtitle_network_logging"
+                    style="@android:style/TextAppearance.Material.Title"
+                    android:textColor="?android:attr/textColorPrimaryInverse"
+                />
+                <TextView
+                    android:id="@+id/network_logging_warning"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:text="@string/monitoring_description_network_logging"
+                    style="@android:style/TextAppearance.Material.Subhead"
+                    android:textColor="?android:attr/textColorPrimaryInverse"
+                />
+            </LinearLayout>
+        </LinearLayout>
+    </LinearLayout>
+</ScrollView>
diff --git a/packages/SystemUI/res/layout/recents_grid_task_view.xml b/packages/SystemUI/res/layout/recents_grid_task_view.xml
new file mode 100644
index 0000000..53bec70
--- /dev/null
+++ b/packages/SystemUI/res/layout/recents_grid_task_view.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.systemui.recents.views.grid.GridTaskView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:focusable="true">
+    <com.android.systemui.recents.views.TaskViewThumbnail
+        android:id="@+id/task_view_thumbnail"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <include layout="@layout/recents_task_view_header" />
+
+    <!-- TODO: Move this into a view stub -->
+    <include layout="@layout/recents_task_view_lock_to_app"/>
+
+    <!-- The incompatible app toast -->
+    <include layout="@layout/recents_task_view_incompatible_app_toast"/>
+</com.android.systemui.recents.views.grid.GridTaskView>
+
+
diff --git a/packages/SystemUI/res/layout/recents_task_view.xml b/packages/SystemUI/res/layout/recents_task_view.xml
index c8e5b61..015e4a2 100644
--- a/packages/SystemUI/res/layout/recents_task_view.xml
+++ b/packages/SystemUI/res/layout/recents_task_view.xml
@@ -4,9 +4,9 @@
      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.
@@ -26,35 +26,10 @@
     <include layout="@layout/recents_task_view_header" />
 
     <!-- TODO: Move this into a view stub -->
-    <com.android.systemui.statusbar.AlphaOptimizedFrameLayout
-        android:id="@+id/lock_to_app_fab"
-        android:layout_width="@dimen/recents_lock_to_app_size"
-        android:layout_height="@dimen/recents_lock_to_app_size"
-        android:layout_gravity="bottom|end"
-        android:layout_marginEnd="15dp"
-        android:layout_marginBottom="15dp"
-        android:translationZ="4dp"
-        android:contentDescription="@string/recents_lock_to_app_button_label"
-        android:background="@drawable/recents_lock_to_task_button_bg"
-        android:visibility="invisible"
-        android:alpha="0">
-        <ImageView
-            android:layout_width="@dimen/recents_lock_to_app_icon_size"
-            android:layout_height="@dimen/recents_lock_to_app_icon_size"
-            android:layout_gravity="center"
-            android:src="@drawable/recents_lock_to_app_pin" />
-    </com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
+    <include layout="@layout/recents_task_view_lock_to_app"/>
 
     <!-- The incompatible app toast -->
-    <ViewStub android:id="@+id/incompatible_app_toast_stub"
-                android:inflatedId="@+id/incompatible_app_toast"
-                android:layout="@*android:layout/transient_notification"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="top|center_horizontal"
-                android:layout_marginTop="48dp"
-                android:layout_marginLeft="16dp"
-                android:layout_marginRight="16dp" />
+    <include layout="@layout/recents_task_view_incompatible_app_toast"/>
 </com.android.systemui.recents.views.TaskView>
 
 
diff --git a/packages/SystemUI/res/layout/recents_task_view_incompatible_app_toast.xml b/packages/SystemUI/res/layout/recents_task_view_incompatible_app_toast.xml
new file mode 100644
index 0000000..d573d6b
--- /dev/null
+++ b/packages/SystemUI/res/layout/recents_task_view_incompatible_app_toast.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<ViewStub
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/incompatible_app_toast_stub"
+    android:inflatedId="@+id/incompatible_app_toast"
+    android:layout="@*android:layout/transient_notification"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="top|center_horizontal"
+    android:layout_marginTop="48dp"
+    android:layout_marginLeft="16dp"
+    android:layout_marginRight="16dp" />
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/recents_task_view_lock_to_app.xml b/packages/SystemUI/res/layout/recents_task_view_lock_to_app.xml
new file mode 100644
index 0000000..8cece11
--- /dev/null
+++ b/packages/SystemUI/res/layout/recents_task_view_lock_to_app.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.systemui.statusbar.AlphaOptimizedFrameLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/lock_to_app_fab"
+    android:layout_width="@dimen/recents_lock_to_app_size"
+    android:layout_height="@dimen/recents_lock_to_app_size"
+    android:layout_gravity="bottom|end"
+    android:layout_marginEnd="15dp"
+    android:layout_marginBottom="15dp"
+    android:translationZ="4dp"
+    android:contentDescription="@string/recents_lock_to_app_button_label"
+    android:background="@drawable/recents_lock_to_task_button_bg"
+    android:visibility="invisible"
+    android:alpha="0">
+    <ImageView
+        android:layout_width="@dimen/recents_lock_to_app_icon_size"
+        android:layout_height="@dimen/recents_lock_to_app_icon_size"
+        android:layout_gravity="center"
+        android:src="@drawable/recents_lock_to_app_pin" />
+</com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values-sw900dp/config.xml b/packages/SystemUI/res/values-sw900dp/config.xml
index 182fa36..d8f9ef4 100644
--- a/packages/SystemUI/res/values-sw900dp/config.xml
+++ b/packages/SystemUI/res/values-sw900dp/config.xml
@@ -19,6 +19,6 @@
 <resources>
 
     <!-- Nav bar button default ordering/layout -->
-    <string name="config_navBarLayout" translatable="false">space[.2],back,home;space;menu_ime,recent,space[.2]</string>
+    <string name="config_navBarLayout" translatable="false">back,home;space;menu_ime,recent</string>
 
 </resources>
diff --git a/packages/SystemUI/res/values-sw900dp/dimens.xml b/packages/SystemUI/res/values-sw900dp/dimens.xml
index 72e10c2..2cff976 100644
--- a/packages/SystemUI/res/values-sw900dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw900dp/dimens.xml
@@ -17,7 +17,6 @@
 -->
 <resources>
 
-    <!-- All ryu nav buttons are th same size -->
     <dimen name="button_size">80dp</dimen>
     <dimen name="navigation_side_padding">@dimen/button_size</dimen>
     <dimen name="navigation_key_width">@dimen/button_size</dimen>
@@ -27,14 +26,6 @@
     <dimen name="key_button_ripple_max_width">76dp</dimen>
 
     <!-- The padding around the navigation buttons -->
-    <dimen name="navigation_key_padding">5dp</dimen>
+    <dimen name="navigation_key_padding">0dp</dimen>
 
-    <!-- The inner radius of the halo. -->
-    <dimen name="halo_inner_radius">12dp</dimen>
-
-    <!-- The thickness of the halo. -->
-    <dimen name="halo_thickness">1dp</dimen>
-
-    <!-- The diameter of the halo. This is 2*(halo_inner_radius + halo_thickness). -->
-    <dimen name="halo_diameter">26dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index dca9767..e6194a9 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -212,6 +212,13 @@
     <!-- How far the expanded QS panel peeks from the header in collapsed state. -->
     <dimen name="qs_peek_height">0dp</dimen>
 
+    <!-- How large the icons in the quick settings footer dialog are -->
+    <dimen name="qs_footer_dialog_icon_size">24sp</dimen>
+    <!-- Left and right margin of the icons -->
+    <dimen name="qs_footer_dialog_icon_margin">8sp</dimen>
+    <!-- Alpha value of network logging icon -->
+    <item name="qs_footer_dialog_network_logging_icon_alpha" format="float" type="dimen">0.3</item>
+
     <!-- Zen mode panel: condition item button padding -->
     <dimen name="zen_mode_condition_detail_button_padding">8dp</dimen>
 
diff --git a/packages/SystemUI/res/values/dimens_grid.xml b/packages/SystemUI/res/values/dimens_grid.xml
new file mode 100644
index 0000000..94ffdb1
--- /dev/null
+++ b/packages/SystemUI/res/values/dimens_grid.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
+*/
+-->
+<resources>
+  <dimen name="recents_grid_padding_left_right">32dp</dimen>
+  <dimen name="recents_grid_padding_top_bottom">150dp</dimen>
+  <dimen name="recents_grid_padding_task_view">20dp</dimen>
+  <dimen name="recents_grid_task_view_header_height">44dp</dimen>
+  <dimen name="recents_grid_task_view_header_button_padding">8dp</dimen>
+</resources>
+
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 2ea475a..0bb58e3 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1013,6 +1013,13 @@
     <!-- Monitoring dialog title for normal devices  [CHAR LIMIT=35]-->
     <string name="monitoring_title">Network monitoring</string>
 
+    <!-- STOPSHIP(b/33655277) Monitoring strings still need to be finalized and approved -->
+    <!-- Monitoring dialog subtitle for the section describing VPN [CHAR LIMIT=TODO]-->
+    <string name="monitoring_subtitle_vpn">VPN</string>
+
+    <!-- Monitoring dialog subtitle for the section describing network logging [CHAR LIMIT=TODO]-->
+    <string name="monitoring_subtitle_network_logging">Network Logging</string>
+
     <!-- Monitoring dialog disable vpn button [CHAR LIMIT=30] -->
     <string name="disable_vpn">Disable VPN</string>
 
@@ -1022,6 +1029,19 @@
     <!-- Monitoring dialog device owner body text [CHAR LIMIT=400] -->
     <string name="monitoring_description_device_owned">Your device is managed by <xliff:g id="organization">%1$s</xliff:g>.\n\nYour administrator can monitor and manage settings, corporate access, apps, data associated with your device, and your device\'s location information. For more information, contact your administrator.</string>
 
+    <!-- Monitoring dialog: Part of text body explaining that a VPN is connected and what it can do, for devices managed by a Device Owner app [CHAR LIMIT=130] -->
+    <string name="monitoring_description_do_body_vpn">You\'re connected to <xliff:g id="vpn_app">%1$s</xliff:g>, which can monitor your network activity, including emails, apps, and websites.</string>
+
+    <!-- Monitoring dialog: Space that separates the VPN body text and the "Open VPN Settings" link that follows it. [CHAR LIMIT=5] -->
+    <string name="monitoring_description_vpn_settings_separator">" "</string>
+
+    <!-- Monitoring dialog: Link to open the VPN settings page [CHAR LIMIT=TODO] -->
+    <string name="monitoring_description_vpn_settings">Open VPN Settings</string>
+
+    <!-- Monitoring dialog: Network logging text [CHAR LIMIT=TODO] -->
+    <string name="monitoring_description_network_logging">Your admin has turned on network logging, which monitors traffic on your device.\n\nFor more information contact your admin.</string>
+
+
     <!-- Monitoring dialog VPN text [CHAR LIMIT=400] -->
     <string name="monitoring_description_vpn">You gave an app permission to set up a VPN connection.\n\nThis app can monitor your device and network activity, including emails, apps, and websites.</string>
 
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
index 9eceeac..c96506712 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
@@ -31,6 +31,7 @@
 import com.android.internal.app.AssistUtils;
 import com.android.internal.app.IVoiceInteractionSessionListener;
 import com.android.internal.app.IVoiceInteractionSessionShowCallback;
+import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.R;
 import com.android.systemui.statusbar.BaseStatusBar;
 import com.android.systemui.statusbar.CommandQueue;
@@ -282,7 +283,7 @@
 
     @Nullable
     private ComponentName getAssistInfo() {
-        return mAssistUtils.getAssistComponentForUser(UserHandle.USER_CURRENT);
+        return mAssistUtils.getAssistComponentForUser(KeyguardUpdateMonitor.getCurrentUser());
     }
 
     public void showDisclosure() {
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingLog.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingLog.java
index 1338d9d..71ddba5 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingLog.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingLog.java
@@ -84,6 +84,11 @@
         log("I", tag, s);
     }
 
+    public static void wLogcat(String tag, String s) {
+        Log.w(TAG, tag + "\t" + s);
+        log("W", tag, s);
+    }
+
     public static void w(String tag, String s) {
         if (LOGCAT) {
             Log.w(TAG, tag + "\t" + s);
@@ -133,7 +138,7 @@
         pw.println();
     }
 
-    public static synchronized void wtf(String tag, String s) {
+    public static synchronized void wtf(String tag, String s, Throwable here) {
         if (!ENABLED) {
             return;
         }
@@ -161,6 +166,6 @@
             Log.e(TAG, "Unable to write log, build must be debuggable.");
         }
 
-        Log.wtf(TAG, tag + " " + s + "; " + fileMessage);
+        Log.wtf(TAG, tag + " " + s + "; " + fileMessage, here);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManager.java
index 8fc555f..1abea37 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManager.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManager.java
@@ -71,6 +71,7 @@
     private boolean mSessionActive = false;
     private int mState = StatusBarState.SHADE;
     private boolean mScreenOn;
+    private Runnable mPendingWtf;
 
     protected final ContentObserver mSettingsObserver = new ContentObserver(mHandler) {
         @Override
@@ -136,6 +137,7 @@
     private void onSessionStart() {
         if (FalsingLog.ENABLED) {
             FalsingLog.i("onSessionStart", "classifierEnabled=" + isClassiferEnabled());
+            clearPendingWtf();
         }
         mBouncerOn = false;
         mSessionActive = true;
@@ -172,13 +174,35 @@
         if (FalsingLog.ENABLED) {
             // We're getting some false wtfs from touches that happen after the device went
             // to sleep. Only report missing sessions that happen when the device is interactive.
-            if (!mSessionActive && mContext.getSystemService(PowerManager.class).isInteractive()) {
-                FalsingLog.wtf("isFalseTouch", new StringBuilder()
+            if (!mSessionActive && mContext.getSystemService(PowerManager.class).isInteractive()
+                    && mPendingWtf == null) {
+                int enabled = isEnabled() ? 1 : 0;
+                int screenOn = mScreenOn ? 1 : 0;
+                String state = StatusBarState.toShortString(mState);
+                Throwable here = new Throwable("here");
+                FalsingLog.wLogcat("isFalseTouch", new StringBuilder()
                         .append("Session is not active, yet there's a query for a false touch.")
-                        .append(" enabled=").append(isEnabled() ? 1 : 0)
-                        .append(" mScreenOn=").append(mScreenOn ? 1 : 0)
-                        .append(" mState=").append(StatusBarState.toShortString(mState))
+                        .append(" enabled=").append(enabled)
+                        .append(" mScreenOn=").append(screenOn)
+                        .append(" mState=").append(state)
+                        .append(". Escalating to WTF if screen does not turn on soon.")
                         .toString());
+
+                // Unfortunately we're also getting false positives for touches that happen right
+                // after the screen turns on, but before that notification has made it to us.
+                // Unfortunately there's no good way to catch that, except to wait and see if we get
+                // the screen on notification soon.
+                mPendingWtf = () -> FalsingLog.wtf("isFalseTouch", new StringBuilder()
+                        .append("Session did not become active after query for a false touch.")
+                        .append(" enabled=").append(enabled)
+                        .append('/').append(isEnabled() ? 1 : 0)
+                        .append(" mScreenOn=").append(screenOn)
+                        .append('/').append(mScreenOn ? 1 : 0)
+                        .append(" mState=").append(state)
+                        .append('/').append(StatusBarState.toShortString(mState))
+                        .append(". Look for warnings ~1000ms earlier to see root cause.")
+                        .toString(), here);
+                mHandler.postDelayed(mPendingWtf, 1000);
             }
         }
         if (mAccessibilityManager.isTouchExplorationEnabled()) {
@@ -189,6 +213,13 @@
         return mHumanInteractionClassifier.isFalseTouch();
     }
 
+    private void clearPendingWtf() {
+        if (mPendingWtf != null) {
+            mHandler.removeCallbacks(mPendingWtf);
+            mPendingWtf = null;
+        }
+    }
+
     @Override
     public synchronized void onSensorChanged(SensorEvent event) {
         mDataCollector.onSensorChanged(event);
@@ -224,6 +255,7 @@
             FalsingLog.i("onScreenTurningOn", new StringBuilder()
                     .append("from=").append(mScreenOn ? 1 : 0)
                     .toString());
+            clearPendingWtf();
         }
         mScreenOn = true;
         if (sessionEntrypoint()) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 54cd77b..be7a8be 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -17,6 +17,8 @@
 package com.android.systemui.keyguard;
 
 import static android.provider.Settings.System.SCREEN_OFF_TIMEOUT;
+
+import static com.android.internal.telephony.IccCardConstants.State.ABSENT;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT;
@@ -443,7 +445,11 @@
                                 resetStateLocked();
                             }
                         }
-                        onSimNotReadyLocked();
+                        if (simState == ABSENT) {
+                            // MVNO SIMs can become transiently NOT_READY when switching networks,
+                            // so we should only lock when they are ABSENT.
+                            onSimAbsentLocked();
+                        }
                     }
                     break;
                 case PIN_REQUIRED:
@@ -470,7 +476,7 @@
                                   + "show permanently disabled message in lockscreen.");
                             resetStateLocked();
                         }
-                        onSimNotReadyLocked();
+                        onSimAbsentLocked();
                     }
                     break;
                 case READY:
@@ -484,22 +490,20 @@
                 default:
                     if (DEBUG_SIM_STATES) Log.v(TAG, "Unspecific state: " + simState);
                     synchronized (KeyguardViewMediator.this) {
-                        onSimNotReadyLocked();
+                        onSimAbsentLocked();
                     }
                     break;
             }
         }
 
-        private void onSimNotReadyLocked() {
+        private void onSimAbsentLocked() {
             if (isSecure() && mLockWhenSimRemoved) {
                 mLockWhenSimRemoved = false;
                 MetricsLogger.action(mContext,
                         MetricsEvent.ACTION_LOCK_BECAUSE_SIM_REMOVED, mShowing);
                 if (!mShowing) {
-                    if (DEBUG_SIM_STATES) Log.d(TAG, "SIM removed, showing keyguard");
+                    Log.i(TAG, "SIM removed, showing keyguard");
                     doKeyguardLocked(null);
-                } else {
-                    resetStateLocked();
                 }
             }
         }
@@ -1246,7 +1250,7 @@
             // if the setup wizard hasn't run yet, don't show
             final boolean requireSim = !SystemProperties.getBoolean("keyguard.no_require_sim", false);
             final boolean absent = SubscriptionManager.isValidSubscriptionId(
-                    mUpdateMonitor.getNextSubIdForState(IccCardConstants.State.ABSENT));
+                    mUpdateMonitor.getNextSubIdForState(ABSENT));
             final boolean disabled = SubscriptionManager.isValidSubscriptionId(
                     mUpdateMonitor.getNextSubIdForState(IccCardConstants.State.PERM_DISABLED));
             final boolean lockedOrMissing = mUpdateMonitor.isSimPinSecure()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java
index ccb28e9..9431d8d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java
@@ -24,9 +24,14 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.SpannableStringBuilder;
+import android.text.method.LinkMovementMethod;
+import android.text.style.ClickableSpan;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.View.OnClickListener;
 import android.widget.ImageView;
 import android.widget.TextView;
@@ -46,6 +51,7 @@
     private final View mRootView;
     private final TextView mFooterText;
     private final ImageView mFooterIcon;
+    private final ImageView mFooterIcon2;
     private final Context mContext;
     private final Callback mCallback = new Callback();
 
@@ -57,8 +63,10 @@
 
     private boolean mIsVisible;
     private boolean mIsIconVisible;
+    private boolean mIsIcon2Visible;
     private int mFooterTextId;
     private int mFooterIconId;
+    private int mFooterIcon2Id;
 
     public QSFooter(QSPanel qsPanel, Context context) {
         mRootView = LayoutInflater.from(context)
@@ -66,7 +74,9 @@
         mRootView.setOnClickListener(this);
         mFooterText = (TextView) mRootView.findViewById(R.id.footer_text);
         mFooterIcon = (ImageView) mRootView.findViewById(R.id.footer_icon);
+        mFooterIcon2 = (ImageView) mRootView.findViewById(R.id.footer_icon2);
         mFooterIconId = R.drawable.ic_qs_vpn;
+        mFooterIcon2Id = R.drawable.ic_qs_network_logging;
         mContext = context;
         mMainHandler = new Handler();
     }
@@ -103,6 +113,10 @@
     }
 
     private void handleClick() {
+        showDeviceMonitoringDialog();
+    }
+
+    public void showDeviceMonitoringDialog() {
         mHost.collapsePanels();
         // TODO: Delay dialog creation until after panels are collapsed.
         createDialog();
@@ -113,17 +127,29 @@
     }
 
     private void handleRefreshState() {
-        mIsIconVisible = mSecurityController.isVpnEnabled();
         // If the device has device owner, show "Device may be monitored", but --
         // TODO See b/25779452 -- device owner doesn't actually have monitoring power.
+        boolean isVpnEnabled = mSecurityController.isVpnEnabled();
+        boolean isNetworkLoggingEnabled = mSecurityController.isNetworkLoggingEnabled();
+        mIsIconVisible = isVpnEnabled || isNetworkLoggingEnabled;
+        mIsIcon2Visible = isVpnEnabled && isNetworkLoggingEnabled;
         if (mSecurityController.isDeviceManaged()) {
             mFooterTextId = R.string.device_owned_footer;
             mIsVisible = true;
+            int footerIconId = isVpnEnabled
+                    ? R.drawable.ic_qs_vpn
+                    : R.drawable.ic_qs_network_logging;
+            if (mFooterIconId != footerIconId) {
+                mFooterIconId = footerIconId;
+                mMainHandler.post(mUpdateIcon);
+            }
         } else {
             boolean isBranded = mSecurityController.isVpnBranded();
             mFooterTextId = isBranded ? R.string.branded_vpn_footer : R.string.vpn_footer;
             // Update the VPN footer icon, if needed.
-            int footerIconId = isBranded ? R.drawable.ic_qs_branded_vpn : R.drawable.ic_qs_vpn;
+            int footerIconId = isVpnEnabled
+                    ? (isBranded ? R.drawable.ic_qs_branded_vpn : R.drawable.ic_qs_vpn)
+                    : R.drawable.ic_qs_network_logging;
             if (mFooterIconId != footerIconId) {
                 mFooterIconId = footerIconId;
                 mMainHandler.post(mUpdateIcon);
@@ -142,24 +168,60 @@
     }
 
     private void createDialog() {
-        String deviceOwner = mSecurityController.getDeviceOwnerName();
-        String profileOwner = mSecurityController.getProfileOwnerName();
-        String primaryVpn = mSecurityController.getPrimaryVpnName();
-        String profileVpn = mSecurityController.getProfileVpnName();
-        boolean managed = mSecurityController.hasProfileOwner();
-        boolean isBranded = deviceOwner == null && mSecurityController.isVpnBranded();
+        final String deviceOwnerPackage = mSecurityController.getDeviceOwnerName();
+        final String profileOwnerPackage = mSecurityController.getProfileOwnerName();
+        final boolean isNetworkLoggingEnabled = mSecurityController.isNetworkLoggingEnabled();
+        final String primaryVpn = mSecurityController.getPrimaryVpnName();
+        final String profileVpn = mSecurityController.getProfileVpnName();
+        boolean hasProfileOwner = mSecurityController.hasProfileOwner();
+        boolean isBranded = deviceOwnerPackage == null && mSecurityController.isVpnBranded();
 
         mDialog = new SystemUIDialog(mContext);
         if (!isBranded) {
-            mDialog.setTitle(getTitle(deviceOwner));
+            mDialog.setTitle(getTitle(deviceOwnerPackage));
         }
-        mDialog.setMessage(getMessage(deviceOwner, profileOwner, primaryVpn, profileVpn, managed,
-                isBranded));
+        CharSequence msg = getMessage(deviceOwnerPackage, profileOwnerPackage, primaryVpn,
+                profileVpn, hasProfileOwner, isBranded);
+        if (deviceOwnerPackage == null) {
+            mDialog.setMessage(msg);
+        } else {
+            View dialogView = LayoutInflater.from(mContext)
+                   .inflate(R.layout.quick_settings_footer_dialog, null, false);
+            mDialog.setView(dialogView);
+            TextView deviceOwnerWarning =
+                    (TextView) dialogView.findViewById(R.id.device_owner_warning);
+            deviceOwnerWarning.setText(msg);
+            // Make the link "learn more" clickable.
+            deviceOwnerWarning.setMovementMethod(new LinkMovementMethod());
+            if (primaryVpn == null) {
+                dialogView.findViewById(R.id.vpn_icon).setVisibility(View.GONE);
+                dialogView.findViewById(R.id.vpn_subtitle).setVisibility(View.GONE);
+                dialogView.findViewById(R.id.vpn_warning).setVisibility(View.GONE);
+            } else {
+                final SpannableStringBuilder message = new SpannableStringBuilder();
+                message.append(mContext.getString(R.string.monitoring_description_do_body_vpn,
+                        primaryVpn));
+                message.append(mContext.getString(
+                        R.string.monitoring_description_vpn_settings_separator));
+                message.append(mContext.getString(R.string.monitoring_description_vpn_settings),
+                        new VpnSpan(), 0);
+
+                TextView vpnWarning = (TextView) dialogView.findViewById(R.id.vpn_warning);
+                vpnWarning.setText(message);
+                // Make the link "Open VPN Settings" clickable.
+                vpnWarning.setMovementMethod(new LinkMovementMethod());
+            }
+            if (!isNetworkLoggingEnabled) {
+                dialogView.findViewById(R.id.network_logging_icon).setVisibility(View.GONE);
+                dialogView.findViewById(R.id.network_logging_subtitle).setVisibility(View.GONE);
+                dialogView.findViewById(R.id.network_logging_warning).setVisibility(View.GONE);
+            }
+        }
+
         mDialog.setButton(DialogInterface.BUTTON_POSITIVE, getPositiveButton(isBranded), this);
-        if (mSecurityController.isVpnEnabled() && !mSecurityController.isVpnRestricted()) {
-            mDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getSettingsButton(), this);
-        }
         mDialog.show();
+        mDialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT,
+                                      ViewGroup.LayoutParams.WRAP_CONTENT);
     }
 
     private String getSettingsButton() {
@@ -170,22 +232,15 @@
         return mContext.getString(isBranded ? android.R.string.ok : R.string.quick_settings_done);
     }
 
-    private String getMessage(String deviceOwner, String profileOwner, String primaryVpn,
-            String profileVpn, boolean primaryUserIsManaged, boolean isBranded) {
-        // Show a special warning when the device has device owner, but --
-        // TODO See b/25779452 -- device owner doesn't actually have monitoring power.
-        if (deviceOwner != null) {
-            if (primaryVpn != null) {
-                return mContext.getString(R.string.monitoring_description_vpn_app_device_owned,
-                        deviceOwner, primaryVpn);
-            } else {
-                return mContext.getString(R.string.monitoring_description_device_owned,
-                        deviceOwner);
-            }
+    protected CharSequence getMessage(String deviceOwnerPackage, String profileOwnerPackage,
+            String primaryVpn, String profileVpn, boolean hasProfileOwner, boolean isBranded) {
+        if (deviceOwnerPackage != null) {
+            return mContext.getString(R.string.monitoring_description_device_owned,
+                    deviceOwnerPackage);
         } else if (primaryVpn != null) {
             if (profileVpn != null) {
                 return mContext.getString(R.string.monitoring_description_app_personal_work,
-                        profileOwner, profileVpn, primaryVpn);
+                        profileOwnerPackage, profileVpn, primaryVpn);
             } else {
                 if (isBranded) {
                     return mContext.getString(R.string.branded_monitoring_description_app_personal,
@@ -197,10 +252,10 @@
             }
         } else if (profileVpn != null) {
             return mContext.getString(R.string.monitoring_description_app_work,
-                    profileOwner, profileVpn);
-        } else if (profileOwner != null && primaryUserIsManaged) {
+                    profileOwnerPackage, profileVpn);
+        } else if (profileOwnerPackage != null && hasProfileOwner) {
             return mContext.getString(R.string.monitoring_description_device_owned,
-                    profileOwner);
+                    profileOwnerPackage);
         } else {
             // No device owner, no personal VPN, no work VPN, no user owner. Why are we here?
             return null;
@@ -219,6 +274,7 @@
         @Override
         public void run() {
             mFooterIcon.setImageResource(mFooterIconId);
+            mFooterIcon2.setImageResource(mFooterIcon2Id);
         }
     };
 
@@ -230,6 +286,7 @@
             }
             mRootView.setVisibility(mIsVisible ? View.VISIBLE : View.GONE);
             mFooterIcon.setVisibility(mIsIconVisible ? View.VISIBLE : View.INVISIBLE);
+            mFooterIcon2.setVisibility(mIsIcon2Visible ? View.VISIBLE : View.INVISIBLE);
         }
     };
 
@@ -267,4 +324,13 @@
         }
     }
 
+    protected class VpnSpan extends ClickableSpan {
+        @Override
+        public void onClick(View widget) {
+            final Intent intent = new Intent(Settings.ACTION_VPN_SETTINGS);
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+            mDialog.dismiss();
+            mContext.startActivity(intent);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index fef8930..bdb488f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -511,6 +511,10 @@
         return mFooter;
     }
 
+    public void showDeviceMonitoringDialog() {
+        mFooter.showDeviceMonitoringDialog();
+    }
+
     private class H extends Handler {
         private static final int SHOW_DETAIL = 1;
         private static final int SET_TILE_VISIBILITY = 2;
diff --git a/packages/SystemUI/src/com/android/systemui/recents/Recents.java b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
index eb9beb6..ba50c66 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/Recents.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
@@ -57,7 +57,6 @@
 import com.android.systemui.recents.events.ui.RecentsDrawnEvent;
 import com.android.systemui.recents.misc.SystemServicesProxy;
 import com.android.systemui.recents.model.RecentsTaskLoader;
-import com.android.systemui.recents.grid.RecentsGridImpl;
 import com.android.systemui.recents.tv.RecentsTvImpl;
 import com.android.systemui.stackdivider.Divider;
 
@@ -84,7 +83,6 @@
     static {
         RECENTS_ACTIVITIES.add(RecentsImpl.RECENTS_ACTIVITY);
         RECENTS_ACTIVITIES.add(RecentsTvImpl.RECENTS_TV_ACTIVITY);
-        RECENTS_ACTIVITIES.add(RecentsGridImpl.RECENTS_MOSAIC_ACTIVITY);
     }
 
     // Purely for experimentation
@@ -207,8 +205,6 @@
                 getSystemService(Context.UI_MODE_SERVICE);
         if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
             mImpl = new RecentsTvImpl(mContext);
-        } else if (SystemProperties.getBoolean("ro.recents.grid", false) == true) {
-            mImpl = new RecentsGridImpl(mContext);
         } else {
             mImpl = new RecentsImpl(mContext);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java
index 73c6e6e..711f0c6 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java
@@ -20,6 +20,7 @@
 import android.content.res.Resources;
 import android.graphics.Rect;
 
+import android.os.SystemProperties;
 import com.android.systemui.R;
 import com.android.systemui.recents.misc.SystemServicesProxy;
 
@@ -58,6 +59,10 @@
     public boolean fakeShadows;
     public int svelteLevel;
 
+    // Whether this product supports Grid-based Recents. If this is field is set to true, then
+    // Recents will layout task views in a grid mode when there's enough space in the screen.
+    public boolean isGridEnabled;
+
     public RecentsConfiguration(Context context) {
         // Load only resources that can not change after the first load either through developer
         // settings or via multi window
@@ -66,6 +71,7 @@
         Resources res = appContext.getResources();
         fakeShadows = res.getBoolean(R.bool.config_recents_fake_shadows);
         svelteLevel = res.getInteger(R.integer.recents_svelte_level);
+        isGridEnabled = SystemProperties.getBoolean("ro.recents.grid", false);
 
         float screenDensity = context.getResources().getDisplayMetrics().density;
         smallestWidth = ssp.getDeviceSmallestWidth();
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
index 42d1b61..2263a4e 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
@@ -180,10 +180,7 @@
         ssp.registerTaskStackListener(mTaskStackListener);
 
         // Initialize the static configuration resources
-        LayoutInflater inflater = LayoutInflater.from(mContext);
         mDummyStackView = new TaskStackView(mContext);
-        mHeaderBar = (TaskViewHeader) inflater.inflate(R.layout.recents_task_view_header,
-                null, false);
         reloadResources();
     }
 
@@ -204,14 +201,6 @@
         Resources res = mContext.getResources();
         reloadResources();
         mDummyStackView.reloadOnConfigurationChange();
-        // Update the header bar direction directly as it is not attached to anything and does not
-        // layout except in updateHeaderBarLayout()
-        mHeaderBar.setLayoutDirection(res.getConfiguration().getLayoutDirection());
-        mHeaderBar.onConfigurationChanged();
-        mHeaderBar.forceLayout();
-        mHeaderBar.measure(
-                MeasureSpec.makeMeasureSpec(mHeaderBar.getMeasuredWidth(), MeasureSpec.EXACTLY),
-                MeasureSpec.makeMeasureSpec(mHeaderBar.getMeasuredHeight(), MeasureSpec.EXACTLY));
     }
 
     /**
@@ -582,7 +571,13 @@
                 R.dimen.recents_task_view_header_height,
                 R.dimen.recents_task_view_header_height_tablet_land,
                 R.dimen.recents_task_view_header_height,
-                R.dimen.recents_task_view_header_height_tablet_land);
+                R.dimen.recents_task_view_header_height_tablet_land,
+                R.dimen.recents_grid_task_view_header_height);
+
+        LayoutInflater inflater = LayoutInflater.from(mContext);
+        mHeaderBar = (TaskViewHeader) inflater.inflate(R.layout.recents_task_view_header,
+                null, false);
+        mHeaderBar.setLayoutDirection(res.getConfiguration().getLayoutDirection());
     }
 
     /**
@@ -721,7 +716,7 @@
                 if (task.isFreeformTask()) {
                     mTmpTransform = stackLayout.getStackTransformScreenCoordinates(task,
                             stackScroller.getStackScroll(), mTmpTransform, null,
-                            windowOverrideRect);
+                            windowOverrideRect, false /* useGridLayout */);
                     Bitmap thumbnail = drawThumbnailTransitionBitmap(task, mTmpTransform,
                             mThumbTransitionBitmapCache);
                     Rect toTaskRect = new Rect();
@@ -771,7 +766,8 @@
         stackView.updateLayoutAlgorithm(true /* boundScroll */);
         stackView.updateToInitialState();
         stackView.getStackAlgorithm().getStackTransformScreenCoordinates(launchTask,
-                stackView.getScroller().getStackScroll(), mTmpTransform, null, windowOverrideRect);
+                stackView.getScroller().getStackScroll(), mTmpTransform, null, windowOverrideRect,
+                Recents.getConfiguration().isGridEnabled);
         return mTmpTransform;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/grid/RecentsGridActivity.java b/packages/SystemUI/src/com/android/systemui/recents/grid/RecentsGridActivity.java
deleted file mode 100644
index cf45570..0000000
--- a/packages/SystemUI/src/com/android/systemui/recents/grid/RecentsGridActivity.java
+++ /dev/null
@@ -1,317 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.recents.grid;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.content.res.Configuration;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.MetricsProto.MetricsEvent;
-import com.android.systemui.R;
-import com.android.systemui.recents.Recents;
-import com.android.systemui.recents.RecentsActivity;
-import com.android.systemui.recents.RecentsActivityLaunchState;
-import com.android.systemui.recents.RecentsConfiguration;
-import com.android.systemui.recents.RecentsImpl;
-import com.android.systemui.recents.events.EventBus;
-import com.android.systemui.recents.events.activity.ConfigurationChangedEvent;
-import com.android.systemui.recents.events.activity.HideRecentsEvent;
-import com.android.systemui.recents.events.activity.LaunchNextTaskRequestEvent;
-import com.android.systemui.recents.events.activity.LaunchTaskEvent;
-import com.android.systemui.recents.events.activity.ToggleRecentsEvent;
-import com.android.systemui.recents.events.component.RecentsVisibilityChangedEvent;
-import com.android.systemui.recents.events.ui.AllTaskViewsDismissedEvent;
-import com.android.systemui.recents.events.ui.DeleteTaskDataEvent;
-import com.android.systemui.recents.events.ui.DismissAllTaskViewsEvent;
-import com.android.systemui.recents.events.ui.DismissTaskViewEvent;
-import com.android.systemui.recents.events.ui.TaskViewDismissedEvent;
-import com.android.systemui.recents.misc.SystemServicesProxy;
-import com.android.systemui.recents.misc.Utilities;
-import com.android.systemui.recents.model.RecentsTaskLoadPlan;
-import com.android.systemui.recents.model.RecentsTaskLoader;
-import com.android.systemui.recents.model.Task;
-import com.android.systemui.recents.model.TaskStack;
-import com.android.systemui.recents.views.TaskView;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * The main grid recents activity started by the RecentsImpl.
- */
-public class RecentsGridActivity extends Activity implements ViewTreeObserver.OnPreDrawListener {
-    private final static String TAG = "RecentsGridActivity";
-
-    private TaskStack mTaskStack;
-    private List<Task> mTasks = new ArrayList<>();
-    private List<View> mTaskViews = new ArrayList<>();
-    private FrameLayout mRecentsView;
-    private TextView mEmptyView;
-    private View mClearAllButton;
-    private int mDisplayOrientation = Configuration.ORIENTATION_UNDEFINED;
-    private Rect mDisplayRect = new Rect();
-    private LayoutInflater mInflater;
-    private boolean mTouchExplorationEnabled;
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.recents_grid);
-        SystemServicesProxy ssp = Recents.getSystemServices();
-
-        mInflater = LayoutInflater.from(this);
-        mDisplayOrientation = Utilities.getAppConfiguration(this).orientation;
-        mDisplayRect = ssp.getDisplayRect();
-        mTouchExplorationEnabled = ssp.isTouchExplorationEnabled();
-
-        mRecentsView = (FrameLayout) findViewById(R.id.recents_view);
-        LinearLayout recentsContainer = (LinearLayout) findViewById(R.id.recents_container);
-        mEmptyView = (TextView) mInflater.inflate(R.layout.recents_empty, recentsContainer, false);
-        mClearAllButton = findViewById(R.id.button);
-
-        FrameLayout.LayoutParams emptyViewLayoutParams = new FrameLayout.LayoutParams(
-            FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);
-        emptyViewLayoutParams.gravity = Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL;
-        mEmptyView.setLayoutParams(emptyViewLayoutParams);
-        mRecentsView.addView(mEmptyView);
-
-        mClearAllButton.setVisibility(View.VISIBLE);
-        LinearLayout.LayoutParams lp =
-                (LinearLayout.LayoutParams) mClearAllButton.getLayoutParams();
-        lp.gravity = Gravity.END;
-
-        mClearAllButton.setOnClickListener(v -> {
-            EventBus.getDefault().send(new DismissAllTaskViewsEvent());
-        });
-
-        mRecentsView.setOnClickListener(v -> {
-            EventBus.getDefault().send(new HideRecentsEvent(
-                    false /* triggeredFromAltTab */, false /* triggeredFromHomeKey */));
-        });
-
-        EventBus.getDefault().register(this, RecentsActivity.EVENT_BUS_PRIORITY);
-    }
-
-    private TaskView createView() {
-        return (TaskView) mInflater.inflate(R.layout.recents_task_view, mRecentsView, false);
-    }
-
-    private void clearTaskViews() {
-        for (View taskView : mTaskViews) {
-            ViewGroup parent = (ViewGroup) taskView.getParent();
-            if (parent != null) {
-                parent.removeView(taskView);
-            }
-        }
-        mTaskViews.clear();
-    }
-
-    private void updateControlVisibility() {
-        boolean empty = (mTasks.size() == 0);
-        mClearAllButton.setVisibility(empty ? View.INVISIBLE : View.VISIBLE);
-        mEmptyView.setVisibility(empty ? View.VISIBLE : View.INVISIBLE);
-        if (empty) {
-            mEmptyView.bringToFront();
-        }
-    }
-
-    private void updateRecentsTasks() {
-        RecentsTaskLoader loader = Recents.getTaskLoader();
-        RecentsTaskLoadPlan plan = RecentsImpl.consumeInstanceLoadPlan();
-        if (plan == null) {
-            plan = loader.createLoadPlan(this);
-        }
-        RecentsConfiguration config = Recents.getConfiguration();
-        RecentsActivityLaunchState launchState = config.getLaunchState();
-        if (!plan.hasTasks()) {
-            loader.preloadTasks(plan, -1, !launchState.launchedFromHome);
-        }
-        int numVisibleTasks = 9;
-        mTaskStack = plan.getTaskStack();
-        RecentsTaskLoadPlan.Options loadOpts = new RecentsTaskLoadPlan.Options();
-        loadOpts.runningTaskId = launchState.launchedToTaskId;
-        loadOpts.numVisibleTasks = numVisibleTasks;
-        loadOpts.numVisibleTaskThumbnails = numVisibleTasks;
-        loader.loadTasks(this, plan, loadOpts);
-
-        List<Task> stackTasks = mTaskStack.getStackTasks();
-        Collections.reverse(stackTasks);
-        mTasks = stackTasks;
-
-        updateControlVisibility();
-
-        clearTaskViews();
-        for (int i = 0; i < mTasks.size(); i++) {
-            Task task = mTasks.get(i);
-            TaskView taskView = createView();
-            taskView.onTaskBound(task, mTouchExplorationEnabled, mDisplayOrientation, mDisplayRect);
-            Recents.getTaskLoader().loadTaskData(task);
-            taskView.setTouchEnabled(true);
-            // Show dismiss button right away.
-            taskView.startNoUserInteractionAnimation();
-            mTaskViews.add(taskView);
-        }
-    }
-
-    @Override
-    protected void onStart() {
-        super.onStart();
-        EventBus.getDefault().send(new RecentsVisibilityChangedEvent(this, true));
-        mRecentsView.getViewTreeObserver().addOnPreDrawListener(this);
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-        updateRecentsTasks();
-    }
-
-    @Override
-    protected void onStop() {
-        super.onStop();
-        EventBus.getDefault().send(new RecentsVisibilityChangedEvent(this, false));
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        EventBus.getDefault().unregister(this);
-    }
-
-    @Override
-    public void onBackPressed() {
-        // Back behaves like the recents button so just trigger a toggle event.
-        EventBus.getDefault().send(new ToggleRecentsEvent());
-    }
-
-    @Override
-    public boolean onPreDraw() {
-        mRecentsView.getViewTreeObserver().removeOnPreDrawListener(this);
-        int width = mRecentsView.getWidth();
-        int height = mRecentsView.getHeight();
-
-        List<Rect> rects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
-            mTasks.size(), width, height, false /* allowLineOfThree */, 30 /* padding */);
-        for (int i = 0; i < rects.size(); i++) {
-            Rect rect = rects.get(i);
-            View taskView = mTaskViews.get(i);
-            taskView.setLayoutParams(new FrameLayout.LayoutParams(rect.width(), rect.height()));
-            taskView.setTranslationX(rect.left);
-            taskView.setTranslationY(rect.top);
-            mRecentsView.addView(taskView);
-        }
-        return true;
-    }
-
-    void dismissRecentsToHome() {
-        Intent startMain = new Intent(Intent.ACTION_MAIN);
-        startMain.addCategory(Intent.CATEGORY_HOME);
-        startActivity(startMain);
-    }
-
-    /**** EventBus events ****/
-
-    public final void onBusEvent(HideRecentsEvent event) {
-        if (event.triggeredFromAltTab) {
-            // Do nothing for now.
-        } else if (event.triggeredFromHomeKey) {
-            dismissRecentsToHome();
-        }
-    }
-
-    public final void onBusEvent(ToggleRecentsEvent event) {
-        // Always go back home for simplicity for now. If recents is entered from another app, this
-        // code will eventually need to go back to the original app.
-        dismissRecentsToHome();
-    }
-
-    public final void onBusEvent(DismissTaskViewEvent event) {
-        int taskIndex = mTaskViews.indexOf(event.taskView);
-        if (taskIndex != -1) {
-            mTasks.remove(taskIndex);
-            ((ViewGroup) event.taskView.getParent()).removeView(event.taskView);
-            mTaskViews.remove(taskIndex);
-            EventBus.getDefault().send(
-                    new TaskViewDismissedEvent(event.taskView.getTask(), event.taskView, null));
-        }
-    }
-
-    public final void onBusEvent(TaskViewDismissedEvent event) {
-        mRecentsView.announceForAccessibility(this.getString(
-            R.string.accessibility_recents_item_dismissed, event.task.title));
-        updateControlVisibility();
-
-        EventBus.getDefault().send(new DeleteTaskDataEvent(event.task));
-
-        MetricsLogger.action(this, MetricsEvent.OVERVIEW_DISMISS,
-                event.task.key.getComponent().toString());
-    }
-
-    public final void onBusEvent(DeleteTaskDataEvent event) {
-        // Remove any stored data from the loader.
-        RecentsTaskLoader loader = Recents.getTaskLoader();
-        loader.deleteTaskData(event.task, false);
-
-        // Remove the task from activity manager.
-        SystemServicesProxy ssp = Recents.getSystemServices();
-        ssp.removeTask(event.task.key.id);
-    }
-
-    public final void onBusEvent(final DismissAllTaskViewsEvent event) {
-        // Keep track of the tasks which will have their data removed.
-        ArrayList<Task> tasks = new ArrayList<>(mTaskStack.getStackTasks());
-        mRecentsView.announceForAccessibility(this.getString(
-                R.string.accessibility_recents_all_items_dismissed));
-        mTaskStack.removeAllTasks();
-        for (int i = tasks.size() - 1; i >= 0; i--) {
-            EventBus.getDefault().send(new DeleteTaskDataEvent(tasks.get(i)));
-        }
-        mTasks = new ArrayList<>();
-        updateRecentsTasks();
-
-        MetricsLogger.action(this, MetricsEvent.OVERVIEW_DISMISS_ALL);
-    }
-
-    public final void onBusEvent(AllTaskViewsDismissedEvent event) {
-        SystemServicesProxy ssp = Recents.getSystemServices();
-        if (!ssp.hasDockedTask()) {
-            dismissRecentsToHome();
-        }
-    }
-
-    public final void onBusEvent(LaunchNextTaskRequestEvent event) {
-        // Always go back home for simplicity for now. Quick switch will be supported soon.
-        EventBus.getDefault().send(new HideRecentsEvent(false, true));
-    }
-
-    public final void onBusEvent(LaunchTaskEvent event) {
-        startActivity(event.task.key.baseIntent);
-    }
-}
-
diff --git a/packages/SystemUI/src/com/android/systemui/recents/grid/RecentsGridImpl.java b/packages/SystemUI/src/com/android/systemui/recents/grid/RecentsGridImpl.java
deleted file mode 100644
index ba7a4a7..0000000
--- a/packages/SystemUI/src/com/android/systemui/recents/grid/RecentsGridImpl.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.recents.grid;
-
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.Intent;
-import android.os.UserHandle;
-
-import com.android.systemui.recents.RecentsImpl;
-import com.android.systemui.recents.events.EventBus;
-import com.android.systemui.recents.events.activity.RecentsActivityStartingEvent;
-
-public class RecentsGridImpl extends RecentsImpl {
-    public static final String RECENTS_MOSAIC_ACTIVITY =
-            "com.android.systemui.recents.grid.RecentsGridActivity";
-
-    public RecentsGridImpl(Context context) {
-        super(context);
-    }
-
-    @Override
-    protected void startRecentsActivity(ActivityManager.RunningTaskInfo runningTask,
-            boolean isHomeStackVisible, boolean animate, int growTarget) {
-        Intent intent = new Intent();
-        intent.setClassName(RECENTS_PACKAGE, RECENTS_MOSAIC_ACTIVITY);
-        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
-                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
-                | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
-
-        mContext.startActivityAsUser(intent, UserHandle.CURRENT);
-        EventBus.getDefault().send(new RecentsActivityStartingEvent());
-    }
-}
-
diff --git a/packages/SystemUI/src/com/android/systemui/recents/grid/TaskGridLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/grid/TaskGridLayoutAlgorithm.java
deleted file mode 100644
index 6e2c6c0..0000000
--- a/packages/SystemUI/src/com/android/systemui/recents/grid/TaskGridLayoutAlgorithm.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.recents.grid;
-
-import android.graphics.Rect;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.List;
-
-class TaskGridLayoutAlgorithm {
-
-    private static final String TAG = "TaskGridLayoutAlgorithm";
-
-    static List<Rect> getRectsForTaskCount(int count, int containerWidth, int containerHeight,
-            boolean allowLineOfThree, int padding) {
-        return getRectsForTaskCount(count, containerWidth, containerHeight, allowLineOfThree,
-                padding, null);
-    }
-
-    static List<Rect> getRectsForTaskCount(int count, int containerWidth, int containerHeight,
-            boolean allowLineOfThree, int padding, Rect preCalculatedTile) {
-        int singleLineMaxCount = allowLineOfThree ? 3 : 2;
-        List<Rect> rects = new ArrayList<>(count);
-        boolean landscape = (containerWidth > containerHeight);
-
-        // We support at most 9 tasks in this layout.
-        count = Math.min(count, 9);
-
-        if (count == 0) {
-            return rects;
-        }
-        if (count <= singleLineMaxCount) {
-            if (landscape) {
-                // Single line.
-                int taskWidth = 0;
-                int emptySpace = 0;
-                if (preCalculatedTile != null) {
-                    taskWidth = preCalculatedTile.width();
-                    emptySpace = containerWidth - (count * taskWidth) - (count - 1) * padding;
-                } else {
-                    // Divide available space in equal parts.
-                    taskWidth = (containerWidth - (count - 1) * padding) / count;
-                }
-                for (int i = 0; i < count; i++) {
-                    int left = emptySpace / 2 + i * taskWidth + i * padding;
-                    rects.add(new Rect(left, 0, left + taskWidth, containerHeight));
-                }
-            } else {
-                // Single column. Divide available space in equal parts.
-                int taskHeight = (containerHeight - (count - 1) * padding) / count;
-                for (int i = 0; i < count; i++) {
-                    int top = i * taskHeight + i * padding;
-                    rects.add(new Rect(0, top, containerWidth, top + taskHeight));
-                }
-            }
-        } else if (count < 7) {
-            // Two lines.
-            int lineHeight = (containerHeight - padding) / 2;
-            int lineTaskCount = (int) Math.ceil((double) count / 2);
-            List<Rect> rectsA = getRectsForTaskCount(
-                    lineTaskCount, containerWidth, lineHeight, true /* allowLineOfThree */, padding,
-                            null);
-            List<Rect> rectsB = getRectsForTaskCount(
-                    count - lineTaskCount, containerWidth, lineHeight, true /* allowLineOfThree */,
-                            padding, rectsA.get(0));
-            for (Rect rect : rectsB) {
-                rect.offset(0, lineHeight + padding);
-            }
-            rects.addAll(rectsA);
-            rects.addAll(rectsB);
-        } else {
-            // Three lines.
-            int lineHeight = (containerHeight - 2 * padding) / 3;
-            int lineTaskCount = (int) Math.ceil((double) count / 3);
-            List<Rect> rectsA = getRectsForTaskCount(
-                lineTaskCount, containerWidth, lineHeight, true /* allowLineOfThree */, padding, null);
-            List<Rect> rectsB = getRectsForTaskCount(
-                lineTaskCount, containerWidth, lineHeight, true /* allowLineOfThree */, padding,
-                        rectsA.get(0));
-            List<Rect> rectsC = getRectsForTaskCount(
-                count - (2 * lineTaskCount), containerWidth, lineHeight,
-                         true /* allowLineOfThree */, padding, rectsA.get(0));
-            for (Rect rect : rectsB) {
-                rect.offset(0, lineHeight + padding);
-            }
-            for (Rect rect : rectsC) {
-                rect.offset(0, 2 * (lineHeight + padding));
-            }
-            rects.addAll(rectsA);
-            rects.addAll(rectsB);
-            rects.addAll(rectsC);
-        }
-        return rects;
-    }
-}
-
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java b/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java
index 2c5c437..d64fc36 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java
@@ -79,14 +79,15 @@
     public static final Rect EMPTY_RECT = new Rect();
 
     /**
-     * @return the first parent walking up the view hierarchy that has the given class type.
+     * @return the first parent walking up the view hierarchy that has the given class type (or
+     * a subclass).
      *
      * @param parentClass must be a class derived from {@link View}
      */
     public static <T extends View> T findParent(View v, Class<T> parentClass) {
         ViewParent parent = v.getParent();
         while (parent != null) {
-            if (parent.getClass().equals(parentClass)) {
+            if (parentClass.isAssignableFrom(parent.getClass())) {
                 return (T) parent;
             }
             parent = parent.getParent();
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
index 745f5a5..178cb9f 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
@@ -849,6 +849,24 @@
         return null;
     }
 
+    /**
+     * Returns the task in stack tasks which should be launched next if Recents are toggled
+     * again, or null if there is no task to be launched.
+     */
+    public Task getNextLaunchTarget() {
+        int taskCount = getTaskCount();
+        if (taskCount == 0) {
+            return null;
+        }
+        int launchTaskIndex = indexOfStackTask(getLaunchTarget());
+        if (launchTaskIndex != -1) {
+            launchTaskIndex = Math.max(0, launchTaskIndex - 1);
+        } else {
+            launchTaskIndex = getTaskCount() - 1;
+        }
+        return getStackTasks().get(launchTaskIndex);
+    }
+
     /** Returns the index of this task in this current task stack */
     public int indexOfStackTask(Task t) {
         return mStackTaskList.indexOf(t);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/AnimateableViewBounds.java b/packages/SystemUI/src/com/android/systemui/recents/views/AnimateableViewBounds.java
index 253d06a..dba085e 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/AnimateableViewBounds.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/AnimateableViewBounds.java
@@ -30,17 +30,17 @@
     private static final float MIN_ALPHA = 0.1f;
     private static final float MAX_ALPHA = 0.8f;
 
-    View mSourceView;
+    protected View mSourceView;
     @ViewDebug.ExportedProperty(category="recents")
-    Rect mClipRect = new Rect();
+    protected Rect mClipRect = new Rect();
     @ViewDebug.ExportedProperty(category="recents")
-    Rect mClipBounds = new Rect();
+    protected Rect mClipBounds = new Rect();
     @ViewDebug.ExportedProperty(category="recents")
-    Rect mLastClipBounds = new Rect();
+    protected Rect mLastClipBounds = new Rect();
     @ViewDebug.ExportedProperty(category="recents")
-    int mCornerRadius;
+    protected int mCornerRadius;
     @ViewDebug.ExportedProperty(category="recents")
-    float mAlpha = 1f;
+    protected float mAlpha = 1f;
 
     public AnimateableViewBounds(View source, int cornerRadius) {
         mSourceView = source;
@@ -110,7 +110,7 @@
         return mClipRect.bottom;
     }
 
-    private void updateClipBounds() {
+    protected void updateClipBounds() {
         mClipBounds.set(Math.max(0, mClipRect.left), Math.max(0, mClipRect.top),
                 mSourceView.getWidth() - Math.max(0, mClipRect.right),
                 mSourceView.getHeight() - Math.max(0, mClipRect.bottom));
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
index 24ef433..8d32c9c 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
@@ -95,7 +95,6 @@
     private static final int SHOW_STACK_ACTION_BUTTON_DURATION = 134;
     private static final int HIDE_STACK_ACTION_BUTTON_DURATION = 100;
 
-    private TaskStack mStack;
     private TaskStackView mTaskStackView;
     private TextView mStackActionButton;
     private TextView mEmptyView;
@@ -195,7 +194,6 @@
      * Called from RecentsActivity when the task stack is updated.
      */
     public void updateStack(TaskStack stack, boolean setStackViewTasks) {
-        mStack = stack;
         if (setStackViewTasks) {
             mTaskStackView.setTasks(stack, true /* allowNotifyStackChanges */);
         }
@@ -212,7 +210,7 @@
      * Returns the current TaskStack.
      */
     public TaskStack getStack() {
-        return mStack;
+        return mTaskStackView.getStack();
     }
 
     /*
@@ -251,8 +249,7 @@
     /** Launches the task that recents was launched from if possible */
     public boolean launchPreviousTask() {
         if (mTaskStackView != null) {
-            TaskStack stack = mTaskStackView.getStack();
-            Task task = stack.getLaunchTarget();
+            Task task = getStack().getLaunchTarget();
             if (task != null) {
                 TaskView taskView = mTaskStackView.getChildViewForTask(task);
                 EventBus.getDefault().send(new LaunchTaskEvent(taskView, task, null,
@@ -340,7 +337,8 @@
 
         if (RecentsDebugFlags.Static.EnableStackActionButton) {
             // Measure the stack action button within the constraints of the space above the stack
-            Rect buttonBounds = mTaskStackView.mLayoutAlgorithm.mStackActionButtonRect;
+            Rect buttonBounds = mTaskStackView.mLayoutAlgorithm.getStackActionButtonRect(
+                    mTaskStackView.useGridLayout());
             measureChild(mStackActionButton,
                     MeasureSpec.makeMeasureSpec(buttonBounds.width(), MeasureSpec.AT_MOST),
                     MeasureSpec.makeMeasureSpec(buttonBounds.height(), MeasureSpec.AT_MOST));
@@ -437,8 +435,9 @@
 
     public final void onBusEvent(LaunchTaskEvent event) {
         mLastTaskLaunchedWasFreeform = event.task.isFreeformTask();
-        mTransitionHelper.launchTaskFromRecents(mStack, event.task, mTaskStackView, event.taskView,
-                event.screenPinningRequested, event.targetTaskBounds, event.targetTaskStack);
+        mTransitionHelper.launchTaskFromRecents(getStack(), event.task, mTaskStackView,
+                event.taskView, event.screenPinningRequested, event.targetTaskBounds,
+                event.targetTaskStack);
     }
 
     public final void onBusEvent(DismissRecentsToHomeAnimationStarted event) {
@@ -514,8 +513,7 @@
                         EventBus.getDefault().send(new DockedFirstAnimationFrameEvent());
                         // Remove the task and don't bother relaying out, as all the tasks will be
                         // relaid out when the stack changes on the multiwindow change event
-                        mTaskStackView.getStack().removeTask(event.task, null,
-                                true /* fromDockGesture */);
+                        getStack().removeTask(event.task, null, true /* fromDockGesture */);
                     }
                 };
 
@@ -536,7 +534,7 @@
                 MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_DRAG_DROP,
                         event.task.getTopComponent().flattenToShortString());
             } else {
-                EventBus.getDefault().send(new DragEndCancelledEvent(mStack, event.task,
+                EventBus.getDefault().send(new DragEndCancelledEvent(getStack(), event.task,
                         event.taskView));
             }
         } else {
@@ -598,7 +596,7 @@
     public final void onBusEvent(EnterRecentsWindowAnimationCompletedEvent event) {
         RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
         if (!launchState.launchedViaDockGesture && !launchState.launchedFromApp
-                && mStack.getTaskCount() > 0) {
+                && getStack().getTaskCount() > 0) {
             animateBackgroundScrim(1f,
                     TaskStackAnimationHelper.ENTER_FROM_HOME_TRANSLATION_DURATION);
         }
@@ -775,7 +773,8 @@
      * @return the bounds of the stack action button.
      */
     private Rect getStackActionButtonBoundsFromStackLayout() {
-        Rect actionButtonRect = new Rect(mTaskStackView.mLayoutAlgorithm.mStackActionButtonRect);
+        Rect actionButtonRect = new Rect(mTaskStackView.mLayoutAlgorithm.getStackActionButtonRect(
+                mTaskStackView.useGridLayout()));
         int left = isLayoutRtl()
                 ? actionButtonRect.left - mStackActionButton.getPaddingLeft()
                 : actionButtonRect.right + mStackActionButton.getPaddingRight()
@@ -797,8 +796,8 @@
         writer.print(" [0x"); writer.print(id); writer.print("]");
         writer.println();
 
-        if (mStack != null) {
-            mStack.dump(innerPrefix, writer);
+        if (getStack() != null) {
+            getStack().dump(innerPrefix, writer);
         }
         if (mTaskStackView != null) {
             mTaskStackView.dump(innerPrefix, writer);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java
index 493e618..c1f4c8a 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java
@@ -157,7 +157,7 @@
 
             // Get the current transform for the task, which will be used to position it offscreen
             stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
-                    null);
+                    null, mStackView.useGridLayout());
 
             if (hideTask) {
                 tv.setVisibility(View.INVISIBLE);
@@ -230,7 +230,7 @@
             // Get the current transform for the task, which will be updated to the final transform
             // to animate to depending on how recents was invoked
             stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
-                    null);
+                    null, mStackView.useGridLayout());
 
             if (launchState.launchedFromApp && !launchState.launchedViaDockGesture) {
                 if (task.isLaunchTarget) {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
index fce7f9d..f0644a5 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
@@ -38,6 +38,7 @@
 import com.android.systemui.recents.misc.Utilities;
 import com.android.systemui.recents.model.Task;
 import com.android.systemui.recents.model.TaskStack;
+import com.android.systemui.recents.views.grid.TaskGridLayoutAlgorithm;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
@@ -240,14 +241,14 @@
     // This is the current system insets
     @ViewDebug.ExportedProperty(category="recents")
     public Rect mSystemInsets = new Rect();
-    // This is the bounds of the stack action above the stack rect
-    @ViewDebug.ExportedProperty(category="recents")
-    public Rect mStackActionButtonRect = new Rect();
 
     // The visible ranges when the stack is focused and unfocused
     private Range mUnfocusedRange;
     private Range mFocusedRange;
 
+    // This is the bounds of the stack action above the stack rect
+    @ViewDebug.ExportedProperty(category="recents")
+    private Rect mStackActionButtonRect = new Rect();
     // The base top margin for the stack from the system insets
     @ViewDebug.ExportedProperty(category="recents")
     private int mBaseTopMargin;
@@ -326,7 +327,7 @@
     @ViewDebug.ExportedProperty(category="recents")
     int mMinTranslationZ;
     @ViewDebug.ExportedProperty(category="recents")
-    int mMaxTranslationZ;
+    public int mMaxTranslationZ;
 
     // Optimization, allows for quick lookup of task -> index
     private SparseIntArray mTaskIndexMap = new SparseIntArray();
@@ -334,6 +335,7 @@
 
     // The freeform workspace layout
     FreeformWorkspaceLayoutAlgorithm mFreeformLayoutAlgorithm;
+    TaskGridLayoutAlgorithm mTaskGridLayoutAlgorithm;
 
     // The transform to place TaskViews at the front and back of the stack respectively
     TaskViewTransform mBackOfStackTransform = new TaskViewTransform();
@@ -344,19 +346,7 @@
         mContext = context;
         mCb = cb;
         mFreeformLayoutAlgorithm = new FreeformWorkspaceLayoutAlgorithm(context);
-        mMinMargin = res.getDimensionPixelSize(R.dimen.recents_layout_min_margin);
-        mBaseTopMargin = getDimensionForDevice(context,
-                R.dimen.recents_layout_top_margin_phone,
-                R.dimen.recents_layout_top_margin_tablet,
-                R.dimen.recents_layout_top_margin_tablet_xlarge);
-        mBaseSideMargin = getDimensionForDevice(context,
-                R.dimen.recents_layout_side_margin_phone,
-                R.dimen.recents_layout_side_margin_tablet,
-                R.dimen.recents_layout_side_margin_tablet_xlarge);
-        mBaseBottomMargin = res.getDimensionPixelSize(R.dimen.recents_layout_bottom_margin);
-        mFreeformStackGap =
-                res.getDimensionPixelSize(R.dimen.recents_freeform_layout_bottom_margin);
-
+        mTaskGridLayoutAlgorithm = new TaskGridLayoutAlgorithm(context);
         reloadOnConfigurationChange(context);
     }
 
@@ -381,6 +371,7 @@
                 R.dimen.recents_layout_initial_top_offset_tablet,
                 R.dimen.recents_layout_initial_top_offset_tablet,
                 R.dimen.recents_layout_initial_top_offset_tablet,
+                R.dimen.recents_layout_initial_top_offset_tablet,
                 R.dimen.recents_layout_initial_top_offset_tablet);
         mBaseInitialBottomOffset = getDimensionForDevice(context,
                 R.dimen.recents_layout_initial_bottom_offset_phone_port,
@@ -388,8 +379,24 @@
                 R.dimen.recents_layout_initial_bottom_offset_tablet,
                 R.dimen.recents_layout_initial_bottom_offset_tablet,
                 R.dimen.recents_layout_initial_bottom_offset_tablet,
+                R.dimen.recents_layout_initial_bottom_offset_tablet,
                 R.dimen.recents_layout_initial_bottom_offset_tablet);
         mFreeformLayoutAlgorithm.reloadOnConfigurationChange(context);
+        mTaskGridLayoutAlgorithm.reloadOnConfigurationChange(context);
+        mMinMargin = res.getDimensionPixelSize(R.dimen.recents_layout_min_margin);
+        mBaseTopMargin = getDimensionForDevice(context,
+                R.dimen.recents_layout_top_margin_phone,
+                R.dimen.recents_layout_top_margin_tablet,
+                R.dimen.recents_layout_top_margin_tablet_xlarge,
+                R.dimen.recents_layout_top_margin_tablet);
+        mBaseSideMargin = getDimensionForDevice(context,
+                R.dimen.recents_layout_side_margin_phone,
+                R.dimen.recents_layout_side_margin_tablet,
+                R.dimen.recents_layout_side_margin_tablet_xlarge,
+                R.dimen.recents_layout_side_margin_tablet);
+        mBaseBottomMargin = res.getDimensionPixelSize(R.dimen.recents_layout_bottom_margin);
+        mFreeformStackGap =
+                res.getDimensionPixelSize(R.dimen.recents_freeform_layout_bottom_margin);
     }
 
     /**
@@ -406,6 +413,7 @@
     public boolean setSystemInsets(Rect systemInsets) {
         boolean changed = !mSystemInsets.equals(systemInsets);
         mSystemInsets.set(systemInsets);
+        mTaskGridLayoutAlgorithm.setSystemInsets(systemInsets);
         return changed;
     }
 
@@ -471,6 +479,9 @@
 
             updateFrontBackTransforms();
         }
+
+        // Initialize the grid layout
+        mTaskGridLayoutAlgorithm.initialize(displayRect, windowRect);
     }
 
     /**
@@ -722,6 +733,11 @@
         }
     }
 
+    public Rect getStackActionButtonRect(boolean useGridLayout) {
+        return useGridLayout
+                ? mTaskGridLayoutAlgorithm.getStackActionButtonRect() : mStackActionButtonRect;
+    }
+
     /**
      * Returns the TaskViewTransform that would put the task just off the back of the stack.
      */
@@ -826,24 +842,30 @@
      * is what the view is measured and laid out with.
      */
     public TaskViewTransform getStackTransform(Task task, float stackScroll,
-            TaskViewTransform transformOut, TaskViewTransform frontTransform) {
+            TaskViewTransform transformOut, TaskViewTransform frontTransform,
+            boolean useGridLayout) {
         return getStackTransform(task, stackScroll, mFocusState, transformOut, frontTransform,
-                false /* forceUpdate */, false /* ignoreTaskOverrides */);
+                false /* forceUpdate */, false /* ignoreTaskOverrides */, useGridLayout);
     }
 
     public TaskViewTransform getStackTransform(Task task, float stackScroll,
             TaskViewTransform transformOut, TaskViewTransform frontTransform,
-            boolean ignoreTaskOverrides) {
+            boolean ignoreTaskOverrides, boolean useGridLayout) {
         return getStackTransform(task, stackScroll, mFocusState, transformOut, frontTransform,
-                false /* forceUpdate */, ignoreTaskOverrides);
+                false /* forceUpdate */, ignoreTaskOverrides, useGridLayout);
     }
 
     public TaskViewTransform getStackTransform(Task task, float stackScroll, int focusState,
             TaskViewTransform transformOut, TaskViewTransform frontTransform, boolean forceUpdate,
-            boolean ignoreTaskOverrides) {
+            boolean ignoreTaskOverrides, boolean useGridLayout) {
         if (mFreeformLayoutAlgorithm.isTransformAvailable(task, this)) {
             mFreeformLayoutAlgorithm.getTransform(task, transformOut, this);
             return transformOut;
+        } else if (useGridLayout) {
+            int taskIndex = mTaskIndexMap.get(task.key.id);
+            int taskCount = mTaskIndexMap.size();
+            mTaskGridLayoutAlgorithm.getTransform(taskIndex, taskCount, transformOut, this);
+            return transformOut;
         } else {
             // Return early if we have an invalid index
             int nonOverrideTaskProgress = mTaskIndexMap.get(task.key.id, -1);
@@ -865,10 +887,10 @@
      */
     public TaskViewTransform getStackTransformScreenCoordinates(Task task, float stackScroll,
             TaskViewTransform transformOut, TaskViewTransform frontTransform,
-            Rect windowOverrideRect) {
+            Rect windowOverrideRect, boolean useGridLayout) {
         TaskViewTransform transform = getStackTransform(task, stackScroll, mFocusState,
                 transformOut, frontTransform, true /* forceUpdate */,
-                false /* ignoreTaskOverrides */);
+                false /* ignoreTaskOverrides */, useGridLayout);
         return transformToScreenCoordinates(transform, windowOverrideRect);
     }
 
@@ -1089,9 +1111,9 @@
      * Retrieves resources that are constant regardless of the current configuration of the device.
      */
     public static int getDimensionForDevice(Context ctx, int phoneResId,
-            int tabletResId, int xlargeTabletResId) {
+            int tabletResId, int xlargeTabletResId, int gridLayoutResId) {
         return getDimensionForDevice(ctx, phoneResId, phoneResId, tabletResId, tabletResId,
-                xlargeTabletResId, xlargeTabletResId);
+                xlargeTabletResId, xlargeTabletResId, gridLayoutResId);
     }
 
     /**
@@ -1099,12 +1121,14 @@
      */
     public static int getDimensionForDevice(Context ctx, int phonePortResId, int phoneLandResId,
             int tabletPortResId, int tabletLandResId, int xlargeTabletPortResId,
-            int xlargeTabletLandResId) {
+            int xlargeTabletLandResId, int gridLayoutResId) {
         RecentsConfiguration config = Recents.getConfiguration();
         Resources res = ctx.getResources();
         boolean isLandscape = Utilities.getAppConfiguration(ctx).orientation ==
                 Configuration.ORIENTATION_LANDSCAPE;
-        if (config.isXLargeScreen) {
+        if (config.isGridEnabled) {
+            return res.getDimensionPixelSize(gridLayoutResId);
+        } else if (config.isXLargeScreen) {
             return res.getDimensionPixelSize(isLandscape
                     ? xlargeTabletLandResId
                     : xlargeTabletPortResId);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
index fc580a5..8d91b4f 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
@@ -92,6 +92,7 @@
 import com.android.systemui.recents.model.Task;
 import com.android.systemui.recents.model.TaskStack;
 
+import com.android.systemui.recents.views.grid.GridTaskView;
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -158,6 +159,7 @@
     private int mTaskCornerRadiusPx;
     private int mDividerSize;
     private int mStartTimerIndicatorDuration;
+    private boolean mDraggingOverDockState;
 
     @ViewDebug.ExportedProperty(category="recents")
     private boolean mTaskViewsClipDirty = true;
@@ -279,6 +281,9 @@
             }
         });
         setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+        if (ssp.hasFreeformWorkspaceSupport()) {
+            setWillNotDraw(false);
+        }
 
         mFreeformWorkspaceBackground = (GradientDrawable) getContext().getDrawable(
                 R.drawable.recents_freeform_workspace_bg);
@@ -496,13 +501,13 @@
 
             // Calculate the current and (if necessary) the target transform for the task
             transform = mLayoutAlgorithm.getStackTransform(task, curStackScroll,
-                    taskTransforms.get(i), frontTransform, ignoreTaskOverrides);
+                    taskTransforms.get(i), frontTransform, ignoreTaskOverrides, useGridLayout());
             if (useTargetStackScroll && !transform.visible) {
                 // If we have a target stack scroll and the task is not currently visible, then we
                 // just update the transform at the new scroll
                 // TODO: Optimize this
-                transformAtTarget = mLayoutAlgorithm.getStackTransform(task,
-                        targetStackScroll, new TaskViewTransform(), frontTransformAtTarget);
+                transformAtTarget = mLayoutAlgorithm.getStackTransform(task, targetStackScroll,
+                    new TaskViewTransform(), frontTransformAtTarget, useGridLayout());
                 if (transformAtTarget.visible) {
                     transform.copyFrom(transformAtTarget);
                 }
@@ -733,7 +738,7 @@
             } else {
                 mLayoutAlgorithm.getStackTransform(task, mStackScroller.getStackScroll(),
                         focusState, transform, null, true /* forceUpdate */,
-                        false /* ignoreTaskOverrides */);
+                        false /* ignoreTaskOverrides */, useGridLayout());
             }
             transform.visible = true;
         }
@@ -750,7 +755,7 @@
             Task task = tasks.get(i);
             TaskViewTransform transform = transformsOut.get(i);
             mLayoutAlgorithm.getStackTransform(task, stackScroll, focusState, transform, null,
-                    true /* forceUpdate */, ignoreTaskOverrides);
+                    true /* forceUpdate */, ignoreTaskOverrides, useGridLayout());
             transform.visible = true;
         }
     }
@@ -779,6 +784,11 @@
      * Updates the clip for each of the task views from back to front.
      */
     private void clipTaskViews() {
+        // We never clip task views in grid layout
+        if (Recents.getConfiguration().isGridEnabled) {
+            return;
+        }
+
         // Update the clip on each task child
         List<TaskView> taskViews = getTaskViews();
         TaskView tmpTv = null;
@@ -1335,14 +1345,7 @@
             setFocusedTask(focusedTaskIndex, false /* scrollToTask */,
                     false /* requestViewFocus */);
         }
-
-        // Update the stack action button visibility
-        if (mStackScroller.getStackScroll() < SHOW_STACK_ACTION_BUTTON_SCROLL_THRESHOLD &&
-                mStack.getTaskCount() > 0) {
-            EventBus.getDefault().send(new ShowStackActionButtonEvent(false /* translate */));
-        } else {
-            EventBus.getDefault().send(new HideStackActionButtonEvent());
-        }
+        updateStackActionButtonVisibility();
     }
 
     public boolean isTouchPointInView(float x, float y, TaskView tv) {
@@ -1503,7 +1506,11 @@
 
     @Override
     public TaskView createView(Context context) {
-        return (TaskView) mInflater.inflate(R.layout.recents_task_view, this, false);
+        if (Recents.getConfiguration().isGridEnabled) {
+            return (GridTaskView) mInflater.inflate(R.layout.recents_grid_task_view, this, false);
+        } else {
+            return (TaskView) mInflater.inflate(R.layout.recents_task_view, this, false);
+        }
     }
 
     @Override
@@ -1560,11 +1567,6 @@
         // Bind the task view to the new task
         bindTaskView(tv, task);
 
-        // If the doze trigger has already fired, then update the state for this task view
-        if (mUIDozeTrigger.isAsleep()) {
-            tv.setNoUserInteractionState();
-        }
-
         // Set the new state for this view, including the callbacks and view clipping
         tv.setCallbacks(this);
         tv.setTouchEnabled(true);
@@ -1594,6 +1596,12 @@
         // Rebind the task and request that this task's data be filled into the TaskView
         tv.onTaskBound(task, mTouchExplorationEnabled, mDisplayOrientation, mDisplayRect);
 
+        // If the doze trigger has already fired, then update the state for this task view
+        if (mUIDozeTrigger.isAsleep() ||
+                Recents.getSystemServices().hasFreeformWorkspaceSupport()) {
+            tv.setNoUserInteractionState();
+        }
+
         // Load the task data
         Recents.getTaskLoader().loadTaskData(task);
     }
@@ -1632,7 +1640,8 @@
             relayoutTaskViewsOnNextFrame(animation);
         }
 
-        if (mEnterAnimationComplete) {
+        // In grid layout, the stack action button always remains visible.
+        if (mEnterAnimationComplete && !useGridLayout()) {
             if (prevScroll > SHOW_STACK_ACTION_BUTTON_SCROLL_THRESHOLD &&
                     curScroll <= SHOW_STACK_ACTION_BUTTON_SCROLL_THRESHOLD &&
                     mStack.getTaskCount() > 0) {
@@ -1679,17 +1688,11 @@
             return;
         }
 
-        int launchTaskIndex = mStack.indexOfStackTask(mStack.getLaunchTarget());
-        if (launchTaskIndex != -1) {
-            launchTaskIndex = Math.max(0, launchTaskIndex - 1);
-        } else {
-            launchTaskIndex = mStack.getTaskCount() - 1;
-        }
-        if (launchTaskIndex != -1) {
+        final Task launchTask = mStack.getNextLaunchTarget();
+        if (launchTask != null) {
             // Stop all animations
             cancelAllTaskViewAnimations();
 
-            final Task launchTask = mStack.getStackTasks().get(launchTaskIndex);
             float curScroll = mStackScroller.getStackScroll();
             float targetScroll = mLayoutAlgorithm.getStackScrollForTaskAtInitialOffset(launchTask);
             float absScrollDiff = Math.abs(targetScroll - curScroll);
@@ -1832,7 +1835,7 @@
         // Enlarge the dragged view slightly
         float finalScale = event.taskView.getScaleX() * DRAG_SCALE_FACTOR;
         mLayoutAlgorithm.getStackTransform(event.task, getScroller().getStackScroll(),
-                mTmpTransform, null);
+                mTmpTransform, null, useGridLayout());
         mTmpTransform.scale = finalScale;
         mTmpTransform.translationZ = mLayoutAlgorithm.mMaxTranslationZ + 1;
         mTmpTransform.dimAlpha = 0f;
@@ -1853,6 +1856,7 @@
                 Interpolators.FAST_OUT_SLOW_IN);
         boolean ignoreTaskOverrides = false;
         if (event.dropTarget instanceof TaskStack.DockState) {
+            mDraggingOverDockState = true;
             // Calculate the new task stack bounds that matches the window size that Recents will
             // have after the drop
             final TaskStack.DockState dockState = (TaskStack.DockState) event.dropTarget;
@@ -1872,6 +1876,7 @@
             updateLayoutAlgorithm(true /* boundScroll */);
             ignoreTaskOverrides = true;
         } else {
+            mDraggingOverDockState = false;
             // Restore the pre-drag task stack bounds, but ensure that we don't layout the dragging
             // task view, so add it back to the ignore set after updating the layout
             removeIgnoreTask(event.task);
@@ -1882,6 +1887,7 @@
     }
 
     public final void onBusEvent(final DragEndEvent event) {
+        mDraggingOverDockState = false;
         // We don't handle drops on the dock regions
         if (event.dropTarget instanceof TaskStack.DockState) {
             // However, we do need to reset the overrides, since the last state of this task stack
@@ -1969,7 +1975,9 @@
                 @Override
                 public void run() {
                     // Start the dozer to trigger to trigger any UI that shows after a timeout
-                    mUIDozeTrigger.startDozing();
+                    if (!Recents.getSystemServices().hasFreeformWorkspaceSupport()) {
+                        mUIDozeTrigger.startDozing();
+                    }
 
                     // Update the focused state here -- since we only set the focused task without
                     // requesting view focus in onFirstLayout(), actually request view focus and
@@ -2049,6 +2057,9 @@
             }
         }
 
+        // Update the Clear All button in case we're switching in or out of grid layout.
+        updateStackActionButtonVisibility();
+
         // Trigger a new layout and update to the initial state if necessary
         if (event.fromMultiWindow) {
             mInitialState = INITIAL_STATE_UPDATE_LAYOUT_ONLY;
@@ -2119,6 +2130,20 @@
     }
 
     /**
+     * Check whether we should use the grid layout.
+     * We use the grid layout for Recents iff all the following is true:
+     *  1. Grid-mode is enabled.
+     *  2. The activity is not in multi-window mode.
+     *  3. The user is not dragging a task view over the dock state.
+     * @return True if we should use the grid layout.
+     */
+    public boolean useGridLayout() {
+        return Recents.getConfiguration().isGridEnabled
+            && !((RecentsActivity) mContext).isInMultiWindowMode()
+            && !mDraggingOverDockState;
+    }
+
+    /**
      * Reads current system flags related to accessibility and screen pinning.
      */
     private void readSystemFlags() {
@@ -2128,6 +2153,17 @@
                 Settings.System.LOCK_TO_APP_ENABLED) != 0;
     }
 
+    private void updateStackActionButtonVisibility() {
+        // Always show the button in grid layout.
+        if (useGridLayout() ||
+                (mStackScroller.getStackScroll() < SHOW_STACK_ACTION_BUTTON_SCROLL_THRESHOLD &&
+                        mStack.getTaskCount() > 0)) {
+            EventBus.getDefault().send(new ShowStackActionButtonEvent(false /* translate */));
+        } else {
+            EventBus.getDefault().send(new HideStackActionButtonEvent());
+        }
+    }
+
     public void dump(String prefix, PrintWriter writer) {
         String innerPrefix = prefix + "  ";
         String id = Integer.toHexString(System.identityHashCode(this));
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
index d44aa84..aeb85d0 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
@@ -168,7 +168,7 @@
     /** Touch preprocessing for handling below */
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         // Pass through to swipe helper if we are swiping
-        mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev);
+        mInterceptedBySwipeHelper = isSwipingEnabled() && mSwipeHelper.onInterceptTouchEvent(ev);
         if (mInterceptedBySwipeHelper) {
             return true;
         }
@@ -680,4 +680,11 @@
     public float getScaledDismissSize() {
         return 1.5f * Math.max(mSv.getWidth(), mSv.getHeight());
     }
+
+    /**
+     * Returns whether swiping is enabled.
+     */
+    private boolean isSwipingEnabled() {
+        return !mSv.useGridLayout();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
index 115e65a..93ab0bd 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
@@ -148,7 +148,7 @@
     private ArrayList<Animator> mTmpAnimators = new ArrayList<>();
 
     @ViewDebug.ExportedProperty(deepExport=true, prefix="thumbnail_")
-    TaskViewThumbnail mThumbnailView;
+    protected TaskViewThumbnail mThumbnailView;
     @ViewDebug.ExportedProperty(deepExport=true, prefix="header_")
     TaskViewHeader mHeaderView;
     private View mActionButtonView;
@@ -176,8 +176,7 @@
         super(context, attrs, defStyleAttr, defStyleRes);
         RecentsConfiguration config = Recents.getConfiguration();
         Resources res = context.getResources();
-        mViewBounds = new AnimateableViewBounds(this, res.getDimensionPixelSize(
-                R.dimen.recents_task_view_shadow_rounded_corners_radius));
+        mViewBounds = createOutlineProvider();
         if (config.fakeShadows) {
             setBackground(new FakeShadowDrawable(res, config));
         }
@@ -194,7 +193,9 @@
      * Called from RecentsActivity when it is relaunched.
      */
     void onReload(boolean isResumingFromVisible) {
-        resetNoUserInteractionState();
+        if (!Recents.getSystemServices().hasFreeformWorkspaceSupport()) {
+            resetNoUserInteractionState();
+        }
         if (!isResumingFromVisible) {
             resetViewProperties();
         }
@@ -205,6 +206,12 @@
         return mTask;
     }
 
+    /* Create an outline provider to clip and outline the view */
+    protected AnimateableViewBounds createOutlineProvider() {
+        return new AnimateableViewBounds(this, mContext.getResources().getDimensionPixelSize(
+            R.dimen.recents_task_view_shadow_rounded_corners_radius));
+    }
+
     /** Returns the view bounds. */
     AnimateableViewBounds getViewBounds() {
         return mViewBounds;
@@ -232,7 +239,7 @@
     /**
      * Update the task view when the configuration changes.
      */
-    void onConfigurationChanged() {
+    protected void onConfigurationChanged() {
         mHeaderView.onConfigurationChanged();
     }
 
@@ -666,10 +673,16 @@
     @Override
     public boolean onLongClick(View v) {
         SystemServicesProxy ssp = Recents.getSystemServices();
-        // Since we are clipping the view to the bounds, manually do the hit test
+        boolean inBounds = false;
         Rect clipBounds = new Rect(mViewBounds.mClipBounds);
-        clipBounds.scale(getScaleX());
-        boolean inBounds = clipBounds.contains(mDownTouchPos.x, mDownTouchPos.y);
+        if (!clipBounds.isEmpty()) {
+            // If we are clipping the view to the bounds, manually do the hit test.
+            clipBounds.scale(getScaleX());
+            inBounds = clipBounds.contains(mDownTouchPos.x, mDownTouchPos.y);
+        } else {
+            // Otherwise just make sure we're within the view's bounds.
+            inBounds = mDownTouchPos.x <= getWidth() && mDownTouchPos.y <= getHeight();
+        }
         if (v == this && inBounds && !ssp.hasDockedTask()) {
             // Start listening for drag events
             setClipViewInStack(false);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
index 691e599..c0cc83f 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
@@ -290,14 +290,16 @@
                 R.dimen.recents_task_view_header_height,
                 R.dimen.recents_task_view_header_height_tablet_land,
                 R.dimen.recents_task_view_header_height,
-                R.dimen.recents_task_view_header_height_tablet_land);
+                R.dimen.recents_task_view_header_height_tablet_land,
+                R.dimen.recents_grid_task_view_header_height);
         int headerButtonPadding = TaskStackLayoutAlgorithm.getDimensionForDevice(getContext(),
                 R.dimen.recents_task_view_header_button_padding,
                 R.dimen.recents_task_view_header_button_padding,
                 R.dimen.recents_task_view_header_button_padding,
                 R.dimen.recents_task_view_header_button_padding_tablet_land,
                 R.dimen.recents_task_view_header_button_padding,
-                R.dimen.recents_task_view_header_button_padding_tablet_land);
+                R.dimen.recents_task_view_header_button_padding_tablet_land,
+                R.dimen.recents_grid_task_view_header_button_padding);
         if (headerBarHeight != mHeaderBarHeight || headerButtonPadding != mHeaderButtonPadding) {
             mHeaderBarHeight = headerBarHeight;
             mHeaderButtonPadding = headerButtonPadding;
@@ -603,10 +605,7 @@
                     Constants.Metrics.DismissSourceHeaderButton);
         } else if (v == mMoveTaskButton) {
             TaskView tv = Utilities.findParent(this, TaskView.class);
-            Rect bounds = mMoveTaskTargetStackId == FREEFORM_WORKSPACE_STACK_ID
-                    ? new Rect(mTaskViewRect)
-                    : new Rect();
-            EventBus.getDefault().send(new LaunchTaskEvent(tv, mTask, bounds,
+            EventBus.getDefault().send(new LaunchTaskEvent(tv, mTask, null,
                     mMoveTaskTargetStackId, false));
         } else if (v == mAppInfoView) {
             EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask));
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java
index c46adf1..16521f7 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java
@@ -60,6 +60,8 @@
     @ViewDebug.ExportedProperty(category="recents")
     private float mThumbnailScale;
     private float mFullscreenThumbnailScale;
+    private boolean mSizeToFit = false;
+    private boolean mOverlayHeaderOnThumbnailActionBar = true;
     private ActivityManager.TaskThumbnailInfo mThumbnailInfo;
 
     private int mCornerRadius;
@@ -133,10 +135,12 @@
                 (int) (mThumbnailRect.width() * mThumbnailScale));
         int thumbnailHeight = Math.min(viewHeight,
                 (int) (mThumbnailRect.height() * mThumbnailScale));
+
         if (mBitmapShader != null && thumbnailWidth > 0 && thumbnailHeight > 0) {
-            int topOffset = mTaskBar != null
-                    ? mTaskBar.getHeight() - mCornerRadius
-                    : 0;
+            int topOffset = 0;
+            if (mTaskBar != null && mOverlayHeaderOnThumbnailActionBar) {
+                topOffset = mTaskBar.getHeight() - mCornerRadius;
+            }
 
             // Draw the background, there will be some small overdraw with the thumbnail
             if (thumbnailWidth < viewWidth) {
@@ -230,7 +234,7 @@
                 // If we haven't measured or the thumbnail is invalid, skip the thumbnail drawing
                 // and only draw the background color
                 mThumbnailScale = 0f;
-            } else if (isStackTask) {
+            } else if (isStackTask && !mSizeToFit) {
                 float invThumbnailScale = 1f / mFullscreenThumbnailScale;
                 if (mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT) {
                     if (mThumbnailInfo.screenOrientation == Configuration.ORIENTATION_PORTRAIT) {
@@ -262,6 +266,19 @@
         }
     }
 
+    /** Sets whether the thumbnail should be resized to fit the task view in all orientations. */
+    public void setSizeToFit(boolean flag) {
+        mSizeToFit = flag;
+    }
+
+    /**
+     * Sets whether the header should overlap (and hide) the action bar in the thumbnail, or
+     * be stacked just above it.
+     */
+    public void setOverlayHeaderOnThumbnailActionBar(boolean flag) {
+        mOverlayHeaderOnThumbnailActionBar = flag;
+    }
+
     /** Updates the clip rect based on the given task bar. */
     void updateClipToTaskBar(View taskBar) {
         mTaskBar = taskBar;
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/grid/AnimateableGridViewBounds.java b/packages/SystemUI/src/com/android/systemui/recents/views/grid/AnimateableGridViewBounds.java
new file mode 100644
index 0000000..a029478
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/grid/AnimateableGridViewBounds.java
@@ -0,0 +1,32 @@
+/*
+ * 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.systemui.recents.views.grid;
+
+import android.view.View;
+import com.android.systemui.recents.views.AnimateableViewBounds;
+
+/* An outline provider for grid-based task views. */
+class AnimateableGridViewBounds extends AnimateableViewBounds {
+
+    public AnimateableGridViewBounds(View source, int cornerRadius) {
+        super(source, cornerRadius);
+    }
+
+    @Override
+    protected void updateClipBounds() {
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/grid/GridTaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/grid/GridTaskView.java
new file mode 100644
index 0000000..6300400
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/grid/GridTaskView.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.systemui.recents.views.grid;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import com.android.systemui.R;
+import com.android.systemui.recents.views.AnimateableViewBounds;
+import com.android.systemui.recents.views.TaskView;
+
+public class GridTaskView extends TaskView {
+
+    /** The height, in pixels, of the header view. */
+    private int mHeaderHeight;
+
+    public GridTaskView(Context context) {
+        this(context, null);
+    }
+
+    public GridTaskView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public GridTaskView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public GridTaskView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        mHeaderHeight = context.getResources().getDimensionPixelSize(
+                R.dimen.recents_grid_task_view_header_height);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        // Show the full thumbnail and don't overlap with the header.
+        mThumbnailView.setSizeToFit(true);
+        mThumbnailView.setOverlayHeaderOnThumbnailActionBar(false);
+        mThumbnailView.updateThumbnailScale();
+        mThumbnailView.setTranslationY(mHeaderHeight);
+    }
+
+    @Override
+    protected AnimateableViewBounds createOutlineProvider() {
+        return new AnimateableGridViewBounds(this, mContext.getResources().getDimensionPixelSize(
+            R.dimen.recents_task_view_shadow_rounded_corners_radius));
+    }
+
+    @Override
+    protected void onConfigurationChanged() {
+        super.onConfigurationChanged();
+        mHeaderHeight = mContext.getResources().getDimensionPixelSize(
+                R.dimen.recents_grid_task_view_header_height);
+        mThumbnailView.setTranslationY(mHeaderHeight);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/grid/TaskGridLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/grid/TaskGridLayoutAlgorithm.java
new file mode 100644
index 0000000..65a8ee2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/grid/TaskGridLayoutAlgorithm.java
@@ -0,0 +1,157 @@
+/*
+ * 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.systemui.recents.views.grid;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.WindowManager;
+
+import com.android.systemui.R;
+import com.android.systemui.recents.misc.Utilities;
+import com.android.systemui.recents.views.TaskStackLayoutAlgorithm;
+import com.android.systemui.recents.views.TaskViewTransform;
+
+public class TaskGridLayoutAlgorithm  {
+
+    private final String TAG = "TaskGridLayoutAlgorithm";
+    private final int MAX_LAYOUT_TASK_COUNT = 8;
+
+    /** The horizontal padding around the whole recents view. */
+    private int mPaddingLeftRight;
+    /** The vertical padding around the whole recents view. */
+    private int mPaddingTopBottom;
+    /** The padding between task views. */
+    private int mPaddingTaskView;
+
+    private Rect mDisplayRect;
+    private Rect mWindowRect;
+    private Point mScreenSize = new Point();
+
+    private Rect mTaskGridRect;
+
+    /** The height, in pixels, of each task view's title bar. */
+    private int mTitleBarHeight;
+
+    /** The aspect ratio of each task thumbnail, without the title bar. */
+    private float mAppAspectRatio;
+    private Rect mSystemInsets = new Rect();
+
+    public TaskGridLayoutAlgorithm(Context context) {
+        reloadOnConfigurationChange(context);
+    }
+
+    public void reloadOnConfigurationChange(Context context) {
+        Resources res = context.getResources();
+        mPaddingLeftRight = res.getDimensionPixelSize(R.dimen.recents_grid_padding_left_right);
+        mPaddingTopBottom = res.getDimensionPixelSize(R.dimen.recents_grid_padding_top_bottom);
+        mPaddingTaskView = res.getDimensionPixelSize(R.dimen.recents_grid_padding_task_view);
+
+        mTaskGridRect = new Rect();
+        mTitleBarHeight = res.getDimensionPixelSize(R.dimen.recents_grid_task_view_header_height);
+
+        WindowManager windowManager = (WindowManager) context
+                .getSystemService(Context.WINDOW_SERVICE);
+        windowManager.getDefaultDisplay().getRealSize(mScreenSize);
+
+        updateAppAspectRatio();
+    }
+
+    public TaskViewTransform getTransform(int taskIndex, int taskCount,
+        TaskViewTransform transformOut, TaskStackLayoutAlgorithm stackLayout) {
+
+        int layoutTaskCount = Math.min(MAX_LAYOUT_TASK_COUNT, taskCount);
+
+        // We also need to invert the index in order to display the most recent tasks first.
+        int taskLayoutIndex = taskCount - taskIndex - 1;
+
+        int tasksPerLine = layoutTaskCount < 2 ? 1 : (
+                layoutTaskCount < 5 ? 2 : (
+                        layoutTaskCount < 7 ? 3 : 4));
+        int lines = layoutTaskCount < 3 ? 1 : 2;
+
+        int taskWidth, taskHeight;
+        int maxTaskWidth = (mDisplayRect.width() - 2 * mPaddingLeftRight
+                - (tasksPerLine - 1) * mPaddingTaskView) / tasksPerLine;
+        int maxTaskHeight = (mDisplayRect.height() - 2 * mPaddingTopBottom
+                - (lines - 1) * mPaddingTaskView) / lines;
+
+        if (maxTaskHeight >= maxTaskWidth / mAppAspectRatio + mTitleBarHeight) {
+            // Width bound.
+            taskWidth = maxTaskWidth;
+            taskHeight = (int) (maxTaskWidth / mAppAspectRatio + mTitleBarHeight);
+        } else {
+            // Height bound.
+            taskHeight = maxTaskHeight;
+            taskWidth = (int) ((taskHeight - mTitleBarHeight) * mAppAspectRatio);
+        }
+        int emptySpaceX = mDisplayRect.width() - 2 * mPaddingLeftRight
+                - (tasksPerLine * taskWidth) - (tasksPerLine - 1) * mPaddingTaskView;
+        int emptySpaceY = mDisplayRect.height() - 2 * mPaddingTopBottom
+                - (lines * taskHeight) - (lines - 1) * mPaddingTaskView;
+
+        mTaskGridRect.set(0, 0, taskWidth, taskHeight);
+
+        int xIndex = taskLayoutIndex % tasksPerLine;
+        int yIndex = taskLayoutIndex / tasksPerLine;
+        int x = emptySpaceX / 2 + mPaddingLeftRight + (taskWidth + mPaddingTaskView) * xIndex;
+        int y = emptySpaceY / 2 + mPaddingTopBottom + (taskHeight + mPaddingTaskView) * yIndex;
+        float z = stackLayout.mMaxTranslationZ;
+
+        float dimAlpha = 0f;
+        float viewOutlineAlpha = 0f;
+        boolean isTaskViewVisible = (taskLayoutIndex < MAX_LAYOUT_TASK_COUNT);
+
+        // Fill out the transform
+        transformOut.scale = 1f;
+        transformOut.alpha = isTaskViewVisible ? 1f : 0f;
+        transformOut.translationZ = z;
+        transformOut.dimAlpha = dimAlpha;
+        transformOut.viewOutlineAlpha = viewOutlineAlpha;
+        transformOut.rect.set(mTaskGridRect);
+        transformOut.rect.offset(x, y);
+        Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale);
+        // We only show the 8 most recent tasks.
+        transformOut.visible = isTaskViewVisible;
+        return transformOut;
+    }
+
+    public void initialize(Rect displayRect, Rect windowRect) {
+        mDisplayRect = displayRect;
+        mWindowRect = windowRect;
+    }
+
+    public void setSystemInsets(Rect systemInsets) {
+        mSystemInsets = systemInsets;
+        updateAppAspectRatio();
+    }
+
+    private void updateAppAspectRatio() {
+        int usableWidth = mScreenSize.x - mSystemInsets.left - mSystemInsets.right;
+        int usableHeight = mScreenSize.y - mSystemInsets.top - mSystemInsets.bottom;
+        mAppAspectRatio = (float) usableWidth / (float) usableHeight;
+    }
+
+    public Rect getStackActionButtonRect() {
+        Rect buttonRect = new Rect(mDisplayRect);
+        buttonRect.right -= mPaddingLeftRight;
+        buttonRect.left += mPaddingLeftRight;
+        buttonRect.bottom = buttonRect.top + mPaddingTopBottom;
+        return buttonRect;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGuts.java
index c850a25..63d2891 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGuts.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGuts.java
@@ -242,9 +242,9 @@
                 return Ranking.IMPORTANCE_UNSPECIFIED;
             }
         } else {
-            if (mBlock.isChecked()) {
+            if (mBlock != null && mBlock.isChecked()) {
                 return Ranking.IMPORTANCE_NONE;
-            } else if (mSilent.isChecked()) {
+            } else if (mSilent != null && mSilent.isChecked()) {
                 return Ranking.IMPORTANCE_LOW;
             } else {
                 return Ranking.IMPORTANCE_UNSPECIFIED;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index c37373d..90d3f45c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -36,6 +36,7 @@
 import android.app.ActivityManager;
 import android.app.ActivityManagerNative;
 import android.app.ActivityOptions;
+import android.app.admin.DevicePolicyManager;
 import android.app.IActivityManager;
 import android.app.Notification;
 import android.app.PendingIntent;
@@ -995,6 +996,7 @@
         filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
         filter.addAction(Intent.ACTION_SCREEN_OFF);
         filter.addAction(Intent.ACTION_SCREEN_ON);
+        filter.addAction(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG);
         context.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null, null);
 
         IntentFilter demoFilter = new IntentFilter();
@@ -2914,7 +2916,7 @@
 
         runPostCollapseRunnables();
         setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false);
-        showBouncer();
+        showBouncerIfKeyguard();
         recomputeDisableFlags(true /* animate */);
 
         // Trimming will happen later if Keyguard is showing - doing it here might cause a jank in
@@ -3581,6 +3583,9 @@
             else if (Intent.ACTION_SCREEN_ON.equals(action)) {
                 notifyNavigationBarScreenOn(true);
             }
+            else if (DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG.equals(action)) {
+                mQSPanel.showDeviceMonitoringDialog();
+            }
         }
     };
 
@@ -4447,13 +4452,17 @@
         return false;
     }
 
-    private void showBouncer() {
+    private void showBouncerIfKeyguard() {
         if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) {
-            mWaitingForKeyguardExit = mStatusBarKeyguardViewManager.isShowing();
-            mStatusBarKeyguardViewManager.dismiss();
+            showBouncer();
         }
     }
 
+    private void showBouncer() {
+        mWaitingForKeyguardExit = mStatusBarKeyguardViewManager.isShowing();
+        mStatusBarKeyguardViewManager.dismiss();
+    }
+
     private void instantExpandNotificationsPanel() {
 
         // Make our window larger and the panel expanded.
@@ -4550,7 +4559,7 @@
     public void onTrackingStopped(boolean expand) {
         if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) {
             if (!expand && !mUnlockMethodCache.canSkipBouncer()) {
-                showBouncer();
+                showBouncerIfKeyguard();
             }
         }
     }
@@ -4641,7 +4650,7 @@
                 || !mShowLockscreenNotifications || mFalsingManager.shouldEnforceBouncer();
         if (isLockscreenPublicMode() && fullShadeNeedsBouncer) {
             mLeaveOpenOnKeyguardHide = true;
-            showBouncer();
+            showBouncerIfKeyguard();
             mDraggedDownRow = row;
             mPendingRemoteInputView = null;
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java
index 014afae..63d4381 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java
@@ -21,6 +21,7 @@
     boolean hasProfileOwner();
     String getDeviceOwnerName();
     String getProfileOwnerName();
+    boolean isNetworkLoggingEnabled();
     boolean isVpnEnabled();
     boolean isVpnRestricted();
     /** Whether the VPN app should use branded VPN iconography.  */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
index 07d3b59..65a119a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
@@ -154,6 +154,11 @@
     }
 
     @Override
+    public boolean isNetworkLoggingEnabled() {
+        return mDevicePolicyManager.isNetworkLoggingEnabled(null);
+    }
+
+    @Override
     public boolean isVpnEnabled() {
         for (int profileId : mUserManager.getProfileIdsWithDisabled(mVpnUserId)) {
             if (mCurrentVpns.get(profileId) != null) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/grid/TaskGridLayoutAlgorithmTest.java b/packages/SystemUI/tests/src/com/android/systemui/recents/grid/TaskGridLayoutAlgorithmTest.java
deleted file mode 100644
index e5a74ae..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/grid/TaskGridLayoutAlgorithmTest.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.systemui.recents.grid;
-
-import android.graphics.Rect;
-import android.test.suitebuilder.annotation.SmallTest;
-import com.android.systemui.SysuiTestCase;
-
-import java.util.List;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertTrue;
-
-@SmallTest
-public class TaskGridLayoutAlgorithmTest extends SysuiTestCase {
-
-    public void testOneTile() {
-        List<Rect> rects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
-                1, 1000, 500, false /* allowLineOfThree */, 0 /* padding */);
-        assertEquals(1, rects.size());
-        Rect singleRect = rects.get(0);
-        assertEquals(1000, singleRect.width());
-    }
-
-    public void testTwoTilesLandscape() {
-        List<Rect> rects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
-                2, 1200, 500, false /* allowLineOfThree */, 0 /* padding */);
-        assertEquals(2, rects.size());
-        for (Rect rect : rects) {
-            assertEquals(600, rect.width());
-            assertEquals(500, rect.height());
-        }
-    }
-
-    public void testTwoTilesLandscapeWithPadding() {
-        List<Rect> rects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
-                2, 1200, 500, false /* allowLineOfThree */, 10 /* padding */);
-        assertEquals(2, rects.size());
-        Rect rectA = rects.get(0);
-        Rect rectB = rects.get(1);
-        assertEquals(595, rectA.width());
-        assertEquals(595, rectB.width());
-        assertEquals(605, rectB.left);
-    }
-
-    public void testTwoTilesPortrait() {
-        List<Rect> rects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
-                2, 500, 1200, false /* allowLineOfThree */, 0 /* padding */);
-        assertEquals(2, rects.size());
-        for (Rect rect : rects) {
-            assertEquals(500, rect.width());
-            assertEquals(600, rect.height());
-        }
-    }
-
-    public void testThreeTiles() {
-        List<Rect> rects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
-                3, 1200, 500, false /* allowLineOfThree */, 0 /* padding */);
-        assertEquals(3, rects.size());
-        for (Rect rect : rects) {
-            assertEquals(600, rect.width());
-            assertEquals(250, rect.height());
-        }
-        // The third tile should be on the second line, in the middle.
-        Rect rectC = rects.get(2);
-        assertEquals(300, rectC.left);
-        assertEquals(250, rectC.top);
-    }
-
-    public void testFourTiles() {
-        List<Rect> rects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
-                4, 1200, 500, false /* allowLineOfThree */, 0 /* padding */);
-        assertEquals(4, rects.size());
-        for (Rect rect : rects) {
-            assertEquals(600, rect.width());
-            assertEquals(250, rect.height());
-        }
-        Rect rectD = rects.get(3);
-        assertEquals(600, rectD.left);
-        assertEquals(250, rectD.top);
-    }
-
-    public void testNineTiles() {
-        List<Rect> rects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
-                9, 1200, 600, false /* allowLineOfThree */, 0 /* padding */);
-        assertEquals(9, rects.size());
-        for (Rect rect : rects) {
-            assertEquals(400, rect.width());
-            assertEquals(200, rect.height());
-        }
-        Rect rectE = rects.get(4);
-        assertEquals(400, rectE.left);
-        assertEquals(200, rectE.top);
-        Rect rectI = rects.get(8);
-        assertEquals(800, rectI.left);
-        assertEquals(400, rectI.top);
-    }
-}
-
diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto
index 81d645e..20cca8e 100644
--- a/proto/src/metrics_constants.proto
+++ b/proto/src/metrics_constants.proto
@@ -2219,6 +2219,26 @@
     // OS: N-MR2
     ACTION_LOCK_BECAUSE_SIM_REMOVED = 497;
 
+    // ACTION: A captive portal was detected during network validation
+    // CATEGORY: NOTIFICATION
+    // OS: N-MR2
+    NOTIFICATION_NETWORK_SIGN_IN = 740;
+
+    // ACTION: An unvalidated network without Internet was selected by the user
+    // CATEGORY: NOTIFICATION
+    // OS: N-MR2
+    NOTIFICATION_NETWORK_NO_INTERNET = 741;
+
+    // ACTION: A validated network failed revalidation and lost Internet access
+    // CATEGORY: NOTIFICATION
+    // OS: N-MR2
+    NOTIFICATION_NETWORK_LOST_INTERNET = 742;
+
+    // ACTION: The system default network switched to a different network
+    // CATEGORY: NOTIFICATION
+    // OS: N-MR2
+    NOTIFICATION_NETWORK_SWITCH = 743;
+
     // ---- End N-MR2 Constants, all N-MR2 constants go above this line ----
 
     // Add new aosp constants above this line.
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 58bb5f3..34ccb7b 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -538,7 +538,7 @@
 
     @Override
     public void interrupt(int userId) {
-        CopyOnWriteArrayList<Service> services;
+        List<IAccessibilityServiceClient> interfacesToInterrupt;
         synchronized (mLock) {
             // We treat calls from a profile as if made by its parent as profiles
             // share the accessibility state of the parent. The call below
@@ -549,15 +549,24 @@
             if (resolvedUserId != mCurrentUserId) {
                 return;
             }
-            services = getUserStateLocked(resolvedUserId).mBoundServices;
+            List<Service> services = getUserStateLocked(resolvedUserId).mBoundServices;
+            int numServices = services.size();
+            interfacesToInterrupt = new ArrayList<>(numServices);
+            for (int i = 0; i < numServices; i++) {
+                Service service = services.get(i);
+                IBinder a11yServiceBinder = service.mService;
+                IAccessibilityServiceClient a11yServiceInterface = service.mServiceInterface;
+                if ((a11yServiceBinder != null) && (a11yServiceInterface != null)) {
+                    interfacesToInterrupt.add(a11yServiceInterface);
+                }
+            }
         }
-        for (int i = 0, count = services.size(); i < count; i++) {
-            Service service = services.get(i);
+        for (int i = 0, count = interfacesToInterrupt.size(); i < count; i++) {
             try {
-                service.mServiceInterface.onInterrupt();
+                interfacesToInterrupt.get(i).onInterrupt();
             } catch (RemoteException re) {
-                Slog.e(LOG_TAG, "Error during sending interrupt request to "
-                    + service.mService, re);
+                Slog.e(LOG_TAG, "Error sending interrupt request to "
+                        + interfacesToInterrupt.get(i), re);
             }
         }
     }
diff --git a/services/core/java/com/android/server/BluetoothManagerService.java b/services/core/java/com/android/server/BluetoothManagerService.java
index dbb8e0f..7f42d1c 100644
--- a/services/core/java/com/android/server/BluetoothManagerService.java
+++ b/services/core/java/com/android/server/BluetoothManagerService.java
@@ -101,6 +101,7 @@
     private static final int MESSAGE_USER_UNLOCKED = 301;
     private static final int MESSAGE_ADD_PROXY_DELAYED = 400;
     private static final int MESSAGE_BIND_PROFILE_SERVICE = 401;
+
     private static final int MAX_SAVE_RETRIES = 3;
     private static final int MAX_ERROR_RESTART_RETRIES = 6;
 
@@ -207,7 +208,7 @@
                     } finally {
                         mBluetoothLock.readLock().unlock();
                     }
-                    Slog.d(TAG, "Airplane Mode change - current state: " + st);
+                    Slog.d(TAG, "State " + BluetoothAdapter.nameForState(st));
 
                     if (isAirplaneModeOn()) {
                         // Clear registered LE apps to force shut-off
@@ -316,13 +317,15 @@
 
     /**
      *  Save the Bluetooth on/off state
-     *
      */
     private void persistBluetoothSetting(int value) {
         if (DBG) Slog.d(TAG, "Persisting Bluetooth Setting: " + value);
+        // waive WRITE_SECURE_SETTINGS permission check
+        long callingIdentity = Binder.clearCallingIdentity();
         Settings.Global.putInt(mContext.getContentResolver(),
                                Settings.Global.BLUETOOTH_ON,
                                value);
+        Binder.restoreCallingIdentity(callingIdentity);
     }
 
     /**
@@ -405,6 +408,10 @@
     public void registerStateChangeCallback(IBluetoothStateChangeCallback callback) {
         mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM,
                                                 "Need BLUETOOTH permission");
+        if (callback == null) {
+          Slog.w(TAG, "registerStateChangeCallback: Callback is null!");
+          return;
+        }
         Message msg = mHandler.obtainMessage(MESSAGE_REGISTER_STATE_CHANGE_CALLBACK);
         msg.obj = callback;
         mHandler.sendMessage(msg);
@@ -413,6 +420,10 @@
     public void unregisterStateChangeCallback(IBluetoothStateChangeCallback callback) {
         mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM,
                                                 "Need BLUETOOTH permission");
+        if (callback == null) {
+          Slog.w(TAG, "unregisterStateChangeCallback: Callback is null!");
+          return;
+        }
         Message msg = mHandler.obtainMessage(MESSAGE_UNREGISTER_STATE_CHANGE_CALLBACK);
         msg.obj = callback;
         mHandler.sendMessage(msg);
@@ -439,7 +450,7 @@
     public int getState() {
         if ((Binder.getCallingUid() != Process.SYSTEM_UID) &&
                 (!checkIfCallerIsForegroundUser())) {
-            Slog.w(TAG, "getState(): not allowed for non-active and non system user");
+            Slog.w(TAG, "getState(): report OFF for non-active and non system user");
             return BluetoothAdapter.STATE_OFF;
         }
 
@@ -579,20 +590,26 @@
     }
 
     /**
-     * Action taken when GattService is turned off
+     * Action taken when GattService is turned on
      */
     private void onBluetoothGattServiceUp() {
         if (DBG) Slog.d(TAG,"BluetoothGatt Service is Up");
         try {
             mBluetoothLock.readLock().lock();
-            if (isBleAppPresent() == false && mBluetooth != null
-                  && mBluetooth.getState() == BluetoothAdapter.STATE_BLE_ON) {
+            if (mBluetooth == null) {
+                if (DBG) Slog.w(TAG, "onBluetoothServiceUp: mBluetooth is null!");
+                return;
+            }
+            int st = mBluetooth.getState();
+            if (st != BluetoothAdapter.STATE_BLE_ON) {
+                if (DBG) Slog.v(TAG, "onBluetoothServiceUp: state isn't BLE_ON: " +
+                        BluetoothAdapter.nameForState(st));
+                return;
+            }
+            if (isBluetoothPersistedStateOnBluetooth() || !isBleAppPresent()) {
+                // This triggers transition to STATE_ON
                 mBluetooth.onLeServiceUp();
-
-                // waive WRITE_SECURE_SETTINGS permission check
-                long callingIdentity = Binder.clearCallingIdentity();
                 persistBluetoothSetting(BLUETOOTH_ON_BLUETOOTH);
-                Binder.restoreCallingIdentity(callingIdentity);
             }
         } catch (RemoteException e) {
             Slog.e(TAG,"Unable to call onServiceUp", e);
@@ -677,7 +694,8 @@
 
         if (DBG) {
             Slog.d(TAG,"enable():  mBluetooth =" + mBluetooth +
-                    " mBinding = " + mBinding + " mState = " + mState);
+                    " mBinding = " + mBinding + " mState = " +
+                    BluetoothAdapter.nameForState(mState));
         }
 
         synchronized(mReceiver) {
@@ -717,10 +735,7 @@
 
         synchronized(mReceiver) {
             if (persist) {
-                // waive WRITE_SECURE_SETTINGS permission check
-                long callingIdentity = Binder.clearCallingIdentity();
                 persistBluetoothSetting(BLUETOOTH_OFF);
-                Binder.restoreCallingIdentity(callingIdentity);
             }
             mEnableExternal = false;
             sendDisableMsg();
@@ -756,7 +771,7 @@
     public void unbindAndFinish() {
         if (DBG) {
             Slog.d(TAG,"unbindAndFinish(): " + mBluetooth +
-                " mBinding = " + mBinding);
+                " mBinding = " + mBinding + " mUnbinding = " + mUnbinding);
         }
 
         try {
@@ -772,16 +787,13 @@
                 } catch (RemoteException re) {
                     Slog.e(TAG, "Unable to unregister BluetoothCallback",re);
                 }
-
-                if (DBG) Slog.d(TAG, "Sending unbind request.");
                 mBluetoothBinder = null;
                 mBluetooth = null;
-                //Unbind
                 mContext.unbindService(mConnection);
                 mUnbinding = false;
                 mBinding = false;
             } else {
-                mUnbinding=false;
+                mUnbinding = false;
             }
             mBluetoothGatt = null;
         } finally {
@@ -1051,7 +1063,6 @@
      * Inform BluetoothAdapter instances that Adapter service is up
      */
     private void sendBluetoothServiceUpCallback() {
-        if (DBG) Slog.d(TAG,"Calling onBluetoothServiceUp callbacks");
         try {
             int n = mCallbacks.beginBroadcast();
             Slog.d(TAG,"Broadcasting onBluetoothServiceUp() to " + n + " receivers.");
@@ -1070,7 +1081,6 @@
      * Inform BluetoothAdapter instances that Adapter service is down
      */
     private void sendBluetoothServiceDownCallback() {
-        if (DBG) Slog.d(TAG,"Calling onBluetoothServiceDown callbacks");
         try {
             int n = mCallbacks.beginBroadcast();
             Slog.d(TAG,"Broadcasting onBluetoothServiceDown() to " + n + " receivers.");
@@ -1142,34 +1152,33 @@
     }
 
     private class BluetoothServiceConnection implements ServiceConnection {
-        public void onServiceConnected(ComponentName className, IBinder service) {
-            if (DBG) Slog.d(TAG, "BluetoothServiceConnection: " + className.getClassName());
+        public void onServiceConnected(ComponentName componentName, IBinder service) {
+            String name = componentName.getClassName();
+            if (DBG) Slog.d(TAG, "BluetoothServiceConnection: " + name);
             Message msg = mHandler.obtainMessage(MESSAGE_BLUETOOTH_SERVICE_CONNECTED);
-            // TBD if (className.getClassName().equals(IBluetooth.class.getName())) {
-            if (className.getClassName().equals("com.android.bluetooth.btservice.AdapterService")) {
+            if (name.equals("com.android.bluetooth.btservice.AdapterService")) {
                 msg.arg1 = SERVICE_IBLUETOOTH;
-                // } else if (className.getClassName().equals(IBluetoothGatt.class.getName())) {
-            } else if (className.getClassName().equals("com.android.bluetooth.gatt.GattService")) {
+            } else if (name.equals("com.android.bluetooth.gatt.GattService")) {
                 msg.arg1 = SERVICE_IBLUETOOTHGATT;
             } else {
-                Slog.e(TAG, "Unknown service connected: " + className.getClassName());
+                Slog.e(TAG, "Unknown service connected: " + name);
                 return;
             }
             msg.obj = service;
             mHandler.sendMessage(msg);
         }
 
-        public void onServiceDisconnected(ComponentName className) {
-            // Called if we unexpected disconnected.
-            if (DBG) Slog.d(TAG, "BluetoothServiceConnection, disconnected: " +
-                           className.getClassName());
+        public void onServiceDisconnected(ComponentName componentName) {
+            // Called if we unexpectedly disconnect.
+            String name = componentName.getClassName();
+            if (DBG) Slog.d(TAG, "BluetoothServiceConnection, disconnected: " + name);
             Message msg = mHandler.obtainMessage(MESSAGE_BLUETOOTH_SERVICE_DISCONNECTED);
-            if (className.getClassName().equals("com.android.bluetooth.btservice.AdapterService")) {
+            if (name.equals("com.android.bluetooth.btservice.AdapterService")) {
                 msg.arg1 = SERVICE_IBLUETOOTH;
-            } else if (className.getClassName().equals("com.android.bluetooth.gatt.GattService")) {
+            } else if (name.equals("com.android.bluetooth.gatt.GattService")) {
                 msg.arg1 = SERVICE_IBLUETOOTHGATT;
             } else {
-                Slog.e(TAG, "Unknown service disconnected: " + className.getClassName());
+                Slog.e(TAG, "Unknown service disconnected: " + name);
                 return;
             }
             mHandler.sendMessage(msg);
@@ -1187,7 +1196,6 @@
 
         @Override
         public void handleMessage(Message msg) {
-            if (DBG) Slog.d (TAG, "Message: " + msg.what);
             switch (msg.what) {
                 case MESSAGE_GET_NAME_AND_ADDRESS:
                     if (DBG) Slog.d(TAG, "MESSAGE_GET_NAME_AND_ADDRESS");
@@ -1225,7 +1233,7 @@
 
                 case MESSAGE_ENABLE:
                     if (DBG) {
-                        Slog.d(TAG, "MESSAGE_ENABLE: mBluetooth = " + mBluetooth);
+                        Slog.d(TAG, "MESSAGE_ENABLE(" + msg.arg1 + "): mBluetooth = " + mBluetooth);
                     }
                     mHandler.removeMessages(MESSAGE_RESTART_BLUETOOTH_SERVICE);
                     mEnable = true;
@@ -1236,8 +1244,9 @@
                         if (mBluetooth != null) {
                             int state = mBluetooth.getState();
                             if (state == BluetoothAdapter.STATE_BLE_ON) {
-                                Slog.w(TAG, "BT is in BLE_ON State");
+                                Slog.w(TAG, "BT Enable in BLE_ON State, going to ON");
                                 mBluetooth.onLeServiceUp();
+                                persistBluetoothSetting(BLUETOOTH_ON_BLUETOOTH);
                                 break;
                             }
                         }
@@ -1275,6 +1284,7 @@
                     break;
 
                 case MESSAGE_DISABLE:
+                    if (DBG) Slog.d(TAG, "MESSAGE_DISABLE: mBluetooth = " + mBluetooth);
                     mHandler.removeMessages(MESSAGE_RESTART_BLUETOOTH_SERVICE);
                     if (mEnable && mBluetooth != null) {
                         waitForOnOff(true, false);
@@ -1290,31 +1300,25 @@
                 case MESSAGE_REGISTER_ADAPTER:
                 {
                     IBluetoothManagerCallback callback = (IBluetoothManagerCallback) msg.obj;
-                    boolean added = mCallbacks.register(callback);
-                    Slog.d(TAG,"Added callback: " +  (callback == null? "null": callback)  +":" +added );
-                }
+                    mCallbacks.register(callback);
                     break;
+                }
                 case MESSAGE_UNREGISTER_ADAPTER:
                 {
                     IBluetoothManagerCallback callback = (IBluetoothManagerCallback) msg.obj;
-                    boolean removed = mCallbacks.unregister(callback);
-                    Slog.d(TAG,"Removed callback: " +  (callback == null? "null": callback)  +":" + removed);
+                    mCallbacks.unregister(callback);
                     break;
                 }
                 case MESSAGE_REGISTER_STATE_CHANGE_CALLBACK:
                 {
                     IBluetoothStateChangeCallback callback = (IBluetoothStateChangeCallback) msg.obj;
-                    if (callback != null) {
-                        mStateChangeCallbacks.register(callback);
-                    }
+                    mStateChangeCallbacks.register(callback);
                     break;
                 }
                 case MESSAGE_UNREGISTER_STATE_CHANGE_CALLBACK:
                 {
                     IBluetoothStateChangeCallback callback = (IBluetoothStateChangeCallback) msg.obj;
-                    if (callback != null) {
-                        mStateChangeCallbacks.unregister(callback);
-                    }
+                    mStateChangeCallbacks.unregister(callback);
                     break;
                 }
                 case MESSAGE_ADD_PROXY_DELAYED:
@@ -1387,13 +1391,11 @@
                         //Do enable request
                         try {
                             if (mQuietEnable == false) {
-                                if(!mBluetooth.enable()) {
+                                if (!mBluetooth.enable()) {
                                     Slog.e(TAG,"IBluetooth.enable() returned false");
                                 }
-                            }
-                            else
-                            {
-                                if(!mBluetooth.enableNoAutoConnect()) {
+                            } else {
+                                if (!mBluetooth.enableNoAutoConnect()) {
                                     Slog.e(TAG,"IBluetooth.enableNoAutoConnect() returned false");
                                 }
                             }
@@ -1411,19 +1413,14 @@
                     }
                     break;
                 }
-                case MESSAGE_TIMEOUT_BIND: {
-                    Slog.e(TAG, "MESSAGE_TIMEOUT_BIND");
-                    mBluetoothLock.writeLock().lock();
-                    mBinding = false;
-                    mBluetoothLock.writeLock().unlock();
-
-                    break;
-                }
                 case MESSAGE_BLUETOOTH_STATE_CHANGE:
                 {
                     int prevState = msg.arg1;
                     int newState = msg.arg2;
-                    if (DBG) Slog.d(TAG, "MESSAGE_BLUETOOTH_STATE_CHANGE: prevState = " + prevState + ", newState =" + newState);
+                    if (DBG) {
+                      Slog.d(TAG, "MESSAGE_BLUETOOTH_STATE_CHANGE: " + BluetoothAdapter.nameForState(prevState) + " > " +
+                        BluetoothAdapter.nameForState(newState));
+                    }
                     mState = newState;
                     bluetoothStateChangeHandler(prevState, newState);
                     // handle error state transition case from TURNING_ON to OFF
@@ -1463,7 +1460,7 @@
                 }
                 case MESSAGE_BLUETOOTH_SERVICE_DISCONNECTED:
                 {
-                    Slog.e(TAG, "MESSAGE_BLUETOOTH_SERVICE_DISCONNECTED: " + msg.arg1);
+                    Slog.e(TAG, "MESSAGE_BLUETOOTH_SERVICE_DISCONNECTED(" + msg.arg1 + ")");
                     try {
                         mBluetoothLock.writeLock().lock();
                         if (msg.arg1 == SERVICE_IBLUETOOTH) {
@@ -1474,7 +1471,7 @@
                             mBluetoothGatt = null;
                             break;
                         } else {
-                            Slog.e(TAG, "Bad msg.arg1: " + msg.arg1);
+                            Slog.e(TAG, "Unknown argument for service disconnect!");
                             break;
                         }
                     } finally {
@@ -1511,8 +1508,7 @@
                 }
                 case MESSAGE_RESTART_BLUETOOTH_SERVICE:
                 {
-                    Slog.d(TAG, "MESSAGE_RESTART_BLUETOOTH_SERVICE:"
-                        +" Restart IBluetooth service");
+                    Slog.d(TAG, "MESSAGE_RESTART_BLUETOOTH_SERVICE");
                     /* Enable without persisting the setting as
                      it doesnt change when IBluetooth
                      service restarts */
@@ -1520,7 +1516,13 @@
                     handleEnable(mQuietEnable);
                     break;
                 }
-
+                case MESSAGE_TIMEOUT_BIND: {
+                    Slog.e(TAG, "MESSAGE_TIMEOUT_BIND");
+                    mBluetoothLock.writeLock().lock();
+                    mBinding = false;
+                    mBluetoothLock.writeLock().unlock();
+                    break;
+                }
                 case MESSAGE_TIMEOUT_UNBIND:
                 {
                     Slog.e(TAG, "MESSAGE_TIMEOUT_UNBIND");
@@ -1606,11 +1608,10 @@
                     } else if (mBinding || mBluetooth != null) {
                         Message userMsg = mHandler.obtainMessage(MESSAGE_USER_SWITCHED);
                         userMsg.arg2 = 1 + msg.arg2;
-                        // if user is switched when service is being binding
-                        // delay sending MESSAGE_USER_SWITCHED
+                        // if user is switched when service is binding retry after a delay
                         mHandler.sendMessageDelayed(userMsg, USER_SWITCHED_TIME_MS);
                         if (DBG) {
-                            Slog.d(TAG, "delay MESSAGE_USER_SWITCHED " + userMsg.arg2);
+                            Slog.d(TAG, "Retry MESSAGE_USER_SWITCHED " + userMsg.arg2);
                         }
                     }
                     break;
@@ -1711,7 +1712,7 @@
                     parentUser == foregroundUser    ||
                     callingAppId == Process.NFC_UID ||
                     callingAppId == mSystemUiUid;
-            if (DBG) {
+            if (DBG && !valid) {
                 Slog.d(TAG, "checkIfCallerIsForegroundUser: valid=" + valid
                     + " callingUser=" + callingUser
                     + " parentUser=" + parentUser
@@ -1724,7 +1725,8 @@
     }
 
     private void sendBleStateChanged(int prevState, int newState) {
-        if (DBG) Slog.d(TAG,"BLE State Change Intent: " + prevState + " -> " + newState);
+        if (DBG) Slog.d(TAG,"Sending BLE State Change: " + BluetoothAdapter.nameForState(prevState) +
+            " > " + BluetoothAdapter.nameForState(newState));
         // Send broadcast message to everyone else
         Intent intent = new Intent(BluetoothAdapter.ACTION_BLE_STATE_CHANGED);
         intent.putExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, prevState);
@@ -1735,76 +1737,76 @@
 
     private void bluetoothStateChangeHandler(int prevState, int newState) {
         boolean isStandardBroadcast = true;
-        if (DBG) Slog.d(TAG, "bluetoothStateChangeHandler: " + prevState + " ->  " + newState);
-        if (prevState != newState) {
-            //Notify all proxy objects first of adapter state change
-            if (newState == BluetoothAdapter.STATE_BLE_ON ||
-                    newState == BluetoothAdapter.STATE_OFF) {
-                boolean intermediate_off = (prevState == BluetoothAdapter.STATE_TURNING_OFF
-                   && newState == BluetoothAdapter.STATE_BLE_ON);
+        if (prevState == newState) { // No change. Nothing to do.
+            return;
+        }
+        // Notify all proxy objects first of adapter state change
+        if (newState == BluetoothAdapter.STATE_BLE_ON ||
+                newState == BluetoothAdapter.STATE_OFF) {
+            boolean intermediate_off = (prevState == BluetoothAdapter.STATE_TURNING_OFF
+               && newState == BluetoothAdapter.STATE_BLE_ON);
 
-                if (newState == BluetoothAdapter.STATE_OFF) {
-                    // If Bluetooth is off, send service down event to proxy objects, and unbind
-                    if (DBG) Slog.d(TAG, "Bluetooth is complete turn off");
-                    sendBluetoothServiceDownCallback();
-                    unbindAndFinish();
-                    sendBleStateChanged(prevState, newState);
-                    // Don't broadcast as it has already been broadcast before
-                    isStandardBroadcast = false;
-
-                } else if (!intermediate_off) {
-                    // connect to GattService
-                    if (DBG) Slog.d(TAG, "Bluetooth is in LE only mode");
-                    if (mBluetoothGatt != null) {
-                        if (DBG) Slog.d(TAG, "Calling BluetoothGattServiceUp");
-                        onBluetoothGattServiceUp();
-                    } else {
-                        if (DBG) Slog.d(TAG, "Binding Bluetooth GATT service");
-                        if (mContext.getPackageManager().hasSystemFeature(
-                                                        PackageManager.FEATURE_BLUETOOTH_LE)) {
-                            Intent i = new Intent(IBluetoothGatt.class.getName());
-                            doBind(i, mConnection, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT, UserHandle.CURRENT);
-                        }
-                    }
-                    sendBleStateChanged(prevState, newState);
-                    //Don't broadcase this as std intent
-                    isStandardBroadcast = false;
-
-                } else if (intermediate_off){
-                    if (DBG) Slog.d(TAG, "Intermediate off, back to LE only mode");
-                    // For LE only mode, broadcast as is
-                    sendBleStateChanged(prevState, newState);
-                    sendBluetoothStateCallback(false); // BT is OFF for general users
-                    // Broadcast as STATE_OFF
-                    newState = BluetoothAdapter.STATE_OFF;
-                    sendBrEdrDownCallback();
-                }
-            } else if (newState == BluetoothAdapter.STATE_ON) {
-                boolean isUp = (newState==BluetoothAdapter.STATE_ON);
-                sendBluetoothStateCallback(isUp);
+            if (newState == BluetoothAdapter.STATE_OFF) {
+                // If Bluetooth is off, send service down event to proxy objects, and unbind
+                if (DBG) Slog.d(TAG, "Bluetooth is complete send Service Down");
+                sendBluetoothServiceDownCallback();
+                unbindAndFinish();
                 sendBleStateChanged(prevState, newState);
-
-            } else if (newState == BluetoothAdapter.STATE_BLE_TURNING_ON ||
-                    newState == BluetoothAdapter.STATE_BLE_TURNING_OFF ) {
-                sendBleStateChanged(prevState, newState);
+                // Don't broadcast as it has already been broadcast before
                 isStandardBroadcast = false;
 
-            } else if (newState == BluetoothAdapter.STATE_TURNING_ON ||
-                    newState == BluetoothAdapter.STATE_TURNING_OFF) {
-                sendBleStateChanged(prevState, newState);
-            }
-
-            if (isStandardBroadcast) {
-                if (prevState == BluetoothAdapter.STATE_BLE_ON) {
-                    // Show prevState of BLE_ON as OFF to standard users
-                    prevState = BluetoothAdapter.STATE_OFF;
+            } else if (!intermediate_off) {
+                // connect to GattService
+                if (DBG) Slog.d(TAG, "Bluetooth is in LE only mode");
+                if (mBluetoothGatt != null) {
+                    if (DBG) Slog.d(TAG, "Calling BluetoothGattServiceUp");
+                    onBluetoothGattServiceUp();
+                } else {
+                    if (DBG) Slog.d(TAG, "Binding Bluetooth GATT service");
+                    if (mContext.getPackageManager().hasSystemFeature(
+                                                    PackageManager.FEATURE_BLUETOOTH_LE)) {
+                        Intent i = new Intent(IBluetoothGatt.class.getName());
+                        doBind(i, mConnection, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT, UserHandle.CURRENT);
+                    }
                 }
-                Intent intent = new Intent(BluetoothAdapter.ACTION_STATE_CHANGED);
-                intent.putExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, prevState);
-                intent.putExtra(BluetoothAdapter.EXTRA_STATE, newState);
-                intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
-                mContext.sendBroadcastAsUser(intent, UserHandle.ALL, BLUETOOTH_PERM);
+                sendBleStateChanged(prevState, newState);
+                //Don't broadcase this as std intent
+                isStandardBroadcast = false;
+
+            } else if (intermediate_off) {
+                if (DBG) Slog.d(TAG, "Intermediate off, back to LE only mode");
+                // For LE only mode, broadcast as is
+                sendBleStateChanged(prevState, newState);
+                sendBluetoothStateCallback(false); // BT is OFF for general users
+                // Broadcast as STATE_OFF
+                newState = BluetoothAdapter.STATE_OFF;
+                sendBrEdrDownCallback();
             }
+        } else if (newState == BluetoothAdapter.STATE_ON) {
+            boolean isUp = (newState == BluetoothAdapter.STATE_ON);
+            sendBluetoothStateCallback(isUp);
+            sendBleStateChanged(prevState, newState);
+
+        } else if (newState == BluetoothAdapter.STATE_BLE_TURNING_ON ||
+                newState == BluetoothAdapter.STATE_BLE_TURNING_OFF ) {
+            sendBleStateChanged(prevState, newState);
+            isStandardBroadcast = false;
+
+        } else if (newState == BluetoothAdapter.STATE_TURNING_ON ||
+                newState == BluetoothAdapter.STATE_TURNING_OFF) {
+            sendBleStateChanged(prevState, newState);
+        }
+
+        if (isStandardBroadcast) {
+            if (prevState == BluetoothAdapter.STATE_BLE_ON) {
+                // Show prevState of BLE_ON as OFF to standard users
+                prevState = BluetoothAdapter.STATE_OFF;
+            }
+            Intent intent = new Intent(BluetoothAdapter.ACTION_STATE_CHANGED);
+            intent.putExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, prevState);
+            intent.putExtra(BluetoothAdapter.EXTRA_STATE, newState);
+            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+            mContext.sendBroadcastAsUser(intent, UserHandle.ALL, BLUETOOTH_PERM);
         }
     }
 
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 614a94f..2693272 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -864,6 +864,7 @@
 
         mAvoidBadWifiTracker = createAvoidBadWifiTracker(
                 mContext, mHandler, () -> rematchForAvoidBadWifiUpdate());
+        mAvoidBadWifiTracker.start();
     }
 
     private NetworkRequest createInternetRequestForTransport(
diff --git a/services/core/java/com/android/server/DiskStatsService.java b/services/core/java/com/android/server/DiskStatsService.java
index 8ca675a..dd95f67 100644
--- a/services/core/java/com/android/server/DiskStatsService.java
+++ b/services/core/java/com/android/server/DiskStatsService.java
@@ -22,6 +22,15 @@
 import android.os.StatFs;
 import android.os.SystemClock;
 import android.os.storage.StorageManager;
+import android.util.Log;
+
+import com.android.server.storage.DiskStatsFileLogger;
+import com.android.server.storage.DiskStatsLoggingService;
+
+import libcore.io.IoUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
 
 import java.io.File;
 import java.io.FileDescriptor;
@@ -35,11 +44,13 @@
  */
 public class DiskStatsService extends Binder {
     private static final String TAG = "DiskStatsService";
+    private static final String DISKSTATS_DUMP_FILE = "/data/system/diskstats_cache.json";
 
     private final Context mContext;
 
     public DiskStatsService(Context context) {
         mContext = context;
+        DiskStatsLoggingService.schedule(context);
     }
 
     @Override
@@ -84,6 +95,10 @@
             pw.println("File-based Encryption: true");
         }
 
+        if (isCheckin(args)) {
+            reportCachedValues(pw);
+        }
+
         // TODO: Read /proc/yaffs and report interesting values;
         // add configurable (through args) performance test parameters.
     }
@@ -114,4 +129,45 @@
             return;
         }
     }
+
+    private boolean isCheckin(String[] args) {
+        for (String opt : args) {
+            if ("--checkin".equals(opt)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void reportCachedValues(PrintWriter pw) {
+        try {
+            String jsonString = IoUtils.readFileAsString(DISKSTATS_DUMP_FILE);
+            JSONObject json = new JSONObject(jsonString);
+            pw.print("App Size: ");
+            pw.println(json.getLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY));
+            pw.print("App Cache Size: ");
+            pw.println(json.getLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY));
+            pw.print("Photos Size: ");
+            pw.println(json.getLong(DiskStatsFileLogger.PHOTOS_KEY));
+            pw.print("Videos Size: ");
+            pw.println(json.getLong(DiskStatsFileLogger.VIDEOS_KEY));
+            pw.print("Audio Size: ");
+            pw.println(json.getLong(DiskStatsFileLogger.AUDIO_KEY));
+            pw.print("Downloads Size: ");
+            pw.println(json.getLong(DiskStatsFileLogger.DOWNLOADS_KEY));
+            pw.print("System Size: ");
+            pw.println(json.getLong(DiskStatsFileLogger.SYSTEM_KEY));
+            pw.print("Other Size: ");
+            pw.println(json.getLong(DiskStatsFileLogger.MISC_KEY));
+            pw.print("Package Names: ");
+            pw.println(json.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY));
+            pw.print("App Sizes: ");
+            pw.println(json.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY));
+            pw.print("Cache Sizes: ");
+            pw.println(json.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY));
+        } catch (IOException | JSONException e) {
+            Log.w(TAG, "exception reading diskstats cache file", e);
+        }
+    }
+
 }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index d2babe3..b68af43 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -1212,6 +1212,15 @@
     /**
      * Set while we are wanting to sleep, to prevent any
      * activities from being started/resumed.
+     *
+     * TODO(b/33594039): Clarify the actual state transitions represented by mSleeping.
+     *
+     * Currently mSleeping is set to true when transitioning into the sleep state, and remains true
+     * while in the sleep state until there is a pending transition out of sleep, in which case
+     * mSleeping is set to false, and remains false while awake.
+     *
+     * Whether mSleeping can quickly toggled between true/false without the device actually
+     * display changing states is undefined.
      */
     private boolean mSleeping = false;
 
@@ -1537,8 +1546,8 @@
     static final int NOTIFY_PINNED_STACK_ANIMATION_ENDED_LISTENERS_MSG = 66;
     static final int NOTIFY_FORCED_RESIZABLE_MSG = 67;
     static final int NOTIFY_ACTIVITY_DISMISSING_DOCKED_STACK_MSG = 68;
-    static final int VR_MODE_APPLY_IF_NEEDED_MSG = 69;
-    static final int SHOW_UNSUPPORTED_DISPLAY_SIZE_DIALOG_MSG = 70;
+    static final int SHOW_UNSUPPORTED_DISPLAY_SIZE_DIALOG_MSG = 69;
+    static final int NOTIFY_VR_SLEEPING_MSG = 70;
 
     static final int FIRST_ACTIVITY_STACK_MSG = 100;
     static final int FIRST_BROADCAST_QUEUE_MSG = 200;
@@ -2354,14 +2363,8 @@
                     }
                 }
                 vrService.setVrMode(vrMode, requestedPackage, userId, callingPackage);
-            } break;
-            case VR_MODE_APPLY_IF_NEEDED_MSG: {
-                final ActivityRecord r = (ActivityRecord) msg.obj;
-                final boolean needsVrMode = r != null && r.requestedVrComponent != null;
-                if (needsVrMode) {
-                    applyVrMode(msg.arg1 == 1, r.requestedVrComponent, r.userId,
-                            r.info.getComponentName(), false);
-                }
+            } case NOTIFY_VR_SLEEPING_MSG: {
+                notifyVrManagerOfSleepState(msg.arg1 != 0);
             } break;
             }
         }
@@ -3164,20 +3167,17 @@
                 mHandler.obtainMessage(VR_MODE_CHANGE_MSG, 0, 0, r));
     }
 
-    private void applyVrModeIfNeededLocked(ActivityRecord r, boolean enable) {
+    private void sendNotifyVrManagerOfSleepState(boolean isSleeping) {
         mHandler.sendMessage(
-                mHandler.obtainMessage(VR_MODE_APPLY_IF_NEEDED_MSG, enable ? 1 : 0, 0, r));
+                mHandler.obtainMessage(NOTIFY_VR_SLEEPING_MSG, isSleeping ? 1 : 0, 0));
     }
 
-    private void applyVrMode(boolean enabled, ComponentName packageName, int userId,
-            ComponentName callingPackage, boolean immediate) {
-        VrManagerInternal vrService =
-                LocalServices.getService(VrManagerInternal.class);
-        if (immediate) {
-            vrService.setVrModeImmediate(enabled, packageName, userId, callingPackage);
-        } else {
-            vrService.setVrMode(enabled, packageName, userId, callingPackage);
+    private void notifyVrManagerOfSleepState(boolean isSleeping) {
+        final VrManagerInternal vrService = LocalServices.getService(VrManagerInternal.class);
+        if (vrService == null) {
+            return;
         }
+        vrService.onSleepStateChanged(isSleeping);
     }
 
     final void showAskCompatModeDialogLocked(ActivityRecord r) {
@@ -6770,7 +6770,6 @@
 
                     // Some stack visibility might change (e.g. docked stack)
                     mStackSupervisor.ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
-                    applyVrModeIfNeededLocked(mFocusedActivity, true);
                 }
             }
         } finally {
@@ -11699,6 +11698,7 @@
             startTimeTrackingFocusedActivityLocked();
             mTopProcessState = ActivityManager.PROCESS_STATE_TOP;
             mStackSupervisor.comeOutOfSleepIfNeededLocked();
+            sendNotifyVrManagerOfSleepState(false);
             updateOomAdjLocked();
         } else if (!mSleeping && shouldSleepLocked()) {
             mSleeping = true;
@@ -11707,6 +11707,7 @@
             }
             mTopProcessState = ActivityManager.PROCESS_STATE_TOP_SLEEPING;
             mStackSupervisor.goingToSleepLocked();
+            sendNotifyVrManagerOfSleepState(true);
             updateOomAdjLocked();
 
             // Initialize the wake times of all processes.
@@ -21932,21 +21933,6 @@
         public SleepToken acquireSleepToken(String tag) {
             Preconditions.checkNotNull(tag);
 
-            ComponentName requestedVrService = null;
-            ComponentName callingVrActivity = null;
-            int userId = -1;
-            synchronized (ActivityManagerService.this) {
-                if (mFocusedActivity != null) {
-                    requestedVrService = mFocusedActivity.requestedVrComponent;
-                    callingVrActivity = mFocusedActivity.info.getComponentName();
-                    userId = mFocusedActivity.userId;
-                }
-            }
-
-            if (requestedVrService != null) {
-                applyVrMode(false, requestedVrService, userId, callingVrActivity, true);
-            }
-
             synchronized (ActivityManagerService.this) {
                 SleepTokenImpl token = new SleepTokenImpl(tag);
                 mSleepTokens.add(token);
diff --git a/services/core/java/com/android/server/connectivity/ConnectStats.java b/services/core/java/com/android/server/connectivity/ConnectStats.java
new file mode 100644
index 0000000..d6de815
--- /dev/null
+++ b/services/core/java/com/android/server/connectivity/ConnectStats.java
@@ -0,0 +1,123 @@
+/*
+ * 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.connectivity;
+
+import android.system.OsConstants;
+import android.util.IntArray;
+import android.util.SparseIntArray;
+import com.android.internal.util.TokenBucket;
+import com.android.server.connectivity.metrics.IpConnectivityLogClass.ConnectStatistics;
+import com.android.server.connectivity.metrics.IpConnectivityLogClass.Pair;
+
+/**
+ * A class that aggregates connect() statistics and helps build
+ * IpConnectivityLogClass.ConnectStatistics instances.
+ *
+ * {@hide}
+ */
+public class ConnectStats {
+    private final static int EALREADY     = OsConstants.EALREADY;
+    private final static int EINPROGRESS  = OsConstants.EINPROGRESS;
+
+    /** How many events resulted in a given errno. */
+    private final SparseIntArray mErrnos = new SparseIntArray();
+    /** Latencies of blocking connects. TODO: add non-blocking connects latencies. */
+    private final IntArray mLatencies = new IntArray();
+    /** TokenBucket for rate limiting latency recording. */
+    private final TokenBucket mLatencyTb;
+    /** Maximum number of latency values recorded. */
+    private final int mMaxLatencyRecords;
+    /** Total count of successful connects. */
+    private int mConnectCount = 0;
+    /** Total count of successful connects with IPv6 socket address. */
+    private int mIpv6ConnectCount = 0;
+
+    public ConnectStats(TokenBucket tb, int maxLatencyRecords) {
+        mLatencyTb = tb;
+        mMaxLatencyRecords = maxLatencyRecords;
+    }
+
+    public ConnectStatistics toProto() {
+        ConnectStatistics stats = new ConnectStatistics();
+        stats.connectCount = mConnectCount;
+        stats.ipv6AddrCount = mIpv6ConnectCount;
+        stats.latenciesMs = mLatencies.toArray();
+        stats.errnosCounters = toPairArrays(mErrnos);
+        return stats;
+    }
+
+    public void addEvent(int errno, int latencyMs, String ipAddr) {
+        if (isSuccess(errno)) {
+            countConnect(ipAddr);
+            countLatency(errno, latencyMs);
+        } else {
+            countError(errno);
+        }
+    }
+
+    private void countConnect(String ipAddr) {
+        mConnectCount++;
+        if (isIPv6(ipAddr)) mIpv6ConnectCount++;
+    }
+
+    private void countLatency(int errno, int ms) {
+        if (isNonBlocking(errno)) {
+            // Ignore connect() on non-blocking sockets
+            return;
+        }
+        if (!mLatencyTb.get()) {
+            // Rate limited
+            return;
+        }
+        if (mLatencies.size() >= mMaxLatencyRecords) {
+            // Hard limit the total number of latency measurements.
+            return;
+        }
+        mLatencies.add(ms);
+    }
+
+    private void countError(int errno) {
+        final int newcount = mErrnos.get(errno, 0) + 1;
+        mErrnos.put(errno, newcount);
+    }
+
+    private static boolean isSuccess(int errno) {
+        return (errno == 0) || isNonBlocking(errno);
+    }
+
+    private static boolean isNonBlocking(int errno) {
+        // On non-blocking TCP sockets, connect() immediately returns EINPROGRESS.
+        // On non-blocking TCP sockets that are connecting, connect() immediately returns EALREADY.
+        return (errno == EINPROGRESS) || (errno == EALREADY);
+    }
+
+    private static boolean isIPv6(String ipAddr) {
+        return ipAddr.contains(":");
+    }
+
+    private static Pair[] toPairArrays(SparseIntArray counts) {
+        final int s = counts.size();
+        Pair[] pairs = new Pair[s];
+        for (int i = 0; i < s; i++) {
+            Pair p = new Pair();
+            p.key = counts.keyAt(i);
+            p.value = counts.valueAt(i);
+            pairs[i] = p;
+        }
+        return pairs;
+    }
+}
diff --git a/services/core/java/com/android/server/connectivity/IpConnectivityEventBuilder.java b/services/core/java/com/android/server/connectivity/IpConnectivityEventBuilder.java
index f1d01e0..2a2d1ab 100644
--- a/services/core/java/com/android/server/connectivity/IpConnectivityEventBuilder.java
+++ b/services/core/java/com/android/server/connectivity/IpConnectivityEventBuilder.java
@@ -43,10 +43,10 @@
     private IpConnectivityEventBuilder() {
     }
 
-    public static byte[] serialize(int dropped, List<ConnectivityMetricsEvent> events)
+    public static byte[] serialize(int dropped, List<IpConnectivityEvent> events)
             throws IOException {
         final IpConnectivityLog log = new IpConnectivityLog();
-        log.events = toProto(events);
+        log.events = events.toArray(new IpConnectivityEvent[events.size()]);
         log.droppedEvents = dropped;
         if ((log.events.length > 0) || (dropped > 0)) {
             // Only write version number if log has some information at all.
@@ -55,7 +55,7 @@
         return IpConnectivityLog.toByteArray(log);
     }
 
-    public static IpConnectivityEvent[] toProto(List<ConnectivityMetricsEvent> eventsIn) {
+    public static List<IpConnectivityEvent> toProto(List<ConnectivityMetricsEvent> eventsIn) {
         final ArrayList<IpConnectivityEvent> eventsOut = new ArrayList<>(eventsIn.size());
         for (ConnectivityMetricsEvent in : eventsIn) {
             final IpConnectivityEvent out = toProto(in);
@@ -64,7 +64,7 @@
             }
             eventsOut.add(out);
         }
-        return eventsOut.toArray(new IpConnectivityEvent[eventsOut.size()]);
+        return eventsOut;
     }
 
     public static IpConnectivityEvent toProto(ConnectivityMetricsEvent ev) {
diff --git a/services/core/java/com/android/server/connectivity/IpConnectivityMetrics.java b/services/core/java/com/android/server/connectivity/IpConnectivityMetrics.java
index 6654814..76c3528 100644
--- a/services/core/java/com/android/server/connectivity/IpConnectivityMetrics.java
+++ b/services/core/java/com/android/server/connectivity/IpConnectivityMetrics.java
@@ -19,10 +19,13 @@
 import android.content.Context;
 import android.net.ConnectivityMetricsEvent;
 import android.net.IIpConnectivityMetrics;
+import android.net.INetdEventCallback;
 import android.net.metrics.ApfProgramEvent;
 import android.net.metrics.IpConnectivityLog;
+import android.os.Binder;
 import android.os.IBinder;
 import android.os.Parcelable;
+import android.os.Process;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
@@ -33,14 +36,14 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.TokenBucket;
 import com.android.server.SystemService;
+import com.android.server.connectivity.metrics.IpConnectivityLogClass.IpConnectivityEvent;
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.List;
 import java.util.function.ToIntFunction;
 
-import static com.android.server.connectivity.metrics.IpConnectivityLogClass.IpConnectivityEvent;
-
 /** {@hide} */
 final public class IpConnectivityMetrics extends SystemService {
     private static final String TAG = IpConnectivityMetrics.class.getSimpleName();
@@ -60,6 +63,8 @@
     // Maximum size of the event buffer.
     private static final int MAXIMUM_BUFFER_SIZE = DEFAULT_BUFFER_SIZE * 10;
 
+    private static final int MAXIMUM_CONNECT_LATENCY_RECORDS = 20000;
+
     private static final int ERROR_RATE_LIMITED = -1;
 
     // Lock ensuring that concurrent manipulations of the event buffer are correct.
@@ -157,9 +162,15 @@
             initBuffer();
         }
 
+        final List<IpConnectivityEvent> protoEvents = IpConnectivityEventBuilder.toProto(events);
+
+        if (mNetdListener != null) {
+            mNetdListener.flushStatistics(protoEvents);
+        }
+
         final byte[] data;
         try {
-            data = IpConnectivityEventBuilder.serialize(dropped, events);
+            data = IpConnectivityEventBuilder.serialize(dropped, protoEvents);
         } catch (IOException e) {
             Log.e(TAG, "could not serialize events", e);
             return "";
@@ -260,6 +271,33 @@
         private void enforcePermission(String what) {
             getContext().enforceCallingOrSelfPermission(what, "IpConnectivityMetrics");
         }
+
+        private void enforceNetdEventListeningPermission() {
+            final int uid = Binder.getCallingUid();
+            if (uid != Process.SYSTEM_UID) {
+                throw new SecurityException(String.format("Uid %d has no permission to listen for"
+                        + " netd events.", uid));
+            }
+        }
+
+        @Override
+        public boolean registerNetdEventCallback(INetdEventCallback callback) {
+            enforceNetdEventListeningPermission();
+            if (mNetdListener == null) {
+                return false;
+            }
+            return mNetdListener.registerNetdEventCallback(callback);
+        }
+
+        @Override
+        public boolean unregisterNetdEventCallback() {
+            enforceNetdEventListeningPermission();
+            if (mNetdListener == null) {
+                // if the service is null, we aren't registered anyway
+                return true;
+            }
+            return mNetdListener.unregisterNetdEventCallback();
+        }
     };
 
     private static final ToIntFunction<Context> READ_BUFFER_SIZE = (ctx) -> {
diff --git a/services/core/java/com/android/server/connectivity/NetdEventListenerService.java b/services/core/java/com/android/server/connectivity/NetdEventListenerService.java
index 4b175d7..3f056a5 100644
--- a/services/core/java/com/android/server/connectivity/NetdEventListenerService.java
+++ b/services/core/java/com/android/server/connectivity/NetdEventListenerService.java
@@ -19,23 +19,27 @@
 import android.content.Context;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
+import android.net.INetdEventCallback;
 import android.net.Network;
 import android.net.NetworkRequest;
 import android.net.metrics.DnsEvent;
 import android.net.metrics.INetdEventListener;
 import android.net.metrics.IpConnectivityLog;
+import android.os.RemoteException;
+import android.text.format.DateUtils;
 import android.util.Log;
-
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
-
+import com.android.internal.util.TokenBucket;
+import com.android.server.connectivity.metrics.IpConnectivityLogClass.ConnectStatistics;
+import com.android.server.connectivity.metrics.IpConnectivityLogClass.IpConnectivityEvent;
 import java.io.PrintWriter;
 import java.util.Arrays;
+import java.util.List;
 import java.util.SortedMap;
 import java.util.TreeMap;
 
-
 /**
  * Implementation of the INetdEventListener interface.
  */
@@ -44,12 +48,18 @@
     public static final String SERVICE_NAME = "netd_listener";
 
     private static final String TAG = NetdEventListenerService.class.getSimpleName();
-    private static final boolean DBG = true;
+    private static final boolean DBG = false;
     private static final boolean VDBG = false;
 
     // TODO: read this constant from system property
     private static final int MAX_LOOKUPS_PER_DNS_EVENT = 100;
 
+    // Rate limit connect latency logging to 1 measurement per 15 seconds (5760 / day) with maximum
+    // bursts of 5000 measurements.
+    private static final int CONNECT_LATENCY_BURST_LIMIT  = 5000;
+    private static final int CONNECT_LATENCY_FILL_RATE    = 15 * (int) DateUtils.SECOND_IN_MILLIS;
+    private static final int CONNECT_LATENCY_MAXIMUM_RECORDS = 20000;
+
     // Stores the results of a number of consecutive DNS lookups on the same network.
     // This class is not thread-safe and it is the responsibility of the service to call its methods
     // on one thread at a time.
@@ -86,7 +96,7 @@
             byte[] returnCodes = Arrays.copyOf(mReturnCodes, mEventCount);
             int[] latenciesMs = Arrays.copyOf(mLatenciesMs, mEventCount);
             mMetricsLog.log(new DnsEvent(mNetId, eventTypes, returnCodes, latenciesMs));
-            maybeLog(String.format("Logging %d results for netId %d", mEventCount, mNetId));
+            maybeLog("Logging %d results for netId %d", mEventCount, mNetId);
             mEventCount = 0;
         }
 
@@ -119,6 +129,27 @@
         }
     };
 
+    @GuardedBy("this")
+    private final TokenBucket mConnectTb =
+            new TokenBucket(CONNECT_LATENCY_FILL_RATE, CONNECT_LATENCY_BURST_LIMIT);
+    @GuardedBy("this")
+    private ConnectStats mConnectStats = makeConnectStats();
+
+    // Callback should only be registered/unregistered when logging is being enabled/disabled in DPM
+    // by the device owner. It's DevicePolicyManager's responsibility to ensure that.
+    @GuardedBy("this")
+    private INetdEventCallback mNetdEventCallback;
+
+    public synchronized boolean registerNetdEventCallback(INetdEventCallback callback) {
+        mNetdEventCallback = callback;
+        return true;
+    }
+
+    public synchronized boolean unregisterNetdEventCallback() {
+        mNetdEventCallback = null;
+        return true;
+    }
+
     public NetdEventListenerService(Context context) {
         this(context.getSystemService(ConnectivityManager.class), new IpConnectivityLog());
     }
@@ -136,9 +167,9 @@
     // Called concurrently by multiple binder threads.
     // This method must not block or perform long-running operations.
     public synchronized void onDnsEvent(int netId, int eventType, int returnCode, int latencyMs,
-            String hostname, String[] ipAddresses, int ipAddressesCount, int uid) {
-        maybeVerboseLog(String.format("onDnsEvent(%d, %d, %d, %d)",
-                netId, eventType, returnCode, latencyMs));
+            String hostname, String[] ipAddresses, int ipAddressesCount, int uid)
+            throws RemoteException {
+        maybeVerboseLog("onDnsEvent(%d, %d, %d, %dms)", netId, eventType, returnCode, latencyMs);
 
         DnsEventBatch batch = mEventBatches.get(netId);
         if (batch == null) {
@@ -146,14 +177,38 @@
             mEventBatches.put(netId, batch);
         }
         batch.addResult((byte) eventType, (byte) returnCode, latencyMs);
+
+        if (mNetdEventCallback != null) {
+            mNetdEventCallback.onDnsEvent(hostname, ipAddresses, ipAddressesCount,
+                    System.currentTimeMillis(), uid);
+        }
     }
 
     @Override
     // Called concurrently by multiple binder threads.
     // This method must not block or perform long-running operations.
-    public synchronized void onConnectEvent(int netId, int latencyMs, String ipAddr, int port,
-            int uid) {
-        maybeVerboseLog(String.format("onConnectEvent(%d, %d)", netId, latencyMs));
+    public synchronized void onConnectEvent(int netId, int error, int latencyMs, String ipAddr, int port,
+            int uid) throws RemoteException {
+        maybeVerboseLog("onConnectEvent(%d, %d)", netId, latencyMs);
+
+        mConnectStats.addEvent(error, latencyMs, ipAddr);
+
+        if (mNetdEventCallback != null) {
+            mNetdEventCallback.onConnectEvent(ipAddr, port, System.currentTimeMillis(), uid);
+        }
+    }
+
+    public synchronized void flushStatistics(List<IpConnectivityEvent> events) {
+        events.add(flushConnectStats());
+        // TODO: migrate DnsEventBatch to IpConnectivityLogClass.DNSLatencies
+    }
+
+    private IpConnectivityEvent flushConnectStats() {
+        IpConnectivityEvent ev = new IpConnectivityEvent();
+        ev.connectStatistics = mConnectStats.toProto();
+        // TODO: add transport information
+        mConnectStats = makeConnectStats();
+        return ev;
     }
 
     public synchronized void dump(PrintWriter writer) {
@@ -163,14 +218,19 @@
         for (DnsEventBatch batch : mEventBatches.values()) {
             pw.println(batch.toString());
         }
+        // TODO: also dump ConnectStats
         pw.decreaseIndent();
     }
 
-    private static void maybeLog(String s) {
-        if (DBG) Log.d(TAG, s);
+    private ConnectStats makeConnectStats() {
+        return new ConnectStats(mConnectTb, CONNECT_LATENCY_MAXIMUM_RECORDS);
     }
 
-    private static void maybeVerboseLog(String s) {
-        if (VDBG) Log.d(TAG, s);
+    private static void maybeLog(String s, Object... args) {
+        if (DBG) Log.d(TAG, String.format(s, args));
+    }
+
+    private static void maybeVerboseLog(String s, Object... args) {
+        if (VDBG) Log.d(TAG, String.format(s, args));
     }
 }
diff --git a/services/core/java/com/android/server/connectivity/NetworkMonitor.java b/services/core/java/com/android/server/connectivity/NetworkMonitor.java
index 5e98859..9ffe2b7 100644
--- a/services/core/java/com/android/server/connectivity/NetworkMonitor.java
+++ b/services/core/java/com/android/server/connectivity/NetworkMonitor.java
@@ -433,6 +433,8 @@
                             }));
                     intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL,
                             mLastPortalProbeResult.detectUrl);
+                    intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_USER_AGENT,
+                            getCaptivePortalUserAgent(mContext));
                     intent.setFlags(
                             Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
                     mContext.startActivityAsUser(intent, UserHandle.CURRENT);
diff --git a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
index c6bf4c5..497a6b7 100644
--- a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
+++ b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
@@ -19,7 +19,6 @@
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
-import android.widget.Toast;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
@@ -27,17 +26,40 @@
 import android.os.UserHandle;
 import android.telephony.TelephonyManager;
 import android.util.Slog;
-
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.widget.Toast;
 import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsProto.MetricsEvent;
 
-import static android.net.NetworkCapabilities.*;
-
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 
 public class NetworkNotificationManager {
 
-    public static enum NotificationType { SIGN_IN, NO_INTERNET, LOST_INTERNET, NETWORK_SWITCH };
+    public static enum NotificationType {
+        LOST_INTERNET(MetricsEvent.NOTIFICATION_NETWORK_LOST_INTERNET),
+        NETWORK_SWITCH(MetricsEvent.NOTIFICATION_NETWORK_SWITCH),
+        NO_INTERNET(MetricsEvent.NOTIFICATION_NETWORK_NO_INTERNET),
+        SIGN_IN(MetricsEvent.NOTIFICATION_NETWORK_SIGN_IN);
 
-    private static final String NOTIFICATION_ID = "Connectivity.Notification";
+        public final int eventId;
+
+        NotificationType(int eventId) {
+            this.eventId = eventId;
+            Holder.sIdToTypeMap.put(eventId, this);
+        }
+
+        private static class Holder {
+            private static SparseArray<NotificationType> sIdToTypeMap = new SparseArray<>();
+        }
+
+        public static NotificationType getFromId(int id) {
+            return Holder.sIdToTypeMap.get(id);
+        }
+    };
 
     private static final String TAG = NetworkNotificationManager.class.getSimpleName();
     private static final boolean DBG = true;
@@ -46,11 +68,14 @@
     private final Context mContext;
     private final TelephonyManager mTelephonyManager;
     private final NotificationManager mNotificationManager;
+    // Tracks the types of notifications managed by this instance, from creation to cancellation.
+    private final SparseIntArray mNotificationTypeMap;
 
     public NetworkNotificationManager(Context c, TelephonyManager t, NotificationManager n) {
         mContext = c;
         mTelephonyManager = t;
         mNotificationManager = n;
+        mNotificationTypeMap = new SparseIntArray();
     }
 
     // TODO: deal more gracefully with multi-transport networks.
@@ -100,8 +125,10 @@
      */
     public void showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai,
             NetworkAgentInfo switchToNai, PendingIntent intent, boolean highPriority) {
-        int transportType;
-        String extraInfo;
+        final String tag = tagFor(id);
+        final int eventId = notifyType.eventId;
+        final int transportType;
+        final String extraInfo;
         if (nai != null) {
             transportType = getFirstTransportType(nai);
             extraInfo = nai.networkInfo.getExtraInfo();
@@ -114,9 +141,10 @@
         }
 
         if (DBG) {
-            Slog.d(TAG, "showNotification id=" + id + " " + notifyType
-                    + " transportType=" + getTransportName(transportType)
-                    + " extraInfo=" + extraInfo + " highPriority=" + highPriority);
+            Slog.d(TAG, String.format(
+                    "showNotification tag=%s event=%s transport=%s extraInfo=%s highPrioriy=%s",
+                    tag, nameOf(eventId), getTransportName(transportType), extraInfo,
+                    highPriority));
         }
 
         Resources r = Resources.getSystem();
@@ -154,7 +182,7 @@
             details = r.getString(R.string.network_switch_metered_detail, toTransport,
                     fromTransport);
         } else {
-            Slog.wtf(TAG, "Unknown notification type " + notifyType + "on network transport "
+            Slog.wtf(TAG, "Unknown notification type " + notifyType + " on network transport "
                     + getTransportName(transportType));
             return;
         }
@@ -184,22 +212,32 @@
 
         Notification notification = builder.build();
 
+        mNotificationTypeMap.put(id, eventId);
         try {
-            mNotificationManager.notifyAsUser(NOTIFICATION_ID, id, notification, UserHandle.ALL);
+            mNotificationManager.notifyAsUser(tag, eventId, notification, UserHandle.ALL);
         } catch (NullPointerException npe) {
             Slog.d(TAG, "setNotificationVisible: visible notificationManager error", npe);
         }
     }
 
     public void clearNotification(int id) {
+        final String tag = tagFor(id);
+        if (mNotificationTypeMap.indexOfKey(id) < 0) {
+            Slog.e(TAG, "cannot clear unknown notification with tag=" + tag);
+            return;
+        }
+        final int eventId = mNotificationTypeMap.get(id);
         if (DBG) {
-            Slog.d(TAG, "clearNotification id=" + id);
+            Slog.d(TAG, String.format("clearing notification tag=%s event=%s", tag,
+                   nameOf(eventId)));
         }
         try {
-            mNotificationManager.cancelAsUser(NOTIFICATION_ID, id, UserHandle.ALL);
+            mNotificationManager.cancelAsUser(tag, eventId, UserHandle.ALL);
         } catch (NullPointerException npe) {
-            Slog.d(TAG, "setNotificationVisible: cancel notificationManager error", npe);
+            Slog.d(TAG, String.format(
+                    "failed to clear notification tag=%s event=%s", tag, nameOf(eventId)), npe);
         }
+        mNotificationTypeMap.delete(id);
     }
 
     /**
@@ -222,4 +260,15 @@
                 R.string.network_switch_metered_toast, fromTransport, toTransport);
         Toast.makeText(mContext, text, Toast.LENGTH_LONG).show();
     }
+
+    @VisibleForTesting
+    static String tagFor(int id) {
+        return String.format("ConnectivityNotification:%d", id);
+    }
+
+    @VisibleForTesting
+    static String nameOf(int eventId) {
+        NotificationType t = NotificationType.getFromId(eventId);
+        return (t != null) ? t.name() : "UNKNOWN";
+    }
 }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 3bd533d..b9d06a0 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -5398,15 +5398,23 @@
                             result.remove(xpResolveInfo);
                         }
                         if (result.size() == 0 && !addEphemeral) {
+                            // No result in current profile, but found candidate in parent user.
+                            // And we are not going to add emphemeral app, so we can return the
+                            // result straight away.
                             result.add(xpDomainInfo.resolveInfo);
                             return result;
                         }
+                    } else if (result.size() <= 1 && !addEphemeral) {
+                        // No result in parent user and <= 1 result in current profile, and we
+                        // are not going to add emphemeral app, so we can return the result without
+                        // further processing.
+                        return result;
                     }
-                    if (result.size() > 1 || addEphemeral) {
-                        result = filterCandidatesWithDomainPreferredActivitiesLPr(
-                                intent, flags, result, xpDomainInfo, userId);
-                        sortResult = true;
-                    }
+                    // We have more than one candidate (combining results from current and parent
+                    // profile), so we need filtering and sorting.
+                    result = filterCandidatesWithDomainPreferredActivitiesLPr(
+                            intent, flags, result, xpDomainInfo, userId);
+                    sortResult = true;
                 }
             } else {
                 final PackageParser.Package pkg = mPackages.get(pkgName);
@@ -21099,6 +21107,11 @@
                 return mSettings.wasPackageEverLaunchedLPr(packageName, userId);
             }
         }
+
+        @Override
+        public String getNameForUid(int uid) {
+            return PackageManagerService.this.getNameForUid(uid);
+        }
     }
 
     @Override
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index b2f6cd2..889c52a 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -152,6 +152,7 @@
 import com.android.server.policy.keyguard.KeyguardServiceDelegate;
 import com.android.server.policy.keyguard.KeyguardServiceDelegate.DrawnListener;
 import com.android.server.statusbar.StatusBarManagerInternal;
+import com.android.server.vr.VrManagerInternal;
 
 import java.io.File;
 import java.io.FileReader;
@@ -6513,6 +6514,7 @@
                 mKeyguardDelegate.onScreenTurnedOff();
             }
         }
+        reportScreenStateToVrManager(false);
     }
 
     // Called on the DisplayManager's DisplayPowerController thread.
@@ -6548,6 +6550,15 @@
                 mKeyguardDelegate.onScreenTurnedOn();
             }
         }
+        reportScreenStateToVrManager(true);
+    }
+
+    private void reportScreenStateToVrManager(boolean isScreenOn) {
+        VrManagerInternal vrService = LocalServices.getService(VrManagerInternal.class);
+        if (vrService == null) {
+            return;
+        }
+        vrService.onScreenStateChanged(isScreenOn);
     }
 
     private void finishWindowsDrawn() {
diff --git a/services/core/java/com/android/server/storage/AppCollector.java b/services/core/java/com/android/server/storage/AppCollector.java
new file mode 100644
index 0000000..cf05e9f
--- /dev/null
+++ b/services/core/java/com/android/server/storage/AppCollector.java
@@ -0,0 +1,160 @@
+/*
+ * 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.storage;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageStatsObserver;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageStats;
+import android.content.pm.UserInfo;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserManager;
+import android.os.storage.VolumeInfo;
+import android.util.Log;
+import com.android.internal.os.BackgroundThread;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * AppCollector asynchronously collects package sizes.
+ */
+public class AppCollector {
+    private static String TAG = "AppCollector";
+
+    private CompletableFuture<List<PackageStats>> mStats;
+    private final BackgroundHandler mBackgroundHandler;
+
+    /**
+     * Constrcuts a new AppCollector which runs on the provided volume.
+     * @param context Android context used to get
+     * @param volume Volume to check for apps.
+     */
+    public AppCollector(Context context, VolumeInfo volume) {
+        mBackgroundHandler = new BackgroundHandler(BackgroundThread.get().getLooper(),
+                volume,
+                context.getPackageManager(),
+                (UserManager) context.getSystemService(Context.USER_SERVICE));
+    }
+
+    /**
+     * Returns a list of package stats for the context and volume. Note that in a multi-user
+     * environment, this may return stats for the same package multiple times. These "duplicate"
+     * entries will have the package stats for the package for a given user, not the package in
+     * aggregate.
+     * @param timeoutMillis Milliseconds before timing out and returning early with null.
+     */
+    public List<PackageStats> getPackageStats(long timeoutMillis) {
+        synchronized(this) {
+            if (mStats == null) {
+                mStats = new CompletableFuture<>();
+                mBackgroundHandler.sendEmptyMessage(BackgroundHandler.MSG_START_LOADING_SIZES);
+            }
+        }
+
+        List<PackageStats> value = null;
+        try {
+            value = mStats.get(timeoutMillis, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException | ExecutionException e) {
+            Log.e(TAG, "An exception occurred while getting app storage", e);
+        } catch (TimeoutException e) {
+            Log.e(TAG, "AppCollector timed out");
+        }
+        return value;
+    }
+
+    private class StatsObserver extends IPackageStatsObserver.Stub {
+        private AtomicInteger mCount;
+        private final ArrayList<PackageStats> mPackageStats;
+
+        public StatsObserver(int count) {
+            mCount = new AtomicInteger(count);
+            mPackageStats = new ArrayList<>(count);
+        }
+
+        @Override
+        public void onGetStatsCompleted(PackageStats packageStats, boolean succeeded)
+                throws RemoteException {
+            if (succeeded) {
+                mPackageStats.add(packageStats);
+            }
+
+            if (mCount.decrementAndGet() == 0) {
+                mStats.complete(mPackageStats);
+            }
+        }
+    }
+
+    private class BackgroundHandler extends Handler {
+        static final int MSG_START_LOADING_SIZES = 0;
+        private final VolumeInfo mVolume;
+        private final PackageManager mPm;
+        private final UserManager mUm;
+
+        BackgroundHandler(Looper looper, VolumeInfo volume, PackageManager pm, UserManager um) {
+            super(looper);
+            mVolume = volume;
+            mPm = pm;
+            mUm = um;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_START_LOADING_SIZES: {
+                    final List<ApplicationInfo> apps = mPm.getInstalledApplications(
+                            PackageManager.GET_UNINSTALLED_PACKAGES
+                                    | PackageManager.GET_DISABLED_COMPONENTS);
+
+                    final List<ApplicationInfo> volumeApps = new ArrayList<>();
+                    for (ApplicationInfo app : apps) {
+                        if (Objects.equals(app.volumeUuid, mVolume.getFsUuid())) {
+                            volumeApps.add(app);
+                        }
+                    }
+
+                    List<UserInfo> users = mUm.getUsers();
+                    final int count = users.size() * volumeApps.size();
+                    if (count == 0) {
+                        mStats.complete(new ArrayList<>());
+                    }
+
+                    // Kick off the async package size query for all apps.
+                    final StatsObserver observer = new StatsObserver(count);
+                    for (UserInfo user : users) {
+                        for (ApplicationInfo app : volumeApps) {
+                            mPm.getPackageSizeInfoAsUser(app.packageName, user.id,
+                                    observer);
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/storage/DiskStatsFileLogger.java b/services/core/java/com/android/server/storage/DiskStatsFileLogger.java
new file mode 100644
index 0000000..22299df
--- /dev/null
+++ b/services/core/java/com/android/server/storage/DiskStatsFileLogger.java
@@ -0,0 +1,166 @@
+/*
+ * 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/LICENSE2.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.storage;
+
+import android.content.pm.PackageStats;
+import android.os.Environment;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.server.storage.FileCollector.MeasurementResult;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * DiskStatsFileLogger logs collected storage information to a file in a JSON format.
+ *
+ * The following information is cached in the file:
+ * 1. Size of images on disk.
+ * 2. Size of videos on disk.
+ * 3. Size of audio on disk.
+ * 4. Size of the downloads folder.
+ * 5. System size.
+ * 6. Aggregate and individual app and app cache sizes.
+ * 7. How much storage couldn't be categorized in one of the above categories.
+ */
+public class DiskStatsFileLogger {
+    private static final String TAG = "DiskStatsLogger";
+
+    public static final String PHOTOS_KEY = "photosSize";
+    public static final String VIDEOS_KEY = "videosSize";
+    public static final String AUDIO_KEY = "audioSize";
+    public static final String DOWNLOADS_KEY = "downloadsSize";
+    public static final String SYSTEM_KEY = "systemSize";
+    public static final String MISC_KEY = "otherSize";
+    public static final String APP_SIZE_AGG_KEY = "appSize";
+    public static final String APP_CACHE_AGG_KEY = "cacheSize";
+    public static final String PACKAGE_NAMES_KEY = "packageNames";
+    public static final String APP_SIZES_KEY = "appSizes";
+    public static final String APP_CACHES_KEY = "cacheSizes";
+    public static final String LAST_QUERY_TIMESTAMP_KEY = "queryTime";
+
+    private MeasurementResult mResult;
+    private long mDownloadsSize;
+    private long mSystemSize;
+    private List<PackageStats> mPackageStats;
+
+    /**
+     * Constructs a DiskStatsFileLogger with calculated measurement results.
+     */
+    public DiskStatsFileLogger(MeasurementResult result, MeasurementResult downloadsResult,
+            List<PackageStats> stats, long systemSize) {
+        mResult = result;
+        mDownloadsSize = downloadsResult.totalAccountedSize();
+        mSystemSize = systemSize;
+        mPackageStats = stats;
+    }
+
+    /**
+     * Dumps the storage collection output to a file.
+     * @param file File to write the output into.
+     * @throws FileNotFoundException
+     */
+    public void dumpToFile(File file) throws FileNotFoundException {
+        PrintWriter pw = new PrintWriter(file);
+        JSONObject representation = getJsonRepresentation();
+        if (representation != null) {
+            pw.println(representation);
+        }
+        pw.close();
+    }
+
+    private JSONObject getJsonRepresentation() {
+        JSONObject json = new JSONObject();
+        try {
+            json.put(LAST_QUERY_TIMESTAMP_KEY, System.currentTimeMillis());
+            json.put(PHOTOS_KEY, mResult.imagesSize);
+            json.put(VIDEOS_KEY, mResult.videosSize);
+            json.put(AUDIO_KEY, mResult.audioSize);
+            json.put(DOWNLOADS_KEY, mDownloadsSize);
+            json.put(SYSTEM_KEY, mSystemSize);
+            json.put(MISC_KEY, mResult.miscSize);
+            addAppsToJson(json);
+        } catch (JSONException e) {
+            Log.e(TAG, e.toString());
+            return null;
+        }
+
+        return json;
+    }
+
+    private void addAppsToJson(JSONObject json) throws JSONException {
+        JSONArray names = new JSONArray();
+        JSONArray appSizeList = new JSONArray();
+        JSONArray cacheSizeList = new JSONArray();
+
+        long appSizeSum = 0L;
+        long cacheSizeSum = 0L;
+        boolean isExternal = Environment.isExternalStorageEmulated();
+        for (Map.Entry<String, PackageStats> entry : mergePackagesAcrossUsers().entrySet()) {
+            PackageStats stat = entry.getValue();
+            long appSize = stat.codeSize + stat.dataSize;
+            long cacheSize = stat.cacheSize;
+            if (isExternal) {
+                appSize += stat.externalCodeSize + stat.externalDataSize;
+                cacheSize += stat.externalCacheSize;
+            }
+            appSizeSum += appSize;
+            cacheSizeSum += cacheSize;
+
+            names.put(stat.packageName);
+            appSizeList.put(appSize);
+            cacheSizeList.put(cacheSize);
+        }
+        json.put(PACKAGE_NAMES_KEY, names);
+        json.put(APP_SIZES_KEY, appSizeList);
+        json.put(APP_CACHES_KEY, cacheSizeList);
+        json.put(APP_SIZE_AGG_KEY, appSizeSum);
+        json.put(APP_CACHE_AGG_KEY, cacheSizeSum);
+    }
+
+    /**
+     * A given package may exist for multiple users with distinct sizes. This function merges
+     * the duplicated packages together and sums up their sizes to get the actual totals for the
+     * package.
+     * @return A mapping of package name to merged package stats.
+     */
+    private ArrayMap<String, PackageStats> mergePackagesAcrossUsers() {
+        ArrayMap<String, PackageStats> packageMap = new ArrayMap<>();
+        for (PackageStats stat : mPackageStats) {
+            PackageStats existingStats = packageMap.get(stat.packageName);
+            if (existingStats != null) {
+                existingStats.cacheSize += stat.cacheSize;
+                existingStats.codeSize += stat.codeSize;
+                existingStats.dataSize += stat.dataSize;
+                existingStats.externalCacheSize += stat.externalCacheSize;
+                existingStats.externalCodeSize += stat.externalCodeSize;
+                existingStats.externalDataSize += stat.externalDataSize;
+            } else {
+                packageMap.put(stat.packageName, new PackageStats(stat));
+            }
+        }
+        return packageMap;
+    }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/storage/DiskStatsLoggingService.java b/services/core/java/com/android/server/storage/DiskStatsLoggingService.java
new file mode 100644
index 0000000..4a86175
--- /dev/null
+++ b/services/core/java/com/android/server/storage/DiskStatsLoggingService.java
@@ -0,0 +1,170 @@
+/*
+ * 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/LICENSE2.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.storage;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageStats;
+import android.os.AsyncTask;
+import android.os.BatteryManager;
+import android.os.Environment;
+import android.os.Environment.UserEnvironment;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.storage.FileCollector.MeasurementResult;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * DiskStatsLoggingService is a JobService which collects storage categorization information and
+ * app size information on a roughly daily cadence.
+ */
+public class DiskStatsLoggingService extends JobService {
+    private static final String TAG = "DiskStatsLogService";
+    public static final String DUMPSYS_CACHE_PATH = "/data/system/diskstats_cache.json";
+    private static final int JOB_DISKSTATS_LOGGING = 0x4449534b; // DISK
+    private static ComponentName sDiskStatsLoggingService = new ComponentName(
+            "android",
+            DiskStatsLoggingService.class.getName());
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        // We need to check the preconditions again because they may not be enforced for
+        // subsequent runs.
+        if (!isCharging(this)) {
+            jobFinished(params, true);
+            return false;
+        }
+
+        final int userId = UserHandle.myUserId();
+        UserEnvironment environment = new UserEnvironment(userId);
+        AppCollector collector = new AppCollector(this,
+                getPackageManager().getPrimaryStorageCurrentVolume());
+        LogRunnable task = new LogRunnable();
+        task.setRootDirectory(environment.getExternalStorageDirectory());
+        task.setDownloadsDirectory(
+                environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS));
+        task.setSystemSize(FileCollector.getSystemSize(this));
+        task.setLogOutputFile(new File(DUMPSYS_CACHE_PATH));
+        task.setAppCollector(collector);
+        task.setJobService(this, params);
+        AsyncTask.execute(task);
+        return true;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        // TODO: Try to stop being handled.
+        return false;
+    }
+
+    /**
+     * Schedules a DiskStats collection task. This task only runs on device idle while charging
+     * once every 24 hours.
+     * @param context Context to use to get a job scheduler.
+     */
+    public static void schedule(Context context) {
+        JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+
+        js.schedule(new JobInfo.Builder(JOB_DISKSTATS_LOGGING, sDiskStatsLoggingService)
+                .setRequiresDeviceIdle(true)
+                .setRequiresCharging(true)
+                .setPeriodic(TimeUnit.DAYS.toMillis(1))
+                .build());
+    }
+
+    private static boolean isCharging(Context context) {
+        BatteryManager batteryManager = context.getSystemService(BatteryManager.class);
+        if (batteryManager != null) {
+            return batteryManager.isCharging();
+        }
+        return false;
+    }
+
+    @VisibleForTesting
+    static class LogRunnable implements Runnable {
+        private static final long TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(5);
+
+        private JobService mJobService;
+        private JobParameters mParams;
+        private AppCollector mCollector;
+        private File mOutputFile;
+        private File mRootDirectory;
+        private File mDownloadsDirectory;
+        private long mSystemSize;
+
+        public void setRootDirectory(File file) {
+            mRootDirectory = file;
+        }
+
+        public void setDownloadsDirectory(File file) {
+            mDownloadsDirectory = file;
+        }
+
+        public void setAppCollector(AppCollector collector) {
+            mCollector = collector;
+        }
+
+        public void setLogOutputFile(File file) {
+            mOutputFile = file;
+        }
+
+        public void setSystemSize(long size) {
+            mSystemSize = size;
+        }
+
+        public void setJobService(JobService jobService, JobParameters params) {
+            mJobService = jobService;
+            mParams = params;
+        }
+
+        public void run() {
+            FileCollector.MeasurementResult mainCategories =
+                    FileCollector.getMeasurementResult(mRootDirectory);
+            FileCollector.MeasurementResult downloads =
+                    FileCollector.getMeasurementResult(mDownloadsDirectory);
+
+            logToFile(mainCategories, downloads, mCollector.getPackageStats(TIMEOUT_MILLIS),
+                    mSystemSize);
+
+            if (mJobService != null) {
+                mJobService.jobFinished(mParams, false);
+            }
+        }
+
+        private void logToFile(MeasurementResult mainCategories, MeasurementResult downloads,
+                List<PackageStats> stats, long systemSize) {
+            DiskStatsFileLogger logger = new DiskStatsFileLogger(mainCategories, downloads, stats,
+                    systemSize);
+            try {
+                mOutputFile.createNewFile();
+                logger.dumpToFile(mOutputFile);
+            } catch (IOException e) {
+                Log.e(TAG, "Exception while writing opportunistic disk file cache.", e);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/storage/FileCollector.java b/services/core/java/com/android/server/storage/FileCollector.java
new file mode 100644
index 0000000..90f9f139
--- /dev/null
+++ b/services/core/java/com/android/server/storage/FileCollector.java
@@ -0,0 +1,247 @@
+/*
+ * 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/LICENSE2.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.storage;
+
+import android.annotation.IntDef;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.storage.StorageManager;
+import android.os.storage.VolumeInfo;
+import android.util.ArrayMap;
+
+import java.io.File;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Map;
+
+/**
+ * FileCollector walks over a directory and categorizes storage usage by their type.
+ */
+public class FileCollector {
+    private static final int UNRECOGNIZED = -1;
+    private static final int IMAGES = 0;
+    private static final int VIDEO = 1;
+    private static final int AUDIO = 2;
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            UNRECOGNIZED,
+            IMAGES,
+            VIDEO,
+            AUDIO })
+    private @interface FileTypes {}
+
+
+    private static final Map<String, Integer> EXTENSION_MAP = new ArrayMap<String, Integer>();
+    static {
+        // Audio
+        EXTENSION_MAP.put("aac", AUDIO);
+        EXTENSION_MAP.put("amr", AUDIO);
+        EXTENSION_MAP.put("awb", AUDIO);
+        EXTENSION_MAP.put("snd", AUDIO);
+        EXTENSION_MAP.put("flac", AUDIO);
+        EXTENSION_MAP.put("mp3", AUDIO);
+        EXTENSION_MAP.put("mpga", AUDIO);
+        EXTENSION_MAP.put("mpega", AUDIO);
+        EXTENSION_MAP.put("mp2", AUDIO);
+        EXTENSION_MAP.put("m4a", AUDIO);
+        EXTENSION_MAP.put("aif", AUDIO);
+        EXTENSION_MAP.put("aiff", AUDIO);
+        EXTENSION_MAP.put("aifc", AUDIO);
+        EXTENSION_MAP.put("gsm", AUDIO);
+        EXTENSION_MAP.put("mka", AUDIO);
+        EXTENSION_MAP.put("m3u", AUDIO);
+        EXTENSION_MAP.put("wma", AUDIO);
+        EXTENSION_MAP.put("wax", AUDIO);
+        EXTENSION_MAP.put("ra", AUDIO);
+        EXTENSION_MAP.put("rm", AUDIO);
+        EXTENSION_MAP.put("ram", AUDIO);
+        EXTENSION_MAP.put("pls", AUDIO);
+        EXTENSION_MAP.put("sd2", AUDIO);
+        EXTENSION_MAP.put("wav", AUDIO);
+        EXTENSION_MAP.put("ogg", AUDIO);
+        EXTENSION_MAP.put("oga", AUDIO);
+        // Video
+        EXTENSION_MAP.put("3gpp", VIDEO);
+        EXTENSION_MAP.put("3gp", VIDEO);
+        EXTENSION_MAP.put("3gpp2", VIDEO);
+        EXTENSION_MAP.put("3g2", VIDEO);
+        EXTENSION_MAP.put("avi", VIDEO);
+        EXTENSION_MAP.put("dl", VIDEO);
+        EXTENSION_MAP.put("dif", VIDEO);
+        EXTENSION_MAP.put("dv", VIDEO);
+        EXTENSION_MAP.put("fli", VIDEO);
+        EXTENSION_MAP.put("m4v", VIDEO);
+        EXTENSION_MAP.put("ts", VIDEO);
+        EXTENSION_MAP.put("mpeg", VIDEO);
+        EXTENSION_MAP.put("mpg", VIDEO);
+        EXTENSION_MAP.put("mpe", VIDEO);
+        EXTENSION_MAP.put("mp4", VIDEO);
+        EXTENSION_MAP.put("vob", VIDEO);
+        EXTENSION_MAP.put("qt", VIDEO);
+        EXTENSION_MAP.put("mov", VIDEO);
+        EXTENSION_MAP.put("mxu", VIDEO);
+        EXTENSION_MAP.put("webm", VIDEO);
+        EXTENSION_MAP.put("lsf", VIDEO);
+        EXTENSION_MAP.put("lsx", VIDEO);
+        EXTENSION_MAP.put("mkv", VIDEO);
+        EXTENSION_MAP.put("mng", VIDEO);
+        EXTENSION_MAP.put("asf", VIDEO);
+        EXTENSION_MAP.put("asx", VIDEO);
+        EXTENSION_MAP.put("wm", VIDEO);
+        EXTENSION_MAP.put("wmv", VIDEO);
+        EXTENSION_MAP.put("wmx", VIDEO);
+        EXTENSION_MAP.put("wvx", VIDEO);
+        EXTENSION_MAP.put("movie", VIDEO);
+        EXTENSION_MAP.put("wrf", VIDEO);
+        // Images
+        EXTENSION_MAP.put("bmp", IMAGES);
+        EXTENSION_MAP.put("gif", IMAGES);
+        EXTENSION_MAP.put("jpg", IMAGES);
+        EXTENSION_MAP.put("jpeg", IMAGES);
+        EXTENSION_MAP.put("jpe", IMAGES);
+        EXTENSION_MAP.put("pcx", IMAGES);
+        EXTENSION_MAP.put("png", IMAGES);
+        EXTENSION_MAP.put("svg", IMAGES);
+        EXTENSION_MAP.put("svgz", IMAGES);
+        EXTENSION_MAP.put("tiff", IMAGES);
+        EXTENSION_MAP.put("tif", IMAGES);
+        EXTENSION_MAP.put("wbmp", IMAGES);
+        EXTENSION_MAP.put("webp", IMAGES);
+        EXTENSION_MAP.put("dng", IMAGES);
+        EXTENSION_MAP.put("cr2", IMAGES);
+        EXTENSION_MAP.put("ras", IMAGES);
+        EXTENSION_MAP.put("art", IMAGES);
+        EXTENSION_MAP.put("jng", IMAGES);
+        EXTENSION_MAP.put("nef", IMAGES);
+        EXTENSION_MAP.put("nrw", IMAGES);
+        EXTENSION_MAP.put("orf", IMAGES);
+        EXTENSION_MAP.put("rw2", IMAGES);
+        EXTENSION_MAP.put("pef", IMAGES);
+        EXTENSION_MAP.put("psd", IMAGES);
+        EXTENSION_MAP.put("pnm", IMAGES);
+        EXTENSION_MAP.put("pbm", IMAGES);
+        EXTENSION_MAP.put("pgm", IMAGES);
+        EXTENSION_MAP.put("ppm", IMAGES);
+        EXTENSION_MAP.put("srw", IMAGES);
+        EXTENSION_MAP.put("arw", IMAGES);
+        EXTENSION_MAP.put("rgb", IMAGES);
+        EXTENSION_MAP.put("xbm", IMAGES);
+        EXTENSION_MAP.put("xpm", IMAGES);
+        EXTENSION_MAP.put("xwd", IMAGES);
+    }
+
+    /**
+     * Returns the file categorization measurement result.
+     * @param path Directory to collect and categorize storage in.
+     */
+    public static MeasurementResult getMeasurementResult(File path) {
+        return collectFiles(StorageManager.maybeTranslateEmulatedPathToInternal(path),
+                new MeasurementResult());
+    }
+
+    /**
+     * Returns the size of a system for a given context. This is done by finding the difference
+     * between the shared data and the total primary storage size.
+     * @param context Context to use to get storage information.
+     */
+    public static long getSystemSize(Context context) {
+        PackageManager pm = context.getPackageManager();
+        VolumeInfo primaryVolume = pm.getPrimaryStorageCurrentVolume();
+
+        StorageManager sm = context.getSystemService(StorageManager.class);
+        VolumeInfo shared = sm.findEmulatedForPrivate(primaryVolume);
+        if (shared == null) {
+            return 0;
+        }
+
+        final long sharedDataSize = shared.getPath().getTotalSpace();
+        long systemSize = sm.getPrimaryStorageSize() - sharedDataSize;
+
+        // This case is not exceptional -- we just fallback to the shared data volume in this case.
+        if (systemSize <= 0) {
+            return 0;
+        }
+
+        return systemSize;
+    }
+
+    private static MeasurementResult collectFiles(File file, MeasurementResult result) {
+        File[] files = file.listFiles();
+
+        if (files == null) {
+            return result;
+        }
+
+        for (File f : files) {
+            if (f.isDirectory()) {
+                try {
+                    collectFiles(f, result);
+                } catch (StackOverflowError e) {
+                    return result;
+                }
+            } else {
+                handleFile(result, f);
+            }
+        }
+
+        return result;
+    }
+
+    private static void handleFile(MeasurementResult result, File f) {
+        long fileSize = f.length();
+        int fileType = EXTENSION_MAP.getOrDefault(getExtensionForFile(f), UNRECOGNIZED);
+        switch (fileType) {
+            case AUDIO:
+                result.audioSize += fileSize;
+                break;
+            case VIDEO:
+                result.videosSize += fileSize;
+                break;
+            case IMAGES:
+                result.imagesSize += fileSize;
+                break;
+            default:
+                result.miscSize += fileSize;
+        }
+    }
+
+    private static String getExtensionForFile(File file) {
+        String fileName = file.getName();
+        int index = fileName.lastIndexOf('.');
+        if (index == -1) {
+            return "";
+        }
+        return fileName.substring(index + 1).toLowerCase();
+    }
+
+    /**
+     * MeasurementResult contains a storage categorization result.
+     */
+    public static class MeasurementResult {
+        public long imagesSize;
+        public long videosSize;
+        public long miscSize;
+        public long audioSize;
+
+        /**
+         * Sums up the storage taken by all of the categorizable sizes in the measurement.
+         */
+        public long totalAccountedSize() {
+            return imagesSize + videosSize + miscSize + audioSize;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/vr/VrManagerInternal.java b/services/core/java/com/android/server/vr/VrManagerInternal.java
index ad87a88..0fc1900 100644
--- a/services/core/java/com/android/server/vr/VrManagerInternal.java
+++ b/services/core/java/com/android/server/vr/VrManagerInternal.java
@@ -56,25 +56,27 @@
             int userId, @NonNull ComponentName calling);
 
     /**
-     * Set the current VR mode state immediately.
+     * Set whether the system has acquired a sleep token.
      *
-     * @param enabled {@code true} to enable VR mode.
-     * @param packageName The package name of the requested VrListenerService to bind.
-     * @param userId the user requesting the VrListenerService component.
-     * @param calling the component currently using VR mode, or null to leave unchanged.
+     * @param isAsleep is {@code true} if the device is asleep, or {@code false} otherwise.
      */
-    public abstract void setVrModeImmediate(boolean enabled, @NonNull ComponentName packageName,
-            int userId, @NonNull ComponentName calling);
+    public abstract void onSleepStateChanged(boolean isAsleep);
 
+    /**
+     * Set whether the display used for VR output is on.
+     *
+     * @param isScreenOn is {@code true} if the display is on and can receive commands,
+     *      or {@code false} otherwise.
+     */
+    public abstract void onScreenStateChanged(boolean isScreenOn);
 
-   /**
-    * Return NO_ERROR if the given package is installed on the device and enabled as a
-    * VrListenerService for the given current user, or a negative error code indicating a failure.
-    *
-    * @param packageName the name of the package to check, or null to select the default package.
-    * @return NO_ERROR if the given package is installed and is enabled, or a negative error code
-    *       given in {@link android.service.vr.VrModeException} on failure.
-    */
+    /**
+     * Return NO_ERROR if the given package is installed on the device and enabled as a
+     * VrListenerService for the given current user, or a negative error code indicating a failure.
+     *
+     * @param packageName the name of the package to check, or null to select the default package.
+     * @return NO_ERROR if the given package is installed and is enabled, or a negative error code
+     *       given in {@link android.service.vr.VrModeException} on failure.
+     */
     public abstract int hasVrPackage(@NonNull ComponentName packageName, int userId);
-
 }
diff --git a/services/core/java/com/android/server/vr/VrManagerService.java b/services/core/java/com/android/server/vr/VrManagerService.java
index fdadc8d..1083e0a 100644
--- a/services/core/java/com/android/server/vr/VrManagerService.java
+++ b/services/core/java/com/android/server/vr/VrManagerService.java
@@ -101,6 +101,14 @@
     private static final int PENDING_STATE_DELAY_MS = 300;
     private static final int EVENT_LOG_SIZE = 32;
     private static final int INVALID_APPOPS_MODE = -1;
+    /** Null set of sleep sleep flags. */
+    private static final int FLAG_NONE = 0;
+    /** Flag set when the device is not sleeping. */
+    private static final int FLAG_AWAKE = 1;
+    /** Flag set when the screen has been turned on. */
+    private static final int FLAG_SCREEN_ON = 2;
+    /** Flag indicating that all system sleep flags have been set.*/
+    private static final int FLAG_ALL = FLAG_AWAKE | FLAG_SCREEN_ON;
 
     private static native void initializeNative();
     private static native void setVrModeNative(boolean enabled);
@@ -110,6 +118,7 @@
     private final IBinder mOverlayToken = new Binder();
 
     // State protected by mLock
+    private boolean mVrModeAllowed;
     private boolean mVrModeEnabled;
     private EnabledComponentsObserver mComponentObserver;
     private ManagedApplicationService mCurrentVrService;
@@ -125,10 +134,64 @@
     private VrState mPendingState;
     private final ArrayDeque<VrState> mLoggingDeque = new ArrayDeque<>(EVENT_LOG_SIZE);
     private final NotificationAccessManager mNotifAccessManager = new NotificationAccessManager();
+    /** Tracks the state of the screen and keyguard UI.*/
+    private int mSystemSleepFlags = FLAG_NONE;
 
     private static final int MSG_VR_STATE_CHANGE = 0;
     private static final int MSG_PENDING_VR_STATE_CHANGE = 1;
 
+    /**
+     * Set whether VR mode may be enabled.
+     * <p/>
+     * If VR mode is not allowed to be enabled, calls to set VR mode will be cached.  When VR mode
+     * is again allowed to be enabled, the most recent cached state will be applied.
+     *
+     * @param allowed {@code true} if calling any of the setVrMode methods may cause the device to
+     *   enter VR mode.
+     */
+    private void setVrModeAllowedLocked(boolean allowed) {
+        if (mVrModeAllowed != allowed) {
+            mVrModeAllowed = allowed;
+            Slog.i(TAG, "VR mode is " + ((allowed) ? "allowed" : "disallowed"));
+            if (mVrModeAllowed) {
+                consumeAndApplyPendingStateLocked();
+            } else {
+                // Set pending state to current state.
+                mPendingState = (mVrModeEnabled && mCurrentVrService != null)
+                    ? new VrState(mVrModeEnabled, mCurrentVrService.getComponent(),
+                        mCurrentVrService.getUserId(), mCurrentVrModeComponent)
+                    : null;
+
+                // Unbind current VR service and do necessary callbacks.
+                updateCurrentVrServiceLocked(false, null, 0, null);
+            }
+        }
+    }
+
+    private void setSleepState(boolean isAsleep) {
+        synchronized(mLock) {
+
+            if (!isAsleep) {
+                mSystemSleepFlags |= FLAG_AWAKE;
+            } else {
+                mSystemSleepFlags &= ~FLAG_AWAKE;
+            }
+
+            setVrModeAllowedLocked(mSystemSleepFlags == FLAG_ALL);
+        }
+    }
+
+    private void setScreenOn(boolean isScreenOn) {
+        synchronized(mLock) {
+            if (isScreenOn) {
+                mSystemSleepFlags |= FLAG_SCREEN_ON;
+            } else {
+                mSystemSleepFlags &= ~FLAG_SCREEN_ON;
+            }
+            setVrModeAllowedLocked(mSystemSleepFlags == FLAG_ALL);
+        }
+    }
+
     private final Handler mHandler = new Handler() {
         @Override
         public void handleMessage(Message msg) {
@@ -148,7 +211,9 @@
                 } break;
                 case MSG_PENDING_VR_STATE_CHANGE : {
                     synchronized(mLock) {
-                        VrManagerService.this.consumeAndApplyPendingStateLocked();
+                        if (mVrModeAllowed) {
+                           VrManagerService.this.consumeAndApplyPendingStateLocked();
+                        }
                     }
                 } break;
                 default :
@@ -268,8 +333,8 @@
             }
             mNotifAccessManager.update(enabledPackages);
 
-            if (mCurrentVrService == null) {
-                return; // No active services
+            if (!mVrModeAllowed) {
+                return; // Don't do anything, we shouldn't be in VR mode.
             }
 
             // If there is a pending state change, we'd better deal with that first
@@ -321,6 +386,7 @@
                 return;
             }
             pw.println("********* Dump of VrManagerService *********");
+            pw.println("VR mode is currently: " + ((mVrModeAllowed) ? "allowed" : "disallowed"));
             pw.println("Previous state transitions:\n");
             String tab = "  ";
             dumpStateTransitions(pw);
@@ -374,13 +440,17 @@
         @Override
         public void setVrMode(boolean enabled, ComponentName packageName, int userId,
                 ComponentName callingPackage) {
-            VrManagerService.this.setVrMode(enabled, packageName, userId, callingPackage, false);
+            VrManagerService.this.setVrMode(enabled, packageName, userId, callingPackage);
         }
 
         @Override
-        public void setVrModeImmediate(boolean enabled, ComponentName packageName, int userId,
-                ComponentName callingPackage) {
-            VrManagerService.this.setVrMode(enabled, packageName, userId, callingPackage, true);
+        public void onSleepStateChanged(boolean isAsleep) {
+            VrManagerService.this.setSleepState(isAsleep);
+        }
+
+        @Override
+        public void onScreenStateChanged(boolean isScreenOn) {
+            VrManagerService.this.setScreenOn(isScreenOn);
         }
 
         @Override
@@ -424,6 +494,10 @@
 
                 mComponentObserver.rebuildAll();
             }
+        } else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
+            synchronized (mLock) {
+                mVrModeAllowed = true;
+            }
         }
     }
 
@@ -466,12 +540,16 @@
                     false, mOverlayToken, null, oldUserId);
         }
 
+        if (!mVrModeEnabled) {
+            return;
+        }
+
         // Apply the restrictions for the current user based on vr state
         String[] exemptions = (exemptedPackage == null) ? new String[0] :
                 new String[] { exemptedPackage };
 
         appOpsManager.setUserRestrictionForUser(AppOpsManager.OP_SYSTEM_ALERT_WINDOW,
-                mVrModeEnabled, mOverlayToken, exemptions, newUserId);
+                true, mOverlayToken, exemptions, newUserId);
     }
 
     private void updateDependentAppOpsLocked(String newVrServicePackage, int newUserId,
@@ -512,7 +590,8 @@
 
             boolean validUserComponent = (mComponentObserver.isValid(component, userId) ==
                     EnabledComponentsObserver.NO_ERROR);
-            if (!mVrModeEnabled && !enabled) {
+            boolean goingIntoVrMode = validUserComponent && enabled;
+            if (!mVrModeEnabled && !goingIntoVrMode) {
                 return validUserComponent; // Disabled -> Disabled transition does nothing.
             }
 
@@ -520,29 +599,37 @@
                     ? mCurrentVrService.getComponent().getPackageName() : null;
             final int oldUserId = mCurrentVrModeUser;
 
-            // Always send mode change events.
-            changeVrModeLocked(enabled);
+            // Notify system services and VR HAL of mode change.
+            changeVrModeLocked(goingIntoVrMode);
 
-            if (!enabled || !validUserComponent) {
-                // Unbind whatever is running
+            boolean nothingChanged = false;
+            if (!goingIntoVrMode) {
+                // Not going into VR mode, unbind whatever is running
                 if (mCurrentVrService != null) {
                     Slog.i(TAG, "Disconnecting " + mCurrentVrService.getComponent() + " for user " +
                             mCurrentVrService.getUserId());
                     mCurrentVrService.disconnect();
                     mCurrentVrService = null;
+                } else {
+                    nothingChanged = true;
                 }
             } else {
+                // Going into VR mode
                 if (mCurrentVrService != null) {
-                    // Unbind any running service that doesn't match the component/user selection
+                    // Unbind any running service that doesn't match the latest component/user
+                    // selection.
                     if (mCurrentVrService.disconnectIfNotMatching(component, userId)) {
                         Slog.i(TAG, "Disconnecting " + mCurrentVrService.getComponent() +
                                 " for user " + mCurrentVrService.getUserId());
                         createAndConnectService(component, userId);
                         sendUpdatedCaller = true;
+                    } else {
+                        nothingChanged = true;
                     }
-                    // The service with the correct component/user is bound
+                    // The service with the correct component/user is already bound, do nothing.
                 } else {
-                    // Nothing was previously running, bind a new service
+                    // Nothing was previously running, bind a new service for the latest
+                    // component/user selection.
                     createAndConnectService(component, userId);
                     sendUpdatedCaller = true;
                 }
@@ -577,7 +664,10 @@
                     }
                 });
             }
-            logStateLocked();
+
+            if (!nothingChanged) {
+                logStateLocked();
+            }
 
             return validUserComponent;
         } finally {
@@ -663,16 +753,28 @@
     private void grantCoarseLocationPermissionIfNeeded(String pkg, int userId) {
         // Don't clobber the user if permission set in current state explicitly
         if (!isPermissionUserUpdated(Manifest.permission.ACCESS_COARSE_LOCATION, pkg, userId)) {
-            mContext.getPackageManager().grantRuntimePermission(pkg,
-                    Manifest.permission.ACCESS_COARSE_LOCATION, new UserHandle(userId));
+            try {
+                mContext.getPackageManager().grantRuntimePermission(pkg,
+                        Manifest.permission.ACCESS_COARSE_LOCATION, new UserHandle(userId));
+            } catch (IllegalArgumentException e) {
+                // Package was removed during update.
+                Slog.w(TAG, "Could not grant coarse location permission, package " + pkg
+                    + " was removed.");
+            }
         }
     }
 
     private void revokeCoarseLocationPermissionIfNeeded(String pkg, int userId) {
         // Don't clobber the user if permission set in current state explicitly
         if (!isPermissionUserUpdated(Manifest.permission.ACCESS_COARSE_LOCATION, pkg, userId)) {
-            mContext.getPackageManager().revokeRuntimePermission(pkg,
-                    Manifest.permission.ACCESS_COARSE_LOCATION, new UserHandle(userId));
+            try {
+                mContext.getPackageManager().revokeRuntimePermission(pkg,
+                        Manifest.permission.ACCESS_COARSE_LOCATION, new UserHandle(userId));
+            } catch (IllegalArgumentException e) {
+                // Package was removed during update.
+                Slog.w(TAG, "Could not revoke coarse location permission, package " + pkg
+                    + " was removed.");
+            }
         }
     }
 
@@ -772,7 +874,10 @@
                     mPendingState.targetPackageName, mPendingState.userId,
                     mPendingState.callingPackage);
             mPendingState = null;
+        } else {
+            updateCurrentVrServiceLocked(false, null, 0, null);
         }
+
     }
 
     private void logStateLocked() {
@@ -822,13 +927,20 @@
     /*
      * Implementation of VrManagerInternal calls.  These are callable from system services.
      */
-
     private void setVrMode(boolean enabled, @NonNull ComponentName targetPackageName,
-            int userId, @NonNull ComponentName callingPackage, boolean immediate) {
+            int userId, @NonNull ComponentName callingPackage) {
 
         synchronized (mLock) {
+            VrState pending = new VrState(enabled, targetPackageName, userId, callingPackage);
+            if (!mVrModeAllowed) {
+                // We're not allowed to be in VR mode.  Make this state pending.  This will be
+                // applied the next time we are allowed to enter VR mode unless it is superseded by
+                // another call.
+                mPendingState = pending;
+                return;
+            }
 
-            if (!enabled && mCurrentVrService != null && !immediate) {
+            if (!enabled && mCurrentVrService != null) {
                 // If we're transitioning out of VR mode, delay briefly to avoid expensive HAL calls
                 // and service bind/unbind in case we are immediately switching to another VR app.
                 if (mPendingState == null) {
@@ -836,7 +948,7 @@
                             PENDING_STATE_DELAY_MS);
                 }
 
-                mPendingState = new VrState(enabled, targetPackageName, userId, callingPackage);
+                mPendingState = pending;
                 return;
             } else {
                 mHandler.removeMessages(MSG_PENDING_VR_STATE_CHANGE);
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 3720940..d7535bb 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -1563,6 +1563,19 @@
                 throw new IllegalStateException("Wallpaper not yet initialized for user " + userId);
             }
             final long ident = Binder.clearCallingIdentity();
+
+            // Live wallpapers can't be specified for keyguard.  If we're using a static
+            // system+lock image currently, migrate the system wallpaper to be a lock-only
+            // image as part of making a different live component active as the system
+            // wallpaper.
+            if (mImageWallpaper.equals(wallpaper.wallpaperComponent)) {
+                if (mLockWallpaperMap.get(userId) == null) {
+                    // We're using the static imagery and there is no lock-specific image in place,
+                    // therefore it's a shared system+lock image that we need to migrate.
+                    migrateSystemToLockWallpaperLocked(userId);
+                }
+            }
+
             try {
                 wallpaper.imageWallpaperPending = false;
                 if (bindWallpaperComponentLocked(name, false, true, wallpaper, null)) {
diff --git a/services/core/jni/com_android_server_SystemServer.cpp b/services/core/jni/com_android_server_SystemServer.cpp
index c7d6b95..e46490b 100644
--- a/services/core/jni/com_android_server_SystemServer.cpp
+++ b/services/core/jni/com_android_server_SystemServer.cpp
@@ -22,15 +22,22 @@
 #include <cutils/properties.h>
 #include <utils/Log.h>
 #include <utils/misc.h>
+#include <utils/AndroidThreads.h>
 
 namespace android {
 
+static int start_sensor_service(void* /*unused*/) {
+    SensorService::instantiate();
+    return 0;
+}
+
 static void android_server_SystemServer_startSensorService(JNIEnv* /* env */, jobject /* clazz */) {
     char propBuf[PROPERTY_VALUE_MAX];
     property_get("system_init.startsensorservice", propBuf, "1");
     if (strcmp(propBuf, "1") == 0) {
-        // Start the sensor service
-        SensorService::instantiate();
+        // Start the sensor service in a new thread
+        createThreadEtc(start_sensor_service, nullptr,
+                        "StartSensorThread", PRIORITY_FOREGROUND);
     }
 }
 
diff --git a/services/core/proto/ipconnectivity.proto b/services/core/proto/ipconnectivity.proto
index 29b318f..cf372bc 100644
--- a/services/core/proto/ipconnectivity.proto
+++ b/services/core/proto/ipconnectivity.proto
@@ -17,6 +17,22 @@
   optional int32 network_id = 1;
 };
 
+// Transport describes a physical technology used by a network. It is a subset
+// of the TRANSPORT_* constants defined in android.net.NetworkCapabilities.
+enum Transport {
+  UNKNOWN   = 0;
+  BLUETOOTH = 1;
+  CELLULAR  = 2;
+  ETHERNET  = 3;
+  WIFI      = 4;
+};
+
+// A pair of (key, value) integers for describing histogram-like statistics.
+message Pair {
+  optional int32 key = 1;
+  optional int32 value = 2;
+};
+
 // Logs changes in the system default network. Changes can be 1) acquiring a
 // default network with no previous default, 2) a switch of the system default
 // network to a new default network, 3) a loss of the system default network.
@@ -49,7 +65,8 @@
 // This message is associated to android.net.metrics.IpReachabilityEvent.
 message IpReachabilityEvent {
   // The interface name (wlan, rmnet, lo, ...) on which the probe was sent.
-  optional string if_name = 1;
+  // Deprecated since version 2, replaced by transport field.
+  optional string if_name = 1 [deprecated = true];
 
   // The event type code of the probe, represented by constants defined in
   // android.net.metrics.IpReachabilityEvent.
@@ -93,6 +110,7 @@
 
 // Logs DNS lookup latencies. Repeated fields must have the same length.
 // This message is associated to android.net.metrics.DnsEvent.
+// Deprecated since version 2.
 message DNSLookupBatch {
   // The id of the network on which the DNS lookups took place.
   optional NetworkId network_id = 1;
@@ -107,13 +125,62 @@
   repeated int32 latencies_ms = 4;
 };
 
+// Represents a collections of DNS lookup latencies and counters for a
+// particular combination of DNS query type and return code.
+// Since version 2.
+message DNSLatencies {
+  // The type of the DNS lookups, as defined in android.net.metrics.DnsEvent.
+  // Acts as a key for a set of DNS query results.
+  // Possible values are: 0 for getaddrinfo, 1 for gethostbyname.
+  optional int32 type = 1;
+
+  // The return value of the DNS resolver for the DNS lookups.
+  // Acts as a key for a set of DNS query results.
+  // Possible values are: 0 for success, or errno code for failures.
+  optional int32 return_code = 2;
+
+  // The number of query operations recorded.
+  optional int32 query_count = 3;
+
+  // The number of query operations returning A IPv4 records.
+  optional int32 a_count = 4;
+
+  // The number of query operations returning AAAA IPv6 records.
+  optional int32 aaaa_count = 5;
+
+  // The time it took for each DNS lookup to complete. The number of repeated
+  // values can be less than query_count in case of event rate-limiting.
+  repeated int32 latencies_ms = 6;
+};
+
+// Represents latency and errno statistics of the connect() system call.
+// Since version 2.
+message ConnectStatistics {
+  // The number of connect() operations recorded.
+  optional int32 connect_count = 1;
+
+  // The number of connect() operations with IPv6 socket address.
+  optional int32 ipv6_addr_count = 2;
+
+  // The time it took for each successful connect() operation to complete.
+  // The number of repeated values can be less than connect_count in case of
+  // event rate-limiting.
+  repeated int32 latencies_ms = 3;
+
+  // Counts of all error values returned by failed connect() operations.
+  // The Pair key field is the errno code. The Pair value field is the count
+  // for that errno code.
+  repeated Pair errnos_counters = 4;
+};
+
 // Represents a DHCP event on a single interface, which can be a DHCPClient
 // state transition or a response packet parsing error.
 // This message is associated to android.net.metrics.DhcpClientEvent and
 // android.net.metrics.DhcpErrorEvent.
 message DHCPEvent {
   // The interface name (wlan, rmnet, lo, ...) on which the event happened.
-  optional string if_name = 1;
+  // Deprecated since version 2, replaced by transport field.
+  optional string if_name = 1 [deprecated = true];
 
   oneof value {
     // The name of a state in the DhcpClient state machine, represented by
@@ -217,7 +284,8 @@
 // This message is associated to android.net.metrics.IpManagerEvent.
 message IpProvisioningEvent {
   // The interface name (wlan, rmnet, lo, ...) on which the probe was sent.
-  optional string if_name = 1;
+  // Deprecated since version 2, replaced by transport field.
+  optional string if_name = 1 [deprecated = true];
 
   // The code of the IP provisioning event, represented by constants defined in
   // android.net.metrics.IpManagerEvent.
@@ -228,11 +296,15 @@
 }
 
 // Represents one of the IP connectivity event defined in this file.
-// Next tag: 12
+// Next tag: 15
 message IpConnectivityEvent {
   // Time in ms when the event was recorded.
   optional int64 time_ms = 1;
 
+  // Physical transport of the network on which the event happened.
+  // Since version 2.
+  optional Transport transport = 12;
+
   // Event type.
   oneof event {
 
@@ -246,7 +318,14 @@
     NetworkEvent network_event = 4;
 
     // A batch of DNS lookups.
-    DNSLookupBatch dns_lookup_batch = 5;
+    // Deprecated in the nyc-mr2 release since version 2, and replaced by dns_latencies.
+    DNSLookupBatch dns_lookup_batch = 5 [deprecated = true];
+
+    // DNS lookup latency statistics.
+    DNSLatencies dns_latencies = 13;
+
+    // Connect latency and errno statistics.
+    ConnectStatistics connect_statistics = 14;
 
     // A DHCP client event or DHCP receive error.
     DHCPEvent dhcp_event = 6;
@@ -277,9 +356,9 @@
   optional int32 dropped_events = 2;
 
   // The version number of the metrics events being collected.
-  //  nyc-dev: not populated, implicitly 0
-  //  nyc-dr1: not populated, implicitly 1 (sailfish and marlin only)
-  //  nyc-mr1: not populated, implicitly 1
-  //  nyc-mr2: 2
+  //  nyc-dev: not populated, implicitly 0.
+  //  nyc-dr1: not populated, implicitly 1 (sailfish and marlin only).
+  //  nyc-mr1: not populated, implicitly 1.
+  //  nyc-mr2: 2.
   optional int32 version = 3;
 };
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 96331e8..46cc3a9 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -17,6 +17,7 @@
 package com.android.server.devicepolicy;
 
 import static android.Manifest.permission.MANAGE_CA_CERTIFICATES;
+import static android.app.admin.DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG;
 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
 import static android.app.admin.DevicePolicyManager.WIPE_EXTERNAL_STORAGE;
 import static android.app.admin.DevicePolicyManager.WIPE_RESET_PROTECTION_DATA;
@@ -50,6 +51,7 @@
 import android.app.admin.DevicePolicyManager;
 import android.app.admin.DevicePolicyManagerInternal;
 import android.app.admin.IDevicePolicyManager;
+import android.app.admin.NetworkEvent;
 import android.app.admin.SecurityLog;
 import android.app.admin.SecurityLog.SecurityEvent;
 import android.app.admin.SystemUpdatePolicy;
@@ -77,8 +79,10 @@
 import android.media.AudioManager;
 import android.media.IAudioService;
 import android.net.ConnectivityManager;
+import android.net.IIpConnectivityMetrics;
 import android.net.ProxyInfo;
 import android.net.Uri;
+import android.net.metrics.IpConnectivityLog;
 import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
 import android.os.AsyncTask;
@@ -215,6 +219,7 @@
 
     private static final int MONITORING_CERT_NOTIFICATION_ID = R.plurals.ssl_ca_cert_warning;
     private static final int PROFILE_WIPED_NOTIFICATION_ID = 1001;
+    private static final int NETWORK_LOGGING_NOTIFICATION_ID = 1002;
 
     private static final String ATTR_PERMISSION_PROVIDER = "permission-provider";
     private static final String ATTR_SETUP_COMPLETE = "setup-complete";
@@ -352,6 +357,7 @@
     boolean mIsWatch;
 
     private final SecurityLogMonitor mSecurityLogMonitor;
+    private NetworkLogger mNetworkLogger;
 
     private final AtomicBoolean mRemoteBugreportServiceIsActive = new AtomicBoolean();
     private final AtomicBoolean mRemoteBugreportSharingAccepted = new AtomicBoolean();
@@ -478,6 +484,19 @@
             final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE,
                     getSendingUserId());
 
+            /*
+             * Network logging would ideally be started in setDeviceOwnerSystemPropertyLocked(),
+             * however it's too early in the boot process to register with IIpConnectivityMetrics
+             * to listen for events.
+             */
+            if (Intent.ACTION_USER_STARTED.equals(action)
+                    && userHandle == mOwners.getDeviceOwnerUserId()) {
+                synchronized (DevicePolicyManagerService.this) {
+                    if (isNetworkLoggingEnabledInternalLocked()) {
+                        setNetworkLoggingActiveInternal(true);
+                    }
+                }
+            }
             if (Intent.ACTION_BOOT_COMPLETED.equals(action)
                     && userHandle == mOwners.getDeviceOwnerUserId()
                     && getDeviceOwnerRemoteBugreportUri() != null) {
@@ -549,6 +568,7 @@
         private static final String TAG_DISABLE_ACCOUNT_MANAGEMENT = "disable-account-management";
         private static final String TAG_REQUIRE_AUTO_TIME = "require_auto_time";
         private static final String TAG_FORCE_EPHEMERAL_USERS = "force_ephemeral_users";
+        private static final String TAG_IS_NETWORK_LOGGING_ENABLED = "is_network_logging_enabled";
         private static final String TAG_ACCOUNT_TYPE = "account-type";
         private static final String TAG_PERMITTED_ACCESSIBILITY_SERVICES
                 = "permitted-accessiblity-services";
@@ -587,6 +607,8 @@
         private static final String TAG_PARENT_ADMIN = "parent-admin";
         private static final String TAG_ORGANIZATION_COLOR = "organization-color";
         private static final String TAG_ORGANIZATION_NAME = "organization-name";
+        private static final String ATTR_LAST_NETWORK_LOGGING_NOTIFICATION = "last-notification";
+        private static final String ATTR_NUM_NETWORK_LOGGING_NOTIFICATIONS = "num-notifications";
 
         final DeviceAdminInfo info;
 
@@ -643,6 +665,12 @@
         boolean disableScreenCapture = false; // Can only be set by a device/profile owner.
         boolean requireAutoTime = false; // Can only be set by a device owner.
         boolean forceEphemeralUsers = false; // Can only be set by a device owner.
+        boolean isNetworkLoggingEnabled = false; // Can only be set by a device owner.
+
+        // one notification after enabling + 3 more after reboots
+        static final int DEF_MAXIMUM_NETWORK_LOGGING_NOTIFICATIONS_SHOWN = 4;
+        int numNetworkLoggingNotifications = 0;
+        long lastNetworkLoggingNotificationTimeMs = 0; // Time in milliseconds since epoch
 
         ActiveAdmin parentAdmin;
         final boolean isParent;
@@ -851,6 +879,15 @@
                 out.attribute(null, ATTR_VALUE, Boolean.toString(forceEphemeralUsers));
                 out.endTag(null, TAG_FORCE_EPHEMERAL_USERS);
             }
+            if (isNetworkLoggingEnabled) {
+                out.startTag(null, TAG_IS_NETWORK_LOGGING_ENABLED);
+                out.attribute(null, ATTR_VALUE, Boolean.toString(isNetworkLoggingEnabled));
+                out.attribute(null, ATTR_NUM_NETWORK_LOGGING_NOTIFICATIONS,
+                        Integer.toString(numNetworkLoggingNotifications));
+                out.attribute(null, ATTR_LAST_NETWORK_LOGGING_NOTIFICATION,
+                        Long.toString(lastNetworkLoggingNotificationTimeMs));
+                out.endTag(null, TAG_IS_NETWORK_LOGGING_ENABLED);
+            }
             if (disabledKeyguardFeatures != DEF_KEYGUARD_FEATURES_DISABLED) {
                 out.startTag(null, TAG_DISABLE_KEYGUARD_FEATURES);
                 out.attribute(null, ATTR_VALUE, Integer.toString(disabledKeyguardFeatures));
@@ -1037,6 +1074,13 @@
                 } else if (TAG_FORCE_EPHEMERAL_USERS.equals(tag)) {
                     forceEphemeralUsers = Boolean.parseBoolean(
                             parser.getAttributeValue(null, ATTR_VALUE));
+                } else if (TAG_IS_NETWORK_LOGGING_ENABLED.equals(tag)) {
+                    isNetworkLoggingEnabled = Boolean.parseBoolean(
+                            parser.getAttributeValue(null, ATTR_VALUE));
+                    lastNetworkLoggingNotificationTimeMs = Long.parseLong(
+                            parser.getAttributeValue(null, ATTR_LAST_NETWORK_LOGGING_NOTIFICATION));
+                    numNetworkLoggingNotifications = Integer.parseInt(
+                            parser.getAttributeValue(null, ATTR_NUM_NETWORK_LOGGING_NOTIFICATIONS));
                 } else if (TAG_DISABLE_KEYGUARD_FEATURES.equals(tag)) {
                     disabledKeyguardFeatures = Integer.parseInt(
                             parser.getAttributeValue(null, ATTR_VALUE));
@@ -1277,6 +1321,8 @@
                     pw.println(requireAutoTime);
             pw.print(prefix); pw.print("forceEphemeralUsers=");
                     pw.println(forceEphemeralUsers);
+            pw.print(prefix); pw.print("isNetworkLoggingEnabled=");
+                    pw.println(isNetworkLoggingEnabled);
             pw.print(prefix); pw.print("disabledKeyguardFeatures=");
                     pw.println(disabledKeyguardFeatures);
             pw.print(prefix); pw.print("crossProfileWidgetProviders=");
@@ -1403,6 +1449,11 @@
             return mContext.getSystemService(NotificationManager.class);
         }
 
+        IIpConnectivityMetrics getIIpConnectivityMetrics() {
+            return (IIpConnectivityMetrics) IIpConnectivityMetrics.Stub.asInterface(
+                ServiceManager.getService(IpConnectivityLog.SERVICE_NAME));
+        }
+
         PowerManagerInternal getPowerManagerInternal() {
             return LocalServices.getService(PowerManagerInternal.class);
         }
@@ -6497,6 +6548,16 @@
         }
     }
 
+    private void enforceDeviceOwnerOrManageUsers() {
+        synchronized (this) {
+            if (getActiveAdminWithPolicyForUidLocked(null, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER,
+                    mInjector.binderGetCallingUid()) != null) {
+                return;
+            }
+        }
+        enforceManageUsers();
+    }
+
     private void ensureCallerPackage(@Nullable String packageName) {
         if (packageName == null) {
             Preconditions.checkState(isCallerWithSystemUid(),
@@ -9037,12 +9098,33 @@
     }
 
     private synchronized void disableDeviceOwnerManagedSingleUserFeaturesIfNeeded() {
-        if (!isDeviceOwnerManagedSingleUserDevice()) {
+        final boolean isSingleUserManagedDevice = isDeviceOwnerManagedSingleUserDevice();
+
+        // disable security logging if needed
+        if (!isSingleUserManagedDevice) {
             mInjector.securityLogSetLoggingEnabledProperty(false);
-            Slog.w(LOG_TAG, "Security logging turned off as it's no longer a single user device.");
+            Slog.w(LOG_TAG, "Security logging turned off as it's no longer a single user managed"
+                    + " device.");
+        }
+
+        // disable backup service if needed
+        // note: when clearing DO, the backup service shouldn't be disabled if it was enabled by
+        // the device owner
+        if (mOwners.hasDeviceOwner() && !isSingleUserManagedDevice) {
+            setBackupServiceEnabledInternal(false);
+            Slog.w(LOG_TAG, "Backup is off as it's a managed device that has more that one user.");
+        }
+
+        // disable network logging if needed
+        if (!isSingleUserManagedDevice) {
+            setNetworkLoggingActiveInternal(false);
+            Slog.w(LOG_TAG, "Network logging turned off as it's no longer a single user managed"
+                    + " device.");
+            // if there still is a device owner, disable logging policy, otherwise the admin
+            // has been nuked
             if (mOwners.hasDeviceOwner()) {
-                setBackupServiceEnabledInternal(false);
-                Slog.w(LOG_TAG, "Backup is off as it's a managed device that has more that one user.");
+                getDeviceOwnerAdminLocked().isNetworkLoggingEnabled = false;
+                saveSettingsLocked(mOwners.getDeviceOwnerUserId());
             }
         }
     }
@@ -9386,4 +9468,126 @@
             }
         }
     }
+
+    @Override
+    public synchronized void setNetworkLoggingEnabled(ComponentName admin, boolean enabled) {
+        if (!mHasFeature) {
+            return;
+        }
+        Preconditions.checkNotNull(admin);
+        ensureDeviceOwnerManagingSingleUser(admin);
+
+        if (enabled == isNetworkLoggingEnabledInternalLocked()) {
+            // already in the requested state
+            return;
+        }
+        ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
+        deviceOwner.isNetworkLoggingEnabled = enabled;
+        if (!enabled) {
+            deviceOwner.numNetworkLoggingNotifications = 0;
+            deviceOwner.lastNetworkLoggingNotificationTimeMs = 0;
+        }
+        saveSettingsLocked(mInjector.userHandleGetCallingUserId());
+
+        setNetworkLoggingActiveInternal(enabled);
+    }
+
+    private synchronized void setNetworkLoggingActiveInternal(boolean active) {
+        final long callingIdentity = mInjector.binderClearCallingIdentity();
+        try {
+            if (active) {
+                mNetworkLogger = new NetworkLogger(this, mInjector.getPackageManagerInternal());
+                if (!mNetworkLogger.startNetworkLogging()) {
+                    mNetworkLogger = null;
+                    Slog.wtf(LOG_TAG, "Network logging could not be started due to the logging"
+                            + " service not being available yet.");
+                }
+                sendNetworkLoggingNotificationLocked();
+            } else {
+                if (mNetworkLogger != null && !mNetworkLogger.stopNetworkLogging()) {
+                    mNetworkLogger = null;
+                    Slog.wtf(LOG_TAG, "Network logging could not be stopped due to the logging"
+                            + " service not being available yet.");
+                }
+                mNetworkLogger = null;
+            }
+        } finally {
+            mInjector.binderRestoreCallingIdentity(callingIdentity);
+        }
+    }
+
+    @Override
+    public boolean isNetworkLoggingEnabled(ComponentName admin) {
+        if (!mHasFeature) {
+            return false;
+        }
+        synchronized (this) {
+            enforceDeviceOwnerOrManageUsers();
+            return isNetworkLoggingEnabledInternalLocked();
+        }
+    }
+
+    private boolean isNetworkLoggingEnabledInternalLocked() {
+        ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
+        return (deviceOwner != null) && deviceOwner.isNetworkLoggingEnabled;
+    }
+
+    /*
+     * A maximum of 1200 events are returned, and the total marshalled size is in the order of
+     * 100kB, so returning a List instead of ParceledListSlice is acceptable.
+     * Ideally this would be done with ParceledList, however it only supports homogeneous types.
+     *
+     * @see NetworkLoggingHandler#MAX_EVENTS_PER_BATCH
+     */
+    @Override
+    public synchronized List<NetworkEvent> retrieveNetworkLogs(ComponentName admin,
+            long batchToken) {
+        if (!mHasFeature) {
+            return null;
+        }
+        Preconditions.checkNotNull(admin);
+        ensureDeviceOwnerManagingSingleUser(admin);
+
+        if (mNetworkLogger == null) {
+            return null;
+        }
+        return isNetworkLoggingEnabledInternalLocked()
+                ? mNetworkLogger.retrieveLogs(batchToken)
+                : null;
+    }
+
+    private void sendNetworkLoggingNotificationLocked() {
+        final ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
+        if (deviceOwner == null || !deviceOwner.isNetworkLoggingEnabled) {
+            return;
+        }
+        if (deviceOwner.numNetworkLoggingNotifications >=
+                ActiveAdmin.DEF_MAXIMUM_NETWORK_LOGGING_NOTIFICATIONS_SHOWN) {
+            return;
+        }
+        final long now = System.currentTimeMillis();
+        if (now - deviceOwner.lastNetworkLoggingNotificationTimeMs < MS_PER_DAY) {
+            return;
+        }
+        deviceOwner.numNetworkLoggingNotifications++;
+        if (deviceOwner.numNetworkLoggingNotifications
+                >= ActiveAdmin.DEF_MAXIMUM_NETWORK_LOGGING_NOTIFICATIONS_SHOWN) {
+            deviceOwner.lastNetworkLoggingNotificationTimeMs = 0;
+        } else {
+            deviceOwner.lastNetworkLoggingNotificationTimeMs = now;
+        }
+        final Intent intent = new Intent(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG);
+        intent.setPackage("com.android.systemui");
+        final PendingIntent pendingIntent = PendingIntent.getBroadcastAsUser(mContext, 0, intent, 0,
+                UserHandle.CURRENT);
+        Notification notification = new Notification.Builder(mContext)
+                .setSmallIcon(R.drawable.ic_qs_network_logging)
+                .setContentTitle(mContext.getString(R.string.network_logging_notification_title))
+                .setContentText(mContext.getString(R.string.network_logging_notification_text))
+                .setShowWhen(true)
+                .setContentIntent(pendingIntent)
+                .build();
+        mInjector.getNotificationManager().notify(NETWORK_LOGGING_NOTIFICATION_ID, notification);
+        saveSettingsLocked(mOwners.getDeviceOwnerUserId());
+    }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java
new file mode 100644
index 0000000..49ae2bc3
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java
@@ -0,0 +1,154 @@
+/*
+ * 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.devicepolicy;
+
+import android.app.admin.ConnectEvent;
+import android.app.admin.DnsEvent;
+import android.app.admin.NetworkEvent;
+import android.content.pm.PackageManagerInternal;
+import android.net.IIpConnectivityMetrics;
+import android.net.INetdEventCallback;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.server.ServiceThread;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A class for managing network logging.
+ * This class is not thread-safe, callers should synchronize access.
+ */
+final class NetworkLogger {
+
+    private static final String TAG = NetworkLogger.class.getSimpleName();
+
+    private final DevicePolicyManagerService mDpm;
+    private final PackageManagerInternal mPm;
+    private final AtomicBoolean mIsLoggingEnabled = new AtomicBoolean(false);
+
+    private IIpConnectivityMetrics mIpConnectivityMetrics;
+    private ServiceThread mHandlerThread;
+    private NetworkLoggingHandler mNetworkLoggingHandler;
+
+    private final INetdEventCallback mNetdEventCallback = new INetdEventCallback.Stub() {
+        @Override
+        public void onDnsEvent(String hostname, String[] ipAddresses, int ipAddressesCount,
+                long timestamp, int uid) {
+            if (!mIsLoggingEnabled.get()) {
+                return;
+            }
+            DnsEvent dnsEvent = new DnsEvent(hostname, ipAddresses, ipAddressesCount,
+                    mPm.getNameForUid(uid), timestamp);
+            sendNetworkEvent(dnsEvent);
+        }
+
+        @Override
+        public void onConnectEvent(String ipAddr, int port, long timestamp, int uid) {
+            if (!mIsLoggingEnabled.get()) {
+                return;
+            }
+            ConnectEvent connectEvent = new ConnectEvent(ipAddr, port, mPm.getNameForUid(uid),
+                    timestamp);
+            sendNetworkEvent(connectEvent);
+        }
+
+        private void sendNetworkEvent(NetworkEvent event) {
+            Message msg = mNetworkLoggingHandler.obtainMessage(
+                    NetworkLoggingHandler.LOG_NETWORK_EVENT_MSG);
+            Bundle bundle = new Bundle();
+            bundle.putParcelable(NetworkLoggingHandler.NETWORK_EVENT_KEY, event);
+            msg.setData(bundle);
+            mNetworkLoggingHandler.sendMessage(msg);
+        }
+    };
+
+    NetworkLogger(DevicePolicyManagerService dpm, PackageManagerInternal pm) {
+        mDpm = dpm;
+        mPm = pm;
+    }
+
+    private boolean checkIpConnectivityMetricsService() {
+        if (mIpConnectivityMetrics != null) {
+            return true;
+        }
+        final IIpConnectivityMetrics service = mDpm.mInjector.getIIpConnectivityMetrics();
+        if (service == null) {
+            return false;
+        }
+        mIpConnectivityMetrics = service;
+        return true;
+    }
+
+    boolean startNetworkLogging() {
+        Log.d(TAG, "Starting network logging.");
+        if (!checkIpConnectivityMetricsService()) {
+            // the IIpConnectivityMetrics service should have been present at this point
+            Slog.wtf(TAG, "Failed to register callback with IIpConnectivityMetrics.");
+            return false;
+        }
+        try {
+           if (mIpConnectivityMetrics.registerNetdEventCallback(mNetdEventCallback)) {
+                mHandlerThread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND,
+                        /* allowIo */ false);
+                mHandlerThread.start();
+                mNetworkLoggingHandler = new NetworkLoggingHandler(mHandlerThread.getLooper(),
+                        mDpm);
+                mNetworkLoggingHandler.scheduleBatchFinalization(
+                        NetworkLoggingHandler.BATCH_FINALIZATION_TIMEOUT_MS);
+                mIsLoggingEnabled.set(true);
+                return true;
+            } else {
+                return false;
+            }
+        } catch (RemoteException re) {
+            Slog.wtf(TAG, "Failed to make remote calls to register the callback", re);
+            return false;
+        }
+    }
+
+    boolean stopNetworkLogging() {
+        Log.d(TAG, "Stopping network logging");
+        // stop the logging regardless of whether we fail to unregister listener
+        mIsLoggingEnabled.set(false);
+        try {
+            if (!checkIpConnectivityMetricsService()) {
+                // the IIpConnectivityMetrics service should have been present at this point
+                Slog.wtf(TAG, "Failed to unregister callback with IIpConnectivityMetrics.");
+                // logging is forcefully disabled even if unregistering fails
+                return true;
+            }
+            return mIpConnectivityMetrics.unregisterNetdEventCallback();
+        } catch (RemoteException re) {
+            Slog.wtf(TAG, "Failed to make remote calls to unregister the callback", re);
+        } finally {
+            mHandlerThread.quitSafely();
+            return true;
+        }
+    }
+
+    List<NetworkEvent> retrieveLogs(long batchToken) {
+        return mNetworkLoggingHandler.retrieveFullLogBatch(batchToken);
+    }
+}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLoggingHandler.java b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLoggingHandler.java
new file mode 100644
index 0000000..957d4c5
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLoggingHandler.java
@@ -0,0 +1,124 @@
+/*
+ * 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.devicepolicy;
+
+import android.app.admin.DeviceAdminReceiver;
+import android.app.admin.ConnectEvent;
+import android.app.admin.DnsEvent;
+import android.app.admin.NetworkEvent;
+import android.net.IIpConnectivityMetrics;
+import android.net.INetdEventCallback;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A Handler class for managing network logging on a background thread.
+ */
+final class NetworkLoggingHandler extends Handler {
+
+    private static final String TAG = NetworkLoggingHandler.class.getSimpleName();
+
+    static final String NETWORK_EVENT_KEY = "network_event";
+
+    // est. ~128kB of memory usage per full batch TODO(mkarpinski): fine tune based on testing data
+    // If this value changes, update DevicePolicyManager#retrieveNetworkLogs() javadoc
+    private static final int MAX_EVENTS_PER_BATCH = 1200;
+    static final long BATCH_FINALIZATION_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(90);
+
+    static final int LOG_NETWORK_EVENT_MSG = 1;
+    static final int FINALIZE_BATCH_MSG = 2;
+
+    private final DevicePolicyManagerService mDpm;
+
+    // threadsafe as it's Handler's thread confined
+    @GuardedBy("this")
+    private ArrayList<NetworkEvent> mNetworkEvents = new ArrayList<NetworkEvent>();
+
+    @GuardedBy("this")
+    private ArrayList<NetworkEvent> mFullBatch;
+
+    // each full batch is represented by its token, which the DPC has to provide back to revieve it
+    @GuardedBy("this")
+    private long mCurrentFullBatchToken;
+
+    NetworkLoggingHandler(Looper looper, DevicePolicyManagerService dpm) {
+        super(looper);
+        mDpm = dpm;
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+            case LOG_NETWORK_EVENT_MSG: {
+                NetworkEvent networkEvent = msg.getData().getParcelable(NETWORK_EVENT_KEY);
+                if (networkEvent != null) {
+                    mNetworkEvents.add(networkEvent);
+                    if (mNetworkEvents.size() >= MAX_EVENTS_PER_BATCH) {
+                        finalizeBatchAndNotifyDeviceOwner();
+                    }
+                }
+                break;
+            }
+            case FINALIZE_BATCH_MSG: {
+                finalizeBatchAndNotifyDeviceOwner();
+                break;
+            }
+        }
+    }
+
+    void scheduleBatchFinalization(long delay) {
+        removeMessages(FINALIZE_BATCH_MSG);
+        sendMessageDelayed(obtainMessage(FINALIZE_BATCH_MSG), delay);
+    }
+
+    private synchronized void finalizeBatchAndNotifyDeviceOwner() {
+        mFullBatch = mNetworkEvents;
+        // start a new batch from scratch
+        mNetworkEvents = new ArrayList<NetworkEvent>();
+        scheduleBatchFinalization(BATCH_FINALIZATION_TIMEOUT_MS);
+        // notify DO that there's a new non-empty batch waiting
+        if (mFullBatch.size() > 0) {
+            mCurrentFullBatchToken++;
+            Bundle extras = new Bundle();
+            extras.putLong(DeviceAdminReceiver.EXTRA_NETWORK_LOGS_TOKEN, mCurrentFullBatchToken);
+            extras.putInt(DeviceAdminReceiver.EXTRA_NETWORK_LOGS_COUNT, mFullBatch.size());
+            Log.d(TAG, "Sending network logging batch broadcast to device owner, batchToken: "
+                    + mCurrentFullBatchToken);
+            mDpm.sendDeviceOwnerCommand(DeviceAdminReceiver.ACTION_NETWORK_LOGS_AVAILABLE, extras);
+        } else {
+            mFullBatch = null;
+        }
+    }
+
+    synchronized List<NetworkEvent> retrieveFullLogBatch(long batchToken) {
+        if (batchToken != mCurrentFullBatchToken) {
+            return null;
+        }
+        return mFullBatch;
+    }
+}
+
diff --git a/services/net/java/android/net/dhcp/DhcpPacket.java b/services/net/java/android/net/dhcp/DhcpPacket.java
index ef4bc02..060ded6 100644
--- a/services/net/java/android/net/dhcp/DhcpPacket.java
+++ b/services/net/java/android/net/dhcp/DhcpPacket.java
@@ -25,8 +25,10 @@
  * Defines basic data and operations needed to build and use packets for the
  * DHCP protocol.  Subclasses create the specific packets used at each
  * stage of the negotiation.
+ *
+ * @hide
  */
-abstract class DhcpPacket {
+public abstract class DhcpPacket {
     protected static final String TAG = "DhcpPacket";
 
     // dhcpcd has a minimum lease of 20 seconds, but DhcpStateMachine would refuse to wake up the
diff --git a/services/net/java/android/net/ip/ConnectivityPacketTracker.java b/services/net/java/android/net/ip/ConnectivityPacketTracker.java
new file mode 100644
index 0000000..884a8a7
--- /dev/null
+++ b/services/net/java/android/net/ip/ConnectivityPacketTracker.java
@@ -0,0 +1,148 @@
+/*
+ * 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.net.ip;
+
+import static android.system.OsConstants.*;
+
+import android.net.NetworkUtils;
+import android.net.util.BlockingSocketReader;
+import android.net.util.ConnectivityPacketSummary;
+import android.os.Handler;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.PacketSocketAddress;
+import android.util.Log;
+import android.util.LocalLog;
+
+import libcore.io.IoBridge;
+import libcore.util.HexEncoding;
+
+import java.io.FileDescriptor;
+import java.io.InterruptedIOException;
+import java.io.IOException;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+
+
+/**
+ * Critical connectivity packet tracking daemon.
+ *
+ * Tracks ARP, DHCPv4, and IPv6 RS/RA/NS/NA packets.
+ *
+ * This class's constructor, start() and stop() methods must only be called
+ * from the same thread on which the passed in |log| is accessed.
+ *
+ * Log lines include a hexdump of the packet, which can be decoded via:
+ *
+ *     echo -n H3XSTR1NG | sed -e 's/\([0-9A-F][0-9A-F]\)/\1 /g' -e 's/^/000000 /'
+ *                       | text2pcap - -
+ *                       | tcpdump -n -vv -e -r -
+ *
+ * @hide
+ */
+public class ConnectivityPacketTracker {
+    private static final String TAG = ConnectivityPacketTracker.class.getSimpleName();
+    private static final boolean DBG = false;
+    private static final String MARK_START = "--- START ---";
+    private static final String MARK_STOP = "--- STOP ---";
+
+    private final String mTag;
+    private final Handler mHandler;
+    private final LocalLog mLog;
+    private final BlockingSocketReader mPacketListener;
+
+    public ConnectivityPacketTracker(NetworkInterface netif, LocalLog log) {
+        final String ifname;
+        final int ifindex;
+        final byte[] hwaddr;
+        final int mtu;
+
+        try {
+            ifname = netif.getName();
+            ifindex = netif.getIndex();
+            hwaddr = netif.getHardwareAddress();
+            mtu = netif.getMTU();
+        } catch (NullPointerException|SocketException e) {
+            throw new IllegalArgumentException("bad network interface", e);
+        }
+
+        mTag = TAG + "." + ifname;
+        mHandler = new Handler();
+        mLog = log;
+        mPacketListener = new PacketListener(ifindex, hwaddr, mtu);
+    }
+
+    public void start() {
+        mLog.log(MARK_START);
+        mPacketListener.start();
+    }
+
+    public void stop() {
+        mPacketListener.stop();
+        mLog.log(MARK_STOP);
+    }
+
+    private final class PacketListener extends BlockingSocketReader {
+        private final int mIfIndex;
+        private final byte mHwAddr[];
+
+        PacketListener(int ifindex, byte[] hwaddr, int mtu) {
+            super(mtu);
+            mIfIndex = ifindex;
+            mHwAddr = hwaddr;
+        }
+
+        @Override
+        protected FileDescriptor createSocket() {
+            FileDescriptor s = null;
+            try {
+                // TODO: Evaluate switching to SOCK_DGRAM and changing the
+                // BlockingSocketReader's read() to recvfrom(), so that this
+                // might work on non-ethernet-like links (via SLL).
+                s = Os.socket(AF_PACKET, SOCK_RAW, 0);
+                NetworkUtils.attachControlPacketFilter(s, ARPHRD_ETHER);
+                Os.bind(s, new PacketSocketAddress((short) ETH_P_ALL, mIfIndex));
+            } catch (ErrnoException | IOException e) {
+                logError("Failed to create packet tracking socket: ", e);
+                closeSocket(s);
+                return null;
+            }
+            return s;
+        }
+
+        @Override
+        protected void handlePacket(byte[] recvbuf, int length) {
+            final String summary = ConnectivityPacketSummary.summarize(
+                    mHwAddr, recvbuf, length);
+            if (summary == null) return;
+
+            if (DBG) Log.d(mTag, summary);
+            addLogEntry(summary +
+                        "\n[" + new String(HexEncoding.encode(recvbuf, 0, length)) + "]");
+        }
+
+        @Override
+        protected void logError(String msg, Exception e) {
+            Log.e(mTag, msg, e);
+            addLogEntry(msg + e);
+        }
+
+        private void addLogEntry(String entry) {
+            mHandler.post(() -> mLog.log(entry));
+        }
+    }
+}
diff --git a/services/net/java/android/net/ip/IpManager.java b/services/net/java/android/net/ip/IpManager.java
index 39f14e5..87018ec 100644
--- a/services/net/java/android/net/ip/IpManager.java
+++ b/services/net/java/android/net/ip/IpManager.java
@@ -374,6 +374,7 @@
     private static final int EVENT_DHCPACTION_TIMEOUT = 10;
 
     private static final int MAX_LOG_RECORDS = 500;
+    private static final int MAX_PACKET_RECORDS = 100;
 
     private static final boolean NO_CALLBACKS = false;
     private static final boolean SEND_CALLBACKS = true;
@@ -399,6 +400,7 @@
     private final WakeupMessage mDhcpActionTimeoutAlarm;
     private final AvoidBadWifiTracker mAvoidBadWifiTracker;
     private final LocalLog mLocalLog;
+    private final LocalLog mConnectivityPacketLog;
     private final MessageHandlingLogger mMsgStateLogger;
     private final IpConnectivityLog mMetricsLog = new IpConnectivityLog();
 
@@ -438,6 +440,10 @@
         mCallback = new LoggingCallbackWrapper(callback);
         mNwService = nwService;
 
+        mLocalLog = new LocalLog(MAX_LOG_RECORDS);
+        mConnectivityPacketLog = new LocalLog(MAX_PACKET_RECORDS);
+        mMsgStateLogger = new MessageHandlingLogger();
+
         mNetlinkTracker = new NetlinkTracker(
                 mInterfaceName,
                 new NetlinkTracker.Callback() {
@@ -451,48 +457,79 @@
                 super.interfaceAdded(iface);
                 if (mClatInterfaceName.equals(iface)) {
                     mCallback.setNeighborDiscoveryOffload(false);
+                } else if (!mInterfaceName.equals(iface)) {
+                    return;
                 }
+
+                final String msg = "interfaceAdded(" + iface +")";
+                logMsg(msg);
             }
 
             @Override
             public void interfaceRemoved(String iface) {
                 super.interfaceRemoved(iface);
+                // TODO: Also observe mInterfaceName going down and take some
+                // kind of appropriate action.
                 if (mClatInterfaceName.equals(iface)) {
                     // TODO: consider sending a message to the IpManager main
                     // StateMachine thread, in case "NDO enabled" state becomes
                     // tied to more things that 464xlat operation.
                     mCallback.setNeighborDiscoveryOffload(true);
+                } else if (!mInterfaceName.equals(iface)) {
+                    return;
                 }
+
+                final String msg = "interfaceRemoved(" + iface +")";
+                logMsg(msg);
+            }
+
+            private void logMsg(String msg) {
+                Log.d(mTag, msg);
+                getHandler().post(() -> { mLocalLog.log("OBSERVED " + msg); });
             }
         };
 
-        try {
-            mNwService.registerObserver(mNetlinkTracker);
-        } catch (RemoteException e) {
-            Log.e(mTag, "Couldn't register NetlinkTracker: " + e.toString());
-        }
+        mLinkProperties = new LinkProperties();
+        mLinkProperties.setInterfaceName(mInterfaceName);
 
-        mAvoidBadWifiTracker = new AvoidBadWifiTracker(mContext, getHandler());
-
-        resetLinkProperties();
+        mAvoidBadWifiTracker = new AvoidBadWifiTracker(mContext, getHandler(),
+                () -> { mLocalLog.log("OBSERVED AvoidBadWifi changed"); });
 
         mProvisioningTimeoutAlarm = new WakeupMessage(mContext, getHandler(),
                 mTag + ".EVENT_PROVISIONING_TIMEOUT", EVENT_PROVISIONING_TIMEOUT);
         mDhcpActionTimeoutAlarm = new WakeupMessage(mContext, getHandler(),
                 mTag + ".EVENT_DHCPACTION_TIMEOUT", EVENT_DHCPACTION_TIMEOUT);
 
-        // Super simple StateMachine.
+        // Anything the StateMachine may access must have been instantiated
+        // before this point.
+        configureAndStartStateMachine();
+
+        // Anything that may send messages to the StateMachine must only be
+        // configured to do so after the StateMachine has started (above).
+        startStateMachineUpdaters();
+    }
+
+    private void configureAndStartStateMachine() {
         addState(mStoppedState);
         addState(mStartedState);
             addState(mRunningState, mStartedState);
         addState(mStoppingState);
 
         setInitialState(mStoppedState);
-        mLocalLog = new LocalLog(MAX_LOG_RECORDS);
-        mMsgStateLogger = new MessageHandlingLogger();
+
         super.start();
     }
 
+    private void startStateMachineUpdaters() {
+        try {
+            mNwService.registerObserver(mNetlinkTracker);
+        } catch (RemoteException e) {
+            Log.e(mTag, "Couldn't register NetlinkTracker: " + e.toString());
+        }
+
+        mAvoidBadWifiTracker.start();
+    }
+
     @Override
     protected void onQuitting() {
         mCallback.onQuit();
@@ -501,6 +538,7 @@
     // Shut down this IpManager instance altogether.
     public void shutdown() {
         stop();
+        mAvoidBadWifiTracker.shutdown();
         quit();
     }
 
@@ -574,7 +612,7 @@
         }
 
         IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
-        pw.println("APF dump:");
+        pw.println(mTag + " APF dump:");
         pw.increaseIndent();
         // Thread-unsafe access to mApfFilter but just used for debugging.
         ApfFilter apfFilter = mApfFilter;
@@ -590,6 +628,12 @@
         pw.increaseIndent();
         mLocalLog.readOnlyLocalLog().dump(fd, pw, args);
         pw.decreaseIndent();
+
+        pw.println();
+        pw.println(mTag + " connectivity packet log:");
+        pw.increaseIndent();
+        mConnectivityPacketLog.readOnlyLocalLog().dump(fd, pw, args);
+        pw.decreaseIndent();
     }
 
 
@@ -1185,6 +1229,7 @@
     }
 
     class RunningState extends State {
+        private ConnectivityPacketTracker mPacketTracker;
         private boolean mDhcpActionInFlight;
 
         @Override
@@ -1197,6 +1242,9 @@
                 mCallback.setFallbackMulticastFilter(mMulticastFiltering);
             }
 
+            mPacketTracker = createPacketTracker();
+            if (mPacketTracker != null) mPacketTracker.start();
+
             if (mConfiguration.mEnableIPv6 && !startIPv6()) {
                 doImmediateProvisioningFailure(IpManagerEvent.ERROR_STARTING_IPV6);
                 transitionTo(mStoppingState);
@@ -1231,6 +1279,11 @@
                 mDhcpClient.doQuit();
             }
 
+            if (mPacketTracker != null) {
+                mPacketTracker.stop();
+                mPacketTracker = null;
+            }
+
             if (mApfFilter != null) {
                 mApfFilter.shutdown();
                 mApfFilter = null;
@@ -1239,6 +1292,14 @@
             resetLinkProperties();
         }
 
+        private ConnectivityPacketTracker createPacketTracker() {
+            try {
+                return new ConnectivityPacketTracker(mNetworkInterface, mConnectivityPacketLog);
+            } catch (IllegalArgumentException e) {
+                return null;
+            }
+        }
+
         private void ensureDhcpAction() {
             if (!mDhcpActionInFlight) {
                 mCallback.onPreDhcpAction();
diff --git a/services/net/java/android/net/util/AvoidBadWifiTracker.java b/services/net/java/android/net/util/AvoidBadWifiTracker.java
index c14e811..2abaeb1 100644
--- a/services/net/java/android/net/util/AvoidBadWifiTracker.java
+++ b/services/net/java/android/net/util/AvoidBadWifiTracker.java
@@ -57,7 +57,11 @@
     private final Context mContext;
     private final Handler mHandler;
     private final Runnable mReevaluateRunnable;
+    private final Uri mUri;
+    private final ContentResolver mResolver;
     private final SettingObserver mSettingObserver;
+    private final BroadcastReceiver mBroadcastReceiver;
+
     private volatile boolean mAvoidBadWifi = true;
 
     public AvoidBadWifiTracker(Context ctx, Handler handler) {
@@ -68,19 +72,36 @@
         mContext = ctx;
         mHandler = handler;
         mReevaluateRunnable = () -> { if (update() && cb != null) cb.run(); };
+        mUri = Settings.Global.getUriFor(NETWORK_AVOID_BAD_WIFI);
+        mResolver = mContext.getContentResolver();
         mSettingObserver = new SettingObserver();
-
-        final IntentFilter intentFilter = new IntentFilter();
-        intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
-        mContext.registerReceiverAsUser(new BroadcastReceiver() {
+        mBroadcastReceiver = new BroadcastReceiver() {
+            @Override
             public void onReceive(Context context, Intent intent) {
                 reevaluate();
             }
-        }, UserHandle.ALL, intentFilter, null, null);
+        };
 
         update();
     }
 
+    public void start() {
+        mResolver.registerContentObserver(mUri, false, mSettingObserver);
+
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+        mContext.registerReceiverAsUser(
+                mBroadcastReceiver, UserHandle.ALL, intentFilter, null, null);
+
+        reevaluate();
+    }
+
+    public void shutdown() {
+        mResolver.unregisterContentObserver(mSettingObserver);
+
+        mContext.unregisterReceiver(mBroadcastReceiver);
+    }
+
     public boolean currentValue() {
         return mAvoidBadWifi;
     }
@@ -100,8 +121,7 @@
     }
 
     public String getSettingsValue() {
-        final ContentResolver resolver = mContext.getContentResolver();
-        return Settings.Global.getString(resolver, NETWORK_AVOID_BAD_WIFI);
+        return Settings.Global.getString(mResolver, NETWORK_AVOID_BAD_WIFI);
     }
 
     @VisibleForTesting
@@ -117,12 +137,8 @@
     }
 
     private class SettingObserver extends ContentObserver {
-        private final Uri mUri = Settings.Global.getUriFor(NETWORK_AVOID_BAD_WIFI);
-
         public SettingObserver() {
             super(null);
-            final ContentResolver resolver = mContext.getContentResolver();
-            resolver.registerContentObserver(mUri, false, this);
         }
 
         @Override
diff --git a/services/net/java/android/net/util/BlockingSocketReader.java b/services/net/java/android/net/util/BlockingSocketReader.java
new file mode 100644
index 0000000..12fa1e5
--- /dev/null
+++ b/services/net/java/android/net/util/BlockingSocketReader.java
@@ -0,0 +1,155 @@
+/*
+ * 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.net.util;
+
+import android.annotation.Nullable;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+
+import libcore.io.IoBridge;
+
+import java.io.FileDescriptor;
+import java.io.InterruptedIOException;
+import java.io.IOException;
+
+
+/**
+ * A thread that reads from a socket and passes the received packets to a
+ * subclass's handlePacket() method.  The packet receive buffer is recycled
+ * on every read call, so subclasses should make any copies they would like
+ * inside their handlePacket() implementation.
+ *
+ * All public methods may be called from any thread.
+ *
+ * @hide
+ */
+public abstract class BlockingSocketReader {
+    public static final int DEFAULT_RECV_BUF_SIZE = 2 * 1024;
+
+    private final byte[] mPacket;
+    private final Thread mThread;
+    private volatile FileDescriptor mSocket;
+    private volatile boolean mRunning;
+    private volatile long mPacketsReceived;
+
+    // Make it slightly easier for subclasses to properly close a socket
+    // without having to know this incantation.
+    public static final void closeSocket(@Nullable FileDescriptor fd) {
+        try {
+            IoBridge.closeAndSignalBlockedThreads(fd);
+        } catch (IOException ignored) {}
+    }
+
+    protected BlockingSocketReader() {
+        this(DEFAULT_RECV_BUF_SIZE);
+    }
+
+    protected BlockingSocketReader(int recvbufsize) {
+        if (recvbufsize < DEFAULT_RECV_BUF_SIZE) {
+            recvbufsize = DEFAULT_RECV_BUF_SIZE;
+        }
+        mPacket = new byte[recvbufsize];
+        mThread = new Thread(() -> { mainLoop(); });
+    }
+
+    public final boolean start() {
+        if (mSocket != null) return false;
+
+        try {
+            mSocket = createSocket();
+        } catch (Exception e) {
+            logError("Failed to create socket: ", e);
+            return false;
+        }
+
+        if (mSocket == null) return false;
+
+        mRunning = true;
+        mThread.start();
+        return true;
+    }
+
+    public final void stop() {
+        mRunning = false;
+        closeSocket(mSocket);
+        mSocket = null;
+    }
+
+    public final boolean isRunning() { return mRunning; }
+
+    public final long numPacketsReceived() { return mPacketsReceived; }
+
+    /**
+     * Subclasses MUST create the listening socket here, including setting
+     * all desired socket options, interface or address/port binding, etc.
+     */
+    protected abstract FileDescriptor createSocket();
+
+    /**
+     * Called by the main loop for every packet.  Any desired copies of
+     * |recvbuf| should be made in here, and the underlying byte array is
+     * reused across all reads.
+     */
+    protected void handlePacket(byte[] recvbuf, int length) {}
+
+    /**
+     * Called by the main loop to log errors.  In some cases |e| may be null.
+     */
+    protected void logError(String msg, Exception e) {}
+
+    /**
+     * Called by the main loop just prior to exiting.
+     */
+    protected void onExit() {}
+
+    private final void mainLoop() {
+        while (isRunning()) {
+            final int bytesRead;
+
+            try {
+                // Blocking read.
+                // TODO: See if this can be converted to recvfrom.
+                bytesRead = Os.read(mSocket, mPacket, 0, mPacket.length);
+                if (bytesRead < 1) {
+                    if (isRunning()) logError("Socket closed, exiting", null);
+                    break;
+                }
+                mPacketsReceived++;
+            } catch (ErrnoException e) {
+                if (e.errno != OsConstants.EINTR) {
+                    if (isRunning()) logError("read error: ", e);
+                    break;
+                }
+                continue;
+            } catch (IOException ioe) {
+                if (isRunning()) logError("read error: ", ioe);
+                continue;
+            }
+
+            try {
+                handlePacket(mPacket, bytesRead);
+            } catch (Exception e) {
+                logError("Unexpected exception: ", e);
+                break;
+            }
+        }
+
+        stop();
+        onExit();
+    }
+}
diff --git a/services/net/java/android/net/util/ConnectivityPacketSummary.java b/services/net/java/android/net/util/ConnectivityPacketSummary.java
new file mode 100644
index 0000000..699ba5b
--- /dev/null
+++ b/services/net/java/android/net/util/ConnectivityPacketSummary.java
@@ -0,0 +1,375 @@
+/*
+ * 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.net.util;
+
+import android.net.dhcp.DhcpPacket;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.StringJoiner;
+
+import static android.system.OsConstants.*;
+import static android.net.util.NetworkConstants.*;
+
+
+/**
+ * Critical connectivity packet summarizing class.
+ *
+ * Outputs short descriptions of ARP, DHCPv4, and IPv6 RS/RA/NS/NA packets.
+ *
+ * @hide
+ */
+public class ConnectivityPacketSummary {
+    private static final String TAG = ConnectivityPacketSummary.class.getSimpleName();
+
+    private final byte[] mHwAddr;
+    private final byte[] mBytes;
+    private final int mLength;
+    private final ByteBuffer mPacket;
+    private final String mSummary;
+
+    public static String summarize(byte[] hwaddr, byte[] buffer) {
+        return summarize(hwaddr, buffer, buffer.length);
+    }
+
+    // Methods called herein perform some but by no means all error checking.
+    // They may throw runtime exceptions on malformed packets.
+    public static String summarize(byte[] hwaddr, byte[] buffer, int length) {
+        if ((hwaddr == null) || (hwaddr.length != ETHER_ADDR_LEN)) return null;
+        if (buffer == null) return null;
+        length = Math.min(length, buffer.length);
+        return (new ConnectivityPacketSummary(hwaddr, buffer, length)).toString();
+    }
+
+    private ConnectivityPacketSummary(byte[] hwaddr, byte[] buffer, int length) {
+        mHwAddr = hwaddr;
+        mBytes = buffer;
+        mLength = Math.min(length, mBytes.length);
+        mPacket = ByteBuffer.wrap(mBytes, 0, mLength);
+        mPacket.order(ByteOrder.BIG_ENDIAN);
+
+        final StringJoiner sj = new StringJoiner(" ");
+        // TODO: support other link-layers, or even no link-layer header.
+        parseEther(sj);
+        mSummary = sj.toString();
+    }
+
+    public String toString() {
+        return mSummary;
+    }
+
+    private void parseEther(StringJoiner sj) {
+        if (mPacket.remaining() < ETHER_HEADER_LEN) {
+            sj.add("runt:").add(asString(mPacket.remaining()));
+            return;
+        }
+
+        mPacket.position(ETHER_SRC_ADDR_OFFSET);
+        final ByteBuffer srcMac = (ByteBuffer) mPacket.slice().limit(ETHER_ADDR_LEN);
+        sj.add(ByteBuffer.wrap(mHwAddr).equals(srcMac) ? "TX" : "RX");
+        sj.add(getMacAddressString(srcMac));
+
+        mPacket.position(ETHER_DST_ADDR_OFFSET);
+        final ByteBuffer dstMac = (ByteBuffer) mPacket.slice().limit(ETHER_ADDR_LEN);
+        sj.add(">").add(getMacAddressString(dstMac));
+
+        mPacket.position(ETHER_TYPE_OFFSET);
+        final int etherType = asUint(mPacket.getShort());
+        switch (etherType) {
+            case ETHER_TYPE_ARP:
+                sj.add("arp");
+                parseARP(sj);
+                break;
+            case ETHER_TYPE_IPV4:
+                sj.add("ipv4");
+                parseIPv4(sj);
+                break;
+            case ETHER_TYPE_IPV6:
+                sj.add("ipv6");
+                parseIPv6(sj);
+                break;
+            default:
+                // Unknown ether type.
+                sj.add("ethtype").add(asString(etherType));
+                break;
+        }
+    }
+
+    private void parseARP(StringJoiner sj) {
+        if (mPacket.remaining() < ARP_PAYLOAD_LEN) {
+            sj.add("runt:").add(asString(mPacket.remaining()));
+            return;
+        }
+
+        if (asUint(mPacket.getShort()) != ARP_HWTYPE_ETHER ||
+            asUint(mPacket.getShort()) != ETHER_TYPE_IPV4 ||
+            asUint(mPacket.get()) != ETHER_ADDR_LEN ||
+            asUint(mPacket.get()) != IPV4_ADDR_LEN) {
+            sj.add("unexpected header");
+            return;
+        }
+
+        final int opCode = asUint(mPacket.getShort());
+
+        final String senderHwAddr = getMacAddressString(mPacket);
+        final String senderIPv4 = getIPv4AddressString(mPacket);
+        getMacAddressString(mPacket);  // target hardware address, unused
+        final String targetIPv4 = getIPv4AddressString(mPacket);
+
+        if (opCode == ARP_REQUEST) {
+            sj.add("who-has").add(targetIPv4);
+        } else if (opCode == ARP_REPLY) {
+            sj.add("reply").add(senderIPv4).add(senderHwAddr);
+        } else {
+            sj.add("unknown opcode").add(asString(opCode));
+        }
+    }
+
+    private void parseIPv4(StringJoiner sj) {
+        if (!mPacket.hasRemaining()) {
+            sj.add("runt");
+            return;
+        }
+
+        final int startOfIpLayer = mPacket.position();
+        final int ipv4HeaderLength = (mPacket.get(startOfIpLayer) & IPV4_IHL_MASK) * 4;
+        if (mPacket.remaining() < ipv4HeaderLength ||
+            mPacket.remaining() < IPV4_HEADER_MIN_LEN) {
+            sj.add("runt:").add(asString(mPacket.remaining()));
+            return;
+        }
+        final int startOfTransportLayer = startOfIpLayer + ipv4HeaderLength;
+
+        mPacket.position(startOfIpLayer + IPV4_FLAGS_OFFSET);
+        final int flagsAndFragment = asUint(mPacket.getShort());
+        final boolean isFragment = (flagsAndFragment & IPV4_FRAGMENT_MASK) != 0;
+
+        mPacket.position(startOfIpLayer + IPV4_PROTOCOL_OFFSET);
+        final int protocol = asUint(mPacket.get());
+
+        mPacket.position(startOfIpLayer + IPV4_SRC_ADDR_OFFSET);
+        final String srcAddr = getIPv4AddressString(mPacket);
+
+        mPacket.position(startOfIpLayer + IPV4_DST_ADDR_OFFSET);
+        final String dstAddr = getIPv4AddressString(mPacket);
+
+        sj.add(srcAddr).add(">").add(dstAddr);
+
+        mPacket.position(startOfTransportLayer);
+        if (protocol == IPPROTO_UDP) {
+            sj.add("udp");
+            if (isFragment) sj.add("fragment");
+            else parseUDP(sj);
+        } else {
+            sj.add("proto").add(asString(protocol));
+            if (isFragment) sj.add("fragment");
+        }
+    }
+
+    private void parseIPv6(StringJoiner sj) {
+        if (mPacket.remaining() < IPV6_HEADER_LEN) {
+            sj.add("runt:").add(asString(mPacket.remaining()));
+            return;
+        }
+
+        final int startOfIpLayer = mPacket.position();
+
+        mPacket.position(startOfIpLayer + IPV6_PROTOCOL_OFFSET);
+        final int protocol = asUint(mPacket.get());
+
+        mPacket.position(startOfIpLayer + IPV6_SRC_ADDR_OFFSET);
+        final String srcAddr = getIPv6AddressString(mPacket);
+        final String dstAddr = getIPv6AddressString(mPacket);
+
+        sj.add(srcAddr).add(">").add(dstAddr);
+
+        mPacket.position(startOfIpLayer + IPV6_HEADER_LEN);
+        if (protocol == IPPROTO_ICMPV6) {
+            sj.add("icmp6");
+            parseICMPv6(sj);
+        } else {
+            sj.add("proto").add(asString(protocol));
+        }
+    }
+
+    private void parseICMPv6(StringJoiner sj) {
+        if (mPacket.remaining() < ICMPV6_HEADER_MIN_LEN) {
+            sj.add("runt:").add(asString(mPacket.remaining()));
+            return;
+        }
+
+        final int icmp6Type = asUint(mPacket.get());
+        final int icmp6Code = asUint(mPacket.get());
+        mPacket.getShort();  // checksum, unused
+
+        switch (icmp6Type) {
+            case ICMPV6_ROUTER_SOLICITATION:
+                sj.add("rs");
+                parseICMPv6RouterSolicitation(sj);
+                break;
+            case ICMPV6_ROUTER_ADVERTISEMENT:
+                sj.add("ra");
+                parseICMPv6RouterAdvertisement(sj);
+                break;
+            case ICMPV6_NEIGHBOR_SOLICITATION:
+                sj.add("ns");
+                parseICMPv6NeighborMessage(sj);
+                break;
+            case ICMPV6_NEIGHBOR_ADVERTISEMENT:
+                sj.add("na");
+                parseICMPv6NeighborMessage(sj);
+                break;
+            default:
+                sj.add("type").add(asString(icmp6Type));
+                sj.add("code").add(asString(icmp6Code));
+                break;
+        }
+    }
+
+    private void parseICMPv6RouterSolicitation(StringJoiner sj) {
+        final int RESERVED = 4;
+        if (mPacket.remaining() < RESERVED) {
+            sj.add("runt:").add(asString(mPacket.remaining()));
+            return;
+        }
+
+        mPacket.position(mPacket.position() + RESERVED);
+        parseICMPv6NeighborDiscoveryOptions(sj);
+    }
+
+    private void parseICMPv6RouterAdvertisement(StringJoiner sj) {
+        final int FLAGS_AND_TIMERS = 3 * 4;
+        if (mPacket.remaining() < FLAGS_AND_TIMERS) {
+            sj.add("runt:").add(asString(mPacket.remaining()));
+            return;
+        }
+
+        mPacket.position(mPacket.position() + FLAGS_AND_TIMERS);
+        parseICMPv6NeighborDiscoveryOptions(sj);
+    }
+
+    private void parseICMPv6NeighborMessage(StringJoiner sj) {
+        final int RESERVED = 4;
+        final int minReq = RESERVED + IPV6_ADDR_LEN;
+        if (mPacket.remaining() < minReq) {
+            sj.add("runt:").add(asString(mPacket.remaining()));
+            return;
+        }
+
+        mPacket.position(mPacket.position() + RESERVED);
+        sj.add(getIPv6AddressString(mPacket));
+        parseICMPv6NeighborDiscoveryOptions(sj);
+    }
+
+    private void parseICMPv6NeighborDiscoveryOptions(StringJoiner sj) {
+        // All ND options are TLV, where T is one byte and L is one byte equal
+        // to the length of T + L + V in units of 8 octets.
+        while (mPacket.remaining() >= ICMPV6_ND_OPTION_MIN_LENGTH) {
+            final int ndType = asUint(mPacket.get());
+            final int ndLength = asUint(mPacket.get());
+            final int ndBytes = ndLength * ICMPV6_ND_OPTION_LENGTH_SCALING_FACTOR - 2;
+            if (mPacket.remaining() < ndBytes) break;
+            final int position = mPacket.position();
+
+            switch (ndType) {
+                    case ICMPV6_ND_OPTION_SLLA:
+                        sj.add("slla");
+                        sj.add(getMacAddressString(mPacket));
+                        break;
+                    case ICMPV6_ND_OPTION_TLLA:
+                        sj.add("tlla");
+                        sj.add(getMacAddressString(mPacket));
+                        break;
+                    case ICMPV6_ND_OPTION_MTU:
+                        sj.add("mtu");
+                        final short reserved = mPacket.getShort();
+                        sj.add(asString(mPacket.getInt()));
+                        break;
+                    default:
+                        // Skip.
+                        break;
+            }
+
+            mPacket.position(position + ndBytes);
+        }
+    }
+
+    private void parseUDP(StringJoiner sj) {
+        if (mPacket.remaining() < UDP_HEADER_LEN) {
+            sj.add("runt:").add(asString(mPacket.remaining()));
+            return;
+        }
+
+        final int previous = mPacket.position();
+        final int srcPort = asUint(mPacket.getShort());
+        final int dstPort = asUint(mPacket.getShort());
+        sj.add(asString(srcPort)).add(">").add(asString(dstPort));
+
+        mPacket.position(previous + UDP_HEADER_LEN);
+        if (srcPort == DHCP4_CLIENT_PORT || dstPort == DHCP4_CLIENT_PORT) {
+            sj.add("dhcp4");
+            parseDHCPv4(sj);
+        }
+    }
+
+    private void parseDHCPv4(StringJoiner sj) {
+        final DhcpPacket dhcpPacket;
+        try {
+            dhcpPacket = DhcpPacket.decodeFullPacket(mBytes, mLength, DhcpPacket.ENCAP_L2);
+            sj.add(dhcpPacket.toString());
+        } catch (DhcpPacket.ParseException e) {
+            sj.add("parse error: " + e);
+        }
+    }
+
+    private static String getIPv4AddressString(ByteBuffer ipv4) {
+        return getIpAddressString(ipv4, IPV4_ADDR_LEN);
+    }
+
+    private static String getIPv6AddressString(ByteBuffer ipv6) {
+        return getIpAddressString(ipv6, IPV6_ADDR_LEN);
+    }
+
+    private static String getIpAddressString(ByteBuffer ip, int byteLength) {
+        if (ip == null || ip.remaining() < byteLength) return "invalid";
+
+        byte[] bytes = new byte[byteLength];
+        ip.get(bytes, 0, byteLength);
+        try {
+            InetAddress addr = InetAddress.getByAddress(bytes);
+            return addr.getHostAddress();
+        } catch (UnknownHostException uhe) {
+            return "unknown";
+        }
+    }
+
+    private static String getMacAddressString(ByteBuffer mac) {
+        if (mac == null || mac.remaining() < ETHER_ADDR_LEN) return "invalid";
+
+        byte[] bytes = new byte[ETHER_ADDR_LEN];
+        mac.get(bytes, 0, bytes.length);
+        Byte[] printableBytes = new Byte[bytes.length];
+        int i = 0;
+        for (byte b : bytes) printableBytes[i++] = b;
+
+        final String MAC48_FORMAT = "%02x:%02x:%02x:%02x:%02x:%02x";
+        return String.format(MAC48_FORMAT, printableBytes);
+    }
+}
diff --git a/services/net/java/android/net/util/NetworkConstants.java b/services/net/java/android/net/util/NetworkConstants.java
new file mode 100644
index 0000000..362f757
--- /dev/null
+++ b/services/net/java/android/net/util/NetworkConstants.java
@@ -0,0 +1,146 @@
+/*
+ * 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.net.util;
+
+import java.nio.ByteBuffer;
+
+
+/**
+ * Networking protocol constants.
+ *
+ * Includes:
+ *     - constants that describe packet layout
+ *     - various helper functions
+ *
+ * @hide
+ */
+public final class NetworkConstants {
+    private NetworkConstants() { throw new RuntimeException("no instance permitted"); }
+
+    /**
+     * Ethernet constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc894
+     *     - https://tools.ietf.org/html/rfc7042
+     *     - http://www.iana.org/assignments/ethernet-numbers/ethernet-numbers.xhtml
+     *     - http://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml
+     */
+    public static final int ETHER_DST_ADDR_OFFSET = 0;
+    public static final int ETHER_SRC_ADDR_OFFSET = 6;
+    public static final int ETHER_ADDR_LEN = 6;
+
+    public static final int ETHER_TYPE_OFFSET = 12;
+    public static final int ETHER_TYPE_LENGTH = 2;
+    public static final int ETHER_TYPE_ARP  = 0x0806;
+    public static final int ETHER_TYPE_IPV4 = 0x0800;
+    public static final int ETHER_TYPE_IPV6 = 0x86dd;
+
+    public static final int ETHER_HEADER_LEN = 14;
+
+    private static final byte FF = asByte(0xff);
+    public static final byte[] ETHER_ADDR_BROADCAST = {
+        FF, FF, FF, FF, FF, FF
+    };
+
+    /**
+     * ARP constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc826
+     *     - http://www.iana.org/assignments/arp-parameters/arp-parameters.xhtml
+     */
+    public static final int ARP_PAYLOAD_LEN = 28;  // For Ethernet+IPv4.
+    public static final int ARP_REQUEST = 1;
+    public static final int ARP_REPLY   = 2;
+    public static final int ARP_HWTYPE_RESERVED_LO = 0;
+    public static final int ARP_HWTYPE_ETHER       = 1;
+    public static final int ARP_HWTYPE_RESERVED_HI = 0xffff;
+
+    /**
+     * IPv4 constants.
+     *
+     * See als:
+     *     - https://tools.ietf.org/html/rfc791
+     */
+    public static final int IPV4_HEADER_MIN_LEN = 20;
+    public static final int IPV4_IHL_MASK = 0xf;
+    public static final int IPV4_FLAGS_OFFSET = 6;
+    public static final int IPV4_FRAGMENT_MASK = 0x1fff;
+    public static final int IPV4_PROTOCOL_OFFSET = 9;
+    public static final int IPV4_SRC_ADDR_OFFSET = 12;
+    public static final int IPV4_DST_ADDR_OFFSET = 16;
+    public static final int IPV4_ADDR_LEN = 4;
+
+    /**
+     * IPv6 constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc2460
+     */
+    public static final int IPV6_HEADER_LEN = 40;
+    public static final int IPV6_PROTOCOL_OFFSET = 6;
+    public static final int IPV6_SRC_ADDR_OFFSET = 8;
+    public static final int IPV6_DST_ADDR_OFFSET = 24;
+    public static final int IPV6_ADDR_LEN = 16;
+
+    /**
+     * ICMPv6 constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc4443
+     *     - https://tools.ietf.org/html/rfc4861
+     */
+    public static final int ICMPV6_HEADER_MIN_LEN = 4;
+    public static final int ICMPV6_ROUTER_SOLICITATION    = 133;
+    public static final int ICMPV6_ROUTER_ADVERTISEMENT   = 134;
+    public static final int ICMPV6_NEIGHBOR_SOLICITATION  = 135;
+    public static final int ICMPV6_NEIGHBOR_ADVERTISEMENT = 136;
+
+    public static final int ICMPV6_ND_OPTION_MIN_LENGTH = 8;
+    public static final int ICMPV6_ND_OPTION_LENGTH_SCALING_FACTOR = 8;
+    public static final int ICMPV6_ND_OPTION_SLLA = 1;
+    public static final int ICMPV6_ND_OPTION_TLLA = 2;
+    public static final int ICMPV6_ND_OPTION_MTU  = 5;
+
+    /**
+     * UDP constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc768
+     */
+    public static final int UDP_HEADER_LEN = 8;
+
+    /**
+     * DHCP(v4) constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc2131
+     */
+    public static final int DHCP4_SERVER_PORT = 67;
+    public static final int DHCP4_CLIENT_PORT = 68;
+
+    /**
+     * Utility functions.
+     */
+    public static byte asByte(int i) { return (byte) i; }
+
+    public static String asString(int i) { return Integer.toString(i); }
+
+    public static int asUint(byte b) { return (b & 0xff); }
+    public static int asUint(short s) { return (s & 0xffff); }
+}
diff --git a/services/tests/servicestests/Android.mk b/services/tests/servicestests/Android.mk
index 50e0662..7886b5e 100644
--- a/services/tests/servicestests/Android.mk
+++ b/services/tests/servicestests/Android.mk
@@ -21,7 +21,8 @@
     guava \
     android-support-test \
     mockito-target \
-    ShortcutManagerTestUtils
+    ShortcutManagerTestUtils \
+    truth-prebuilt
 
 LOCAL_JAVA_LIBRARIES := android.test.runner
 
@@ -46,6 +47,11 @@
 
 LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
 
+# Code coverage puts us over the dex limit, so enable multi-dex for coverage-enabled builds
+ifeq (true,$(EMMA_INSTRUMENT))
+LOCAL_JACK_FLAGS := --multi-dex native
+endif # EMMA_INSTRUMENT_STATIC
+
 include $(BUILD_PACKAGE)
 
 #########################################################################
diff --git a/services/tests/servicestests/src/android/net/util/BlockingSocketReaderTest.java b/services/tests/servicestests/src/android/net/util/BlockingSocketReaderTest.java
new file mode 100644
index 0000000..e03350f
--- /dev/null
+++ b/services/tests/servicestests/src/android/net/util/BlockingSocketReaderTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.net.util;
+
+import static android.system.OsConstants.*;
+
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructTimeval;
+
+import libcore.io.IoBridge;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import junit.framework.TestCase;
+
+
+/**
+ * Tests for BlockingSocketReader.
+ *
+ * @hide
+ */
+public class BlockingSocketReaderTest extends TestCase {
+    static final InetAddress LOOPBACK6 = Inet6Address.getLoopbackAddress();
+    static final StructTimeval TIMEO = StructTimeval.fromMillis(500);
+
+    protected CountDownLatch mLatch;
+    protected FileDescriptor mLocalSocket;
+    protected InetSocketAddress mLocalSockName;
+    protected byte[] mLastRecvBuf;
+    protected boolean mExited;
+    protected BlockingSocketReader mReceiver;
+
+    @Override
+    public void setUp() {
+        resetLatch();
+        mLocalSocket = null;
+        mLocalSockName = null;
+        mLastRecvBuf = null;
+        mExited = false;
+
+        mReceiver = new BlockingSocketReader() {
+            @Override
+            protected FileDescriptor createSocket() {
+                FileDescriptor s = null;
+                try {
+                    s = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
+                    Os.bind(s, LOOPBACK6, 0);
+                    mLocalSockName = (InetSocketAddress) Os.getsockname(s);
+                    Os.setsockoptTimeval(s, SOL_SOCKET, SO_SNDTIMEO, TIMEO);
+                } catch (ErrnoException|SocketException e) {
+                    closeSocket(s);
+                    fail();
+                    return null;
+                }
+
+                mLocalSocket = s;
+                return s;
+            }
+
+            @Override
+            protected void handlePacket(byte[] recvbuf, int length) {
+                mLastRecvBuf = Arrays.copyOf(recvbuf, length);
+                mLatch.countDown();
+            }
+
+            @Override
+            protected void onExit() {
+                mExited = true;
+                mLatch.countDown();
+            }
+        };
+    }
+
+    @Override
+    public void tearDown() {
+        if (mReceiver != null) mReceiver.stop();
+        mReceiver = null;
+    }
+
+    void resetLatch() { mLatch = new CountDownLatch(1); }
+
+    void waitForActivity() throws Exception {
+        assertTrue(mLatch.await(500, TimeUnit.MILLISECONDS));
+        resetLatch();
+    }
+
+    void sendPacket(byte[] contents) throws Exception {
+        final DatagramSocket sender = new DatagramSocket();
+        sender.connect(mLocalSockName);
+        sender.send(new DatagramPacket(contents, contents.length));
+        sender.close();
+    }
+
+    public void testBasicWorking() throws Exception {
+        assertTrue(mReceiver.start());
+        assertTrue(mLocalSockName != null);
+        assertEquals(LOOPBACK6, mLocalSockName.getAddress());
+        assertTrue(0 < mLocalSockName.getPort());
+        assertTrue(mLocalSocket != null);
+        assertFalse(mExited);
+
+        final byte[] one = "one 1".getBytes("UTF-8");
+        sendPacket(one);
+        waitForActivity();
+        assertEquals(1, mReceiver.numPacketsReceived());
+        assertTrue(Arrays.equals(one, mLastRecvBuf));
+        assertFalse(mExited);
+
+        final byte[] two = "two 2".getBytes("UTF-8");
+        sendPacket(two);
+        waitForActivity();
+        assertEquals(2, mReceiver.numPacketsReceived());
+        assertTrue(Arrays.equals(two, mLastRecvBuf));
+        assertFalse(mExited);
+
+        mReceiver.stop();
+        waitForActivity();
+        assertEquals(2, mReceiver.numPacketsReceived());
+        assertTrue(Arrays.equals(two, mLastRecvBuf));
+        assertTrue(mExited);
+    }
+}
diff --git a/services/tests/servicestests/src/android/net/util/ConnectivityPacketSummaryTest.java b/services/tests/servicestests/src/android/net/util/ConnectivityPacketSummaryTest.java
new file mode 100644
index 0000000..766e5c0
--- /dev/null
+++ b/services/tests/servicestests/src/android/net/util/ConnectivityPacketSummaryTest.java
@@ -0,0 +1,377 @@
+/*
+ * 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.net.util;
+
+import static android.net.util.NetworkConstants.*;
+
+import libcore.util.HexEncoding;
+
+import junit.framework.TestCase;
+
+
+/**
+ * Tests for ConnectivityPacketSummary.
+ *
+ * @hide
+ */
+public class ConnectivityPacketSummaryTest extends TestCase {
+    private static final byte[] MYHWADDR = {
+        asByte(0x80), asByte(0x7a), asByte(0xbf), asByte(0x6f), asByte(0x48), asByte(0xf3)
+    };
+
+    private String getSummary(String hexBytes) {
+        hexBytes = hexBytes.replaceAll("\\s+", "");
+        final byte[] bytes = HexEncoding.decode(hexBytes.toCharArray(), false);
+        return ConnectivityPacketSummary.summarize(MYHWADDR, bytes);
+    }
+
+    public void testParseICMPv6DADProbe() {
+        final String packet =
+                // Ethernet
+                "3333FF6F48F3 807ABF6F48F3 86DD" +
+                // IPv6
+                "600000000018 3A FF" +
+                "00000000000000000000000000000000" +
+                "FF0200000000000000000001FF6F48F3" +
+                // ICMPv6
+                "87 00 A8E7" +
+                "00000000" +
+                "FE80000000000000827ABFFFFE6F48F3";
+
+        final String expected =
+                "TX 80:7a:bf:6f:48:f3 > 33:33:ff:6f:48:f3 ipv6" +
+                " :: > ff02::1:ff6f:48f3 icmp6" +
+                " ns fe80::827a:bfff:fe6f:48f3";
+
+        assertEquals(expected, getSummary(packet));
+    }
+
+    public void testParseICMPv6RS() {
+        final String packet =
+                // Ethernet
+                "333300000002 807ABF6F48F3 86DD" +
+                // IPv6
+                "600000000010 3A FF" +
+                "FE80000000000000827ABFFFFE6F48F3" +
+                "FF020000000000000000000000000002" +
+                // ICMPv6 RS
+                "85 00 6973" +
+                "00000000" +
+                "01 01 807ABF6F48F3";
+
+        final String expected =
+                "TX 80:7a:bf:6f:48:f3 > 33:33:00:00:00:02 ipv6" +
+                " fe80::827a:bfff:fe6f:48f3 > ff02::2 icmp6" +
+                " rs slla 80:7a:bf:6f:48:f3";
+
+        assertEquals(expected, getSummary(packet));
+    }
+
+    public void testParseICMPv6RA() {
+        final String packet =
+                // Ethernet
+                "807ABF6F48F3 100E7E263FC1 86DD" +
+                // IPv6
+                "600000000068 3A FF" +
+                "FE80000000000000FA000004FD000001" +
+                "FE80000000000000827ABFFFFE6F48F3" +
+                // ICMPv6 RA
+                "86 00 8141" +
+                "40 00 0E10" +
+                "00000000" +
+                "00000000" +
+                "01 01 00005E000265" +
+                "05 01 0000000005DC" +
+                "19 05 000000000E10" +
+                "      20014860486000000000000000008844" +
+                "      20014860486000000000000000008888" +
+                "03 04 40 C0" +
+                "      00278D00" +
+                "      00093A80" +
+                "      00000000" +
+                "      2401FA000004FD000000000000000000";
+
+        final String expected =
+                "RX 10:0e:7e:26:3f:c1 > 80:7a:bf:6f:48:f3 ipv6" +
+                " fe80::fa00:4:fd00:1 > fe80::827a:bfff:fe6f:48f3 icmp6" +
+                " ra slla 00:00:5e:00:02:65 mtu 1500";
+
+        assertEquals(expected, getSummary(packet));
+    }
+
+    public void testParseICMPv6NS() {
+        final String packet =
+                // Ethernet
+                  "807ABF6F48F3 100E7E263FC1 86DD" +
+                  // IPv6
+                  "6C0000000020 3A FF" +
+                  "FE80000000000000FA000004FD000001" +
+                  "FF0200000000000000000001FF01C146" +
+                  // ICMPv6 NS
+                  "87 00 8AD4" +
+                  "00000000" +
+                  "2401FA000004FD0015EA6A5C7B01C146" +
+                  "01 01 00005E000265";
+
+        final String expected =
+                "RX 10:0e:7e:26:3f:c1 > 80:7a:bf:6f:48:f3 ipv6" +
+                " fe80::fa00:4:fd00:1 > ff02::1:ff01:c146 icmp6" +
+                " ns 2401:fa00:4:fd00:15ea:6a5c:7b01:c146 slla 00:00:5e:00:02:65";
+
+        assertEquals(expected, getSummary(packet));
+    }
+
+    public void testParseICMPv6NA() {
+        final String packet =
+                // Ethernet
+                "00005E000265 807ABF6F48F3 86DD" +
+                "600000000020 3A FF" +
+                "2401FA000004FD0015EA6A5C7B01C146" +
+                "FE80000000000000FA000004FD000001" +
+                "88 00 E8126" +
+                "0000000" +
+                "2401FA000004FD0015EA6A5C7B01C146" +
+                "02 01 807ABF6F48F3";
+
+        final String expected =
+                "TX 80:7a:bf:6f:48:f3 > 00:00:5e:00:02:65 ipv6" +
+                " 2401:fa00:4:fd00:15ea:6a5c:7b01:c146 > fe80::fa00:4:fd00:1 icmp6" +
+                " na 2401:fa00:4:fd00:15ea:6a5c:7b01:c146 tlla 80:7a:bf:6f:48:f3";
+
+        assertEquals(expected, getSummary(packet));
+    }
+
+    public void testParseARPRequest() {
+        final String packet =
+                // Ethernet
+                  "FFFFFFFFFFFF 807ABF6F48F3 0806" +
+                  // ARP
+                  "0001 0800 06 04" +
+                  // Request
+                  "0001" +
+                  "807ABF6F48F3 64706ADB" +
+                  "000000000000 64706FFD";
+
+        final String expected =
+                "TX 80:7a:bf:6f:48:f3 > ff:ff:ff:ff:ff:ff arp" +
+                " who-has 100.112.111.253";
+
+        assertEquals(expected, getSummary(packet));
+    }
+
+    public void testParseARPReply() {
+        final String packet =
+                // Ethernet
+                  "807ABF6F48F3 288A1CA8DFC1 0806" +
+                  // ARP
+                  "0001 0800 06 04" +
+                  // Reply
+                  "0002" +
+                  "288A1CA8DFC1 64706FFD"+
+                  "807ABF6F48F3 64706ADB" +
+                  // Ethernet padding to packet min size.
+                  "0000000000000000000000000000";
+
+        final String expected =
+                "RX 28:8a:1c:a8:df:c1 > 80:7a:bf:6f:48:f3 arp" +
+                " reply 100.112.111.253 28:8a:1c:a8:df:c1";
+
+        assertEquals(expected, getSummary(packet));
+    }
+
+    public void testParseDHCPv4Discover() {
+        final String packet =
+                // Ethernet
+                "FFFFFFFFFFFF 807ABF6F48F3 0800" +
+                // IPv4
+                "451001580000400040113986" +
+                "00000000" +
+                "FFFFFFFF" +
+                // UDP
+                "0044 0043" +
+                "0144 5559" +
+                // DHCPv4
+                "01 01 06 00" +
+                "79F7ACA4" +
+                "0000 0000" +
+                "00000000" +
+                "00000000" +
+                "00000000" +
+                "00000000" +
+                "807ABF6F48F300000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "63 82 53 63" +
+                "35 01 01" +
+                "3D 07 01807ABF6F48F3" +
+                "39 02 05DC" +
+                "3C 12 616E64726F69642D646863702D372E312E32" +
+                "0C 18 616E64726F69642D36623030366333313333393835343139" +
+                "37 0A 01 03 06 0F 1A 1C 33 3A 3B 2B" +
+                "FF" +
+                "00";
+
+        final String expectedPrefix =
+                "TX 80:7a:bf:6f:48:f3 > ff:ff:ff:ff:ff:ff ipv4" +
+                " 0.0.0.0 > 255.255.255.255 udp" +
+                " 68 > 67 dhcp4" +
+                " 80:7a:bf:6f:48:f3 DISCOVER";
+
+        assertTrue(getSummary(packet).startsWith(expectedPrefix));
+    }
+
+    public void testParseDHCPv4Offer() {
+        final String packet =
+                // Ethernet
+                "807ABF6F48F3 288A1CA8DFC1 0800" +
+                // IPv4
+                "4500013D4D2C0000401188CB" +
+                "64706FFD" +
+                "64706ADB" +
+                // UDP
+                "0043 0044" +
+                "0129 371D" +
+                // DHCPv4
+                "02 01 06 01" +
+                "79F7ACA4" +
+                "0000 0000" +
+                "00000000" +
+                "64706ADB" +
+                "00000000" +
+                "00000000" +
+                "807ABF6F48F300000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "63 82 53 63" +
+                "35 01 02" +
+                "36 04 AC188A0B" +
+                "33 04 00000708" +
+                "01 04 FFFFF000" +
+                "03 04 64706FFE" +
+                "06 08 08080808" +
+                "      08080404" +
+                "FF0001076165313A363636FF";
+
+        final String expectedPrefix =
+                "RX 28:8a:1c:a8:df:c1 > 80:7a:bf:6f:48:f3 ipv4" +
+                " 100.112.111.253 > 100.112.106.219 udp" +
+                " 67 > 68 dhcp4" +
+                " 80:7a:bf:6f:48:f3 OFFER";
+
+        assertTrue(getSummary(packet).startsWith(expectedPrefix));
+    }
+
+    public void testParseDHCPv4Request() {
+        final String packet =
+                // Ethernet
+                "FFFFFFFFFFFF 807ABF6F48F3 0800" +
+                // IPv4
+                "45100164000040004011397A" +
+                "00000000" +
+                "FFFFFFFF" +
+                // UDP
+                "0044 0043" +
+                "0150 E5C7" +
+                // DHCPv4
+                "01 01 06 00" +
+                "79F7ACA4" +
+                "0001 0000" +
+                "00000000" +
+                "00000000" +
+                "00000000" +
+                "00000000" +
+                "807ABF6F48F300000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "63 82 53 63" +
+                "35 01 03" +
+                "3D 07 01807ABF6F48F3" +
+                "32 04 64706ADB" +
+                "36 04 AC188A0B" +
+                "39 02 05DC" +
+                "3C 12 616E64726F69642D646863702D372E312E32" +
+                "0C 18 616E64726F69642D36623030366333313333393835343139" +
+                "37 0A 01 03 06 0F 1A 1C 33 3A 3B 2B" +
+                "FF" +
+                "00";
+
+        final String expectedPrefix =
+                "TX 80:7a:bf:6f:48:f3 > ff:ff:ff:ff:ff:ff ipv4" +
+                " 0.0.0.0 > 255.255.255.255 udp" +
+                " 68 > 67 dhcp4" +
+                " 80:7a:bf:6f:48:f3 REQUEST";
+
+        assertTrue(getSummary(packet).startsWith(expectedPrefix));
+    }
+
+    public void testParseDHCPv4Ack() {
+        final String packet =
+                // Ethernet
+                "807ABF6F48F3 288A1CA8DFC1 0800" +
+                // IPv4
+                "4500013D4D3B0000401188BC" +
+                "64706FFD" +
+                "64706ADB" +
+                // UDP
+                "0043 0044" +
+                "0129 341C" +
+                // DHCPv4
+                "02 01 06 01" +
+                "79F7ACA4" +
+                "0001 0000" +
+                "00000000" +
+                "64706ADB" +
+                "00000000" +
+                "00000000" +
+                "807ABF6F48F300000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "63 82 53 63" +
+                "35 01 05" +
+                "36 04 AC188A0B" +
+                "33 04 00000708" +
+                "01 04 FFFFF000" +
+                "03 04 64706FFE" +
+                "06 08 08080808" +
+                "      08080404" +
+                "FF0001076165313A363636FF";
+
+        final String expectedPrefix =
+                "RX 28:8a:1c:a8:df:c1 > 80:7a:bf:6f:48:f3 ipv4" +
+                " 100.112.111.253 > 100.112.106.219 udp" +
+                " 67 > 68 dhcp4" +
+                " 80:7a:bf:6f:48:f3 ACK";
+
+        assertTrue(getSummary(packet).startsWith(expectedPrefix));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/connectivity/IpConnectivityEventBuilderTest.java b/services/tests/servicestests/src/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
index 84f0f90..011e505 100644
--- a/services/tests/servicestests/src/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
+++ b/services/tests/servicestests/src/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
@@ -16,6 +16,17 @@
 
 package com.android.server.connectivity;
 
+import static com.android.server.connectivity.MetricsTestUtil.aBool;
+import static com.android.server.connectivity.MetricsTestUtil.aByteArray;
+import static com.android.server.connectivity.MetricsTestUtil.aLong;
+import static com.android.server.connectivity.MetricsTestUtil.aString;
+import static com.android.server.connectivity.MetricsTestUtil.aType;
+import static com.android.server.connectivity.MetricsTestUtil.anInt;
+import static com.android.server.connectivity.MetricsTestUtil.anIntArray;
+import static com.android.server.connectivity.MetricsTestUtil.b;
+import static com.android.server.connectivity.MetricsTestUtil.describeIpEvent;
+import static com.android.server.connectivity.metrics.IpConnectivityLogClass.IpConnectivityLog;
+
 import android.net.ConnectivityMetricsEvent;
 import android.net.metrics.ApfProgramEvent;
 import android.net.metrics.ApfStats;
@@ -28,24 +39,13 @@
 import android.net.metrics.NetworkEvent;
 import android.net.metrics.RaEvent;
 import android.net.metrics.ValidationProbeEvent;
-import com.google.protobuf.nano.MessageNano;
+import android.test.suitebuilder.annotation.SmallTest;
 import java.util.Arrays;
 import junit.framework.TestCase;
 
-import static com.android.server.connectivity.metrics.IpConnectivityLogClass.IpConnectivityLog;
-import static com.android.server.connectivity.MetricsTestUtil.aBool;
-import static com.android.server.connectivity.MetricsTestUtil.aByteArray;
-import static com.android.server.connectivity.MetricsTestUtil.aLong;
-import static com.android.server.connectivity.MetricsTestUtil.aString;
-import static com.android.server.connectivity.MetricsTestUtil.aType;
-import static com.android.server.connectivity.MetricsTestUtil.anInt;
-import static com.android.server.connectivity.MetricsTestUtil.anIntArray;
-import static com.android.server.connectivity.MetricsTestUtil.b;
-import static com.android.server.connectivity.MetricsTestUtil.describeIpEvent;
-import static com.android.server.connectivity.MetricsTestUtil.ipEv;
-
 public class IpConnectivityEventBuilderTest extends TestCase {
 
+    @SmallTest
     public void testDefaultNetworkEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(DefaultNetworkEvent.class),
@@ -58,6 +58,8 @@
         String want = joinLines(
                 "dropped_events: 0",
                 "events <",
+                "  time_ms: 1",
+                "  transport: 0",
                 "  default_network_event <",
                 "    network_id <",
                 "      network_id: 102",
@@ -70,13 +72,13 @@
                 "    transport_types: 2",
                 "    transport_types: 3",
                 "  >",
-                "  time_ms: 1",
                 ">",
                 "version: 2");
 
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testDhcpClientEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(DhcpClientEvent.class),
@@ -87,19 +89,20 @@
         String want = joinLines(
                 "dropped_events: 0",
                 "events <",
+                "  time_ms: 1",
+                "  transport: 0",
                 "  dhcp_event <",
                 "    duration_ms: 192",
-                "    error_code: 0",
                 "    if_name: \"wlan0\"",
                 "    state_transition: \"SomeState\"",
                 "  >",
-                "  time_ms: 1",
                 ">",
                 "version: 2");
 
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testDhcpErrorEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(DhcpErrorEvent.class),
@@ -109,19 +112,20 @@
         String want = joinLines(
                 "dropped_events: 0",
                 "events <",
+                "  time_ms: 1",
+                "  transport: 0",
                 "  dhcp_event <",
                 "    duration_ms: 0",
-                "    error_code: 50397184",
                 "    if_name: \"wlan0\"",
-                "    state_transition: \"\"",
+                "    error_code: 50397184",
                 "  >",
-                "  time_ms: 1",
                 ">",
                 "version: 2");
 
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testDnsEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(DnsEvent.class),
@@ -133,6 +137,8 @@
         String want = joinLines(
                 "dropped_events: 0",
                 "events <",
+                "  time_ms: 1",
+                "  transport: 0",
                 "  dns_lookup_batch <",
                 "    event_types: 1",
                 "    event_types: 1",
@@ -162,13 +168,13 @@
                 "    return_codes: 200",
                 "    return_codes: 178",
                 "  >",
-                "  time_ms: 1",
                 ">",
                 "version: 2");
 
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testIpManagerEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(IpManagerEvent.class),
@@ -179,18 +185,20 @@
         String want = joinLines(
                 "dropped_events: 0",
                 "events <",
+                "  time_ms: 1",
+                "  transport: 0",
                 "  ip_provisioning_event <",
                 "    event_type: 1",
                 "    if_name: \"wlan0\"",
                 "    latency_ms: 5678",
                 "  >",
-                "  time_ms: 1",
                 ">",
                 "version: 2");
 
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testIpReachabilityEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(IpReachabilityEvent.class),
@@ -200,17 +208,19 @@
         String want = joinLines(
                 "dropped_events: 0",
                 "events <",
+                "  time_ms: 1",
+                "  transport: 0",
                 "  ip_reachability_event <",
                 "    event_type: 512",
                 "    if_name: \"wlan0\"",
                 "  >",
-                "  time_ms: 1",
                 ">",
                 "version: 2");
 
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testNetworkEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(NetworkEvent.class),
@@ -221,6 +231,8 @@
         String want = joinLines(
                 "dropped_events: 0",
                 "events <",
+                "  time_ms: 1",
+                "  transport: 0",
                 "  network_event <",
                 "    event_type: 5",
                 "    latency_ms: 20410",
@@ -228,13 +240,13 @@
                 "      network_id: 100",
                 "    >",
                 "  >",
-                "  time_ms: 1",
                 ">",
                 "version: 2");
 
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testValidationProbeEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(ValidationProbeEvent.class),
@@ -247,6 +259,7 @@
                 "dropped_events: 0",
                 "events <",
                 "  time_ms: 1",
+                "  transport: 0",
                 "  validation_probe_event <",
                 "    latency_ms: 40730",
                 "    network_id <",
@@ -261,6 +274,7 @@
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testApfProgramEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(ApfProgramEvent.class),
@@ -273,6 +287,8 @@
         String want = joinLines(
                 "dropped_events: 0",
                 "events <",
+                "  time_ms: 1",
+                "  transport: 0",
                 "  apf_program_event <",
                 "    current_ras: 9",
                 "    drop_multicast: true",
@@ -281,13 +297,13 @@
                 "    lifetime: 200",
                 "    program_length: 2048",
                 "  >",
-                "  time_ms: 1",
                 ">",
                 "version: 2");
 
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testApfStatsSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(ApfStats.class),
@@ -303,6 +319,8 @@
         String want = joinLines(
                 "dropped_events: 0",
                 "events <",
+                "  time_ms: 1",
+                "  transport: 0",
                 "  apf_statistics <",
                 "    dropped_ras: 2",
                 "    duration_ms: 45000",
@@ -313,13 +331,13 @@
                 "    received_ras: 10",
                 "    zero_lifetime_ras: 1",
                 "  >",
-                "  time_ms: 1",
                 ">",
                 "version: 2");
 
         verifySerialization(want, ev);
     }
 
+    @SmallTest
     public void testRaEventSerialization() {
         ConnectivityMetricsEvent ev = describeIpEvent(
                 aType(RaEvent.class),
@@ -333,6 +351,8 @@
         String want = joinLines(
                 "dropped_events: 0",
                 "events <",
+                "  time_ms: 1",
+                "  transport: 0",
                 "  ra_event <",
                 "    dnssl_lifetime: -1",
                 "    prefix_preferred_lifetime: 300",
@@ -341,7 +361,6 @@
                 "    route_info_lifetime: -1",
                 "    router_lifetime: 2000",
                 "  >",
-                "  time_ms: 1",
                 ">",
                 "version: 2");
 
@@ -350,9 +369,9 @@
 
     static void verifySerialization(String want, ConnectivityMetricsEvent... input) {
         try {
-            byte[] got = IpConnectivityEventBuilder.serialize(0, Arrays.asList(input));
-            IpConnectivityLog log = new IpConnectivityLog();
-            MessageNano.mergeFrom(log, got);
+            byte[] got = IpConnectivityEventBuilder.serialize(0,
+                    IpConnectivityEventBuilder.toProto(Arrays.asList(input)));
+            IpConnectivityLog log = IpConnectivityLog.parseFrom(got);
             assertEquals(want, log.toString());
         } catch (Exception e) {
             fail(e.toString());
diff --git a/services/tests/servicestests/src/com/android/server/connectivity/IpConnectivityMetricsTest.java b/services/tests/servicestests/src/com/android/server/connectivity/IpConnectivityMetricsTest.java
index aa491bb..450653c 100644
--- a/services/tests/servicestests/src/com/android/server/connectivity/IpConnectivityMetricsTest.java
+++ b/services/tests/servicestests/src/com/android/server/connectivity/IpConnectivityMetricsTest.java
@@ -16,6 +16,9 @@
 
 package com.android.server.connectivity;
 
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
 import android.content.Context;
 import android.net.ConnectivityMetricsEvent;
 import android.net.IIpConnectivityMetrics;
@@ -29,9 +32,9 @@
 import android.net.metrics.RaEvent;
 import android.net.metrics.ValidationProbeEvent;
 import android.os.Parcelable;
+import android.test.suitebuilder.annotation.SmallTest;
 import android.util.Base64;
 import com.android.server.connectivity.metrics.IpConnectivityLogClass;
-import com.google.protobuf.nano.MessageNano;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.Collections;
@@ -43,10 +46,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
 public class IpConnectivityMetricsTest extends TestCase {
     static final IpReachabilityEvent FAKE_EV =
             new IpReachabilityEvent("wlan0", IpReachabilityEvent.NUD_FAILED);
@@ -61,6 +60,7 @@
         mService = new IpConnectivityMetrics(mCtx, (ctx) -> 2000);
     }
 
+    @SmallTest
     public void testLoggingEvents() throws Exception {
         IpConnectivityLog logger = new IpConnectivityLog(mMockService);
 
@@ -74,6 +74,7 @@
         assertEventsEqual(expectedEvent(3), got.get(2));
     }
 
+    @SmallTest
     public void testLoggingEventsWithMultipleCallers() throws Exception {
         IpConnectivityLog logger = new IpConnectivityLog(mMockService);
 
@@ -101,6 +102,7 @@
         }
     }
 
+    @SmallTest
     public void testBufferFlushing() {
         String output1 = getdump("flush");
         assertEquals("", output1);
@@ -113,6 +115,7 @@
         assertEquals("", output3);
     }
 
+    @SmallTest
     public void testRateLimiting() {
         final IpConnectivityLog logger = new IpConnectivityLog(mService.impl);
         final ApfProgramEvent ev = new ApfProgramEvent(0, 0, 0, 0, 0);
@@ -134,6 +137,7 @@
         assertEquals("", output2);
     }
 
+    @SmallTest
     public void testEndToEndLogging() {
         IpConnectivityLog logger = new IpConnectivityLog(mService.impl);
 
@@ -154,22 +158,25 @@
         String want = joinLines(
                 "dropped_events: 0",
                 "events <",
+                "  time_ms: 100",
+                "  transport: 0",
                 "  ip_reachability_event <",
                 "    event_type: 512",
                 "    if_name: \"wlan0\"",
                 "  >",
-                "  time_ms: 100",
                 ">",
                 "events <",
+                "  time_ms: 200",
+                "  transport: 0",
                 "  dhcp_event <",
                 "    duration_ms: 192",
-                "    error_code: 0",
                 "    if_name: \"wlan0\"",
                 "    state_transition: \"SomeState\"",
                 "  >",
-                "  time_ms: 200",
                 ">",
                 "events <",
+                "  time_ms: 300",
+                "  transport: 0",
                 "  default_network_event <",
                 "    network_id <",
                 "      network_id: 102",
@@ -182,18 +189,19 @@
                 "    transport_types: 2",
                 "    transport_types: 3",
                 "  >",
-                "  time_ms: 300",
                 ">",
                 "events <",
+                "  time_ms: 400",
+                "  transport: 0",
                 "  ip_provisioning_event <",
                 "    event_type: 1",
                 "    if_name: \"wlan0\"",
                 "    latency_ms: 5678",
                 "  >",
-                "  time_ms: 400",
                 ">",
                 "events <",
                 "  time_ms: 500",
+                "  transport: 0",
                 "  validation_probe_event <",
                 "    latency_ms: 40730",
                 "    network_id <",
@@ -204,6 +212,8 @@
                 "  >",
                 ">",
                 "events <",
+                "  time_ms: 600",
+                "  transport: 0",
                 "  apf_statistics <",
                 "    dropped_ras: 2",
                 "    duration_ms: 45000",
@@ -214,9 +224,10 @@
                 "    received_ras: 10",
                 "    zero_lifetime_ras: 1",
                 "  >",
-                "  time_ms: 600",
                 ">",
                 "events <",
+                "  time_ms: 700",
+                "  transport: 0",
                 "  ra_event <",
                 "    dnssl_lifetime: -1",
                 "    prefix_preferred_lifetime: 300",
@@ -225,7 +236,6 @@
                 "    route_info_lifetime: -1",
                 "    router_lifetime: 2000",
                 "  >",
-                "  time_ms: 700",
                 ">",
                 "version: 2");
 
@@ -254,8 +264,7 @@
         try {
             byte[] got = Base64.decode(output, Base64.DEFAULT);
             IpConnectivityLogClass.IpConnectivityLog log =
-                    new IpConnectivityLogClass.IpConnectivityLog();
-            MessageNano.mergeFrom(log, got);
+                    IpConnectivityLogClass.IpConnectivityLog.parseFrom(got);
             assertEquals(want, log.toString());
         } catch (Exception e) {
             fail(e.toString());
@@ -283,10 +292,5 @@
     }
 
     static final Comparator<ConnectivityMetricsEvent> EVENT_COMPARATOR =
-        new Comparator<ConnectivityMetricsEvent>() {
-            @Override
-            public int compare(ConnectivityMetricsEvent ev1, ConnectivityMetricsEvent ev2) {
-                return (int) (ev1.timestamp - ev2.timestamp);
-            }
-        };
+        Comparator.comparingLong((ev) -> ev.timestamp);
 }
diff --git a/services/tests/servicestests/src/com/android/server/connectivity/NetdEventListenerServiceTest.java b/services/tests/servicestests/src/com/android/server/connectivity/NetdEventListenerServiceTest.java
index 9e2fd62..97afa60 100644
--- a/services/tests/servicestests/src/com/android/server/connectivity/NetdEventListenerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/connectivity/NetdEventListenerServiceTest.java
@@ -16,23 +16,34 @@
 
 package com.android.server.connectivity;
 
-import android.net.ConnectivityManager.NetworkCallback;
 import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
 import android.net.Network;
 import android.net.metrics.DnsEvent;
 import android.net.metrics.INetdEventListener;
 import android.net.metrics.IpConnectivityLog;
-
+import android.os.RemoteException;
+import android.system.OsConstants;
+import android.test.suitebuilder.annotation.SmallTest;
+import com.android.server.connectivity.metrics.IpConnectivityLogClass.IpConnectivityEvent;
+import java.io.FileOutputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.OptionalInt;
+import java.util.stream.IntStream;
 import junit.framework.TestCase;
 import org.junit.Before;
 import org.junit.Test;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertTrue;
-
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.eq;
@@ -40,13 +51,6 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
-import java.io.FileOutputStream;
-import java.io.PrintWriter;
-import java.util.Arrays;
-import java.util.List;
-import java.util.OptionalInt;
-import java.util.stream.IntStream;
-
 public class NetdEventListenerServiceTest extends TestCase {
 
     // TODO: read from NetdEventListenerService after this constant is read from system property
@@ -66,50 +70,56 @@
         }
     }
 
+    private static final String EXAMPLE_IPV4 = "192.0.2.1";
+    private static final String EXAMPLE_IPV6 = "2001:db8:1200::2:1";
+
     NetdEventListenerService mNetdEventListenerService;
 
     @Mock ConnectivityManager mCm;
     @Mock IpConnectivityLog mLog;
     ArgumentCaptor<NetworkCallback> mCallbackCaptor;
-    ArgumentCaptor<DnsEvent> mEvCaptor;
+    ArgumentCaptor<DnsEvent> mDnsEvCaptor;
 
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mCallbackCaptor = ArgumentCaptor.forClass(NetworkCallback.class);
-        mEvCaptor = ArgumentCaptor.forClass(DnsEvent.class);
+        mDnsEvCaptor = ArgumentCaptor.forClass(DnsEvent.class);
         mNetdEventListenerService = new NetdEventListenerService(mCm, mLog);
 
         verify(mCm, times(1)).registerNetworkCallback(any(), mCallbackCaptor.capture());
     }
 
-    public void testOneBatch() throws Exception {
+    @SmallTest
+    public void testOneDnsBatch() throws Exception {
         log(105, LATENCIES);
         log(106, Arrays.copyOf(LATENCIES, BATCH_SIZE - 1)); // one lookup short of a batch event
 
-        verifyLoggedEvents(new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES));
+        verifyLoggedDnsEvents(new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES));
 
         log(106, Arrays.copyOfRange(LATENCIES, BATCH_SIZE - 1, BATCH_SIZE));
 
-        mEvCaptor = ArgumentCaptor.forClass(DnsEvent.class); // reset argument captor
-        verifyLoggedEvents(
+        mDnsEvCaptor = ArgumentCaptor.forClass(DnsEvent.class); // reset argument captor
+        verifyLoggedDnsEvents(
             new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
             new DnsEvent(106, EVENT_TYPES, RETURN_CODES, LATENCIES));
     }
 
-    public void testSeveralBatches() throws Exception {
+    @SmallTest
+    public void testSeveralDmsBatches() throws Exception {
         log(105, LATENCIES);
         log(106, LATENCIES);
         log(105, LATENCIES);
         log(107, LATENCIES);
 
-        verifyLoggedEvents(
+        verifyLoggedDnsEvents(
             new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
             new DnsEvent(106, EVENT_TYPES, RETURN_CODES, LATENCIES),
             new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
             new DnsEvent(107, EVENT_TYPES, RETURN_CODES, LATENCIES));
     }
 
-    public void testBatchAndNetworkLost() throws Exception {
+    @SmallTest
+    public void testDnsBatchAndNetworkLost() throws Exception {
         byte[] eventTypes = Arrays.copyOf(EVENT_TYPES, 20);
         byte[] returnCodes = Arrays.copyOf(RETURN_CODES, 20);
         int[] latencies = Arrays.copyOf(LATENCIES, 20);
@@ -119,13 +129,14 @@
         mCallbackCaptor.getValue().onLost(new Network(105));
         log(105, LATENCIES);
 
-        verifyLoggedEvents(
+        verifyLoggedDnsEvents(
             new DnsEvent(105, eventTypes, returnCodes, latencies),
             new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
             new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES));
     }
 
-    public void testConcurrentBatchesAndDumps() throws Exception {
+    @SmallTest
+    public void testConcurrentDnsBatchesAndDumps() throws Exception {
         final long stop = System.currentTimeMillis() + 100;
         final PrintWriter pw = new PrintWriter(new FileOutputStream("/dev/null"));
         new Thread() {
@@ -136,34 +147,132 @@
             }
         }.start();
 
-        logAsync(105, LATENCIES);
-        logAsync(106, LATENCIES);
-        logAsync(107, LATENCIES);
+        logDnsAsync(105, LATENCIES);
+        logDnsAsync(106, LATENCIES);
+        logDnsAsync(107, LATENCIES);
 
-        verifyLoggedEvents(500,
+        verifyLoggedDnsEvents(500,
             new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
             new DnsEvent(106, EVENT_TYPES, RETURN_CODES, LATENCIES),
             new DnsEvent(107, EVENT_TYPES, RETURN_CODES, LATENCIES));
     }
 
-    public void testConcurrentBatchesAndNetworkLoss() throws Exception {
-        logAsync(105, LATENCIES);
+    @SmallTest
+    public void testConcurrentDnsBatchesAndNetworkLoss() throws Exception {
+        logDnsAsync(105, LATENCIES);
         Thread.sleep(10L);
-        // call onLost() asynchronously to logAsync's onDnsEvent() calls.
+        // call onLost() asynchronously to logDnsAsync's onDnsEvent() calls.
         mCallbackCaptor.getValue().onLost(new Network(105));
 
         // do not verify unpredictable batch
         verify(mLog, timeout(500).times(1)).log(any());
     }
 
+    @SmallTest
+    public void testConnectLogging() throws Exception {
+        final int OK = 0;
+        Thread[] logActions = {
+            // ignored
+            connectEventAction(OsConstants.EALREADY, 0, EXAMPLE_IPV4),
+            connectEventAction(OsConstants.EALREADY, 0, EXAMPLE_IPV6),
+            connectEventAction(OsConstants.EINPROGRESS, 0, EXAMPLE_IPV4),
+            connectEventAction(OsConstants.EINPROGRESS, 0, EXAMPLE_IPV6),
+            connectEventAction(OsConstants.EINPROGRESS, 0, EXAMPLE_IPV6),
+            // valid latencies
+            connectEventAction(OK, 110, EXAMPLE_IPV4),
+            connectEventAction(OK, 23, EXAMPLE_IPV4),
+            connectEventAction(OK, 45, EXAMPLE_IPV4),
+            connectEventAction(OK, 56, EXAMPLE_IPV4),
+            connectEventAction(OK, 523, EXAMPLE_IPV6),
+            connectEventAction(OK, 214, EXAMPLE_IPV6),
+            connectEventAction(OK, 67, EXAMPLE_IPV6),
+            // errors
+            connectEventAction(OsConstants.EPERM, 0, EXAMPLE_IPV4),
+            connectEventAction(OsConstants.EPERM, 0, EXAMPLE_IPV4),
+            connectEventAction(OsConstants.EAGAIN, 0, EXAMPLE_IPV4),
+            connectEventAction(OsConstants.EACCES, 0, EXAMPLE_IPV4),
+            connectEventAction(OsConstants.EACCES, 0, EXAMPLE_IPV4),
+            connectEventAction(OsConstants.EACCES, 0, EXAMPLE_IPV6),
+            connectEventAction(OsConstants.EADDRINUSE, 0, EXAMPLE_IPV4),
+            connectEventAction(OsConstants.ETIMEDOUT, 0, EXAMPLE_IPV4),
+            connectEventAction(OsConstants.ETIMEDOUT, 0, EXAMPLE_IPV6),
+            connectEventAction(OsConstants.ETIMEDOUT, 0, EXAMPLE_IPV6),
+            connectEventAction(OsConstants.ECONNREFUSED, 0, EXAMPLE_IPV4),
+        };
+
+        for (Thread t : logActions) {
+            t.start();
+        }
+        for (Thread t : logActions) {
+            t.join();
+        }
+
+        List<IpConnectivityEvent> events = new ArrayList<>();
+        mNetdEventListenerService.flushStatistics(events);
+
+        IpConnectivityEvent got = events.get(0);
+        String want = joinLines(
+                "time_ms: 0",
+                "transport: 0",
+                "connect_statistics <",
+                "  connect_count: 12",
+                "  errnos_counters <",
+                "    key: 1",
+                "    value: 2",
+                "  >",
+                "  errnos_counters <",
+                "    key: 11",
+                "    value: 1",
+                "  >",
+                "  errnos_counters <",
+                "    key: 13",
+                "    value: 3",
+                "  >",
+                "  errnos_counters <",
+                "    key: 98",
+                "    value: 1",
+                "  >",
+                "  errnos_counters <",
+                "    key: 110",
+                "    value: 3",
+                "  >",
+                "  errnos_counters <",
+                "    key: 111",
+                "    value: 1",
+                "  >",
+                "  ipv6_addr_count: 6",
+                "  latencies_ms: 23",
+                "  latencies_ms: 45",
+                "  latencies_ms: 56",
+                "  latencies_ms: 67",
+                "  latencies_ms: 110",
+                "  latencies_ms: 214",
+                "  latencies_ms: 523");
+        verifyConnectEvent(want, got);
+    }
+
+    Thread connectEventAction(int error, int latencyMs, String ipAddr) {
+        return new Thread(() -> {
+            try {
+                mNetdEventListenerService.onConnectEvent(100, error, latencyMs, ipAddr, 80, 1);
+            } catch (Exception e) {
+                fail(e.toString());
+            }
+        });
+    }
+
     void log(int netId, int[] latencies) {
-        for (int l : latencies) {
-            mNetdEventListenerService.onDnsEvent(netId, EVENT_TYPE, RETURN_CODE, l, null, null, 0,
-                    0);
+        try {
+            for (int l : latencies) {
+                mNetdEventListenerService.onDnsEvent(netId, EVENT_TYPE, RETURN_CODE, l, null, null,
+                        0, 0);
+            }
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
         }
     }
 
-    void logAsync(int netId, int[] latencies) {
+    void logDnsAsync(int netId, int[] latencies) {
         new Thread() {
             public void run() {
                 log(netId, latencies);
@@ -171,15 +280,15 @@
         }.start();
     }
 
-    void verifyLoggedEvents(DnsEvent... expected) {
-        verifyLoggedEvents(0, expected);
+    void verifyLoggedDnsEvents(DnsEvent... expected) {
+        verifyLoggedDnsEvents(0, expected);
     }
 
-    void verifyLoggedEvents(int wait, DnsEvent... expectedEvents) {
-        verify(mLog, timeout(wait).times(expectedEvents.length)).log(mEvCaptor.capture());
-        for (DnsEvent got : mEvCaptor.getAllValues()) {
+    void verifyLoggedDnsEvents(int wait, DnsEvent... expectedEvents) {
+        verify(mLog, timeout(wait).times(expectedEvents.length)).log(mDnsEvCaptor.capture());
+        for (DnsEvent got : mDnsEvCaptor.getAllValues()) {
             OptionalInt index = IntStream.range(0, expectedEvents.length)
-                    .filter(i -> eventsEqual(expectedEvents[i], got))
+                    .filter(i -> dnsEventsEqual(expectedEvents[i], got))
                     .findFirst();
             // Don't match same expected event more than once.
             index.ifPresent(i -> expectedEvents[i] = null);
@@ -188,11 +297,30 @@
     }
 
     /** equality function for DnsEvent to avoid overriding equals() and hashCode(). */
-    static boolean eventsEqual(DnsEvent expected, DnsEvent got) {
+    static boolean dnsEventsEqual(DnsEvent expected, DnsEvent got) {
         return (expected == got) || ((expected != null) && (got != null)
                 && (expected.netId == got.netId)
                 && Arrays.equals(expected.eventTypes, got.eventTypes)
                 && Arrays.equals(expected.returnCodes, got.returnCodes)
                 && Arrays.equals(expected.latenciesMs, got.latenciesMs));
     }
+
+    static String joinLines(String ... elems) {
+        StringBuilder b = new StringBuilder();
+        for (String s : elems) {
+            b.append(s).append("\n");
+        }
+        return b.toString();
+    }
+
+    static void verifyConnectEvent(String expected, IpConnectivityEvent got) {
+        try {
+            Arrays.sort(got.connectStatistics.latenciesMs);
+            Arrays.sort(got.connectStatistics.errnosCounters,
+                    Comparator.comparingInt((p) -> p.key));
+            assertEquals(expected, got.toString());
+        } catch (Exception e) {
+            fail(e.toString());
+        }
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/connectivity/NetworkNotificationManagerTest.java b/services/tests/servicestests/src/com/android/server/connectivity/NetworkNotificationManagerTest.java
new file mode 100644
index 0000000..21c2de7
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/connectivity/NetworkNotificationManagerTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.connectivity;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.telephony.TelephonyManager;
+import android.test.suitebuilder.annotation.SmallTest;
+import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import junit.framework.TestCase;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.*;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class NetworkNotificationManagerTest extends TestCase {
+
+    static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities();
+    static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities();
+    static {
+        CELL_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
+        CELL_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+
+        WIFI_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
+        WIFI_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+    }
+
+    @Mock Context mCtx;
+    @Mock Resources mResources;
+    @Mock PackageManager mPm;
+    @Mock TelephonyManager mTelephonyManager;
+    @Mock NotificationManager mNotificationManager;
+    @Mock NetworkAgentInfo mWifiNai;
+    @Mock NetworkAgentInfo mCellNai;
+    @Mock NetworkInfo mNetworkInfo;
+    ArgumentCaptor<Notification> mCaptor;
+
+    NetworkNotificationManager mManager;
+
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mCaptor = ArgumentCaptor.forClass(Notification.class);
+        mWifiNai.networkCapabilities = WIFI_CAPABILITIES;
+        mWifiNai.networkInfo = mNetworkInfo;
+        mCellNai.networkCapabilities = CELL_CAPABILITIES;
+        mCellNai.networkInfo = mNetworkInfo;
+        when(mCtx.getResources()).thenReturn(mResources);
+        when(mCtx.getPackageManager()).thenReturn(mPm);
+        when(mCtx.getApplicationInfo()).thenReturn(new ApplicationInfo());
+        when(mNetworkInfo.getExtraInfo()).thenReturn("extra");
+        when(mResources.getColor(anyInt(), any())).thenReturn(0xFF607D8B);
+
+        mManager = new NetworkNotificationManager(mCtx, mTelephonyManager, mNotificationManager);
+    }
+
+    @SmallTest
+    public void testNotificationsShownAndCleared() {
+        final int NETWORK_ID_BASE = 100;
+        List<NotificationType> types = Arrays.asList(NotificationType.values());
+        List<Integer> ids = new ArrayList<>(types.size());
+        for (int i = 0; i < ids.size(); i++) {
+            ids.add(NETWORK_ID_BASE + i);
+        }
+        Collections.shuffle(ids);
+        Collections.shuffle(types);
+
+        for (int i = 0; i < ids.size(); i++) {
+            mManager.showNotification(ids.get(i), types.get(i), mWifiNai, mCellNai, null, false);
+        }
+
+        Collections.shuffle(ids);
+        for (int i = 0; i < ids.size(); i++) {
+            mManager.clearNotification(ids.get(i));
+        }
+
+        for (int i = 0; i < ids.size(); i++) {
+            final int id = ids.get(i);
+            final int eventId = types.get(i).eventId;
+            final String tag = NetworkNotificationManager.tagFor(id);
+            verify(mNotificationManager, times(1)).notifyAsUser(eq(tag), eq(eventId), any(), any());
+            verify(mNotificationManager, times(1)).cancelAsUser(eq(tag), eq(eventId), any());
+        }
+    }
+
+    @SmallTest
+    public void testNoInternetNotificationsNotShownForCellular() {
+        mManager.showNotification(100, NO_INTERNET, mCellNai, mWifiNai, null, false);
+        mManager.showNotification(101, LOST_INTERNET, mCellNai, mWifiNai, null, false);
+
+        verify(mNotificationManager, never()).notifyAsUser(any(), anyInt(), any(), any());
+
+        mManager.showNotification(102, NO_INTERNET, mWifiNai, mCellNai, null, false);
+
+        final int eventId = NO_INTERNET.eventId;
+        final String tag = NetworkNotificationManager.tagFor(102);
+        verify(mNotificationManager, times(1)).notifyAsUser(eq(tag), eq(eventId), any(), any());
+    }
+
+    @SmallTest
+    public void testNotificationsNotShownIfNoInternetCapability() {
+        mWifiNai.networkCapabilities = new NetworkCapabilities();
+        mWifiNai.networkCapabilities .addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
+        mManager.showNotification(102, NO_INTERNET, mWifiNai, mCellNai, null, false);
+        mManager.showNotification(103, LOST_INTERNET, mWifiNai, mCellNai, null, false);
+        mManager.showNotification(104, NETWORK_SWITCH, mWifiNai, mCellNai, null, false);
+
+        verify(mNotificationManager, never()).notifyAsUser(any(), anyInt(), any(), any());
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java
new file mode 100644
index 0000000..315d37c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.devicepolicy;
+
+import android.app.admin.ConnectEvent;
+import android.app.admin.DnsEvent;
+import android.os.Parcel;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import static junit.framework.Assert.assertEquals;
+
+@SmallTest
+public class NetworkEventTest extends DpmTestBase {
+
+    /**
+     * Test parceling and unparceling of a ConnectEvent.
+     */
+    public void testConnectEventParceling() {
+        ConnectEvent event = new ConnectEvent("127.0.0.1", 80, "com.android.whateverdude", 100000);
+        Parcel p = Parcel.obtain();
+        p.writeParcelable(event, 0);
+        p.setDataPosition(0);
+        ConnectEvent unparceledEvent = p.readParcelable(NetworkEventTest.class.getClassLoader());
+        p.recycle();
+        assertEquals(event.getIpAddress(), unparceledEvent.getIpAddress());
+        assertEquals(event.getPort(), unparceledEvent.getPort());
+        assertEquals(event.getPackageName(), unparceledEvent.getPackageName());
+        assertEquals(event.getTimestamp(), unparceledEvent.getTimestamp());
+    }
+
+    /**
+     * Test parceling and unparceling of a DnsEvent.
+     */
+    public void testDnsEventParceling() {
+        DnsEvent event = new DnsEvent("d.android.com", new String[]{"192.168.0.1", "127.0.0.1"}, 2,
+                "com.android.whateverdude", 100000);
+        Parcel p = Parcel.obtain();
+        p.writeParcelable(event, 0);
+        p.setDataPosition(0);
+        DnsEvent unparceledEvent = p.readParcelable(NetworkEventTest.class.getClassLoader());
+        p.recycle();
+        assertEquals(event.getHostname(), unparceledEvent.getHostname());
+        assertEquals(event.getIpAddresses()[0], unparceledEvent.getIpAddresses()[0]);
+        assertEquals(event.getIpAddresses()[1], unparceledEvent.getIpAddresses()[1]);
+        assertEquals(event.getIpAddressesCount(), unparceledEvent.getIpAddressesCount());
+        assertEquals(event.getPackageName(), unparceledEvent.getPackageName());
+        assertEquals(event.getTimestamp(), unparceledEvent.getTimestamp());
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/storage/AppCollectorTest.java b/services/tests/servicestests/src/com/android/server/storage/AppCollectorTest.java
new file mode 100644
index 0000000..da22e77
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/storage/AppCollectorTest.java
@@ -0,0 +1,201 @@
+/*
+ * 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.storage;
+
+import android.content.pm.UserInfo;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageStatsObserver;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageStats;
+import android.os.UserManager;
+import android.os.storage.VolumeInfo;
+import android.test.AndroidTestCase;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.when;
+
+@RunWith(JUnit4.class)
+public class AppCollectorTest extends AndroidTestCase {
+    private static final long TIMEOUT = TimeUnit.MINUTES.toMillis(1);
+    @Mock private Context mContext;
+    @Mock private PackageManager mPm;
+    @Mock private UserManager mUm;
+    private List<ApplicationInfo> mApps;
+    private List<UserInfo> mUsers;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+        mApps = new ArrayList<>();
+        when(mContext.getPackageManager()).thenReturn(mPm);
+        when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUm);
+
+        // Set up the app list.
+        when(mPm.getInstalledApplications(anyInt())).thenReturn(mApps);
+
+        // Set up the user list with a single user (0).
+        mUsers = new ArrayList<>();
+        mUsers.add(new UserInfo(0, "", 0));
+        when(mUm.getUsers()).thenReturn(mUsers);
+    }
+
+    @Test
+    public void testNoApps() throws Exception {
+        VolumeInfo volume = new VolumeInfo("testuuid", 0, null, null);
+        volume.fsUuid = "testuuid";
+        AppCollector collector = new AppCollector(mContext, volume);
+
+        assertThat(collector.getPackageStats(TIMEOUT)).isEmpty();
+    }
+
+    @Test
+    public void testAppOnExternalVolume() throws Exception {
+        addApplication("com.test.app", "differentuuid");
+        VolumeInfo volume = new VolumeInfo("testuuid", 0, null, null);
+        volume.fsUuid = "testuuid";
+        AppCollector collector = new AppCollector(mContext, volume);
+
+        assertThat(collector.getPackageStats(TIMEOUT)).isEmpty();
+    }
+
+    @Test
+    public void testOneValidApp() throws Exception {
+        addApplication("com.test.app", "testuuid");
+        VolumeInfo volume = new VolumeInfo("testuuid", 0, null, null);
+        volume.fsUuid = "testuuid";
+        AppCollector collector = new AppCollector(mContext, volume);
+        PackageStats stats = new PackageStats("com.test.app");
+
+        // Set up this to handle the asynchronous call to the PackageManager. This returns the
+        // package info for the specified package.
+        doAnswer(new Answer<Void>() {
+             @Override
+             public Void answer(InvocationOnMock invocation) {
+                 try {
+                     ((IPackageStatsObserver.Stub) invocation.getArguments()[2])
+                             .onGetStatsCompleted(stats, true);
+                 } catch (Exception e) {
+                     // We fail instead of just letting the exception fly because throwing
+                     // out of the callback like this on the background thread causes the test
+                     // runner to crash, rather than reporting the failure.
+                     fail();
+                 }
+                 return null;
+             }
+        }).when(mPm).getPackageSizeInfoAsUser(eq("com.test.app"), eq(0), any());
+
+
+        // Because getPackageStats is a blocking call, we block execution of the test until the
+        // call finishes. In order to finish the call, we need the above answer to execute.
+        List<PackageStats> myStats = new ArrayList<>();
+        CountDownLatch latch = new CountDownLatch(1);
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                myStats.addAll(collector.getPackageStats(TIMEOUT));
+                latch.countDown();
+            }
+        }).start();
+        latch.await();
+
+        assertThat(myStats).containsExactly(stats);
+    }
+
+    @Test
+    public void testMultipleUsersOneApp() throws Exception {
+        addApplication("com.test.app", "testuuid");
+        ApplicationInfo otherUsersApp = new ApplicationInfo();
+        otherUsersApp.packageName = "com.test.app";
+        otherUsersApp.volumeUuid = "testuuid";
+        otherUsersApp.uid = 1;
+        mUsers.add(new UserInfo(1, "", 0));
+
+        VolumeInfo volume = new VolumeInfo("testuuid", 0, null, null);
+        volume.fsUuid = "testuuid";
+        AppCollector collector = new AppCollector(mContext, volume);
+        PackageStats stats = new PackageStats("com.test.app");
+        PackageStats otherStats = new PackageStats("com.test.app");
+        otherStats.userHandle = 1;
+
+        // Set up this to handle the asynchronous call to the PackageManager. This returns the
+        // package info for our packages.
+        doAnswer(new Answer<Void>() {
+             @Override
+             public Void answer(InvocationOnMock invocation) {
+                 try {
+                     ((IPackageStatsObserver.Stub) invocation.getArguments()[2])
+                             .onGetStatsCompleted(stats, true);
+
+                     // Now callback for the other uid.
+                     ((IPackageStatsObserver.Stub) invocation.getArguments()[2])
+                             .onGetStatsCompleted(otherStats, true);
+                 } catch (Exception e) {
+                     // We fail instead of just letting the exception fly because throwing
+                     // out of the callback like this on the background thread causes the test
+                     // runner to crash, rather than reporting the failure.
+                     fail();
+                 }
+                 return null;
+             }
+        }).when(mPm).getPackageSizeInfoAsUser(eq("com.test.app"), eq(0), any());
+
+
+        // Because getPackageStats is a blocking call, we block execution of the test until the
+        // call finishes. In order to finish the call, we need the above answer to execute.
+        List<PackageStats> myStats = new ArrayList<>();
+        CountDownLatch latch = new CountDownLatch(1);
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                myStats.addAll(collector.getPackageStats(TIMEOUT));
+                latch.countDown();
+            }
+        }).start();
+        latch.await();
+
+        // This should
+        assertThat(myStats).containsAllOf(stats, otherStats);
+    }
+
+    private void addApplication(String packageName, String uuid) {
+        ApplicationInfo info = new ApplicationInfo();
+        info.packageName = packageName;
+        info.volumeUuid = uuid;
+        mApps.add(info);
+    }
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/storage/DiskStatsFileLoggerTest.java b/services/tests/servicestests/src/com/android/server/storage/DiskStatsFileLoggerTest.java
new file mode 100644
index 0000000..2aca702
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/storage/DiskStatsFileLoggerTest.java
@@ -0,0 +1,239 @@
+/*
+ * 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.storage;
+
+import android.content.pm.PackageStats;
+import android.test.AndroidTestCase;
+import android.util.ArraySet;
+import libcore.io.IoUtils;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.util.ArrayList;
+
+import static com.google.common.truth.Truth.assertThat;
+
+@RunWith(JUnit4.class)
+public class DiskStatsFileLoggerTest extends AndroidTestCase {
+    @Rule public TemporaryFolder temporaryFolder;
+    public FileCollector.MeasurementResult mMainResult;
+    public FileCollector.MeasurementResult mDownloadsResult;
+    private ArrayList<PackageStats> mPackages;
+    private File mOutputFile;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        temporaryFolder = new TemporaryFolder();
+        temporaryFolder.create();
+        mOutputFile = temporaryFolder.newFile();
+        mMainResult = new FileCollector.MeasurementResult();
+        mDownloadsResult = new FileCollector.MeasurementResult();
+        mPackages = new ArrayList<>();
+    }
+
+    @Test
+    public void testEmptyStorage() throws Exception {
+        DiskStatsFileLogger logger = new DiskStatsFileLogger(
+                mMainResult, mDownloadsResult,mPackages, 0L);
+
+        logger.dumpToFile(mOutputFile);
+
+        JSONObject output = getOutputFileAsJson();
+        assertThat(output.getLong(DiskStatsFileLogger.PHOTOS_KEY)).isEqualTo(0L);
+        assertThat(output.getLong(DiskStatsFileLogger.VIDEOS_KEY)).isEqualTo(0L);
+        assertThat(output.getLong(DiskStatsFileLogger.AUDIO_KEY)).isEqualTo(0L);
+        assertThat(output.getLong(DiskStatsFileLogger.DOWNLOADS_KEY)).isEqualTo(0L);
+        assertThat(output.getLong(DiskStatsFileLogger.SYSTEM_KEY)).isEqualTo(0L);
+        assertThat(output.getLong(DiskStatsFileLogger.MISC_KEY)).isEqualTo(0L);
+        assertThat(output.getLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY)).isEqualTo(0L);
+        assertThat(output.getLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY)).isEqualTo(0L);
+        assertThat(
+                output.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY).length()).isEqualTo(0L);
+        assertThat(output.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY).length()).isEqualTo(0L);
+        assertThat(output.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY).length()).isEqualTo(0L);
+    }
+
+    @Test
+    public void testMeasurementResultsReported() throws Exception {
+        mMainResult.audioSize = 1;
+        mMainResult.imagesSize = 10;
+        mMainResult.miscSize = 100;
+        mDownloadsResult.miscSize = 1000;
+        DiskStatsFileLogger logger = new DiskStatsFileLogger(
+                mMainResult, mDownloadsResult,mPackages, 3L);
+
+        logger.dumpToFile(mOutputFile);
+
+        JSONObject output = getOutputFileAsJson();
+        assertThat(output.getLong(DiskStatsFileLogger.AUDIO_KEY)).isEqualTo(1L);
+        assertThat(output.getLong(DiskStatsFileLogger.PHOTOS_KEY)).isEqualTo(10L);
+        assertThat(output.getLong(DiskStatsFileLogger.MISC_KEY)).isEqualTo(100L);
+        assertThat(output.getLong(DiskStatsFileLogger.DOWNLOADS_KEY)).isEqualTo(1000L);
+        assertThat(output.getLong(DiskStatsFileLogger.SYSTEM_KEY)).isEqualTo(3L);
+    }
+
+    @Test
+    public void testAppsReported() throws Exception {
+        PackageStats firstPackage = new PackageStats("com.test.app");
+        firstPackage.codeSize = 100;
+        firstPackage.dataSize = 1000;
+        firstPackage.cacheSize = 20;
+        mPackages.add(firstPackage);
+
+        PackageStats secondPackage = new PackageStats("com.test.app2");
+        secondPackage.codeSize = 10;
+        secondPackage.dataSize = 1;
+        secondPackage.cacheSize = 2;
+        mPackages.add(secondPackage);
+
+        DiskStatsFileLogger logger = new DiskStatsFileLogger(
+                mMainResult, mDownloadsResult, mPackages, 0L);
+        logger.dumpToFile(mOutputFile);
+
+        JSONObject output = getOutputFileAsJson();
+        assertThat(output.getLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY)).isEqualTo(1111);
+        assertThat(output.getLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY)).isEqualTo(22);
+
+        JSONArray packageNames = output.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY);
+        assertThat(packageNames.length()).isEqualTo(2);
+        JSONArray appSizes = output.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY);
+        assertThat(appSizes.length()).isEqualTo(2);
+        JSONArray cacheSizes = output.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY);
+        assertThat(cacheSizes.length()).isEqualTo(2);
+
+        // We need to do this crazy Set over this because the DiskStatsFileLogger provides no
+        // guarantee of the ordering of the apps in its output. By using a set, we avoid any order
+        // problems.
+        ArraySet<AppSizeGrouping> apps = new ArraySet<>();
+        for (int i = 0; i < packageNames.length(); i++) {
+            AppSizeGrouping app = new AppSizeGrouping(packageNames.getString(i),
+                    appSizes.getLong(i), cacheSizes.getLong(i));
+            apps.add(app);
+        }
+        assertThat(apps).containsAllOf(new AppSizeGrouping("com.test.app", 1100, 20),
+                new AppSizeGrouping("com.test.app2", 11, 2));
+    }
+
+    @Test
+    public void testEmulatedExternalStorageCounted() throws Exception {
+        PackageStats app = new PackageStats("com.test.app");
+        app.dataSize = 1000;
+        app.externalDataSize = 1000;
+        app.cacheSize = 20;
+        mPackages.add(app);
+
+        DiskStatsFileLogger logger = new DiskStatsFileLogger(
+                mMainResult, mDownloadsResult, mPackages, 0L);
+        logger.dumpToFile(mOutputFile);
+
+        JSONObject output = getOutputFileAsJson();
+        JSONArray appSizes = output.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY);
+        assertThat(appSizes.length()).isEqualTo(1);
+        assertThat(appSizes.getLong(0)).isEqualTo(2000);
+    }
+
+    @Test
+    public void testDuplicatePackageNameIsMergedAcrossMultipleUsers() throws Exception {
+        PackageStats app = new PackageStats("com.test.app");
+        app.dataSize = 1000;
+        app.externalDataSize = 1000;
+        app.cacheSize = 20;
+        app.userHandle = 0;
+        mPackages.add(app);
+
+        PackageStats secondApp = new PackageStats("com.test.app");
+        secondApp.dataSize = 100;
+        secondApp.externalDataSize = 100;
+        secondApp.cacheSize = 2;
+        secondApp.userHandle = 1;
+        mPackages.add(secondApp);
+
+        DiskStatsFileLogger logger = new DiskStatsFileLogger(
+                mMainResult, mDownloadsResult, mPackages, 0L);
+        logger.dumpToFile(mOutputFile);
+
+        JSONObject output = getOutputFileAsJson();
+        assertThat(output.getLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY)).isEqualTo(2200);
+        assertThat(output.getLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY)).isEqualTo(22);
+        JSONArray packageNames = output.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY);
+        assertThat(packageNames.length()).isEqualTo(1);
+        assertThat(packageNames.getString(0)).isEqualTo("com.test.app");
+
+        JSONArray appSizes = output.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY);
+        assertThat(appSizes.length()).isEqualTo(1);
+        assertThat(appSizes.getLong(0)).isEqualTo(2200);
+
+        JSONArray cacheSizes = output.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY);
+        assertThat(cacheSizes.length()).isEqualTo(1);
+        assertThat(cacheSizes.getLong(0)).isEqualTo(22);
+    }
+
+    private JSONObject getOutputFileAsJson() throws Exception {
+        return new JSONObject(IoUtils.readFileAsString(mOutputFile.getAbsolutePath()));
+    }
+
+    /**
+     * This class exists for putting zipped app size information arrays into a set for comparison
+     * purposes.
+     */
+    private class AppSizeGrouping {
+        public String packageName;
+        public long appSize;
+        public long cacheSize;
+
+        public AppSizeGrouping(String packageName, long appSize, long cacheSize) {
+            this.packageName = packageName;
+            this.appSize = appSize;
+            this.cacheSize = cacheSize;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = 17;
+            result = 37 * result + (int)(appSize ^ (appSize >>> 32));
+            result = 37 * result + (int)(cacheSize ^ (cacheSize >>> 32));
+            result = 37 * result + packageName.hashCode();
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof AppSizeGrouping)) {
+                return false;
+            }
+            if (this == o) {
+                return true;
+            }
+            AppSizeGrouping grouping = (AppSizeGrouping) o;
+            return packageName.equals(grouping.packageName) && appSize == grouping.appSize &&
+                    cacheSize == grouping.cacheSize;
+        }
+
+        @Override
+        public String toString() {
+            return packageName + " " + appSize + " " + cacheSize;
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/storage/DiskStatsLoggingServiceTest.java b/services/tests/servicestests/src/com/android/server/storage/DiskStatsLoggingServiceTest.java
new file mode 100644
index 0000000..357ce74
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/storage/DiskStatsLoggingServiceTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.storage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.when;
+
+import android.content.pm.PackageStats;
+import android.test.AndroidTestCase;
+
+import com.android.server.storage.DiskStatsLoggingService.LogRunnable;
+
+import libcore.io.IoUtils;
+
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.io.PrintStream;
+import java.util.ArrayList;
+
+@RunWith(JUnit4.class)
+public class DiskStatsLoggingServiceTest extends AndroidTestCase {
+    @Rule public TemporaryFolder mTemporaryFolder;
+    @Rule public TemporaryFolder mDownloads;
+    @Rule public TemporaryFolder mRootDirectory;
+    @Mock private AppCollector mCollector;
+    private File mInputFile;
+
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+        mTemporaryFolder = new TemporaryFolder();
+        mTemporaryFolder.create();
+        mInputFile = mTemporaryFolder.newFile();
+        mDownloads = new TemporaryFolder();
+        mDownloads.create();
+        mRootDirectory = new TemporaryFolder();
+        mRootDirectory.create();
+    }
+
+    @Test
+    public void testEmptyLog() throws Exception {
+        LogRunnable task = new LogRunnable();
+        task.setAppCollector(mCollector);
+        task.setDownloadsDirectory(mDownloads.getRoot());
+        task.setRootDirectory(mRootDirectory.getRoot());
+        task.setLogOutputFile(mInputFile);
+        task.setSystemSize(0L);
+        task.run();
+
+        JSONObject json = getJsonOutput();
+        assertThat(json.getLong(DiskStatsFileLogger.PHOTOS_KEY)).isEqualTo(0L);
+        assertThat(json.getLong(DiskStatsFileLogger.VIDEOS_KEY)).isEqualTo(0L);
+        assertThat(json.getLong(DiskStatsFileLogger.AUDIO_KEY)).isEqualTo(0L);
+        assertThat(json.getLong(DiskStatsFileLogger.DOWNLOADS_KEY)).isEqualTo(0L);
+        assertThat(json.getLong(DiskStatsFileLogger.SYSTEM_KEY)).isEqualTo(0L);
+        assertThat(json.getLong(DiskStatsFileLogger.MISC_KEY)).isEqualTo(0L);
+        assertThat(json.getLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY)).isEqualTo(0L);
+        assertThat(json.getLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY)).isEqualTo(0L);
+        assertThat(
+                json.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY).length()).isEqualTo(0L);
+        assertThat(json.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY).length()).isEqualTo(0L);
+        assertThat(json.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY).length()).isEqualTo(0L);
+    }
+
+    @Test
+    public void testPopulatedLogTask() throws Exception {
+        // Write data to directories.
+        writeDataToFile(mDownloads.newFile(), "lol");
+        writeDataToFile(mRootDirectory.newFile("test.jpg"), "1234");
+        writeDataToFile(mRootDirectory.newFile("test.mp4"), "12345");
+        writeDataToFile(mRootDirectory.newFile("test.mp3"), "123456");
+        writeDataToFile(mRootDirectory.newFile("test.whatever"), "1234567");
+
+        // Write apps.
+        ArrayList<PackageStats> apps = new ArrayList<>();
+        PackageStats testApp = new PackageStats("com.test.app");
+        testApp.dataSize = 5L;
+        testApp.cacheSize = 55L;
+        testApp.codeSize = 10L;
+        apps.add(testApp);
+        when(mCollector.getPackageStats(anyInt())).thenReturn(apps);
+
+        LogRunnable task = new LogRunnable();
+        task.setAppCollector(mCollector);
+        task.setDownloadsDirectory(mDownloads.getRoot());
+        task.setRootDirectory(mRootDirectory.getRoot());
+        task.setLogOutputFile(mInputFile);
+        task.setSystemSize(10L);
+        task.run();
+
+        JSONObject json = getJsonOutput();
+        assertThat(json.getLong(DiskStatsFileLogger.PHOTOS_KEY)).isEqualTo(4L);
+        assertThat(json.getLong(DiskStatsFileLogger.VIDEOS_KEY)).isEqualTo(5L);
+        assertThat(json.getLong(DiskStatsFileLogger.AUDIO_KEY)).isEqualTo(6L);
+        assertThat(json.getLong(DiskStatsFileLogger.DOWNLOADS_KEY)).isEqualTo(3L);
+        assertThat(json.getLong(DiskStatsFileLogger.SYSTEM_KEY)).isEqualTo(10L);
+        assertThat(json.getLong(DiskStatsFileLogger.MISC_KEY)).isEqualTo(7L);
+        assertThat(json.getLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY)).isEqualTo(15L);
+        assertThat(json.getLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY)).isEqualTo(55L);
+        assertThat(
+                json.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY).length()).isEqualTo(1L);
+        assertThat(json.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY).length()).isEqualTo(1L);
+        assertThat(json.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY).length()).isEqualTo(1L);
+    }
+
+    private void writeDataToFile(File f, String data) throws Exception{
+        PrintStream out = new PrintStream(f);
+        out.print(data);
+        out.close();
+    }
+
+    private JSONObject getJsonOutput() throws Exception {
+        return new JSONObject(IoUtils.readFileAsString(mInputFile.getAbsolutePath()));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/storage/FileCollectorTest.java b/services/tests/servicestests/src/com/android/server/storage/FileCollectorTest.java
new file mode 100644
index 0000000..f1b3442
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/storage/FileCollectorTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.storage;
+
+import android.test.AndroidTestCase;
+import com.android.server.storage.FileCollector.MeasurementResult;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.PrintStream;
+
+import static com.google.common.truth.Truth.assertThat;
+
+@RunWith(JUnit4.class)
+public class FileCollectorTest extends AndroidTestCase {
+    @Rule
+    public TemporaryFolder temporaryFolder;
+
+    @Before
+    public void setUp() throws Exception {
+        temporaryFolder = new TemporaryFolder();
+        temporaryFolder.create();
+    }
+
+    @Test
+    public void testEmpty() throws Exception {
+        MeasurementResult result = FileCollector.getMeasurementResult(temporaryFolder.getRoot());
+        assertThat(result.totalAccountedSize()).isEqualTo(0L);
+    }
+
+    @Test
+    public void testImageFile() throws Exception {
+        writeDataToFile(temporaryFolder.newFile("test.jpg"), "1234");
+
+        MeasurementResult result = FileCollector.getMeasurementResult(temporaryFolder.getRoot());
+
+        assertThat(result.imagesSize).isEqualTo(4);
+    }
+
+    @Test
+    public void testVideoFile() throws Exception {
+        writeDataToFile(temporaryFolder.newFile("test.mp4"), "1234");
+
+        MeasurementResult result = FileCollector.getMeasurementResult(temporaryFolder.getRoot());
+
+        assertThat(result.videosSize).isEqualTo(4);
+    }
+
+    @Test
+    public void testAudioFile() throws Exception {
+        writeDataToFile(temporaryFolder.newFile("test.mp3"), "1234");
+
+        MeasurementResult result = FileCollector.getMeasurementResult(temporaryFolder.getRoot());
+
+        assertThat(result.audioSize).isEqualTo(4);
+    }
+
+    @Test
+    public void testMiscFile() throws Exception {
+        writeDataToFile(temporaryFolder.newFile("test"), "1234");
+
+        MeasurementResult result = FileCollector.getMeasurementResult(temporaryFolder.getRoot());
+
+        assertThat(result.miscSize).isEqualTo(4);
+    }
+
+    @Test
+    public void testNestedFile() throws Exception {
+        File directory = temporaryFolder.newFolder();
+        writeDataToFile(new File(directory, "test"), "1234");
+
+        MeasurementResult result = FileCollector.getMeasurementResult(temporaryFolder.getRoot());
+
+        assertThat(result.miscSize).isEqualTo(4);
+    }
+
+    @Test
+    public void testMultipleFiles() throws Exception {
+        writeDataToFile(temporaryFolder.newFile("test"), "1234");
+        writeDataToFile(temporaryFolder.newFile("test2"), "12345");
+
+        MeasurementResult result = FileCollector.getMeasurementResult(temporaryFolder.getRoot());
+
+        assertThat(result.miscSize).isEqualTo(9);
+    }
+
+    @Test
+    public void testTotalSize() throws Exception {
+        writeDataToFile(temporaryFolder.newFile("test.jpg"), "1");
+        writeDataToFile(temporaryFolder.newFile("test.mp3"), "1");
+        writeDataToFile(temporaryFolder.newFile("test.mp4"), "1");
+        writeDataToFile(temporaryFolder.newFile("test"), "1");
+
+        MeasurementResult result = FileCollector.getMeasurementResult(temporaryFolder.getRoot());
+
+        assertThat(result.totalAccountedSize()).isEqualTo(4);
+    }
+
+    @Test
+    public void testFileEndsWithPeriod() throws Exception {
+        writeDataToFile(temporaryFolder.newFile("test."), "1");
+
+        MeasurementResult result = FileCollector.getMeasurementResult(temporaryFolder.getRoot());
+
+        assertThat(result.miscSize).isEqualTo(1);
+        assertThat(result.totalAccountedSize()).isEqualTo(1);
+    }
+
+    public void testIgnoreFileExtensionCase() throws Exception {
+        writeDataToFile(temporaryFolder.newFile("test.JpG"), "1234");
+
+        MeasurementResult result = FileCollector.getMeasurementResult(temporaryFolder.getRoot());
+
+        assertThat(result.imagesSize).isEqualTo(4);
+    }
+
+    private void writeDataToFile(File f, String data) throws Exception{
+        PrintStream out = new PrintStream(f);
+        out.print(data);
+        out.close();
+    }
+}
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index eb5b288..dc91bdd 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -599,6 +599,15 @@
     public static final String KEY_SUPPORT_CONFERENCE_CALL_BOOL = "support_conference_call_bool";
 
     /**
+     * Determines whether High Definition audio property is displayed in the dialer UI.
+     * If {@code false}, remove the HD audio property from the connection so that HD audio related
+     * UI is not displayed. If {@code true}, keep HD audio property as it is configured.
+     * @hide
+     */
+    public static final String KEY_DISPLAY_HD_AUDIO_PROPERTY_BOOL =
+            "display_hd_audio_property_bool";
+
+    /**
      * Determines whether video conference calls are supported by a carrier.  When {@code true},
      * video calls can be merged into conference calls, {@code false} otherwiwse.
      * <p>
@@ -1027,6 +1036,39 @@
     public static final String KEY_PREF_NETWORK_NOTIFICATION_DELAY_INT =
             "network_notification_delay_int";
 
+    /**
+     * Determine whether user edited tether APN (type dun) has effect
+     * {@code false} - Default. APN with dun type in telephony database has no effect.
+     *
+     * {@code true}  - DUN APN added/edited in ApnEditor will be used for tethering data call.
+     *
+     * @hide
+     */
+    public static final String KEY_EDITABLE_TETHER_APN_BOOL =
+            "editable_tether_apn_bool";
+
+    /**
+     * Indicates whether the carrier supports 3gpp call forwarding MMI codes while roaming. If
+     * false, the user will be notified that call forwarding is not available when the MMI code
+     * fails.
+     * @hide
+     */
+    public static final String KEY_SUPPORT_3GPP_CALL_FORWARDING_WHILE_ROAMING_BOOL =
+        "support_3gpp_call_forwarding_while_roaming_bool";
+
+    /**
+     * An array containing custom call forwarding number prefixes that will be blocked while the
+     * device is reporting that it is roaming. By default, there are no custom call
+     * forwarding prefixes and none of these numbers will be filtered. If one or more entries are
+     * present, the system will not complete the call and display an error message.
+     *
+     * To display a message to the user when call forwarding fails for 3gpp MMI codes while roaming,
+     * use the {@link #KEY_SUPPORT_3GPP_CALL_FORWARDING_WHILE_ROAMING_BOOL} option instead.
+     * @hide
+     */
+    public static final String KEY_CALL_FORWARDING_BLOCKS_WHILE_ROAMING_STRING_ARRAY =
+            "call_forwarding_blocks_while_roaming_string_array";
+
     /** The default value for every variable. */
     private final static PersistableBundle sDefaults;
 
@@ -1130,6 +1172,7 @@
         sDefaults.putInt(KEY_CDMA_DTMF_TONE_DELAY_INT, 100);
         sDefaults.putBoolean(KEY_SUPPORT_CONFERENCE_CALL_BOOL, true);
         sDefaults.putBoolean(KEY_SUPPORT_VIDEO_CONFERENCE_CALL_BOOL, false);
+        sDefaults.putBoolean(KEY_DISPLAY_HD_AUDIO_PROPERTY_BOOL, true);
         sDefaults.putBoolean(KEY_EDITABLE_ENHANCED_4G_LTE_BOOL, true);
         sDefaults.putBoolean(KEY_HIDE_IMS_APN_BOOL, false);
         sDefaults.putBoolean(KEY_HIDE_PREFERRED_NETWORK_TYPE_BOOL, false);
@@ -1207,6 +1250,9 @@
         sDefaults.putBoolean(KEY_STK_DISABLE_LAUNCH_BROWSER_BOOL, false);
         sDefaults.putStringArray(KEY_CARRIER_WIFI_STRING_ARRAY, null);
         sDefaults.putInt(KEY_PREF_NETWORK_NOTIFICATION_DELAY_INT, -1);
+        sDefaults.putBoolean(KEY_EDITABLE_TETHER_APN_BOOL, false);
+        sDefaults.putBoolean(KEY_SUPPORT_3GPP_CALL_FORWARDING_WHILE_ROAMING_BOOL, true);
+        sDefaults.putStringArray(KEY_CALL_FORWARDING_BLOCKS_WHILE_ROAMING_STRING_ARRAY, null);
     }
 
     /**
diff --git a/telephony/java/android/telephony/DisconnectCause.java b/telephony/java/android/telephony/DisconnectCause.java
index 0334254..88fd848 100644
--- a/telephony/java/android/telephony/DisconnectCause.java
+++ b/telephony/java/android/telephony/DisconnectCause.java
@@ -233,6 +233,13 @@
      */
     public static final int DIALED_ON_WRONG_SLOT = 56;
 
+    /**
+     * The call being placed was detected as a call forwarding number and was being dialed while
+     * roaming on a carrier that does not allow this.
+     * @hide
+     */
+    public static final int DIALED_CALL_FORWARDING_WHILE_ROAMING = 57;
+
     //*********************************************************************************************
     // When adding a disconnect type:
     // 1) Please assign the new type the next id value below.
@@ -241,14 +248,14 @@
     // 4) Update toString() with the newly added disconnect type.
     // 5) Update android.telecom.DisconnectCauseUtil with any mappings to a telecom.DisconnectCause.
     //
-    // NextId: 57
+    // NextId: 58
     //*********************************************************************************************
 
     /** Smallest valid value for call disconnect codes. */
     public static final int MINIMUM_VALID_VALUE = NOT_DISCONNECTED;
 
     /** Largest valid value for call disconnect codes. */
-    public static final int MAXIMUM_VALID_VALUE = DIALED_ON_WRONG_SLOT;
+    public static final int MAXIMUM_VALID_VALUE = DIALED_CALL_FORWARDING_WHILE_ROAMING;
 
     /** Private constructor to avoid class instantiation. */
     private DisconnectCause() {
@@ -370,6 +377,8 @@
             return "DATA_LIMIT_REACHED";
         case DIALED_ON_WRONG_SLOT:
             return "DIALED_ON_WRONG_SLOT";
+        case DIALED_CALL_FORWARDING_WHILE_ROAMING:
+            return "DIALED_CALL_FORWARDING_WHILE_ROAMING";
         default:
             return "INVALID: " + cause;
         }