Merge "Import translations. DO NOT MERGE" into oc-mr1-dev
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 3fa8927..757795e 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -762,6 +762,10 @@
     private boolean mDestroyed;
     private boolean mDoReportFullyDrawn = true;
     private boolean mRestoredFromBundle;
+
+    /** {@code true} if the activity lifecycle is in a state which supports picture-in-picture.
+     * This only affects the client-side exception, the actual state check still happens in AMS. */
+    private boolean mCanEnterPictureInPicture = false;
     /** true if the activity is going through a transient pause */
     /*package*/ boolean mTemporaryPause = false;
     /** true if the activity is being destroyed in order to recreate it with a new configuration */
@@ -2091,6 +2095,10 @@
             if (params == null) {
                 throw new IllegalArgumentException("Expected non-null picture-in-picture params");
             }
+            if (!mCanEnterPictureInPicture) {
+                throw new IllegalStateException("Activity must be resumed to enter"
+                        + " picture-in-picture");
+            }
             return ActivityManagerNative.getDefault().enterPictureInPictureMode(mToken, params);
         } catch (RemoteException e) {
             return false;
@@ -6957,25 +6965,29 @@
         return mParent != null ? mParent.getActivityToken() : mToken;
     }
 
-    final void performCreateCommon() {
+    final void performCreate(Bundle icicle) {
+        performCreate(icicle, null);
+    }
+
+    final void performCreate(Bundle icicle, PersistableBundle persistentState) {
+        mCanEnterPictureInPicture = true;
+        restoreHasCurrentPermissionRequest(icicle);
+        if (persistentState != null) {
+            onCreate(icicle, persistentState);
+        } else {
+            onCreate(icicle);
+        }
+        mActivityTransitionState.readState(icicle);
+
         mVisibleFromClient = !mWindow.getWindowStyle().getBoolean(
                 com.android.internal.R.styleable.Window_windowNoDisplay, false);
         mFragments.dispatchActivityCreated();
         mActivityTransitionState.setEnterActivityOptions(this, getActivityOptions());
     }
 
-    final void performCreate(Bundle icicle) {
-        restoreHasCurrentPermissionRequest(icicle);
-        onCreate(icicle);
-        mActivityTransitionState.readState(icicle);
-        performCreateCommon();
-    }
-
-    final void performCreate(Bundle icicle, PersistableBundle persistentState) {
-        restoreHasCurrentPermissionRequest(icicle);
-        onCreate(icicle, persistentState);
-        mActivityTransitionState.readState(icicle);
-        performCreateCommon();
+    final void performNewIntent(Intent intent) {
+        mCanEnterPictureInPicture = true;
+        onNewIntent(intent);
     }
 
     final void performStart() {
@@ -7126,6 +7138,9 @@
         mDoReportFullyDrawn = false;
         mFragments.doLoaderStop(mChangingConfigurations /*retain*/);
 
+        // Disallow entering picture-in-picture after the activity has been stopped
+        mCanEnterPictureInPicture = false;
+
         if (!mStopped) {
             if (mWindow != null) {
                 mWindow.closeAllPanels();
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index 467fc95..e260967 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -48,6 +48,7 @@
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 import android.view.Window;
+
 import com.android.internal.content.ReferrerIntent;
 
 import java.io.File;
@@ -1305,7 +1306,7 @@
      * @param intent The new intent being received.
      */
     public void callActivityOnNewIntent(Activity activity, Intent intent) {
-        activity.onNewIntent(intent);
+        activity.performNewIntent(intent);
     }
 
     /**
diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java
index 4b98e35..81ab407 100644
--- a/core/java/android/webkit/WebView.java
+++ b/core/java/android/webkit/WebView.java
@@ -1926,13 +1926,14 @@
      * For applications targeted to API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN} or below,
      * all public methods (including the inherited ones) can be accessed, see the
      * important security note below for implications.
-     * <p> Note that injected objects will not
-     * appear in JavaScript until the page is next (re)loaded. For example:
+     * <p> Note that injected objects will not appear in JavaScript until the page is next
+     * (re)loaded. JavaScript should be enabled before injecting the object. For example:
      * <pre>
      * class JsObject {
      *    {@literal @}JavascriptInterface
      *    public String toString() { return "injectedObject"; }
      * }
+     * webview.getSettings().setJavaScriptEnabled(true);
      * webView.addJavascriptInterface(new JsObject(), "injectedObject");
      * webView.loadData("<!DOCTYPE html><title></title>", "text/html", null);
      * webView.loadUrl("javascript:alert(injectedObject.toString())");</pre>
diff --git a/packages/SettingsLib/res/values/arrays.xml b/packages/SettingsLib/res/values/arrays.xml
index 1f1b67e..0bf2eda 100644
--- a/packages/SettingsLib/res/values/arrays.xml
+++ b/packages/SettingsLib/res/values/arrays.xml
@@ -121,8 +121,8 @@
         <item>Use System Selection (Default)</item>
         <item>SBC</item>
         <item>AAC</item>
-        <item><xliff:g id="aptx">Qualcomm(R) aptX(TM) audio</xliff:g></item>
-        <item><xliff:g id="aptx_hd">Qualcomm(R) aptX(TM) HD audio</xliff:g></item>
+        <item><xliff:g id="qualcomm">Qualcomm®</xliff:g> <xliff:g id="aptx">aptX™</xliff:g> audio</item>
+        <item><xliff:g id="qualcomm">Qualcomm®</xliff:g> <xliff:g id="aptx_hd">aptX™ HD</xliff:g> audio</item>
         <item>LDAC</item>
         <item>Enable Optional Codecs</item>
         <item>Disable Optional Codecs</item>
@@ -145,8 +145,8 @@
         <item>Use System Selection (Default)</item>
         <item>SBC</item>
         <item>AAC</item>
-        <item><xliff:g id="aptx">Qualcomm(R) aptX(TM) audio</xliff:g></item>
-        <item><xliff:g id="aptx_hd">Qualcomm(R) aptX(TM) HD audio</xliff:g></item>
+        <item><xliff:g id="qualcomm">Qualcomm®</xliff:g> <xliff:g id="aptx">aptX™</xliff:g> audio</item>
+        <item><xliff:g id="qualcomm">Qualcomm®</xliff:g> <xliff:g id="aptx_hd">aptX™ HD</xliff:g> audio</item>
         <item>LDAC</item>
         <item>Enable Optional Codecs</item>
         <item>Disable Optional Codecs</item>
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java
index b8c5aca..330eaf0 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java
@@ -51,6 +51,7 @@
 import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.TextUtils;
+import android.text.format.DateUtils;
 import android.text.style.TtsSpan;
 import android.util.Log;
 
@@ -124,11 +125,20 @@
     private final ConcurrentHashMap<String, ScanResult> mScanResultCache =
             new ConcurrentHashMap<String, ScanResult>(32);
 
-    /** Map of BSSIDs to speed values for individual ScanResults. */
-    private final Map<String, Integer> mScanResultScores = new HashMap<>();
+    /**
+     * Map of BSSIDs to scored networks for individual bssids.
+     *
+     * <p>This cache should not be evicted with scan results, as the values here are used to
+     * generate a fallback in the absence of scores for the visible APs.
+     */
+    private final Map<String, TimestampedScoredNetwork> mScoredNetworkCache = new HashMap<>();
+
+    /** Maximum age in millis of cached scored networks in {@link #mScoredNetworkCache}. */
+    @VisibleForTesting static final long MAX_CACHED_SCORE_AGE_MILLIS =
+            24 * DateUtils.DAY_IN_MILLIS;
 
     /** Maximum age of scan results to hold onto while actively scanning. **/
-    private static final long MAX_SCAN_RESULT_AGE_MS = 15000;
+    private static final long MAX_SCAN_RESULT_AGE_MILLIS = 15000;
 
     static final String KEY_NETWORKINFO = "key_networkinfo";
     static final String KEY_WIFIINFO = "key_wifiinfo";
@@ -138,6 +148,7 @@
     static final String KEY_SPEED = "key_speed";
     static final String KEY_PSKTYPE = "key_psktype";
     static final String KEY_SCANRESULTCACHE = "key_scanresultcache";
+    static final String KEY_SCOREDNETWORKCACHE = "key_scorednetworkcache";
     static final String KEY_CONFIG = "key_config";
     static final String KEY_FQDN = "key_fqdn";
     static final String KEY_PROVIDER_FRIENDLY_NAME = "key_provider_friendly_name";
@@ -188,7 +199,7 @@
 
     private Object mTag;
 
-    private int mSpeed = Speed.NONE;
+    @Speed private int mSpeed = Speed.NONE;
     private boolean mIsScoredNetworkMetered = false;
 
     // used to co-relate internal vs returned accesspoint.
@@ -238,6 +249,13 @@
                 mScanResultCache.put(result.BSSID, result);
             }
         }
+        if (savedState.containsKey(KEY_SCOREDNETWORKCACHE)) {
+            ArrayList<TimestampedScoredNetwork> scoredNetworkArrayList =
+                    savedState.getParcelableArrayList(KEY_SCOREDNETWORKCACHE);
+            for (TimestampedScoredNetwork timedScore : scoredNetworkArrayList) {
+                mScoredNetworkCache.put(timedScore.getScore().networkKey.wifiKey.bssid, timedScore);
+            }
+        }
         if (savedState.containsKey(KEY_FQDN)) {
             mFqdn = savedState.getString(KEY_FQDN);
         }
@@ -308,8 +326,8 @@
         this.mNetworkInfo = that.mNetworkInfo;
         this.mScanResultCache.clear();
         this.mScanResultCache.putAll(that.mScanResultCache);
-        this.mScanResultScores.clear();
-        this.mScanResultScores.putAll(that.mScanResultScores);
+        this.mScoredNetworkCache.clear();
+        this.mScoredNetworkCache.putAll(that.mScoredNetworkCache);
         this.mId = that.mId;
         this.mSpeed = that.mSpeed;
         this.mIsScoredNetworkMetered = that.mIsScoredNetworkMetered;
@@ -347,7 +365,7 @@
         if (isSaved() && !other.isSaved()) return -1;
         if (!isSaved() && other.isSaved()) return 1;
 
-        // Faster speeds go before slower speeds
+        // Faster speeds go before slower speeds - but only if visible change in speed label
         if (getSpeed() != other.getSpeed()) {
             return other.getSpeed() - getSpeed();
         }
@@ -425,7 +443,6 @@
      */
     boolean update(WifiNetworkScoreCache scoreCache, boolean scoringUiEnabled) {
         boolean scoreChanged = false;
-        mScanResultScores.clear();
         if (scoringUiEnabled) {
             scoreChanged = updateScores(scoreCache);
         }
@@ -435,38 +452,99 @@
     /**
      * Updates the AccessPoint rankingScore and speed, returning true if the data has changed.
      *
+     * <p>Any cached {@link TimestampedScoredNetwork} objects older than
+     * {@link #MAX_CACHED_SCORE_AGE_MILLIS} will be removed when this method is invoked.
+     *
+     * <p>Precondition: {@link #mRssi} is up to date before invoking this method.
+     *
      * @param scoreCache The score cache to use to retrieve scores.
+     * @return true if the set speed has changed
      */
     private boolean updateScores(WifiNetworkScoreCache scoreCache) {
-        int oldSpeed = mSpeed;
-        mSpeed = Speed.NONE;
-
+        long nowMillis = SystemClock.elapsedRealtime();
         for (ScanResult result : mScanResultCache.values()) {
             ScoredNetwork score = scoreCache.getScoredNetwork(result);
             if (score == null) {
                 continue;
             }
-
-            int speed = score.calculateBadge(result.level);
-            mScanResultScores.put(result.BSSID, speed);
-            mSpeed = Math.max(mSpeed, speed);
-        }
-
-        // set mSpeed to the connected ScanResult if the AccessPoint is the active network
-        if (isActive() && mInfo != null) {
-            NetworkKey key = new NetworkKey(new WifiKey(
-                    AccessPoint.convertToQuotedString(ssid), mInfo.getBSSID()));
-            ScoredNetwork score = scoreCache.getScoredNetwork(key);
-            if (score != null) {
-                mSpeed = score.calculateBadge(mInfo.getRssi());
+            TimestampedScoredNetwork timedScore = mScoredNetworkCache.get(result.BSSID);
+            if (timedScore == null) {
+                mScoredNetworkCache.put(
+                        result.BSSID, new TimestampedScoredNetwork(score, nowMillis));
+            } else {
+                // Update data since the has been seen in the score cache
+                timedScore.update(score, nowMillis);
             }
         }
 
-        if(WifiTracker.sVerboseLogging) {
-            Log.i(TAG, String.format("%s: Set speed to %d", ssid, mSpeed));
+        // Remove old cached networks
+        long evictionCutoff = nowMillis - MAX_CACHED_SCORE_AGE_MILLIS;
+        Iterator<TimestampedScoredNetwork> iterator = mScoredNetworkCache.values().iterator();
+        iterator.forEachRemaining(timestampedScoredNetwork -> {
+            if (timestampedScoredNetwork.getUpdatedTimestampMillis() < evictionCutoff) {
+                iterator.remove();
+            }
+        });
+
+        return updateSpeed();
+    }
+
+    /**
+     * Updates the internal speed, returning true if the update resulted in a speed label change.
+     */
+    private boolean updateSpeed() {
+        int oldSpeed = mSpeed;
+        mSpeed = generateAverageSpeedForSsid();
+
+        // set speed to the connected ScanResult if the AccessPoint is the active network
+        if (isActive() && mInfo != null) {
+            TimestampedScoredNetwork timedScore = mScoredNetworkCache.get(mInfo.getBSSID());
+            if (timedScore != null) {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Set score using specific access point curve for connected AP: "
+                            + getSsidStr());
+                }
+                // TODO(b/63073866): Map using getLevel rather than specific rssi value so score
+                // doesn't change without a visible wifi bar change.
+                int speed = timedScore.getScore().calculateBadge(mInfo.getRssi());
+                if (speed != Speed.NONE) {
+                    mSpeed = speed;
+                }
+            }
         }
 
-        return oldSpeed != mSpeed;
+        boolean changed = oldSpeed != mSpeed;
+        if(WifiTracker.sVerboseLogging && changed) {
+            Log.i(TAG, String.format("%s: Set speed to %d", ssid, mSpeed));
+        }
+        return changed;
+    }
+
+    /** Creates a speed value for the current {@link #mRssi} by averaging all non zero badges. */
+    @Speed private int generateAverageSpeedForSsid() {
+        if (mScoredNetworkCache.isEmpty()) {
+            return Speed.NONE;
+        }
+
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, String.format("Generating fallbackspeed for %s using cache: %s",
+                    getSsidStr(), mScoredNetworkCache));
+        }
+
+        int count = 0;
+        int totalSpeed = 0;
+        for (TimestampedScoredNetwork timedScore : mScoredNetworkCache.values()) {
+            int speed = timedScore.getScore().calculateBadge(mRssi);
+            if (speed != Speed.NONE) {
+                count++;
+                totalSpeed += speed;
+            }
+        }
+        int speed = count == 0 ? Speed.NONE : totalSpeed / count;
+        if (WifiTracker.sVerboseLogging) {
+            Log.i(TAG, String.format("%s generated fallback speed is: %d", getSsidStr(), speed));
+        }
+        return roundToClosestSpeedEnum(speed);
     }
 
     /**
@@ -501,7 +579,7 @@
         for (Iterator<ScanResult> iter = mScanResultCache.values().iterator(); iter.hasNext(); ) {
             ScanResult result = iter.next();
             // result timestamp is in microseconds
-            if (nowMs - result.timestamp / 1000 > MAX_SCAN_RESULT_AGE_MS) {
+            if (nowMs - result.timestamp / 1000 > MAX_SCAN_RESULT_AGE_MILLIS) {
                 iter.remove();
             }
         }
@@ -582,8 +660,6 @@
 
     /** Updates {@link #mSeen} based on the scan result cache. */
     private void updateSeen() {
-        // TODO(sghuman): Set to now if connected
-
         long seen = 0;
         for (ScanResult result : mScanResultCache.values()) {
             if (result.timestamp > seen) {
@@ -942,17 +1018,23 @@
         }
         stringBuilder.append("=").append(result.frequency);
         stringBuilder.append(",").append(result.level);
-        if (hasSpeed(result)) {
+        int speed = getSpecificApSpeed(result);
+        if (speed != Speed.NONE) {
             stringBuilder.append(",")
-                    .append(getSpeedLabel(mScanResultScores.get(result.BSSID)));
+                    .append(getSpeedLabel(speed));
         }
         stringBuilder.append("}");
         return stringBuilder.toString();
     }
 
-    private boolean hasSpeed(ScanResult result) {
-        return mScanResultScores.containsKey(result.BSSID)
-                && mScanResultScores.get(result.BSSID) != Speed.NONE;
+    @Speed private int getSpecificApSpeed(ScanResult result) {
+        TimestampedScoredNetwork timedScore = mScoredNetworkCache.get(result.BSSID);
+        if (timedScore == null) {
+            return Speed.NONE;
+        }
+        // For debugging purposes we may want to use mRssi rather than result.level as the average
+        // speed wil be determined by mRssi
+        return timedScore.getScore().calculateBadge(result.level);
     }
 
     /**
@@ -1067,6 +1149,8 @@
         evictOldScanResults();
         savedState.putParcelableArrayList(KEY_SCANRESULTCACHE,
                 new ArrayList<ScanResult>(mScanResultCache.values()));
+        savedState.putParcelableArrayList(KEY_SCOREDNETWORKCACHE,
+                new ArrayList<>(mScoredNetworkCache.values()));
         if (mNetworkInfo != null) {
             savedState.putParcelable(KEY_NETWORKINFO, mNetworkInfo);
         }
@@ -1105,8 +1189,12 @@
             updateRssi();
             int newLevel = getLevel();
 
-            if (newLevel > 0 && newLevel != oldLevel && mAccessPointListener != null) {
-                mAccessPointListener.onLevelChanged(this);
+            if (newLevel > 0 && newLevel != oldLevel) {
+                // Only update labels on visible rssi changes
+                updateSpeed();
+                if (mAccessPointListener != null) {
+                    mAccessPointListener.onLevelChanged(this);
+                }
             }
             // This flag only comes from scans, is not easily saved in config
             if (security == SECURITY_PSK) {
@@ -1191,7 +1279,23 @@
     }
 
     @Nullable
-    private String getSpeedLabel(int speed) {
+    @Speed
+    private int roundToClosestSpeedEnum(int speed) {
+        if (speed < Speed.SLOW) {
+            return Speed.NONE;
+        } else if (speed < (Speed.SLOW + Speed.MODERATE) / 2) {
+            return Speed.SLOW;
+        } else if (speed < (Speed.MODERATE + Speed.FAST) / 2) {
+            return Speed.MODERATE;
+        } else if (speed < (Speed.FAST + Speed.VERY_FAST) / 2) {
+            return Speed.FAST;
+        } else {
+            return Speed.VERY_FAST;
+        }
+    }
+
+    @Nullable
+    private String getSpeedLabel(@Speed int speed) {
         switch (speed) {
             case Speed.VERY_FAST:
                 return mContext.getString(R.string.speed_label_very_fast);
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java b/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java
index 93bf3c7..3dec1d3 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java
@@ -24,6 +24,7 @@
 import android.net.wifi.WifiInfo;
 import android.os.Bundle;
 import android.support.annotation.Keep;
+
 import com.android.settingslib.wifi.AccessPoint.Speed;
 
 import java.util.ArrayList;
@@ -58,6 +59,7 @@
 
     Context mContext;
     private ArrayList<ScanResult> mScanResultCache;
+    private ArrayList<TimestampedScoredNetwork> mScoredNetworkCache;
 
     @Keep
     public TestAccessPointBuilder(Context context) {
@@ -85,6 +87,9 @@
         if (mScanResultCache != null) {
             bundle.putParcelableArrayList(AccessPoint.KEY_SCANRESULTCACHE, mScanResultCache);
         }
+        if (mScoredNetworkCache != null) {
+            bundle.putParcelableArrayList(AccessPoint.KEY_SCOREDNETWORKCACHE, mScoredNetworkCache);
+        }
         bundle.putInt(AccessPoint.KEY_SECURITY, mSecurity);
         bundle.putInt(AccessPoint.KEY_SPEED, mSpeed);
         bundle.putBoolean(AccessPoint.KEY_IS_CARRIER_AP, mIsCarrierAp);
@@ -238,4 +243,10 @@
         mCarrierName = carrierName;
         return this;
     }
+
+    public TestAccessPointBuilder setScoredNetworkCache(
+            ArrayList<TimestampedScoredNetwork> scoredNetworkCache) {
+        mScoredNetworkCache = scoredNetworkCache;
+        return this;
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/TimestampedScoredNetwork.java b/packages/SettingsLib/src/com/android/settingslib/wifi/TimestampedScoredNetwork.java
new file mode 100644
index 0000000..cb15a79
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/TimestampedScoredNetwork.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.wifi;
+
+import android.net.ScoredNetwork;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Data encapsulation object to associate a time with a {@link ScoredNetwork}
+ */
+class TimestampedScoredNetwork implements Parcelable {
+    private ScoredNetwork mScore;
+    private long mUpdatedTimestampMillis;
+
+    TimestampedScoredNetwork(ScoredNetwork score, long updatedTimestampMillis) {
+        mScore = score;
+        mUpdatedTimestampMillis = updatedTimestampMillis;
+    }
+
+    protected TimestampedScoredNetwork(Parcel in) {
+        mScore = ScoredNetwork.CREATOR.createFromParcel(in);
+        mUpdatedTimestampMillis = in.readLong();
+    }
+
+    public void update(ScoredNetwork score, long updatedTimestampMillis) {
+        mScore = score;
+        mUpdatedTimestampMillis = updatedTimestampMillis;
+    }
+
+    public ScoredNetwork getScore() {
+        return mScore;
+    }
+
+    public long getUpdatedTimestampMillis() {
+        return mUpdatedTimestampMillis;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(mScore, flags);
+        dest.writeLong(mUpdatedTimestampMillis);
+    }
+
+    public static final Creator<TimestampedScoredNetwork> CREATOR =
+            new Creator<TimestampedScoredNetwork>() {
+                @Override
+                public TimestampedScoredNetwork createFromParcel(Parcel in) {
+                    return new TimestampedScoredNetwork(in);
+                }
+
+                @Override
+                public TimestampedScoredNetwork[] newArray(int size) {
+                    return new TimestampedScoredNetwork[size];
+                }
+            };
+}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java
index ae59d37..6f1b25f 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java
@@ -21,8 +21,6 @@
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -50,8 +48,8 @@
 import android.text.style.TtsSpan;
 
 import com.android.settingslib.R;
-
 import com.android.settingslib.wifi.AccessPoint.Speed;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -59,17 +57,36 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class AccessPointTest {
 
-    private static final String TEST_SSID = "test_ssid";
+    private static final String TEST_SSID = "\"test_ssid\"";
+    private static final int NUM_SCAN_RESULTS = 5;
+
+    private static final ArrayList<ScanResult> SCAN_RESULTS = buildScanResultCache();
+
+    private static final RssiCurve FAST_BADGE_CURVE =
+            new RssiCurve(-150, 10, new byte[]{Speed.FAST});
+    public static final String TEST_BSSID = "00:00:00:00:00:00";
+
     private Context mContext;
     @Mock private RssiCurve mockBadgeCurve;
     @Mock private WifiNetworkScoreCache mockWifiNetworkScoreCache;
 
+    private static ScanResult createScanResult(String ssid, String bssid, int rssi) {
+        ScanResult scanResult = new ScanResult();
+        scanResult.SSID = ssid;
+        scanResult.level = rssi;
+        scanResult.BSSID = bssid;
+        scanResult.timestamp = SystemClock.elapsedRealtime() * 1000;
+        scanResult.capabilities = "";
+        return scanResult;
+    }
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -400,7 +417,7 @@
     }
 
     @Test
-    public void testSpeedLabel_isDerivedFromConnectedBssid() {
+    public void testSpeedLabel_isDerivedFromConnectedBssidWhenScoreAvailable() {
         int rssi = -55;
         String bssid = "00:00:00:00:00:00";
         int networkId = 123;
@@ -411,24 +428,42 @@
         info.setBSSID(bssid);
         info.setNetworkId(networkId);
 
+        ArrayList<ScanResult> scanResults = new ArrayList<>();
+        ScanResult scanResultUnconnected = createScanResult(TEST_SSID, "11:11:11:11:11:11", rssi);
+        scanResults.add(scanResultUnconnected);
+
+        ScanResult scanResultConnected = createScanResult(TEST_SSID, bssid, rssi);
+        scanResults.add(scanResultConnected);
+
         AccessPoint ap =
                 new TestAccessPointBuilder(mContext)
                         .setActive(true)
                         .setNetworkId(networkId)
                         .setSsid(TEST_SSID)
-                        .setScanResultCache(buildScanResultCache())
+                        .setScanResultCache(scanResults)
                         .setWifiInfo(info)
                         .build();
 
-        NetworkKey key = new NetworkKey(new WifiKey('"' + TEST_SSID + '"', bssid));
-        when(mockWifiNetworkScoreCache.getScoredNetwork(key))
+        when(mockWifiNetworkScoreCache.getScoredNetwork(scanResultUnconnected))
                 .thenReturn(buildScoredNetworkWithMockBadgeCurve());
-        when(mockBadgeCurve.lookupScore(anyInt())).thenReturn((byte) AccessPoint.Speed.FAST);
+        when(mockBadgeCurve.lookupScore(anyInt())).thenReturn((byte) Speed.SLOW);
+
+        int connectedSpeed = Speed.VERY_FAST;
+        RssiCurve connectedBadgeCurve = mock(RssiCurve.class);
+        Bundle attr1 = new Bundle();
+        attr1.putParcelable(ScoredNetwork.ATTRIBUTES_KEY_BADGING_CURVE, connectedBadgeCurve);
+        ScoredNetwork connectedScore = new ScoredNetwork(
+                NetworkKey.createFromScanResult(scanResultConnected),
+                connectedBadgeCurve,
+                false /* meteredHint */,
+                attr1);
+        when(mockWifiNetworkScoreCache.getScoredNetwork(scanResultConnected))
+                .thenReturn(connectedScore);
+        when(connectedBadgeCurve.lookupScore(anyInt())).thenReturn((byte) connectedSpeed);
 
         ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */);
 
-        verify(mockWifiNetworkScoreCache, times(2)).getScoredNetwork(key);
-        assertThat(ap.getSpeed()).isEqualTo(AccessPoint.Speed.FAST);
+        assertThat(ap.getSpeed()).isEqualTo(connectedSpeed);
     }
 
     @Test
@@ -562,11 +597,16 @@
     }
 
     private ScoredNetwork buildScoredNetworkWithMockBadgeCurve() {
+        return buildScoredNetworkWithGivenBadgeCurve(mockBadgeCurve);
+
+    }
+
+    private ScoredNetwork buildScoredNetworkWithGivenBadgeCurve(RssiCurve badgeCurve) {
         Bundle attr1 = new Bundle();
-        attr1.putParcelable(ScoredNetwork.ATTRIBUTES_KEY_BADGING_CURVE, mockBadgeCurve);
+        attr1.putParcelable(ScoredNetwork.ATTRIBUTES_KEY_BADGING_CURVE, badgeCurve);
         return new ScoredNetwork(
-                new NetworkKey(new WifiKey("\"ssid\"", "00:00:00:00:00:00")),
-                mockBadgeCurve,
+                new NetworkKey(new WifiKey(TEST_SSID, TEST_BSSID)),
+                badgeCurve,
                 false /* meteredHint */,
                 attr1);
 
@@ -574,19 +614,14 @@
 
     private AccessPoint createAccessPointWithScanResultCache() {
         Bundle bundle = new Bundle();
-        ArrayList<ScanResult> scanResults = buildScanResultCache();
-        bundle.putParcelableArrayList(AccessPoint.KEY_SCANRESULTCACHE, scanResults);
+        bundle.putParcelableArrayList(AccessPoint.KEY_SCANRESULTCACHE, SCAN_RESULTS);
         return new AccessPoint(mContext, bundle);
     }
 
-    private ArrayList<ScanResult> buildScanResultCache() {
+    private static ArrayList<ScanResult> buildScanResultCache() {
         ArrayList<ScanResult> scanResults = new ArrayList<>();
         for (int i = 0; i < 5; i++) {
-            ScanResult scanResult = new ScanResult();
-            scanResult.level = i;
-            scanResult.BSSID = "bssid-" + i;
-            scanResult.timestamp = SystemClock.elapsedRealtime() * 1000;
-            scanResult.capabilities = "";
+            ScanResult scanResult = createScanResult(TEST_SSID, "bssid-" + i, i);
             scanResults.add(scanResult);
         }
         return scanResults;
@@ -600,6 +635,18 @@
         return configuration;
     }
 
+    private AccessPoint createApWithFastTimestampedScoredNetworkCache(
+            long elapsedTimeMillis) {
+        TimestampedScoredNetwork recentScore = new TimestampedScoredNetwork(
+                buildScoredNetworkWithGivenBadgeCurve(FAST_BADGE_CURVE),
+                elapsedTimeMillis);
+        return new TestAccessPointBuilder(mContext)
+                .setSsid(TEST_SSID)
+                .setScoredNetworkCache(
+                        new ArrayList<>(Arrays.asList(recentScore)))
+                .build();
+    }
+
     /**
     * Assert that the first AccessPoint appears before the second AccessPoint
     * once sorting has been completed.
@@ -849,4 +896,194 @@
 
         ap.update(null, wifiInfo, networkInfo);
     }
+
+    @Test
+    public void testSpeedLabelAveragesAllBssidScores() {
+        AccessPoint ap = createAccessPointWithScanResultCache();
+
+        int speed1 = Speed.MODERATE;
+        RssiCurve badgeCurve1 = mock(RssiCurve.class);
+        when(badgeCurve1.lookupScore(anyInt())).thenReturn((byte) speed1);
+        when(mockWifiNetworkScoreCache.getScoredNetwork(SCAN_RESULTS.get(0)))
+                .thenReturn(buildScoredNetworkWithGivenBadgeCurve(badgeCurve1));
+        int speed2 = Speed.VERY_FAST;
+        RssiCurve badgeCurve2 = mock(RssiCurve.class);
+        when(badgeCurve2.lookupScore(anyInt())).thenReturn((byte) speed2);
+        when(mockWifiNetworkScoreCache.getScoredNetwork(SCAN_RESULTS.get(1)))
+                .thenReturn(buildScoredNetworkWithGivenBadgeCurve(badgeCurve2));
+
+        int expectedSpeed = (speed1 + speed2) / 2;
+
+        ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */);
+
+        assertThat(ap.getSpeed()).isEqualTo(expectedSpeed);
+    }
+
+    @Test
+    public void testSpeedLabelAverageIgnoresNoSpeedScores() {
+        AccessPoint ap = createAccessPointWithScanResultCache();
+
+        int speed1 = Speed.VERY_FAST;
+        RssiCurve badgeCurve1 = mock(RssiCurve.class);
+        when(badgeCurve1.lookupScore(anyInt())).thenReturn((byte) speed1);
+        when(mockWifiNetworkScoreCache.getScoredNetwork(SCAN_RESULTS.get(0)))
+                .thenReturn(buildScoredNetworkWithGivenBadgeCurve(badgeCurve1));
+        int speed2 = Speed.NONE;
+        RssiCurve badgeCurve2 = mock(RssiCurve.class);
+        when(badgeCurve2.lookupScore(anyInt())).thenReturn((byte) speed2);
+        when(mockWifiNetworkScoreCache.getScoredNetwork(SCAN_RESULTS.get(1)))
+                .thenReturn(buildScoredNetworkWithGivenBadgeCurve(badgeCurve2));
+
+        ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */);
+
+        assertThat(ap.getSpeed()).isEqualTo(speed1);
+    }
+
+    @Test
+    public void testSpeedLabelUsesFallbackScoreWhenConnectedAccessPointScoreUnavailable() {
+        int rssi = -55;
+        String bssid = "00:00:00:00:00:00";
+        int networkId = 123;
+
+        WifiInfo info = new WifiInfo();
+        info.setRssi(rssi);
+        info.setSSID(WifiSsid.createFromAsciiEncoded(TEST_SSID));
+        info.setBSSID(bssid);
+        info.setNetworkId(networkId);
+
+        ArrayList<ScanResult> scanResults = new ArrayList<>();
+        ScanResult scanResultUnconnected = createScanResult(TEST_SSID, "11:11:11:11:11:11", rssi);
+        scanResults.add(scanResultUnconnected);
+
+        ScanResult scanResultConnected = createScanResult(TEST_SSID, bssid, rssi);
+        scanResults.add(scanResultConnected);
+
+        AccessPoint ap =
+                new TestAccessPointBuilder(mContext)
+                        .setActive(true)
+                        .setNetworkId(networkId)
+                        .setSsid(TEST_SSID)
+                        .setScanResultCache(scanResults)
+                        .setWifiInfo(info)
+                        .build();
+
+        int fallbackSpeed = Speed.SLOW;
+        when(mockWifiNetworkScoreCache.getScoredNetwork(scanResultUnconnected))
+                .thenReturn(buildScoredNetworkWithMockBadgeCurve());
+        when(mockBadgeCurve.lookupScore(anyInt())).thenReturn((byte) fallbackSpeed);
+
+        when(mockWifiNetworkScoreCache.getScoredNetwork(scanResultConnected))
+                .thenReturn(null);
+
+        ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */);
+
+        assertThat(ap.getSpeed()).isEqualTo(fallbackSpeed);
+    }
+
+    @Test
+    public void testScoredNetworkCacheBundling() {
+        long timeMillis = SystemClock.elapsedRealtime();
+        AccessPoint ap = createApWithFastTimestampedScoredNetworkCache(timeMillis);
+        Bundle bundle = new Bundle();
+        ap.saveWifiState(bundle);
+
+        ArrayList<TimestampedScoredNetwork> list =
+                bundle.getParcelableArrayList(AccessPoint.KEY_SCOREDNETWORKCACHE);
+        assertThat(list).hasSize(1);
+        assertThat(list.get(0).getUpdatedTimestampMillis()).isEqualTo(timeMillis);
+
+        RssiCurve curve = list.get(0).getScore().attributes.getParcelable(
+                ScoredNetwork.ATTRIBUTES_KEY_BADGING_CURVE);
+        assertThat(curve).isEqualTo(FAST_BADGE_CURVE);
+    }
+
+    @Test
+    public void testRecentNetworkScoresAreUsedForSpeedLabelGeneration() {
+        AccessPoint ap =
+                createApWithFastTimestampedScoredNetworkCache(SystemClock.elapsedRealtime());
+
+        ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */);
+
+        assertThat(ap.getSpeed()).isEqualTo(Speed.FAST);
+    }
+
+    @Test
+    public void testNetworkScoresAreUsedForSpeedLabelGenerationWhenWithinAgeRange() {
+        long withinRangeTimeMillis =
+                SystemClock.elapsedRealtime() - (AccessPoint.MAX_CACHED_SCORE_AGE_MILLIS - 10000);
+        AccessPoint ap =
+                createApWithFastTimestampedScoredNetworkCache(withinRangeTimeMillis);
+
+        ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */);
+
+        assertThat(ap.getSpeed()).isEqualTo(Speed.FAST);
+    }
+
+    @Test
+    public void testOldNetworkScoresAreNotUsedForSpeedLabelGeneration() {
+        long tooOldTimeMillis =
+                SystemClock.elapsedRealtime() - (AccessPoint.MAX_CACHED_SCORE_AGE_MILLIS + 1);
+        AccessPoint ap =
+                createApWithFastTimestampedScoredNetworkCache(tooOldTimeMillis);
+
+        ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */);
+
+        assertThat(ap.getSpeed()).isEqualTo(Speed.NONE);
+    }
+
+    @Test
+    public void testUpdateScoresRefreshesScoredNetworkCacheTimestamps () {
+        long tooOldTimeMillis =
+                SystemClock.elapsedRealtime() - (AccessPoint.MAX_CACHED_SCORE_AGE_MILLIS + 1);
+
+        ScoredNetwork scoredNetwork = buildScoredNetworkWithGivenBadgeCurve(FAST_BADGE_CURVE);
+        TimestampedScoredNetwork recentScore = new TimestampedScoredNetwork(
+                scoredNetwork,
+                tooOldTimeMillis);
+        AccessPoint ap = new TestAccessPointBuilder(mContext)
+                .setSsid(TEST_SSID)
+                .setBssid(TEST_BSSID)
+                .setActive(true)
+                .setScoredNetworkCache(
+                        new ArrayList(Arrays.asList(recentScore)))
+                .setScanResultCache(SCAN_RESULTS)
+                .build();
+
+        when(mockWifiNetworkScoreCache.getScoredNetwork(any(ScanResult.class)))
+                .thenReturn(scoredNetwork);
+
+        ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */);
+
+        // Fast should still be returned since cache was updated with recent time
+        assertThat(ap.getSpeed()).isEqualTo(Speed.FAST);
+    }
+
+    @Test
+    public void testUpdateScoresRefreshesScoredNetworkCacheWithNewSpeed () {
+        long tooOldTimeMillis =
+                SystemClock.elapsedRealtime() - (AccessPoint.MAX_CACHED_SCORE_AGE_MILLIS + 1);
+
+        ScoredNetwork scoredNetwork = buildScoredNetworkWithGivenBadgeCurve(FAST_BADGE_CURVE);
+        TimestampedScoredNetwork recentScore = new TimestampedScoredNetwork(
+                scoredNetwork,
+                tooOldTimeMillis);
+        AccessPoint ap = new TestAccessPointBuilder(mContext)
+                .setSsid(TEST_SSID)
+                .setBssid(TEST_BSSID)
+                .setActive(true)
+                .setScoredNetworkCache(
+                        new ArrayList(Arrays.asList(recentScore)))
+                .setScanResultCache(SCAN_RESULTS)
+                .build();
+
+        int newSpeed = Speed.MODERATE;
+        when(mockWifiNetworkScoreCache.getScoredNetwork(any(ScanResult.class)))
+                .thenReturn(buildScoredNetworkWithMockBadgeCurve());
+        when(mockBadgeCurve.lookupScore(anyInt())).thenReturn((byte) newSpeed);
+
+        ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */);
+
+        // Fast should still be returned since cache was updated with recent time
+        assertThat(ap.getSpeed()).isEqualTo(newSpeed);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
index 562210c..f844866 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
@@ -367,7 +367,7 @@
         });
 
         // Set the window background
-        getWindow().setBackgroundDrawable(mRecentsView.getBackgroundScrim());
+        mRecentsView.updateBackgroundScrim(getWindow(), isInMultiWindowMode());
 
         // Create the home intent runnable
         mHomeIntent = new Intent(Intent.ACTION_MAIN, null);
@@ -556,6 +556,9 @@
     public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
         super.onMultiWindowModeChanged(isInMultiWindowMode);
 
+        // Set the window background
+        mRecentsView.updateBackgroundScrim(getWindow(), isInMultiWindowMode);
+
         reloadTaskStack(isInMultiWindowMode, true /* sendConfigChangedEvent */);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
index 1b86143..79558a3 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
@@ -24,7 +24,6 @@
 import android.app.ActivityManager;
 import android.app.ActivityManager.TaskSnapshot;
 import android.app.ActivityOptions;
-import android.app.ActivityOptions.OnAnimationFinishedListener;
 import android.app.ActivityOptions.OnAnimationStartedListener;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
@@ -36,6 +35,7 @@
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
 import android.os.SystemClock;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.MutableBoolean;
 import android.util.Pair;
@@ -76,6 +76,7 @@
 import com.android.systemui.recents.model.RecentsTaskLoadPlan;
 import com.android.systemui.recents.model.RecentsTaskLoader;
 import com.android.systemui.recents.model.Task;
+import com.android.systemui.recents.model.Task.TaskKey;
 import com.android.systemui.recents.model.TaskGrouping;
 import com.android.systemui.recents.model.TaskStack;
 import com.android.systemui.recents.model.ThumbnailData;
@@ -110,6 +111,8 @@
     // duration, then we will toggle recents after this duration.
     private final static int FAST_ALT_TAB_DELAY_MS = 225;
 
+    private final static ArraySet<TaskKey> EMPTY_SET = new ArraySet<>();
+
     public final static String RECENTS_PACKAGE = "com.android.systemui";
     public final static String RECENTS_ACTIVITY = "com.android.systemui.recents.RecentsActivity";
 
@@ -129,39 +132,38 @@
             // Preloads the next task
             RecentsConfiguration config = Recents.getConfiguration();
             if (config.svelteLevel == RecentsConfiguration.SVELTE_NONE) {
-
                 // Load the next task only if we aren't svelte
                 SystemServicesProxy ssp = Recents.getSystemServices();
                 ActivityManager.RunningTaskInfo runningTaskInfo = ssp.getRunningTask();
                 RecentsTaskLoader loader = Recents.getTaskLoader();
                 RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
                 loader.preloadTasks(plan, -1, false /* includeFrontMostExcludedTask */);
+                TaskStack stack = plan.getTaskStack();
+                RecentsActivityLaunchState launchState = new RecentsActivityLaunchState();
+                RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
 
-                // This callback is made when a new activity is launched and the old one is paused
-                // so ignore the current activity and try and preload the thumbnail for the
-                // previous one.
-                VisibilityReport visibilityReport;
-                synchronized (mDummyStackView) {
-                    mDummyStackView.getStack().removeAllTasks(false /* notifyStackChanges */);
-                    mDummyStackView.setTasks(plan.getTaskStack(), false /* allowNotify */);
-                    updateDummyStackViewLayout(plan.getTaskStack(),
+                synchronized (mBackgroundLayoutAlgorithm) {
+                    // This callback is made when a new activity is launched and the old one is
+                    // paused so ignore the current activity and try and preload the thumbnail for
+                    // the previous one.
+                    updateDummyStackViewLayout(mBackgroundLayoutAlgorithm, stack,
                             getWindowRect(null /* windowRectOverride */));
 
                     // Launched from app is always the worst case (in terms of how many
                     // thumbnails/tasks visible)
-                    RecentsActivityLaunchState launchState = new RecentsActivityLaunchState();
                     launchState.launchedFromApp = true;
-                    mDummyStackView.updateLayoutAlgorithm(true /* boundScroll */, launchState);
-                    visibilityReport = mDummyStackView.computeStackVisibilityReport();
-                }
+                    mBackgroundLayoutAlgorithm.update(plan.getTaskStack(), EMPTY_SET, launchState);
+                    VisibilityReport visibilityReport =
+                            mBackgroundLayoutAlgorithm.computeStackVisibilityReport(
+                                    stack.getStackTasks());
 
-                RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
-                launchOpts.runningTaskId = runningTaskInfo != null ? runningTaskInfo.id : -1;
-                launchOpts.numVisibleTasks = visibilityReport.numVisibleTasks;
-                launchOpts.numVisibleTaskThumbnails = visibilityReport.numVisibleThumbnails;
-                launchOpts.onlyLoadForCache = true;
-                launchOpts.onlyLoadPausedActivities = true;
-                launchOpts.loadThumbnails = true;
+                    launchOpts.runningTaskId = runningTaskInfo != null ? runningTaskInfo.id : -1;
+                    launchOpts.numVisibleTasks = visibilityReport.numVisibleTasks;
+                    launchOpts.numVisibleTaskThumbnails = visibilityReport.numVisibleThumbnails;
+                    launchOpts.onlyLoadForCache = true;
+                    launchOpts.onlyLoadPausedActivities = true;
+                    launchOpts.loadThumbnails = true;
+                }
                 loader.loadTasks(mContext, plan, launchOpts);
             }
         }
@@ -230,17 +232,15 @@
     boolean mLaunchedWhileDocking;
 
     // Task launching
-    Rect mTaskStackBounds = new Rect();
+    Rect mTmpBounds = new Rect();
     TaskViewTransform mTmpTransform = new TaskViewTransform();
-    int mStatusBarHeight;
-    int mNavBarHeight;
-    int mNavBarWidth;
     int mTaskBarHeight;
 
     // Header (for transition)
     TaskViewHeader mHeaderBar;
     final Object mHeaderBarLock = new Object();
-    protected TaskStackView mDummyStackView;
+    private TaskStackView mDummyStackView;
+    private TaskStackLayoutAlgorithm mBackgroundLayoutAlgorithm;
 
     // Variables to keep track of if we need to start recents after binding
     protected boolean mTriggeredFromAltTab;
@@ -259,6 +259,7 @@
     public RecentsImpl(Context context) {
         mContext = context;
         mHandler = new Handler();
+        mBackgroundLayoutAlgorithm = new TaskStackLayoutAlgorithm(context, null);
 
         // Initialize the static foreground thread
         ForegroundThread.get();
@@ -288,8 +289,9 @@
 
     public void onConfigurationChanged() {
         reloadResources();
-        synchronized (mDummyStackView) {
-            mDummyStackView.reloadOnConfigurationChange();
+        mDummyStackView.reloadOnConfigurationChange();
+        synchronized (mBackgroundLayoutAlgorithm) {
+            mBackgroundLayoutAlgorithm.reloadOnConfigurationChange(mContext);
         }
     }
 
@@ -698,12 +700,6 @@
     private void reloadResources() {
         Resources res = mContext.getResources();
 
-        mStatusBarHeight = res.getDimensionPixelSize(
-                com.android.internal.R.dimen.status_bar_height);
-        mNavBarHeight = res.getDimensionPixelSize(
-                com.android.internal.R.dimen.navigation_bar_height);
-        mNavBarWidth = res.getDimensionPixelSize(
-                com.android.internal.R.dimen.navigation_bar_width);
         mTaskBarHeight = TaskStackLayoutAlgorithm.getDimensionForDevice(mContext,
                 R.dimen.recents_task_view_header_height,
                 R.dimen.recents_task_view_header_height,
@@ -719,7 +715,8 @@
         mHeaderBar.setLayoutDirection(res.getConfiguration().getLayoutDirection());
     }
 
-    private void updateDummyStackViewLayout(TaskStack stack, Rect windowRect) {
+    private void updateDummyStackViewLayout(TaskStackLayoutAlgorithm stackLayout,
+            TaskStack stack, Rect windowRect) {
         SystemServicesProxy ssp = Recents.getSystemServices();
         Rect displayRect = ssp.getDisplayRect();
         Rect systemInsets = new Rect();
@@ -735,18 +732,14 @@
         calculateWindowStableInsets(systemInsets, windowRect, displayRect);
         windowRect.offsetTo(0, 0);
 
-        synchronized (mDummyStackView) {
-            TaskStackLayoutAlgorithm stackLayout = mDummyStackView.getStackAlgorithm();
-
-            // Rebind the header bar and draw it for the transition
-            stackLayout.setSystemInsets(systemInsets);
-            if (stack != null) {
-                stackLayout.getTaskStackBounds(displayRect, windowRect, systemInsets.top,
-                        systemInsets.left, systemInsets.right, mTaskStackBounds);
-                stackLayout.reset();
-                stackLayout.initialize(displayRect, windowRect, mTaskStackBounds,
-                        TaskStackLayoutAlgorithm.StackState.getStackStateForStack(stack));
-            }
+        // Rebind the header bar and draw it for the transition
+        stackLayout.setSystemInsets(systemInsets);
+        if (stack != null) {
+            stackLayout.getTaskStackBounds(displayRect, windowRect, systemInsets.top,
+                    systemInsets.left, systemInsets.right, mTmpBounds);
+            stackLayout.reset();
+            stackLayout.initialize(displayRect, windowRect, mTmpBounds,
+                    TaskStackLayoutAlgorithm.StackState.getStackStateForStack(stack));
         }
     }
 
@@ -768,26 +761,23 @@
     private void updateHeaderBarLayout(TaskStack stack, Rect windowRectOverride) {
         Rect windowRect = getWindowRect(windowRectOverride);
         int taskViewWidth = 0;
-        boolean useGridLayout = false;
-        synchronized (mDummyStackView) {
-            useGridLayout = mDummyStackView.useGridLayout();
-            updateDummyStackViewLayout(stack, windowRect);
-            if (stack != null) {
-                TaskStackLayoutAlgorithm stackLayout = mDummyStackView.getStackAlgorithm();
-                mDummyStackView.getStack().removeAllTasks(false /* notifyStackChanges */);
-                mDummyStackView.setTasks(stack, false /* allowNotifyStackChanges */);
-                // Get the width of a task view so that we know how wide to draw the header bar.
-                if (useGridLayout) {
-                    TaskGridLayoutAlgorithm gridLayout = mDummyStackView.getGridAlgorithm();
-                    gridLayout.initialize(windowRect);
-                    taskViewWidth = (int) gridLayout.getTransform(0 /* taskIndex */,
-                            stack.getTaskCount(), new TaskViewTransform(),
-                            stackLayout).rect.width();
-                } else {
-                    Rect taskViewBounds = stackLayout.getUntransformedTaskViewBounds();
-                    if (!taskViewBounds.isEmpty()) {
-                        taskViewWidth = taskViewBounds.width();
-                    }
+        boolean useGridLayout = mDummyStackView.useGridLayout();
+        updateDummyStackViewLayout(mDummyStackView.getStackAlgorithm(), stack, windowRect);
+        if (stack != null) {
+            TaskStackLayoutAlgorithm stackLayout = mDummyStackView.getStackAlgorithm();
+            mDummyStackView.getStack().removeAllTasks(false /* notifyStackChanges */);
+            mDummyStackView.setTasks(stack, false /* allowNotifyStackChanges */);
+            // Get the width of a task view so that we know how wide to draw the header bar.
+            if (useGridLayout) {
+                TaskGridLayoutAlgorithm gridLayout = mDummyStackView.getGridAlgorithm();
+                gridLayout.initialize(windowRect);
+                taskViewWidth = (int) gridLayout.getTransform(0 /* taskIndex */,
+                        stack.getTaskCount(), new TaskViewTransform(),
+                        stackLayout).rect.width();
+            } else {
+                Rect taskViewBounds = stackLayout.getUntransformedTaskViewBounds();
+                if (!taskViewBounds.isEmpty()) {
+                    taskViewWidth = taskViewBounds.width();
                 }
             }
         }
@@ -870,18 +860,12 @@
         final boolean isLowRamDevice = Recents.getConfiguration().isLowRamDevice;
         if (runningTask != null && runningTask.stackId == FREEFORM_WORKSPACE_STACK_ID) {
             ArrayList<AppTransitionAnimationSpec> specs = new ArrayList<>();
-            ArrayList<Task> tasks;
-            TaskStackLayoutAlgorithm stackLayout;
-            TaskStackViewScroller stackScroller;
+            ArrayList<Task> tasks = mDummyStackView.getStack().getStackTasks();
+            TaskStackLayoutAlgorithm stackLayout = mDummyStackView.getStackAlgorithm();
+            TaskStackViewScroller stackScroller = mDummyStackView.getScroller();
 
-            synchronized (mDummyStackView) {
-                tasks = mDummyStackView.getStack().getStackTasks();
-                stackLayout = mDummyStackView.getStackAlgorithm();
-                stackScroller = mDummyStackView.getScroller();
-
-                mDummyStackView.updateLayoutAlgorithm(true /* boundScroll */);
-                mDummyStackView.updateToInitialState();
-            }
+            mDummyStackView.updateLayoutAlgorithm(true /* boundScroll */);
+            mDummyStackView.updateToInitialState();
 
             for (int i = tasks.size() - 1; i >= 0; i--) {
                 Task task = tasks.get(i);
@@ -1044,10 +1028,8 @@
         updateHeaderBarLayout(stack, windowOverrideRect);
 
         // Prepare the dummy stack for the transition
-        TaskStackLayoutAlgorithm.VisibilityReport stackVr;
-        synchronized (mDummyStackView) {
-            stackVr = mDummyStackView.computeStackVisibilityReport();
-        }
+        TaskStackLayoutAlgorithm.VisibilityReport stackVr =
+                mDummyStackView.computeStackVisibilityReport();
 
         // Update the remaining launch state
         launchState.launchedNumVisibleTasks = stackVr.numVisibleTasks;
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
index ccaf3cd..71f06cb 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
@@ -20,13 +20,17 @@
 
 import android.animation.Animator;
 import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.app.ActivityOptions.OnAnimationStartedListener;
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.util.ArraySet;
 import android.util.AttributeSet;
@@ -37,6 +41,7 @@
 import android.view.View;
 import android.view.ViewDebug;
 import android.view.ViewPropertyAnimator;
+import android.view.Window;
 import android.view.WindowInsets;
 import android.widget.FrameLayout;
 import android.widget.TextView;
@@ -117,7 +122,15 @@
 
     private float mBusynessFactor;
     private GradientDrawable mBackgroundScrim;
-    private Animator mBackgroundScrimAnimator;
+    private ColorDrawable mMultiWindowBackgroundScrim;
+    private ValueAnimator mBackgroundScrimAnimator;
+    private Point mTmpDisplaySize = new Point();
+
+    private final AnimatorUpdateListener mUpdateBackgroundScrimAlpha = (animation) -> {
+        int alpha = (Integer) animation.getAnimatedValue();
+        mBackgroundScrim.setAlpha(alpha);
+        mMultiWindowBackgroundScrim.setAlpha(alpha);
+    };
 
     private RecentsTransitionHelper mTransitionHelper;
     @ViewDebug.ExportedProperty(deepExport=true, prefix="touch_")
@@ -146,10 +159,7 @@
         mTouchHandler = new RecentsViewTouchHandler(this);
         mFlingAnimationUtils = new FlingAnimationUtils(context, 0.3f);
         mBackgroundScrim = new GradientDrawable(context);
-        mBackgroundScrim.setCallback(this);
-
-        boolean usingDarkText = Color.luminance(
-                Utils.getColorAttr(mContext, R.attr.wallpaperTextColor)) < 0.5f;
+        mMultiWindowBackgroundScrim = new ColorDrawable();
 
         LayoutInflater inflater = LayoutInflater.from(context);
         mEmptyView = (TextView) inflater.inflate(R.layout.recents_empty, this, false);
@@ -244,6 +254,7 @@
             } else {
                 mBackgroundScrim.setAlpha(0);
             }
+            mMultiWindowBackgroundScrim.setAlpha(mBackgroundScrim.getAlpha());
         }
     }
 
@@ -300,8 +311,14 @@
     /**
      * Returns the window background scrim.
      */
-    public Drawable getBackgroundScrim() {
-        return mBackgroundScrim;
+    public void updateBackgroundScrim(Window window, boolean isInMultiWindow) {
+        if (isInMultiWindow) {
+            mBackgroundScrim.setCallback(null);
+            window.setBackgroundDrawable(mMultiWindowBackgroundScrim);
+        } else {
+            mMultiWindowBackgroundScrim.setCallback(null);
+            window.setBackgroundDrawable(mBackgroundScrim);
+        }
     }
 
     /**
@@ -401,6 +418,9 @@
      */
     public void setScrimColors(ColorExtractor.GradientColors scrimColors, boolean animated) {
         mBackgroundScrim.setColors(scrimColors, animated);
+        int alpha = mMultiWindowBackgroundScrim.getAlpha();
+        mMultiWindowBackgroundScrim.setColor(scrimColors.getMainColor());
+        mMultiWindowBackgroundScrim.setAlpha(alpha);
     }
 
     @Override
@@ -470,8 +490,10 @@
 
         // Needs to know the screen size since the gradient never scales up or down
         // even when bounds change.
-        mBackgroundScrim.setScreenSize(right - left, bottom - top);
+        mContext.getDisplay().getRealSize(mTmpDisplaySize);
+        mBackgroundScrim.setScreenSize(mTmpDisplaySize.x, mTmpDisplaySize.y);
         mBackgroundScrim.setBounds(left, top, right, bottom);
+        mMultiWindowBackgroundScrim.setBounds(0, 0, mTmpDisplaySize.x, mTmpDisplaySize.y);
 
         if (RecentsDebugFlags.Static.EnableStackActionButton) {
             // Layout the stack action button such that its drawable is start-aligned with the
@@ -916,12 +938,12 @@
         // Calculate the absolute alpha to animate from
         final int fromAlpha = mBackgroundScrim.getAlpha();
         final int toAlpha = (int) (alpha * 255);
-        mBackgroundScrimAnimator = ObjectAnimator.ofInt(mBackgroundScrim, Utilities.DRAWABLE_ALPHA,
-                fromAlpha, toAlpha);
+        mBackgroundScrimAnimator = ValueAnimator.ofInt(fromAlpha, toAlpha);
         mBackgroundScrimAnimator.setDuration(duration);
         mBackgroundScrimAnimator.setInterpolator(toAlpha > fromAlpha
                 ? Interpolators.ALPHA_IN
                 : Interpolators.ALPHA_OUT);
+        mBackgroundScrimAnimator.addUpdateListener(mUpdateBackgroundScrimAlpha);
         mBackgroundScrimAnimator.start();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
index d810ea4..eaa32ee 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
@@ -354,7 +354,6 @@
     TaskViewTransform mFrontOfStackTransform = new TaskViewTransform();
 
     public TaskStackLayoutAlgorithm(Context context, TaskStackLayoutAlgorithmCallbacks cb) {
-        Resources res = context.getResources();
         mContext = context;
         mCb = cb;
         mFreeformLayoutAlgorithm = new FreeformWorkspaceLayoutAlgorithm(context);
@@ -519,7 +518,7 @@
      * Computes the minimum and maximum scroll progress values and the progress values for each task
      * in the stack.
      */
-    void update(TaskStack stack, ArraySet<Task.TaskKey> ignoreTasksSet,
+    public void update(TaskStack stack, ArraySet<Task.TaskKey> ignoreTasksSet,
             RecentsActivityLaunchState launchState) {
         SystemServicesProxy ssp = Recents.getSystemServices();
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 04698827..694c72f 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -714,6 +714,7 @@
                 && (mAudioManager.isStreamAffectedByRingerMode(mActiveStream) || mExpanded)
                 && !mZenPanel.isEditing();
 
+        TransitionManager.endTransitions(mDialogView);
         TransitionManager.beginDelayedTransition(mDialogView, getTransition());
         if (wasVisible != visible && !visible) {
             prepareForCollapse();
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 32d3445..5c5a6b7 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -8045,7 +8045,7 @@
                 // Activity supports picture-in-picture, now check that we can enter PiP at this
                 // point, if it is
                 if (!r.checkEnterPictureInPictureState("enterPictureInPictureMode",
-                        false /* noThrow */, false /* beforeStopping */)) {
+                        false /* beforeStopping */)) {
                     return false;
                 }
 
diff --git a/services/core/java/com/android/server/am/ActivityRecord.java b/services/core/java/com/android/server/am/ActivityRecord.java
index e5985c5..874bd1e 100644
--- a/services/core/java/com/android/server/am/ActivityRecord.java
+++ b/services/core/java/com/android/server/am/ActivityRecord.java
@@ -1186,10 +1186,9 @@
      * @param beforeStopping Whether this check is for an auto-enter-pip operation, that is to say
      *         the activity has requested to enter PiP when it would otherwise be stopped.
      *
-     * @return whether this activity is currently allowed to enter PIP, throwing an exception if
-     *         the activity is not currently visible and {@param noThrow} is not set.
+     * @return whether this activity is currently allowed to enter PIP.
      */
-    boolean checkEnterPictureInPictureState(String caller, boolean noThrow, boolean beforeStopping) {
+    boolean checkEnterPictureInPictureState(String caller, boolean beforeStopping) {
         if (!supportsPictureInPicture()) {
             return false;
         }
@@ -1237,13 +1236,7 @@
                     return isNotLockedOrOnKeyguard && !hasPinnedStack;
                 }
             default:
-                if (noThrow) {
-                    return false;
-                } else {
-                    throw new IllegalStateException(caller
-                            + ": Current activity is not visible (state=" + state.name() + ") "
-                            + "r=" + this);
-                }
+                return false;
         }
     }
 
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index c2656a1..eb3177a 100644
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -2076,7 +2076,7 @@
         if (DEBUG_VISIBILITY) Slog.v(TAG_VISIBILITY, "Making invisible: " + r + " " + r.state);
         try {
             final boolean canEnterPictureInPicture = r.checkEnterPictureInPictureState(
-                    "makeInvisible", true /* noThrow */, true /* beforeStopping */);
+                    "makeInvisible", true /* beforeStopping */);
             // Defer telling the client it is hidden if it can enter Pip and isn't current stopped
             // or stopping. This gives it a chance to enter Pip in onPause().
             final boolean deferHidingClient = canEnterPictureInPicture
@@ -2390,7 +2390,7 @@
             // represent the last resumed activity. However, the last focus stack does if it isn't null.
             final ActivityRecord lastResumed = lastFocusedStack.mResumedActivity;
             lastResumedCanPip = lastResumed != null && lastResumed.checkEnterPictureInPictureState(
-                    "resumeTopActivity", true /* noThrow */, userLeaving /* beforeStopping */);
+                    "resumeTopActivity", userLeaving /* beforeStopping */);
         }
         // If the flag RESUME_WHILE_PAUSING is set, then continue to schedule the previous activity
         // to be paused, while at the same time resuming the new resume activity only if the
diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java
index 2b4f4e6..a985b4f 100644
--- a/services/core/java/com/android/server/connectivity/Tethering.java
+++ b/services/core/java/com/android/server/connectivity/Tethering.java
@@ -49,6 +49,7 @@
 import android.net.INetworkPolicyManager;
 import android.net.INetworkStatsService;
 import android.net.IpPrefix;
+import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
@@ -1196,6 +1197,7 @@
         // to tear itself down.
         private final ArrayList<TetherInterfaceStateMachine> mNotifyList;
         private final IPv6TetheringCoordinator mIPv6TetheringCoordinator;
+        private final OffloadWrapper mOffload;
 
         private static final int UPSTREAM_SETTLE_TIME_MS     = 10000;
 
@@ -1220,33 +1222,11 @@
 
             mNotifyList = new ArrayList<>();
             mIPv6TetheringCoordinator = new IPv6TetheringCoordinator(mNotifyList, mLog);
+            mOffload = new OffloadWrapper();
 
             setInitialState(mInitialState);
         }
 
-        private void startOffloadController() {
-            mOffloadController.start();
-            sendOffloadExemptPrefixes();
-        }
-
-        private void sendOffloadExemptPrefixes() {
-            sendOffloadExemptPrefixes(mUpstreamNetworkMonitor.getLocalPrefixes());
-        }
-
-        private void sendOffloadExemptPrefixes(Set<IpPrefix> localPrefixes) {
-            // Add in well-known minimum set.
-            PrefixUtils.addNonForwardablePrefixes(localPrefixes);
-            // Add tragically hardcoded prefixes.
-            localPrefixes.add(PrefixUtils.DEFAULT_WIFI_P2P_PREFIX);
-
-            // Add prefixes for all downstreams, regardless of IP serving mode.
-            for (TetherInterfaceStateMachine tism : mNotifyList) {
-                localPrefixes.addAll(PrefixUtils.localPrefixesFrom(tism.linkProperties()));
-            }
-
-            mOffloadController.setLocalPrefixes(localPrefixes);
-        }
-
         class InitialState extends State {
             @Override
             public boolean processMessage(Message message) {
@@ -1404,7 +1384,7 @@
 
         protected void handleNewUpstreamNetworkState(NetworkState ns) {
             mIPv6TetheringCoordinator.updateUpstreamNetworkState(ns);
-            mOffloadController.setUpstreamLinkProperties((ns != null) ? ns.linkProperties : null);
+            mOffload.updateUpstreamNetworkState(ns);
         }
 
         private void handleInterfaceServingStateActive(int mode, TetherInterfaceStateMachine who) {
@@ -1414,9 +1394,12 @@
             }
 
             if (mode == IControlsTethering.STATE_TETHERED) {
+                // No need to notify OffloadController just yet as there are no
+                // "offload-able" prefixes to pass along. This will handled
+                // when the TISM informs Tethering of its LinkProperties.
                 mForwardedDownstreams.add(who);
             } else {
-                mOffloadController.removeDownstreamInterface(who.interfaceName());
+                mOffload.excludeDownstreamInterface(who.interfaceName());
                 mForwardedDownstreams.remove(who);
             }
 
@@ -1441,7 +1424,7 @@
         private void handleInterfaceServingStateInactive(TetherInterfaceStateMachine who) {
             mNotifyList.remove(who);
             mIPv6TetheringCoordinator.removeActiveDownstream(who);
-            mOffloadController.removeDownstreamInterface(who.interfaceName());
+            mOffload.excludeDownstreamInterface(who.interfaceName());
             mForwardedDownstreams.remove(who);
 
             // If this is a Wi-Fi interface, tell WifiManager of any errors.
@@ -1455,7 +1438,7 @@
 
         private void handleUpstreamNetworkMonitorCallback(int arg1, Object o) {
             if (arg1 == UpstreamNetworkMonitor.NOTIFY_LOCAL_PREFIXES) {
-                sendOffloadExemptPrefixes((Set<IpPrefix>) o);
+                mOffload.sendOffloadExemptPrefixes((Set<IpPrefix>) o);
                 return;
             }
 
@@ -1525,7 +1508,7 @@
                 // TODO: De-duplicate with updateUpstreamWanted() below.
                 if (upstreamWanted()) {
                     mUpstreamWanted = true;
-                    startOffloadController();
+                    mOffload.start();
                     chooseUpstreamType(true);
                     mTryCell = false;
                 }
@@ -1533,7 +1516,7 @@
 
             @Override
             public void exit() {
-                mOffloadController.stop();
+                mOffload.stop();
                 mUpstreamNetworkMonitor.stop();
                 mSimChange.stopListening();
                 notifyDownstreamsOfNewUpstreamIface(null);
@@ -1545,9 +1528,9 @@
                 mUpstreamWanted = upstreamWanted();
                 if (mUpstreamWanted != previousUpstreamWanted) {
                     if (mUpstreamWanted) {
-                        startOffloadController();
+                        mOffload.start();
                     } else {
-                        mOffloadController.stop();
+                        mOffload.stop();
                     }
                 }
                 return previousUpstreamWanted;
@@ -1602,12 +1585,9 @@
                     case EVENT_IFACE_UPDATE_LINKPROPERTIES: {
                         final LinkProperties newLp = (LinkProperties) message.obj;
                         if (message.arg1 == IControlsTethering.STATE_TETHERED) {
-                            mOffloadController.notifyDownstreamLinkProperties(newLp);
+                            mOffload.updateDownstreamLinkProperties(newLp);
                         } else {
-                            mOffloadController.removeDownstreamInterface(newLp.getInterfaceName());
-                            // Another interface might be in local-only hotspot mode;
-                            // resend all local prefixes to the OffloadController.
-                            sendOffloadExemptPrefixes();
+                            mOffload.excludeDownstreamInterface(newLp.getInterfaceName());
                         }
                         break;
                     }
@@ -1722,6 +1702,82 @@
                 } catch (Exception e) {}
             }
         }
+
+        // A wrapper class to handle multiple situations where several calls to
+        // the OffloadController need to happen together.
+        //
+        // TODO: This suggests that the interface between OffloadController and
+        // Tethering is in need of improvement. Refactor these calls into the
+        // OffloadController implementation.
+        class OffloadWrapper {
+            public void start() {
+                mOffloadController.start();
+                sendOffloadExemptPrefixes();
+            }
+
+            public void stop() {
+                mOffloadController.stop();
+            }
+
+            public void updateUpstreamNetworkState(NetworkState ns) {
+                mOffloadController.setUpstreamLinkProperties(
+                        (ns != null) ? ns.linkProperties : null);
+            }
+
+            public void updateDownstreamLinkProperties(LinkProperties newLp) {
+                // Update the list of offload-exempt prefixes before adding
+                // new prefixes on downstream interfaces to the offload HAL.
+                sendOffloadExemptPrefixes();
+                mOffloadController.notifyDownstreamLinkProperties(newLp);
+            }
+
+            public void excludeDownstreamInterface(String ifname) {
+                // This and other interfaces may be in local-only hotspot mode;
+                // resend all local prefixes to the OffloadController.
+                sendOffloadExemptPrefixes();
+                mOffloadController.removeDownstreamInterface(ifname);
+            }
+
+            public void sendOffloadExemptPrefixes() {
+                sendOffloadExemptPrefixes(mUpstreamNetworkMonitor.getLocalPrefixes());
+            }
+
+            public void sendOffloadExemptPrefixes(final Set<IpPrefix> localPrefixes) {
+                // Add in well-known minimum set.
+                PrefixUtils.addNonForwardablePrefixes(localPrefixes);
+                // Add tragically hardcoded prefixes.
+                localPrefixes.add(PrefixUtils.DEFAULT_WIFI_P2P_PREFIX);
+
+                // Maybe add prefixes or addresses for downstreams, depending on
+                // the IP serving mode of each.
+                for (TetherInterfaceStateMachine tism : mNotifyList) {
+                    final LinkProperties lp = tism.linkProperties();
+
+                    switch (tism.servingMode()) {
+                        case IControlsTethering.STATE_UNAVAILABLE:
+                        case IControlsTethering.STATE_AVAILABLE:
+                            // No usable LinkProperties in these states.
+                            continue;
+                        case IControlsTethering.STATE_TETHERED:
+                            // Only add IPv4 /32 and IPv6 /128 prefixes. The
+                            // directly-connected prefixes will be sent as
+                            // downstream "offload-able" prefixes.
+                            for (LinkAddress addr : lp.getAllLinkAddresses()) {
+                                final InetAddress ip = addr.getAddress();
+                                if (ip.isLinkLocalAddress()) continue;
+                                localPrefixes.add(PrefixUtils.ipAddressAsPrefix(ip));
+                            }
+                            break;
+                        case IControlsTethering.STATE_LOCAL_ONLY:
+                            // Add prefixes covering all local IPs.
+                            localPrefixes.addAll(PrefixUtils.localPrefixesFrom(lp));
+                            break;
+                    }
+                }
+
+                mOffloadController.setLocalPrefixes(localPrefixes);
+            }
+        }
     }
 
     @Override
diff --git a/services/core/java/com/android/server/connectivity/tethering/OffloadController.java b/services/core/java/com/android/server/connectivity/tethering/OffloadController.java
index ef18e4e..6d5c428 100644
--- a/services/core/java/com/android/server/connectivity/tethering/OffloadController.java
+++ b/services/core/java/com/android/server/connectivity/tethering/OffloadController.java
@@ -48,6 +48,7 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -69,6 +70,7 @@
     private final INetworkManagementService mNms;
     private final ITetheringStatsProvider mStatsProvider;
     private final SharedLog mLog;
+    private final HashMap<String, LinkProperties> mDownstreams;
     private boolean mConfigInitialized;
     private boolean mControlInitialized;
     private LinkProperties mUpstreamLinkProperties;
@@ -100,6 +102,7 @@
         mNms = nms;
         mStatsProvider = new OffloadTetheringStatsProvider();
         mLog = log.forSubComponent(TAG);
+        mDownstreams = new HashMap<>();
         mExemptPrefixes = new HashSet<>();
         mLastLocalPrefixStrs = new HashSet<>();
 
@@ -257,6 +260,11 @@
         }
     }
 
+    private String currentUpstreamInterface() {
+        return (mUpstreamLinkProperties != null)
+                ? mUpstreamLinkProperties.getInterfaceName() : null;
+    }
+
     private void maybeUpdateStats(String iface) {
         if (TextUtils.isEmpty(iface)) {
             return;
@@ -281,9 +289,7 @@
 
     private boolean maybeUpdateDataLimit(String iface) {
         // setDataLimit may only be called while offload is occuring on this upstream.
-        if (!started() ||
-                mUpstreamLinkProperties == null ||
-                !TextUtils.equals(iface, mUpstreamLinkProperties.getInterfaceName())) {
+        if (!started() || !TextUtils.equals(iface, currentUpstreamInterface())) {
             return true;
         }
 
@@ -296,9 +302,7 @@
     }
 
     private void updateStatsForCurrentUpstream() {
-        if (mUpstreamLinkProperties != null) {
-            maybeUpdateStats(mUpstreamLinkProperties.getInterfaceName());
-        }
+        maybeUpdateStats(currentUpstreamInterface());
     }
 
     public void setUpstreamLinkProperties(LinkProperties lp) {
@@ -325,17 +329,42 @@
     }
 
     public void notifyDownstreamLinkProperties(LinkProperties lp) {
+        final String ifname = lp.getInterfaceName();
+        final LinkProperties oldLp = mDownstreams.put(ifname, new LinkProperties(lp));
+        if (Objects.equals(oldLp, lp)) return;
+
         if (!started()) return;
 
-        // TODO: Cache LinkProperties on a per-ifname basis and compute the
-        // deltas, calling addDownstream()/removeDownstream() accordingly.
+        final List<RouteInfo> oldRoutes = (oldLp != null) ? oldLp.getRoutes() : new ArrayList<>();
+        final List<RouteInfo> newRoutes = lp.getRoutes();
+
+        // For each old route, if not in new routes: remove.
+        for (RouteInfo oldRoute : oldRoutes) {
+            if (shouldIgnoreDownstreamRoute(oldRoute)) continue;
+            if (!newRoutes.contains(oldRoute)) {
+                mHwInterface.removeDownstreamPrefix(ifname, oldRoute.getDestination().toString());
+            }
+        }
+
+        // For each new route, if not in old routes: add.
+        for (RouteInfo newRoute : newRoutes) {
+            if (shouldIgnoreDownstreamRoute(newRoute)) continue;
+            if (!oldRoutes.contains(newRoute)) {
+                mHwInterface.addDownstreamPrefix(ifname, newRoute.getDestination().toString());
+            }
+        }
     }
 
     public void removeDownstreamInterface(String ifname) {
+        final LinkProperties lp = mDownstreams.remove(ifname);
+        if (lp == null) return;
+
         if (!started()) return;
 
-        // TODO: Check cache for LinkProperties of ifname and, if present,
-        // call removeDownstream() accordingly.
+        for (RouteInfo route : lp.getRoutes()) {
+            if (shouldIgnoreDownstreamRoute(route)) continue;
+            mHwInterface.removeDownstreamPrefix(ifname, route.getDestination().toString());
+        }
     }
 
     private boolean isOffloadDisabled() {
@@ -442,6 +471,13 @@
         return localPrefixStrs;
     }
 
+    private static boolean shouldIgnoreDownstreamRoute(RouteInfo route) {
+        // Ignore any link-local routes.
+        if (!route.getDestinationLinkAddress().isGlobalPreferred()) return true;
+
+        return false;
+    }
+
     public void dump(IndentingPrintWriter pw) {
         if (isOffloadDisabled()) {
             pw.println("Offload disabled");
diff --git a/services/core/java/com/android/server/connectivity/tethering/OffloadHardwareInterface.java b/services/core/java/com/android/server/connectivity/tethering/OffloadHardwareInterface.java
index 86ff0a6..865a989 100644
--- a/services/core/java/com/android/server/connectivity/tethering/OffloadHardwareInterface.java
+++ b/services/core/java/com/android/server/connectivity/tethering/OffloadHardwareInterface.java
@@ -236,6 +236,44 @@
         return results.success;
     }
 
+    public boolean addDownstreamPrefix(String ifname, String prefix) {
+        final String logmsg = String.format("addDownstreamPrefix(%s, %s)", ifname, prefix);
+
+        final CbResults results = new CbResults();
+        try {
+            mOffloadControl.addDownstream(ifname, prefix,
+                    (boolean success, String errMsg) -> {
+                        results.success = success;
+                        results.errMsg = errMsg;
+                    });
+        } catch (RemoteException e) {
+            record(logmsg, e);
+            return false;
+        }
+
+        record(logmsg, results);
+        return results.success;
+    }
+
+    public boolean removeDownstreamPrefix(String ifname, String prefix) {
+        final String logmsg = String.format("removeDownstreamPrefix(%s, %s)", ifname, prefix);
+
+        final CbResults results = new CbResults();
+        try {
+            mOffloadControl.removeDownstream(ifname, prefix,
+                    (boolean success, String errMsg) -> {
+                        results.success = success;
+                        results.errMsg = errMsg;
+                    });
+        } catch (RemoteException e) {
+            record(logmsg, e);
+            return false;
+        }
+
+        record(logmsg, results);
+        return results.success;
+    }
+
     private void record(String msg, Throwable t) {
         mLog.e(msg + YIELDS + "exception: " + t);
     }
diff --git a/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java b/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
index 69678df..57d2502 100644
--- a/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
+++ b/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
@@ -115,6 +115,7 @@
     private final LinkProperties mLinkProperties;
 
     private int mLastError;
+    private int mServingMode;
     private String mMyUpstreamIfaceName;  // may change over time
     private NetworkInterface mNetworkInterface;
     private byte[] mHwAddr;
@@ -142,6 +143,7 @@
         mLinkProperties = new LinkProperties();
         resetLinkProperties();
         mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
+        mServingMode = IControlsTethering.STATE_AVAILABLE;
 
         mInitialState = new InitialState();
         mLocalHotspotState = new LocalHotspotState();
@@ -161,6 +163,8 @@
 
     public int lastError() { return mLastError; }
 
+    public int servingMode() { return mServingMode; }
+
     public LinkProperties linkProperties() { return new LinkProperties(mLinkProperties); }
 
     public void stop() { sendMessage(CMD_INTERFACE_DOWN); }
@@ -448,6 +452,7 @@
     }
 
     private void sendInterfaceState(int newInterfaceState) {
+        mServingMode = newInterfaceState;
         mTetherController.updateInterfaceState(
                 TetherInterfaceStateMachine.this, newInterfaceState, mLastError);
         sendLinkProperties();
diff --git a/services/net/java/android/net/util/NetworkConstants.java b/services/net/java/android/net/util/NetworkConstants.java
index 9b3bc3f..6065268 100644
--- a/services/net/java/android/net/util/NetworkConstants.java
+++ b/services/net/java/android/net/util/NetworkConstants.java
@@ -87,6 +87,7 @@
     public static final int IPV4_PROTOCOL_OFFSET = 9;
     public static final int IPV4_SRC_ADDR_OFFSET = 12;
     public static final int IPV4_DST_ADDR_OFFSET = 16;
+    public static final int IPV4_ADDR_BITS = 32;
     public static final int IPV4_ADDR_LEN = 4;
 
     /**
@@ -99,6 +100,7 @@
     public static final int IPV6_PROTOCOL_OFFSET = 6;
     public static final int IPV6_SRC_ADDR_OFFSET = 8;
     public static final int IPV6_DST_ADDR_OFFSET = 24;
+    public static final int IPV6_ADDR_BITS = 128;
     public static final int IPV6_ADDR_LEN = 16;
     public static final int IPV6_MIN_MTU = 1280;
     public static final int RFC7421_PREFIX_LENGTH = 64;
diff --git a/services/net/java/android/net/util/PrefixUtils.java b/services/net/java/android/net/util/PrefixUtils.java
index 962aab4..f60694a 100644
--- a/services/net/java/android/net/util/PrefixUtils.java
+++ b/services/net/java/android/net/util/PrefixUtils.java
@@ -20,6 +20,8 @@
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 
+import java.net.Inet4Address;
+import java.net.InetAddress;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
@@ -68,6 +70,13 @@
         return new IpPrefix(addr.getAddress(), addr.getPrefixLength());
     }
 
+    public static IpPrefix ipAddressAsPrefix(InetAddress ip) {
+        final int bitLength = (ip instanceof Inet4Address)
+                ? NetworkConstants.IPV4_ADDR_BITS
+                : NetworkConstants.IPV6_ADDR_BITS;
+        return new IpPrefix(ip, bitLength);
+    }
+
     private static IpPrefix pfx(String prefixStr) {
         return new IpPrefix(prefixStr);
     }
diff --git a/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java b/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java
index d5bbed7..622a7be 100644
--- a/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java
+++ b/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java
@@ -79,6 +79,15 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class OffloadControllerTest {
+    private static final String RNDIS0 = "test_rndis0";
+    private static final String RMNET0 = "test_rmnet_data0";
+    private static final String WLAN0 = "test_wlan0";
+
+    private static final String IPV6_LINKLOCAL = "fe80::/64";
+    private static final String IPV6_DOC_PREFIX = "2001:db8::/64";
+    private static final String IPV6_DISCARD_PREFIX = "100::/64";
+    private static final String USB_PREFIX = "192.168.42.0/24";
+    private static final String WIFI_PREFIX = "192.168.43.0/24";
 
     @Mock private OffloadHardwareInterface mHardware;
     @Mock private ApplicationInfo mApplicationInfo;
@@ -234,10 +243,8 @@
         inOrder.verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture());
         ArrayList<String> localPrefixes = mStringArrayCaptor.getValue();
         assertEquals(4, localPrefixes.size());
-        assertTrue(localPrefixes.contains("127.0.0.0/8"));
-        assertTrue(localPrefixes.contains("192.0.2.0/24"));
-        assertTrue(localPrefixes.contains("fe80::/64"));
-        assertTrue(localPrefixes.contains("2001:db8::/64"));
+        assertArrayListContains(localPrefixes,
+                "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64");
         inOrder.verifyNoMoreInteractions();
 
         offload.setUpstreamLinkProperties(null);
@@ -352,12 +359,9 @@
         inOrder.verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture());
         localPrefixes = mStringArrayCaptor.getValue();
         assertEquals(6, localPrefixes.size());
-        assertTrue(localPrefixes.contains("127.0.0.0/8"));
-        assertTrue(localPrefixes.contains("192.0.2.0/24"));
-        assertTrue(localPrefixes.contains("fe80::/64"));
-        assertTrue(localPrefixes.contains("2001:db8::/64"));
-        assertTrue(localPrefixes.contains("2001:db8::6173:7369:676e:6564/128"));
-        assertTrue(localPrefixes.contains("2001:db8::7261:6e64:6f6d/128"));
+        assertArrayListContains(localPrefixes,
+                "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64",
+                "2001:db8::6173:7369:676e:6564/128", "2001:db8::7261:6e64:6f6d/128");
         // The relevant parts of the LinkProperties have not changed, but at the
         // moment we do not de-dup upstream LinkProperties this carefully.
         inOrder.verify(mHardware, times(1)).setUpstreamParameters(
@@ -441,6 +445,8 @@
         waitForIdle();
         // There is no current upstream, so no stats are fetched.
         inOrder.verify(mHardware, never()).getForwardedStats(eq(ethernetIface));
+        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+                eq(null), eq(null), eq(null), eq(null));
         inOrder.verifyNoMoreInteractions();
 
         assertEquals(2, stats.size());
@@ -545,4 +551,79 @@
         callback.onStoppedLimitReached();
         verify(mNMService, times(1)).tetherLimitReached(mTetherStatsProviderCaptor.getValue());
     }
+
+    @Test
+    public void testAddRemoveDownstreams() throws Exception {
+        setupFunctioningHardwareInterface();
+        enableOffload();
+
+        final OffloadController offload = makeOffloadController();
+        offload.start();
+
+        final InOrder inOrder = inOrder(mHardware);
+        inOrder.verify(mHardware, times(1)).initOffloadConfig();
+        inOrder.verify(mHardware, times(1)).initOffloadControl(
+                any(OffloadHardwareInterface.ControlCallback.class));
+        inOrder.verifyNoMoreInteractions();
+
+        // Tethering makes several calls to setLocalPrefixes() before add/remove
+        // downstream calls are made. This is not tested here; only the behavior
+        // of notifyDownstreamLinkProperties() and removeDownstreamInterface()
+        // are tested.
+
+        // [1] USB tethering is started.
+        final LinkProperties usbLinkProperties = new LinkProperties();
+        usbLinkProperties.setInterfaceName(RNDIS0);
+        usbLinkProperties.addLinkAddress(new LinkAddress("192.168.42.1/24"));
+        usbLinkProperties.addRoute(new RouteInfo(new IpPrefix(USB_PREFIX)));
+        offload.notifyDownstreamLinkProperties(usbLinkProperties);
+        inOrder.verify(mHardware, times(1)).addDownstreamPrefix(RNDIS0, USB_PREFIX);
+        inOrder.verifyNoMoreInteractions();
+
+        // [2] Routes for IPv6 link-local prefixes should never be added.
+        usbLinkProperties.addRoute(new RouteInfo(new IpPrefix(IPV6_LINKLOCAL)));
+        offload.notifyDownstreamLinkProperties(usbLinkProperties);
+        inOrder.verify(mHardware, never()).addDownstreamPrefix(eq(RNDIS0), anyString());
+        inOrder.verifyNoMoreInteractions();
+
+        // [3] Add an IPv6 prefix for good measure. Only new offload-able
+        // prefixes should be passed to the HAL.
+        usbLinkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64"));
+        usbLinkProperties.addRoute(new RouteInfo(new IpPrefix(IPV6_DOC_PREFIX)));
+        offload.notifyDownstreamLinkProperties(usbLinkProperties);
+        inOrder.verify(mHardware, times(1)).addDownstreamPrefix(RNDIS0, IPV6_DOC_PREFIX);
+        inOrder.verifyNoMoreInteractions();
+
+        // [4] Adding addresses doesn't affect notifyDownstreamLinkProperties().
+        // The address is passed in by a separate setLocalPrefixes() invocation.
+        usbLinkProperties.addLinkAddress(new LinkAddress("2001:db8::2/64"));
+        offload.notifyDownstreamLinkProperties(usbLinkProperties);
+        inOrder.verify(mHardware, never()).addDownstreamPrefix(eq(RNDIS0), anyString());
+
+        // [5] Differences in local routes are converted into addDownstream()
+        // and removeDownstream() invocations accordingly.
+        usbLinkProperties.removeRoute(new RouteInfo(new IpPrefix(IPV6_DOC_PREFIX), null, RNDIS0));
+        usbLinkProperties.addRoute(new RouteInfo(new IpPrefix(IPV6_DISCARD_PREFIX)));
+        offload.notifyDownstreamLinkProperties(usbLinkProperties);
+        inOrder.verify(mHardware, times(1)).removeDownstreamPrefix(RNDIS0, IPV6_DOC_PREFIX);
+        inOrder.verify(mHardware, times(1)).addDownstreamPrefix(RNDIS0, IPV6_DISCARD_PREFIX);
+        inOrder.verifyNoMoreInteractions();
+
+        // [6] Removing a downstream interface which was never added causes no
+        // interactions with the HAL.
+        offload.removeDownstreamInterface(WLAN0);
+        inOrder.verifyNoMoreInteractions();
+
+        // [7] Removing an active downstream removes all remaining prefixes.
+        offload.removeDownstreamInterface(RNDIS0);
+        inOrder.verify(mHardware, times(1)).removeDownstreamPrefix(RNDIS0, USB_PREFIX);
+        inOrder.verify(mHardware, times(1)).removeDownstreamPrefix(RNDIS0, IPV6_DISCARD_PREFIX);
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    private static void assertArrayListContains(ArrayList<String> list, String... elems) {
+        for (String element : elems) {
+            assertTrue(list.contains(element));
+        }
+    }
 }