Merge "Binding on-demand #1: TransportClient infra + PerformBackupTask usage"

This commit is contained in:
Bernardo Rufino
2017-11-22 14:23:04 +00:00
committed by Android (Google) Code Review
12 changed files with 1129 additions and 45 deletions

View File

@@ -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<String> fullBackupList = new ArrayList<>();
ArrayList<String> 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;
}

View File

@@ -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<ComponentName> 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<String, ComponentName> mBoundTransports = new ArrayMap<>();
/** Names of transports we've bound to at least once */
@GuardedBy("mTransportLock")
private final Map<String, ComponentName> 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<String, ComponentName> 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);
}
}

View File

@@ -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.<String>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;

View File

@@ -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);
}

View File

@@ -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<BackupRequest> mQueue;
ArrayList<BackupRequest> 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<BackupRequest> queue, @Nullable DataChangedJournal journal,
IBackupObserver observer, IBackupManagerMonitor monitor,
List<String> pendingFullBackups, boolean userInitiated, boolean nonIncremental) {
@Nullable OnTaskFinishedListener listener, List<String> 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

View File

@@ -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<String> kvPackages;
public ArrayList<String> fullPackages;
public IBackupObserver observer;
public IBackupManagerMonitor monitor;
public OnTaskFinishedListener listener;
public boolean userInitiated;
public boolean nonIncrementalBackup;
public BackupParams(IBackupTransport transport, String dirName, ArrayList<String> kvPackages,
ArrayList<String> fullPackages, IBackupObserver observer,
IBackupManagerMonitor monitor, boolean userInitiated, boolean nonIncrementalBackup) {
this.transport = transport;
public BackupParams(TransportClient transportClient, String dirName,
ArrayList<String> kvPackages, ArrayList<String> 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;
}

View File

@@ -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.
*
* <p>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)}.
*
* <p>DO NOT forget to unbind otherwise there will be dangling connections floating around.
*
* <p>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<TransportConnectionListener, String> 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).
*
* <p>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.
*
* <p>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.
*
* <p>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:
*
* <ul>
* <li>Some code called {@link #unbind(String)} before you got a callback.
* <li>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.
* <li>The framework returns false for {@link Context#bindServiceAsUser(Intent,
* ServiceConnection, int, UserHandle)} (or one of its variants). Check documentation for
* when this happens.
* </ul>
*
* 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.
*
* <p>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.
*
* <p>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).
*
* <p>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<IBackupTransport> 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}.
*
* <p>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<TransportConnectionListener, String> 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 "<UNKNOWN = " + state + ">";
}
}
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;
}
}

View File

@@ -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}.
*
* <p>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);
}
}

View File

@@ -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);
}

View File

@@ -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");
}
}

View File

@@ -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() {}
}

View File

@@ -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<ServiceConnection> connectionCaptor =
ArgumentCaptor.forClass(ServiceConnection.class);
verify(context)
.bindServiceAsUser(
any(Intent.class),
connectionCaptor.capture(),
anyInt(),
any(UserHandle.class));
return connectionCaptor.getValue();
}
}