API to select backup transport

This cl adds an API to select a backup
transport by its component name and
receive a callback when BackupManager
is bound to the transport. Calling this API
will make BackupManager bind to the transport
if it isn't already bound to it.

Also fixes the issue where BackupManager would
detect only one transport per package.

Ref: go/backup-transport-switching

Bug: 33616220
Test: Manually tested. GTS tests will be put up shortly.

Change-Id: I8c23bdbb84ceb05eb1fad9b3a8b9c4441cb06c74
diff --git a/Android.mk b/Android.mk
index 21bd76b..2ccdc6de 100644
--- a/Android.mk
+++ b/Android.mk
@@ -107,6 +107,7 @@
 	core/java/android/app/backup/IFullBackupRestoreObserver.aidl \
 	core/java/android/app/backup/IRestoreObserver.aidl \
 	core/java/android/app/backup/IRestoreSession.aidl \
+	core/java/android/app/backup/ISelectBackupTransportCallback.aidl \
 	core/java/android/app/usage/IStorageStatsManager.aidl \
 	core/java/android/app/usage/IUsageStatsManager.aidl \
 	core/java/android/bluetooth/IBluetooth.aidl \
diff --git a/api/system-current.txt b/api/system-current.txt
index 37fca36..144bce8 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -6768,15 +6768,18 @@
     method public int requestBackup(java.lang.String[], android.app.backup.BackupObserver);
     method public int requestBackup(java.lang.String[], android.app.backup.BackupObserver, int);
     method public int requestRestore(android.app.backup.RestoreObserver);
-    method public java.lang.String selectBackupTransport(java.lang.String);
+    method public deprecated java.lang.String selectBackupTransport(java.lang.String);
+    method public void selectBackupTransport(android.content.ComponentName, android.app.backup.SelectBackupTransportCallback);
     method public void setAutoRestore(boolean);
     method public void setBackupEnabled(boolean);
     field public static final int ERROR_AGENT_FAILURE = -1003; // 0xfffffc15
     field public static final int ERROR_BACKUP_NOT_ALLOWED = -2001; // 0xfffff82f
     field public static final int ERROR_PACKAGE_NOT_FOUND = -2002; // 0xfffff82e
     field public static final int ERROR_TRANSPORT_ABORTED = -1000; // 0xfffffc18
+    field public static final int ERROR_TRANSPORT_INVALID = -2; // 0xfffffffe
     field public static final int ERROR_TRANSPORT_PACKAGE_REJECTED = -1002; // 0xfffffc16
     field public static final int ERROR_TRANSPORT_QUOTA_EXCEEDED = -1005; // 0xfffffc13
+    field public static final int ERROR_TRANSPORT_UNAVAILABLE = -1; // 0xffffffff
     field public static final int FLAG_NON_INCREMENTAL_BACKUP = 1; // 0x1
     field public static final java.lang.String PACKAGE_MANAGER_SENTINEL = "@pm@";
     field public static final int SUCCESS = 0; // 0x0
@@ -6891,6 +6894,12 @@
     field public long token;
   }
 
+  public abstract class SelectBackupTransportCallback {
+    ctor public SelectBackupTransportCallback();
+    method public void onFailure(int);
+    method public void onSuccess(java.lang.String);
+  }
+
   public class SharedPreferencesBackupHelper extends android.app.backup.FileBackupHelperBase implements android.app.backup.BackupHelper {
     ctor public SharedPreferencesBackupHelper(android.content.Context, java.lang.String...);
     method public void performBackup(android.os.ParcelFileDescriptor, android.app.backup.BackupDataOutput, android.os.ParcelFileDescriptor);
diff --git a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
index 780db5e..7e91391 100644
--- a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
+++ b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
@@ -18,20 +18,24 @@
 
 import android.app.backup.BackupManager;
 import android.app.backup.BackupProgress;
-import android.app.backup.RestoreSet;
 import android.app.backup.IBackupManager;
 import android.app.backup.IBackupObserver;
 import android.app.backup.IRestoreObserver;
 import android.app.backup.IRestoreSession;
+import android.app.backup.RestoreSet;
+import android.app.backup.ISelectBackupTransportCallback;
+import android.content.ComponentName;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageInfo;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.UserHandle;
+import android.util.Log;
 
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
 
 public final class Bmgr {
     IBackupManager mBmgr;
@@ -363,6 +367,11 @@
                 return;
             }
 
+            if ("-c".equals(which)) {
+                doTransportByComponent();
+                return;
+            }
+
             String old = mBmgr.selectBackupTransport(which);
             if (old == null) {
                 System.out.println("Unknown transport '" + which
@@ -370,12 +379,50 @@
             } else {
                 System.out.println("Selected transport " + which + " (formerly " + old + ")");
             }
+
         } catch (RemoteException e) {
             System.err.println(e.toString());
             System.err.println(BMGR_NOT_RUNNING_ERR);
         }
     }
 
+    private void doTransportByComponent() {
+        String which = nextArg();
+        if (which == null) {
+            showUsage();
+            return;
+        }
+
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        try {
+            mBmgr.selectBackupTransportAsync(ComponentName.unflattenFromString(which),
+                    new ISelectBackupTransportCallback.Stub() {
+                        @Override
+                        public void onSuccess(String transportName) {
+                            System.out.println("Success. Selected transport: " + transportName);
+                            latch.countDown();
+                        }
+
+                        @Override
+                        public void onFailure(int reason) {
+                            System.err.println("Failure. error=" + reason);
+                            latch.countDown();
+                        }
+                    });
+        } catch (RemoteException e) {
+            System.err.println(e.toString());
+            System.err.println(BMGR_NOT_RUNNING_ERR);
+            return;
+        }
+
+        try {
+            latch.await();
+        } catch (InterruptedException e) {
+            System.err.println("Operation interrupted.");
+        }
+    }
+
     private void doWipe() {
         String transport = nextArg();
         if (transport == null) {
@@ -427,7 +474,16 @@
     }
 
     private void doListTransports() {
+        String arg = nextArg();
+
         try {
+            if ("-c".equals(arg)) {
+                for (ComponentName transport : mBmgr.listAllTransportComponents()) {
+                    System.out.println(transport.flattenToShortString());
+                }
+                return;
+            }
+
             String current = mBmgr.getCurrentTransport();
             String[] transports = mBmgr.listAllTransports();
             if (transports == null || transports.length == 0) {
@@ -649,9 +705,9 @@
         System.err.println("       bmgr backup PACKAGE");
         System.err.println("       bmgr enable BOOL");
         System.err.println("       bmgr enabled");
-        System.err.println("       bmgr list transports");
+        System.err.println("       bmgr list transports [-c]");
         System.err.println("       bmgr list sets");
-        System.err.println("       bmgr transport WHICH");
+        System.err.println("       bmgr transport WHICH|-c WHICH_COMPONENT");
         System.err.println("       bmgr restore TOKEN");
         System.err.println("       bmgr restore TOKEN PACKAGE...");
         System.err.println("       bmgr restore PACKAGE");
@@ -673,15 +729,18 @@
         System.err.println("the backup mechanism.");
         System.err.println("");
         System.err.println("The 'list transports' command reports the names of the backup transports");
-        System.err.println("currently available on the device.  These names can be passed as arguments");
+        System.err.println("BackupManager is currently bound to. These names can be passed as arguments");
         System.err.println("to the 'transport' and 'wipe' commands.  The currently active transport");
-        System.err.println("is indicated with a '*' character.");
+        System.err.println("is indicated with a '*' character. If -c flag is used, all available");
+        System.err.println("transport components on the device are listed. These can be used with");
+        System.err.println("the component variant of 'transport' command.");
         System.err.println("");
         System.err.println("The 'list sets' command reports the token and name of each restore set");
         System.err.println("available to the device via the currently active transport.");
         System.err.println("");
         System.err.println("The 'transport' command designates the named transport as the currently");
-        System.err.println("active one.  This setting is persistent across reboots.");
+        System.err.println("active one.  This setting is persistent across reboots. If -c flag is");
+        System.err.println("specified, the following string is treated as a component name.");
         System.err.println("");
         System.err.println("The 'restore' command when given just a restore token initiates a full-system");
         System.err.println("restore operation from the currently active transport.  It will deliver");
diff --git a/core/java/android/app/backup/BackupManager.java b/core/java/android/app/backup/BackupManager.java
index 540683d..f0abe33 100644
--- a/core/java/android/app/backup/BackupManager.java
+++ b/core/java/android/app/backup/BackupManager.java
@@ -17,6 +17,7 @@
 package android.app.backup;
 
 import android.annotation.SystemApi;
+import android.content.ComponentName;
 import android.content.Context;
 import android.os.Handler;
 import android.os.Message;
@@ -157,6 +158,25 @@
     @SystemApi
     public static final String PACKAGE_MANAGER_SENTINEL = "@pm@";
 
+
+    /**
+     * This error code is passed to {@link SelectBackupTransportCallback#onFailure(int)}
+     * if the requested transport is unavailable.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int ERROR_TRANSPORT_UNAVAILABLE = -1;
+
+    /**
+     * This error code is passed to {@link SelectBackupTransportCallback#onFailure(int)} if the
+     * requested transport is not a valid BackupTransport.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int ERROR_TRANSPORT_INVALID = -2;
+
     private Context mContext;
     private static IBackupManager sService;
 
@@ -390,17 +410,20 @@
     }
 
     /**
-     * Specify the current backup transport.  Callers must hold the
-     * android.permission.BACKUP permission to use this method.
+     * Specify the current backup transport.
+     *
+     * <p> Callers must hold the android.permission.BACKUP permission to use this method.
      *
      * @param transport The name of the transport to select.  This should be one
-     *   of the names returned by {@link #listAllTransports()}.
+     *   of the names returned by {@link #listAllTransports()}. This is the String returned by
+     *   {@link BackupTransport#name()} for the particular transport.
      * @return The name of the previously selected transport.  If the given transport
      *   name is not one of the currently available transports, no change is made to
      *   the current transport setting and the method returns null.
      *
      * @hide
      */
+    @Deprecated
     @SystemApi
     public String selectBackupTransport(String transport) {
         checkServiceBinder();
@@ -415,6 +438,34 @@
     }
 
     /**
+     * Specify the current backup transport and get notified when the transport is ready to be used.
+     * This method is async because BackupManager might need to bind to the specified transport
+     * which is in a separate process.
+     *
+     * <p>Callers must hold the android.permission.BACKUP permission to use this method.
+     *
+     * @param transport ComponentName of the service hosting the transport. This is different from
+     *                  the transport's name that is returned by {@link BackupTransport#name()}.
+     * @param listener A listener object to get a callback on the transport being selected.
+     *
+     * @hide
+     */
+    @SystemApi
+    public void selectBackupTransport(ComponentName transport,
+            SelectBackupTransportCallback listener) {
+        checkServiceBinder();
+        if (sService != null) {
+            try {
+                SelectTransportListenerWrapper wrapper = listener == null ?
+                        null : new SelectTransportListenerWrapper(mContext, listener);
+                sService.selectBackupTransportAsync(transport, wrapper);
+            } catch (RemoteException e) {
+                Log.e(TAG, "selectBackupTransportAsync() couldn't connect");
+            }
+        }
+    }
+
+    /**
      * Schedule an immediate backup attempt for all pending key/value updates.  This
      * is primarily intended for transports to use when they detect a suitable
      * opportunity for doing a backup pass.  If there are no pending updates to
@@ -598,4 +649,35 @@
                 mHandler.obtainMessage(MSG_FINISHED, status, 0));
         }
     }
+
+    private class SelectTransportListenerWrapper extends ISelectBackupTransportCallback.Stub {
+
+        private final Handler mHandler;
+        private final SelectBackupTransportCallback mListener;
+
+        SelectTransportListenerWrapper(Context context, SelectBackupTransportCallback listener) {
+            mHandler = new Handler(context.getMainLooper());
+            mListener = listener;
+        }
+
+        @Override
+        public void onSuccess(final String transportName) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mListener.onSuccess(transportName);
+                }
+            });
+        }
+
+        @Override
+        public void onFailure(final int reason) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mListener.onFailure(reason);
+                }
+            });
+        }
+    }
 }
diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl
index fe23c28..1657e2e 100644
--- a/core/java/android/app/backup/IBackupManager.aidl
+++ b/core/java/android/app/backup/IBackupManager.aidl
@@ -19,8 +19,10 @@
 import android.app.backup.IBackupObserver;
 import android.app.backup.IFullBackupRestoreObserver;
 import android.app.backup.IRestoreSession;
+import android.app.backup.ISelectBackupTransportCallback;
 import android.os.ParcelFileDescriptor;
 import android.content.Intent;
+import android.content.ComponentName;
 
 /**
  * Direct interface to the Backup Manager Service that applications invoke on.  The only
@@ -217,6 +219,8 @@
      */
     String[] listAllTransports();
 
+    ComponentName[] listAllTransportComponents();
+
     /**
      * Retrieve the list of whitelisted transport components.  Callers do </i>not</i> need
      * any special permission.
@@ -238,6 +242,21 @@
     String selectBackupTransport(String transport);
 
     /**
+     * Specify the current backup transport and get notified when the transport is ready to be used.
+     * This method is async because BackupManager might need to bind to the specified transport
+     * which is in a separate process.
+     *
+     * <p>Callers must hold the android.permission.BACKUP permission to use this method.
+     *
+     * @param transport ComponentName of the service hosting the transport. This is different from
+     *                  the transport's name that is returned by {@link BackupTransport#name()}.
+     * @param listener A listener object to get a callback on the transport being selected.
+     *
+     * @hide
+     */
+    void selectBackupTransportAsync(in ComponentName transport, ISelectBackupTransportCallback listener);
+
+    /**
      * Get the configuration Intent, if any, from the given transport.  Callers must
      * hold the android.permission.BACKUP permission in order to use this method.
      *
diff --git a/core/java/android/app/backup/ISelectBackupTransportCallback.aidl b/core/java/android/app/backup/ISelectBackupTransportCallback.aidl
new file mode 100644
index 0000000..5de7c5e
--- /dev/null
+++ b/core/java/android/app/backup/ISelectBackupTransportCallback.aidl
@@ -0,0 +1,41 @@
+/*
+ * 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.app.backup;
+
+/**
+ * Callback class for receiving success or failure callbacks on selecting a backup transport. These
+ * methods will all be called on your application's main thread.
+ *
+ * @hide
+ */
+oneway interface ISelectBackupTransportCallback {
+
+    /**
+     * Called when BackupManager has successfully bound to the requested transport.
+     *
+     * @param transportName Name of the selected transport. This is the String returned by
+     *        {@link BackupTransport#name()}.
+     */
+    void onSuccess(String transportName);
+
+    /**
+     * Called when BackupManager fails to bind to the requested transport.
+     *
+     * @param reason Error code denoting reason for failure.
+     */
+    void onFailure(int reason);
+}
diff --git a/core/java/android/app/backup/SelectBackupTransportCallback.java b/core/java/android/app/backup/SelectBackupTransportCallback.java
new file mode 100644
index 0000000..0c8a0dc
--- /dev/null
+++ b/core/java/android/app/backup/SelectBackupTransportCallback.java
@@ -0,0 +1,44 @@
+/*
+ * 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.app.backup;
+
+import android.annotation.SystemApi;
+
+/**
+ * Callback class for receiving success or failure callbacks on selecting a backup transport. These
+ * methods will all be called on your application's main thread.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class SelectBackupTransportCallback {
+
+    /**
+     * Called when BackupManager has successfully bound to the requested transport.
+     *
+     * @param transportName Name of the selected transport. This is the String returned by
+     *        {@link BackupTransport#name()}.
+     */
+    public void onSuccess(String transportName){}
+
+    /**
+     * Called when BackupManager fails to bind to the requested transport.
+     *
+     * @param reason Error code denoting reason for failure.
+     */
+    public void onFailure(int reason){}
+}
diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java
index 7e82586..88c05b5 100644
--- a/services/backup/java/com/android/server/backup/BackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/BackupManagerService.java
@@ -34,13 +34,15 @@
 import android.app.backup.BackupTransport;
 import android.app.backup.FullBackup;
 import android.app.backup.FullBackupDataOutput;
-import android.app.backup.IBackupObserver;
-import android.app.backup.RestoreDescription;
-import android.app.backup.RestoreSet;
 import android.app.backup.IBackupManager;
+import android.app.backup.IBackupObserver;
 import android.app.backup.IFullBackupRestoreObserver;
 import android.app.backup.IRestoreObserver;
 import android.app.backup.IRestoreSession;
+import android.app.backup.ISelectBackupTransportCallback;
+import android.app.backup.RestoreDescription;
+import android.app.backup.RestoreSet;
+import android.app.backup.SelectBackupTransportCallback;
 import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -55,16 +57,15 @@
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
-import android.content.pm.Signature;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.Signature;
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
+import android.os.Environment.UserEnvironment;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
@@ -79,15 +80,12 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.WorkSource;
-import android.os.Environment.UserEnvironment;
 import android.os.storage.IStorageManager;
 import android.os.storage.StorageManager;
 import android.provider.Settings;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.text.TextUtils;
-import android.util.ArrayMap;
-import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.EventLog;
 import android.util.Log;
@@ -105,6 +103,8 @@
 import com.android.server.SystemService;
 import com.android.server.backup.PackageManagerBackupAgent.Metadata;
 
+import libcore.io.IoUtils;
+
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
@@ -139,7 +139,6 @@
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Random;
@@ -166,8 +165,6 @@
 import javax.crypto.spec.PBEKeySpec;
 import javax.crypto.spec.SecretKeySpec;
 
-import libcore.io.IoUtils;
-
 public class BackupManagerService {
 
     private static final String TAG = "BackupManagerService";
@@ -271,6 +268,8 @@
     private IStorageManager mStorageManager;
     IBackupManager mBackupManagerBinder;
 
+    private final TransportManager mTransportManager;
+
     boolean mEnabled;   // access to this is synchronized on 'this'
     boolean mProvisioned;
     boolean mAutoRestore;
@@ -322,16 +321,6 @@
     final Object mClearDataLock = new Object();
     volatile boolean mClearingData;
 
-    // Transport bookkeeping
-    final ArraySet<ComponentName> mTransportWhitelist;
-    final Intent mTransportServiceIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST);
-    final ArrayMap<String,String> mTransportNames
-            = new ArrayMap<String,String>();             // component name -> registration name
-    final ArrayMap<String,IBackupTransport> mTransports
-            = new ArrayMap<String,IBackupTransport>();   // registration name -> binder
-    final ArrayMap<String,TransportConnection> mTransportConnections
-            = new ArrayMap<String,TransportConnection>();
-    String mCurrentTransport;
     ActiveRestoreSession mActiveRestoreSession;
 
     // Watch the device provisioning operation during setup
@@ -756,7 +745,7 @@
             {
                 mLastBackupPass = System.currentTimeMillis();
 
-                IBackupTransport transport = getTransport(mCurrentTransport);
+                IBackupTransport transport = mTransportManager.getCurrentTransportBinder();
                 if (transport == null) {
                     Slog.v(TAG, "Backup requested but no transport available");
                     synchronized (mQueueLock) {
@@ -1202,32 +1191,19 @@
         // Set up our transport options and initialize the default transport
         // TODO: Don't create transports that we don't need to?
         SystemConfig systemConfig = SystemConfig.getInstance();
-        mTransportWhitelist = systemConfig.getBackupTransportWhitelist();
+        Set<ComponentName> transportWhitelist = systemConfig.getBackupTransportWhitelist();
 
         String transport = Settings.Secure.getString(context.getContentResolver(),
                 Settings.Secure.BACKUP_TRANSPORT);
         if (TextUtils.isEmpty(transport)) {
             transport = null;
         }
-        mCurrentTransport = transport;
-        if (DEBUG) Slog.v(TAG, "Starting with transport " + mCurrentTransport);
+        String currentTransport = transport;
+        if (DEBUG) Slog.v(TAG, "Starting with transport " + currentTransport);
 
-        // Find all transport hosts and bind to their services
-        // TODO: http://b/22388012
-        List<ResolveInfo> hosts = mPackageManager.queryIntentServicesAsUser(
-                mTransportServiceIntent, 0, UserHandle.USER_SYSTEM);
-        if (DEBUG) {
-            Slog.v(TAG, "Found transports: " + ((hosts == null) ? "null" : hosts.size()));
-        }
-        if (hosts != null) {
-            for (int i = 0; i < hosts.size(); i++) {
-                final ServiceInfo transportService = hosts.get(i).serviceInfo;
-                if (MORE_DEBUG) {
-                    Slog.v(TAG, "   " + transportService.packageName + "/" + transportService.name);
-                }
-                tryBindTransport(transportService);
-            }
-        }
+        mTransportManager = new TransportManager(context, transportWhitelist, currentTransport,
+                mTransportBoundListener);
+        mTransportManager.registerAllTransports();
 
         // Now that we know about valid backup participants, parse any
         // leftover journal files into the pending backup set
@@ -1751,7 +1727,7 @@
         mBackupHandler.removeMessages(MSG_RETRY_INIT);
 
         try {
-            IBackupTransport transport = getTransport(transportName);
+            IBackupTransport transport = mTransportManager.getTransportBinder(transportName);
             if (transport != null) {
                 String transportDirName = transport.transportDirName();
                 File stateDir = new File(mBaseStateDir, transportDirName);
@@ -1829,49 +1805,39 @@
         }
     }
 
-    // Add a transport to our set of available backends.  If 'transport' is null, this
-    // is an unregistration, and the transport's entry is removed from our bookkeeping.
-    private void registerTransport(String name, String component, IBackupTransport transport) {
-        synchronized (mTransports) {
-            if (DEBUG) Slog.v(TAG, "Registering transport "
-                    + component + "::" + name + " = " + transport);
-            if (transport != null) {
-                mTransports.put(name, transport);
-                mTransportNames.put(component, name);
-            } else {
-                mTransports.remove(mTransportNames.get(component));
-                mTransportNames.remove(component);
-                // Nothing further to do in the unregistration case
-                return;
-            }
-        }
+    private TransportManager.TransportBoundListener mTransportBoundListener =
+            new TransportManager.TransportBoundListener() {
+        @Override
+        public boolean onTransportBound(IBackupTransport transport) {
+            // If the init sentinel file exists, we need to be sure to perform the init
+            // as soon as practical.  We also create the state directory at registration
+            // time to ensure it's present from the outset.
+            String name = null;
+            try {
+                name = transport.name();
+                String transportDirName = transport.transportDirName();
+                File stateDir = new File(mBaseStateDir, transportDirName);
+                stateDir.mkdirs();
 
-        // If the init sentinel file exists, we need to be sure to perform the init
-        // as soon as practical.  We also create the state directory at registration
-        // time to ensure it's present from the outset.
-        try {
-            String transportName = transport.transportDirName();
-            File stateDir = new File(mBaseStateDir, transportName);
-            stateDir.mkdirs();
+                File initSentinel = new File(stateDir, INIT_SENTINEL_FILE_NAME);
+                if (initSentinel.exists()) {
+                    synchronized (mQueueLock) {
+                        mPendingInits.add(name);
 
-            File initSentinel = new File(stateDir, INIT_SENTINEL_FILE_NAME);
-            if (initSentinel.exists()) {
-                synchronized (mQueueLock) {
-                    mPendingInits.add(name);
-
-                    // TODO: pick a better starting time than now + 1 minute
-                    long delay = 1000 * 60; // one minute, in milliseconds
-                    mAlarmManager.set(AlarmManager.RTC_WAKEUP,
-                            System.currentTimeMillis() + delay, mRunInitIntent);
+                        // TODO: pick a better starting time than now + 1 minute
+                        long delay = 1000 * 60; // one minute, in milliseconds
+                        mAlarmManager.set(AlarmManager.RTC_WAKEUP,
+                                System.currentTimeMillis() + delay, mRunInitIntent);
+                    }
                 }
+                return true;
+            } catch (Exception e) {
+                // the transport threw when asked its file naming prefs; declare it invalid
+                Slog.w(TAG, "Failed to regiser transport: " + name);
+                return false;
             }
-        } catch (Exception e) {
-            // the transport threw when asked its file naming prefs; declare it invalid
-            Slog.e(TAG, "Unable to register transport as " + name);
-            mTransportNames.remove(component);
-            mTransports.remove(name);
         }
-    }
+    };
 
     // ----- Track installation/removal of packages -----
     BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@@ -1899,75 +1865,17 @@
 
                 // At package-changed we only care about looking at new transport states
                 if (changed) {
-                    try {
-                        String[] components =
-                                intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST);
+                    String[] components =
+                            intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST);
 
-                        if (MORE_DEBUG) {
-                            Slog.i(TAG, "Package " + pkgName + " changed; rechecking");
-                            for (int i = 0; i < components.length; i++) {
-                                Slog.i(TAG, "   * " + components[i]);
-                            }
-                        }
-
-                        // In general we need to try to bind any time we see a component enable
-                        // state change, because that change may have made a transport available.
-                        // However, because we currently only support a single transport component
-                        // per package, we can skip the bind attempt if the change (a) affects a
-                        // package known to host a transport, but (b) does not affect the known
-                        // transport component itself.
-                        //
-                        // In addition, if the change *is* to a known transport component, we need
-                        // to unbind it before retrying the binding.
-                        boolean tryBind = true;
-                        synchronized (mTransports) {
-                            TransportConnection conn = mTransportConnections.get(pkgName);
-                            if (conn != null) {
-                                // We have a bound transport in this package; do we need to rebind it?
-                                final ServiceInfo svc = conn.mTransport;
-                                ComponentName svcName =
-                                        new ComponentName(svc.packageName, svc.name);
-                                if (svc.packageName.equals(pkgName)) {
-                                    final String className = svcName.getClassName();
-                                    if (MORE_DEBUG) {
-                                        Slog.i(TAG, "Checking need to rebind " + className);
-                                    }
-                                    // See whether it's the transport component within this package
-                                    boolean isTransport = false;
-                                    for (int i = 0; i < components.length; i++) {
-                                        if (className.equals(components[i])) {
-                                            // Okay, it's an existing transport component.
-                                            final String flatName = svcName.flattenToShortString();
-                                            mContext.unbindService(conn);
-                                            mTransportConnections.remove(pkgName);
-                                            mTransports.remove(mTransportNames.get(flatName));
-                                            mTransportNames.remove(flatName);
-                                            isTransport = true;
-                                            break;
-                                        }
-                                    }
-                                    if (!isTransport) {
-                                        // A non-transport component within a package that is hosting
-                                        // a bound transport
-                                        tryBind = false;
-                                    }
-                                }
-                            }
-                        }
-                        // and now (re)bind as appropriate
-                        if (tryBind) {
-                            if (MORE_DEBUG) {
-                                Slog.i(TAG, "Yes, need to recheck binding");
-                            }
-                            PackageInfo app = mPackageManager.getPackageInfo(pkgName, 0);
-                            checkForTransportAndBind(app);
-                        }
-                    } catch (NameNotFoundException e) {
-                        // Nope, can't find it - just ignore
-                        if (MORE_DEBUG) {
-                            Slog.w(TAG, "Can't find changed package " + pkgName);
+                    if (MORE_DEBUG) {
+                        Slog.i(TAG, "Package " + pkgName + " changed; rechecking");
+                        for (int i = 0; i < components.length; i++) {
+                            Slog.i(TAG, "   * " + components[i]);
                         }
                     }
+
+                    mTransportManager.onPackageChanged(pkgName, components);
                     return; // nothing more to do in the PACKAGE_CHANGED case
                 }
 
@@ -2015,19 +1923,7 @@
                             writeFullBackupScheduleAsync();
                         }
 
-                        // Transport maintenance: rebind to known existing transports that have
-                        // just been updated; and bind to any newly-installed transport services.
-                        synchronized (mTransports) {
-                            final TransportConnection conn = mTransportConnections.get(packageName);
-                            if (conn != null) {
-                                if (MORE_DEBUG) {
-                                    Slog.i(TAG, "Transport package changed; rebinding");
-                                }
-                                bindTransport(conn.mTransport);
-                            } else {
-                                checkForTransportAndBind(app);
-                            }
-                        }
+                        mTransportManager.onPackageAdded(packageName);
 
                     } catch (NameNotFoundException e) {
                         // doesn't really exist; ignore it
@@ -2051,107 +1947,13 @@
                         removePackageParticipantsLocked(pkgList, uid);
                     }
                 }
+                for (String pkgName : pkgList) {
+                    mTransportManager.onPackageRemoved(pkgName);
+                }
             }
         }
     };
 
-    // ----- Track connection to transports service -----
-    class TransportConnection implements ServiceConnection {
-        ServiceInfo mTransport;
-
-        public TransportConnection(ServiceInfo transport) {
-            mTransport = transport;
-        }
-
-        @Override
-        public void onServiceConnected(ComponentName component, IBinder service) {
-            if (DEBUG) Slog.v(TAG, "Connected to transport " + component);
-            final String name = component.flattenToShortString();
-            try {
-                IBackupTransport transport = IBackupTransport.Stub.asInterface(service);
-                registerTransport(transport.name(), name, transport);
-                EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, name, 1);
-            } catch (Exception e) {
-                Slog.e(TAG, "Unable to register transport " + component
-                        + ": " + e.getMessage());
-                EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, name, 0);
-            }
-        }
-
-        @Override
-        public void onServiceDisconnected(ComponentName component) {
-            if (DEBUG) Slog.v(TAG, "Disconnected from transport " + component);
-            final String name = component.flattenToShortString();
-            EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, name, 0);
-            registerTransport(null, name, null);
-        }
-    };
-
-    // Check whether the given package hosts a transport, and bind if so
-    void checkForTransportAndBind(PackageInfo pkgInfo) {
-        Intent intent = new Intent(mTransportServiceIntent)
-                .setPackage(pkgInfo.packageName);
-        // TODO: http://b/22388012
-        List<ResolveInfo> hosts = mPackageManager.queryIntentServicesAsUser(
-                intent, 0, UserHandle.USER_SYSTEM);
-        if (hosts != null) {
-            final int N = hosts.size();
-            for (int i = 0; i < N; i++) {
-                final ServiceInfo info = hosts.get(i).serviceInfo;
-                tryBindTransport(info);
-            }
-        }
-    }
-
-    // Verify that the service exists and is hosted by a privileged app, then proceed to bind
-    boolean tryBindTransport(ServiceInfo info) {
-        try {
-            PackageInfo packInfo = mPackageManager.getPackageInfo(info.packageName, 0);
-            if ((packInfo.applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED)
-                    != 0) {
-                return bindTransport(info);
-            } else {
-                Slog.w(TAG, "Transport package " + info.packageName + " not privileged");
-            }
-        } catch (NameNotFoundException e) {
-            Slog.w(TAG, "Problem resolving transport package " + info.packageName);
-        }
-        return false;
-    }
-
-    // Actually bind; presumes that we have already validated the transport service
-    boolean bindTransport(ServiceInfo transport) {
-        ComponentName svcName = new ComponentName(transport.packageName, transport.name);
-        if (!mTransportWhitelist.contains(svcName)) {
-            Slog.w(TAG, "Proposed transport " + svcName + " not whitelisted; ignoring");
-            return false;
-        }
-
-        if (MORE_DEBUG) {
-            Slog.i(TAG, "Binding to transport host " + svcName);
-        }
-        Intent intent = new Intent(mTransportServiceIntent);
-        intent.setComponent(svcName);
-
-        TransportConnection connection;
-        synchronized (mTransports) {
-            connection = mTransportConnections.get(transport.packageName);
-            if (null == connection) {
-                connection = new TransportConnection(transport);
-                mTransportConnections.put(transport.packageName, connection);
-            } else {
-                // This is a rebind due to package upgrade.  The service won't be
-                // automatically relaunched for us until we explicitly rebind, but
-                // we need to unbind the now-orphaned original connection.
-                mContext.unbindService(connection);
-            }
-        }
-        // TODO: http://b/22388012
-        return mContext.bindServiceAsUser(intent,
-                connection, Context.BIND_AUTO_CREATE,
-                UserHandle.SYSTEM);
-    }
-
     // Add the backup agents in the given packages to our set of known backup participants.
     // If 'packageNames' is null, adds all backup agents in the whole system.
     void addPackageParticipantsLocked(String[] packageNames) {
@@ -2352,34 +2154,12 @@
         }
     }
 
-    // Return the given transport
-    private IBackupTransport getTransport(String transportName) {
-        synchronized (mTransports) {
-            IBackupTransport transport = mTransports.get(transportName);
-            if (transport == null) {
-                Slog.w(TAG, "Requested unavailable transport: " + transportName);
-            }
-            return transport;
-        }
-    }
-
     // What name is this transport registered under...?
     private String getTransportName(IBackupTransport transport) {
         if (MORE_DEBUG) {
             Slog.v(TAG, "Searching for transport name of " + transport);
         }
-        synchronized (mTransports) {
-            final int N = mTransports.size();
-            for (int i = 0; i < N; i++) {
-                if (mTransports.valueAt(i).equals(transport)) {
-                    if (MORE_DEBUG) {
-                        Slog.v(TAG, "  Name found: " + mTransports.keyAt(i));
-                    }
-                    return mTransports.keyAt(i);
-                }
-            }
-        }
-        return null;
+        return mTransportManager.getTransportName(transport);
     }
 
     // fire off a backup agent, blocking until it attaches or times out
@@ -2505,7 +2285,7 @@
             throw new IllegalArgumentException("No packages are provided for backup");
         }
 
-        IBackupTransport transport = getTransport(mCurrentTransport);
+        IBackupTransport transport = mTransportManager.getCurrentTransportBinder();
         if (transport == null) {
             sendBackupFinished(observer, BackupManager.ERROR_TRANSPORT_ABORTED);
             return BackupManager.ERROR_TRANSPORT_ABORTED;
@@ -3025,7 +2805,7 @@
                     if (MORE_DEBUG) Slog.d(TAG, "Server requires init; rerunning");
                     addBackupTrace("init required; rerunning");
                     try {
-                        final String name = getTransportName(mTransport);
+                        final String name = mTransportManager.getTransportName(mTransport);
                         if (name != null) {
                             mPendingInits.add(name);
                         } else {
@@ -4503,7 +4283,7 @@
                     return;
                 }
 
-                IBackupTransport transport = getTransport(mCurrentTransport);
+                IBackupTransport transport = mTransportManager.getCurrentTransportBinder();
                 if (transport == null) {
                     Slog.w(TAG, "Transport not present; full data backup not performed");
                     backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
@@ -5119,7 +4899,7 @@
 
                 headBusy = false;
 
-                if (!fullBackupAllowable(getTransport(mCurrentTransport))) {
+                if (!fullBackupAllowable(mTransportManager.getCurrentTransportBinder())) {
                     if (MORE_DEBUG) {
                         Slog.i(TAG, "Preconditions not met; not running full backup");
                     }
@@ -9115,7 +8895,8 @@
         public void run() {
             try {
                 for (String transportName : mQueue) {
-                    IBackupTransport transport = getTransport(transportName);
+                    IBackupTransport transport =
+                            mTransportManager.getTransportBinder(transportName);
                     if (transport == null) {
                         Slog.e(TAG, "Requested init for " + transportName + " but not found");
                         continue;
@@ -9312,7 +9093,8 @@
             if (MORE_DEBUG) Slog.v(TAG, "Found the app - running clear process");
             mBackupHandler.removeMessages(MSG_RETRY_CLEAR);
             synchronized (mQueueLock) {
-                final IBackupTransport transport = getTransport(transportName);
+                final IBackupTransport transport =
+                        mTransportManager.getTransportBinder(transportName);
                 if (transport == null) {
                     // transport is currently unavailable -- make sure to retry
                     Message msg = mBackupHandler.obtainMessage(MSG_RETRY_CLEAR,
@@ -9450,7 +9232,7 @@
             throw new IllegalStateException("Restore supported only for the device owner");
         }
 
-        if (!fullBackupAllowable(getTransport(mCurrentTransport))) {
+        if (!fullBackupAllowable(mTransportManager.getCurrentTransportBinder())) {
             Slog.i(TAG, "Full backup not currently possible -- key/value backup not yet run?");
         } else {
             if (DEBUG) {
@@ -9718,10 +9500,7 @@
                     if (wasEnabled && mProvisioned) {
                         // NOTE: we currently flush every registered transport, not just
                         // the currently-active one.
-                        HashSet<String> allTransports;
-                        synchronized (mTransports) {
-                            allTransports = new HashSet<String>(mTransports.keySet());
-                        }
+                        String[] allTransports = mTransportManager.getBoundTransportNames();
                         // build the set of transports for which we are posting an init
                         for (String transport : allTransports) {
                             recordInitPendingLocked(true, transport);
@@ -9774,36 +9553,27 @@
     public String getCurrentTransport() {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
                 "getCurrentTransport");
-        if (MORE_DEBUG) Slog.v(TAG, "... getCurrentTransport() returning " + mCurrentTransport);
-        return mCurrentTransport;
+        String currentTransport = mTransportManager.getCurrentTransportName();
+        if (MORE_DEBUG) Slog.v(TAG, "... getCurrentTransport() returning " + currentTransport);
+        return currentTransport;
     }
 
     // Report all known, available backup transports
     public String[] listAllTransports() {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "listAllTransports");
 
-        String[] list = null;
-        ArrayList<String> known = new ArrayList<String>();
-        for (Map.Entry<String, IBackupTransport> entry : mTransports.entrySet()) {
-            if (entry.getValue() != null) {
-                known.add(entry.getKey());
-            }
-        }
+        return mTransportManager.getBoundTransportNames();
+    }
 
-        if (known.size() > 0) {
-            list = new String[known.size()];
-            known.toArray(list);
-        }
-        return list;
+    public ComponentName[] listAllTransportComponents() {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "listAllTransportComponents");
+        return mTransportManager.getAllTransportCompenents();
     }
 
     public String[] getTransportWhitelist() {
         // No permission check, intentionally.
-        String[] whitelist = new String[mTransportWhitelist.size()];
-        for (int i = mTransportWhitelist.size() - 1; i >= 0; i--) {
-            whitelist[i] = mTransportWhitelist.valueAt(i).flattenToShortString();
-        }
-        return whitelist;
+        return mTransportManager.getTransportWhitelist().toArray(new String[0]);
     }
 
     // Select which transport to use for the next backup operation.
@@ -9811,22 +9581,58 @@
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
                 "selectBackupTransport");
 
-        synchronized (mTransports) {
-            final long oldId = Binder.clearCallingIdentity();
-            try {
-                String prevTransport = mCurrentTransport;
-                mCurrentTransport = transport;
-                Settings.Secure.putString(mContext.getContentResolver(),
-                        Settings.Secure.BACKUP_TRANSPORT, transport);
-                Slog.v(TAG, "selectBackupTransport() set " + mCurrentTransport
-                        + " returning " + prevTransport);
-                return prevTransport;
-            } finally {
-                Binder.restoreCallingIdentity(oldId);
-            }
+        final long oldId = Binder.clearCallingIdentity();
+        try {
+            String prevTransport = mTransportManager.selectTransport(transport);
+            Settings.Secure.putString(mContext.getContentResolver(),
+                    Settings.Secure.BACKUP_TRANSPORT, transport);
+            Slog.v(TAG, "selectBackupTransport() set " + mTransportManager.getCurrentTransportName()
+                    + " returning " + prevTransport);
+            return prevTransport;
+        } finally {
+            Binder.restoreCallingIdentity(oldId);
         }
     }
 
+    public void selectBackupTransportAsync(final ComponentName transport,
+            final ISelectBackupTransportCallback listener) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "selectBackupTransportAsync");
+
+        final long oldId = Binder.clearCallingIdentity();
+
+        Slog.v(TAG, "selectBackupTransportAsync() called with transport " +
+                transport.flattenToShortString());
+
+        mTransportManager.ensureTransportReady(transport, new SelectBackupTransportCallback() {
+            @Override
+            public void onSuccess(String transportName) {
+                mTransportManager.selectTransport(transportName);
+                Settings.Secure.putString(mContext.getContentResolver(),
+                        Settings.Secure.BACKUP_TRANSPORT,
+                        mTransportManager.getCurrentTransportName());
+                Slog.v(TAG, "Transport successfully selected: " + transport.flattenToShortString());
+                try {
+                    listener.onSuccess(transportName);
+                } catch (RemoteException e) {
+                    // Nothing to do here.
+                }
+            }
+
+            @Override
+            public void onFailure(int reason) {
+                Slog.v(TAG, "Failed to select transport: " + transport.flattenToShortString());
+                try {
+                    listener.onFailure(reason);
+                } catch (RemoteException e) {
+                    // Nothing to do here.
+                }
+            }
+        });
+
+        Binder.restoreCallingIdentity(oldId);
+    }
+
     // Supply the configuration Intent for the given transport.  If the name is not one
     // of the available transports, or if the transport does not supply any configuration
     // UI, the method returns null.
@@ -9834,18 +9640,16 @@
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
                 "getConfigurationIntent");
 
-        synchronized (mTransports) {
-            final IBackupTransport transport = mTransports.get(transportName);
-            if (transport != null) {
-                try {
-                    final Intent intent = transport.configurationIntent();
-                    if (MORE_DEBUG) Slog.d(TAG, "getConfigurationIntent() returning config intent "
-                            + intent);
-                    return intent;
-                } catch (Exception e) {
-                    /* fall through to return null */
-                    Slog.e(TAG, "Unable to get configuration intent from transport: " + e.getMessage());
-                }
+        final IBackupTransport transport = mTransportManager.getTransportBinder(transportName);
+        if (transport != null) {
+            try {
+                final Intent intent = transport.configurationIntent();
+                if (MORE_DEBUG) Slog.d(TAG, "getConfigurationIntent() returning config intent "
+                        + intent);
+                return intent;
+            } catch (Exception e) {
+                /* fall through to return null */
+                Slog.e(TAG, "Unable to get configuration intent from transport: " + e.getMessage());
             }
         }
 
@@ -9861,17 +9665,15 @@
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
                 "getDestinationString");
 
-        synchronized (mTransports) {
-            final IBackupTransport transport = mTransports.get(transportName);
-            if (transport != null) {
-                try {
-                    final String text = transport.currentDestinationString();
-                    if (MORE_DEBUG) Slog.d(TAG, "getDestinationString() returning " + text);
-                    return text;
-                } catch (Exception e) {
-                    /* fall through to return null */
-                    Slog.e(TAG, "Unable to get string from transport: " + e.getMessage());
-                }
+        final IBackupTransport transport = mTransportManager.getTransportBinder(transportName);
+        if (transport != null) {
+            try {
+                final String text = transport.currentDestinationString();
+                if (MORE_DEBUG) Slog.d(TAG, "getDestinationString() returning " + text);
+                return text;
+            } catch (Exception e) {
+                /* fall through to return null */
+                Slog.e(TAG, "Unable to get string from transport: " + e.getMessage());
             }
         }
 
@@ -9883,18 +9685,16 @@
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
                 "getDataManagementIntent");
 
-        synchronized (mTransports) {
-            final IBackupTransport transport = mTransports.get(transportName);
-            if (transport != null) {
-                try {
-                    final Intent intent = transport.dataManagementIntent();
-                    if (MORE_DEBUG) Slog.d(TAG, "getDataManagementIntent() returning intent "
-                            + intent);
-                    return intent;
-                } catch (Exception e) {
-                    /* fall through to return null */
-                    Slog.e(TAG, "Unable to get management intent from transport: " + e.getMessage());
-                }
+        final IBackupTransport transport = mTransportManager.getTransportBinder(transportName);
+        if (transport != null) {
+            try {
+                final Intent intent = transport.dataManagementIntent();
+                if (MORE_DEBUG) Slog.d(TAG, "getDataManagementIntent() returning intent "
+                        + intent);
+                return intent;
+            } catch (Exception e) {
+                /* fall through to return null */
+                Slog.e(TAG, "Unable to get management intent from transport: " + e.getMessage());
             }
         }
 
@@ -9907,17 +9707,15 @@
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
                 "getDataManagementLabel");
 
-        synchronized (mTransports) {
-            final IBackupTransport transport = mTransports.get(transportName);
-            if (transport != null) {
-                try {
-                    final String text = transport.dataManagementLabel();
-                    if (MORE_DEBUG) Slog.d(TAG, "getDataManagementLabel() returning " + text);
-                    return text;
-                } catch (Exception e) {
-                    /* fall through to return null */
-                    Slog.e(TAG, "Unable to get management label from transport: " + e.getMessage());
-                }
+        final IBackupTransport transport = mTransportManager.getTransportBinder(transportName);
+        if (transport != null) {
+            try {
+                final String text = transport.dataManagementLabel();
+                if (MORE_DEBUG) Slog.d(TAG, "getDataManagementLabel() returning " + text);
+                return text;
+            } catch (Exception e) {
+                /* fall through to return null */
+                Slog.e(TAG, "Unable to get management label from transport: " + e.getMessage());
             }
         }
 
@@ -9979,7 +9777,7 @@
         }
 
         // Do we have a transport to fetch data for us?
-        IBackupTransport transport = getTransport(mCurrentTransport);
+        IBackupTransport transport = mTransportManager.getCurrentTransportBinder();
         if (transport == null) {
             if (DEBUG) Slog.w(TAG, "No transport");
             skip = true;
@@ -10033,7 +9831,7 @@
 
         boolean needPermission = true;
         if (transport == null) {
-            transport = mCurrentTransport;
+            transport = mTransportManager.getCurrentTransportName();
 
             if (packageName != null) {
                 PackageInfo app = null;
@@ -10127,7 +9925,7 @@
                     appIsStopped(packageInfo.applicationInfo)) {
                 return false;
             }
-            IBackupTransport transport = getTransport(mCurrentTransport);
+            IBackupTransport transport = mTransportManager.getCurrentTransportBinder();
             if (transport != null) {
                 try {
                     return transport.isAppEligibleForBackup(packageInfo,
@@ -10156,7 +9954,7 @@
 
         ActiveRestoreSession(String packageName, String transport) {
             mPackageName = packageName;
-            mRestoreTransport = getTransport(transport);
+            mRestoreTransport = mTransportManager.getTransportBinder(transport);
         }
 
         public void markTimedOut() {
@@ -10515,7 +10313,7 @@
             pw.println("  next scheduled: " + KeyValueBackupJob.nextScheduled());
 
             pw.println("Transport whitelist:");
-            for (ComponentName transport : mTransportWhitelist) {
+            for (ComponentName transport : mTransportManager.getTransportWhitelist()) {
                 pw.print("    ");
                 pw.println(transport.flattenToShortString());
             }
@@ -10524,9 +10322,9 @@
             final String[] transports = listAllTransports();
             if (transports != null) {
                 for (String t : listAllTransports()) {
-                    pw.println((t.equals(mCurrentTransport) ? "  * " : "    ") + t);
+                    pw.println((t.equals(mTransportManager.getCurrentTransportName()) ? "  * " : "    ") + t);
                     try {
-                        IBackupTransport transport = getTransport(t);
+                        IBackupTransport transport = mTransportManager.getTransportBinder(t);
                         File dir = new File(mBaseStateDir, transport.transportDirName());
                         pw.println("       destination: " + transport.currentDestinationString());
                         pw.println("       intent: " + transport.configurationIntent());
diff --git a/services/backup/java/com/android/server/backup/Trampoline.java b/services/backup/java/com/android/server/backup/Trampoline.java
index d677f5e..a1a2c95 100644
--- a/services/backup/java/com/android/server/backup/Trampoline.java
+++ b/services/backup/java/com/android/server/backup/Trampoline.java
@@ -20,6 +20,8 @@
 import android.app.backup.IBackupObserver;
 import android.app.backup.IFullBackupRestoreObserver;
 import android.app.backup.IRestoreSession;
+import android.app.backup.ISelectBackupTransportCallback;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Binder;
@@ -275,6 +277,12 @@
     }
 
     @Override
+    public ComponentName[] listAllTransportComponents() throws RemoteException {
+        BackupManagerService svc = mService;
+        return (svc != null) ? svc.listAllTransportComponents() : null;
+    }
+
+    @Override
     public String[] getTransportWhitelist() {
         BackupManagerService svc = mService;
         return (svc != null) ? svc.getTransportWhitelist() : null;
@@ -287,6 +295,15 @@
     }
 
     @Override
+    public void selectBackupTransportAsync(ComponentName transport,
+            ISelectBackupTransportCallback listener) throws RemoteException {
+        BackupManagerService svc = mService;
+        if (svc != null) {
+            svc.selectBackupTransportAsync(transport, listener);
+        }
+    }
+
+    @Override
     public Intent getConfigurationIntent(String transport) throws RemoteException {
         BackupManagerService svc = mService;
         return (svc != null) ? svc.getConfigurationIntent(transport) : null;
diff --git a/services/backup/java/com/android/server/backup/TransportManager.java b/services/backup/java/com/android/server/backup/TransportManager.java
new file mode 100644
index 0000000..93d5a1e
--- /dev/null
+++ b/services/backup/java/com/android/server/backup/TransportManager.java
@@ -0,0 +1,410 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.backup;
+
+import android.app.backup.BackupManager;
+import android.app.backup.SelectBackupTransportCallback;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.backup.IBackupTransport;
+import com.android.server.EventLogTags;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Handles in-memory bookkeeping of all BackupTransport objects.
+ */
+class TransportManager {
+
+    private static final String TAG = "BackupTransportManager";
+
+    private static final String SERVICE_ACTION_TRANSPORT_HOST = "android.backup.TRANSPORT_HOST";
+
+    private final Intent mTransportServiceIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST);
+    private final Context mContext;
+    private final PackageManager mPackageManager;
+    private final Set<ComponentName> mTransportWhitelist;
+
+    /**
+     * This listener is called after we bind to any transport. If it returns true, this is a valid
+     * transport.
+     */
+    private final TransportBoundListener mTransportBoundListener;
+
+    private String mCurrentTransportName;
+
+    /** Lock on this before accessing mValidTransports and mBoundTransports. */
+    private final Object mTransportLock = new Object();
+
+    /**
+     * We have detected these transports on the device. Unless in exceptional cases, we are also
+     * bound to all of these.
+     */
+    @GuardedBy("mTransportLock")
+    private final Map<ComponentName, TransportConnection> mValidTransports = new ArrayMap<>();
+
+    /** We are currently bound to these transports. */
+    @GuardedBy("mTransportLock")
+    private final Map<String, ComponentName> mBoundTransports = new ArrayMap<>();
+
+    TransportManager(Context context, Set<ComponentName> whitelist, String defaultTransport,
+            TransportBoundListener listener) {
+        mContext = context;
+        mPackageManager = context.getPackageManager();
+        mTransportWhitelist = whitelist;
+        mCurrentTransportName = defaultTransport;
+        mTransportBoundListener = listener;
+    }
+
+    void onPackageAdded(String packageName) {
+        // New package added. Bind to all transports it contains.
+        synchronized (mTransportLock) {
+            log_verbose("Package added. Binding to all transports. " + packageName);
+            bindToAllInternal(packageName, null /* all components */);
+        }
+    }
+
+    void onPackageRemoved(String packageName) {
+        // Package removed. Remove all its transports from our list. These transports have already
+        // been removed from mBoundTransports because onServiceDisconnected would already been
+        // called on TransportConnection objects.
+        synchronized (mTransportLock) {
+            for (ComponentName transport : mValidTransports.keySet()) {
+                if (transport.getPackageName().equals(packageName)) {
+                    TransportConnection removed = mValidTransports.remove(transport);
+                    if (removed != null) {
+                        mContext.unbindService(removed);
+                        log_verbose("Package removed, Removing transport: " +
+                                transport.flattenToShortString());
+                    }
+                }
+            }
+        }
+    }
+
+    void onPackageChanged(String packageName, String[] components) {
+        synchronized (mTransportLock) {
+            // Remove all changed components from mValidTransports. We'll bind to them again
+            // and re-add them if still valid.
+            for (String component : components) {
+                ComponentName componentName = new ComponentName(packageName, component);
+                TransportConnection removed = mValidTransports.remove(componentName);
+                if (removed != null) {
+                    mContext.unbindService(removed);
+                    log_verbose("Package changed. Removing transport: " +
+                            componentName.flattenToShortString());
+                }
+            }
+            bindToAllInternal(packageName, components);
+        }
+    }
+
+    IBackupTransport getTransportBinder(String transportName) {
+        synchronized (mTransportLock) {
+            ComponentName component = mBoundTransports.get(transportName);
+            if (component == null) {
+                Slog.w(TAG, "Transport " + transportName + " not bound.");
+                return null;
+            }
+            TransportConnection conn = mValidTransports.get(component);
+            if (conn == null) {
+                Slog.w(TAG, "Transport " + transportName + " not valid.");
+                return null;
+            }
+            return conn.getBinder();
+        }
+    }
+
+    IBackupTransport getCurrentTransportBinder() {
+        return getTransportBinder(mCurrentTransportName);
+    }
+
+    String getTransportName(IBackupTransport binder) {
+        synchronized (mTransportLock) {
+            for (TransportConnection conn : mValidTransports.values()) {
+                if (conn.getBinder() == binder) {
+                    return conn.getName();
+                }
+            }
+        }
+        return null;
+    }
+
+    String[] getBoundTransportNames() {
+        synchronized (mTransportLock) {
+            return mBoundTransports.keySet().toArray(new String[0]);
+        }
+    }
+
+    ComponentName[] getAllTransportCompenents() {
+        synchronized (mTransportLock) {
+            return mValidTransports.keySet().toArray(new ComponentName[0]);
+        }
+    }
+
+    String getCurrentTransportName() {
+        return mCurrentTransportName;
+    }
+
+    Set<ComponentName> getTransportWhitelist() {
+        return mTransportWhitelist;
+    }
+
+    String selectTransport(String transport) {
+        synchronized (mTransportLock) {
+            String prevTransport = mCurrentTransportName;
+            mCurrentTransportName = transport;
+            return prevTransport;
+        }
+    }
+
+    void ensureTransportReady(ComponentName transportComponent, SelectBackupTransportCallback listener) {
+        synchronized (mTransportLock) {
+            TransportConnection conn = mValidTransports.get(transportComponent);
+            if (conn == null) {
+                listener.onFailure(BackupManager.ERROR_TRANSPORT_UNAVAILABLE);
+                return;
+            }
+            // Transport can be unbound if the process hosting it crashed.
+            conn.bindIfUnbound();
+            conn.addListener(listener);
+        }
+    }
+
+    void registerAllTransports() {
+        bindToAllInternal(null /* all packages */, null /* all components */);
+    }
+
+    /**
+     * Bind to all transports belonging to the given package and the given component list.
+     * null acts a wildcard.
+     *
+     * If packageName is null, bind to all transports in all packages.
+     * If components is null, bind to all transports in the given package.
+     */
+    private void bindToAllInternal(String packageName, String[] components) {
+        PackageInfo pkgInfo = null;
+        if (packageName != null) {
+            try {
+                pkgInfo = mPackageManager.getPackageInfo(packageName, 0);
+            } catch (PackageManager.NameNotFoundException e) {
+                Slog.w(TAG, "Package not found: " + packageName);
+                return;
+            }
+        }
+
+        Intent intent = new Intent(mTransportServiceIntent);
+        if (packageName != null) {
+            intent.setPackage(packageName);
+        }
+
+        List<ResolveInfo> hosts = mPackageManager.queryIntentServicesAsUser(
+                intent, 0, UserHandle.USER_SYSTEM);
+        if (hosts != null) {
+            for (ResolveInfo host : hosts) {
+                final ServiceInfo info = host.serviceInfo;
+                boolean shouldBind = false;
+                if (components != null && packageName != null) {
+                    for (String component : components) {
+                        ComponentName cn = new ComponentName(pkgInfo.packageName, component);
+                        if (info.getComponentName().equals(cn)) {
+                            shouldBind = true;
+                            break;
+                        }
+                    }
+                } else {
+                    shouldBind = true;
+                }
+                if (shouldBind && isTransportTrusted(info.getComponentName())) {
+                    tryBindTransport(info);
+                }
+            }
+        }
+    }
+
+    /** Transport has to be whitelisted and privileged. */
+    private boolean isTransportTrusted(ComponentName transport) {
+        if (!mTransportWhitelist.contains(transport)) {
+            Slog.w(TAG, "BackupTransport " + transport.flattenToShortString() +
+                    " not whitelisted.");
+            return false;
+        }
+        try {
+            PackageInfo packInfo = mPackageManager.getPackageInfo(transport.getPackageName(), 0);
+            if ((packInfo.applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED)
+                    == 0) {
+                Slog.w(TAG, "Transport package " + transport.getPackageName() + " not privileged");
+                return false;
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            Slog.w(TAG, "Package not found.", e);
+            return false;
+        }
+        return true;
+    }
+
+    private void tryBindTransport(ServiceInfo transport) {
+        Slog.d(TAG, "Binding to transport: " + transport.getComponentName().flattenToShortString());
+        // TODO: b/22388012 (Multi user backup and restore)
+        TransportConnection connection = new TransportConnection(transport.getComponentName());
+        if (bindToTransport(transport.getComponentName(), connection)) {
+            synchronized (mTransportLock) {
+                mValidTransports.put(transport.getComponentName(), connection);
+            }
+        } else {
+            Slog.w(TAG, "Couldn't bind to transport " + transport.getComponentName());
+        }
+    }
+
+    private boolean bindToTransport(ComponentName componentName, ServiceConnection connection) {
+        Intent intent = new Intent(mTransportServiceIntent)
+                .setComponent(componentName);
+        return mContext.bindServiceAsUser(intent, connection, Context.BIND_AUTO_CREATE,
+                UserHandle.SYSTEM);
+    }
+
+    private class TransportConnection implements ServiceConnection {
+
+        // Hold mTransportsLock to access these fields so as to provide a consistent view of them.
+        private IBackupTransport mBinder;
+        private final List<SelectBackupTransportCallback> mListeners = new ArrayList<>();
+        private String mTransportName;
+
+        private final ComponentName mTransportComponent;
+
+        private TransportConnection(ComponentName transportComponent) {
+            mTransportComponent = transportComponent;
+        }
+
+        @Override
+        public void onServiceConnected(ComponentName component, IBinder binder) {
+            synchronized (mTransportLock) {
+                mBinder = IBackupTransport.Stub.asInterface(binder);
+                boolean success = false;
+
+                EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE,
+                    component.flattenToShortString(), 1);
+
+                try {
+                    mTransportName = mBinder.name();
+                    // BackupManager requests some fields from the transport. If they are
+                    // invalid, throw away this transport.
+                    success = mTransportBoundListener.onTransportBound(mBinder);
+                } catch (RemoteException e) {
+                    success = false;
+                    Slog.e(TAG, "Couldn't get transport name.", e);
+                } finally {
+                    if (success) {
+                        Slog.d(TAG, "Bound to transport: " + component.flattenToShortString());
+                        mBoundTransports.put(mTransportName, component);
+                        for (SelectBackupTransportCallback listener : mListeners) {
+                            listener.onSuccess(mTransportName);
+                        }
+                    } else {
+                        Slog.w(TAG, "Bound to transport " + component.flattenToShortString() +
+                                " but it is invalid");
+                        EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE,
+                                component.flattenToShortString(), 0);
+                        mContext.unbindService(this);
+                        mValidTransports.remove(component);
+                        mBinder = null;
+                        for (SelectBackupTransportCallback listener : mListeners) {
+                            listener.onFailure(BackupManager.ERROR_TRANSPORT_INVALID);
+                        }
+                    }
+                    mListeners.clear();
+                }
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName component) {
+            synchronized (mTransportLock) {
+                mBinder = null;
+                mBoundTransports.remove(mTransportName);
+            }
+            EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE,
+                    component.flattenToShortString(), 0);
+            Slog.w(TAG, "Disconnected from transport " + component.flattenToShortString());
+        }
+
+        private IBackupTransport getBinder() {
+            synchronized (mTransportLock) {
+                return mBinder;
+            }
+        }
+
+        private String getName() {
+            synchronized (mTransportLock) {
+                return mTransportName;
+            }
+        }
+
+        private void bindIfUnbound() {
+            synchronized (mTransportLock) {
+                if (mBinder == null) {
+                    Slog.d(TAG,
+                            "Rebinding to transport " + mTransportComponent.flattenToShortString());
+                    bindToTransport(mTransportComponent, this);
+                }
+            }
+        }
+
+        private void addListener(SelectBackupTransportCallback listener) {
+            synchronized (mTransportLock) {
+                if (mBinder == null) {
+                    // We are waiting for bind to complete. If mBinder is set to null after the bind
+                    // is complete due to transport being invalid, we won't find 'this' connection
+                    // object in mValidTransports list and this function can't be called.
+                    mListeners.add(listener);
+                } else {
+                    listener.onSuccess(mTransportName);
+                }
+            }
+        }
+    }
+
+    interface TransportBoundListener {
+        /** Should return true if this is a valid transport. */
+        boolean onTransportBound(IBackupTransport binder);
+    }
+
+    private static void log_verbose(String message) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Slog.v(TAG, message);
+        }
+    }
+}