Merge "Instrument captive portal login activity"
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 6faded53..204ef80 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -116,7 +116,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;
@@ -1500,7 +1499,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: {
@@ -1570,7 +1568,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:
@@ -1596,7 +1593,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:
@@ -1737,32 +1733,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 {
@@ -6123,7 +6093,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/android/bluetooth/BluetoothDevice.java b/core/java/android/bluetooth/BluetoothDevice.java
index f158f5f..6bc88b0 100644
--- a/core/java/android/bluetooth/BluetoothDevice.java
+++ b/core/java/android/bluetooth/BluetoothDevice.java
@@ -1669,7 +1669,7 @@
      */
     public BluetoothGatt connectGatt(Context context, boolean autoConnect,
                                      BluetoothGattCallback callback, int transport) {
-        return (connectGatt(context, autoConnect,callback, TRANSPORT_AUTO, PHY_LE_1M_MASK));
+        return (connectGatt(context, autoConnect,callback, transport, PHY_LE_1M_MASK));
     }
 
     /**
@@ -1693,7 +1693,7 @@
      */
     public BluetoothGatt connectGatt(Context context, boolean autoConnect,
                                      BluetoothGattCallback callback, int transport, int phy) {
-        return connectGatt(context, autoConnect,callback, TRANSPORT_AUTO, PHY_LE_1M_MASK, null);
+        return connectGatt(context, autoConnect,callback, transport, phy, null);
     }
 
     /**
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 1d73c79..b7dff96 100644
--- a/core/java/com/android/internal/os/ZygoteInit.java
+++ b/core/java/com/android/internal/os/ZygoteInit.java
@@ -167,8 +167,8 @@
 
     private static void preloadOpenGL() {
         String driverPackageName = SystemProperties.get(PROPERTY_GFX_DRIVER);
-        if (!SystemProperties.getBoolean(PROPERTY_DISABLE_OPENGL_PRELOADING, false) ||
-                driverPackageName == null || driverPackageName.isEmpty()) {
+        if (!SystemProperties.getBoolean(PROPERTY_DISABLE_OPENGL_PRELOADING, false) &&
+                (driverPackageName == null || driverPackageName.isEmpty())) {
             EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
         }
     }
@@ -648,8 +648,6 @@
         try {
             Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "ZygoteInit");
             RuntimeInit.enableDdms();
-            // Start profiling the zygote initialization.
-            SamplingProfilerIntegration.start();
 
             boolean startSystemServer = false;
             String socketName = "zygote";
@@ -679,9 +677,6 @@
                 SystemClock.uptimeMillis());
             Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
 
-            // Finish profiling the zygote initialization.
-            SamplingProfilerIntegration.writeZygoteSnapshot();
-
             // Do an initial gc to clean up after startup
             Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PostZygoteInitGC");
             gcAndFinalize();
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 07f1b45..81504c0 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -620,6 +620,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];
@@ -746,6 +747,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 75c71aa..71d9fc8 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;
@@ -484,7 +485,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/packages/SystemUI/src/com/android/systemui/qs/QSTile.java b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
index f743a4b..dbe2b2e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
@@ -312,7 +312,9 @@
     protected abstract void setListening(boolean listening);
 
     protected void handleDestroy() {
-        setListening(false);
+        if (mListeners.size() != 0) {
+            setListening(false);
+        }
         mCallbacks.clear();
     }
 
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 fbf1aa4..0000000
--- a/services/core/java/com/android/server/SamplingProfilerService.java
+++ /dev/null
@@ -1,121 +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 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) {
-        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
-
-        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 4858dfa..08df271 100644
--- a/services/core/java/com/android/server/connectivity/Tethering.java
+++ b/services/core/java/com/android/server/connectivity/Tethering.java
@@ -1232,8 +1232,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();
@@ -1245,58 +1245,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;
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 c64e705..cb50e9f 100644
--- a/services/core/java/com/android/server/connectivity/tethering/OffloadController.java
+++ b/services/core/java/com/android/server/connectivity/tethering/OffloadController.java
@@ -20,10 +20,15 @@
 
 import android.content.ContentResolver;
 import android.net.LinkProperties;
+import android.net.RouteInfo;
 import android.net.util.SharedLog;
 import android.os.Handler;
 import android.provider.Settings;
 
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+
 /**
  * A class to encapsulate the business logic of programming the tethering
  * hardware offload interface.
@@ -92,8 +97,12 @@
     public void setUpstreamLinkProperties(LinkProperties lp) {
         if (!started()) return;
 
-        // TODO: setUpstreamParameters().
-        mUpstreamLinkProperties = lp;
+        mUpstreamLinkProperties = (lp != null) ? new LinkProperties(lp) : null;
+        // TODO: examine return code and decide what to do if programming
+        // upstream parameters fails (probably just wait for a subsequent
+        // onOffloadEvent() callback to tell us offload is available again and
+        // then reapply all state).
+        pushUpstreamParameters();
     }
 
     // TODO: public void addDownStream(...)
@@ -106,4 +115,40 @@
     private boolean started() {
         return mConfigInitialized && mControlInitialized;
     }
+
+    private boolean pushUpstreamParameters() {
+        if (mUpstreamLinkProperties == null) {
+            return mHwInterface.setUpstreamParameters(null, null, null, null);
+        }
+
+        // A stacked interface cannot be an upstream for hardware offload.
+        // Consequently, we examine only the primary interface name, look at
+        // getAddresses() rather than getAllAddresses(), and check getRoutes()
+        // rather than getAllRoutes().
+        final String iface = mUpstreamLinkProperties.getInterfaceName();
+        final ArrayList<String> v6gateways = new ArrayList<>();
+        String v4addr = null;
+        String v4gateway = null;
+
+        for (InetAddress ip : mUpstreamLinkProperties.getAddresses()) {
+            if (ip instanceof Inet4Address) {
+                v4addr = ip.getHostAddress();
+                break;
+            }
+        }
+
+        // Find the gateway addresses of all default routes of either address family.
+        for (RouteInfo ri : mUpstreamLinkProperties.getRoutes()) {
+            if (!ri.hasGateway()) continue;
+
+            final String gateway = ri.getGateway().getHostAddress();
+            if (ri.isIPv4Default()) {
+                v4gateway = gateway;
+            } else if (ri.isIPv6Default()) {
+                v6gateways.add(gateway);
+            }
+        }
+
+        return mHwInterface.setUpstreamParameters(iface, v4addr, v4gateway, v6gateways);
+    }
 }
diff --git a/services/core/java/com/android/server/connectivity/tethering/OffloadHardwareInterface.java b/services/core/java/com/android/server/connectivity/tethering/OffloadHardwareInterface.java
index 0429ab3..3ecf0d1 100644
--- a/services/core/java/com/android/server/connectivity/tethering/OffloadHardwareInterface.java
+++ b/services/core/java/com/android/server/connectivity/tethering/OffloadHardwareInterface.java
@@ -23,6 +23,8 @@
 import android.os.RemoteException;
 import android.net.util.SharedLog;
 
+import java.util.ArrayList;
+
 
 /**
  * Capture tethering dependencies, for injection.
@@ -103,6 +105,25 @@
         mControlCallback = null;
     }
 
+    public boolean setUpstreamParameters(
+            String iface, String v4addr, String v4gateway, ArrayList<String> v6gws) {
+        final CbResults results = new CbResults();
+        try {
+            mOffloadControl.setUpstreamParameters(
+                    iface, v4addr, v4gateway, v6gws,
+                    (boolean success, String errMsg) -> {
+                        results.success = success;
+                        results.errMsg = errMsg;
+                    });
+        } catch (RemoteException e) {
+            mLog.e("failed to setUpstreamParameters: " + e);
+            return false;
+        }
+
+        if (!results.success) mLog.e("setUpstreamParameters failed: " + results.errMsg);
+        return results.success;
+    }
+
     private static class TetheringOffloadCallback extends ITetheringOffloadCallback.Stub {
         public final Handler handler;
         public final ControlCallback controlCb;
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 e1cbc91..99ad00b 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -51,7 +51,6 @@
 import com.android.internal.R;
 import com.android.internal.app.NightDisplayController;
 import com.android.internal.os.BinderInternal;
-import com.android.internal.os.SamplingProfilerIntegration;
 import com.android.internal.os.ZygoteInit;
 import com.android.internal.policy.EmergencyAffordanceManager;
 import com.android.internal.widget.ILockSettings;
@@ -281,18 +280,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();
 
@@ -568,8 +555,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",
@@ -1088,21 +1073,6 @@
             }
             Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
 
-            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);
-                }
-                Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
-            }
-
             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/test-runner/Android.mk b/test-runner/Android.mk
index 3c36e42..0752661 100644
--- a/test-runner/Android.mk
+++ b/test-runner/Android.mk
@@ -16,6 +16,8 @@
 
 LOCAL_PATH:= $(call my-dir)
 
+# Build the android.test.runner library
+# =====================================
 include $(CLEAR_VARS)
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
@@ -26,5 +28,56 @@
 
 include $(BUILD_JAVA_LIBRARY)
 
+# Build the android.test.mock library
+# ===================================
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src/android/test/mock)
+
+LOCAL_JAVA_LIBRARIES := core-oj core-libart framework
+
+LOCAL_MODULE:= android.test.mock
+
+include $(BUILD_JAVA_LIBRARY)
+
+# Generate the stub source files for android.test.mock.sdk
+# ========================================================
+include $(CLEAR_VARS)
+LOCAL_SRC_FILES := $(call all-java-files-under, src/android/test/mock)
+
+LOCAL_JAVA_LIBRARIES := core-oj core-libart framework
+LOCAL_MODULE_CLASS := JAVA_LIBRARIES
+LOCAL_DROIDDOC_SOURCE_PATH := $(LOCAL_PATH)/src/android/test/mock
+
+LOCAL_DROIDDOC_OPTIONS:= \
+    -stubpackages android.test.mock \
+    -stubs $(TARGET_OUT_COMMON_INTERMEDIATES)/JAVA_LIBRARIES/android.test.mock.sdk_intermediates/src \
+    -nodocs
+
+LOCAL_UNINSTALLABLE_MODULE := true
+LOCAL_MODULE := android-test-mock-stubs-gen
+
+include $(BUILD_DROIDDOC)
+
+# Remember the target that will trigger the code generation.
+android_test_mock_gen_stamp := $(full_target)
+
+# Build the android.test.mock.sdk library
+# =======================================
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := android.test.mock.sdk
+
+LOCAL_SOURCE_FILES_ALL_GENERATED := true
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# Make sure to run droiddoc first to generate the stub source files.
+$(full_classes_compiled_jar) : $(android_test_mock_gen_stamp)
+$(full_classes_jack) : $(android_test_mock_gen_stamp)
+
+# Archive a copy of the classes.jar in SDK build.
+$(call dist-for-goals,sdk win_sdk,$(full_classes_jar):android.test.mock.jar)
+
 # additionally, build unit tests in a separate .apk
 include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tests/net/java/com/android/server/connectivity/TetheringTest.java b/tests/net/java/com/android/server/connectivity/TetheringTest.java
index b6922d4..be0924a 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
@@ -363,7 +368,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 fb7971e..4d340d1 100644
--- a/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java
+++ b/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java
@@ -17,15 +17,23 @@
 package com.android.server.connectivity.tethering;
 
 import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyObject;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 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;
 import android.net.util.SharedLog;
 import android.provider.Settings;
 import android.provider.Settings.SettingNotFoundException;
@@ -35,9 +43,13 @@
 import android.test.mock.MockContentResolver;
 import com.android.internal.util.test.FakeSettingsProvider;
 
+import java.net.InetAddress;
+import java.util.ArrayList;
+
 import org.junit.Before;
 import org.junit.runner.RunWith;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -48,14 +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);
     }
 
@@ -114,4 +129,87 @@
         inOrder.verify(mHardware, never()).initOffloadControl(anyObject());
         inOrder.verifyNoMoreInteractions();
     }
+
+    @Test
+    public void testSetUpstreamLinkPropertiesWorking() throws Exception {
+        setupFunctioningHardwareInterface();
+        final OffloadController offload =
+                new OffloadController(null, mHardware, mContentResolver, new SharedLog("test"));
+        offload.start();
+
+        final InOrder inOrder = inOrder(mHardware);
+        inOrder.verify(mHardware, times(1)).initOffloadConfig();
+        inOrder.verify(mHardware, times(1)).initOffloadControl(
+                any(OffloadHardwareInterface.ControlCallback.class));
+        inOrder.verifyNoMoreInteractions();
+
+        offload.setUpstreamLinkProperties(null);
+        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+                eq(null), eq(null), eq(null), eq(null));
+        inOrder.verifyNoMoreInteractions();
+        reset(mHardware);
+
+        final LinkProperties lp = new LinkProperties();
+
+        final String testIfName = "rmnet_data17";
+        lp.setInterfaceName(testIfName);
+        offload.setUpstreamLinkProperties(lp);
+        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+                eq(testIfName), eq(null), eq(null), mStringArrayCaptor.capture());
+        assertTrue(mStringArrayCaptor.getValue().isEmpty());
+        inOrder.verifyNoMoreInteractions();
+
+        final String ipv4Addr = "192.0.2.5";
+        final String linkAddr = ipv4Addr + "/24";
+        lp.addLinkAddress(new LinkAddress(linkAddr));
+        offload.setUpstreamLinkProperties(lp);
+        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+                eq(testIfName), eq(ipv4Addr), eq(null), mStringArrayCaptor.capture());
+        assertTrue(mStringArrayCaptor.getValue().isEmpty());
+        inOrder.verifyNoMoreInteractions();
+
+        final String ipv4Gateway = "192.0.2.1";
+        lp.addRoute(new RouteInfo(InetAddress.getByName(ipv4Gateway)));
+        offload.setUpstreamLinkProperties(lp);
+        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+                eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture());
+        assertTrue(mStringArrayCaptor.getValue().isEmpty());
+        inOrder.verifyNoMoreInteractions();
+
+        final String ipv6Gw1 = "fe80::cafe";
+        lp.addRoute(new RouteInfo(InetAddress.getByName(ipv6Gw1)));
+        offload.setUpstreamLinkProperties(lp);
+        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+                eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture());
+        ArrayList<String> v6gws = mStringArrayCaptor.getValue();
+        assertEquals(1, v6gws.size());
+        assertTrue(v6gws.contains(ipv6Gw1));
+        inOrder.verifyNoMoreInteractions();
+
+        final String ipv6Gw2 = "fe80::d00d";
+        lp.addRoute(new RouteInfo(InetAddress.getByName(ipv6Gw2)));
+        offload.setUpstreamLinkProperties(lp);
+        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+                eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture());
+        v6gws = mStringArrayCaptor.getValue();
+        assertEquals(2, v6gws.size());
+        assertTrue(v6gws.contains(ipv6Gw1));
+        assertTrue(v6gws.contains(ipv6Gw2));
+        inOrder.verifyNoMoreInteractions();
+
+        final LinkProperties stacked = new LinkProperties();
+        stacked.setInterfaceName("stacked");
+        stacked.addLinkAddress(new LinkAddress("192.0.2.129/25"));
+        stacked.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254")));
+        stacked.addRoute(new RouteInfo(InetAddress.getByName("fe80::bad:f00")));
+        assertTrue(lp.addStackedLink(stacked));
+        offload.setUpstreamLinkProperties(lp);
+        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+                eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture());
+        v6gws = mStringArrayCaptor.getValue();
+        assertEquals(2, v6gws.size());
+        assertTrue(v6gws.contains(ipv6Gw1));
+        assertTrue(v6gws.contains(ipv6Gw2));
+        inOrder.verifyNoMoreInteractions();
+    }
 }
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);
+    }
 }