Merge "Add Network, NetworkCapabilities, StaticIpConfiguration common test cases" into qt-dev
diff --git a/cmds/statsd/src/StatsService.cpp b/cmds/statsd/src/StatsService.cpp
index 1433252..4c97c34 100644
--- a/cmds/statsd/src/StatsService.cpp
+++ b/cmds/statsd/src/StatsService.cpp
@@ -1202,9 +1202,10 @@
     return Status::ok();
 }
 
-Status StatsService::sendBinaryPushStateChangedAtom(const android::String16& trainName,
-                                                    int64_t trainVersionCode, int options,
-                                                    int32_t state,
+Status StatsService::sendBinaryPushStateChangedAtom(const android::String16& trainNameIn,
+                                                    const int64_t trainVersionCodeIn,
+                                                    const int options,
+                                                    const int32_t state,
                                                     const std::vector<int64_t>& experimentIdsIn) {
     uid_t uid = IPCThreadState::self()->getCallingUid();
     // For testing
@@ -1224,34 +1225,64 @@
     // TODO: add verifier permission
 
     bool readTrainInfoSuccess = false;
-    InstallTrainInfo trainInfo;
-    if (trainVersionCode == -1 || experimentIdsIn.empty() || trainName.size() == 0) {
-        readTrainInfoSuccess = StorageManager::readTrainInfo(trainInfo);
-    }
+    InstallTrainInfo trainInfoOnDisk;
+    readTrainInfoSuccess = StorageManager::readTrainInfo(trainInfoOnDisk);
 
-    if (trainVersionCode == -1 && readTrainInfoSuccess) {
-        trainVersionCode = trainInfo.trainVersionCode;
+    bool resetExperimentIds = false;
+    int64_t trainVersionCode = trainVersionCodeIn;
+    std::string trainNameUtf8 = std::string(String8(trainNameIn).string());
+    if (readTrainInfoSuccess) {
+        // Keep the old train version if we received an empty version.
+        if (trainVersionCodeIn == -1) {
+            trainVersionCode = trainInfoOnDisk.trainVersionCode;
+        } else if (trainVersionCodeIn != trainInfoOnDisk.trainVersionCode) {
+        // Reset experiment ids if we receive a new non-empty train version.
+            resetExperimentIds = true;
+        }
+
+        // Keep the old train name if we received an empty train name.
+        if (trainNameUtf8.size() == 0) {
+            trainNameUtf8 = trainInfoOnDisk.trainName;
+        } else if (trainNameUtf8 != trainInfoOnDisk.trainName) {
+            // Reset experiment ids if we received a new valid train name.
+            resetExperimentIds = true;
+        }
+
+        // Reset if we received a different experiment id.
+        if (!experimentIdsIn.empty() &&
+                (trainInfoOnDisk.experimentIds.empty() ||
+                 experimentIdsIn[0] != trainInfoOnDisk.experimentIds[0])) {
+            resetExperimentIds = true;
+        }
     }
 
     // Find the right experiment IDs
     std::vector<int64_t> experimentIds;
-    if (readTrainInfoSuccess && experimentIdsIn.empty()) {
-        experimentIds = trainInfo.experimentIds;
-    } else {
+    if (resetExperimentIds || !readTrainInfoSuccess) {
         experimentIds = experimentIdsIn;
+    } else {
+        experimentIds = trainInfoOnDisk.experimentIds;
+    }
+
+    if (!experimentIds.empty()) {
+        int64_t firstId = experimentIds[0];
+        switch (state) {
+            case android::util::BINARY_PUSH_STATE_CHANGED__STATE__INSTALL_SUCCESS:
+                experimentIds.push_back(firstId + 1);
+                break;
+            case android::util::BINARY_PUSH_STATE_CHANGED__STATE__INSTALLER_ROLLBACK_INITIATED:
+                experimentIds.push_back(firstId + 2);
+                break;
+            case android::util::BINARY_PUSH_STATE_CHANGED__STATE__INSTALLER_ROLLBACK_SUCCESS:
+                experimentIds.push_back(firstId + 3);
+                break;
+        }
     }
 
     // Flatten the experiment IDs to proto
     vector<uint8_t> experimentIdsProtoBuffer;
     writeExperimentIdsToProto(experimentIds, &experimentIdsProtoBuffer);
-
-    // Find the right train name
-    std::string trainNameUtf8;
-    if (readTrainInfoSuccess && trainName.size() == 0) {
-        trainNameUtf8 = trainInfo.trainName;
-    } else {
-        trainNameUtf8 = std::string(String8(trainName).string());
-    }
+    StorageManager::writeTrainInfo(trainVersionCode, trainNameUtf8, state, experimentIds);
 
     userid_t userId = multiuser_get_user_id(uid);
     bool requiresStaging = options & IStatsManager::FLAG_REQUIRE_STAGING;
@@ -1260,7 +1291,6 @@
     LogEvent event(trainNameUtf8, trainVersionCode, requiresStaging, rollbackEnabled,
                    requiresLowLatencyMonitor, state, experimentIdsProtoBuffer, userId);
     mProcessor->OnLogEvent(&event);
-    StorageManager::writeTrainInfo(trainVersionCode, trainNameUtf8, state, experimentIds);
     return Status::ok();
 }
 
diff --git a/cmds/statsd/src/StatsService.h b/cmds/statsd/src/StatsService.h
index 929d260..0b6df8b 100644
--- a/cmds/statsd/src/StatsService.h
+++ b/cmds/statsd/src/StatsService.h
@@ -189,8 +189,11 @@
      * Binder call to log BinaryPushStateChanged atom.
      */
     virtual Status sendBinaryPushStateChangedAtom(
-            const android::String16& trainName, int64_t trainVersionCode, int options,
-            int32_t state, const std::vector<int64_t>& experimentIds) override;
+            const android::String16& trainNameIn,
+            const int64_t trainVersionCodeIn,
+            const int options,
+            const int32_t state,
+            const std::vector<int64_t>& experimentIdsIn) override;
 
     /**
      * Binder call to get registered experiment IDs.
diff --git a/cmds/statsd/src/logd/LogEvent.cpp b/cmds/statsd/src/logd/LogEvent.cpp
index ca874b5..0ade531 100644
--- a/cmds/statsd/src/logd/LogEvent.cpp
+++ b/cmds/statsd/src/logd/LogEvent.cpp
@@ -678,7 +678,8 @@
     writeFieldValueTreeToStream(mTagId, getValues(), &protoOutput);
 }
 
-void writeExperimentIdsToProto(const std::vector<int64_t>& experimentIds, std::vector<uint8_t>* protoOut) {
+void writeExperimentIdsToProto(const std::vector<int64_t>& experimentIds,
+                               std::vector<uint8_t>* protoOut) {
     ProtoOutputStream proto;
     for (const auto& expId : experimentIds) {
         proto.write(FIELD_TYPE_INT64 | FIELD_COUNT_REPEATED | FIELD_ID_EXPERIMENT_ID,
diff --git a/packages/NetworkStack/src/android/net/apf/ApfFilter.java b/packages/NetworkStack/src/android/net/apf/ApfFilter.java
index 663e2f1..359c859 100644
--- a/packages/NetworkStack/src/android/net/apf/ApfFilter.java
+++ b/packages/NetworkStack/src/android/net/apf/ApfFilter.java
@@ -39,6 +39,7 @@
 import android.content.IntentFilter;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
+import android.net.NattKeepalivePacketDataParcelable;
 import android.net.TcpKeepalivePacketDataParcelable;
 import android.net.apf.ApfGenerator.IllegalInstructionException;
 import android.net.apf.ApfGenerator.Register;
@@ -1691,13 +1692,13 @@
     }
 
     /**
-     * Add keepalive ack packet filter.
+     * Add TCP keepalive ack packet filter.
      * This will add a filter to drop acks to the keepalive packet passed as an argument.
      *
      * @param slot The index used to access the filter.
      * @param sentKeepalivePacket The attributes of the sent keepalive packet.
      */
-    public synchronized void addKeepalivePacketFilter(final int slot,
+    public synchronized void addTcpKeepalivePacketFilter(final int slot,
             final TcpKeepalivePacketDataParcelable sentKeepalivePacket) {
         log("Adding keepalive ack(" + slot + ")");
         if (null != mKeepaliveAcks.get(slot)) {
@@ -1711,6 +1712,18 @@
     }
 
     /**
+     * Add NATT keepalive packet filter.
+     * This will add a filter to drop NATT keepalive packet which is passed as an argument.
+     *
+     * @param slot The index used to access the filter.
+     * @param sentKeepalivePacket The attributes of the sent keepalive packet.
+     */
+    public synchronized void addNattKeepalivePacketFilter(final int slot,
+            final NattKeepalivePacketDataParcelable sentKeepalivePacket) {
+        Log.e(TAG, "APF add NATT keepalive filter is not implemented");
+    }
+
+    /**
      * Remove keepalive packet filter.
      *
      * @param slot The index used to access the filter.
diff --git a/packages/NetworkStack/src/android/net/ip/IpClient.java b/packages/NetworkStack/src/android/net/ip/IpClient.java
index 96e09fa..dc74c04 100644
--- a/packages/NetworkStack/src/android/net/ip/IpClient.java
+++ b/packages/NetworkStack/src/android/net/ip/IpClient.java
@@ -29,6 +29,7 @@
 import android.net.IpPrefix;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
+import android.net.NattKeepalivePacketDataParcelable;
 import android.net.NetworkStackIpMemoryStore;
 import android.net.ProvisioningConfigurationParcelable;
 import android.net.ProxyInfo;
@@ -371,6 +372,10 @@
     private boolean mMulticastFiltering;
     private long mStartTimeMillis;
 
+    /* This must match the definition in KeepaliveTracker.KeepaliveInfo */
+    private static final int TYPE_NATT = 1;
+    private static final int TYPE_TCP = 2;
+
     /**
      * Reading the snapshot is an asynchronous operation initiated by invoking
      * Callback.startReadPacketFilter() and completed when the WiFi Service responds with an
@@ -553,6 +558,11 @@
             IpClient.this.addKeepalivePacketFilter(slot, pkt);
         }
         @Override
+        public void addNattKeepalivePacketFilter(int slot, NattKeepalivePacketDataParcelable pkt) {
+            checkNetworkStackCallingPermission();
+            IpClient.this.addNattKeepalivePacketFilter(slot, pkt);
+        }
+        @Override
         public void removeKeepalivePacketFilter(int slot) {
             checkNetworkStackCallingPermission();
             IpClient.this.removeKeepalivePacketFilter(slot);
@@ -691,11 +701,20 @@
     }
 
     /**
-     * Called by WifiStateMachine to add keepalive packet filter before setting up
+     * Called by WifiStateMachine to add TCP keepalive packet filter before setting up
      * keepalive offload.
      */
     public void addKeepalivePacketFilter(int slot, @NonNull TcpKeepalivePacketDataParcelable pkt) {
-        sendMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER_TO_APF, slot, 0 /* Unused */, pkt);
+        sendMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER_TO_APF, slot, TYPE_TCP, pkt);
+    }
+
+    /**
+     *  Called by WifiStateMachine to add NATT keepalive packet filter before setting up
+     *  keepalive offload.
+     */
+    public void addNattKeepalivePacketFilter(int slot,
+            @NonNull NattKeepalivePacketDataParcelable pkt) {
+        sendMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER_TO_APF, slot, TYPE_NATT, pkt);
     }
 
     /**
@@ -1607,9 +1626,16 @@
 
                 case CMD_ADD_KEEPALIVE_PACKET_FILTER_TO_APF: {
                     final int slot = msg.arg1;
+                    final int type = msg.arg2;
+
                     if (mApfFilter != null) {
-                        mApfFilter.addKeepalivePacketFilter(slot,
-                                (TcpKeepalivePacketDataParcelable) msg.obj);
+                        if (type == TYPE_NATT) {
+                            mApfFilter.addNattKeepalivePacketFilter(slot,
+                                    (NattKeepalivePacketDataParcelable) msg.obj);
+                        } else {
+                            mApfFilter.addTcpKeepalivePacketFilter(slot,
+                                    (TcpKeepalivePacketDataParcelable) msg.obj);
+                        }
                     }
                     break;
                 }
diff --git a/packages/NetworkStack/tests/src/android/net/apf/ApfTest.java b/packages/NetworkStack/tests/src/android/net/apf/ApfTest.java
index a0e508f..93ab3be 100644
--- a/packages/NetworkStack/tests/src/android/net/apf/ApfTest.java
+++ b/packages/NetworkStack/tests/src/android/net/apf/ApfTest.java
@@ -1553,7 +1553,7 @@
         parcel.seq = seqNum;
         parcel.ack = ackNum;
 
-        apfFilter.addKeepalivePacketFilter(slot1, parcel);
+        apfFilter.addTcpKeepalivePacketFilter(slot1, parcel);
         program = cb.getApfProgram();
 
         // Verify IPv4 keepalive ack packet is dropped
@@ -1592,7 +1592,7 @@
             ipv6Parcel.seq = seqNum;
             ipv6Parcel.ack = ackNum;
 
-            apfFilter.addKeepalivePacketFilter(slot1, ipv6Parcel);
+            apfFilter.addTcpKeepalivePacketFilter(slot1, ipv6Parcel);
             program = cb.getApfProgram();
 
             // Verify IPv6 keepalive ack packet is dropped
@@ -1614,8 +1614,8 @@
             apfFilter.removeKeepalivePacketFilter(slot1);
 
             // Verify multiple filters
-            apfFilter.addKeepalivePacketFilter(slot1, parcel);
-            apfFilter.addKeepalivePacketFilter(slot2, ipv6Parcel);
+            apfFilter.addTcpKeepalivePacketFilter(slot1, parcel);
+            apfFilter.addTcpKeepalivePacketFilter(slot2, ipv6Parcel);
             program = cb.getApfProgram();
 
             // Verify IPv4 keepalive ack packet is dropped
diff --git a/packages/SystemUI/res/layout/contextual.xml b/packages/SystemUI/res/layout/contextual.xml
index 9b6ccae..90a7768 100644
--- a/packages/SystemUI/res/layout/contextual.xml
+++ b/packages/SystemUI/res/layout/contextual.xml
@@ -42,16 +42,10 @@
         android:layout_height="match_parent"
         android:visibility="invisible"
     />
-    <com.android.systemui.statusbar.policy.KeyButtonView
-        android:id="@+id/rotate_suggestion"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_weight="0"
-        android:scaleType="center"
-        android:visibility="invisible"
-        android:contentDescription="@string/accessibility_rotate_button"
-        android:paddingStart="@dimen/navigation_key_padding"
-        android:paddingEnd="@dimen/navigation_key_padding"
+    <include layout="@layout/rotate_suggestion"
+             android:layout_width="match_parent"
+             android:layout_height="match_parent"
+             android:visibility="invisible"
     />
     <com.android.systemui.statusbar.policy.KeyButtonView
         android:id="@+id/accessibility_button"
diff --git a/packages/SystemUI/res/layout/rotate_suggestion.xml b/packages/SystemUI/res/layout/rotate_suggestion.xml
new file mode 100644
index 0000000..d7f67db
--- /dev/null
+++ b/packages/SystemUI/res/layout/rotate_suggestion.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2019 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.policy.KeyButtonView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/rotate_suggestion"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_weight="0"
+    android:scaleType="center"
+    android:visibility="invisible"
+    android:contentDescription="@string/accessibility_rotate_button"
+    android:paddingStart="@dimen/navigation_key_padding"
+    android:paddingEnd="@dimen/navigation_key_padding"
+/>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/start_contextual.xml b/packages/SystemUI/res/layout/start_contextual.xml
new file mode 100644
index 0000000..e022c73
--- /dev/null
+++ b/packages/SystemUI/res/layout/start_contextual.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+             xmlns:systemui="http://schemas.android.com/apk/res-auto"
+             android:id="@+id/start_menu_container"
+             android:layout_width="@dimen/navigation_key_width"
+             android:layout_height="match_parent"
+             android:importantForAccessibility="no"
+             android:focusable="false"
+             android:clipChildren="false"
+             android:clipToPadding="false"
+             >
+    <include layout="@layout/rotate_suggestion"
+             android:layout_width="match_parent"
+             android:layout_height="match_parent"
+             android:visibility="invisible"
+    />
+    <include layout="@layout/back"
+             android:layout_width="match_parent"
+             android:layout_height="match_parent"
+             android:visibility="invisible"
+    />
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 7338687..4abe9f0 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -331,7 +331,7 @@
     <!-- Nav bar button default ordering/layout -->
     <string name="config_navBarLayout" translatable="false">left[.5W],back[1WC];home;recent[1WC],right[.5W]</string>
     <string name="config_navBarLayoutQuickstep" translatable="false">back[1.7WC];home;contextual[1.7WC]</string>
-    <string name="config_navBarLayoutHandle" translatable="false">back[1.7WC];home_handle;ime_switcher[1.7WC]</string>
+    <string name="config_navBarLayoutHandle" translatable="false">start_contextual[.1WC];home_handle;ime_switcher[.1WC]</string>
 
     <bool name="quick_settings_show_full_alarm">false</bool>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index acc03c4..60d7126 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1345,6 +1345,7 @@
     <!-- Screen pinning dialog description. -->
     <string name="screen_pinning_description">This keeps it in view until you unpin. Touch &amp; hold Back and Overview to unpin.</string>
     <string name="screen_pinning_description_recents_invisible">This keeps it in view until you unpin. Touch &amp; hold Back and Home to unpin.</string>
+    <string name="screen_pinning_description_gestural">This keeps it in view until you unpin. Swipe up &amp; hold to unpin.</string>
     <!-- Screen pinning dialog description. -->
     <string name="screen_pinning_description_accessible">This keeps it in view until you unpin. Touch &amp; hold Overview to unpin.</string>
     <string name="screen_pinning_description_recents_invisible_accessible">This keeps it in view until you unpin. Touch &amp; hold Home to unpin.</string>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
index 6f44623..2c5fa60 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
@@ -96,4 +96,9 @@
      * Notifies that the accessibility button in the system's navigation area has been long clicked
      */
     void notifyAccessibilityButtonLongClicked() = 16;
+
+    /**
+     * Ends the system screen pinning.
+     */
+    void stopScreenPinning() = 17;
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 3ace705..5ed6a42 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -35,6 +35,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 
 import android.annotation.FloatRange;
+import android.app.ActivityTaskManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -151,6 +152,25 @@
         }
 
         @Override
+        public void stopScreenPinning() {
+            if (!verifyCaller("stopScreenPinning")) {
+                return;
+            }
+            long token = Binder.clearCallingIdentity();
+            try {
+                mHandler.post(() -> {
+                    try {
+                        ActivityTaskManager.getService().stopSystemLockTaskMode();
+                    } catch (RemoteException e) {
+                        Log.e(TAG_OPS, "Failed to stop screen pinning");
+                    }
+                });
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override
         public void onStatusBarMotionEvent(MotionEvent event) {
             if (!verifyCaller("onStatusBarMotionEvent")) {
                 return;
diff --git a/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java
index 07391ed..ade903d 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java
@@ -48,14 +48,17 @@
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.WindowManagerWrapper;
 import com.android.systemui.statusbar.phone.NavigationBarView;
+import com.android.systemui.statusbar.phone.NavigationModeController;
 import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.util.leak.RotationUtils;
 
 import java.util.ArrayList;
 
-public class ScreenPinningRequest implements View.OnClickListener {
+public class ScreenPinningRequest implements View.OnClickListener,
+        NavigationModeController.ModeChangedListener {
 
     private final Context mContext;
 
@@ -64,6 +67,7 @@
     private final OverviewProxyService mOverviewProxyService;
 
     private RequestWindowView mRequestWindow;
+    private int mNavBarMode;
 
     // Id of task to be pinned or locked.
     private int taskId;
@@ -75,6 +79,7 @@
         mWindowManager = (WindowManager)
                 mContext.getSystemService(Context.WINDOW_SERVICE);
         mOverviewProxyService = Dependency.get(OverviewProxyService.class);
+        mNavBarMode = Dependency.get(NavigationModeController.class).addListener(this);
     }
 
     public void clearPrompt() {
@@ -103,6 +108,11 @@
         mWindowManager.addView(mRequestWindow, lp);
     }
 
+    @Override
+    public void onNavigationModeChanged(int mode) {
+        mNavBarMode = mode;
+    }
+
     public void onConfigurationChanged() {
         if (mRequestWindow != null) {
             mRequestWindow.onConfigurationChanged();
@@ -224,7 +234,9 @@
             mLayout.findViewById(R.id.screen_pinning_text_area)
                     .setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE);
             View buttons = mLayout.findViewById(R.id.screen_pinning_buttons);
-            if (WindowManagerWrapper.getInstance().hasSoftNavigationBar(mContext.getDisplayId())) {
+            WindowManagerWrapper wm = WindowManagerWrapper.getInstance();
+            if (!QuickStepContract.isGesturalMode(mNavBarMode) 
+            	    && wm.hasSoftNavigationBar(mContext.getDisplayId())) {
                 buttons.setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE);
                 swapChildrenIfRtlAndVertical(buttons);
             } else {
@@ -248,7 +260,9 @@
                     && navigationBarView.isRecentsButtonVisible();
             boolean touchExplorationEnabled = mAccessibilityService.isTouchExplorationEnabled();
             int descriptionStringResId;
-            if (recentsVisible) {
+            if (QuickStepContract.isGesturalMode(mNavBarMode)) {
+                descriptionStringResId = R.string.screen_pinning_description_gestural;
+            } else if (recentsVisible) {
                 mLayout.findViewById(R.id.screen_pinning_recents_group).setVisibility(VISIBLE);
                 mLayout.findViewById(R.id.screen_pinning_home_bg_light).setVisibility(INVISIBLE);
                 mLayout.findViewById(R.id.screen_pinning_home_bg).setVisibility(INVISIBLE);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java
index 5f5fad3..6a93c7c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java
@@ -47,7 +47,7 @@
     private Boolean mLongClickable;
     private Float mAlpha;
     private Float mDarkIntensity;
-    private Integer mVisibility = -1;
+    private Integer mVisibility = View.VISIBLE;
     private Boolean mDelayTouchFeedback;
     private KeyButtonDrawable mImageDrawable;
     private View mCurrentView;
@@ -86,7 +86,7 @@
         if (mAlpha != null) {
             view.setAlpha(mAlpha);
         }
-        if (mVisibility != null && mVisibility != -1) {
+        if (mVisibility != null) {
             view.setVisibility(mVisibility);
         }
         if (mAccessibilityDelegate != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ContextualButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ContextualButton.java
index 541c142..5bc17f5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ContextualButton.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ContextualButton.java
@@ -51,7 +51,7 @@
      * Reload the drawable from resource id, should reapply the previous dark intensity.
      */
     public void updateIcon() {
-        if (getCurrentView() == null || !getCurrentView().isAttachedToWindow()) {
+        if (getCurrentView() == null || !getCurrentView().isAttachedToWindow() || mIconResId == 0) {
             return;
         }
         final KeyButtonDrawable currentDrawable = getImageDrawable();
@@ -92,7 +92,7 @@
             setVisibility(View.VISIBLE);
             return true;
         }
-        return mGroup.setButtonVisiblity(getId(), true /* visible */) == View.VISIBLE;
+        return mGroup.setButtonVisibility(getId(), true /* visible */) == View.VISIBLE;
     }
 
     /**
@@ -104,7 +104,7 @@
             setVisibility(View.INVISIBLE);
             return false;
         }
-        return mGroup.setButtonVisiblity(getId(), false /* visible */) != View.VISIBLE;
+        return mGroup.setButtonVisibility(getId(), false /* visible */) != View.VISIBLE;
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ContextualButtonGroup.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ContextualButtonGroup.java
index 02b660f..9e843f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ContextualButtonGroup.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ContextualButtonGroup.java
@@ -37,7 +37,7 @@
     /**
      * Add a contextual button to the group. The order of adding increases in its priority. The
      * priority is used to determine which button should be visible when setting multiple button's
-     * visibility {@see setButtonVisiblity}.
+     * visibility {@see setButtonVisibility}.
      * @param button the button added to the group
      */
     public void addButton(@NonNull ContextualButton button) {
@@ -71,7 +71,7 @@
      * @return if the button is visible after operation
      * @throws RuntimeException if the input id does not match any of the ids in the group
      */
-    public int setButtonVisiblity(@IdRes int buttonResId, boolean visible) {
+    public int setButtonVisibility(@IdRes int buttonResId, boolean visible) {
         final int index = getContextButtonIndex(buttonResId);
         if (index == INVALID_INDEX) {
             throw new RuntimeException("Cannot find the button id of " + buttonResId
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
index 404c07b..963fc54 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
@@ -71,6 +71,7 @@
     public static final String RIGHT = "right";
     public static final String CONTEXTUAL = "contextual";
     public static final String IME_SWITCHER = "ime_switcher";
+    public static final String START_CONTEXTUAL = "start_contextual";
 
     public static final String GRAVITY_SEPARATOR = ";";
     public static final String BUTTON_SEPARATOR = ",";
@@ -419,6 +420,8 @@
             v = inflater.inflate(R.layout.home_handle, parent, false);
         } else if (IME_SWITCHER.equals(button)) {
             v = inflater.inflate(R.layout.ime_switcher, parent, false);
+        } else if (START_CONTEXTUAL.equals(button)) {
+            v = inflater.inflate(R.layout.start_contextual, parent, false);
         } else if (button.startsWith(KEY)) {
             String uri = extractImage(button);
             int code = extractKeycode(button);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index 6f1e161..4333200 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -49,6 +49,8 @@
 import android.view.Surface;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewTreeObserver.InternalInsetsInfo;
+import android.view.ViewTreeObserver.OnComputeInternalInsetsListener;
 import android.view.WindowInsets;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
@@ -135,6 +137,7 @@
     private boolean mImeVisible;
 
     private final SparseArray<ButtonDispatcher> mButtonDispatchers = new SparseArray<>();
+    private final ContextualButtonGroup mStartContextualButtonGroup;
     private final ContextualButtonGroup mContextualButtonGroup;
     private Configuration mConfiguration;
     private Configuration mTmpLastConfiguration;
@@ -233,11 +236,36 @@
         }
     };
 
+    private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener = info -> {
+        // When the nav bar is in 2-button or 3-button mode, or when IME is visible in fully
+        // gestural mode, the entire nav bar should be touchable.
+        if (!QuickStepContract.isGesturalMode(mNavBarMode) || mImeVisible) {
+            info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
+            return;
+        }
+        info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+        RotationContextButton rotationContextButton = getRotateSuggestionButton();
+        // If the rotate suggestion button is not visible in fully gestural mode, the entire nav bar
+        // is not touchable so that the app underneath can be clicked through.
+        if (rotationContextButton.getVisibility() != VISIBLE) {
+            info.touchableRegion.setEmpty();
+        } else {
+            // Set the rotate suggestion button area to be touchable.
+            rotationContextButton.getCurrentView().getLocationInWindow(mTmpPosition);
+            Rect rect = new Rect(mTmpPosition[0], mTmpPosition[1],
+                    mTmpPosition[0] + mRotationButtonBounds.width(),
+                    mTmpPosition[1] + mRotationButtonBounds.height());
+            info.touchableRegion.union(rect);
+        }
+    };
+
     public NavigationBarView(Context context, AttributeSet attrs) {
         super(context, attrs);
 
         mIsVertical = false;
         mLongClickableAccessibilityButton = false;
+        mNavBarMode = Dependency.get(NavigationModeController.class).addListener(this);
+        boolean isGesturalMode = QuickStepContract.isGesturalMode(mNavBarMode);
 
         // Set up the context group of buttons
         mContextualButtonGroup = new ContextualButtonGroup(R.id.menu_container);
@@ -253,12 +281,21 @@
                         R.drawable.ic_sysbar_accessibility_button);
         mContextualButtonGroup.addButton(menuButton);
         mContextualButtonGroup.addButton(imeSwitcherButton);
-        mContextualButtonGroup.addButton(rotateSuggestionButton);
+        if (!isGesturalMode) {
+            mContextualButtonGroup.addButton(rotateSuggestionButton);
+        }
         mContextualButtonGroup.addButton(accessibilityButton);
 
         mOverviewProxyService = Dependency.get(OverviewProxyService.class);
         mRecentsOnboarding = new RecentsOnboarding(context, mOverviewProxyService);
 
+        final ContextualButton backButton = new ContextualButton(R.id.back, 0);
+        mStartContextualButtonGroup = new ContextualButtonGroup(R.id.start_menu_container);
+        if (isGesturalMode) {
+            mStartContextualButtonGroup.addButton(rotateSuggestionButton);
+        }
+        mStartContextualButtonGroup.addButton(backButton);
+
         mConfiguration = new Configuration();
         mTmpLastConfiguration = new Configuration();
         mConfiguration.updateFrom(context.getResources().getConfiguration());
@@ -266,7 +303,7 @@
         mScreenPinningNotify = new ScreenPinningNotify(mContext);
         mBarTransitions = new NavigationBarTransitions(this);
 
-        mButtonDispatchers.put(R.id.back, new ButtonDispatcher(R.id.back));
+        mButtonDispatchers.put(R.id.back, backButton);
         mButtonDispatchers.put(R.id.home, new ButtonDispatcher(R.id.home));
         mButtonDispatchers.put(R.id.home_handle, new ButtonDispatcher(R.id.home_handle));
         mButtonDispatchers.put(R.id.recent_apps, new ButtonDispatcher(R.id.recent_apps));
@@ -275,6 +312,7 @@
         mButtonDispatchers.put(R.id.accessibility_button, accessibilityButton);
         mButtonDispatchers.put(R.id.rotate_suggestion, rotateSuggestionButton);
         mButtonDispatchers.put(R.id.menu_container, mContextualButtonGroup);
+        mButtonDispatchers.put(R.id.start_menu_container, mStartContextualButtonGroup);
         mDeadZone = new DeadZone(this);
 
         mEdgeBackGestureHandler = new EdgeBackGestureHandler(context, mOverviewProxyService);
@@ -390,8 +428,11 @@
     }
 
     public RotationContextButton getRotateSuggestionButton() {
-        return (RotationContextButton) mContextualButtonGroup
-                .getContextButton(R.id.rotate_suggestion);
+        return (RotationContextButton) mButtonDispatchers.get(R.id.rotate_suggestion);
+    }
+
+    public ContextualButtonGroup getStartContextualButtonGroup() {
+        return mStartContextualButtonGroup;
     }
 
     public ButtonDispatcher getHomeHandle() {
@@ -430,6 +471,7 @@
         if (densityChange || dirChange) {
             mRecentIcon = getDrawable(R.drawable.ic_sysbar_recent);
             mContextualButtonGroup.updateIcons();
+            mStartContextualButtonGroup.updateIcons();
         }
         if (orientationChange || densityChange || dirChange) {
             mBackIcon = getBackDrawable();
@@ -437,12 +479,16 @@
     }
 
     public KeyButtonDrawable getBackDrawable() {
-        KeyButtonDrawable drawable = chooseNavigationIconDrawable(R.drawable.ic_sysbar_back,
-                R.drawable.ic_sysbar_back_quick_step);
+        KeyButtonDrawable drawable = getDrawable(getBackDrawableRes());
         orientBackButton(drawable);
         return drawable;
     }
 
+    public @DrawableRes int getBackDrawableRes() {
+        return chooseNavigationIconDrawableRes(R.drawable.ic_sysbar_back,
+                R.drawable.ic_sysbar_back_quick_step);
+    }
+
     public KeyButtonDrawable getHomeDrawable() {
         final boolean quickStepEnabled = mOverviewProxyService.shouldShowSwipeUpUI();
         KeyButtonDrawable drawable = quickStepEnabled
@@ -485,8 +531,13 @@
 
     private KeyButtonDrawable chooseNavigationIconDrawable(@DrawableRes int icon,
             @DrawableRes int quickStepIcon) {
+        return getDrawable(chooseNavigationIconDrawableRes(icon, quickStepIcon));
+    }
+
+    private @DrawableRes int chooseNavigationIconDrawableRes(@DrawableRes int icon,
+            @DrawableRes int quickStepIcon) {
         final boolean quickStepEnabled = mOverviewProxyService.shouldShowSwipeUpUI();
-        return quickStepEnabled ? getDrawable(quickStepIcon) : getDrawable(icon);
+        return quickStepEnabled ? quickStepIcon : icon;
     }
 
     private KeyButtonDrawable getDrawable(@DrawableRes int icon) {
@@ -527,7 +578,6 @@
             mTransitionListener.onBackAltCleared();
         }
         mImeVisible = visible;
-        updateWindowTouchable();
     }
 
     public void setDisabledFlags(int disabledFlags) {
@@ -564,7 +614,7 @@
         updateRecentsIcon();
 
         // Update IME button visibility, a11y and rotate button always overrides the appearance
-        mContextualButtonGroup.setButtonVisiblity(R.id.ime_switcher,
+        mContextualButtonGroup.setButtonVisibility(R.id.ime_switcher,
                 (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) != 0);
 
         mBarTransitions.reapplyDarkIntensity();
@@ -609,6 +659,7 @@
         }
 
         getBackButton().setVisibility(disableBack      ? View.INVISIBLE : View.VISIBLE);
+        mStartContextualButtonGroup.setButtonVisibility(R.id.back, !disableBack);
         getHomeButton().setVisibility(disableHome      ? View.INVISIBLE : View.VISIBLE);
         getRecentsButton().setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE);
     }
@@ -714,11 +765,6 @@
         setWindowFlag(WindowManager.LayoutParams.FLAG_SLIPPERY, slippery);
     }
 
-    public void updateWindowTouchable() {
-        boolean touchable = mImeVisible || !QuickStepContract.isGesturalMode(mNavBarMode);
-        setWindowFlag(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, !touchable);
-    }
-
     private void setWindowFlag(int flags, boolean enable) {
         final ViewGroup navbarView = ((ViewGroup) getParent());
         if (navbarView == null) {
@@ -743,6 +789,7 @@
         mBarTransitions.onNavigationModeChanged(mNavBarMode);
         mEdgeBackGestureHandler.onNavigationModeChanged(mNavBarMode);
         mRecentsOnboarding.onNavigationModeChanged(mNavBarMode);
+        getRotateSuggestionButton().onNavigationModeChanged(mNavBarMode);
 
         // Color adaption is tied with showing home handle, only avaliable if visible
         mTintController.onNavigationModeChanged(mNavBarMode);
@@ -751,17 +798,16 @@
         } else {
             mTintController.stop();
         }
-        updateWindowTouchable();
     }
 
     public void setMenuVisibility(final boolean show) {
-        mContextualButtonGroup.setButtonVisiblity(R.id.menu, show);
+        mContextualButtonGroup.setButtonVisibility(R.id.menu, show);
     }
 
     public void setAccessibilityButtonState(final boolean visible, final boolean longClickable) {
         mLongClickableAccessibilityButton = longClickable;
         getAccessibilityButton().setLongClickable(longClickable);
-        mContextualButtonGroup.setButtonVisiblity(R.id.accessibility_button, visible);
+        mContextualButtonGroup.setButtonVisibility(R.id.accessibility_button, visible);
     }
 
     void hideRecentsOnboarding() {
@@ -1044,12 +1090,11 @@
         onPluginDisconnected(null); // Create default gesture helper
         Dependency.get(PluginManager.class).addPluginListener(this,
                 NavGesture.class, false /* Only one */);
-        int navBarMode = Dependency.get(NavigationModeController.class).addListener(this);
-        onNavigationModeChanged(navBarMode);
+        onNavigationModeChanged(mNavBarMode);
         setUpSwipeUpOnboarding(isQuickStepSwipeUpEnabled());
 
         mEdgeBackGestureHandler.onNavBarAttached();
-        updateWindowTouchable();
+        getViewTreeObserver().addOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener);
     }
 
     @Override
@@ -1065,6 +1110,8 @@
             mButtonDispatchers.valueAt(i).onDestroy();
         }
         mEdgeBackGestureHandler.onNavBarDetached();
+        getViewTreeObserver().removeOnComputeInternalInsetsListener(
+                mOnComputeInternalInsetsListener);
     }
 
     private void setUpSwipeUpOnboarding(boolean connectedToOverviewProxy) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationContextButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationContextButton.java
index e887f5b..7203e57 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationContextButton.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationContextButton.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON;
+
 import static com.android.internal.view.RotationPolicy.NATURAL_ROTATION;
 
 import android.animation.Animator;
@@ -45,6 +47,7 @@
 import com.android.systemui.Interpolators;
 import com.android.systemui.R;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.statusbar.policy.KeyButtonDrawable;
 import com.android.systemui.statusbar.policy.RotationLockController;
@@ -52,7 +55,9 @@
 import java.util.Optional;
 import java.util.function.Consumer;
 
-public class RotationContextButton extends ContextualButton {
+/** Containing logic for the rotation button in nav bar. */
+public class RotationContextButton extends ContextualButton implements
+        NavigationModeController.ModeChangedListener {
     public static final boolean DEBUG_ROTATION = false;
 
     private static final int BUTTON_FADE_IN_OUT_DURATION_MS = 100;
@@ -76,6 +81,7 @@
             () -> mPendingRotationSuggestion = false;
     private Animator mRotateHideAnimator;
     private boolean mAccessibilityFeedbackEnabled;
+    private int mNavBarMode = NAV_BAR_MODE_3BUTTON;
 
     private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
     private final ViewRippler mViewRippler = new ViewRippler();
@@ -304,7 +310,8 @@
     @Override
     protected KeyButtonDrawable getNewDrawable() {
         Context context = new ContextThemeWrapper(getContext().getApplicationContext(), mStyleRes);
-        return KeyButtonDrawable.create(context, mIconResId, false /* shadow */);
+        return KeyButtonDrawable.create(context, mIconResId, false /* shadow */,
+                QuickStepContract.isGesturalMode(mNavBarMode));
     }
 
     @Override
@@ -390,9 +397,9 @@
     }
 
     private int computeRotationProposalTimeout() {
-        if (mAccessibilityFeedbackEnabled) return 20000;
-        if (mHoveringRotationSuggestion) return 16000;
-        return 10000;
+        if (mAccessibilityFeedbackEnabled) return 10000;
+        if (mHoveringRotationSuggestion) return 8000;
+        return 5000;
     }
 
     private boolean isRotateSuggestionIntroduced() {
@@ -414,6 +421,11 @@
         }
     }
 
+    @Override
+    public void onNavigationModeChanged(int mode) {
+        mNavBarMode = mode;
+    }
+
     private class TaskStackListenerImpl extends TaskStackChangeListener {
         // Invalidate any rotation suggestion on task change or activity orientation change
         // Note: all callbacks happen on main thread
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonDrawable.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonDrawable.java
index dd0c344..2bfc311 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonDrawable.java
@@ -30,9 +30,11 @@
 import android.graphics.ColorFilter;
 import android.graphics.Paint;
 import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
 import android.graphics.PorterDuff.Mode;
 import android.graphics.PorterDuffColorFilter;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.Drawable;
 import android.util.FloatProperty;
@@ -77,13 +79,14 @@
 
     private final Paint mIconPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
     private final Paint mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
+    private final Paint mOvalBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
     private final ShadowDrawableState mState;
     private AnimatedVectorDrawable mAnimatedDrawable;
 
     public KeyButtonDrawable(Drawable d, @ColorInt int lightColor, @ColorInt int darkColor,
-            boolean horizontalFlip) {
+            boolean horizontalFlip, boolean hasOvalBg) {
         this(d, new ShadowDrawableState(lightColor, darkColor,
-                d instanceof AnimatedVectorDrawable, horizontalFlip));
+                d instanceof AnimatedVectorDrawable, horizontalFlip, hasOvalBg));
     }
 
     private KeyButtonDrawable(Drawable d, ShadowDrawableState state) {
@@ -98,6 +101,7 @@
             mAnimatedDrawable = (AnimatedVectorDrawable) mState.mChildState.newDrawable().mutate();
             setDrawableBounds(mAnimatedDrawable);
         }
+        mOvalBgPaint.setColor(mState.mDarkColor);
     }
 
     public void setDarkIntensity(float intensity) {
@@ -165,7 +169,12 @@
     public void setColorFilter(ColorFilter colorFilter) {
         mIconPaint.setColorFilter(colorFilter);
         if (mAnimatedDrawable != null) {
-            mAnimatedDrawable.setColorFilter(colorFilter);
+            if (mState.mHasOvalBg) {
+                mAnimatedDrawable.setColorFilter(
+                        new PorterDuffColorFilter(mState.mLightColor, PorterDuff.Mode.SRC_IN));
+            } else {
+                mAnimatedDrawable.setColorFilter(colorFilter);
+            }
         }
         invalidateSelf();
     }
@@ -235,6 +244,10 @@
             return;
         }
 
+        if (mState.mHasOvalBg) {
+            canvas.drawOval(new RectF(bounds), mOvalBgPaint);
+        }
+
         if (mAnimatedDrawable != null) {
             mAnimatedDrawable.draw(canvas);
         } else {
@@ -379,14 +392,16 @@
         final int mLightColor;
         final int mDarkColor;
         final boolean mSupportsAnimation;
+        final boolean mHasOvalBg;
 
         public ShadowDrawableState(@ColorInt int lightColor, @ColorInt int darkColor,
-                boolean animated, boolean horizontalFlip) {
+                boolean animated, boolean horizontalFlip, boolean hasOvalBg) {
             mLightColor = lightColor;
             mDarkColor = darkColor;
             mSupportsAnimation = animated;
             mAlpha = 255;
             mHorizontalFlip = horizontalFlip;
+            mHasOvalBg = hasOvalBg;
         }
 
         @Override
@@ -411,32 +426,51 @@
      * @param ctx Context to get the drawable and determine the dark and light theme
      * @param icon the icon resource id
      * @param hasShadow if a shadow will appear with the drawable
+     * @param hasOvalBg if an oval bg will be drawn
      * @return KeyButtonDrawable
      */
     public static KeyButtonDrawable create(@NonNull Context ctx, @DrawableRes int icon,
-            boolean hasShadow) {
+            boolean hasShadow, boolean hasOvalBg) {
         final int dualToneDarkTheme = Utils.getThemeAttr(ctx, R.attr.darkIconTheme);
         final int dualToneLightTheme = Utils.getThemeAttr(ctx, R.attr.lightIconTheme);
         Context lightContext = new ContextThemeWrapper(ctx, dualToneLightTheme);
         Context darkContext = new ContextThemeWrapper(ctx, dualToneDarkTheme);
-        return KeyButtonDrawable.create(lightContext, darkContext, icon, hasShadow);
+        return KeyButtonDrawable.create(lightContext, darkContext, icon, hasShadow, hasOvalBg);
     }
 
+    /**
+     * Creates a KeyButtonDrawable with a shadow given its icon. For more information, see
+     * {@link #create(Context, int, boolean, boolean)}.
+     */
+    public static KeyButtonDrawable create(@NonNull Context ctx, @DrawableRes int icon,
+            boolean hasShadow) {
+        return create(ctx, icon, hasShadow, false /* hasOvalBg */);
+    }
+
+    /**
+     * Creates a KeyButtonDrawable with a shadow given its icon. For more information, see
+     * {@link #create(Context, int, boolean, boolean)}.
+     */
     public static KeyButtonDrawable create(Context lightContext, Context darkContext,
-            @DrawableRes int iconResId, boolean hasShadow) {
+            @DrawableRes int iconResId, boolean hasShadow, boolean hasOvalBg) {
         return create(lightContext,
             Utils.getColorAttrDefaultColor(lightContext, R.attr.singleToneColor),
             Utils.getColorAttrDefaultColor(darkContext, R.attr.singleToneColor),
-            iconResId, hasShadow);
+            iconResId, hasShadow, hasOvalBg);
     }
 
+    /**
+     * Creates a KeyButtonDrawable with a shadow given its icon. For more information, see
+     * {@link #create(Context, int, boolean, boolean)}.
+     */
     public static KeyButtonDrawable create(Context context, @ColorInt int lightColor,
-        @ColorInt int darkColor, @DrawableRes int iconResId, boolean hasShadow) {
+            @ColorInt int darkColor, @DrawableRes int iconResId, boolean hasShadow,
+            boolean hasOvalBg) {
         final Resources res = context.getResources();
         boolean isRtl = res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
         Drawable d = context.getDrawable(iconResId);
         final KeyButtonDrawable drawable = new KeyButtonDrawable(d, lightColor, darkColor,
-                isRtl && d.isAutoMirrored());
+                isRtl && d.isAutoMirrored(), hasOvalBg);
         if (hasShadow) {
             int offsetX = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_offset_x);
             int offsetY = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_offset_y);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarContextTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarContextTest.java
index c837c9c..cb70a1f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarContextTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarContextTest.java
@@ -80,7 +80,7 @@
 
     @Test
     public void testSetButtonVisibility() throws Exception {
-        assertFalse("By default the group should be invisible.", mGroup.isVisible());
+        assertTrue("By default the group should be visible.", mGroup.isVisible());
 
         // Set button 1 to be visible, make sure it is the only visible button
         showButton(mBtn1);
@@ -89,7 +89,7 @@
         assertFalse(mBtn2.isVisible());
 
         // Hide button 1 and make sure the group is also invisible
-        assertNotEquals(mGroup.setButtonVisiblity(BUTTON_1_ID, false /* visible */), View.VISIBLE);
+        assertNotEquals(mGroup.setButtonVisibility(BUTTON_1_ID, false /* visible */), View.VISIBLE);
         assertFalse("No buttons are visible, group should also be hidden", mGroup.isVisible());
         assertNull("No buttons should be visible", mGroup.getVisibleContextButton());
     }
@@ -97,7 +97,7 @@
     @Test(expected = RuntimeException.class)
     public void testSetButtonVisibilityUnaddedButton() throws Exception {
         int id = mBtn2.getId() + 1;
-        mGroup.setButtonVisiblity(id, true /* visible */);
+        mGroup.setButtonVisibility(id, true /* visible */);
         fail("Did not throw when setting a button with an invalid id");
     }
 
@@ -120,17 +120,17 @@
         assertTrue(mGroup.isButtonVisibleWithinGroup(mBtn2.getId()));
 
         // Hide button 2
-        assertNotEquals(mGroup.setButtonVisiblity(BUTTON_2_ID, false /* visible */), View.VISIBLE);
+        assertNotEquals(mGroup.setButtonVisibility(BUTTON_2_ID, false /* visible */), View.VISIBLE);
         assertEquals("Hiding button 2 should show button 1", mBtn1,
                 mGroup.getVisibleContextButton());
 
         // Hide button 1
-        assertNotEquals(mGroup.setButtonVisiblity(BUTTON_1_ID, false /* visible */), View.VISIBLE);
+        assertNotEquals(mGroup.setButtonVisibility(BUTTON_1_ID, false /* visible */), View.VISIBLE);
         assertEquals("Hiding button 1 should show button 0", mBtn0,
                 mGroup.getVisibleContextButton());
 
         // Hide button 0, all buttons are now invisible
-        assertNotEquals(mGroup.setButtonVisiblity(BUTTON_0_ID, false /* visible */), View.VISIBLE);
+        assertNotEquals(mGroup.setButtonVisibility(BUTTON_0_ID, false /* visible */), View.VISIBLE);
         assertFalse("No buttons are visible, group should also be invisible", mGroup.isVisible());
         assertNull(mGroup.getVisibleContextButton());
         assertFalse(mGroup.isButtonVisibleWithinGroup(mBtn0.getId()));
@@ -144,7 +144,7 @@
         showButton(mBtn2);
 
         // Show button 1
-        assertNotEquals(mGroup.setButtonVisiblity(BUTTON_1_ID, true /* visible */), View.VISIBLE);
+        assertNotEquals(mGroup.setButtonVisibility(BUTTON_1_ID, true /* visible */), View.VISIBLE);
         assertTrue("Showing button 1 lower priority should be hidden but visible underneath",
                 mGroup.isButtonVisibleWithinGroup(BUTTON_1_ID));
         assertFalse(mBtn0.isVisible());
@@ -152,7 +152,7 @@
         assertTrue(mBtn2.isVisible());
 
         // Hide button 1
-        assertNotEquals(mGroup.setButtonVisiblity(BUTTON_1_ID, false /* visible */), View.VISIBLE);
+        assertNotEquals(mGroup.setButtonVisibility(BUTTON_1_ID, false /* visible */), View.VISIBLE);
         assertFalse("Hiding button 1 with lower priority hides itself underneath",
                 mGroup.isButtonVisibleWithinGroup(BUTTON_1_ID));
         assertTrue("A button still visible, group should also be visible", mGroup.isVisible());
@@ -180,9 +180,9 @@
         final Drawable d = mock(Drawable.class);
         final ContextualButton button = spy(mBtn0);
         final KeyButtonDrawable kbd1 = spy(new KeyButtonDrawable(d, unusedColor, unusedColor,
-                false /* horizontalFlip */));
+                false /* horizontalFlip */, false /* hasOvalBg */));
         final KeyButtonDrawable kbd2 = spy(new KeyButtonDrawable(d, unusedColor, unusedColor,
-                false /* horizontalFlip */));
+                false /* horizontalFlip */, false /* hasOvalBg */));
         kbd1.setDarkIntensity(TEST_DARK_INTENSITY);
         kbd2.setDarkIntensity(0f);
 
@@ -198,7 +198,7 @@
     }
 
     private void showButton(ContextualButton button) {
-        assertEquals(View.VISIBLE, mGroup.setButtonVisiblity(button.getId(), true /* visible */));
+        assertEquals(View.VISIBLE, mGroup.setButtonVisibility(button.getId(), true /* visible */));
         assertTrue("After set a button visible, group should also be visible", mGroup.isVisible());
         assertEquals(button, mGroup.getVisibleContextButton());
     }
diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
index 6154726..db2c742 100644
--- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
+++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
@@ -70,6 +70,7 @@
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -588,6 +589,7 @@
             // rollback sessions been applied.
             List<RollbackData> enabling = new ArrayList<>();
             List<RollbackData> restoreInProgress = new ArrayList<>();
+            Set<String> apexPackageNames = new HashSet<>();
             synchronized (mLock) {
                 ensureRollbackDataLoadedLocked();
                 for (RollbackData data : mRollbacks) {
@@ -597,6 +599,12 @@
                         } else if (data.restoreUserDataInProgress) {
                             restoreInProgress.add(data);
                         }
+
+                        for (PackageRollbackInfo info : data.info.getPackages()) {
+                            if (info.isApex()) {
+                                apexPackageNames.add(info.getPackageName());
+                            }
+                        }
                     }
                 }
             }
@@ -634,6 +642,14 @@
                 }
             }
 
+            for (String apexPackageName : apexPackageNames) {
+                // We will not recieve notifications when an apex is updated,
+                // so check now in case any rollbacks ought to be expired. The
+                // onPackagedReplace function is safe to call if the package
+                // hasn't actually been updated.
+                onPackageReplaced(apexPackageName);
+            }
+
             mPackageHealthObserver.onBootCompleted();
         });
     }
diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index 4ed07c3..8a834c8 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -5438,7 +5438,7 @@
             if (isAttached()) {
                 getDisplay().positionChildAtBottom(this);
             }
-            if (!isActivityTypeHome() || getDisplay().isRemoved()) {
+            if (!isActivityTypeHome() || !isAttached()) {
                 remove();
             }
         }
diff --git a/services/net/java/android/net/ip/IIpClient.aidl b/services/net/java/android/net/ip/IIpClient.aidl
index 1e77264..9989c52 100644
--- a/services/net/java/android/net/ip/IIpClient.aidl
+++ b/services/net/java/android/net/ip/IIpClient.aidl
@@ -17,6 +17,7 @@
 
 import android.net.ProxyInfo;
 import android.net.ProvisioningConfigurationParcelable;
+import android.net.NattKeepalivePacketDataParcelable;
 import android.net.TcpKeepalivePacketDataParcelable;
 
 /** @hide */
@@ -33,4 +34,5 @@
     void addKeepalivePacketFilter(int slot, in TcpKeepalivePacketDataParcelable pkt);
     void removeKeepalivePacketFilter(int slot);
     void setL2KeyAndGroupHint(in String l2Key, in String groupHint);
+    void addNattKeepalivePacketFilter(int slot, in NattKeepalivePacketDataParcelable pkt);
 }
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerLogger.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerLogger.java
new file mode 100644
index 0000000..73b4ce7
--- /dev/null
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerLogger.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2019 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.soundtrigger;
+
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.LinkedList;
+
+/**
+* Constructor SoundTriggerLogger class
+*/
+public class SoundTriggerLogger {
+
+    // ring buffer of events to log.
+    private final LinkedList<Event> mEvents;
+
+    private final String mTitle;
+
+    // the maximum number of events to keep in log
+    private final int mMemSize;
+
+    /**
+     * Constructor for Event class.
+     */
+    public abstract static class Event {
+        // formatter for timestamps
+        private static final SimpleDateFormat sFormat = new SimpleDateFormat("MM-dd HH:mm:ss:SSS");
+
+        private final long mTimestamp;
+
+        Event() {
+            mTimestamp = System.currentTimeMillis();
+        }
+
+    /**
+     * Convert event to String
+     * @return StringBuilder
+     */
+        public String toString() {
+            return (new StringBuilder(sFormat.format(new Date(mTimestamp))))
+                    .append(" ").append(eventToString()).toString();
+        }
+
+        /**
+         * Causes the string message for the event to appear in the logcat.
+         * Here is an example of how to create a new event (a StringEvent), adding it to the logger
+         * (an instance of SoundTriggerLogger) while also making it show in the logcat:
+         * <pre>
+         *     myLogger.log(
+         *         (new StringEvent("something for logcat and logger")).printLog(MyClass.TAG) );
+         * </pre>
+         * @param tag the tag for the android.util.Log.v
+         * @return the same instance of the event
+         */
+        public Event printLog(String tag) {
+            Log.i(tag, eventToString());
+            return this;
+        }
+
+        /**
+         * Convert event to String.
+         * This method is only called when the logger history is about to the dumped,
+         * so this method is where expensive String conversions should be made, not when the Event
+         * subclass is created.
+         * Timestamp information will be automatically added, do not include it.
+         * @return a string representation of the event that occurred.
+         */
+        public abstract String eventToString();
+    }
+
+    /**
+    * Constructor StringEvent class
+    */
+    public static class StringEvent extends Event {
+        private final String mMsg;
+
+        public StringEvent(String msg) {
+            mMsg = msg;
+        }
+
+        @Override
+        public String eventToString() {
+            return mMsg;
+        }
+    }
+
+    /**
+     * Constructor for logger.
+     * @param size the maximum number of events to keep in log
+     * @param title the string displayed before the recorded log
+     */
+    public SoundTriggerLogger(int size, String title) {
+        mEvents = new LinkedList<Event>();
+        mMemSize = size;
+        mTitle = title;
+    }
+
+    /**
+     * Constructor for logger.
+     * @param evt the maximum number of events to keep in log
+     */
+    public synchronized void log(Event evt) {
+        if (mEvents.size() >= mMemSize) {
+            mEvents.removeFirst();
+        }
+        mEvents.add(evt);
+    }
+
+    /**
+     * Constructor for logger.
+     * @param pw the maximum number of events to keep in log
+     */
+    public synchronized void dump(PrintWriter pw) {
+        pw.println("ST Event log: " + mTitle);
+        for (Event evt : mEvents) {
+            pw.println(evt.toString());
+        }
+    }
+}
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
index 697469a..9c4c099 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
@@ -180,9 +180,16 @@
                 Slog.i(TAG, "startRecognition(): Uuid : " + parcelUuid);
             }
 
+            sEventLogger.log(new SoundTriggerLogger.StringEvent("startRecognition(): Uuid : "
+                    + parcelUuid));
+
             GenericSoundModel model = getSoundModel(parcelUuid);
             if (model == null) {
                 Slog.e(TAG, "Null model in database for id: " + parcelUuid);
+
+                sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                        "startRecognition(): Null model in database for id: " + parcelUuid));
+
                 return STATUS_ERROR;
             }
 
@@ -196,6 +203,10 @@
             if (DEBUG) {
                 Slog.i(TAG, "stopRecognition(): Uuid : " + parcelUuid);
             }
+
+            sEventLogger.log(new SoundTriggerLogger.StringEvent("stopRecognition(): Uuid : "
+                    + parcelUuid));
+
             if (!isInitialized()) return STATUS_ERROR;
             return mSoundTriggerHelper.stopGenericRecognition(parcelUuid.getUuid(), callback);
         }
@@ -206,6 +217,10 @@
             if (DEBUG) {
                 Slog.i(TAG, "getSoundModel(): id = " + soundModelId);
             }
+
+            sEventLogger.log(new SoundTriggerLogger.StringEvent("getSoundModel(): id = "
+                    + soundModelId));
+
             SoundTrigger.GenericSoundModel model = mDbHelper.getGenericSoundModel(
                     soundModelId.getUuid());
             return model;
@@ -217,6 +232,10 @@
             if (DEBUG) {
                 Slog.i(TAG, "updateSoundModel(): model = " + soundModel);
             }
+
+            sEventLogger.log(new SoundTriggerLogger.StringEvent("updateSoundModel(): model = "
+                    + soundModel));
+
             mDbHelper.updateGenericSoundModel(soundModel);
         }
 
@@ -226,6 +245,10 @@
             if (DEBUG) {
                 Slog.i(TAG, "deleteSoundModel(): id = " + soundModelId);
             }
+
+            sEventLogger.log(new SoundTriggerLogger.StringEvent("deleteSoundModel(): id = "
+                    + soundModelId));
+
             // Unload the model if it is loaded.
             mSoundTriggerHelper.unloadGenericSoundModel(soundModelId.getUuid());
             mDbHelper.deleteGenericSoundModel(soundModelId.getUuid());
@@ -237,11 +260,19 @@
             if (!isInitialized()) return STATUS_ERROR;
             if (soundModel == null || soundModel.uuid == null) {
                 Slog.e(TAG, "Invalid sound model");
+
+                sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                        "loadGenericSoundModel(): Invalid sound model"));
+
                 return STATUS_ERROR;
             }
             if (DEBUG) {
                 Slog.i(TAG, "loadGenericSoundModel(): id = " + soundModel.uuid);
             }
+
+            sEventLogger.log(new SoundTriggerLogger.StringEvent("loadGenericSoundModel(): id = "
+                    + soundModel.uuid));
+
             synchronized (mLock) {
                 SoundModel oldModel = mLoadedModels.get(soundModel.uuid);
                 // If the model we're loading is actually different than what we had loaded, we
@@ -264,15 +295,28 @@
             if (!isInitialized()) return STATUS_ERROR;
             if (soundModel == null || soundModel.uuid == null) {
                 Slog.e(TAG, "Invalid sound model");
+
+                sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                        "loadKeyphraseSoundModel(): Invalid sound model"));
+
                 return STATUS_ERROR;
             }
             if (soundModel.keyphrases == null || soundModel.keyphrases.length != 1) {
                 Slog.e(TAG, "Only one keyphrase per model is currently supported.");
+
+                sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                        "loadKeyphraseSoundModel(): Only one keyphrase per model"
+                        + " is currently supported."));
+
                 return STATUS_ERROR;
             }
             if (DEBUG) {
                 Slog.i(TAG, "loadKeyphraseSoundModel(): id = " + soundModel.uuid);
             }
+
+            sEventLogger.log(new SoundTriggerLogger.StringEvent("loadKeyphraseSoundModel(): id = "
+                    + soundModel.uuid));
+
             synchronized (mLock) {
                 SoundModel oldModel = mLoadedModels.get(soundModel.uuid);
                 // If the model we're loading is actually different than what we had loaded, we
@@ -303,6 +347,9 @@
                 Slog.i(TAG, "startRecognition(): id = " + soundModelId);
             }
 
+            sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                    "startRecognitionForService(): id = " + soundModelId));
+
             IRecognitionStatusCallback callback =
                     new RemoteSoundTriggerDetectionService(soundModelId.getUuid(), params,
                             detectionService, Binder.getCallingUserHandle(), config);
@@ -311,6 +358,10 @@
                 SoundModel soundModel = mLoadedModels.get(soundModelId.getUuid());
                 if (soundModel == null) {
                     Slog.e(TAG, soundModelId + " is not loaded");
+
+                    sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                            "startRecognitionForService():" + soundModelId + " is not loaded"));
+
                     return STATUS_ERROR;
                 }
                 IRecognitionStatusCallback existingCallback = null;
@@ -319,6 +370,11 @@
                 }
                 if (existingCallback != null) {
                     Slog.e(TAG, soundModelId + " is already running");
+
+                    sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                            "startRecognitionForService():"
+                            + soundModelId + " is already running"));
+
                     return STATUS_ERROR;
                 }
                 int ret;
@@ -329,11 +385,19 @@
                         break;
                     default:
                         Slog.e(TAG, "Unknown model type");
+
+                        sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                                "startRecognitionForService(): Unknown model type"));
+
                         return STATUS_ERROR;
                 }
 
                 if (ret != STATUS_OK) {
                     Slog.e(TAG, "Failed to start model: " + ret);
+
+                    sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                            "startRecognitionForService(): Failed to start model:"));
+
                     return ret;
                 }
                 synchronized (mCallbacksLock) {
@@ -351,10 +415,18 @@
                 Slog.i(TAG, "stopRecognition(): id = " + soundModelId);
             }
 
+            sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                    "stopRecognitionForService(): id = " + soundModelId));
+
             synchronized (mLock) {
                 SoundModel soundModel = mLoadedModels.get(soundModelId.getUuid());
                 if (soundModel == null) {
                     Slog.e(TAG, soundModelId + " is not loaded");
+
+                    sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                            "stopRecognitionForService(): " + soundModelId
+                            + " is not loaded"));
+
                     return STATUS_ERROR;
                 }
                 IRecognitionStatusCallback callback = null;
@@ -363,6 +435,11 @@
                 }
                 if (callback == null) {
                     Slog.e(TAG, soundModelId + " is not running");
+
+                    sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                            "stopRecognitionForService(): " + soundModelId
+                            + " is not running"));
+
                     return STATUS_ERROR;
                 }
                 int ret;
@@ -372,11 +449,19 @@
                         break;
                     default:
                         Slog.e(TAG, "Unknown model type");
+
+                        sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                                "stopRecognitionForService(): Unknown model type"));
+
                         return STATUS_ERROR;
                 }
 
                 if (ret != STATUS_OK) {
                     Slog.e(TAG, "Failed to stop model: " + ret);
+
+                    sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                                "stopRecognitionForService(): Failed to stop model: " + ret));
+
                     return ret;
                 }
                 synchronized (mCallbacksLock) {
@@ -394,10 +479,17 @@
                 Slog.i(TAG, "unloadSoundModel(): id = " + soundModelId);
             }
 
+            sEventLogger.log(new SoundTriggerLogger.StringEvent("unloadSoundModel(): id = "
+                    + soundModelId));
+
             synchronized (mLock) {
                 SoundModel soundModel = mLoadedModels.get(soundModelId.getUuid());
                 if (soundModel == null) {
                     Slog.e(TAG, soundModelId + " is not loaded");
+
+                    sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                                "unloadSoundModel(): " + soundModelId + " is not loaded"));
+
                     return STATUS_ERROR;
                 }
                 int ret;
@@ -411,10 +503,18 @@
                         break;
                     default:
                         Slog.e(TAG, "Unknown model type");
+
+                        sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                                "unloadSoundModel(): Unknown model type"));
+
                         return STATUS_ERROR;
                 }
                 if (ret != STATUS_OK) {
                     Slog.e(TAG, "Failed to unload model");
+
+                    sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                            "unloadSoundModel(): Failed to unload model"));
+
                     return ret;
                 }
                 mLoadedModels.remove(soundModelId.getUuid());
@@ -444,10 +544,17 @@
                 Slog.i(TAG, "getModelState(): id = " + soundModelId);
             }
 
+            sEventLogger.log(new SoundTriggerLogger.StringEvent("getModelState(): id = "
+                    + soundModelId));
+
             synchronized (mLock) {
                 SoundModel soundModel = mLoadedModels.get(soundModelId.getUuid());
                 if (soundModel == null) {
                     Slog.e(TAG, soundModelId + " is not loaded");
+
+                    sEventLogger.log(new SoundTriggerLogger.StringEvent("getModelState(): "
+                            + soundModelId + " is not loaded"));
+
                     return ret;
                 }
                 switch (soundModel.type) {
@@ -459,6 +566,10 @@
                         break;
                     default:
                         Slog.e(TAG, "Unknown model type");
+
+                        sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                                "getModelState(): Unknown model type"));
+
                         break;
                 }
 
@@ -708,6 +819,10 @@
                     mService.removeClient(mPuuid);
                 } catch (Exception e) {
                     Slog.e(TAG, mPuuid + ": Cannot remove client", e);
+
+                    sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                            + ": Cannot remove client"));
+
                 }
 
                 mService = null;
@@ -730,6 +845,8 @@
         private void destroy() {
             if (DEBUG) Slog.v(TAG, mPuuid + ": destroy");
 
+            sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid + ": destroy"));
+
             synchronized (mRemoteServiceLock) {
                 disconnectLocked();
 
@@ -761,6 +878,10 @@
                         } catch (Exception e) {
                             Slog.e(TAG, mPuuid + ": Could not stop operation "
                                     + mRunningOpIds.valueAt(i), e);
+
+                            sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                                    + ": Could not stop operation " + mRunningOpIds.valueAt(i)));
+
                         }
                     }
 
@@ -786,6 +907,10 @@
 
                 if (ri == null) {
                     Slog.w(TAG, mPuuid + ": " + mServiceName + " not found");
+
+                    sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                            + ": " + mServiceName + " not found"));
+
                     return;
                 }
 
@@ -793,6 +918,11 @@
                         .equals(ri.serviceInfo.permission)) {
                     Slog.w(TAG, mPuuid + ": " + mServiceName + " does not require "
                             + BIND_SOUND_TRIGGER_DETECTION_SERVICE);
+
+                    sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                            + ": " + mServiceName + " does not require "
+                            + BIND_SOUND_TRIGGER_DETECTION_SERVICE));
+
                     return;
                 }
 
@@ -803,6 +933,10 @@
                     mRemoteServiceWakeLock.acquire();
                 } else {
                     Slog.w(TAG, mPuuid + ": Could not bind to " + mServiceName);
+
+                    sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                            + ": Could not bind to " + mServiceName));
+
                 }
             } finally {
                 Binder.restoreCallingIdentity(token);
@@ -821,6 +955,9 @@
                     Slog.w(TAG, mPuuid + ": Dropped operation as already destroyed or marked for "
                             + "destruction");
 
+                    sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                            + ":Dropped operation as already destroyed or marked for destruction"));
+
                     op.drop();
                     return;
                 }
@@ -847,11 +984,20 @@
                             if (DEBUG || opsAllowed + 10 > opsAdded) {
                                 Slog.w(TAG, mPuuid + ": Dropped operation as too many operations "
                                         + "were run in last 24 hours");
+
+                                sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                                        + ": Dropped operation as too many operations "
+                                        + "were run in last 24 hours"));
+
                             }
 
                             op.drop();
                         } catch (Exception e) {
                             Slog.e(TAG, mPuuid + ": Could not drop operation", e);
+
+                            sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                                        + ": Could not drop operation"));
+
                         }
                     } else {
                         mNumOps.addOp(currentTime);
@@ -866,10 +1012,17 @@
                         try {
                             if (DEBUG) Slog.v(TAG, mPuuid + ": runOp " + opId);
 
+                            sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                                        + ": runOp " + opId));
+
                             op.run(opId, mService);
                             mRunningOpIds.add(opId);
                         } catch (Exception e) {
                             Slog.e(TAG, mPuuid + ": Could not run operation " + opId, e);
+
+                            sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                                        + ": Could not run operation " + opId));
+
                         }
                     }
 
@@ -897,6 +1050,10 @@
         public void onKeyphraseDetected(SoundTrigger.KeyphraseRecognitionEvent event) {
             Slog.w(TAG, mPuuid + "->" + mServiceName + ": IGNORED onKeyphraseDetected(" + event
                     + ")");
+
+            sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid + "->" + mServiceName
+                    + ": IGNORED onKeyphraseDetected(" + event + ")"));
+
         }
 
         /**
@@ -928,6 +1085,8 @@
                             : AudioFormat.CHANNEL_IN_MONO,
                     captureFormat.getEncoding());
 
+            sEventLogger.log(new SoundTriggerLogger.StringEvent("createAudioRecordForEvent"));
+
             return new AudioRecord(attributes, captureFormat, bufferSize,
                     event.getCaptureSession());
         }
@@ -936,6 +1095,9 @@
         public void onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event) {
             if (DEBUG) Slog.v(TAG, mPuuid + ": Generic sound trigger event: " + event);
 
+            sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                    + ": Generic sound trigger event: " + event));
+
             runOrAddOperation(new Operation(
                     // always execute:
                     () -> {
@@ -966,6 +1128,9 @@
         public void onError(int status) {
             if (DEBUG) Slog.v(TAG, mPuuid + ": onError: " + status);
 
+            sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                    + ": onError: " + status));
+
             runOrAddOperation(
                     new Operation(
                             // always execute:
@@ -985,17 +1150,28 @@
         @Override
         public void onRecognitionPaused() {
             Slog.i(TAG, mPuuid + "->" + mServiceName + ": IGNORED onRecognitionPaused");
+
+            sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                    + "->" + mServiceName + ": IGNORED onRecognitionPaused"));
+
         }
 
         @Override
         public void onRecognitionResumed() {
             Slog.i(TAG, mPuuid + "->" + mServiceName + ": IGNORED onRecognitionResumed");
+
+            sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                    + "->" + mServiceName + ": IGNORED onRecognitionResumed"));
+
         }
 
         @Override
         public void onServiceConnected(ComponentName name, IBinder service) {
             if (DEBUG) Slog.v(TAG, mPuuid + ": onServiceConnected(" + service + ")");
 
+            sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                    + ": onServiceConnected(" + service + ")"));
+
             synchronized (mRemoteServiceLock) {
                 mService = ISoundTriggerDetectionService.Stub.asInterface(service);
 
@@ -1016,6 +1192,9 @@
         public void onServiceDisconnected(ComponentName name) {
             if (DEBUG) Slog.v(TAG, mPuuid + ": onServiceDisconnected");
 
+            sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                    + ": onServiceDisconnected"));
+
             synchronized (mRemoteServiceLock) {
                 mService = null;
             }
@@ -1025,6 +1204,9 @@
         public void onBindingDied(ComponentName name) {
             if (DEBUG) Slog.v(TAG, mPuuid + ": onBindingDied");
 
+            sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                    + ": onBindingDied"));
+
             synchronized (mRemoteServiceLock) {
                 destroy();
             }
@@ -1034,6 +1216,9 @@
         public void onNullBinding(ComponentName name) {
             Slog.w(TAG, name + " for model " + mPuuid + " returned a null binding");
 
+            sEventLogger.log(new SoundTriggerLogger.StringEvent(name + " for model "
+                    + mPuuid + " returned a null binding"));
+
             synchronized (mRemoteServiceLock) {
                 disconnectLocked();
             }
@@ -1082,11 +1267,17 @@
         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
             if (!isInitialized()) return;
             mSoundTriggerHelper.dump(fd, pw, args);
+            // log
+            sEventLogger.dump(pw);
         }
 
         private synchronized boolean isInitialized() {
             if (mSoundTriggerHelper == null ) {
                 Slog.e(TAG, "SoundTriggerHelper not initialized.");
+
+                sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                        "SoundTriggerHelper not initialized."));
+
                 return false;
             }
             return true;
@@ -1099,4 +1290,11 @@
             throw new SecurityException("Caller does not hold the permission " + permission);
         }
     }
+
+    //=================================================================
+    // For logging
+
+    private static final SoundTriggerLogger sEventLogger = new SoundTriggerLogger(200,
+            "SoundTrigger activity");
+
 }
diff --git a/tests/RollbackTest/Android.bp b/tests/RollbackTest/Android.bp
index dfc3b6e..e556b0a 100644
--- a/tests/RollbackTest/Android.bp
+++ b/tests/RollbackTest/Android.bp
@@ -88,6 +88,15 @@
     installable: false,
 }
 
+apex {
+    name: "com.android.tests.rollback.testapex.RollbackTestApexV3",
+    manifest: "TestApex/RollbackTestApexV3.json",
+    file_contexts: "apex.test",
+    prebuilts: ["RollbackTestApex.prebuilt.txt"],
+    key: "RollbackTestApex.key",
+    installable: false,
+}
+
 apex_key {
     name: "RollbackTestApex.key",
     public_key: "TestApex/com.android.tests.rollback.testapex.avbpubkey",
@@ -116,6 +125,7 @@
         ":RollbackTestAppASplitV2",
         ":com.android.tests.rollback.testapex.RollbackTestApexV1",
         ":com.android.tests.rollback.testapex.RollbackTestApexV2",
+        ":com.android.tests.rollback.testapex.RollbackTestApexV3",
     ],
     test_config: "RollbackTest.xml",
     sdk_version: "test_current",
diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java
index 7e711c2..3b0e2a5 100644
--- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java
+++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java
@@ -26,6 +26,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import org.junit.After;
@@ -54,6 +55,8 @@
             "com.android.tests.rollback.testapex.RollbackTestApexV1.apex";
     private static final String TEST_APEX_V2 =
             "com.android.tests.rollback.testapex.RollbackTestApexV2.apex";
+    private static final String TEST_APEX_V3 =
+            "com.android.tests.rollback.testapex.RollbackTestApexV3.apex";
 
     /**
      * Adopts common shell permissions needed for rollback tests.
@@ -145,26 +148,13 @@
 
     /**
      * Test rollbacks of staged installs an apk and an apex.
-     * Prepare apex (and apk) phase.
-     */
-    @Test
-    public void testApkAndApexPrepare() throws Exception {
-        RollbackTestUtils.uninstall(TEST_APP_A);
-        assertEquals(-1, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
-
-        // Note: can't uninstall the apex. See note in #testApexOnlyPrepareApex().
-        RollbackTestUtils.installStaged(false, TEST_APP_A_V1, TEST_APEX_V1);
-
-        // At this point, the host test driver will reboot the device and run
-        // testApkAndApexEnableRollback().
-    }
-
-    /**
-     * Test rollbacks of staged installs an apk and an apex.
      * Enable rollback phase.
      */
     @Test
     public void testApkAndApexEnableRollback() throws Exception {
+        RollbackTestUtils.uninstall(TEST_APP_A);
+        RollbackTestUtils.install(TEST_APP_A_V1, false);
+
         assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APEX_PKG));
         assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
 
@@ -225,22 +215,6 @@
 
     /**
      * Test rollbacks of staged installs involving only apex.
-     * Prepare apex phase.
-     */
-    @Test
-    public void testApexOnlyPrepareApex() throws Exception {
-        // Note: We can't uninstall the apex if it is already on device,
-        // because that isn't supported yet (b/123667725). As long as nothing
-        // is failing, this should be fine because we don't expect the tests
-        // to leave the device with v2 of the apex installed.
-        RollbackTestUtils.installStaged(false, TEST_APEX_V1);
-
-        // At this point, the host test driver will reboot the device and run
-        // testApexOnlyEnableRollback().
-    }
-
-    /**
-     * Test rollbacks of staged installs involving only apex.
      * Enable rollback phase.
      */
     @Test
@@ -291,4 +265,51 @@
     public void testApexOnlyConfirmRollback() throws Exception {
         assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APEX_PKG));
     }
+
+    /**
+     * Tests that apex update expires existing rollbacks for that apex.
+     * Enable rollback phase.
+     */
+    @Test
+    public void testApexRollbackExpirationEnableRollback() throws Exception {
+        assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APEX_PKG));
+        RollbackTestUtils.installStaged(true, TEST_APEX_V2);
+
+        // At this point, the host test driver will reboot the device and run
+        // testApexRollbackExpirationUpdateApex().
+    }
+
+    /**
+     * Tests that apex update expires existing rollbacks for that apex.
+     * Update apex phase.
+     */
+    @Test
+    public void testApexRollbackExpirationUpdateApex() throws Exception {
+        assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APEX_PKG));
+        RollbackTestUtils.installStaged(false, TEST_APEX_V3);
+
+        // At this point, the host test driver will reboot the device and run
+        // testApexRollbackExpirationConfirmExpiration().
+    }
+
+    /**
+     * Tests that apex update expires existing rollbacks for that apex.
+     * Confirm expiration phase.
+     */
+    @Test
+    public void testApexRollbackExpirationConfirmExpiration() throws Exception {
+        assertEquals(3, RollbackTestUtils.getInstalledVersion(TEST_APEX_PKG));
+
+        RollbackManager rm = RollbackTestUtils.getRollbackManager();
+        assertNull(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), TEST_APEX_PKG));
+    }
+
+    /**
+     * Helper function called by the host test to install v1 of the test apex,
+     * assuming the test apex is not installed.
+     */
+    @Test
+    public void installTestApexV1() throws Exception {
+        RollbackTestUtils.installStaged(false, TEST_APEX_V1);
+    }
 }
diff --git a/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java b/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java
index ac7f634..1f87ed8 100644
--- a/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java
+++ b/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java
@@ -18,6 +18,7 @@
 
 import static org.junit.Assert.assertTrue;
 
+import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 
@@ -30,6 +31,8 @@
 @RunWith(DeviceJUnit4ClassRunner.class)
 public class StagedRollbackTest extends BaseHostJUnit4Test {
 
+    private static final String TEST_APEX_PKG = "com.android.tests.rollback.testapex";
+
     /**
      * Runs the given phase of a test by calling into the device.
      * Throws an exception if the test phase fails.
@@ -59,8 +62,7 @@
      */
     @Test
     public void testApexOnly() throws Exception {
-        runPhase("testApexOnlyPrepareApex");
-        getDevice().reboot();
+        installTestApexV1();
         runPhase("testApexOnlyEnableRollback");
         getDevice().reboot();
         runPhase("testApexOnlyCommitRollback");
@@ -73,12 +75,45 @@
      */
     @Test
     public void testApkAndApex() throws Exception {
-        runPhase("testApkAndApexPrepare");
-        getDevice().reboot();
+        installTestApexV1();
         runPhase("testApkAndApexEnableRollback");
         getDevice().reboot();
         runPhase("testApkAndApexCommitRollback");
         getDevice().reboot();
         runPhase("testApkAndApexConfirmRollback");
     }
+
+    /**
+     * Tests that apex update expires existing rollbacks for that apex.
+     */
+    @Test
+    public void testApexRollbackExpiration() throws Exception {
+        installTestApexV1();
+        runPhase("testApexRollbackExpirationEnableRollback");
+        getDevice().reboot();
+        runPhase("testApexRollbackExpirationUpdateApex");
+        getDevice().reboot();
+        runPhase("testApexRollbackExpirationConfirmExpiration");
+    }
+
+    /**
+     * Do whatever is necessary to get version 1 of the test apex installed on
+     * the device. Try to do so without extra reboots where possible to keep
+     * the test execution time down.
+     */
+    private void installTestApexV1() throws Exception {
+        for (ITestDevice.ApexInfo apexInfo : getDevice().getActiveApexes()) {
+            if (TEST_APEX_PKG.equals(apexInfo.name)) {
+                if (apexInfo.versionCode == 1) {
+                    return;
+                }
+                getDevice().uninstallPackage(TEST_APEX_PKG);
+                getDevice().reboot();
+                break;
+            }
+        }
+
+        runPhase("installTestApexV1");
+        getDevice().reboot();
+    }
 }
diff --git a/tests/RollbackTest/TestApex/RollbackTestApexV3.json b/tests/RollbackTest/TestApex/RollbackTestApexV3.json
new file mode 100644
index 0000000..87a2c9d
--- /dev/null
+++ b/tests/RollbackTest/TestApex/RollbackTestApexV3.json
@@ -0,0 +1,4 @@
+{
+    "name": "com.android.tests.rollback.testapex",
+    "version": 3
+}
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index c15775f..363ac9c 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -567,6 +567,16 @@
                 protected void preventAutomaticReconnect() {
                     mPreventReconnectReceived.open();
                 }
+
+                @Override
+                protected void addKeepalivePacketFilter(Message msg) {
+                    Log.i(TAG, "Add keepalive packet filter.");
+                }
+
+                @Override
+                protected void removeKeepalivePacketFilter(Message msg) {
+                    Log.i(TAG, "Remove keepalive packet filter.");
+                }
             };
 
             assertEquals(mNetworkAgent.netId, nmNetworkCaptor.getValue().netId);
diff --git a/tools/apilint/apilint.py b/tools/apilint/apilint.py
index 97ca6dc..f41426d 100644
--- a/tools/apilint/apilint.py
+++ b/tools/apilint/apilint.py
@@ -2132,6 +2132,10 @@
 
     # Remove all existing things so we're left with new
     for prev_clazz in prev.values():
+        if prev_clazz.fullname not in cur:
+            # The class was removed this release; we can safely ignore it.
+            continue
+
         cur_clazz = cur[prev_clazz.fullname]
         if not is_interesting(cur_clazz): continue