From af547f4a15a7d6121306a5e973ae7f3709e5df3a Mon Sep 17 00:00:00 2001 From: Bernardo Rufino Date: Mon, 13 Nov 2017 15:49:41 +0000 Subject: [PATCH] Binding on-demand #1: TransportClient infra + PerformBackupTask usage This CL introduces TransportClient infra-structure and integration in TransportManager, plus a few unit tests. Also start using binding on-demand for PerformBackupTask. Ref: http://go/br-binding-on-demand Bug: 17140907 Test: make RunFrameworksServicesRoboTests Test: runtest -p com.android.server.backup frameworks-services Test: adb shell bmgr backupnow With transports manually unbound and also canceled the operation a few times. Then inspected TransportClient logcat Test: gts-tradefed run commandAndExit gts-dev -m GtsBackupTestCases Test: gts-tradefed run commandAndExit gts-dev -m GtsBackupHostTestCases Test: cts-tradefed run commandAndExit cts-dev -m CtsBackupTestCases Test: Manually unbound before GTS/CTS above. Change-Id: I63cbde27131205787d90663603a4f8f61d187607 --- .../RefactoredBackupManagerService.java | 31 +- .../server/backup/TransportManager.java | 80 ++- .../server/backup/internal/BackupHandler.java | 33 +- .../internal/OnTaskFinishedListener.java | 34 ++ .../backup/internal/PerformBackupTask.java | 60 ++- .../server/backup/params/BackupParams.java | 16 +- .../backup/transport/TransportClient.java | 468 ++++++++++++++++++ .../transport/TransportClientManager.java | 83 ++++ .../TransportConnectionListener.java | 37 ++ .../TransportNotAvailableException.java | 32 ++ .../backup/transport/TransportUtils.java | 60 +++ .../backup/transport/TransportClientTest.java | 240 +++++++++ 12 files changed, 1129 insertions(+), 45 deletions(-) create mode 100644 services/backup/java/com/android/server/backup/internal/OnTaskFinishedListener.java create mode 100644 services/backup/java/com/android/server/backup/transport/TransportClient.java create mode 100644 services/backup/java/com/android/server/backup/transport/TransportClientManager.java create mode 100644 services/backup/java/com/android/server/backup/transport/TransportConnectionListener.java create mode 100644 services/backup/java/com/android/server/backup/transport/TransportNotAvailableException.java create mode 100644 services/backup/java/com/android/server/backup/transport/TransportUtils.java create mode 100644 services/robotests/src/com/android/server/backup/transport/TransportClientTest.java diff --git a/services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java b/services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java index a45a4f0c35d3a..dd29a04e5f543 100644 --- a/services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java +++ b/services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java @@ -48,7 +48,6 @@ import android.app.backup.IBackupObserver; import android.app.backup.IFullBackupRestoreObserver; import android.app.backup.IRestoreSession; import android.app.backup.ISelectBackupTransportCallback; -import android.app.backup.SelectBackupTransportCallback; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -103,6 +102,7 @@ import com.android.server.backup.fullbackup.PerformFullTransportBackupTask; import com.android.server.backup.internal.BackupHandler; import com.android.server.backup.internal.BackupRequest; import com.android.server.backup.internal.ClearDataObserver; +import com.android.server.backup.internal.OnTaskFinishedListener; import com.android.server.backup.internal.Operation; import com.android.server.backup.internal.PerformInitializeTask; import com.android.server.backup.internal.ProvisionedObserver; @@ -117,6 +117,7 @@ import com.android.server.backup.params.ClearRetryParams; import com.android.server.backup.params.RestoreParams; import com.android.server.backup.restore.ActiveRestoreSession; import com.android.server.backup.restore.PerformUnifiedRestoreTask; +import com.android.server.backup.transport.TransportClient; import com.android.server.backup.utils.AppBackupUtils; import com.android.server.backup.utils.BackupManagerMonitorUtils; import com.android.server.backup.utils.BackupObserverUtils; @@ -1585,8 +1586,27 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter return BackupManager.ERROR_BACKUP_NOT_ALLOWED; } + // We're using pieces of the new binding on-demand infra-structure and the old always-bound + // infra-structure below this comment. The TransportManager.getCurrentTransportClient() line + // is using the new one and TransportManager.getCurrentTransportBinder() is using the old. + // This is weird but there is a reason. + // This is the natural place to put TransportManager.getCurrentTransportClient() because of + // the null handling below that should be the same for TransportClient. + // TransportClient.connect() would return a IBackupTransport for us (instead of using the + // old infra), but it may block and we don't want this in this thread. + // The only usage of transport in this method is for transport.transportDirName(). When the + // push-from-transport part of binding on-demand is in place we will replace the calls for + // IBackupTransport.transportDirName() with calls for + // TransportManager.transportDirName(transportName) or similar. So we'll leave the old piece + // here until we implement that. + // TODO(brufino): Remove always-bound code mTransportManager.getCurrentTransportBinder() + TransportClient transportClient = + mTransportManager.getCurrentTransportClient("BMS.requestBackup()"); IBackupTransport transport = mTransportManager.getCurrentTransportBinder(); - if (transport == null) { + if (transportClient == null || transport == null) { + if (transportClient != null) { + mTransportManager.disposeOfTransportClient(transportClient, "BMS.requestBackup()"); + } BackupObserverUtils.sendBackupFinished(observer, BackupManager.ERROR_TRANSPORT_ABORTED); monitor = BackupManagerMonitorUtils.monitorEvent(monitor, BackupManagerMonitor.LOG_EVENT_ID_TRANSPORT_IS_NULL, @@ -1594,6 +1614,9 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter return BackupManager.ERROR_TRANSPORT_ABORTED; } + OnTaskFinishedListener listener = + caller -> mTransportManager.disposeOfTransportClient(transportClient, caller); + ArrayList fullBackupList = new ArrayList<>(); ArrayList kvBackupList = new ArrayList<>(); for (String packageName : packages) { @@ -1640,8 +1663,8 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter boolean nonIncrementalBackup = (flags & BackupManager.FLAG_NON_INCREMENTAL_BACKUP) != 0; Message msg = mBackupHandler.obtainMessage(MSG_REQUEST_BACKUP); - msg.obj = new BackupParams(transport, dirName, kvBackupList, fullBackupList, observer, - monitor, true, nonIncrementalBackup); + msg.obj = new BackupParams(transportClient, dirName, kvBackupList, fullBackupList, observer, + monitor, listener, true, nonIncrementalBackup); mBackupHandler.sendMessage(msg); return BackupManager.SUCCESS; } diff --git a/services/backup/java/com/android/server/backup/TransportManager.java b/services/backup/java/com/android/server/backup/TransportManager.java index 7a0173f669afd..a2b5cb883b8a0 100644 --- a/services/backup/java/com/android/server/backup/TransportManager.java +++ b/services/backup/java/com/android/server/backup/TransportManager.java @@ -16,8 +16,9 @@ package com.android.server.backup; +import android.annotation.Nullable; import android.app.backup.BackupManager; -import android.app.backup.SelectBackupTransportCallback; +import android.app.backup.BackupTransport; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -44,9 +45,11 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.backup.IBackupTransport; import com.android.server.EventLogTags; +import com.android.server.backup.transport.TransportClient; +import com.android.server.backup.transport.TransportClientManager; +import com.android.server.backup.transport.TransportConnectionListener; import java.util.ArrayList; -import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -60,8 +63,7 @@ public class TransportManager { private static final String TAG = "BackupTransportManager"; @VisibleForTesting - /* package */ static final String SERVICE_ACTION_TRANSPORT_HOST = - "android.backup.TRANSPORT_HOST"; + public static final String SERVICE_ACTION_TRANSPORT_HOST = "android.backup.TRANSPORT_HOST"; private static final long REBINDING_TIMEOUT_UNPROVISIONED_MS = 30 * 1000; // 30 sec private static final long REBINDING_TIMEOUT_PROVISIONED_MS = 5 * 60 * 1000; // 5 mins @@ -72,6 +74,7 @@ public class TransportManager { private final PackageManager mPackageManager; private final Set mTransportWhitelist; private final Handler mHandler; + private final TransportClientManager mTransportClientManager; /** * This listener is called after we bind to any transport. If it returns true, this is a valid @@ -95,6 +98,10 @@ public class TransportManager { @GuardedBy("mTransportLock") private final Map mBoundTransports = new ArrayMap<>(); + /** Names of transports we've bound to at least once */ + @GuardedBy("mTransportLock") + private final Map mTransportsByName = new ArrayMap<>(); + /** * Callback interface for {@link #ensureTransportReady(ComponentName, TransportReadyCallback)}. */ @@ -123,6 +130,7 @@ public class TransportManager { mCurrentTransportName = defaultTransport; mTransportBoundListener = listener; mHandler = new RebindOnTimeoutHandler(looper); + mTransportClientManager = new TransportClientManager(context); } void onPackageAdded(String packageName) { @@ -204,6 +212,67 @@ public class TransportManager { return null; } + /** + * Returns the transport name associated with {@param transportClient} or {@code null} if not + * found. + */ + @Nullable + public String getTransportName(TransportClient transportClient) { + ComponentName transportComponent = transportClient.getTransportComponent(); + synchronized (mTransportLock) { + for (Map.Entry transportEntry : mTransportsByName.entrySet()) { + if (transportEntry.getValue().equals(transportComponent)) { + return transportEntry.getKey(); + } + } + return null; + } + } + + /** + * Returns a {@link TransportClient} for {@param transportName} or {@code null} if not found. + * + * @param transportName The name of the transport as returned by {@link BackupTransport#name()}. + * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check + * {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more + * details. + * @return A {@link TransportClient} or null if not found. + */ + @Nullable + public TransportClient getTransportClient(String transportName, String caller) { + ComponentName transportComponent = mTransportsByName.get(transportName); + if (transportComponent == null) { + Slog.w(TAG, "Transport " + transportName + " not registered"); + return null; + } + return mTransportClientManager.getTransportClient(transportComponent, caller); + } + + /** + * Returns a {@link TransportClient} for the current transport or null if not found. + * + * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check + * {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more + * details. + * @return A {@link TransportClient} or null if not found. + */ + @Nullable + public TransportClient getCurrentTransportClient(String caller) { + return getTransportClient(mCurrentTransportName, caller); + } + + /** + * Disposes of the {@link TransportClient}. + * + * @param transportClient The {@link TransportClient} to be disposed of. + * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check + * {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more + * details. + */ + public void disposeOfTransportClient(TransportClient transportClient, String caller) { + mTransportClientManager.disposeOfTransportClient(transportClient, caller); + } + String[] getBoundTransportNames() { synchronized (mTransportLock) { return mBoundTransports.keySet().toArray(new String[mBoundTransports.size()]); @@ -374,6 +443,7 @@ public class TransportManager { String componentShortString = component.flattenToShortString().intern(); if (success) { Slog.d(TAG, "Bound to transport: " + componentShortString); + mTransportsByName.put(mTransportName, component); mBoundTransports.put(mTransportName, component); for (TransportReadyCallback listener : mListeners) { listener.onSuccess(mTransportName); @@ -528,7 +598,7 @@ public class TransportManager { // These only exists to make it testable with Robolectric, which is not updated to API level 24 // yet. // TODO: Get rid of this once Robolectric is updated. - private static UserHandle createSystemUserHandle() { + public static UserHandle createSystemUserHandle() { return new UserHandle(UserHandle.USER_SYSTEM); } } diff --git a/services/backup/java/com/android/server/backup/internal/BackupHandler.java b/services/backup/java/com/android/server/backup/internal/BackupHandler.java index 8f823004d9930..9011b95cf6147 100644 --- a/services/backup/java/com/android/server/backup/internal/BackupHandler.java +++ b/services/backup/java/com/android/server/backup/internal/BackupHandler.java @@ -38,6 +38,8 @@ import com.android.server.EventLogTags; import com.android.server.backup.BackupRestoreTask; import com.android.server.backup.DataChangedJournal; import com.android.server.backup.RefactoredBackupManagerService; +import com.android.server.backup.transport.TransportClient; +import com.android.server.backup.TransportManager; import com.android.server.backup.fullbackup.PerformAdbBackupTask; import com.android.server.backup.fullbackup.PerformFullTransportBackupTask; import com.android.server.backup.params.AdbBackupParams; @@ -51,10 +53,8 @@ import com.android.server.backup.params.RestoreParams; import com.android.server.backup.restore.PerformAdbRestoreTask; import com.android.server.backup.restore.PerformUnifiedRestoreTask; -import java.io.File; import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; /** * Asynchronous backup/restore handler thread. @@ -81,7 +81,7 @@ public class BackupHandler extends Handler { public static final int MSG_BACKUP_RESTORE_STEP = 20; public static final int MSG_OP_COMPLETE = 21; - private RefactoredBackupManagerService backupManagerService; + private final RefactoredBackupManagerService backupManagerService; public BackupHandler( RefactoredBackupManagerService backupManagerService, Looper looper) { @@ -91,13 +91,23 @@ public class BackupHandler extends Handler { public void handleMessage(Message msg) { + TransportManager transportManager = backupManagerService.getTransportManager(); switch (msg.what) { case MSG_RUN_BACKUP: { backupManagerService.setLastBackupPass(System.currentTimeMillis()); + String callerLogString = "BH/MSG_RUN_BACKUP"; + TransportClient transportClient = + transportManager.getCurrentTransportClient(callerLogString); IBackupTransport transport = - backupManagerService.getTransportManager().getCurrentTransportBinder(); + transportClient != null + ? transportClient.connect(callerLogString) + : null; if (transport == null) { + if (transportClient != null) { + transportManager + .disposeOfTransportClient(transportClient, callerLogString); + } Slog.v(TAG, "Backup requested but no transport available"); synchronized (backupManagerService.getQueueLock()) { backupManagerService.setBackupRunning(false); @@ -138,9 +148,13 @@ public class BackupHandler extends Handler { // Spin up a backup state sequence and set it running try { String dirName = transport.transportDirName(); + OnTaskFinishedListener listener = + caller -> + transportManager + .disposeOfTransportClient(transportClient, caller); PerformBackupTask pbt = new PerformBackupTask( - backupManagerService, transport, dirName, queue, - oldJournal, null, null, Collections.emptyList(), false, + backupManagerService, transportClient, dirName, queue, + oldJournal, null, null, listener, Collections.emptyList(), false, false /* nonIncremental */); Message pbtMessage = obtainMessage(MSG_BACKUP_RESTORE_STEP, pbt); sendMessage(pbtMessage); @@ -157,6 +171,7 @@ public class BackupHandler extends Handler { } if (!staged) { + transportManager.disposeOfTransportClient(transportClient, callerLogString); // if we didn't actually hand off the wakelock, rewind until next time synchronized (backupManagerService.getQueueLock()) { backupManagerService.setBackupRunning(false); @@ -382,9 +397,9 @@ public class BackupHandler extends Handler { PerformBackupTask pbt = new PerformBackupTask( backupManagerService, - params.transport, params.dirName, - kvQueue, null, params.observer, params.monitor, params.fullPackages, true, - params.nonIncrementalBackup); + params.transportClient, params.dirName, + kvQueue, null, params.observer, params.monitor, params.listener, + params.fullPackages, true, params.nonIncrementalBackup); Message pbtMessage = obtainMessage(MSG_BACKUP_RESTORE_STEP, pbt); sendMessage(pbtMessage); break; diff --git a/services/backup/java/com/android/server/backup/internal/OnTaskFinishedListener.java b/services/backup/java/com/android/server/backup/internal/OnTaskFinishedListener.java new file mode 100644 index 0000000000000..e417f06c8a050 --- /dev/null +++ b/services/backup/java/com/android/server/backup/internal/OnTaskFinishedListener.java @@ -0,0 +1,34 @@ +/* + * 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.internal; + +import com.android.server.backup.transport.TransportClient; +import com.android.server.backup.transport.TransportConnectionListener; + +/** Listener to be called when a task finishes, successfully or not. */ +public interface OnTaskFinishedListener { + OnTaskFinishedListener NOP = caller -> {}; + + /** + * Called when a task finishes, successfully or not. + * + * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check + * {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more + * details. + */ + void onFinished(String caller); +} diff --git a/services/backup/java/com/android/server/backup/internal/PerformBackupTask.java b/services/backup/java/com/android/server/backup/internal/PerformBackupTask.java index c0caa557b4ae5..1fa215a1664a8 100644 --- a/services/backup/java/com/android/server/backup/internal/PerformBackupTask.java +++ b/services/backup/java/com/android/server/backup/internal/PerformBackupTask.java @@ -63,6 +63,8 @@ import com.android.server.backup.KeyValueBackupJob; import com.android.server.backup.PackageManagerBackupAgent; import com.android.server.backup.RefactoredBackupManagerService; import com.android.server.backup.fullbackup.PerformFullTransportBackupTask; +import com.android.server.backup.transport.TransportClient; +import com.android.server.backup.transport.TransportUtils; import com.android.server.backup.utils.AppBackupUtils; import com.android.server.backup.utils.BackupManagerMonitorUtils; import com.android.server.backup.utils.BackupObserverUtils; @@ -112,7 +114,6 @@ public class PerformBackupTask implements BackupRestoreTask { private RefactoredBackupManagerService backupManagerService; private final Object mCancelLock = new Object(); - IBackupTransport mTransport; ArrayList mQueue; ArrayList mOriginalQueue; File mStateDir; @@ -122,6 +123,8 @@ public class PerformBackupTask implements BackupRestoreTask { IBackupObserver mObserver; IBackupManagerMonitor mMonitor; + private final TransportClient mTransportClient; + private final OnTaskFinishedListener mListener; private final PerformFullTransportBackupTask mFullBackupTask; private final int mCurrentOpToken; private volatile int mEphemeralOpToken; @@ -143,17 +146,19 @@ public class PerformBackupTask implements BackupRestoreTask { private volatile boolean mCancelAll; public PerformBackupTask(RefactoredBackupManagerService backupManagerService, - IBackupTransport transport, String dirName, + TransportClient transportClient, String dirName, ArrayList queue, @Nullable DataChangedJournal journal, IBackupObserver observer, IBackupManagerMonitor monitor, - List pendingFullBackups, boolean userInitiated, boolean nonIncremental) { + @Nullable OnTaskFinishedListener listener, List pendingFullBackups, + boolean userInitiated, boolean nonIncremental) { this.backupManagerService = backupManagerService; - mTransport = transport; + mTransportClient = transportClient; mOriginalQueue = queue; mQueue = new ArrayList<>(); mJournal = journal; mObserver = observer; mMonitor = monitor; + mListener = (listener != null) ? listener : OnTaskFinishedListener.NOP; mPendingFullBackups = pendingFullBackups; mUserInitiated = userInitiated; mNonIncremental = nonIncremental; @@ -289,10 +294,10 @@ public class PerformBackupTask implements BackupRestoreTask { if (DEBUG) { Slog.v(TAG, "Beginning backup of " + mQueue.size() + " targets"); } - File pmState = new File(mStateDir, PACKAGE_MANAGER_SENTINEL); try { - final String transportName = mTransport.transportDirName(); + IBackupTransport transport = mTransportClient.connectOrThrow("PBT.beginBackup()"); + final String transportName = transport.transportDirName(); EventLog.writeEvent(EventLogTags.BACKUP_START, transportName); // If we haven't stored package manager metadata yet, we must init the transport. @@ -300,7 +305,7 @@ public class PerformBackupTask implements BackupRestoreTask { Slog.i(TAG, "Initializing (wiping) backup state and transport storage"); backupManagerService.addBackupTrace("initializing transport " + transportName); backupManagerService.resetBackupState(mStateDir); // Just to make sure. - mStatus = mTransport.initializeDevice(); + mStatus = transport.initializeDevice(); backupManagerService.addBackupTrace("transport.initializeDevice() == " + mStatus); if (mStatus == BackupTransport.TRANSPORT_OK) { @@ -324,7 +329,7 @@ public class PerformBackupTask implements BackupRestoreTask { PackageManagerBackupAgent pmAgent = backupManagerService.makeMetadataAgent(); mStatus = invokeAgentForBackup( PACKAGE_MANAGER_SENTINEL, - IBackupAgent.Stub.asInterface(pmAgent.onBind()), mTransport); + IBackupAgent.Stub.asInterface(pmAgent.onBind())); backupManagerService.addBackupTrace("PMBA invoke: " + mStatus); // Because the PMBA is a local instance, it has already executed its @@ -445,7 +450,7 @@ public class PerformBackupTask implements BackupRestoreTask { backupManagerService.addBackupTrace("agent bound; a? = " + (agent != null)); if (agent != null) { mAgentBinder = agent; - mStatus = invokeAgentForBackup(request.packageName, agent, mTransport); + mStatus = invokeAgentForBackup(request.packageName, agent); // at this point we'll either get a completion callback from the // agent, or a timeout message on the main handler. either way, we're // done here as long as we're successful so far. @@ -526,11 +531,14 @@ public class PerformBackupTask implements BackupRestoreTask { // If everything actually went through and this is the first time we've // done a backup, we can now record what the current backup dataset token // is. + String callerLogString = "PBT.finalizeBackup()"; if ((backupManagerService.getCurrentToken() == 0) && (mStatus == BackupTransport.TRANSPORT_OK)) { backupManagerService.addBackupTrace("success; recording token"); try { - backupManagerService.setCurrentToken(mTransport.getCurrentRestoreSet()); + IBackupTransport transport = + mTransportClient.connectOrThrow(callerLogString); + backupManagerService.setCurrentToken(transport.getCurrentRestoreSet()); backupManagerService.writeRestoreTokens(); } catch (Exception e) { // nothing for it at this point, unfortunately, but this will be @@ -553,13 +561,13 @@ public class PerformBackupTask implements BackupRestoreTask { backupManagerService.addBackupTrace("init required; rerunning"); try { final String name = backupManagerService.getTransportManager().getTransportName( - mTransport); + mTransportClient); if (name != null) { backupManagerService.getPendingInits().add(name); } else { if (DEBUG) { - Slog.w(TAG, "Couldn't find name of transport " + mTransport - + " for init"); + Slog.w(TAG, "Couldn't find name of transport " + + mTransportClient.getTransportComponent() + " for init"); } } } catch (Exception e) { @@ -577,17 +585,21 @@ public class PerformBackupTask implements BackupRestoreTask { if (!mCancelAll && mStatus == BackupTransport.TRANSPORT_OK && mPendingFullBackups != null && !mPendingFullBackups.isEmpty()) { + // TODO(brufino): Move the onFinish() call to the full-backup task + mListener.onFinished(callerLogString); Slog.d(TAG, "Starting full backups for: " + mPendingFullBackups); // Acquiring wakelock for PerformFullTransportBackupTask before its start. backupManagerService.getWakelock().acquire(); (new Thread(mFullBackupTask, "full-transport-requested")).start(); } else if (mCancelAll) { + mListener.onFinished(callerLogString); if (mFullBackupTask != null) { mFullBackupTask.unregisterTask(); } BackupObserverUtils.sendBackupFinished(mObserver, BackupManager.ERROR_BACKUP_CANCELLED); } else { + mListener.onFinished(callerLogString); mFullBackupTask.unregisterTask(); switch (mStatus) { case BackupTransport.TRANSPORT_OK: @@ -619,8 +631,7 @@ public class PerformBackupTask implements BackupRestoreTask { // Invoke an agent's doBackup() and start a timeout message spinning on the main // handler in case it doesn't get back to us. - int invokeAgentForBackup(String packageName, IBackupAgent agent, - IBackupTransport transport) { + int invokeAgentForBackup(String packageName, IBackupAgent agent) { if (DEBUG) { Slog.d(TAG, "invokeAgentForBackup on " + packageName); } @@ -671,7 +682,10 @@ public class PerformBackupTask implements BackupRestoreTask { ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_TRUNCATE); - final long quota = mTransport.getBackupQuota(packageName, false /* isFullBackup */); + IBackupTransport transport = + mTransportClient.connectOrThrow("PBT.invokeAgentForBackup()"); + + final long quota = transport.getBackupQuota(packageName, false /* isFullBackup */); callingAgent = true; // Initiate the target's backup pass @@ -888,10 +902,12 @@ public class PerformBackupTask implements BackupRestoreTask { clearAgentState(); backupManagerService.addBackupTrace("operation complete"); + IBackupTransport transport = mTransportClient.connect("PBT.operationComplete()"); ParcelFileDescriptor backupData = null; mStatus = BackupTransport.TRANSPORT_OK; long size = 0; try { + TransportUtils.checkTransport(transport); size = mBackupDataName.length(); if (size > 0) { if (mStatus == BackupTransport.TRANSPORT_OK) { @@ -899,7 +915,7 @@ public class PerformBackupTask implements BackupRestoreTask { ParcelFileDescriptor.MODE_READ_ONLY); backupManagerService.addBackupTrace("sending data to transport"); int flags = mUserInitiated ? BackupTransport.FLAG_USER_INITIATED : 0; - mStatus = mTransport.performBackup(mCurrentPackage, backupData, flags); + mStatus = transport.performBackup(mCurrentPackage, backupData, flags); } // TODO - We call finishBackup() for each application backed up, because @@ -910,7 +926,7 @@ public class PerformBackupTask implements BackupRestoreTask { backupManagerService.addBackupTrace("data delivered: " + mStatus); if (mStatus == BackupTransport.TRANSPORT_OK) { backupManagerService.addBackupTrace("finishing op on transport"); - mStatus = mTransport.finishBackup(); + mStatus = transport.finishBackup(); backupManagerService.addBackupTrace("finished: " + mStatus); } else if (mStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) { backupManagerService.addBackupTrace("transport rejected package"); @@ -981,8 +997,8 @@ public class PerformBackupTask implements BackupRestoreTask { } if (mAgentBinder != null) { try { - long quota = mTransport.getBackupQuota(mCurrentPackage.packageName, - false); + TransportUtils.checkTransport(transport); + long quota = transport.getBackupQuota(mCurrentPackage.packageName, false); mAgentBinder.doQuotaExceeded(size, quota); } catch (Exception e) { Slog.e(TAG, "Unable to notify about quota exceeded: " + e.getMessage()); @@ -1052,7 +1068,9 @@ public class PerformBackupTask implements BackupRestoreTask { // by way of retry/backoff time. long delay; try { - delay = mTransport.requestBackupTime(); + IBackupTransport transport = + mTransportClient.connectOrThrow("PBT.revertAndEndBackup()"); + delay = transport.requestBackupTime(); } catch (Exception e) { Slog.w(TAG, "Unable to contact transport for recommended backoff: " + e.getMessage()); delay = 0; // use the scheduler's default diff --git a/services/backup/java/com/android/server/backup/params/BackupParams.java b/services/backup/java/com/android/server/backup/params/BackupParams.java index 4fd7ddbc28f73..2ba8ec16a45ce 100644 --- a/services/backup/java/com/android/server/backup/params/BackupParams.java +++ b/services/backup/java/com/android/server/backup/params/BackupParams.java @@ -19,30 +19,34 @@ package com.android.server.backup.params; import android.app.backup.IBackupManagerMonitor; import android.app.backup.IBackupObserver; -import com.android.internal.backup.IBackupTransport; +import com.android.server.backup.internal.OnTaskFinishedListener; +import com.android.server.backup.transport.TransportClient; import java.util.ArrayList; public class BackupParams { - public IBackupTransport transport; + public TransportClient transportClient; public String dirName; public ArrayList kvPackages; public ArrayList fullPackages; public IBackupObserver observer; public IBackupManagerMonitor monitor; + public OnTaskFinishedListener listener; public boolean userInitiated; public boolean nonIncrementalBackup; - public BackupParams(IBackupTransport transport, String dirName, ArrayList kvPackages, - ArrayList fullPackages, IBackupObserver observer, - IBackupManagerMonitor monitor, boolean userInitiated, boolean nonIncrementalBackup) { - this.transport = transport; + public BackupParams(TransportClient transportClient, String dirName, + ArrayList kvPackages, ArrayList fullPackages, IBackupObserver observer, + IBackupManagerMonitor monitor, OnTaskFinishedListener listener, boolean userInitiated, + boolean nonIncrementalBackup) { + this.transportClient = transportClient; this.dirName = dirName; this.kvPackages = kvPackages; this.fullPackages = fullPackages; this.observer = observer; this.monitor = monitor; + this.listener = listener; this.userInitiated = userInitiated; this.nonIncrementalBackup = nonIncrementalBackup; } diff --git a/services/backup/java/com/android/server/backup/transport/TransportClient.java b/services/backup/java/com/android/server/backup/transport/TransportClient.java new file mode 100644 index 0000000000000..9c39729f0ddaa --- /dev/null +++ b/services/backup/java/com/android/server/backup/transport/TransportClient.java @@ -0,0 +1,468 @@ +/* + * 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.transport; + +import android.annotation.IntDef; +import android.annotation.Nullable; +import android.annotation.WorkerThread; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.DeadObjectException; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.UserHandle; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.backup.IBackupTransport; +import com.android.internal.util.Preconditions; +import com.android.server.backup.TransportManager; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +/** + * A {@link TransportClient} manages the connection to an {@link IBackupTransport} service, obtained + * via the {@param bindIntent} parameter provided in the constructor. A {@link TransportClient} is + * responsible for only one connection to the transport service, not more. + * + *

After retrieved using {@link TransportManager#getTransportClient(String, String)}, you can + * call either {@link #connect(String)}, if you can block your thread, or {@link + * #connectAsync(TransportConnectionListener, String)}, otherwise, to obtain a {@link + * IBackupTransport} instance. It's meant to be passed around as a token to a connected transport. + * When the connection is not needed anymore you should call {@link #unbind(String)} or indirectly + * via {@link TransportManager#disposeOfTransportClient(TransportClient, String)}. + * + *

DO NOT forget to unbind otherwise there will be dangling connections floating around. + * + *

This class is thread-safe. + * + * @see TransportManager + */ +public class TransportClient { + private static final String TAG = "TransportClient"; + + private final Context mContext; + private final Intent mBindIntent; + private final String mIdentifier; + private final ComponentName mTransportComponent; + private final Handler mListenerHandler; + private final String mPrefixForLog; + private final Object mStateLock = new Object(); + + @GuardedBy("mStateLock") + private final Map mListeners = new ArrayMap<>(); + + @GuardedBy("mStateLock") + @State + private int mState = State.IDLE; + + @GuardedBy("mStateLock") + private volatile IBackupTransport mTransport; + + TransportClient( + Context context, + Intent bindIntent, + ComponentName transportComponent, + String identifier) { + this(context, bindIntent, transportComponent, identifier, Handler.getMain()); + } + + @VisibleForTesting + TransportClient( + Context context, + Intent bindIntent, + ComponentName transportComponent, + String identifier, + Handler listenerHandler) { + mContext = context; + mTransportComponent = transportComponent; + mBindIntent = bindIntent; + mIdentifier = identifier; + mListenerHandler = listenerHandler; + + // For logging + String classNameForLog = mTransportComponent.getShortClassName().replaceFirst(".*\\.", ""); + mPrefixForLog = classNameForLog + "#" + mIdentifier + ": "; + } + + public ComponentName getTransportComponent() { + return mTransportComponent; + } + + // Calls to onServiceDisconnected() or onBindingDied() turn TransportClient UNUSABLE. After one + // of these calls, if a binding happen again the new service can be a different instance. Since + // transports are stateful, we don't want a new instance responding for an old instance's state. + private ServiceConnection mConnection = + new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName componentName, IBinder binder) { + IBackupTransport transport = IBackupTransport.Stub.asInterface(binder); + synchronized (mStateLock) { + checkStateIntegrityLocked(); + + if (mState != State.UNUSABLE) { + log(Log.DEBUG, "Transport connected"); + setStateLocked(State.CONNECTED, transport); + notifyListenersAndClearLocked(transport); + } + } + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + synchronized (mStateLock) { + log(Log.ERROR, "Service disconnected: client UNUSABLE"); + setStateLocked(State.UNUSABLE, null); + // After unbindService() no calls back to mConnection + mContext.unbindService(this); + } + } + + @Override + public void onBindingDied(ComponentName name) { + synchronized (mStateLock) { + checkStateIntegrityLocked(); + + log(Log.ERROR, "Binding died: client UNUSABLE"); + // After unbindService() no calls back to mConnection + switch (mState) { + case State.UNUSABLE: + break; + case State.IDLE: + log(Log.ERROR, "Unexpected state transition IDLE => UNUSABLE"); + setStateLocked(State.UNUSABLE, null); + break; + case State.BOUND_AND_CONNECTING: + setStateLocked(State.UNUSABLE, null); + mContext.unbindService(this); + notifyListenersAndClearLocked(null); + break; + case State.CONNECTED: + setStateLocked(State.UNUSABLE, null); + mContext.unbindService(this); + break; + } + } + } + }; + + /** + * Attempts to connect to the transport (if needed). + * + *

Note that being bound is not the same as connected. To be connected you also need to be + * bound. You go from nothing to bound, then to bound and connected. To have a usable transport + * binder instance you need to be connected. This method will attempt to connect and return an + * usable transport binder regardless of the state of the object, it may already be connected, + * or bound but not connected, not bound at all or even unusable. + * + *

So, a {@link Context#bindServiceAsUser(Intent, ServiceConnection, int, UserHandle)} (or + * one of its variants) can be called or not depending on the inner state. However, it won't be + * called again if we're already bound. For example, if one was already requested but the + * framework has not yet returned (meaning we're bound but still trying to connect) it won't + * trigger another one, just piggyback on the original request. + * + *

It's guaranteed that you are going to get a call back to {@param listener} after this + * call. However, the {@param IBackupTransport} parameter, the transport binder, is not + * guaranteed to be non-null, or if it's non-null it's not guaranteed to be usable - i.e. it can + * throw {@link DeadObjectException}s on method calls. You should check for both in your code. + * The reasons for a null transport binder are: + * + *

    + *
  • Some code called {@link #unbind(String)} before you got a callback. + *
  • The framework had already called {@link + * ServiceConnection#onServiceDisconnected(ComponentName)} or {@link + * ServiceConnection#onBindingDied(ComponentName)} on this object's connection before. + * Check the documentation of those methods for when that happens. + *
  • The framework returns false for {@link Context#bindServiceAsUser(Intent, + * ServiceConnection, int, UserHandle)} (or one of its variants). Check documentation for + * when this happens. + *
+ * + * For unusable transport binders check {@link DeadObjectException}. + * + * @param listener The listener that will be called with the (possibly null or unusable) {@link + * IBackupTransport} instance and this {@link TransportClient} object. + * @param caller A {@link String} identifying the caller for logging/debugging purposes. This + * should be a human-readable short string that is easily identifiable in the logs. Ideally + * TAG.methodName(), where TAG is the one used in logcat. In cases where this is is not very + * descriptive like MyHandler.handleMessage() you should put something that someone reading + * the code would understand, like MyHandler/MSG_FOO. + * @see #connect(String) + * @see DeadObjectException + * @see ServiceConnection#onServiceConnected(ComponentName, IBinder) + * @see ServiceConnection#onServiceDisconnected(ComponentName) + * @see Context#bindServiceAsUser(Intent, ServiceConnection, int, UserHandle) + */ + public void connectAsync(TransportConnectionListener listener, String caller) { + synchronized (mStateLock) { + checkStateIntegrityLocked(); + + switch (mState) { + case State.UNUSABLE: + log(Log.DEBUG, caller, "Async connect: UNUSABLE client"); + notifyListener(listener, null, caller); + break; + case State.IDLE: + boolean hasBound = + mContext.bindServiceAsUser( + mBindIntent, + mConnection, + Context.BIND_AUTO_CREATE, + TransportManager.createSystemUserHandle()); + if (hasBound) { + // We don't need to set a time-out because we are guaranteed to get a call + // back in ServiceConnection, either an onServiceConnected() or + // onBindingDied(). + log(Log.DEBUG, caller, "Async connect: service bound, connecting"); + setStateLocked(State.BOUND_AND_CONNECTING, null); + mListeners.put(listener, caller); + } else { + log(Log.ERROR, "Async connect: bindService returned false"); + // mState remains State.IDLE + mContext.unbindService(mConnection); + notifyListener(listener, null, caller); + } + break; + case State.BOUND_AND_CONNECTING: + log(Log.DEBUG, caller, "Async connect: already connecting, adding listener"); + mListeners.put(listener, caller); + break; + case State.CONNECTED: + log(Log.DEBUG, caller, "Async connect: reusing transport"); + notifyListener(listener, mTransport, caller); + break; + } + } + } + + /** + * Removes the transport binding. + * + * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check + * {@link #connectAsync(TransportConnectionListener, String)} for more details. + */ + public void unbind(String caller) { + synchronized (mStateLock) { + checkStateIntegrityLocked(); + + log(Log.DEBUG, caller, "Unbind requested (was " + stateToString(mState) + ")"); + switch (mState) { + case State.UNUSABLE: + case State.IDLE: + break; + case State.BOUND_AND_CONNECTING: + setStateLocked(State.IDLE, null); + // After unbindService() no calls back to mConnection + mContext.unbindService(mConnection); + notifyListenersAndClearLocked(null); + break; + case State.CONNECTED: + setStateLocked(State.IDLE, null); + mContext.unbindService(mConnection); + break; + } + } + } + + /** + * Attempts to connect to the transport (if needed) and returns it. + * + *

Synchronous version of {@link #connectAsync(TransportConnectionListener, String)}. The + * same observations about state are valid here. Also, what was said about the {@link + * IBackupTransport} parameter of {@link TransportConnectionListener} now apply to the return + * value of this method. + * + *

This is a potentially blocking operation, so be sure to call this carefully on the correct + * threads. You can't call this from the process main-thread (it throws an exception if you do + * so). + * + *

In most cases only the first call to this method will block, the following calls should + * return instantly. However, this is not guaranteed. + * + * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check + * {@link #connectAsync(TransportConnectionListener, String)} for more details. + * @return A {@link IBackupTransport} transport binder instance or null. If it's non-null it can + * still be unusable - throws {@link DeadObjectException} on method calls + */ + @WorkerThread + @Nullable + public IBackupTransport connect(String caller) { + // If called on the main-thread this could deadlock waiting because calls to + // ServiceConnection are on the main-thread as well + Preconditions.checkState( + !Looper.getMainLooper().isCurrentThread(), "Can't call connect() on main thread"); + + IBackupTransport transport = mTransport; + if (transport != null) { + log(Log.DEBUG, caller, "Sync connect: reusing transport"); + return transport; + } + + // If it's already UNUSABLE we return straight away, no need to go to main-thread + synchronized (mStateLock) { + if (mState == State.UNUSABLE) { + log(Log.DEBUG, caller, "Sync connect: UNUSABLE client"); + return null; + } + } + + CompletableFuture transportFuture = new CompletableFuture<>(); + TransportConnectionListener requestListener = + (requestedTransport, transportClient) -> + transportFuture.complete(requestedTransport); + + log(Log.DEBUG, caller, "Sync connect: calling async"); + connectAsync(requestListener, caller); + + try { + return transportFuture.get(); + } catch (InterruptedException | ExecutionException e) { + String error = e.getClass().getSimpleName(); + log(Log.ERROR, caller, error + " while waiting for transport: " + e.getMessage()); + return null; + } + } + + /** + * Tries to connect to the transport, if it fails throws {@link TransportNotAvailableException}. + * + *

Same as {@link #connect(String)} except it throws instead of returning null. + * + * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check + * {@link #connectAsync(TransportConnectionListener, String)} for more details. + * @return A {@link IBackupTransport} transport binder instance. + * @see #connect(String) + * @throws TransportNotAvailableException if connection attempt fails. + */ + @WorkerThread + public IBackupTransport connectOrThrow(String caller) throws TransportNotAvailableException { + IBackupTransport transport = connect(caller); + if (transport == null) { + log(Log.ERROR, caller, "Transport connection failed"); + throw new TransportNotAvailableException(); + } + return transport; + } + + @Override + public String toString() { + return "TransportClient{" + + mTransportComponent.flattenToShortString() + + "#" + + mIdentifier + + "}"; + } + + private void notifyListener( + TransportConnectionListener listener, IBackupTransport transport, String caller) { + log(Log.VERBOSE, caller, "Notifying listener of transport = " + transport); + mListenerHandler.post(() -> listener.onTransportConnectionResult(transport, this)); + } + + @GuardedBy("mStateLock") + private void notifyListenersAndClearLocked(IBackupTransport transport) { + for (Map.Entry entry : mListeners.entrySet()) { + TransportConnectionListener listener = entry.getKey(); + String caller = entry.getValue(); + notifyListener(listener, transport, caller); + } + mListeners.clear(); + } + + @GuardedBy("mStateLock") + private void setStateLocked(@State int state, @Nullable IBackupTransport transport) { + log(Log.VERBOSE, "State: " + stateToString(mState) + " => " + stateToString(state)); + mState = state; + mTransport = transport; + } + + @GuardedBy("mStateLock") + private void checkStateIntegrityLocked() { + switch (mState) { + case State.UNUSABLE: + checkState(mListeners.isEmpty(), "Unexpected listeners when state = UNUSABLE"); + checkState( + mTransport == null, "Transport expected to be null when state = UNUSABLE"); + case State.IDLE: + checkState(mListeners.isEmpty(), "Unexpected listeners when state = IDLE"); + checkState(mTransport == null, "Transport expected to be null when state = IDLE"); + break; + case State.BOUND_AND_CONNECTING: + checkState( + mTransport == null, + "Transport expected to be null when state = BOUND_AND_CONNECTING"); + break; + case State.CONNECTED: + checkState(mListeners.isEmpty(), "Unexpected listeners when state = CONNECTED"); + checkState( + mTransport != null, + "Transport expected to be non-null when state = CONNECTED"); + break; + default: + checkState(false, "Unexpected state = " + stateToString(mState)); + } + } + + private void checkState(boolean assertion, String message) { + if (!assertion) { + log(Log.ERROR, message); + } + } + + private String stateToString(@State int state) { + switch (state) { + case State.UNUSABLE: + return "UNUSABLE"; + case State.IDLE: + return "IDLE"; + case State.BOUND_AND_CONNECTING: + return "BOUND_AND_CONNECTING"; + case State.CONNECTED: + return "CONNECTED"; + default: + return ""; + } + } + + private void log(int priority, String message) { + TransportUtils.log(priority, TAG, message); + } + + private void log(int priority, String caller, String msg) { + TransportUtils.log(priority, TAG, mPrefixForLog, caller, msg); + // TODO(brufino): Log in internal list for dump + // CharSequence time = DateFormat.format("yyyy-MM-dd HH:mm:ss", System.currentTimeMillis()); + } + + @IntDef({State.UNUSABLE, State.IDLE, State.BOUND_AND_CONNECTING, State.CONNECTED}) + @Retention(RetentionPolicy.SOURCE) + private @interface State { + int UNUSABLE = 0; + int IDLE = 1; + int BOUND_AND_CONNECTING = 2; + int CONNECTED = 3; + } +} diff --git a/services/backup/java/com/android/server/backup/transport/TransportClientManager.java b/services/backup/java/com/android/server/backup/transport/TransportClientManager.java new file mode 100644 index 0000000000000..1cbe74716b03a --- /dev/null +++ b/services/backup/java/com/android/server/backup/transport/TransportClientManager.java @@ -0,0 +1,83 @@ +/* + * 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.transport; + +import static com.android.server.backup.TransportManager.SERVICE_ACTION_TRANSPORT_HOST; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import com.android.server.backup.TransportManager; + +/** + * Manages the creation and disposal of {@link TransportClient}s. The only class that should use + * this is {@link TransportManager}, all the other usages should go to {@link TransportManager}. + * + *

TODO(brufino): Implement pool of TransportClients + */ +public class TransportClientManager { + private static final String TAG = "TransportClientManager"; + + private final Context mContext; + private final Object mTransportClientsLock = new Object(); + private int mTransportClientsCreated = 0; + + public TransportClientManager(Context context) { + mContext = context; + } + + /** + * Retrieves a {@link TransportClient} for the transport identified by {@param + * transportComponent}. + * + * @param transportComponent The {@link ComponentName} of the transport. + * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check + * {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more + * details. + * @return A {@link TransportClient}. + */ + public TransportClient getTransportClient(ComponentName transportComponent, String caller) { + Intent bindIntent = + new Intent(SERVICE_ACTION_TRANSPORT_HOST).setComponent(transportComponent); + synchronized (mTransportClientsLock) { + TransportClient transportClient = + new TransportClient( + mContext, + bindIntent, + transportComponent, + Integer.toString(mTransportClientsCreated)); + mTransportClientsCreated++; + TransportUtils.log(Log.DEBUG, TAG, caller, "Retrieving " + transportClient); + return transportClient; + } + } + + /** + * Disposes of the {@link TransportClient}. + * + * @param transportClient The {@link TransportClient} to be disposed of. + * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check + * {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more + * details. + */ + public void disposeOfTransportClient(TransportClient transportClient, String caller) { + TransportUtils.log(Log.DEBUG, TAG, caller, "Disposing of " + transportClient); + transportClient.unbind(caller); + } +} diff --git a/services/backup/java/com/android/server/backup/transport/TransportConnectionListener.java b/services/backup/java/com/android/server/backup/transport/TransportConnectionListener.java new file mode 100644 index 0000000000000..1ccffd01d12c0 --- /dev/null +++ b/services/backup/java/com/android/server/backup/transport/TransportConnectionListener.java @@ -0,0 +1,37 @@ +/* + * 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.transport; + +import android.annotation.Nullable; + +import com.android.internal.backup.IBackupTransport; + +/** + * Listener to be called by {@link TransportClient#connectAsync(TransportConnectionListener, + * String)}. + */ +public interface TransportConnectionListener { + /** + * Called when {@link TransportClient} has a transport binder available or that it decided it + * couldn't obtain one, in which case {@param transport} is null. + * + * @param transport A {@link IBackupTransport} transport binder or null. + * @param transportClient The {@link TransportClient} used to retrieve this transport binder. + */ + void onTransportConnectionResult( + @Nullable IBackupTransport transport, TransportClient transportClient); +} diff --git a/services/backup/java/com/android/server/backup/transport/TransportNotAvailableException.java b/services/backup/java/com/android/server/backup/transport/TransportNotAvailableException.java new file mode 100644 index 0000000000000..a02f03c2d1cbe --- /dev/null +++ b/services/backup/java/com/android/server/backup/transport/TransportNotAvailableException.java @@ -0,0 +1,32 @@ +/* + * 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.transport; + +import com.android.internal.backup.IBackupTransport; + +/** + * Exception thrown when the {@link IBackupTransport} is not available. This happen when a {@link + * TransportClient} connection attempt fails. Check {@link + * TransportClient#connectAsync(TransportConnectionListener, String)} for when that happens. + * + * @see TransportClient#connectAsync(TransportConnectionListener, String) + */ +class TransportNotAvailableException extends Exception { + TransportNotAvailableException() { + super("Transport not available"); + } +} diff --git a/services/backup/java/com/android/server/backup/transport/TransportUtils.java b/services/backup/java/com/android/server/backup/transport/TransportUtils.java new file mode 100644 index 0000000000000..514717f46b9f0 --- /dev/null +++ b/services/backup/java/com/android/server/backup/transport/TransportUtils.java @@ -0,0 +1,60 @@ +/* + * 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.transport; + +import android.annotation.Nullable; +import android.os.DeadObjectException; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.backup.IBackupTransport; + +/** Utility methods for transport-related operations. */ +public class TransportUtils { + + /** + * Throws {@link TransportNotAvailableException} if {@param transport} is null. The semantics is + * similar to a {@link DeadObjectException} coming from a dead transport binder. + */ + public static IBackupTransport checkTransport(@Nullable IBackupTransport transport) + throws TransportNotAvailableException { + if (transport == null) { + throw new TransportNotAvailableException(); + } + return transport; + } + + static void log(int priority, String tag, String message) { + log(priority, tag, null, message); + } + + static void log(int priority, String tag, @Nullable String caller, String message) { + log(priority, tag, "", caller, message); + } + + static void log( + int priority, String tag, String prefix, @Nullable String caller, String message) { + if (Log.isLoggable(tag, priority)) { + if (caller != null) { + prefix += "[" + caller + "] "; + } + Slog.println(priority, tag, prefix + message); + } + } + + private TransportUtils() {} +} diff --git a/services/robotests/src/com/android/server/backup/transport/TransportClientTest.java b/services/robotests/src/com/android/server/backup/transport/TransportClientTest.java new file mode 100644 index 0000000000000..54d233a94815d --- /dev/null +++ b/services/robotests/src/com/android/server/backup/transport/TransportClientTest.java @@ -0,0 +1,240 @@ +/* + * 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.transport; + +import static com.android.server.backup.TransportManager.SERVICE_ACTION_TRANSPORT_HOST; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; +import android.platform.test.annotations.Presubmit; + +import com.android.internal.backup.IBackupTransport; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLooper; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE, sdk = 23) +@Presubmit +public class TransportClientTest { + private static final String PACKAGE_NAME = "some.package.name"; + private static final ComponentName TRANSPORT_COMPONENT = + new ComponentName(PACKAGE_NAME, PACKAGE_NAME + ".transport.Transport"); + + @Mock private Context mContext; + @Mock private TransportConnectionListener mTransportConnectionListener; + @Mock private TransportConnectionListener mTransportConnectionListener2; + @Mock private IBackupTransport.Stub mIBackupTransport; + private TransportClient mTransportClient; + private Intent mBindIntent; + private ShadowLooper mShadowLooper; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + Looper mainLooper = Looper.getMainLooper(); + mShadowLooper = shadowOf(mainLooper); + mBindIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST).setComponent(TRANSPORT_COMPONENT); + mTransportClient = + new TransportClient( + mContext, mBindIntent, TRANSPORT_COMPONENT, "1", new Handler(mainLooper)); + + when(mContext.bindServiceAsUser( + eq(mBindIntent), + any(ServiceConnection.class), + anyInt(), + any(UserHandle.class))) + .thenReturn(true); + } + + // TODO: Testing implementation? Remove? + @Test + public void testConnectAsync_callsBindService() throws Exception { + mTransportClient.connectAsync(mTransportConnectionListener, "caller"); + + verify(mContext) + .bindServiceAsUser( + eq(mBindIntent), + any(ServiceConnection.class), + anyInt(), + any(UserHandle.class)); + } + + @Test + public void testConnectAsync_callsListenerWhenConnected() throws Exception { + mTransportClient.connectAsync(mTransportConnectionListener, "caller"); + + // Simulate framework connecting + ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); + connection.onServiceConnected(TRANSPORT_COMPONENT, mIBackupTransport); + + mShadowLooper.runToEndOfTasks(); + verify(mTransportConnectionListener) + .onTransportConnectionResult(any(IBackupTransport.class), eq(mTransportClient)); + } + + @Test + public void testConnectAsync_whenPendingConnection_callsAllListenersWhenConnected() + throws Exception { + mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); + ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); + + mTransportClient.connectAsync(mTransportConnectionListener2, "caller2"); + + connection.onServiceConnected(TRANSPORT_COMPONENT, mIBackupTransport); + + mShadowLooper.runToEndOfTasks(); + verify(mTransportConnectionListener) + .onTransportConnectionResult(any(IBackupTransport.class), eq(mTransportClient)); + verify(mTransportConnectionListener2) + .onTransportConnectionResult(any(IBackupTransport.class), eq(mTransportClient)); + } + + @Test + public void testConnectAsync_whenAlreadyConnected_callsListener() throws Exception { + mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); + ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); + connection.onServiceConnected(TRANSPORT_COMPONENT, mIBackupTransport); + + mTransportClient.connectAsync(mTransportConnectionListener2, "caller2"); + + mShadowLooper.runToEndOfTasks(); + verify(mTransportConnectionListener2) + .onTransportConnectionResult(any(IBackupTransport.class), eq(mTransportClient)); + } + + @Test + public void testConnectAsync_whenFrameworkDoesntBind_callsListener() throws Exception { + when(mContext.bindServiceAsUser( + eq(mBindIntent), + any(ServiceConnection.class), + anyInt(), + any(UserHandle.class))) + .thenReturn(false); + + mTransportClient.connectAsync(mTransportConnectionListener, "caller"); + + mShadowLooper.runToEndOfTasks(); + verify(mTransportConnectionListener) + .onTransportConnectionResult(isNull(), eq(mTransportClient)); + } + + @Test + public void testConnectAsync_whenFrameworkDoesntBind_releasesConnection() throws Exception { + when(mContext.bindServiceAsUser( + eq(mBindIntent), + any(ServiceConnection.class), + anyInt(), + any(UserHandle.class))) + .thenReturn(false); + + mTransportClient.connectAsync(mTransportConnectionListener, "caller"); + + ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); + verify(mContext).unbindService(eq(connection)); + } + + @Test + public void testConnectAsync_afterServiceDisconnectedBeforeNewConnection_callsListener() + throws Exception { + mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); + ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); + connection.onServiceConnected(TRANSPORT_COMPONENT, mIBackupTransport); + connection.onServiceDisconnected(TRANSPORT_COMPONENT); + + mTransportClient.connectAsync(mTransportConnectionListener2, "caller1"); + + verify(mTransportConnectionListener2) + .onTransportConnectionResult(isNull(), eq(mTransportClient)); + } + + @Test + public void testConnectAsync_afterServiceDisconnectedAfterNewConnection_callsListener() + throws Exception { + mTransportClient.connectAsync(mTransportConnectionListener, "caller1"); + ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); + connection.onServiceConnected(TRANSPORT_COMPONENT, mIBackupTransport); + connection.onServiceDisconnected(TRANSPORT_COMPONENT); + connection.onServiceConnected(TRANSPORT_COMPONENT, mIBackupTransport); + + mTransportClient.connectAsync(mTransportConnectionListener2, "caller1"); + + // Yes, it should return null because the object became unusable, check design doc + verify(mTransportConnectionListener2) + .onTransportConnectionResult(isNull(), eq(mTransportClient)); + } + + // TODO(b/69153972): Support SDK 26 API (ServiceConnection.inBindingDied) for transport tests + /*@Test + public void testConnectAsync_callsListenerIfBindingDies() throws Exception { + mTransportClient.connectAsync(mTransportListener, "caller"); + + ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); + connection.onBindingDied(TRANSPORT_COMPONENT); + + mShadowLooper.runToEndOfTasks(); + verify(mTransportListener).onTransportBound(isNull(), eq(mTransportClient)); + } + + @Test + public void testConnectAsync_whenPendingConnection_callsListenersIfBindingDies() + throws Exception { + mTransportClient.connectAsync(mTransportListener, "caller1"); + ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext); + + mTransportClient.connectAsync(mTransportListener2, "caller2"); + + connection.onBindingDied(TRANSPORT_COMPONENT); + + mShadowLooper.runToEndOfTasks(); + verify(mTransportListener).onTransportBound(isNull(), eq(mTransportClient)); + verify(mTransportListener2).onTransportBound(isNull(), eq(mTransportClient)); + }*/ + + private ServiceConnection verifyBindServiceAsUserAndCaptureServiceConnection(Context context) { + ArgumentCaptor connectionCaptor = + ArgumentCaptor.forClass(ServiceConnection.class); + verify(context) + .bindServiceAsUser( + any(Intent.class), + connectionCaptor.capture(), + anyInt(), + any(UserHandle.class)); + return connectionCaptor.getValue(); + } +}