Merge "" into oc-dev am: 0aaae4ae78
am: 9bb81db15e

Change-Id: Ic0654fa608e873075938dd2b4ad5aaafd59cc2eb
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index df55080..905a3ee 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -122,7 +122,6 @@
 import com.android.internal.content.ReferrerIntent;
 import com.android.internal.os.BinderInternal;
 import com.android.internal.os.RuntimeInit;
-import com.android.internal.os.SamplingProfilerIntegration;
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FastPrintWriter;
@@ -1608,7 +1607,6 @@
                     handlePauseActivity((IBinder) args.arg1, false,
                             (args.argi1 & USER_LEAVING) != 0, args.argi2,
                             (args.argi1 & DONT_REPORT) != 0, args.argi3);
-                    maybeSnapshot();
                     Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                 } break;
                 case PAUSE_ACTIVITY_FINISHING: {
@@ -1678,7 +1676,6 @@
                 case RECEIVER:
                     Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "broadcastReceiveComp");
                     handleReceiver((ReceiverData)msg.obj);
-                    maybeSnapshot();
                     Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                     break;
                 case CREATE_SERVICE:
@@ -1704,7 +1701,6 @@
                 case STOP_SERVICE:
                     Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceStop");
                     handleStopService((IBinder)msg.obj);
-                    maybeSnapshot();
                     Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                     break;
                 case CONFIGURATION_CHANGED:
@@ -1866,32 +1862,6 @@
             }
             if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + codeToString(msg.what));
         }
-
-        private void maybeSnapshot() {
-            if (mBoundApplication != null && SamplingProfilerIntegration.isEnabled()) {
-                // convert the *private* ActivityThread.PackageInfo to *public* known
-                // android.content.pm.PackageInfo
-                String packageName = mBoundApplication.info.mPackageName;
-                android.content.pm.PackageInfo packageInfo = null;
-                try {
-                    Context context = getSystemContext();
-                    if(context == null) {
-                        Log.e(TAG, "cannot get a valid context");
-                        return;
-                    }
-                    PackageManager pm = context.getPackageManager();
-                    if(pm == null) {
-                        Log.e(TAG, "cannot get a valid PackageManager");
-                        return;
-                    }
-                    packageInfo = pm.getPackageInfo(
-                            packageName, PackageManager.GET_ACTIVITIES);
-                } catch (NameNotFoundException e) {
-                    Log.e(TAG, "cannot get package info for " + packageName, e);
-                }
-                SamplingProfilerIntegration.writeSnapshot(mBoundApplication.processName, packageInfo);
-            }
-        }
     }
 
     private class Idler implements MessageQueue.IdleHandler {
@@ -6501,7 +6471,6 @@
 
     public static void main(String[] args) {
         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
-        SamplingProfilerIntegration.start();
 
         // CloseGuard defaults to true and can be quite spammy.  We
         // disable it here, but selectively enable it later (via
diff --git a/core/java/com/android/internal/os/SamplingProfilerIntegration.java b/core/java/com/android/internal/os/SamplingProfilerIntegration.java
deleted file mode 100644
index 6429aa4..0000000
--- a/core/java/com/android/internal/os/SamplingProfilerIntegration.java
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * Copyright (C) 2009 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.internal.os;
-
-import android.content.pm.PackageInfo;
-import android.os.Build;
-import android.os.SystemProperties;
-import android.util.Log;
-import dalvik.system.profiler.BinaryHprofWriter;
-import dalvik.system.profiler.SamplingProfiler;
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.PrintStream;
-import java.util.Date;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.atomic.AtomicBoolean;
-import libcore.io.IoUtils;
-
-/**
- * Integrates the framework with Dalvik's sampling profiler.
- */
-public class SamplingProfilerIntegration {
-
-    private static final String TAG = "SamplingProfilerIntegration";
-
-    public static final String SNAPSHOT_DIR = "/data/snapshots";
-
-    private static final boolean enabled;
-    private static final Executor snapshotWriter;
-    private static final int samplingProfilerMilliseconds;
-    private static final int samplingProfilerDepth;
-
-    /** Whether or not a snapshot is being persisted. */
-    private static final AtomicBoolean pending = new AtomicBoolean(false);
-
-    static {
-        samplingProfilerMilliseconds = SystemProperties.getInt("persist.sys.profiler_ms", 0);
-        samplingProfilerDepth = SystemProperties.getInt("persist.sys.profiler_depth", 4);
-        if (samplingProfilerMilliseconds > 0) {
-            File dir = new File(SNAPSHOT_DIR);
-            dir.mkdirs();
-            // the directory needs to be writable to anybody to allow file writing
-            dir.setWritable(true, false);
-            // the directory needs to be executable to anybody to allow file creation
-            dir.setExecutable(true, false);
-            if (dir.isDirectory()) {
-                snapshotWriter = Executors.newSingleThreadExecutor(new ThreadFactory() {
-                        public Thread newThread(Runnable r) {
-                            return new Thread(r, TAG);
-                        }
-                    });
-                enabled = true;
-                Log.i(TAG, "Profiling enabled. Sampling interval ms: "
-                      + samplingProfilerMilliseconds);
-            } else {
-                snapshotWriter = null;
-                enabled = true;
-                Log.w(TAG, "Profiling setup failed. Could not create " + SNAPSHOT_DIR);
-            }
-        } else {
-            snapshotWriter = null;
-            enabled = false;
-            Log.i(TAG, "Profiling disabled.");
-        }
-    }
-
-    private static SamplingProfiler samplingProfiler;
-    private static long startMillis;
-
-    /**
-     * Is profiling enabled?
-     */
-    public static boolean isEnabled() {
-        return enabled;
-    }
-
-    /**
-     * Starts the profiler if profiling is enabled.
-     */
-    public static void start() {
-        if (!enabled) {
-            return;
-        }
-        if (samplingProfiler != null) {
-            Log.e(TAG, "SamplingProfilerIntegration already started at " + new Date(startMillis));
-            return;
-        }
-
-        ThreadGroup group = Thread.currentThread().getThreadGroup();
-        SamplingProfiler.ThreadSet threadSet = SamplingProfiler.newThreadGroupThreadSet(group);
-        samplingProfiler = new SamplingProfiler(samplingProfilerDepth, threadSet);
-        samplingProfiler.start(samplingProfilerMilliseconds);
-        startMillis = System.currentTimeMillis();
-    }
-
-    /**
-     * Writes a snapshot if profiling is enabled.
-     */
-    public static void writeSnapshot(final String processName, final PackageInfo packageInfo) {
-        if (!enabled) {
-            return;
-        }
-        if (samplingProfiler == null) {
-            Log.e(TAG, "SamplingProfilerIntegration is not started");
-            return;
-        }
-
-        /*
-         * If we're already writing a snapshot, don't bother enqueueing another
-         * request right now. This will reduce the number of individual
-         * snapshots and in turn the total amount of memory consumed (one big
-         * snapshot is smaller than N subset snapshots).
-         */
-        if (pending.compareAndSet(false, true)) {
-            snapshotWriter.execute(new Runnable() {
-                public void run() {
-                    try {
-                        writeSnapshotFile(processName, packageInfo);
-                    } finally {
-                        pending.set(false);
-                    }
-                }
-            });
-        }
-    }
-
-    /**
-     * Writes the zygote's snapshot to internal storage if profiling is enabled.
-     */
-    public static void writeZygoteSnapshot() {
-        if (!enabled) {
-            return;
-        }
-        writeSnapshotFile("zygote", null);
-        samplingProfiler.shutdown();
-        samplingProfiler = null;
-        startMillis = 0;
-    }
-
-    /**
-     * pass in PackageInfo to retrieve various values for snapshot header
-     */
-    private static void writeSnapshotFile(String processName, PackageInfo packageInfo) {
-        if (!enabled) {
-            return;
-        }
-        samplingProfiler.stop();
-
-        /*
-         * We use the global start time combined with the process name
-         * as a unique ID. We can't use a counter because processes
-         * restart. This could result in some overlap if we capture
-         * two snapshots in rapid succession.
-         */
-        String name = processName.replaceAll(":", ".");
-        String path = SNAPSHOT_DIR + "/" + name + "-" + startMillis + ".snapshot";
-        long start = System.currentTimeMillis();
-        OutputStream outputStream = null;
-        try {
-            outputStream = new BufferedOutputStream(new FileOutputStream(path));
-            PrintStream out = new PrintStream(outputStream);
-            generateSnapshotHeader(name, packageInfo, out);
-            if (out.checkError()) {
-                throw new IOException();
-            }
-            BinaryHprofWriter.write(samplingProfiler.getHprofData(), outputStream);
-        } catch (IOException e) {
-            Log.e(TAG, "Error writing snapshot to " + path, e);
-            return;
-        } finally {
-            IoUtils.closeQuietly(outputStream);
-        }
-        // set file readable to the world so that SamplingProfilerService
-        // can put it to dropbox
-        new File(path).setReadable(true, false);
-
-        long elapsed = System.currentTimeMillis() - start;
-        Log.i(TAG, "Wrote snapshot " + path + " in " + elapsed + "ms.");
-        samplingProfiler.start(samplingProfilerMilliseconds);
-    }
-
-    /**
-     * generate header for snapshots, with the following format
-     * (like an HTTP header but without the \r):
-     *
-     * Version: <version number of profiler>\n
-     * Process: <process name>\n
-     * Package: <package name, if exists>\n
-     * Package-Version: <version number of the package, if exists>\n
-     * Build: <fingerprint>\n
-     * \n
-     * <the actual snapshot content begins here...>
-     */
-    private static void generateSnapshotHeader(String processName, PackageInfo packageInfo,
-            PrintStream out) {
-        // profiler version
-        out.println("Version: 3");
-        out.println("Process: " + processName);
-        if (packageInfo != null) {
-            out.println("Package: " + packageInfo.packageName);
-            out.println("Package-Version: " + packageInfo.versionCode);
-        }
-        out.println("Build: " + Build.FINGERPRINT);
-        // single blank line means the end of snapshot header.
-        out.println();
-    }
-}
diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java
index 9e61a99..2c0f8e4 100644
--- a/core/java/com/android/internal/os/ZygoteInit.java
+++ b/core/java/com/android/internal/os/ZygoteInit.java
@@ -694,8 +694,6 @@
                     Trace.TRACE_TAG_DALVIK);
             bootTimingsTraceLog.traceBegin("ZygoteInit");
             RuntimeInit.enableDdms();
-            // Start profiling the zygote initialization.
-            SamplingProfilerIntegration.start();
 
             boolean startSystemServer = false;
             String socketName = "zygote";
@@ -734,9 +732,6 @@
                 Zygote.resetNicePriority();
             }
 
-            // Finish profiling the zygote initialization.
-            SamplingProfilerIntegration.writeZygoteSnapshot();
-
             // Do an initial gc to clean up after startup
             bootTimingsTraceLog.traceBegin("PostZygoteInitGC");
             gcAndFinalize();
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 1adc6dd..659f47d 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -613,6 +613,7 @@
     char useJitProfilesOptsBuf[sizeof("-Xjitsaveprofilinginfo:")-1 + PROPERTY_VALUE_MAX];
     char jitprithreadweightOptBuf[sizeof("-Xjitprithreadweight:")-1 + PROPERTY_VALUE_MAX];
     char jittransitionweightOptBuf[sizeof("-Xjittransitionweight:")-1 + PROPERTY_VALUE_MAX];
+    char hotstartupsamplesOptsBuf[sizeof("-Xps-hot-startup-method-samples:")-1 + PROPERTY_VALUE_MAX];
     char gctypeOptsBuf[sizeof("-Xgc:")-1 + PROPERTY_VALUE_MAX];
     char backgroundgcOptsBuf[sizeof("-XX:BackgroundGC=")-1 + PROPERTY_VALUE_MAX];
     char heaptargetutilizationOptsBuf[sizeof("-XX:HeapTargetUtilization=")-1 + PROPERTY_VALUE_MAX];
@@ -739,6 +740,12 @@
                        jittransitionweightOptBuf,
                        "-Xjittransitionweight:");
 
+    /*
+     * Profile related options.
+     */
+    parseRuntimeOption("dalvik.vm.hot-startup-method-samples", hotstartupsamplesOptsBuf,
+            "-Xps-hot-startup-method-samples:");
+
     property_get("ro.config.low_ram", propBuf, "");
     if (strcmp(propBuf, "true") == 0) {
       addOption("-XX:LowMemoryMode");
diff --git a/packages/CaptivePortalLogin/res/values/strings.xml b/packages/CaptivePortalLogin/res/values/strings.xml
index b1a3852..f486fe4 100644
--- a/packages/CaptivePortalLogin/res/values/strings.xml
+++ b/packages/CaptivePortalLogin/res/values/strings.xml
@@ -5,6 +5,7 @@
     <string name="action_use_network">Use this network as is</string>
     <string name="action_do_not_use_network">Do not use this network</string>
     <string name="action_bar_label">Sign in to network</string>
+    <string name="action_bar_title">Sign in to %1$s</string>
     <string name="ssl_error_warning">The network you&#8217;re trying to join has security issues.</string>
     <string name="ssl_error_example">For example, the login page may not belong to the organization shown.</string>
     <string name="ssl_error_continue">Continue anyway via browser</string>
diff --git a/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java b/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
index 582b660..2703fbf 100644
--- a/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
+++ b/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
@@ -26,6 +26,7 @@
 import android.net.ConnectivityManager.NetworkCallback;
 import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
 import android.net.NetworkRequest;
 import android.net.Proxy;
 import android.net.Uri;
@@ -469,7 +470,15 @@
     }
 
     private String getHeaderTitle() {
-        return getString(R.string.action_bar_label);
+        NetworkInfo info = mCm.getNetworkInfo(mNetwork);
+        if (info == null) {
+            return getString(R.string.action_bar_label);
+        }
+        NetworkCapabilities nc = mCm.getNetworkCapabilities(mNetwork);
+        if (!nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
+            return getString(R.string.action_bar_label);
+        }
+        return getString(R.string.action_bar_title, info.getExtraInfo().replaceAll("^\"|\"$", ""));
     }
 
     private String getHeaderSubtitle(String urlString) {
diff --git a/services/core/java/com/android/server/SamplingProfilerService.java b/services/core/java/com/android/server/SamplingProfilerService.java
deleted file mode 100644
index 5f92695..0000000
--- a/services/core/java/com/android/server/SamplingProfilerService.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server;
-
-import android.content.ContentResolver;
-import android.os.DropBoxManager;
-import android.os.FileObserver;
-import android.os.Binder;
-
-import android.util.Slog;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.os.SystemProperties;
-import android.provider.Settings;
-import com.android.internal.os.SamplingProfilerIntegration;
-import com.android.internal.util.DumpUtils;
-
-import java.io.File;
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.io.PrintWriter;
-
-public class SamplingProfilerService extends Binder {
-
-    private static final String TAG = "SamplingProfilerService";
-    private static final boolean LOCAL_LOGV = false;
-    public static final String SNAPSHOT_DIR = SamplingProfilerIntegration.SNAPSHOT_DIR;
-
-    private final Context mContext;
-    private FileObserver snapshotObserver;
-
-    public SamplingProfilerService(Context context) {
-        mContext = context;
-        registerSettingObserver(context);
-        startWorking(context);
-    }
-
-    private void startWorking(Context context) {
-        if (LOCAL_LOGV) Slog.v(TAG, "starting SamplingProfilerService!");
-
-        final DropBoxManager dropbox =
-                (DropBoxManager) context.getSystemService(Context.DROPBOX_SERVICE);
-
-        // before FileObserver is ready, there could have already been some snapshots
-        // in the directory, we don't want to miss them
-        File[] snapshotFiles = new File(SNAPSHOT_DIR).listFiles();
-        for (int i = 0; snapshotFiles != null && i < snapshotFiles.length; i++) {
-            handleSnapshotFile(snapshotFiles[i], dropbox);
-        }
-
-        // detect new snapshot and put it in dropbox
-        // delete it afterwards no matter what happened before
-        // Note: needs listening at event ATTRIB rather than CLOSE_WRITE, because we set the
-        // readability of snapshot files after writing them!
-        snapshotObserver = new FileObserver(SNAPSHOT_DIR, FileObserver.ATTRIB) {
-            @Override
-            public void onEvent(int event, String path) {
-                handleSnapshotFile(new File(SNAPSHOT_DIR, path), dropbox);
-            }
-        };
-        snapshotObserver.startWatching();
-
-        if (LOCAL_LOGV) Slog.v(TAG, "SamplingProfilerService activated");
-    }
-
-    private void handleSnapshotFile(File file, DropBoxManager dropbox) {
-        try {
-            dropbox.addFile(TAG, file, 0);
-            if (LOCAL_LOGV) Slog.v(TAG, file.getPath() + " added to dropbox");
-        } catch (IOException e) {
-            Slog.e(TAG, "Can't add " + file.getPath() + " to dropbox", e);
-        } finally {
-            file.delete();
-        }
-    }
-
-    private void registerSettingObserver(Context context) {
-        ContentResolver contentResolver = context.getContentResolver();
-        contentResolver.registerContentObserver(
-                Settings.Global.getUriFor(Settings.Global.SAMPLING_PROFILER_MS),
-                false, new SamplingProfilerSettingsObserver(contentResolver));
-    }
-
-    @Override
-    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
-
-        pw.println("SamplingProfilerService:");
-        pw.println("Watching directory: " + SNAPSHOT_DIR);
-    }
-
-    private class SamplingProfilerSettingsObserver extends ContentObserver {
-        private ContentResolver mContentResolver;
-        public SamplingProfilerSettingsObserver(ContentResolver contentResolver) {
-            super(null);
-            mContentResolver = contentResolver;
-            onChange(false);
-        }
-        @Override
-        public void onChange(boolean selfChange) {
-            Integer samplingProfilerMs = Settings.Global.getInt(
-                    mContentResolver, Settings.Global.SAMPLING_PROFILER_MS, 0);
-            // setting this secure property will start or stop sampling profiler,
-            // as well as adjust the the time between taking snapshots.
-            SystemProperties.set("persist.sys.profiler_ms", samplingProfilerMs.toString());
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java
index 5f74d5e..a452404 100644
--- a/services/core/java/com/android/server/connectivity/Tethering.java
+++ b/services/core/java/com/android/server/connectivity/Tethering.java
@@ -117,7 +117,7 @@
  * This class holds much of the business logic to allow Android devices
  * to act as IP gateways via USB, BT, and WiFi interfaces.
  */
-public class Tethering extends BaseNetworkObserver implements IControlsTethering {
+public class Tethering extends BaseNetworkObserver {
 
     private final static String TAG = Tethering.class.getSimpleName();
     private final static boolean DBG = false;
@@ -173,6 +173,8 @@
     private final StateMachine mTetherMasterSM;
     private final OffloadController mOffloadController;
     private final UpstreamNetworkMonitor mUpstreamNetworkMonitor;
+    // TODO: Figure out how to merge this and other downstream-tracking objects
+    // into a single coherent structure.
     private final HashSet<TetherInterfaceStateMachine> mForwardedDownstreams;
     private final SimChangeListener mSimChange;
 
@@ -184,7 +186,7 @@
     private boolean mRndisEnabled;       // track the RNDIS function enabled state
     private boolean mUsbTetherRequested; // true if USB tethering should be started
                                          // when RNDIS is enabled
-    // True iff WiFi tethering should be started when soft AP is ready.
+    // True iff. WiFi tethering should be started when soft AP is ready.
     private boolean mWifiTetherRequested;
 
     public Tethering(Context context, INetworkManagementService nmService,
@@ -1112,6 +1114,7 @@
         static final int EVENT_UPSTREAM_CALLBACK                = BASE_MASTER + 5;
         // we treated the error and want now to clear it
         static final int CMD_CLEAR_ERROR                        = BASE_MASTER + 6;
+        static final int EVENT_IFACE_UPDATE_LINKPROPERTIES      = BASE_MASTER + 7;
 
         private State mInitialState;
         private State mTetherModeAliveState;
@@ -1179,6 +1182,9 @@
                         if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who);
                         handleInterfaceServingStateInactive(who);
                         break;
+                    case EVENT_IFACE_UPDATE_LINKPROPERTIES:
+                        // Silently ignore these for now.
+                        break;
                     default:
                         return NOT_HANDLED;
                 }
@@ -1242,8 +1248,8 @@
             protected void chooseUpstreamType(boolean tryCell) {
                 updateConfiguration(); // TODO - remove?
 
-                final int upstreamType = findPreferredUpstreamType(
-                        getConnectivityManager(), mConfig);
+                final int upstreamType = mUpstreamNetworkMonitor.selectPreferredUpstreamType(
+                        mConfig.preferredUpstreamIfaceTypes);
                 if (upstreamType == ConnectivityManager.TYPE_NONE) {
                     if (tryCell) {
                         mUpstreamNetworkMonitor.registerMobileNetworkRequest();
@@ -1255,58 +1261,6 @@
                 setUpstreamByType(upstreamType);
             }
 
-            // TODO: Move this function into UpstreamNetworkMonitor.
-            protected int findPreferredUpstreamType(ConnectivityManager cm,
-                                                    TetheringConfiguration cfg) {
-                int upType = ConnectivityManager.TYPE_NONE;
-
-                if (VDBG) {
-                    Log.d(TAG, "chooseUpstreamType has upstream iface types:");
-                    for (Integer netType : cfg.preferredUpstreamIfaceTypes) {
-                        Log.d(TAG, " " + netType);
-                    }
-                }
-
-                for (Integer netType : cfg.preferredUpstreamIfaceTypes) {
-                    NetworkInfo info = cm.getNetworkInfo(netType.intValue());
-                    // TODO: if the network is suspended we should consider
-                    // that to be the same as connected here.
-                    if ((info != null) && info.isConnected()) {
-                        upType = netType.intValue();
-                        break;
-                    }
-                }
-
-                final int preferredUpstreamMobileApn = cfg.isDunRequired
-                        ? ConnectivityManager.TYPE_MOBILE_DUN
-                        : ConnectivityManager.TYPE_MOBILE_HIPRI;
-                mLog.log(String.format(
-                        "findPreferredUpstreamType(), preferredApn=%s, got type=%s",
-                        getNetworkTypeName(preferredUpstreamMobileApn),
-                        getNetworkTypeName(upType)));
-
-                switch (upType) {
-                    case ConnectivityManager.TYPE_MOBILE_DUN:
-                    case ConnectivityManager.TYPE_MOBILE_HIPRI:
-                        // If we're on DUN, put our own grab on it.
-                        mUpstreamNetworkMonitor.registerMobileNetworkRequest();
-                        break;
-                    case ConnectivityManager.TYPE_NONE:
-                        break;
-                    default:
-                        /* If we've found an active upstream connection that's not DUN/HIPRI
-                         * we should stop any outstanding DUN/HIPRI start requests.
-                         *
-                         * If we found NONE we don't want to do this as we want any previous
-                         * requests to keep trying to bring up something we can use.
-                         */
-                        mUpstreamNetworkMonitor.releaseMobileNetworkRequest();
-                        break;
-                }
-
-                return upType;
-            }
-
             protected void setUpstreamByType(int upType) {
                 final ConnectivityManager cm = getConnectivityManager();
                 Network network = null;
@@ -1397,6 +1351,7 @@
             if (mode == IControlsTethering.STATE_TETHERED) {
                 mForwardedDownstreams.add(who);
             } else {
+                mOffloadController.removeDownstreamInterface(who.interfaceName());
                 mForwardedDownstreams.remove(who);
             }
 
@@ -1421,6 +1376,7 @@
         private void handleInterfaceServingStateInactive(TetherInterfaceStateMachine who) {
             mNotifyList.remove(who);
             mIPv6TetheringCoordinator.removeActiveDownstream(who);
+            mOffloadController.removeDownstreamInterface(who.interfaceName());
             mForwardedDownstreams.remove(who);
 
             // If this is a Wi-Fi interface, tell WifiManager of any errors.
@@ -1524,6 +1480,15 @@
                         }
                         break;
                     }
+                    case EVENT_IFACE_UPDATE_LINKPROPERTIES: {
+                        final LinkProperties newLp = (LinkProperties) message.obj;
+                        if (message.arg1 == IControlsTethering.STATE_TETHERED) {
+                            mOffloadController.notifyDownstreamLinkProperties(newLp);
+                        } else {
+                            mOffloadController.removeDownstreamInterface(newLp.getInterfaceName());
+                        }
+                        break;
+                    }
                     case CMD_UPSTREAM_CHANGED:
                         updateUpstreamWanted();
                         if (!mUpstreamWanted) break;
@@ -1748,9 +1713,25 @@
         return false;
     }
 
-    @Override
-    public void notifyInterfaceStateChange(String iface, TetherInterfaceStateMachine who,
-                                           int state, int error) {
+    private IControlsTethering makeControlCallback(String ifname) {
+        return new IControlsTethering() {
+            @Override
+            public void updateInterfaceState(
+                    TetherInterfaceStateMachine who, int state, int lastError) {
+                notifyInterfaceStateChange(ifname, who, state, lastError);
+            }
+
+            @Override
+            public void updateLinkProperties(
+                    TetherInterfaceStateMachine who, LinkProperties newLp) {
+                notifyLinkPropertiesChanged(ifname, who, newLp);
+            }
+        };
+    }
+
+    // TODO: Move into TetherMasterSM.
+    private void notifyInterfaceStateChange(
+            String iface, TetherInterfaceStateMachine who, int state, int error) {
         synchronized (mPublicSync) {
             final TetherState tetherState = mTetherStates.get(iface);
             if (tetherState != null && tetherState.stateMachine.equals(who)) {
@@ -1796,6 +1777,24 @@
         sendTetherStateChangedBroadcast();
     }
 
+    private void notifyLinkPropertiesChanged(String iface, TetherInterfaceStateMachine who,
+                                             LinkProperties newLp) {
+        final int state;
+        synchronized (mPublicSync) {
+            final TetherState tetherState = mTetherStates.get(iface);
+            if (tetherState != null && tetherState.stateMachine.equals(who)) {
+                state = tetherState.lastState;
+            } else {
+                mLog.log("got notification from stale iface " + iface);
+                return;
+            }
+        }
+
+        mLog.log(String.format("OBSERVED LinkProperties update iface=%s state=%s", iface, state));
+        final int which = TetherMasterSM.EVENT_IFACE_UPDATE_LINKPROPERTIES;
+        mTetherMasterSM.sendMessage(which, state, 0, newLp);
+    }
+
     private void maybeTrackNewInterfaceLocked(final String iface) {
         // If we don't care about this type of interface, ignore.
         final int interfaceType = ifaceNameToType(iface);
@@ -1813,7 +1812,8 @@
         mLog.log("adding TetheringInterfaceStateMachine for: " + iface);
         final TetherState tetherState = new TetherState(
                 new TetherInterfaceStateMachine(
-                    iface, mLooper, interfaceType, mLog, mNMService, mStatsService, this,
+                    iface, mLooper, interfaceType, mLog, mNMService, mStatsService,
+                    makeControlCallback(iface),
                     new IPv6TetheringInterfaceServices(iface, mNMService, mLog)));
         mTetherStates.put(iface, tetherState);
         tetherState.stateMachine.start();
@@ -1825,7 +1825,7 @@
             mLog.log("attempting to remove unknown iface (" + iface + "), ignoring");
             return;
         }
-        tetherState.stateMachine.sendMessage(TetherInterfaceStateMachine.CMD_INTERFACE_DOWN);
+        tetherState.stateMachine.stop();
         mLog.log("removing TetheringInterfaceStateMachine for: " + iface);
         mTetherStates.remove(iface);
     }
diff --git a/services/core/java/com/android/server/connectivity/tethering/IControlsTethering.java b/services/core/java/com/android/server/connectivity/tethering/IControlsTethering.java
index c5c86bd..aaa63b1 100644
--- a/services/core/java/com/android/server/connectivity/tethering/IControlsTethering.java
+++ b/services/core/java/com/android/server/connectivity/tethering/IControlsTethering.java
@@ -16,25 +16,41 @@
 
 package com.android.server.connectivity.tethering;
 
+import android.net.LinkProperties;
+
 /**
  * @hide
  *
  * Interface with methods necessary to notify that a given interface is ready for tethering.
+ *
+ * Rename to something more representative, e.g. IpServingControlCallback.
+ *
+ * All methods MUST be called on the TetherMasterSM main Looper's thread.
  */
-public interface IControlsTethering {
-    public final int STATE_UNAVAILABLE = 0;
-    public final int STATE_AVAILABLE   = 1;
-    public final int STATE_TETHERED    = 2;
-    public final int STATE_LOCAL_ONLY  = 3;
+public class IControlsTethering {
+    public static final int STATE_UNAVAILABLE = 0;
+    public static final int STATE_AVAILABLE   = 1;
+    public static final int STATE_TETHERED    = 2;
+    public static final int STATE_LOCAL_ONLY  = 3;
 
     /**
-     * Notify that |who| has changed its tethering state.  This may be called from any thread.
+     * Notify that |who| has changed its tethering state.
      *
-     * @param iface a network interface (e.g. "wlan0")
+     * TODO: Remove the need for the |who| argument.
+     *
      * @param who corresponding instance of a TetherInterfaceStateMachine
      * @param state one of IControlsTethering.STATE_*
      * @param lastError one of ConnectivityManager.TETHER_ERROR_*
      */
-    void notifyInterfaceStateChange(String iface, TetherInterfaceStateMachine who,
-                                    int state, int lastError);
+    public void updateInterfaceState(TetherInterfaceStateMachine who, int state, int lastError) {}
+
+    /**
+     * Notify that |who| has new LinkProperties.
+     *
+     * TODO: Remove the need for the |who| argument.
+     *
+     * @param who corresponding instance of a TetherInterfaceStateMachine
+     * @param newLp the new LinkProperties to report
+     */
+    public void updateLinkProperties(TetherInterfaceStateMachine who, LinkProperties newLp) {}
 }
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 cb50e9f..3aca45f 100644
--- a/services/core/java/com/android/server/connectivity/tethering/OffloadController.java
+++ b/services/core/java/com/android/server/connectivity/tethering/OffloadController.java
@@ -105,7 +105,19 @@
         pushUpstreamParameters();
     }
 
-    // TODO: public void addDownStream(...)
+    public void notifyDownstreamLinkProperties(LinkProperties lp) {
+        if (!started()) return;
+
+        // TODO: Cache LinkProperties on a per-ifname basis and compute the
+        // deltas, calling addDownstream()/removeDownstream() accordingly.
+    }
+
+    public void removeDownstreamInterface(String ifname) {
+        if (!started()) return;
+
+        // TODO: Check cache for LinkProperties of ifname and, if present,
+        // call removeDownstream() accordingly.
+    }
 
     private boolean isOffloadDisabled() {
         // Defaults to |false| if not present.
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 4a1d405..82b9ca0 100644
--- a/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
+++ b/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
@@ -90,6 +90,7 @@
 
     private final String mIfaceName;
     private final int mInterfaceType;
+    private final LinkProperties mLinkProperties;
     private final IPv6TetheringInterfaceServices mIPv6TetherSvc;
 
     private int mLastError;
@@ -106,6 +107,8 @@
         mTetherController = tetherController;
         mIfaceName = ifaceName;
         mInterfaceType = interfaceType;
+        mLinkProperties = new LinkProperties();
+        mLinkProperties.setInterfaceName(mIfaceName);
         mIPv6TetherSvc = ipv6Svc;
         mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
 
@@ -127,6 +130,8 @@
 
     public int lastError() { return mLastError; }
 
+    public void stop() { sendMessage(CMD_INTERFACE_DOWN); }
+
     // configured when we start tethering and unconfig'd on error or conclusion
     private boolean configureIfaceIp(boolean enabled) {
         if (VDBG) Log.d(TAG, "configureIfaceIp(" + enabled + ")");
@@ -182,8 +187,12 @@
     }
 
     private void sendInterfaceState(int newInterfaceState) {
-        mTetherController.notifyInterfaceStateChange(
-                mIfaceName, TetherInterfaceStateMachine.this, newInterfaceState, mLastError);
+        mTetherController.updateInterfaceState(
+                TetherInterfaceStateMachine.this, newInterfaceState, mLastError);
+        // TODO: Populate mLinkProperties correctly, and send more sensible
+        // updates more frequently (not just here).
+        mTetherController.updateLinkProperties(
+                TetherInterfaceStateMachine.this, new LinkProperties(mLinkProperties));
     }
 
     class InitialState extends State {
@@ -195,7 +204,6 @@
         @Override
         public boolean processMessage(Message message) {
             maybeLogMessage(this, message.what);
-            boolean retValue = true;
             switch (message.what) {
                 case CMD_TETHER_REQUESTED:
                     mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
@@ -218,10 +226,9 @@
                             (LinkProperties) message.obj);
                     break;
                 default:
-                    retValue = false;
-                    break;
+                    return NOT_HANDLED;
             }
-            return retValue;
+            return HANDLED;
         }
     }
 
diff --git a/services/core/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java b/services/core/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
index cd6038f..b2d5051 100644
--- a/services/core/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
+++ b/services/core/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
@@ -16,6 +16,8 @@
 
 package com.android.server.connectivity.tethering;
 
+import static android.net.ConnectivityManager.getNetworkTypeName;
+import static android.net.ConnectivityManager.TYPE_NONE;
 import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
 import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
 
@@ -176,6 +178,41 @@
         return (network != null) ? mNetworkMap.get(network) : null;
     }
 
+    // So many TODOs here, but chief among them is: make this functionality an
+    // integral part of this class such that whenever a higher priority network
+    // becomes available and useful we (a) file a request to keep it up as
+    // necessary and (b) change all upstream tracking state accordingly (by
+    // passing LinkProperties up to Tethering).
+    //
+    // Next TODO: return NetworkState instead of just the type.
+    public int selectPreferredUpstreamType(Iterable<Integer> preferredTypes) {
+        final TypeStatePair typeStatePair = findFirstAvailableUpstreamByType(
+                mNetworkMap.values(), preferredTypes);
+
+        mLog.log("preferred upstream type: " + getNetworkTypeName(typeStatePair.type));
+
+        switch (typeStatePair.type) {
+            case TYPE_MOBILE_DUN:
+            case TYPE_MOBILE_HIPRI:
+                // If we're on DUN, put our own grab on it.
+                registerMobileNetworkRequest();
+                break;
+            case TYPE_NONE:
+                break;
+            default:
+                /* If we've found an active upstream connection that's not DUN/HIPRI
+                 * we should stop any outstanding DUN/HIPRI start requests.
+                 *
+                 * If we found NONE we don't want to do this as we want any previous
+                 * requests to keep trying to bring up something we can use.
+                 */
+                releaseMobileNetworkRequest();
+                break;
+        }
+
+        return typeStatePair.type;
+    }
+
     private void handleAvailable(int callbackType, Network network) {
         if (VDBG) Log.d(TAG, "EVENT_ON_AVAILABLE for " + network);
 
@@ -365,4 +402,37 @@
     private void notifyTarget(int which, NetworkState netstate) {
         mTarget.sendMessage(mWhat, which, 0, netstate);
     }
+
+    static private class TypeStatePair {
+        public int type = TYPE_NONE;
+        public NetworkState ns = null;
+    }
+
+    static private TypeStatePair findFirstAvailableUpstreamByType(
+            Iterable<NetworkState> netStates, Iterable<Integer> preferredTypes) {
+        final TypeStatePair result = new TypeStatePair();
+
+        for (int type : preferredTypes) {
+            NetworkCapabilities nc;
+            try {
+                nc = ConnectivityManager.networkCapabilitiesForType(type);
+            } catch (IllegalArgumentException iae) {
+                Log.e(TAG, "No NetworkCapabilities mapping for legacy type: " +
+                       ConnectivityManager.getNetworkTypeName(type));
+                continue;
+            }
+
+            for (NetworkState value : netStates) {
+                if (!nc.satisfiedByNetworkCapabilities(value.networkCapabilities)) {
+                    continue;
+                }
+
+                result.type = type;
+                result.ns = value;
+                return result;
+            }
+        }
+
+        return result;
+    }
 }
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 272c11b..9a756b1 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -55,7 +55,6 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.os.BinderInternal;
-import com.android.internal.os.SamplingProfilerIntegration;
 import com.android.internal.util.EmergencyAffordanceManager;
 import com.android.internal.util.ConcurrentUtils;
 import com.android.internal.widget.ILockSettings;
@@ -330,18 +329,6 @@
             // the property. http://b/11463182
             SystemProperties.set("persist.sys.dalvik.vm.lib.2", VMRuntime.getRuntime().vmLibrary());
 
-            // Enable the sampling profiler.
-            if (SamplingProfilerIntegration.isEnabled()) {
-                SamplingProfilerIntegration.start();
-                mProfilerSnapshotTimer = new Timer();
-                mProfilerSnapshotTimer.schedule(new TimerTask() {
-                        @Override
-                        public void run() {
-                            SamplingProfilerIntegration.writeSnapshot("system_server", null);
-                        }
-                    }, SNAPSHOT_INTERVAL, SNAPSHOT_INTERVAL);
-            }
-
             // Mmmmmm... more memory!
             VMRuntime.getRuntime().clearGrowthLimit();
 
@@ -710,8 +697,6 @@
                 false);
         boolean disableTextServices = SystemProperties.getBoolean("config.disable_textservices",
                 false);
-        boolean disableSamplingProfiler = SystemProperties.getBoolean("config.disable_samplingprof",
-                false);
         boolean disableConsumerIr = SystemProperties.getBoolean("config.disable_consumerir", false);
         boolean disableVrManager = SystemProperties.getBoolean("config.disable_vrmanager", false);
         boolean disableCameraService = SystemProperties.getBoolean("config.disable_cameraservice",
@@ -1353,21 +1338,6 @@
             }
             traceEnd();
 
-            if (!disableSamplingProfiler) {
-                traceBeginAndSlog("StartSamplingProfilerService");
-                try {
-                    // need to add this service even if SamplingProfilerIntegration.isEnabled()
-                    // is false, because it is this service that detects system property change and
-                    // turns on SamplingProfilerIntegration. Plus, when sampling profiler doesn't work,
-                    // there is little overhead for running this service.
-                    ServiceManager.addService("samplingprofiler",
-                                new SamplingProfilerService(context));
-                } catch (Throwable e) {
-                    reportWtf("starting SamplingProfiler Service", e);
-                }
-                traceEnd();
-            }
-
             if (!disableNetwork && !disableNetworkTime) {
                 traceBeginAndSlog("StartNetworkTimeUpdateService");
                 try {
diff --git a/telephony/java/android/telephony/MbmsDownloadManager.java b/telephony/java/android/telephony/MbmsDownloadManager.java
index a9ec299..c8c6d01 100644
--- a/telephony/java/android/telephony/MbmsDownloadManager.java
+++ b/telephony/java/android/telephony/MbmsDownloadManager.java
@@ -16,19 +16,24 @@
 
 package android.telephony;
 
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.ServiceConnection;
 import android.net.Uri;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.telephony.mbms.IDownloadCallback;
 import android.telephony.mbms.DownloadRequest;
 import android.telephony.mbms.DownloadStatus;
 import android.telephony.mbms.IMbmsDownloadManagerCallback;
 import android.telephony.mbms.MbmsException;
+import android.telephony.mbms.MbmsUtils;
 import android.telephony.mbms.vendor.IMbmsDownloadService;
 import android.util.Log;
 
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
 
 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
@@ -36,6 +41,8 @@
 public class MbmsDownloadManager {
     private static final String LOG_TAG = MbmsDownloadManager.class.getSimpleName();
 
+    public static final String MBMS_DOWNLOAD_SERVICE_ACTION =
+            "android.telephony.action.EmbmsDownload";
     /**
      * The MBMS middleware should send this when a download of single file has completed or
      * failed. Mandatory extras are
@@ -76,15 +83,15 @@
             "android.telephony.mbms.action.CLEANUP";
 
     /**
-     * Integer extra indicating the result code of the download.
-     * TODO: put in link to error list
-     * TODO: future systemapi (here and and all extras)
+     * Integer extra indicating the result code of the download. One of
+     * {@link #RESULT_SUCCESSFUL}, {@link #RESULT_EXPIRED}, or {@link #RESULT_CANCELLED}.
      */
     public static final String EXTRA_RESULT = "android.telephony.mbms.extra.RESULT";
 
     /**
      * Extra containing the {@link android.telephony.mbms.FileInfo} for which the download result
      * is for. Must not be null.
+     * TODO: future systemapi (here and and all extras) except the two for the app intent
      */
     public static final String EXTRA_INFO = "android.telephony.mbms.extra.INFO";
 
@@ -143,11 +150,23 @@
     public static final String EXTRA_TEMP_FILES_IN_USE =
             "android.telephony.mbms.extra.TEMP_FILES_IN_USE";
 
+    /**
+     * Extra containing a single {@link Uri} indicating the location of the successfully
+     * downloaded file. Set on the intent provided via
+     * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)}.
+     * Will always be set to a non-null value if {@link #EXTRA_RESULT} is set to
+     * {@link #RESULT_SUCCESSFUL}.
+     */
+    public static final String EXTRA_COMPLETED_FILE_URI =
+            "android.telephony.mbms.extra.COMPLETED_FILE_URI";
+
     public static final int RESULT_SUCCESSFUL = 1;
     public static final int RESULT_CANCELLED  = 2;
     public static final int RESULT_EXPIRED    = 3;
     // TODO - more results!
 
+    private static final long BIND_TIMEOUT_MS = 3000;
+
     private final Context mContext;
     private int mSubId = INVALID_SUBSCRIPTION_ID;
 
@@ -199,12 +218,31 @@
     }
 
     private void bindAndInitialize() throws MbmsException {
-        // TODO: bind
-        try {
-            mService.initialize(mDownloadAppName, mSubId, mCallback);
-        } catch (RemoteException e) {
-            throw new MbmsException(0); // TODO: proper error code
-        }
+        // TODO: fold binding for download and streaming into a common utils class.
+        final CountDownLatch latch = new CountDownLatch(1);
+        ServiceConnection bindListener = new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName name, IBinder service) {
+                mService = IMbmsDownloadService.Stub.asInterface(service);
+                latch.countDown();
+            }
+
+            @Override
+            public void onServiceDisconnected(ComponentName name) {
+                mService = null;
+            }
+        };
+
+        Intent bindIntent = new Intent();
+        bindIntent.setComponent(MbmsUtils.toComponentName(
+                MbmsUtils.getMiddlewareServiceInfo(mContext, MBMS_DOWNLOAD_SERVICE_ACTION)));
+
+        // Kick off the binding, and synchronously wait until binding is complete
+        mContext.bindService(bindIntent, bindListener, Context.BIND_AUTO_CREATE);
+
+        MbmsUtils.waitOnLatchWithTimeout(latch, BIND_TIMEOUT_MS);
+
+        // TODO: initialize
     }
 
     /**
@@ -245,6 +283,11 @@
      */
     public DownloadRequest download(DownloadRequest request, IDownloadCallback listener) {
         request.setAppName(mDownloadAppName);
+        try {
+            mService.download(request, listener);
+        } catch (RemoteException e) {
+            mService = null;
+        }
         return request;
     }
 
diff --git a/telephony/java/android/telephony/MbmsStreamingManager.java b/telephony/java/android/telephony/MbmsStreamingManager.java
index e90a63c..f68e243 100644
--- a/telephony/java/android/telephony/MbmsStreamingManager.java
+++ b/telephony/java/android/telephony/MbmsStreamingManager.java
@@ -27,6 +27,7 @@
 import android.os.RemoteException;
 import android.telephony.mbms.MbmsException;
 import android.telephony.mbms.MbmsStreamingManagerCallback;
+import android.telephony.mbms.MbmsUtils;
 import android.telephony.mbms.StreamingService;
 import android.telephony.mbms.StreamingServiceCallback;
 import android.telephony.mbms.StreamingServiceInfo;
@@ -62,7 +63,9 @@
                 Log.i(LOG_TAG, String.format("Connected to service %s", name));
                 synchronized (MbmsStreamingManager.this) {
                     mService = IMbmsStreamingService.Stub.asInterface(service);
-                    mServiceListeners.forEach(ServiceListener::onServiceConnected);
+                    for (ServiceListener l : mServiceListeners) {
+                        l.onServiceConnected();
+                    }
                 }
             }
         }
@@ -72,10 +75,13 @@
             Log.i(LOG_TAG, String.format("Disconnected from service %s", name));
             synchronized (MbmsStreamingManager.this) {
                 mService = null;
-                mServiceListeners.forEach(ServiceListener::onServiceDisconnected);
+                for (ServiceListener l : mServiceListeners) {
+                    l.onServiceDisconnected();
+                }
             }
         }
     };
+
     private List<ServiceListener> mServiceListeners = new LinkedList<>();
 
     private MbmsStreamingManagerCallback mCallbackToApp;
@@ -218,22 +224,6 @@
     }
 
     private void bindAndInitialize() throws MbmsException {
-        // Query for the proper service
-        PackageManager packageManager = mContext.getPackageManager();
-        Intent queryIntent = new Intent();
-        queryIntent.setAction(MBMS_STREAMING_SERVICE_ACTION);
-        List<ResolveInfo> streamingServices = packageManager.queryIntentServices(queryIntent,
-                PackageManager.MATCH_SYSTEM_ONLY);
-
-        if (streamingServices == null || streamingServices.size() == 0) {
-            throw new MbmsException(
-                    MbmsException.ERROR_NO_SERVICE_INSTALLED);
-        }
-        if (streamingServices.size() > 1) {
-            throw new MbmsException(
-                    MbmsException.ERROR_MULTIPLE_SERVICES_INSTALLED);
-        }
-
         // Kick off the binding, and synchronously wait until binding is complete
         final CountDownLatch latch = new CountDownLatch(1);
         ServiceListener bindListener = new ServiceListener() {
@@ -252,13 +242,14 @@
         }
 
         Intent bindIntent = new Intent();
-        bindIntent.setComponent(streamingServices.get(0).getComponentInfo().getComponentName());
+        bindIntent.setComponent(MbmsUtils.toComponentName(
+                MbmsUtils.getMiddlewareServiceInfo(mContext, MBMS_STREAMING_SERVICE_ACTION)));
 
         mContext.bindService(bindIntent, mServiceConnection, Context.BIND_AUTO_CREATE);
 
-        waitOnLatchWithTimeout(latch, BIND_TIMEOUT_MS);
+        MbmsUtils.waitOnLatchWithTimeout(latch, BIND_TIMEOUT_MS);
 
-        // Remove the listener and call the initialization method through the interface.
+       // Remove the listener and call the initialization method through the interface.
         synchronized (this) {
             mServiceListeners.remove(bindListener);
 
@@ -279,17 +270,4 @@
         }
     }
 
-    private static void waitOnLatchWithTimeout(CountDownLatch l, long timeoutMs) {
-        long endTime = System.currentTimeMillis() + timeoutMs;
-        while (System.currentTimeMillis() < endTime) {
-            try {
-                l.await(timeoutMs, TimeUnit.MILLISECONDS);
-            } catch (InterruptedException e) {
-                // keep waiting
-            }
-            if (l.getCount() <= 0) {
-                return;
-            }
-        }
-    }
 }
diff --git a/telephony/java/android/telephony/mbms/MbmsDownloadReceiver.java b/telephony/java/android/telephony/mbms/MbmsDownloadReceiver.java
new file mode 100644
index 0000000..c01ddae
--- /dev/null
+++ b/telephony/java/android/telephony/mbms/MbmsDownloadReceiver.java
@@ -0,0 +1,365 @@
+/*
+ * 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 android.telephony.mbms;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.telephony.MbmsDownloadManager;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * @hide
+ */
+public class MbmsDownloadReceiver extends BroadcastReceiver {
+    private static final String LOG_TAG = "MbmsDownloadReceiver";
+    private static final String TEMP_FILE_SUFFIX = ".embms.temp";
+    private static final int MAX_TEMP_FILE_RETRIES = 5;
+
+    public static final String MBMS_FILE_PROVIDER_META_DATA_KEY = "mbms-file-provider-authority";
+
+    private String mFileProviderAuthorityCache = null;
+    private String mMiddlewarePackageNameCache = null;
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (!verifyIntentContents(intent)) {
+            setResultCode(1 /* TODO: define error constants */);
+            return;
+        }
+
+        if (MbmsDownloadManager.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) {
+            moveDownloadedFile(context, intent);
+            cleanupPostMove(context, intent);
+        } else if (MbmsDownloadManager.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) {
+            generateTempFiles(context, intent);
+        }
+        // TODO: Add handling for ACTION_CLEANUP
+    }
+
+    private boolean verifyIntentContents(Intent intent) {
+        if (MbmsDownloadManager.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) {
+            if (!intent.hasExtra(MbmsDownloadManager.EXTRA_RESULT)) {
+                Log.w(LOG_TAG, "Download result did not include a result code. Ignoring.");
+                return false;
+            }
+            if (!intent.hasExtra(MbmsDownloadManager.EXTRA_REQUEST)) {
+                Log.w(LOG_TAG, "Download result did not include the associated request. Ignoring.");
+                return false;
+            }
+            if (!intent.hasExtra(MbmsDownloadManager.EXTRA_INFO)) {
+                Log.w(LOG_TAG, "Download result did not include the associated file info. " +
+                        "Ignoring.");
+                return false;
+            }
+            if (!intent.hasExtra(MbmsDownloadManager.EXTRA_FINAL_URI)) {
+                Log.w(LOG_TAG, "Download result did not include the path to the final " +
+                        "temp file. Ignoring.");
+                return false;
+            }
+            return true;
+        } else if (MbmsDownloadManager.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) {
+            if (!intent.hasExtra(MbmsDownloadManager.EXTRA_REQUEST)) {
+                Log.w(LOG_TAG, "Temp file request not include the associated request. Ignoring.");
+                return false;
+            }
+            return true;
+        }
+
+        Log.w(LOG_TAG, "Received intent with unknown action: " + intent.getAction());
+        return false;
+    }
+
+    private void moveDownloadedFile(Context context, Intent intent) {
+        DownloadRequest request = intent.getParcelableExtra(MbmsDownloadManager.EXTRA_REQUEST);
+        // TODO: check request against token
+        Intent intentForApp = request.getIntentForApp();
+
+        int result = intent.getIntExtra(MbmsDownloadManager.EXTRA_RESULT,
+                MbmsDownloadManager.RESULT_CANCELLED);
+        intentForApp.putExtra(MbmsDownloadManager.EXTRA_RESULT, result);
+
+        if (result != MbmsDownloadManager.RESULT_SUCCESSFUL) {
+            Log.i(LOG_TAG, "Download request indicated a failed download. Aborting.");
+            context.sendBroadcast(intentForApp);
+            return;
+        }
+
+        Uri destinationUri = request.getDestinationUri();
+        Uri finalTempFile = intent.getParcelableExtra(MbmsDownloadManager.EXTRA_FINAL_URI);
+        if (!verifyTempFilePath(context, request, finalTempFile)) {
+            Log.w(LOG_TAG, "Download result specified an invalid temp file " + finalTempFile);
+            setResultCode(1);
+            return;
+        }
+
+        String relativePath = calculateDestinationFileRelativePath(request,
+                (FileInfo) intent.getParcelableExtra(MbmsDownloadManager.EXTRA_INFO));
+
+        if (!moveTempFile(finalTempFile, destinationUri, relativePath)) {
+            Log.w(LOG_TAG, "Failed to move temp file to final destination");
+            setResultCode(1);
+        }
+
+        context.sendBroadcast(intentForApp);
+        setResultCode(0);
+    }
+
+    private void cleanupPostMove(Context context, Intent intent) {
+        // TODO: account for in-use temp files
+        DownloadRequest request = intent.getParcelableExtra(MbmsDownloadManager.EXTRA_REQUEST);
+        if (request == null) {
+            Log.w(LOG_TAG, "Intent does not include a DownloadRequest. Ignoring.");
+            return;
+        }
+
+        List<Uri> tempFiles = intent.getParcelableExtra(MbmsDownloadManager.EXTRA_TEMP_LIST);
+        if (tempFiles == null) {
+            return;
+        }
+
+        for (Uri tempFileUri : tempFiles) {
+            if (verifyTempFilePath(context, request, tempFileUri)) {
+                File tempFile = new File(tempFileUri.getSchemeSpecificPart());
+                tempFile.delete();
+            }
+        }
+    }
+
+    private void generateTempFiles(Context context, Intent intent) {
+        // TODO: update pursuant to final decision on temp file locations
+        DownloadRequest request = intent.getParcelableExtra(MbmsDownloadManager.EXTRA_REQUEST);
+        if (request == null) {
+            Log.w(LOG_TAG, "Temp file request did not include the associated request. Ignoring.");
+            setResultCode(1 /* TODO: define error constants */);
+            return;
+        }
+        int fdCount = intent.getIntExtra(MbmsDownloadManager.EXTRA_FD_COUNT, 0);
+        List<Uri> pausedList = intent.getParcelableExtra(MbmsDownloadManager.EXTRA_PAUSED_LIST);
+
+        if (fdCount == 0 && (pausedList == null || pausedList.size() == 0)) {
+            Log.i(LOG_TAG, "No temp files actually requested. Ending.");
+            setResultCode(0);
+            setResultExtras(Bundle.EMPTY);
+            return;
+        }
+
+        ArrayList<UriPathPair> freshTempFiles = generateFreshTempFiles(context, request, fdCount);
+        ArrayList<UriPathPair> pausedFiles =
+                generateUrisForPausedFiles(context, request, pausedList);
+
+        Bundle result = new Bundle();
+        result.putParcelableArrayList(MbmsDownloadManager.EXTRA_FREE_URI_LIST, freshTempFiles);
+        result.putParcelableArrayList(MbmsDownloadManager.EXTRA_PAUSED_URI_LIST, pausedFiles);
+        setResultExtras(result);
+    }
+
+    private ArrayList<UriPathPair> generateFreshTempFiles(Context context, DownloadRequest request,
+            int freshFdCount) {
+        File tempFileDir = getEmbmsTempFileDirForRequest(context, request);
+        if (!tempFileDir.exists()) {
+            tempFileDir.mkdirs();
+        }
+
+        // Name the files with the template "N-UUID", where N is the request ID and UUID is a
+        // random uuid.
+        ArrayList<UriPathPair> result = new ArrayList<>(freshFdCount);
+        for (int i = 0; i < freshFdCount; i++) {
+            File tempFile = generateSingleTempFile(tempFileDir);
+            if (tempFile == null) {
+                setResultCode(2 /* TODO: define error constants */);
+                Log.w(LOG_TAG, "Failed to generate a temp file. Moving on.");
+                continue;
+            }
+            Uri fileUri = Uri.fromParts(ContentResolver.SCHEME_FILE, tempFile.getPath(), null);
+            Uri contentUri = MbmsTempFileProvider.getUriForFile(
+                    context, getFileProviderAuthorityCached(context), tempFile);
+            context.grantUriPermission(getMiddlewarePackageCached(context), contentUri,
+                    Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+            result.add(new UriPathPair(fileUri, contentUri));
+        }
+
+        return result;
+    }
+
+    private static File generateSingleTempFile(File tempFileDir) {
+        int numTries = 0;
+        while (numTries < MAX_TEMP_FILE_RETRIES) {
+            numTries++;
+            String fileName =  UUID.randomUUID() + TEMP_FILE_SUFFIX;
+            File tempFile = new File(tempFileDir, fileName);
+            try {
+                if (tempFile.createNewFile()) {
+                    return tempFile.getCanonicalFile();
+                }
+            } catch (IOException e) {
+                continue;
+            }
+        }
+        return null;
+    }
+
+
+    private ArrayList<UriPathPair> generateUrisForPausedFiles(Context context,
+            DownloadRequest request, List<Uri> pausedFiles) {
+        if (pausedFiles == null) {
+            return new ArrayList<>(0);
+        }
+        ArrayList<UriPathPair> result = new ArrayList<>(pausedFiles.size());
+
+        for (Uri fileUri : pausedFiles) {
+            if (!verifyTempFilePath(context, request, fileUri)) {
+                Log.w(LOG_TAG, "Supplied file " + fileUri + " is not a valid temp file to resume");
+                setResultCode(2 /* TODO: define error codes */);
+                continue;
+            }
+            File tempFile = new File(fileUri.getSchemeSpecificPart());
+            if (!tempFile.exists()) {
+                Log.w(LOG_TAG, "Supplied file " + fileUri + " does not exist.");
+                setResultCode(2 /* TODO: define error codes */);
+                continue;
+            }
+            Uri contentUri = MbmsTempFileProvider.getUriForFile(
+                    context, getFileProviderAuthorityCached(context), tempFile);
+            context.grantUriPermission(getMiddlewarePackageCached(context), contentUri,
+                    Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+
+            result.add(new UriPathPair(fileUri, contentUri));
+        }
+        return result;
+    }
+
+    private static String calculateDestinationFileRelativePath(DownloadRequest request,
+            FileInfo info) {
+        // TODO: determine whether this is actually the path determination scheme we want to use
+        List<String> filePathComponents = info.uri.getPathSegments();
+        List<String> requestPathComponents = request.getSourceUri().getPathSegments();
+        Iterator<String> filePathIter = filePathComponents.iterator();
+        Iterator<String> requestPathIter = requestPathComponents.iterator();
+
+        LinkedList<String> relativePathComponents = new LinkedList<>();
+        while (filePathIter.hasNext()) {
+            String currFilePathComponent = filePathIter.next();
+            if (requestPathIter.hasNext()) {
+                String requestFilePathComponent = requestPathIter.next();
+                if (requestFilePathComponent.equals(currFilePathComponent)) {
+                    continue;
+                }
+            }
+            relativePathComponents.add(currFilePathComponent);
+        }
+        return String.join("/", relativePathComponents);
+    }
+
+    private static boolean moveTempFile(Uri fromPath, Uri toPath, String relativePath) {
+        if (!ContentResolver.SCHEME_FILE.equals(fromPath.getScheme())) {
+            Log.w(LOG_TAG, "Moving source uri " + fromPath+ " does not have a file scheme");
+            return false;
+        }
+        if (!ContentResolver.SCHEME_FILE.equals(toPath.getScheme())) {
+            Log.w(LOG_TAG, "Moving destination uri " + toPath + " does not have a file scheme");
+            return false;
+        }
+
+        File fromFile = new File(fromPath.getSchemeSpecificPart());
+        File toFile = new File(toPath.getSchemeSpecificPart(), relativePath);
+        toFile.getParentFile().mkdirs();
+
+        // TODO: This may not work if the two files are on different filesystems. Should we
+        // enforce that the temp file storage and the permanent storage are both in the same fs?
+        return fromFile.renameTo(toFile);
+    }
+
+    private static boolean verifyTempFilePath(Context context, DownloadRequest request,
+            Uri filePath) {
+        // TODO: modify pursuant to final decision on temp file path scheme
+        if (!ContentResolver.SCHEME_FILE.equals(filePath.getScheme())) {
+            Log.w(LOG_TAG, "Uri " + filePath + " does not have a file scheme");
+            return false;
+        }
+
+        String path = filePath.getSchemeSpecificPart();
+        File tempFile = new File(path);
+        if (!tempFile.exists()) {
+            Log.w(LOG_TAG, "File at " + path + " does not exist.");
+            return false;
+        }
+
+        if (!MbmsUtils.isContainedIn(getEmbmsTempFileDirForRequest(context, request), tempFile)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns a File linked to the directory used to store temp files for this request
+     */
+    private static File getEmbmsTempFileDirForRequest(Context context, DownloadRequest request) {
+        File embmsTempFileDir = MbmsTempFileProvider.getEmbmsTempFileDir(
+                context, getFileProviderAuthority(context));
+
+        // TODO: better naming scheme for temp file dirs
+        String tempFileDirName = String.valueOf(request.getFileServiceInfo().getServiceId());
+        return new File(embmsTempFileDir, tempFileDirName);
+    }
+
+    private String getFileProviderAuthorityCached(Context context) {
+        if (mFileProviderAuthorityCache != null) {
+            return mFileProviderAuthorityCache;
+        }
+
+        mFileProviderAuthorityCache = getFileProviderAuthority(context);
+        return mFileProviderAuthorityCache;
+    }
+
+    private static String getFileProviderAuthority(Context context) {
+        ApplicationInfo appInfo;
+        try {
+            appInfo = context.getPackageManager()
+                    .getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new RuntimeException("Package manager couldn't find " + context.getPackageName());
+        }
+        String authority = appInfo.metaData.getString(MBMS_FILE_PROVIDER_META_DATA_KEY);
+        if (authority == null) {
+            throw new RuntimeException("Must declare the file provider authority as meta data");
+        }
+        return authority;
+    }
+
+    private String getMiddlewarePackageCached(Context context) {
+        if (mMiddlewarePackageNameCache == null) {
+            mMiddlewarePackageNameCache = MbmsUtils.getMiddlewareServiceInfo(context,
+                    MbmsDownloadManager.MBMS_DOWNLOAD_SERVICE_ACTION).packageName;
+        }
+        return mMiddlewarePackageNameCache;
+    }
+}
diff --git a/telephony/java/android/telephony/mbms/MbmsTempFileProvider.java b/telephony/java/android/telephony/mbms/MbmsTempFileProvider.java
new file mode 100644
index 0000000..9842581
--- /dev/null
+++ b/telephony/java/android/telephony/mbms/MbmsTempFileProvider.java
@@ -0,0 +1,193 @@
+/*
+ * 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 android.telephony.mbms;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+/**
+ * @hide
+ */
+public class MbmsTempFileProvider extends ContentProvider {
+    public static final String META_DATA_USE_EXTERNAL_STORAGE = "use-external-storage";
+    public static final String META_DATA_TEMP_FILE_DIRECTORY = "temp-file-path";
+    public static final String DEFAULT_TOP_LEVEL_TEMP_DIRECTORY = "androidMbmsTempFileRoot";
+
+    private String mAuthority;
+    private Context mContext;
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    @Override
+    public Cursor query(@NonNull Uri uri, @Nullable String[] projection,
+            @Nullable String selection, @Nullable String[] selectionArgs,
+            @Nullable String sortOrder) {
+        throw new UnsupportedOperationException("No querying supported");
+    }
+
+    @Override
+    public String getType(@NonNull Uri uri) {
+        // EMBMS temp files can contain arbitrary content.
+        return "application/octet-stream";
+    }
+
+    @Override
+    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+        throw new UnsupportedOperationException("No inserting supported");
+    }
+
+    @Override
+    public int delete(@NonNull Uri uri, @Nullable String selection,
+            @Nullable String[] selectionArgs) {
+        throw new UnsupportedOperationException("No deleting supported");
+    }
+
+    @Override
+    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String
+            selection, @Nullable String[] selectionArgs) {
+        throw new UnsupportedOperationException("No updating supported");
+    }
+
+    @Override
+    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+        // ContentProvider has already checked granted permissions
+        final File file = getFileForUri(mContext, mAuthority, uri);
+        final int fileMode = ParcelFileDescriptor.parseMode(mode);
+        return ParcelFileDescriptor.open(file, fileMode);
+    }
+
+    @Override
+    public void attachInfo(Context context, ProviderInfo info) {
+        super.attachInfo(context, info);
+
+        // Sanity check our security
+        if (info.exported) {
+            throw new SecurityException("Provider must not be exported");
+        }
+        if (!info.grantUriPermissions) {
+            throw new SecurityException("Provider must grant uri permissions");
+        }
+
+        mAuthority = info.authority;
+        mContext = context;
+    }
+
+    public static Uri getUriForFile(Context context, String authority, File file) {
+        // Get the canonical path of the temp file
+        String filePath;
+        try {
+            filePath = file.getCanonicalPath();
+        } catch (IOException e) {
+            throw new IllegalArgumentException("Could not get canonical path for file " + file);
+        }
+
+        // Make sure the temp file is contained in the temp file directory as configured in the
+        // manifest
+        File tempFileDir = getEmbmsTempFileDir(context, authority);
+        if (!MbmsUtils.isContainedIn(tempFileDir, file)) {
+            throw new IllegalArgumentException("File " + file + " is not contained in the temp " +
+                    "file directory, which is " + tempFileDir);
+        }
+
+        // Get the canonical path of the temp file directory
+        String tempFileDirPath;
+        try {
+            tempFileDirPath = tempFileDir.getCanonicalPath();
+        } catch (IOException e) {
+            throw new RuntimeException(
+                    "Could not get canonical path for temp file root dir " + tempFileDir);
+        }
+
+        // Start at first char of path under temp file directory
+        String pathFragment;
+        if (tempFileDirPath.endsWith("/")) {
+            pathFragment = filePath.substring(tempFileDirPath.length());
+        } else {
+            pathFragment = filePath.substring(tempFileDirPath.length() + 1);
+        }
+
+        String encodedPath = Uri.encode(pathFragment);
+        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(authority).encodedPath(encodedPath).build();
+    }
+
+    public static File getFileForUri(Context context, String authority, Uri uri)
+            throws FileNotFoundException {
+        if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
+            throw new IllegalArgumentException("Uri must have scheme content");
+        }
+
+        String relPath = Uri.decode(uri.getEncodedPath());
+        File file;
+        File tempFileDir;
+
+        try {
+            tempFileDir = getEmbmsTempFileDir(context, authority).getCanonicalFile();
+            file = new File(tempFileDir, relPath).getCanonicalFile();
+        } catch (IOException e) {
+            throw new FileNotFoundException("Could not resolve paths");
+        }
+
+        if (!file.getPath().startsWith(tempFileDir.getPath())) {
+            throw new SecurityException("Resolved path jumped beyond configured root");
+        }
+
+        return file;
+    }
+
+    /**
+     * Returns a File for the directory used to store temp files for this app
+     */
+    public static File getEmbmsTempFileDir(Context context, String authority) {
+        Bundle metadata = getMetadata(context, authority);
+        File parentDirectory;
+        if (metadata.getBoolean(META_DATA_USE_EXTERNAL_STORAGE, false)) {
+            parentDirectory = context.getExternalFilesDir(null);
+        } else {
+            parentDirectory = context.getFilesDir();
+        }
+
+        String tmpFilePath = metadata.getString(META_DATA_TEMP_FILE_DIRECTORY);
+        if (tmpFilePath == null) {
+            tmpFilePath = DEFAULT_TOP_LEVEL_TEMP_DIRECTORY;
+        }
+        return new File(parentDirectory, tmpFilePath);
+    }
+
+    private static Bundle getMetadata(Context context, String authority) {
+        final ProviderInfo info = context.getPackageManager()
+                .resolveContentProvider(authority, PackageManager.GET_META_DATA);
+        return info.metaData;
+    }
+}
diff --git a/telephony/java/android/telephony/mbms/MbmsUtils.java b/telephony/java/android/telephony/mbms/MbmsUtils.java
new file mode 100644
index 0000000..de30805
--- /dev/null
+++ b/telephony/java/android/telephony/mbms/MbmsUtils.java
@@ -0,0 +1,86 @@
+/*
+ * 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 android.telephony.mbms;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.*;
+import android.content.pm.ServiceInfo;
+import android.telephony.MbmsDownloadManager;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @hide
+ */
+public class MbmsUtils {
+    private static final String LOG_TAG = "MbmsUtils";
+
+    public static boolean isContainedIn(File parent, File child) {
+        try {
+            String parentPath = parent.getCanonicalPath();
+            String childPath = child.getCanonicalPath();
+            return childPath.startsWith(parentPath);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to resolve canonical paths: " + e);
+        }
+    }
+
+    public static void waitOnLatchWithTimeout(CountDownLatch l, long timeoutMs) {
+        long endTime = System.currentTimeMillis() + timeoutMs;
+        while (System.currentTimeMillis() < endTime) {
+            try {
+                l.await(timeoutMs, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                // keep waiting
+            }
+            if (l.getCount() <= 0) {
+                return;
+            }
+        }
+    }
+
+    public static ComponentName toComponentName(ComponentInfo ci) {
+        return new ComponentName(ci.packageName, ci.name);
+    }
+
+    public static ServiceInfo getMiddlewareServiceInfo(Context context, String serviceAction) {
+        // Query for the proper service
+        PackageManager packageManager = context.getPackageManager();
+        Intent queryIntent = new Intent();
+        queryIntent.setAction(serviceAction);
+        List<ResolveInfo> downloadServices = packageManager.queryIntentServices(queryIntent,
+                PackageManager.MATCH_SYSTEM_ONLY);
+
+        if (downloadServices == null || downloadServices.size() == 0) {
+            Log.w(LOG_TAG, "No download services found, cannot get service info");
+            return null;
+        }
+
+        if (downloadServices.size() > 1) {
+            Log.w(LOG_TAG, "More than one download service found, cannot get unique service");
+            return null;
+        }
+        return downloadServices.get(0).serviceInfo;
+    }
+}
diff --git a/telephony/java/android/telephony/mbms/vendor/MbmsStreamingServiceBase.java b/telephony/java/android/telephony/mbms/vendor/MbmsStreamingServiceBase.java
index 5b74312..2c1f085 100644
--- a/telephony/java/android/telephony/mbms/vendor/MbmsStreamingServiceBase.java
+++ b/telephony/java/android/telephony/mbms/vendor/MbmsStreamingServiceBase.java
@@ -82,7 +82,8 @@
      * @param subscriptionId The subscription id to use.
      * @param serviceId The ID of the streaming service that the app has requested.
      * @param listener The listener object on which the app wishes to receive updates.
-     * @return TODO: document possible errors
+     * @return {@link MbmsException#SUCCESS}, {@link MbmsException#ERROR_STREAM_ALREADY_STARTED},
+     *         or {@link MbmsException#ERROR_UNABLE_TO_START_SERVICE}.
      */
     @Override
     public int startStreaming(String appName, int subscriptionId,
@@ -111,6 +112,9 @@
      * Stop streaming the stream identified by {@code serviceId}. Notification of the resulting
      * stream state change should be reported to the app via
      * {@link IStreamingServiceCallback#streamStateChanged(int)}.
+     *
+     * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException}
+     *
      * @param appName The app name as negotiated with the wireless carrier.
      * @param subscriptionId The subscription id to use.
      * @param serviceId The ID of the streaming service that the app wishes to stop.
@@ -126,6 +130,9 @@
      * No notification back to the app is required for this operation, and the callback provided via
      * {@link #startStreaming(String, int, String, IStreamingServiceCallback)} should no longer be
      * used after this method has called by the app.
+     *
+     * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException}
+     *
      * @param appName The app name as negotiated with the wireless carrier.
      * @param subscriptionId The subscription id to use.
      * @param serviceId The ID of the streaming service that the app wishes to dispose of.
@@ -141,6 +148,9 @@
      * app is required for this operation, and the corresponding callback provided via
      * {@link #initialize(IMbmsStreamingManagerCallback, String, int)} should no longer be used
      * after this method has been called by the app.
+     *
+     * May throw an {@link IllegalStateException}
+     *
      * @param appName The app name as negotiated with the wireless carrier.
      * @param subscriptionId The subscription id to use.
      */
diff --git a/tests/net/java/com/android/server/connectivity/TetheringTest.java b/tests/net/java/com/android/server/connectivity/TetheringTest.java
index bd049b8..2137e55 100644
--- a/tests/net/java/com/android/server/connectivity/TetheringTest.java
+++ b/tests/net/java/com/android/server/connectivity/TetheringTest.java
@@ -42,6 +42,7 @@
 import android.content.ContextWrapper;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
 import android.content.res.Resources;
 import android.hardware.usb.UsbManager;
 import android.net.ConnectivityManager;
@@ -85,6 +86,7 @@
 public class TetheringTest {
     private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
 
+    @Mock private ApplicationInfo mApplicationInfo;
     @Mock private Context mContext;
     @Mock private ConnectivityManager mConnectivityManager;
     @Mock private INetworkManagementService mNMService;
@@ -116,6 +118,9 @@
         }
 
         @Override
+        public ApplicationInfo getApplicationInfo() { return mApplicationInfo; }
+
+        @Override
         public ContentResolver getContentResolver() { return mContentResolver; }
 
         @Override
@@ -389,7 +394,6 @@
                 any(NetworkCallback.class), any(Handler.class));
         // In tethering mode, in the default configuration, an explicit request
         // for a mobile network is also made.
-        verify(mConnectivityManager, atLeastOnce()).getNetworkInfo(anyInt());
         verify(mConnectivityManager, times(1)).requestNetwork(
                 any(NetworkRequest.class), any(NetworkCallback.class), eq(0), anyInt(),
                 any(Handler.class));
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 c535c45..4d340d1 100644
--- a/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java
+++ b/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java
@@ -30,6 +30,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.RouteInfo;
@@ -59,15 +60,17 @@
 public class OffloadControllerTest {
 
     @Mock private OffloadHardwareInterface mHardware;
+    @Mock private ApplicationInfo mApplicationInfo;
     @Mock private Context mContext;
     final ArgumentCaptor<ArrayList> mStringArrayCaptor = ArgumentCaptor.forClass(ArrayList.class);
     private MockContentResolver mContentResolver;
 
     @Before public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
+        when(mContext.getApplicationInfo()).thenReturn(mApplicationInfo);
+        when(mContext.getPackageName()).thenReturn("OffloadControllerTest");
         mContentResolver = new MockContentResolver(mContext);
         mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
-        when(mContext.getPackageName()).thenReturn("OffloadControllerTest");
         when(mContext.getContentResolver()).thenReturn(mContentResolver);
     }
 
diff --git a/tests/net/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java b/tests/net/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java
index 27e683c..57c258f 100644
--- a/tests/net/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java
+++ b/tests/net/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java
@@ -16,7 +16,9 @@
 
 package com.android.server.connectivity.tethering;
 
+import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.reset;
@@ -38,6 +40,7 @@
 import android.net.ConnectivityManager;
 import android.net.INetworkStatsService;
 import android.net.InterfaceConfiguration;
+import android.net.LinkProperties;
 import android.net.util.SharedLog;
 import android.os.INetworkManagementService;
 import android.os.RemoteException;
@@ -103,8 +106,9 @@
                 mIPv6TetheringInterfaceServices);
         mTestedSm.start();
         mLooper.dispatchAll();
-        verify(mTetherHelper).notifyInterfaceStateChange(
-                IFACE_NAME, mTestedSm, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
+        verify(mTetherHelper).updateInterfaceState(
+                mTestedSm, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
+        verify(mTetherHelper).updateLinkProperties(eq(mTestedSm), any(LinkProperties.class));
         verifyNoMoreInteractions(mTetherHelper, mNMService, mStatsService);
     }
 
@@ -133,8 +137,9 @@
         initStateMachine(TETHERING_BLUETOOTH);
 
         dispatchCommand(TetherInterfaceStateMachine.CMD_INTERFACE_DOWN);
-        verify(mTetherHelper).notifyInterfaceStateChange(
-                IFACE_NAME, mTestedSm, STATE_UNAVAILABLE, TETHER_ERROR_NO_ERROR);
+        verify(mTetherHelper).updateInterfaceState(
+                mTestedSm, STATE_UNAVAILABLE, TETHER_ERROR_NO_ERROR);
+        verify(mTetherHelper).updateLinkProperties(eq(mTestedSm), any(LinkProperties.class));
         verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
     }
 
@@ -145,8 +150,10 @@
         dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, STATE_TETHERED);
         InOrder inOrder = inOrder(mTetherHelper, mNMService);
         inOrder.verify(mNMService).tetherInterface(IFACE_NAME);
-        inOrder.verify(mTetherHelper).notifyInterfaceStateChange(
-                IFACE_NAME, mTestedSm, STATE_TETHERED, TETHER_ERROR_NO_ERROR);
+        inOrder.verify(mTetherHelper).updateInterfaceState(
+                mTestedSm, STATE_TETHERED, TETHER_ERROR_NO_ERROR);
+        inOrder.verify(mTetherHelper).updateLinkProperties(
+                eq(mTestedSm), any(LinkProperties.class));
         verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
     }
 
@@ -157,8 +164,10 @@
         dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_UNREQUESTED);
         InOrder inOrder = inOrder(mNMService, mStatsService, mTetherHelper);
         inOrder.verify(mNMService).untetherInterface(IFACE_NAME);
-        inOrder.verify(mTetherHelper).notifyInterfaceStateChange(
-                IFACE_NAME, mTestedSm, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
+        inOrder.verify(mTetherHelper).updateInterfaceState(
+                mTestedSm, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
+        inOrder.verify(mTetherHelper).updateLinkProperties(
+                eq(mTestedSm), any(LinkProperties.class));
         verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
     }
 
@@ -171,8 +180,10 @@
         inOrder.verify(mNMService).getInterfaceConfig(IFACE_NAME);
         inOrder.verify(mNMService).setInterfaceConfig(IFACE_NAME, mInterfaceConfiguration);
         inOrder.verify(mNMService).tetherInterface(IFACE_NAME);
-        inOrder.verify(mTetherHelper).notifyInterfaceStateChange(
-                IFACE_NAME, mTestedSm, STATE_TETHERED, TETHER_ERROR_NO_ERROR);
+        inOrder.verify(mTetherHelper).updateInterfaceState(
+                mTestedSm, STATE_TETHERED, TETHER_ERROR_NO_ERROR);
+        inOrder.verify(mTetherHelper).updateLinkProperties(
+                eq(mTestedSm), any(LinkProperties.class));
         verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
     }
 
@@ -180,7 +191,8 @@
     public void handlesFirstUpstreamChange() throws Exception {
         initTetheredStateMachine(TETHERING_BLUETOOTH, null);
 
-        // Telling the state machine about its upstream interface triggers a little more configuration.
+        // Telling the state machine about its upstream interface triggers
+        // a little more configuration.
         dispatchTetherConnectionChanged(UPSTREAM_IFACE);
         InOrder inOrder = inOrder(mNMService);
         inOrder.verify(mNMService).enableNat(IFACE_NAME, UPSTREAM_IFACE);
@@ -248,8 +260,10 @@
         inOrder.verify(mNMService).stopInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mNMService).untetherInterface(IFACE_NAME);
-        inOrder.verify(mTetherHelper).notifyInterfaceStateChange(
-                IFACE_NAME, mTestedSm, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
+        inOrder.verify(mTetherHelper).updateInterfaceState(
+                mTestedSm, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
+        inOrder.verify(mTetherHelper).updateLinkProperties(
+                eq(mTestedSm), any(LinkProperties.class));
         verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
     }
 
@@ -266,8 +280,10 @@
             usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown();
             usbTeardownOrder.verify(mNMService).setInterfaceConfig(
                     IFACE_NAME, mInterfaceConfiguration);
-            usbTeardownOrder.verify(mTetherHelper).notifyInterfaceStateChange(
-                    IFACE_NAME, mTestedSm, STATE_UNAVAILABLE, TETHER_ERROR_NO_ERROR);
+            usbTeardownOrder.verify(mTetherHelper).updateInterfaceState(
+                    mTestedSm, STATE_UNAVAILABLE, TETHER_ERROR_NO_ERROR);
+            usbTeardownOrder.verify(mTetherHelper).updateLinkProperties(
+                    eq(mTestedSm), any(LinkProperties.class));
         }
     }
 
@@ -281,8 +297,10 @@
         usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown();
         usbTeardownOrder.verify(mNMService).setInterfaceConfig(
                 IFACE_NAME, mInterfaceConfiguration);
-        usbTeardownOrder.verify(mTetherHelper).notifyInterfaceStateChange(
-                IFACE_NAME, mTestedSm, STATE_AVAILABLE, TETHER_ERROR_TETHER_IFACE_ERROR);
+        usbTeardownOrder.verify(mTetherHelper).updateInterfaceState(
+                mTestedSm, STATE_AVAILABLE, TETHER_ERROR_TETHER_IFACE_ERROR);
+        usbTeardownOrder.verify(mTetherHelper).updateLinkProperties(
+                eq(mTestedSm), any(LinkProperties.class));
     }
 
     @Test
@@ -294,8 +312,10 @@
         InOrder usbTeardownOrder = inOrder(mNMService, mInterfaceConfiguration, mTetherHelper);
         usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown();
         usbTeardownOrder.verify(mNMService).setInterfaceConfig(IFACE_NAME, mInterfaceConfiguration);
-        usbTeardownOrder.verify(mTetherHelper).notifyInterfaceStateChange(
-                IFACE_NAME, mTestedSm, STATE_AVAILABLE, TETHER_ERROR_ENABLE_NAT_ERROR);
+        usbTeardownOrder.verify(mTetherHelper).updateInterfaceState(
+                mTestedSm, STATE_AVAILABLE, TETHER_ERROR_ENABLE_NAT_ERROR);
+        usbTeardownOrder.verify(mTetherHelper).updateLinkProperties(
+                eq(mTestedSm), any(LinkProperties.class));
     }
 
     @Test
diff --git a/tests/net/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitorTest.java b/tests/net/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitorTest.java
index 9bb392a..fb6066e 100644
--- a/tests/net/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitorTest.java
+++ b/tests/net/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitorTest.java
@@ -18,7 +18,12 @@
 
 import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
 import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
+import static android.net.ConnectivityManager.TYPE_NONE;
+import static android.net.ConnectivityManager.TYPE_WIFI;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -59,6 +64,7 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -240,6 +246,72 @@
         assertFalse(mUNM.mobileNetworkRequested());
     }
 
+    @Test
+    public void testSelectPreferredUpstreamType() throws Exception {
+        final Collection<Integer> preferredTypes = new ArrayList<>();
+        preferredTypes.add(TYPE_WIFI);
+
+        mUNM.start();
+        // There are no networks, so there is nothing to select.
+        assertEquals(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
+
+        final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, TRANSPORT_WIFI);
+        wifiAgent.fakeConnect();
+        // WiFi is up, we should prefer it.
+        assertEquals(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes));
+        wifiAgent.fakeDisconnect();
+        // There are no networks, so there is nothing to select.
+        assertEquals(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
+
+        final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
+        cellAgent.fakeConnect();
+        assertEquals(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
+
+        preferredTypes.add(TYPE_MOBILE_DUN);
+        // This is coupled with preferred types in TetheringConfiguration.
+        mUNM.updateMobileRequiresDun(true);
+        // DUN is available, but only use regular cell: no upstream selected.
+        assertEquals(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
+        preferredTypes.remove(TYPE_MOBILE_DUN);
+        // No WiFi, but our preferred flavour of cell is up.
+        preferredTypes.add(TYPE_MOBILE_HIPRI);
+        // This is coupled with preferred types in TetheringConfiguration.
+        mUNM.updateMobileRequiresDun(false);
+        assertEquals(TYPE_MOBILE_HIPRI, mUNM.selectPreferredUpstreamType(preferredTypes));
+        // Check to see we filed an explicit request.
+        assertEquals(1, mCM.requested.size());
+        NetworkRequest netReq = (NetworkRequest) mCM.requested.values().toArray()[0];
+        assertTrue(netReq.networkCapabilities.hasTransport(TRANSPORT_CELLULAR));
+        assertFalse(netReq.networkCapabilities.hasCapability(NET_CAPABILITY_DUN));
+
+        wifiAgent.fakeConnect();
+        // WiFi is up, and we should prefer it over cell.
+        assertEquals(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes));
+        assertEquals(0, mCM.requested.size());
+
+        preferredTypes.remove(TYPE_MOBILE_HIPRI);
+        preferredTypes.add(TYPE_MOBILE_DUN);
+        // This is coupled with preferred types in TetheringConfiguration.
+        mUNM.updateMobileRequiresDun(true);
+        assertEquals(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes));
+
+        final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
+        dunAgent.networkCapabilities.addCapability(NET_CAPABILITY_DUN);
+        dunAgent.fakeConnect();
+
+        // WiFi is still preferred.
+        assertEquals(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes));
+
+        // WiFi goes down, cell and DUN are still up but only DUN is preferred.
+        wifiAgent.fakeDisconnect();
+        assertEquals(TYPE_MOBILE_DUN, mUNM.selectPreferredUpstreamType(preferredTypes));
+        // Check to see we filed an explicit request.
+        assertEquals(1, mCM.requested.size());
+        netReq = (NetworkRequest) mCM.requested.values().toArray()[0];
+        assertTrue(netReq.networkCapabilities.hasTransport(TRANSPORT_CELLULAR));
+        assertTrue(netReq.networkCapabilities.hasCapability(NET_CAPABILITY_DUN));
+    }
+
     private void assertUpstreamTypeRequested(int upstreamType) throws Exception {
         assertEquals(1, mCM.requested.size());
         assertEquals(1, mCM.legacyTypeMap.size());
@@ -254,6 +326,8 @@
         public Map<NetworkCallback, NetworkRequest> requested = new HashMap<>();
         public Map<NetworkCallback, Integer> legacyTypeMap = new HashMap<>();
 
+        private int mNetworkId = 100;
+
         public TestConnectivityManager(Context ctx, IConnectivityManager svc) {
             super(ctx, svc);
         }
@@ -287,6 +361,8 @@
             return false;
         }
 
+        int getNetworkId() { return ++mNetworkId; }
+
         @Override
         public void requestNetwork(NetworkRequest req, NetworkCallback cb, Handler h) {
             assertFalse(allCallbacks.containsKey(cb));
@@ -360,6 +436,35 @@
         }
     }
 
+    public static class TestNetworkAgent {
+        public final TestConnectivityManager cm;
+        public final Network networkId;
+        public final int transportType;
+        public final NetworkCapabilities networkCapabilities;
+
+        public TestNetworkAgent(TestConnectivityManager cm, int transportType) {
+            this.cm = cm;
+            this.networkId = new Network(cm.getNetworkId());
+            this.transportType = transportType;
+            networkCapabilities = new NetworkCapabilities();
+            networkCapabilities.addTransportType(transportType);
+            networkCapabilities.addCapability(NET_CAPABILITY_INTERNET);
+        }
+
+        public void fakeConnect() {
+            for (NetworkCallback cb : cm.listening.keySet()) {
+                cb.onAvailable(networkId);
+                cb.onCapabilitiesChanged(networkId, copy(networkCapabilities));
+            }
+        }
+
+        public void fakeDisconnect() {
+            for (NetworkCallback cb : cm.listening.keySet()) {
+                cb.onLost(networkId);
+            }
+        }
+    }
+
     public static class TestStateMachine extends StateMachine {
         public final ArrayList<Message> messages = new ArrayList<>();
         private final State mLoggingState = new LoggingState();
@@ -382,4 +487,8 @@
             super.start();
         }
     }
+
+    static NetworkCapabilities copy(NetworkCapabilities nc) {
+        return new NetworkCapabilities(nc);
+    }
 }